"""Membership Catalog application services — Phase 9.1."""
from __future__ import annotations

from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import InMemoryEventPublisher, get_event_publisher
from app.events.types import SportsCenterEventType
from app.models.catalog import (
    AgeGroup,
    ExpirationPolicy,
    FreezingRule,
    MembershipPackage,
    MembershipPlan,
    MembershipRule,
    PricingModel,
    RenewalPolicy,
    SportCategory,
)
from app.models.types import AuditAction
from app.repositories.catalog import (
    AgeGroupRepository,
    ExpirationPolicyRepository,
    FreezingRuleRepository,
    MembershipPackageRepository,
    MembershipPlanRepository,
    MembershipRuleRepository,
    PricingModelRepository,
    RenewalPolicyRepository,
    SportCategoryRepository,
)
from app.repositories.foundation import (
    MembershipTypeRepository,
    SportsCenterRepository,
)
from app.schemas.catalog import (
    AgeGroupCreate,
    AgeGroupUpdate,
    ExpirationPolicyCreate,
    ExpirationPolicyUpdate,
    FreezingRuleCreate,
    FreezingRuleUpdate,
    MembershipPackageCreate,
    MembershipPackageUpdate,
    MembershipPlanCreate,
    MembershipPlanUpdate,
    MembershipRuleCreate,
    MembershipRuleUpdate,
    PricingModelCreate,
    PricingModelUpdate,
    RenewalPolicyCreate,
    RenewalPolicyUpdate,
    SportCategoryCreate,
    SportCategoryUpdate,
)
from app.services.audit_service import AuditService
from app.validators import (
    forbid_sport_specific_hardcoding,
    validate_code,
    validate_currency_code,
    validate_date_range,
    validate_lifecycle_status,
    validate_non_empty,
    validate_non_negative_int,
)
from app.validators.catalog import (
    validate_age_range,
    validate_billing_period,
    validate_money_amount,
    validate_pricing_kind,
    validate_rule_kind,
)
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)


class _CatalogBase:
    entity_type: str
    not_found_code: str
    not_found_message: str
    duplicate_code: str
    create_event: SportsCenterEventType
    update_event: SportsCenterEventType | None = None

    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):
        center = await self.centers.get(tenant_id, sports_center_id)
        if center is None:
            raise NotFoundError("مرکز ورزشی یافت نشد", error_code="sports_center_not_found")
        return center

    async def get(self, tenant_id: UUID, entity_id: UUID):
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError(self.not_found_message, error_code=self.not_found_code)
        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):
        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=self.entity_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 SportCategoryService(_CatalogBase):
    entity_type = "sport_category"
    not_found_code = "sport_category_not_found"
    not_found_message = "دسته ورزشی یافت نشد"
    duplicate_code = "sport_category_code_exists"
    create_event = SportsCenterEventType.SPORT_CATEGORY_CREATED
    update_event = SportsCenterEventType.SPORT_CATEGORY_UPDATED

    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = SportCategoryRepository(session)

    async def create(self, tenant_id: UUID, body: SportCategoryCreate, *, actor: CurrentUser | None = None) -> SportCategory:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        forbid_sport_specific_hardcoding(body.model_dump())
        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=self.duplicate_code)
        entity = SportCategory(
            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),
            sort_order=body.sort_order,
            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=self.entity_type,
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
            message=f"Sport category {entity.code} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=self.create_event,
            aggregate_type=self.entity_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: SportCategoryUpdate, *, actor: CurrentUser | None = None
    ) -> SportCategory:
        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=self.entity_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)
        if self.update_event:
            self.publisher.publish(
                event_type=self.update_event,
                aggregate_type=self.entity_type,
                aggregate_id=entity.id,
                tenant_id=tenant_id,
                payload={"code": entity.code},
            )
        return entity


class AgeGroupService(_CatalogBase):
    entity_type = "age_group"
    not_found_code = "age_group_not_found"
    not_found_message = "گروه سنی یافت نشد"
    duplicate_code = "age_group_code_exists"
    create_event = SportsCenterEventType.AGE_GROUP_CREATED
    update_event = SportsCenterEventType.AGE_GROUP_UPDATED

    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = AgeGroupRepository(session)

    async def create(self, tenant_id: UUID, body: AgeGroupCreate, *, actor: CurrentUser | None = None) -> AgeGroup:
        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=self.duplicate_code)
        min_age, max_age = validate_age_range(body.min_age, body.max_age)
        entity = AgeGroup(
            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),
            min_age=min_age,
            max_age=max_age,
            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=self.entity_type,
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
            message=f"Age group {entity.code} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=self.create_event,
            aggregate_type=self.entity_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: AgeGroupUpdate, *, actor: CurrentUser | None = None
    ) -> AgeGroup:
        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"])
        min_age = data.get("min_age", entity.min_age)
        max_age = data.get("max_age", entity.max_age)
        if "min_age" in data or "max_age" in data:
            min_age, max_age = validate_age_range(min_age, max_age)
            data["min_age"] = min_age
            data["max_age"] = max_age
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type=self.entity_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)
        return entity


class PricingModelService(_CatalogBase):
    entity_type = "pricing_model"
    not_found_code = "pricing_model_not_found"
    not_found_message = "مدل قیمت‌گذاری یافت نشد"
    duplicate_code = "pricing_model_code_exists"
    create_event = SportsCenterEventType.PRICING_MODEL_CREATED
    update_event = SportsCenterEventType.PRICING_MODEL_UPDATED

    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = PricingModelRepository(session)

    async def create(self, tenant_id: UUID, body: PricingModelCreate, *, actor: CurrentUser | None = None) -> PricingModel:
        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=self.duplicate_code)
        entity = PricingModel(
            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),
            kind=validate_pricing_kind(body.kind),
            billing_period=validate_billing_period(body.billing_period),
            currency_code=validate_currency_code(body.currency_code),
            amount=validate_money_amount(body.amount),
            tiers=body.tiers,
            tax_inclusive=body.tax_inclusive,
            external_price_ref=body.external_price_ref,
            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=self.entity_type,
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
            message=f"Pricing model {entity.code} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=self.create_event,
            aggregate_type=self.entity_type,
            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: PricingModelUpdate, *, actor: CurrentUser | None = None
    ) -> PricingModel:
        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_pricing_kind(data["kind"])
        if "billing_period" in data and data["billing_period"] is not None:
            data["billing_period"] = validate_billing_period(data["billing_period"])
        if "currency_code" in data and data["currency_code"] is not None:
            data["currency_code"] = validate_currency_code(data["currency_code"])
        if "amount" in data:
            data["amount"] = validate_money_amount(data["amount"])
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type=self.entity_type,
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=entity.updated_by,
            changes={k: (str(v) if hasattr(v, "__str__") else v) for k, v in data.items()},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


async def _optional_ref(repo, tenant_id: UUID, ref_id: UUID | None, *, error_code: str, message: str):
    if ref_id is None:
        return None
    entity = await repo.get(tenant_id, ref_id)
    if entity is None:
        raise NotFoundError(message, error_code=error_code)
    return entity


class MembershipPackageService(_CatalogBase):
    entity_type = "membership_package"
    not_found_code = "membership_package_not_found"
    not_found_message = "بسته عضویت یافت نشد"
    duplicate_code = "membership_package_code_exists"
    create_event = SportsCenterEventType.MEMBERSHIP_PACKAGE_CREATED
    update_event = SportsCenterEventType.MEMBERSHIP_PACKAGE_UPDATED

    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = MembershipPackageRepository(session)
        self.pricing = PricingModelRepository(session)

    async def create(
        self, tenant_id: UUID, body: MembershipPackageCreate, *, actor: CurrentUser | None = None
    ) -> MembershipPackage:
        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=self.duplicate_code)
        await _optional_ref(
            self.pricing,
            tenant_id,
            body.pricing_model_id,
            error_code="pricing_model_not_found",
            message="مدل قیمت‌گذاری یافت نشد",
        )
        starts, ends = validate_date_range(body.valid_from, body.valid_until)
        entity = MembershipPackage(
            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),
            pricing_model_id=body.pricing_model_id,
            sport_ids=body.sport_ids,
            included_items=body.included_items,
            session_credits=validate_non_negative_int(body.session_credits, field="session_credits"),
            valid_from=starts,
            valid_until=ends,
            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=self.entity_type,
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
            message=f"Membership package {entity.code} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=self.create_event,
            aggregate_type=self.entity_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: MembershipPackageUpdate, *, actor: CurrentUser | None = None
    ) -> MembershipPackage:
        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 "pricing_model_id" in data:
            await _optional_ref(
                self.pricing,
                tenant_id,
                data["pricing_model_id"],
                error_code="pricing_model_not_found",
                message="مدل قیمت‌گذاری یافت نشد",
            )
        if "session_credits" in data:
            data["session_credits"] = validate_non_negative_int(
                data["session_credits"], field="session_credits"
            )
        if "valid_from" in data or "valid_until" in data:
            starts, ends = validate_date_range(
                data.get("valid_from", entity.valid_from),
                data.get("valid_until", entity.valid_until),
            )
            data["valid_from"] = starts
            data["valid_until"] = ends
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type=self.entity_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)
        return entity


class MembershipPlanService(_CatalogBase):
    entity_type = "membership_plan"
    not_found_code = "membership_plan_not_found"
    not_found_message = "طرح عضویت یافت نشد"
    duplicate_code = "membership_plan_code_exists"
    create_event = SportsCenterEventType.MEMBERSHIP_PLAN_CREATED
    update_event = SportsCenterEventType.MEMBERSHIP_PLAN_UPDATED

    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = MembershipPlanRepository(session)
        self.pricing = PricingModelRepository(session)
        self.packages = MembershipPackageRepository(session)
        self.age_groups = AgeGroupRepository(session)
        self.categories = SportCategoryRepository(session)

    async def create(
        self, tenant_id: UUID, body: MembershipPlanCreate, *, actor: CurrentUser | None = None
    ) -> MembershipPlan:
        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=self.duplicate_code)
        await _optional_ref(
            self.pricing, tenant_id, body.pricing_model_id,
            error_code="pricing_model_not_found", message="مدل قیمت‌گذاری یافت نشد",
        )
        await _optional_ref(
            self.packages, tenant_id, body.package_id,
            error_code="membership_package_not_found", message="بسته عضویت یافت نشد",
        )
        await _optional_ref(
            self.age_groups, tenant_id, body.age_group_id,
            error_code="age_group_not_found", message="گروه سنی یافت نشد",
        )
        await _optional_ref(
            self.categories, tenant_id, body.sport_category_id,
            error_code="sport_category_not_found", message="دسته ورزشی یافت نشد",
        )
        entity = MembershipPlan(
            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"),
            billing_period=validate_billing_period(body.billing_period),
            pricing_model_id=body.pricing_model_id,
            package_id=body.package_id,
            age_group_id=body.age_group_id,
            sport_category_id=body.sport_category_id,
            sport_ids=body.sport_ids,
            features=body.features,
            is_default=body.is_default,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type=self.entity_type,
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
            message=f"Membership plan {entity.code} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=self.create_event,
            aggregate_type=self.entity_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: MembershipPlanUpdate, *, actor: CurrentUser | None = None
    ) -> MembershipPlan:
        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 "billing_period" in data and data["billing_period"] is not None:
            data["billing_period"] = validate_billing_period(data["billing_period"])
        if "duration_days" in data:
            data["duration_days"] = validate_non_negative_int(data["duration_days"], field="duration_days")
        for key, repo, code, msg in (
            ("pricing_model_id", self.pricing, "pricing_model_not_found", "مدل قیمت‌گذاری یافت نشد"),
            ("package_id", self.packages, "membership_package_not_found", "بسته عضویت یافت نشد"),
            ("age_group_id", self.age_groups, "age_group_not_found", "گروه سنی یافت نشد"),
            ("sport_category_id", self.categories, "sport_category_not_found", "دسته ورزشی یافت نشد"),
        ):
            if key in data:
                await _optional_ref(repo, tenant_id, data[key], error_code=code, message=msg)
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type=self.entity_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)
        return entity


class MembershipRuleService(_CatalogBase):
    entity_type = "membership_rule"
    not_found_code = "membership_rule_not_found"
    not_found_message = "قانون عضویت یافت نشد"
    duplicate_code = "membership_rule_code_exists"
    create_event = SportsCenterEventType.MEMBERSHIP_RULE_CREATED
    update_event = SportsCenterEventType.MEMBERSHIP_RULE_UPDATED

    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = MembershipRuleRepository(session)
        self.types = MembershipTypeRepository(session)
        self.plans = MembershipPlanRepository(session)
        self.packages = MembershipPackageRepository(session)

    async def create(
        self, tenant_id: UUID, body: MembershipRuleCreate, *, actor: CurrentUser | None = None
    ) -> MembershipRule:
        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=self.duplicate_code)
        await _optional_ref(
            self.types, tenant_id, body.membership_type_id,
            error_code="membership_type_not_found", message="نوع عضویت یافت نشد",
        )
        await _optional_ref(
            self.plans, tenant_id, body.plan_id,
            error_code="membership_plan_not_found", message="طرح عضویت یافت نشد",
        )
        await _optional_ref(
            self.packages, tenant_id, body.package_id,
            error_code="membership_package_not_found", message="بسته عضویت یافت نشد",
        )
        entity = MembershipRule(
            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),
            kind=validate_rule_kind(body.kind),
            membership_type_id=body.membership_type_id,
            plan_id=body.plan_id,
            package_id=body.package_id,
            priority=body.priority,
            expression=body.expression,
            is_blocking=body.is_blocking,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type=self.entity_type,
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
            message=f"Membership rule {entity.code} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=self.create_event,
            aggregate_type=self.entity_type,
            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: MembershipRuleUpdate, *, actor: CurrentUser | None = None
    ) -> MembershipRule:
        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_rule_kind(data["kind"])
        for key, repo, code, msg in (
            ("membership_type_id", self.types, "membership_type_not_found", "نوع عضویت یافت نشد"),
            ("plan_id", self.plans, "membership_plan_not_found", "طرح عضویت یافت نشد"),
            ("package_id", self.packages, "membership_package_not_found", "بسته عضویت یافت نشد"),
        ):
            if key in data:
                await _optional_ref(repo, tenant_id, data[key], error_code=code, message=msg)
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type=self.entity_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)
        return entity


class RenewalPolicyService(_CatalogBase):
    entity_type = "renewal_policy"
    not_found_code = "renewal_policy_not_found"
    not_found_message = "سیاست تمدید یافت نشد"
    duplicate_code = "renewal_policy_code_exists"
    create_event = SportsCenterEventType.RENEWAL_POLICY_CREATED
    update_event = SportsCenterEventType.RENEWAL_POLICY_UPDATED

    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = RenewalPolicyRepository(session)
        self.pricing = PricingModelRepository(session)

    async def create(
        self, tenant_id: UUID, body: RenewalPolicyCreate, *, actor: CurrentUser | None = None
    ) -> RenewalPolicy:
        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=self.duplicate_code)
        await _optional_ref(
            self.pricing, tenant_id, body.pricing_model_id,
            error_code="pricing_model_not_found", message="مدل قیمت‌گذاری یافت نشد",
        )
        entity = RenewalPolicy(
            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),
            auto_renew=body.auto_renew,
            renew_window_days=validate_non_negative_int(body.renew_window_days, field="renew_window_days"),
            grace_period_days=validate_non_negative_int(body.grace_period_days, field="grace_period_days") or 0,
            max_renewals=validate_non_negative_int(body.max_renewals, field="max_renewals"),
            pricing_model_id=body.pricing_model_id,
            rules=body.rules,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type=self.entity_type,
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
            message=f"Renewal policy {entity.code} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=self.create_event,
            aggregate_type=self.entity_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: RenewalPolicyUpdate, *, actor: CurrentUser | None = None
    ) -> RenewalPolicy:
        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"])
        for field in ("renew_window_days", "grace_period_days", "max_renewals"):
            if field in data:
                data[field] = validate_non_negative_int(data[field], field=field)
                if field == "grace_period_days" and data[field] is None:
                    data[field] = 0
        if "pricing_model_id" in data:
            await _optional_ref(
                self.pricing, tenant_id, data["pricing_model_id"],
                error_code="pricing_model_not_found", message="مدل قیمت‌گذاری یافت نشد",
            )
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type=self.entity_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)
        return entity


class FreezingRuleService(_CatalogBase):
    entity_type = "freezing_rule"
    not_found_code = "freezing_rule_not_found"
    not_found_message = "قانون فریز یافت نشد"
    duplicate_code = "freezing_rule_code_exists"
    create_event = SportsCenterEventType.FREEZING_RULE_CREATED
    update_event = SportsCenterEventType.FREEZING_RULE_UPDATED

    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = FreezingRuleRepository(session)

    async def create(
        self, tenant_id: UUID, body: FreezingRuleCreate, *, actor: CurrentUser | None = None
    ) -> FreezingRule:
        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=self.duplicate_code)
        entity = FreezingRule(
            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),
            max_freeze_days=validate_non_negative_int(body.max_freeze_days, field="max_freeze_days"),
            max_freeze_count=validate_non_negative_int(body.max_freeze_count, field="max_freeze_count"),
            min_active_days_before_freeze=validate_non_negative_int(
                body.min_active_days_before_freeze, field="min_active_days_before_freeze"
            ),
            extend_end_date=body.extend_end_date,
            requires_approval=body.requires_approval,
            rules=body.rules,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type=self.entity_type,
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
            message=f"Freezing rule {entity.code} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=self.create_event,
            aggregate_type=self.entity_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: FreezingRuleUpdate, *, actor: CurrentUser | None = None
    ) -> FreezingRule:
        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"])
        for field in ("max_freeze_days", "max_freeze_count", "min_active_days_before_freeze"):
            if field in data:
                data[field] = validate_non_negative_int(data[field], field=field)
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type=self.entity_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)
        return entity


class ExpirationPolicyService(_CatalogBase):
    entity_type = "expiration_policy"
    not_found_code = "expiration_policy_not_found"
    not_found_message = "سیاست انقضا یافت نشد"
    duplicate_code = "expiration_policy_code_exists"
    create_event = SportsCenterEventType.EXPIRATION_POLICY_CREATED
    update_event = SportsCenterEventType.EXPIRATION_POLICY_UPDATED

    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = ExpirationPolicyRepository(session)

    async def create(
        self, tenant_id: UUID, body: ExpirationPolicyCreate, *, actor: CurrentUser | None = None
    ) -> ExpirationPolicy:
        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=self.duplicate_code)
        entity = ExpirationPolicy(
            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),
            expire_after_days=validate_non_negative_int(body.expire_after_days, field="expire_after_days"),
            warn_before_days=validate_non_negative_int(body.warn_before_days, field="warn_before_days"),
            grace_period_days=validate_non_negative_int(body.grace_period_days, field="grace_period_days") or 0,
            auto_expire=body.auto_expire,
            post_expire_status=validate_non_empty(body.post_expire_status, field="post_expire_status"),
            rules=body.rules,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type=self.entity_type,
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
            message=f"Expiration policy {entity.code} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=self.create_event,
            aggregate_type=self.entity_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: ExpirationPolicyUpdate, *, actor: CurrentUser | None = None
    ) -> ExpirationPolicy:
        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 "post_expire_status" in data and data["post_expire_status"] is not None:
            data["post_expire_status"] = validate_non_empty(
                data["post_expire_status"], field="post_expire_status"
            )
        for field in ("expire_after_days", "warn_before_days", "grace_period_days"):
            if field in data:
                data[field] = validate_non_negative_int(data[field], field=field)
                if field == "grace_period_days" and data[field] is None:
                    data[field] = 0
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type=self.entity_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)
        return entity
