"""Sports Center foundation application services — Phase 9.0."""
from __future__ import annotations

from datetime import date, datetime, timezone
from uuid import UUID, uuid4

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import InMemoryEventPublisher, get_event_publisher
from app.events.types import SportsCenterEventType
from app.models.foundation import (
    AttendanceGateway,
    Branch,
    Coach,
    Court,
    Device,
    DeviceProvider,
    Facility,
    Locker,
    LockerRoom,
    Membership,
    MembershipType,
    Room,
    Sport,
    SportsCenter,
    SportsConfiguration,
    SportsEvent,
    SportsPermission,
    SportsRole,
    SportsSetting,
)
from app.models.types import AuditAction, DeviceStatus, LockerStatus
from app.repositories.foundation import (
    AttendanceGatewayRepository,
    BranchRepository,
    CoachRepository,
    CourtRepository,
    DeviceProviderRepository,
    DeviceRepository,
    FacilityRepository,
    LockerRepository,
    LockerRoomRepository,
    MembershipRepository,
    MembershipTypeRepository,
    RoomRepository,
    SportRepository,
    SportsCenterRepository,
    SportsConfigurationRepository,
    SportsEventRepository,
    SportsPermissionRepository,
    SportsRoleRepository,
    SportsSettingRepository,
)
from app.schemas.foundation import (
    AttendanceGatewayCreate,
    AttendanceGatewayUpdate,
    BranchCreate,
    BranchUpdate,
    CoachCreate,
    CoachUpdate,
    CourtCreate,
    CourtUpdate,
    DeviceCreate,
    DeviceProviderCreate,
    DeviceProviderUpdate,
    DeviceUpdate,
    FacilityCreate,
    FacilityUpdate,
    LockerAssignRequest,
    LockerCreate,
    LockerRoomCreate,
    LockerRoomUpdate,
    LockerUpdate,
    MembershipCreate,
    MembershipTypeCreate,
    MembershipTypeUpdate,
    MembershipUpdate,
    RoomCreate,
    RoomUpdate,
    SportCreate,
    SportsCenterCreate,
    SportsCenterUpdate,
    SportsConfigurationCreate,
    SportsConfigurationUpdate,
    SportsEventCreate,
    SportsEventUpdate,
    SportsPermissionCreate,
    SportsPermissionUpdate,
    SportsRoleCreate,
    SportsRoleUpdate,
    SportsSettingUpsert,
    SportUpdate,
)
from app.services.audit_service import AuditService
from app.validators import (
    ensure_optimistic_version,
    forbid_sport_specific_hardcoding,
    validate_capability,
    validate_code,
    validate_currency_code,
    validate_date_range,
    validate_device_status,
    validate_email,
    validate_facility_kind,
    validate_lifecycle_status,
    validate_locker_status,
    validate_membership_status,
    validate_non_empty,
    validate_non_negative_int,
    validate_phone,
    validate_setting_key,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


def _apply_update(entity, data: dict) -> None:
    for key, value in data.items():
        setattr(entity, key, value)


def _next_membership_number() -> str:
    return f"SM-{uuid4().hex[:10].upper()}"


class _BaseService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.audit = AuditService(session)
        self.publisher = publisher or get_event_publisher()
        self.centers = SportsCenterRepository(session)

    async def _require_center(self, tenant_id: UUID, sports_center_id: UUID) -> SportsCenter:
        center = await self.centers.get(tenant_id, sports_center_id)
        if center is None:
            raise NotFoundError("مرکز ورزشی یافت نشد", error_code="sports_center_not_found")
        return center


class SportsCenterService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = SportsCenterRepository(session)

    async def create(self, tenant_id: UUID, body: SportsCenterCreate, *, actor: CurrentUser | None = None) -> SportsCenter:
        forbid_sport_specific_hardcoding(body.model_dump())
        actor_id = actor.user_id if actor else None
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, code) is not None:
            raise AppError("کد مرکز ورزشی تکراری است", status_code=409, error_code="sports_center_code_exists")
        entity = SportsCenter(
            tenant_id=tenant_id,
            code=code,
            name=validate_non_empty(body.name, field="name"),
            description=body.description,
            status=validate_lifecycle_status(body.status),
            legal_name=body.legal_name,
            timezone=validate_non_empty(body.timezone, field="timezone"),
            language=validate_non_empty(body.language, field="language"),
            currency_code=validate_currency_code(body.currency_code),
            settings=body.settings,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="sports_center",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
            message=f"Sports center {entity.code} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=SportsCenterEventType.SPORTS_CENTER_CREATED,
            aggregate_type="sports_center",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "status": entity.status.value},
        )
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: SportsCenterUpdate, *, actor: CurrentUser | None = None) -> SportsCenter:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity.version, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None:
            data["status"] = validate_lifecycle_status(data["status"])
        if "currency_code" in data and data["currency_code"] is not None:
            data["currency_code"] = validate_currency_code(data["currency_code"])
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="sports_center",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=entity.updated_by,
            changes=data,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=SportsCenterEventType.SPORTS_CENTER_UPDATED,
            aggregate_type="sports_center",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "status": entity.status.value},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> SportsCenter:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("مرکز ورزشی یافت نشد", error_code="sports_center_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> SportsCenter:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="sports_center",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=actor.user_id if actor else None,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class BranchService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = BranchRepository(session)

    async def create(self, tenant_id: UUID, body: BranchCreate, *, actor: CurrentUser | None = None) -> Branch:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.sports_center_id, code):
            raise AppError("کد شعبه تکراری است", status_code=409, error_code="branch_code_exists")
        entity = Branch(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            code=code,
            name=validate_non_empty(body.name, field="name"),
            description=body.description,
            status=validate_lifecycle_status(body.status),
            address=body.address,
            timezone=body.timezone,
            working_hours=body.working_hours,
            metadata_json=body.metadata_json,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id, entity_type="branch", entity_id=entity.id,
            action=AuditAction.CREATE, actor_user_id=actor_id, message=f"Branch {entity.code} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=SportsCenterEventType.BRANCH_CREATED,
            aggregate_type="branch", aggregate_id=entity.id, tenant_id=tenant_id,
            payload={"code": entity.code, "sports_center_id": str(entity.sports_center_id)},
        )
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: BranchUpdate, *, actor: CurrentUser | None = None) -> Branch:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None:
            data["status"] = validate_lifecycle_status(data["status"])
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id, entity_type="branch", entity_id=entity.id,
            action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=SportsCenterEventType.BRANCH_UPDATED,
            aggregate_type="branch", aggregate_id=entity.id, tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Branch:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("شعبه یافت نشد", error_code="branch_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Branch:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(
            tenant_id=tenant_id, entity_type="branch", entity_id=entity.id,
            action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class SportService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = SportRepository(session)

    async def create(self, tenant_id: UUID, body: SportCreate, *, actor: CurrentUser | None = None) -> Sport:
        forbid_sport_specific_hardcoding(body.model_dump())
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.sports_center_id, code):
            raise AppError("کد ورزش تکراری است", status_code=409, error_code="sport_code_exists")
        entity = Sport(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            code=code,
            name=validate_non_empty(body.name, field="name"),
            description=body.description,
            status=validate_lifecycle_status(body.status),
            category=body.category,
            attributes=body.attributes,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id, entity_type="sport", entity_id=entity.id,
            action=AuditAction.CREATE, actor_user_id=actor_id, message=f"Sport {entity.code} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=SportsCenterEventType.SPORT_CREATED,
            aggregate_type="sport", aggregate_id=entity.id, tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: SportUpdate, *, actor: CurrentUser | None = None) -> Sport:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None:
            data["status"] = validate_lifecycle_status(data["status"])
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id, entity_type="sport", entity_id=entity.id,
            action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Sport:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("ورزش یافت نشد", error_code="sport_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Sport:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(
            tenant_id=tenant_id, entity_type="sport", entity_id=entity.id,
            action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity
class MembershipTypeService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = MembershipTypeRepository(session)

    async def _validate_catalog_refs(self, tenant_id: UUID, data: dict) -> None:
        from app.repositories.catalog import (
            AgeGroupRepository,
            ExpirationPolicyRepository,
            FreezingRuleRepository,
            MembershipPackageRepository,
            MembershipPlanRepository,
            PricingModelRepository,
            RenewalPolicyRepository,
            SportCategoryRepository,
        )

        checks = (
            ("package_id", MembershipPackageRepository(self.session), "membership_package_not_found", "بسته عضویت یافت نشد"),
            ("plan_id", MembershipPlanRepository(self.session), "membership_plan_not_found", "طرح عضویت یافت نشد"),
            ("pricing_model_id", PricingModelRepository(self.session), "pricing_model_not_found", "مدل قیمت‌گذاری یافت نشد"),
            ("age_group_id", AgeGroupRepository(self.session), "age_group_not_found", "گروه سنی یافت نشد"),
            ("sport_category_id", SportCategoryRepository(self.session), "sport_category_not_found", "دسته ورزشی یافت نشد"),
            ("renewal_policy_id", RenewalPolicyRepository(self.session), "renewal_policy_not_found", "سیاست تمدید یافت نشد"),
            ("freezing_rule_id", FreezingRuleRepository(self.session), "freezing_rule_not_found", "قانون فریز یافت نشد"),
            ("expiration_policy_id", ExpirationPolicyRepository(self.session), "expiration_policy_not_found", "سیاست انقضا یافت نشد"),
        )
        for key, repo, error_code, message in checks:
            ref = data.get(key)
            if ref is None:
                continue
            entity = await repo.get(tenant_id, ref)
            if entity is None:
                raise NotFoundError(message, error_code=error_code)

    async def create(self, tenant_id: UUID, body: MembershipTypeCreate, *, actor: CurrentUser | None = None) -> MembershipType:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.sports_center_id, code):
            raise AppError("کد نوع عضویت تکراری است", status_code=409, error_code="membership_type_code_exists")
        payload = body.model_dump()
        await self._validate_catalog_refs(tenant_id, payload)
        entity = MembershipType(
            tenant_id=tenant_id, sports_center_id=body.sports_center_id, code=code,
            name=validate_non_empty(body.name, field="name"), description=body.description,
            status=validate_lifecycle_status(body.status),
            duration_days=validate_non_negative_int(body.duration_days, field="duration_days"),
            policies=body.policies, sport_ids=body.sport_ids,
            package_id=body.package_id, plan_id=body.plan_id, pricing_model_id=body.pricing_model_id,
            age_group_id=body.age_group_id, sport_category_id=body.sport_category_id,
            renewal_policy_id=body.renewal_policy_id, freezing_rule_id=body.freezing_rule_id,
            expiration_policy_id=body.expiration_policy_id,
            is_transferable=body.is_transferable, sort_order=body.sort_order,
            created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="membership_type", entity_id=entity.id,
                                action=AuditAction.CREATE, actor_user_id=actor_id, message=f"Membership type {entity.code} created")
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.MEMBERSHIP_TYPE_CREATED, aggregate_type="membership_type",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: MembershipTypeUpdate, *, actor: CurrentUser | None = None) -> MembershipType:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None: data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None: data["status"] = validate_lifecycle_status(data["status"])
        if "duration_days" in data: data["duration_days"] = validate_non_negative_int(data["duration_days"], field="duration_days")
        await self._validate_catalog_refs(tenant_id, data)
        _apply_update(entity, data); entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="membership_type", entity_id=entity.id,
                                action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.MEMBERSHIP_TYPE_UPDATED, aggregate_type="membership_type",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> MembershipType:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("نوع عضویت یافت نشد", error_code="membership_type_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> MembershipType:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="membership_type", entity_id=entity.id,
                                action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity


class MembershipService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = MembershipRepository(session)
        self.types = MembershipTypeRepository(session)
        from app.repositories.members import MemberRepository

        self.members = MemberRepository(session)

    async def _resolve_member(self, tenant_id: UUID, member_id: UUID | None, sports_center_id: UUID):
        if member_id is None:
            return None
        member = await self.members.get(tenant_id, member_id)
        if member is None:
            raise NotFoundError("عضو یافت نشد", error_code="member_not_found")
        if member.sports_center_id != sports_center_id:
            raise AppError(
                "عضو متعلق به این مرکز نیست",
                status_code=422,
                error_code="member_center_mismatch",
            )
        return member

    async def create(self, tenant_id: UUID, body: MembershipCreate, *, actor: CurrentUser | None = None) -> Membership:
        from app.validators.members import assert_membership_transition  # noqa: F401

        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        mtype = await self.types.get(tenant_id, body.membership_type_id)
        if mtype is None or mtype.sports_center_id != body.sports_center_id:
            raise NotFoundError("نوع عضویت یافت نشد", error_code="membership_type_not_found")
        member = await self._resolve_member(tenant_id, body.member_id, body.sports_center_id)
        starts_on, ends_on = validate_date_range(body.starts_on, body.ends_on)
        membership_number = validate_code(body.membership_number, field="membership_number") if body.membership_number else _next_membership_number()
        if await self.repo.get_by_membership_number(tenant_id, membership_number):
            raise AppError("شماره عضویت تکراری است", status_code=409, error_code="membership_number_exists")
        display_name = validate_non_empty(body.display_name, field="display_name")
        email = validate_email(body.email)
        mobile = validate_phone(body.mobile, field="mobile")
        if member is not None:
            display_name = display_name or member.display_name
            email = email if body.email is not None else member.email
            mobile = mobile if body.mobile is not None else member.mobile
        entity = Membership(
            tenant_id=tenant_id, sports_center_id=body.sports_center_id, branch_id=body.branch_id,
            membership_type_id=body.membership_type_id, member_id=body.member_id,
            membership_number=membership_number,
            display_name=display_name,
            email=email, mobile=mobile,
            external_customer_ref=body.external_customer_ref, external_crm_contact_ref=body.external_crm_contact_ref,
            status=validate_membership_status(body.status), starts_on=starts_on, ends_on=ends_on,
            profile=body.profile, custom_fields=body.custom_fields, created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="membership", entity_id=entity.id,
                                action=AuditAction.CREATE, actor_user_id=actor_id, message=f"Membership {entity.membership_number} created")
        await self.session.commit(); await self.session.refresh(entity)
        # Legacy shell without Member: keep MEMBER_CREATED for backward compatibility.
        if member is None:
            self.publisher.publish(event_type=SportsCenterEventType.MEMBER_CREATED, aggregate_type="member",
                                   aggregate_id=entity.id, tenant_id=tenant_id,
                                   payload={"membership_number": entity.membership_number, "display_name": entity.display_name})
        self.publisher.publish(event_type=SportsCenterEventType.MEMBERSHIP_CREATED, aggregate_type="membership",
                               aggregate_id=entity.id, tenant_id=tenant_id,
                               payload={"membership_number": entity.membership_number, "status": entity.status.value,
                                        "member_id": str(entity.member_id) if entity.member_id else None})
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: MembershipUpdate, *, actor: CurrentUser | None = None) -> Membership:
        from app.validators.members import assert_membership_transition, validate_freeze_window

        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity.version, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "member_id" in data:
            await self._resolve_member(tenant_id, data["member_id"], entity.sports_center_id)
        if "display_name" in data and data["display_name"] is not None: data["display_name"] = validate_non_empty(data["display_name"], field="display_name")
        if "email" in data: data["email"] = validate_email(data["email"])
        if "mobile" in data: data["mobile"] = validate_phone(data["mobile"], field="mobile")
        if "status" in data and data["status"] is not None:
            new_status = validate_membership_status(data["status"])
            assert_membership_transition(entity.status, new_status)
            data["status"] = new_status
        if "starts_on" in data or "ends_on" in data:
            validate_date_range(data.get("starts_on", entity.starts_on), data.get("ends_on", entity.ends_on))
        if "freeze_starts_on" in data or "freeze_ends_on" in data:
            validate_freeze_window(
                data.get("freeze_starts_on", entity.freeze_starts_on),
                data.get("freeze_ends_on", entity.freeze_ends_on),
            )
        _apply_update(entity, data); entity.version += 1; entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="membership", entity_id=entity.id,
                                action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.MEMBERSHIP_UPDATED, aggregate_type="membership",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"status": entity.status.value})
        return entity

    async def assign_member(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        member_id: UUID,
        *,
        version: int | None = None,
        actor: CurrentUser | None = None,
    ) -> Membership:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity.version, version)
        member = await self._resolve_member(tenant_id, member_id, entity.sports_center_id)
        assert member is not None
        entity.member_id = member.id
        entity.display_name = member.display_name
        entity.email = member.email
        entity.mobile = member.mobile
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="membership",
            entity_id=entity.id,
            action=AuditAction.ASSIGN,
            actor_user_id=entity.updated_by,
            message=f"assigned member {member.member_number}",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=SportsCenterEventType.MEMBERSHIP_ASSIGNED,
            aggregate_type="membership",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"member_id": str(member.id)},
        )
        return entity

    async def change_status(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        target,
        *,
        freeze_starts_on=None,
        freeze_ends_on=None,
        version: int | None = None,
        actor: CurrentUser | None = None,
    ) -> Membership:
        from app.models.types import MembershipStatus
        from app.validators.members import assert_membership_transition, validate_freeze_window

        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity.version, version)
        previous = entity.status
        target_status = validate_membership_status(target)
        assert_membership_transition(previous, target_status)
        if target_status == MembershipStatus.FROZEN:
            starts, ends = validate_freeze_window(freeze_starts_on or date.today(), freeze_ends_on)
            entity.freeze_starts_on = starts
            entity.freeze_ends_on = ends
        if target_status == MembershipStatus.ACTIVE and previous == MembershipStatus.FROZEN:
            entity.freeze_starts_on = None
            entity.freeze_ends_on = None
        entity.status = target_status
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="membership",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=entity.updated_by,
            message=f"status -> {target_status.value}",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        event = SportsCenterEventType.MEMBERSHIP_STATUS_CHANGED
        if target_status == MembershipStatus.FROZEN:
            event = SportsCenterEventType.MEMBERSHIP_FROZEN
        elif previous == MembershipStatus.FROZEN and target_status == MembershipStatus.ACTIVE:
            event = SportsCenterEventType.MEMBERSHIP_UNFROZEN
        self.publisher.publish(
            event_type=event,
            aggregate_type="membership",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"status": entity.status.value, "previous_status": previous.value},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Membership:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("عضویت یافت نشد", error_code="membership_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Membership:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="membership", entity_id=entity.id,
                                action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity


class CoachService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = CoachRepository(session)

    async def create(self, tenant_id: UUID, body: CoachCreate, *, actor: CurrentUser | None = None) -> Coach:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.sports_center_id, code):
            raise AppError("کد مربی تکراری است", status_code=409, error_code="coach_code_exists")
        entity = Coach(
            tenant_id=tenant_id, sports_center_id=body.sports_center_id, branch_id=body.branch_id, code=code,
            display_name=validate_non_empty(body.display_name, field="display_name"),
            email=validate_email(body.email), mobile=validate_phone(body.mobile, field="mobile"),
            status=validate_lifecycle_status(body.status), sport_ids=body.sport_ids,
            qualifications=body.qualifications, external_user_ref=body.external_user_ref,
            staff_kind=body.staff_kind, bio=body.bio, hire_date=body.hire_date,
            payroll_external_ref=body.payroll_external_ref, sports_role_id=body.sports_role_id,
            created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="coach", entity_id=entity.id,
                                action=AuditAction.CREATE, actor_user_id=actor_id, message=f"Coach {entity.code} created")
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.COACH_CREATED, aggregate_type="coach",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: CoachUpdate, *, actor: CurrentUser | None = None) -> Coach:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "display_name" in data and data["display_name"] is not None: data["display_name"] = validate_non_empty(data["display_name"], field="display_name")
        if "email" in data: data["email"] = validate_email(data["email"])
        if "mobile" in data: data["mobile"] = validate_phone(data["mobile"], field="mobile")
        if "status" in data and data["status"] is not None: data["status"] = validate_lifecycle_status(data["status"])
        _apply_update(entity, data); entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="coach", entity_id=entity.id,
                                action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.COACH_UPDATED, aggregate_type="coach",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Coach:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("مربی یافت نشد", error_code="coach_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Coach:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="coach", entity_id=entity.id,
                                action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

class SportsRoleService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = SportsRoleRepository(session)

    async def create(self, tenant_id: UUID, body: SportsRoleCreate, *, actor: CurrentUser | None = None) -> SportsRole:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.sports_center_id, code):
            raise AppError("کد نقش تکراری است", status_code=409, error_code="sports_role_code_exists")
        entity = SportsRole(tenant_id=tenant_id, sports_center_id=body.sports_center_id, code=code,
                            name=validate_non_empty(body.name, field="name"), description=body.description,
                            status=validate_lifecycle_status(body.status), permission_codes=body.permission_codes,
                            created_by=actor_id, updated_by=actor_id)
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_role", entity_id=entity.id,
                                action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: SportsRoleUpdate, *, actor: CurrentUser | None = None) -> SportsRole:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None: data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None: data["status"] = validate_lifecycle_status(data["status"])
        _apply_update(entity, data); entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_role", entity_id=entity.id,
                                action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> SportsRole:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("نقش یافت نشد", error_code="sports_role_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> SportsRole:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_role", entity_id=entity.id,
                                action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity


class SportsPermissionService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = SportsPermissionRepository(session)

    async def create(self, tenant_id: UUID, body: SportsPermissionCreate, *, actor: CurrentUser | None = None) -> SportsPermission:
        actor_id = actor.user_id if actor else None
        code = validate_non_empty(body.code, field="code").lower()
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError("کد مجوز تکراری است", status_code=409, error_code="sports_permission_code_exists")
        entity = SportsPermission(tenant_id=tenant_id, sports_center_id=body.sports_center_id, code=code,
                                  name=validate_non_empty(body.name, field="name"), description=body.description,
                                  status=validate_lifecycle_status(body.status), created_by=actor_id, updated_by=actor_id)
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_permission", entity_id=entity.id,
                                action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: SportsPermissionUpdate, *, actor: CurrentUser | None = None) -> SportsPermission:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None: data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None: data["status"] = validate_lifecycle_status(data["status"])
        _apply_update(entity, data); entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_permission", entity_id=entity.id,
                                action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> SportsPermission:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("مجوز یافت نشد", error_code="sports_permission_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> SportsPermission:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_permission", entity_id=entity.id,
                                action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity


class FacilityService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = FacilityRepository(session)

    async def create(self, tenant_id: UUID, body: FacilityCreate, *, actor: CurrentUser | None = None) -> Facility:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.sports_center_id, code):
            raise AppError("کد تسهیلات تکراری است", status_code=409, error_code="facility_code_exists")
        entity = Facility(tenant_id=tenant_id, sports_center_id=body.sports_center_id, branch_id=body.branch_id, code=code,
                          name=validate_non_empty(body.name, field="name"), description=body.description,
                          kind=validate_facility_kind(body.kind), status=validate_lifecycle_status(body.status),
                          capacity=validate_non_negative_int(body.capacity, field="capacity"),
                          attributes=body.attributes, created_by=actor_id, updated_by=actor_id)
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="facility", entity_id=entity.id,
                                action=AuditAction.CREATE, actor_user_id=actor_id, message=f"Facility {entity.code} created")
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.FACILITY_CREATED, aggregate_type="facility",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code, "kind": entity.kind.value})
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: FacilityUpdate, *, actor: CurrentUser | None = None) -> Facility:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None: data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None: data["status"] = validate_lifecycle_status(data["status"])
        if "kind" in data and data["kind"] is not None: data["kind"] = validate_facility_kind(data["kind"])
        if "capacity" in data: data["capacity"] = validate_non_negative_int(data["capacity"], field="capacity")
        _apply_update(entity, data); entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="facility", entity_id=entity.id,
                                action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.FACILITY_UPDATED, aggregate_type="facility",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Facility:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("تسهیلات یافت نشد", error_code="facility_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Facility:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="facility", entity_id=entity.id,
                                action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity


def _make_simple_coded_service(name, model_cls, repo_cls, create_cls, update_cls, entity_type, not_found_code, not_found_msg, code_exists_code, code_exists_msg, event_created=None):
    """Factory kept unused — services defined explicitly below for clarity."""
    return None


class CourtService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = CourtRepository(session)

    async def create(self, tenant_id: UUID, body: CourtCreate, *, actor: CurrentUser | None = None) -> Court:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.sports_center_id, code):
            raise AppError("کد زمین تکراری است", status_code=409, error_code="court_code_exists")
        entity = Court(tenant_id=tenant_id, sports_center_id=body.sports_center_id, branch_id=body.branch_id,
                       facility_id=body.facility_id, code=code, name=validate_non_empty(body.name, field="name"),
                       status=validate_lifecycle_status(body.status), surface=body.surface, attributes=body.attributes,
                       created_by=actor_id, updated_by=actor_id)
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="court", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: CourtUpdate, *, actor: CurrentUser | None = None) -> Court:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None: data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None: data["status"] = validate_lifecycle_status(data["status"])
        _apply_update(entity, data); entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="court", entity_id=entity.id, action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Court:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("زمین یافت نشد", error_code="court_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Court:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="court", entity_id=entity.id, action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity


class RoomService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = RoomRepository(session)

    async def create(self, tenant_id: UUID, body: RoomCreate, *, actor: CurrentUser | None = None) -> Room:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.sports_center_id, code):
            raise AppError("کد اتاق تکراری است", status_code=409, error_code="room_code_exists")
        entity = Room(tenant_id=tenant_id, sports_center_id=body.sports_center_id, branch_id=body.branch_id,
                      facility_id=body.facility_id, code=code, name=validate_non_empty(body.name, field="name"),
                      status=validate_lifecycle_status(body.status),
                      capacity=validate_non_negative_int(body.capacity, field="capacity"), attributes=body.attributes,
                      created_by=actor_id, updated_by=actor_id)
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="room", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: RoomUpdate, *, actor: CurrentUser | None = None) -> Room:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None: data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None: data["status"] = validate_lifecycle_status(data["status"])
        if "capacity" in data: data["capacity"] = validate_non_negative_int(data["capacity"], field="capacity")
        _apply_update(entity, data); entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="room", entity_id=entity.id, action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Room:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("اتاق یافت نشد", error_code="room_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Room:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="room", entity_id=entity.id, action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity


class LockerRoomService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = LockerRoomRepository(session)

    async def create(self, tenant_id: UUID, body: LockerRoomCreate, *, actor: CurrentUser | None = None) -> LockerRoom:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.sports_center_id, code):
            raise AppError("کد رختکن تکراری است", status_code=409, error_code="locker_room_code_exists")
        entity = LockerRoom(tenant_id=tenant_id, sports_center_id=body.sports_center_id, branch_id=body.branch_id, code=code,
                            name=validate_non_empty(body.name, field="name"), status=validate_lifecycle_status(body.status),
                            attributes=body.attributes, created_by=actor_id, updated_by=actor_id)
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="locker_room", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: LockerRoomUpdate, *, actor: CurrentUser | None = None) -> LockerRoom:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None: data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None: data["status"] = validate_lifecycle_status(data["status"])
        _apply_update(entity, data); entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="locker_room", entity_id=entity.id, action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> LockerRoom:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("رختکن یافت نشد", error_code="locker_room_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> LockerRoom:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="locker_room", entity_id=entity.id, action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

class LockerService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = LockerRepository(session)
        self.locker_rooms = LockerRoomRepository(session)
        self.memberships = MembershipRepository(session)

    async def create(self, tenant_id: UUID, body: LockerCreate, *, actor: CurrentUser | None = None) -> Locker:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        lr = await self.locker_rooms.get(tenant_id, body.locker_room_id)
        if lr is None or lr.sports_center_id != body.sports_center_id:
            raise NotFoundError("رختکن یافت نشد", error_code="locker_room_not_found")
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.locker_room_id, code):
            raise AppError("کد کمد تکراری است", status_code=409, error_code="locker_code_exists")
        entity = Locker(tenant_id=tenant_id, sports_center_id=body.sports_center_id, locker_room_id=body.locker_room_id,
                        code=code, label=body.label, status=validate_locker_status(body.status), attributes=body.attributes,
                        created_by=actor_id, updated_by=actor_id)
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="locker", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: LockerUpdate, *, actor: CurrentUser | None = None) -> Locker:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity.version, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "status" in data and data["status"] is not None: data["status"] = validate_locker_status(data["status"])
        _apply_update(entity, data); entity.version += 1; entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="locker", entity_id=entity.id, action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def assign(self, tenant_id: UUID, entity_id: UUID, body: LockerAssignRequest, *, actor: CurrentUser | None = None) -> Locker:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity.version, body.version)
        membership = await self.memberships.get(tenant_id, body.membership_id)
        if membership is None:
            raise NotFoundError("عضویت یافت نشد", error_code="membership_not_found")
        if entity.status == LockerStatus.ASSIGNED and entity.assigned_membership_id:
            raise AppError("کمد قبلاً تخصیص داده شده است", status_code=409, error_code="locker_already_assigned")
        entity.status = LockerStatus.ASSIGNED
        entity.assigned_membership_id = body.membership_id
        entity.assigned_at = datetime.now(timezone.utc)
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="locker", entity_id=entity.id, action=AuditAction.ASSIGN,
                                actor_user_id=entity.updated_by, message=f"Locker {entity.code} assigned")
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.LOCKER_ASSIGNED, aggregate_type="locker",
                               aggregate_id=entity.id, tenant_id=tenant_id,
                               payload={"code": entity.code, "membership_id": str(body.membership_id)})
        return entity

    async def release(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None, version: int | None = None) -> Locker:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity.version, version)
        prev = entity.assigned_membership_id
        entity.status = LockerStatus.AVAILABLE
        entity.assigned_membership_id = None
        entity.assigned_at = None
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="locker", entity_id=entity.id, action=AuditAction.RELEASE,
                                actor_user_id=entity.updated_by, message=f"Locker {entity.code} released")
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.LOCKER_RELEASED, aggregate_type="locker",
                               aggregate_id=entity.id, tenant_id=tenant_id,
                               payload={"code": entity.code, "previous_membership_id": str(prev) if prev else None})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Locker:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("کمد یافت نشد", error_code="locker_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Locker:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="locker", entity_id=entity.id, action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity


class DeviceProviderService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = DeviceProviderRepository(session)

    async def create(self, tenant_id: UUID, body: DeviceProviderCreate, *, actor: CurrentUser | None = None) -> DeviceProvider:
        actor_id = actor.user_id if actor else None
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError("کد ارائه‌دهنده دستگاه تکراری است", status_code=409, error_code="device_provider_code_exists")
        entity = DeviceProvider(tenant_id=tenant_id, code=code, name=validate_non_empty(body.name, field="name"),
                                capability=validate_capability(body.capability),
                                adapter_key=validate_non_empty(body.adapter_key, field="adapter_key"),
                                status=validate_lifecycle_status(body.status), config_schema=body.config_schema,
                                settings=body.settings, created_by=actor_id, updated_by=actor_id)
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="device_provider", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.DEVICE_PROVIDER_CREATED, aggregate_type="device_provider",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code, "adapter_key": entity.adapter_key})
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: DeviceProviderUpdate, *, actor: CurrentUser | None = None) -> DeviceProvider:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None: data["name"] = validate_non_empty(data["name"], field="name")
        if "capability" in data and data["capability"] is not None: data["capability"] = validate_capability(data["capability"])
        if "adapter_key" in data and data["adapter_key"] is not None: data["adapter_key"] = validate_non_empty(data["adapter_key"], field="adapter_key")
        if "status" in data and data["status"] is not None: data["status"] = validate_lifecycle_status(data["status"])
        _apply_update(entity, data); entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="device_provider", entity_id=entity.id, action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> DeviceProvider:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("ارائه‌دهنده دستگاه یافت نشد", error_code="device_provider_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> DeviceProvider:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="device_provider", entity_id=entity.id, action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity


class DeviceService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = DeviceRepository(session)

    async def create(self, tenant_id: UUID, body: DeviceCreate, *, actor: CurrentUser | None = None) -> Device:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.sports_center_id, code):
            raise AppError("کد دستگاه تکراری است", status_code=409, error_code="device_code_exists")
        entity = Device(tenant_id=tenant_id, sports_center_id=body.sports_center_id, branch_id=body.branch_id,
                        device_provider_id=body.device_provider_id, code=code,
                        name=validate_non_empty(body.name, field="name"),
                        capability=validate_capability(body.capability),
                        status=validate_device_status(body.status), location_ref=body.location_ref,
                        external_device_ref=body.external_device_ref, settings=body.settings,
                        created_by=actor_id, updated_by=actor_id)
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="device", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.DEVICE_CREATED, aggregate_type="device",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: DeviceUpdate, *, actor: CurrentUser | None = None) -> Device:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity.version, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "name" in data and data["name"] is not None: data["name"] = validate_non_empty(data["name"], field="name")
        if "capability" in data and data["capability"] is not None: data["capability"] = validate_capability(data["capability"])
        if "status" in data and data["status"] is not None: data["status"] = validate_device_status(data["status"])
        _apply_update(entity, data); entity.version += 1; entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="device", entity_id=entity.id, action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.DEVICE_UPDATED, aggregate_type="device",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def connect(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Device:
        entity = await self.get(tenant_id, entity_id)
        entity.status = DeviceStatus.CONNECTED
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="device", entity_id=entity.id, action=AuditAction.CONNECT, actor_user_id=entity.updated_by)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.DEVICE_CONNECTED, aggregate_type="device",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code, "status": entity.status.value})
        return entity

    async def disconnect(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Device:
        entity = await self.get(tenant_id, entity_id)
        entity.status = DeviceStatus.DISCONNECTED
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="device", entity_id=entity.id, action=AuditAction.DISCONNECT, actor_user_id=entity.updated_by)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.DEVICE_DISCONNECTED, aggregate_type="device",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code, "status": entity.status.value})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Device:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("دستگاه یافت نشد", error_code="device_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Device:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="device", entity_id=entity.id, action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity


class AttendanceGatewayService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = AttendanceGatewayRepository(session)

    async def create(self, tenant_id: UUID, body: AttendanceGatewayCreate, *, actor: CurrentUser | None = None) -> AttendanceGateway:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.sports_center_id, code):
            raise AppError("کد درگاه حضور تکراری است", status_code=409, error_code="attendance_gateway_code_exists")
        entity = AttendanceGateway(tenant_id=tenant_id, sports_center_id=body.sports_center_id, branch_id=body.branch_id, code=code,
                                   name=validate_non_empty(body.name, field="name"), status=validate_lifecycle_status(body.status),
                                   primary_provider_id=body.primary_provider_id, fallback_provider_id=body.fallback_provider_id,
                                   device_ids=body.device_ids, policies=body.policies, created_by=actor_id, updated_by=actor_id)
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="attendance_gateway", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.ATTENDANCE_GATEWAY_CREATED, aggregate_type="attendance_gateway",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: AttendanceGatewayUpdate, *, actor: CurrentUser | None = None) -> AttendanceGateway:
        entity = await self.get(tenant_id, entity_id)
        previous_provider = entity.primary_provider_id
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None: data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None: data["status"] = validate_lifecycle_status(data["status"])
        _apply_update(entity, data); entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="attendance_gateway", entity_id=entity.id, action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.ATTENDANCE_GATEWAY_UPDATED, aggregate_type="attendance_gateway",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        if "primary_provider_id" in data and data["primary_provider_id"] != previous_provider:
            self.publisher.publish(event_type=SportsCenterEventType.ATTENDANCE_PROVIDER_CHANGED, aggregate_type="attendance_gateway",
                                   aggregate_id=entity.id, tenant_id=tenant_id,
                                   payload={"previous_provider_id": str(previous_provider) if previous_provider else None,
                                            "primary_provider_id": str(entity.primary_provider_id) if entity.primary_provider_id else None})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> AttendanceGateway:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("درگاه حضور یافت نشد", error_code="attendance_gateway_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> AttendanceGateway:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="attendance_gateway", entity_id=entity.id, action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity


class SportsConfigurationService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = SportsConfigurationRepository(session)

    async def create(self, tenant_id: UUID, body: SportsConfigurationCreate, *, actor: CurrentUser | None = None) -> SportsConfiguration:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.sports_center_id, code):
            raise AppError("کد پیکربندی تکراری است", status_code=409, error_code="configuration_code_exists")
        entity = SportsConfiguration(
            tenant_id=tenant_id, sports_center_id=body.sports_center_id, branch_id=body.branch_id, code=code,
            working_hours=body.working_hours, membership_policies=body.membership_policies,
            attendance_policies=body.attendance_policies, booking_policies=body.booking_policies,
            waiting_list=body.waiting_list, custom_fields=body.custom_fields,
            timezone=validate_non_empty(body.timezone, field="timezone"),
            language=validate_non_empty(body.language, field="language"),
            currency_code=validate_currency_code(body.currency_code), created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_configuration", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.CONFIGURATION_CREATED, aggregate_type="sports_configuration",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: SportsConfigurationUpdate, *, actor: CurrentUser | None = None) -> SportsConfiguration:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity.version, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "timezone" in data and data["timezone"] is not None: data["timezone"] = validate_non_empty(data["timezone"], field="timezone")
        if "language" in data and data["language"] is not None: data["language"] = validate_non_empty(data["language"], field="language")
        if "currency_code" in data and data["currency_code"] is not None: data["currency_code"] = validate_currency_code(data["currency_code"])
        _apply_update(entity, data); entity.version += 1; entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_configuration", entity_id=entity.id, action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.CONFIGURATION_UPDATED, aggregate_type="sports_configuration",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> SportsConfiguration:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("پیکربندی یافت نشد", error_code="configuration_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> SportsConfiguration:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_configuration", entity_id=entity.id, action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity


class SportsEventService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = SportsEventRepository(session)

    async def create(self, tenant_id: UUID, body: SportsEventCreate, *, actor: CurrentUser | None = None) -> SportsEvent:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.sports_center_id, code):
            raise AppError("کد رویداد تکراری است", status_code=409, error_code="sports_event_code_exists")
        entity = SportsEvent(tenant_id=tenant_id, sports_center_id=body.sports_center_id, branch_id=body.branch_id,
                             sport_id=body.sport_id, facility_id=body.facility_id, coach_id=body.coach_id, code=code,
                             title=validate_non_empty(body.title, field="title"), description=body.description,
                             status=validate_lifecycle_status(body.status), starts_at=body.starts_at, ends_at=body.ends_at,
                             capacity=validate_non_negative_int(body.capacity, field="capacity"), attributes=body.attributes,
                             created_by=actor_id, updated_by=actor_id)
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_event", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.SPORTS_EVENT_CREATED, aggregate_type="sports_event",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: SportsEventUpdate, *, actor: CurrentUser | None = None) -> SportsEvent:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "title" in data and data["title"] is not None: data["title"] = validate_non_empty(data["title"], field="title")
        if "status" in data and data["status"] is not None: data["status"] = validate_lifecycle_status(data["status"])
        if "capacity" in data: data["capacity"] = validate_non_negative_int(data["capacity"], field="capacity")
        _apply_update(entity, data); entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_event", entity_id=entity.id, action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit(); await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> SportsEvent:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("رویداد ورزشی یافت نشد", error_code="sports_event_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> SportsEvent:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_event", entity_id=entity.id, action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity


class SportsSettingService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = SportsSettingRepository(session)

    async def upsert(self, tenant_id: UUID, body: SportsSettingUpsert, *, actor: CurrentUser | None = None) -> SportsSetting:
        actor_id = actor.user_id if actor else None
        key = validate_setting_key(body.key)
        existing = await self.repo.get_by_key(tenant_id, key, sports_center_id=body.sports_center_id, branch_id=body.branch_id)
        if existing is None:
            entity = SportsSetting(tenant_id=tenant_id, sports_center_id=body.sports_center_id, branch_id=body.branch_id,
                                   key=key, value=body.value, description=body.description, created_by=actor_id, updated_by=actor_id)
            await self.repo.add(entity)
            action = AuditAction.CREATE
        else:
            entity = existing
            entity.value = body.value
            entity.description = body.description
            entity.updated_by = actor_id
            action = AuditAction.UPDATE
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_setting", entity_id=entity.id, action=action, actor_user_id=actor_id)
        await self.session.commit(); await self.session.refresh(entity)
        self.publisher.publish(event_type=SportsCenterEventType.SETTING_UPSERTED, aggregate_type="sports_setting",
                               aggregate_id=entity.id, tenant_id=tenant_id, payload={"key": entity.key})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> SportsSetting:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None: raise NotFoundError("تنظیمات یافت نشد", error_code="setting_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> SportsSetting:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="sports_setting", entity_id=entity.id, action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit(); await self.session.refresh(entity)
        return entity
