"""Member Management application services — Phase 9.2."""
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.members import (
    DigitalMembership,
    EmergencyContact,
    FamilyMember,
    MedicalInformation,
    Member,
    MemberDocument,
    MembershipCard,
    Waiver,
)
from app.models.types import AuditAction, CardKind, CardStatus, WaiverStatus
from app.repositories.foundation import MembershipRepository, SportsCenterRepository
from app.repositories.members import (
    DigitalMembershipRepository,
    EmergencyContactRepository,
    FamilyMemberRepository,
    MedicalInformationRepository,
    MemberDocumentRepository,
    MemberRepository,
    MembershipCardRepository,
    WaiverRepository,
)
from app.schemas.members import (
    DigitalMembershipCreate,
    DigitalMembershipUpdate,
    EmergencyContactCreate,
    EmergencyContactUpdate,
    FamilyMemberCreate,
    FamilyMemberUpdate,
    MedicalInformationUpsert,
    MemberCreate,
    MemberDocumentCreate,
    MemberDocumentUpdate,
    MembershipCardCreate,
    MembershipCardUpdate,
    MemberUpdate,
    WaiverCreate,
    WaiverSignRequest,
    WaiverUpdate,
)
from app.services.audit_service import AuditService
from app.validators import (
    ensure_optimistic_version,
    validate_code,
    validate_email,
    validate_non_empty,
    validate_non_negative_int,
    validate_phone,
)
from app.validators.members import (
    validate_card_kind,
    validate_card_status,
    validate_document_kind,
    validate_family_relationship,
    validate_file_ref,
    validate_member_status,
    validate_waiver_status,
)
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_member_number() -> str:
    return f"MB-{uuid4().hex[:10].upper()}"


def _next_card_number() -> str:
    return f"MC-{uuid4().hex[:12].upper()}"


def _next_pass_code() -> str:
    return f"DP-{uuid4().hex[:12].upper()}"


class _MemberBase:
    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)
        self.members = MemberRepository(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 _require_member(self, tenant_id: UUID, member_id: UUID) -> Member:
        member = await self.members.get(tenant_id, member_id)
        if member is None:
            raise NotFoundError("عضو یافت نشد", error_code="member_not_found")
        return member


class MemberService(_MemberBase):
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        super().__init__(session, publisher)
        self.repo = self.members

    async def create(
        self, tenant_id: UUID, body: MemberCreate, *, actor: CurrentUser | None = None
    ) -> Member:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        member_number = (
            validate_code(body.member_number, field="member_number")
            if body.member_number
            else _next_member_number()
        )
        if await self.repo.get_by_member_number(tenant_id, member_number):
            raise AppError(
                "شماره عضو تکراری است",
                status_code=409,
                error_code="member_number_exists",
            )
        entity = Member(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            branch_id=body.branch_id,
            member_number=member_number,
            display_name=validate_non_empty(body.display_name, field="display_name"),
            first_name=body.first_name,
            last_name=body.last_name,
            email=validate_email(body.email),
            mobile=validate_phone(body.mobile, field="mobile"),
            birth_date=body.birth_date,
            gender=body.gender,
            status=validate_member_status(body.status),
            external_user_ref=body.external_user_ref,
            external_customer_ref=body.external_customer_ref,
            external_crm_contact_ref=body.external_crm_contact_ref,
            preferred_language=body.preferred_language,
            notes=body.notes,
            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="member",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
            message=f"Member {entity.member_number} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=SportsCenterEventType.MEMBER_CREATED,
            aggregate_type="member",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "member_number": entity.member_number,
                "display_name": entity.display_name,
                "status": entity.status.value,
            },
        )
        return entity

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: MemberUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> Member:
        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 "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_member_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="member",
            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.MEMBER_UPDATED,
            aggregate_type="member",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"status": entity.status.value},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Member:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("عضو یافت نشد", error_code="member_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
    ) -> Member:
        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="member",
            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 FamilyMemberService(_MemberBase):
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        super().__init__(session, publisher)
        self.repo = FamilyMemberRepository(session)

    async def create(
        self,
        tenant_id: UUID,
        body: FamilyMemberCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> FamilyMember:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        member = await self._require_member(tenant_id, body.member_id)
        if member.sports_center_id != body.sports_center_id:
            raise AppError(
                "عضو متعلق به این مرکز نیست",
                status_code=422,
                error_code="member_center_mismatch",
            )
        if body.related_member_id:
            await self._require_member(tenant_id, body.related_member_id)
        entity = FamilyMember(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            member_id=body.member_id,
            related_member_id=body.related_member_id,
            display_name=validate_non_empty(body.display_name, field="display_name"),
            relationship=validate_family_relationship(body.relationship),
            email=validate_email(body.email),
            mobile=validate_phone(body.mobile, field="mobile"),
            birth_date=body.birth_date,
            is_emergency_eligible=body.is_emergency_eligible,
            notes=body.notes,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="family_member",
            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.FAMILY_MEMBER_CREATED,
            aggregate_type="family_member",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"member_id": str(entity.member_id)},
        )
        return entity

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: FamilyMemberUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> FamilyMember:
        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 "relationship" in data and data["relationship"] is not None:
            data["relationship"] = validate_family_relationship(data["relationship"])
        if data.get("related_member_id"):
            await self._require_member(tenant_id, data["related_member_id"])
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="family_member",
            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) -> FamilyMember:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("عضو خانواده یافت نشد", error_code="family_member_not_found")
        return entity

    async def list_by_member(
        self, tenant_id: UUID, member_id: UUID, *, offset: int, limit: int
    ):
        await self._require_member(tenant_id, member_id)
        return await self.repo.list_by_member(
            tenant_id, member_id, offset=offset, limit=limit
        )

    async def soft_delete(
        self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None
    ) -> FamilyMember:
        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="family_member",
            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 EmergencyContactService(_MemberBase):
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        super().__init__(session, publisher)
        self.repo = EmergencyContactRepository(session)

    async def create(
        self,
        tenant_id: UUID,
        body: EmergencyContactCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> EmergencyContact:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        member = await self._require_member(tenant_id, body.member_id)
        if member.sports_center_id != body.sports_center_id:
            raise AppError(
                "عضو متعلق به این مرکز نیست",
                status_code=422,
                error_code="member_center_mismatch",
            )
        phone = validate_phone(body.phone, field="phone")
        if not phone:
            raise AppError(
                "شماره تماس اضطراری الزامی است",
                status_code=422,
                error_code="phone_required",
            )
        entity = EmergencyContact(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            member_id=body.member_id,
            display_name=validate_non_empty(body.display_name, field="display_name"),
            relationship=body.relationship,
            phone=phone,
            alternate_phone=validate_phone(body.alternate_phone, field="alternate_phone"),
            email=validate_email(body.email),
            is_primary=body.is_primary,
            notes=body.notes,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="emergency_contact",
            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.EMERGENCY_CONTACT_CREATED,
            aggregate_type="emergency_contact",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"member_id": str(entity.member_id)},
        )
        return entity

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: EmergencyContactUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> EmergencyContact:
        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 "phone" in data:
            phone = validate_phone(data["phone"], field="phone")
            if not phone:
                raise AppError(
                    "شماره تماس اضطراری الزامی است",
                    status_code=422,
                    error_code="phone_required",
                )
            data["phone"] = phone
        if "alternate_phone" in data:
            data["alternate_phone"] = validate_phone(
                data["alternate_phone"], field="alternate_phone"
            )
        if "email" in data:
            data["email"] = validate_email(data["email"])
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="emergency_contact",
            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) -> EmergencyContact:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError(
                "تماس اضطراری یافت نشد", error_code="emergency_contact_not_found"
            )
        return entity

    async def list_by_member(
        self, tenant_id: UUID, member_id: UUID, *, offset: int, limit: int
    ):
        await self._require_member(tenant_id, member_id)
        return await self.repo.list_by_member(
            tenant_id, member_id, offset=offset, limit=limit
        )

    async def soft_delete(
        self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None
    ) -> EmergencyContact:
        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="emergency_contact",
            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 MedicalInformationService(_MemberBase):
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        super().__init__(session, publisher)
        self.repo = MedicalInformationRepository(session)

    async def upsert(
        self,
        tenant_id: UUID,
        body: MedicalInformationUpsert,
        *,
        actor: CurrentUser | None = None,
    ) -> MedicalInformation:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        member = await self._require_member(tenant_id, body.member_id)
        if member.sports_center_id != body.sports_center_id:
            raise AppError(
                "عضو متعلق به این مرکز نیست",
                status_code=422,
                error_code="member_center_mismatch",
            )
        file_ref = (
            validate_file_ref(body.document_file_ref, field="document_file_ref")
            if body.document_file_ref
            else None
        )
        existing = await self.repo.get_by_member(tenant_id, body.member_id)
        if existing is None:
            entity = MedicalInformation(
                tenant_id=tenant_id,
                sports_center_id=body.sports_center_id,
                member_id=body.member_id,
                blood_type=body.blood_type,
                allergies=body.allergies,
                conditions=body.conditions,
                medications=body.medications,
                clearance_status=body.clearance_status,
                clearance_expires_on=body.clearance_expires_on,
                physician_name=body.physician_name,
                physician_phone=validate_phone(body.physician_phone, field="physician_phone"),
                notes=body.notes,
                document_file_ref=file_ref,
                is_confidential=body.is_confidential,
                created_by=actor_id,
                updated_by=actor_id,
            )
            await self.repo.add(entity)
            action = AuditAction.CREATE
            event = SportsCenterEventType.MEDICAL_INFORMATION_UPSERTED
        else:
            entity = existing
            entity.blood_type = body.blood_type
            entity.allergies = body.allergies
            entity.conditions = body.conditions
            entity.medications = body.medications
            entity.clearance_status = body.clearance_status
            entity.clearance_expires_on = body.clearance_expires_on
            entity.physician_name = body.physician_name
            entity.physician_phone = validate_phone(
                body.physician_phone, field="physician_phone"
            )
            entity.notes = body.notes
            entity.document_file_ref = file_ref
            entity.is_confidential = body.is_confidential
            entity.updated_by = actor_id
            action = AuditAction.UPDATE
            event = SportsCenterEventType.MEDICAL_INFORMATION_UPSERTED
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="medical_information",
            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=event,
            aggregate_type="medical_information",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"member_id": str(entity.member_id)},
        )
        return entity

    async def get_by_member(
        self, tenant_id: UUID, member_id: UUID
    ) -> MedicalInformation:
        await self._require_member(tenant_id, member_id)
        entity = await self.repo.get_by_member(tenant_id, member_id)
        if entity is None:
            raise NotFoundError(
                "اطلاعات پزشکی یافت نشد", error_code="medical_information_not_found"
            )
        return entity


class MembershipCardService(_MemberBase):
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        super().__init__(session, publisher)
        self.repo = MembershipCardRepository(session)
        self.memberships = MembershipRepository(session)

    async def create(
        self,
        tenant_id: UUID,
        body: MembershipCardCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> MembershipCard:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        member = await self._require_member(tenant_id, body.member_id)
        if member.sports_center_id != body.sports_center_id:
            raise AppError(
                "عضو متعلق به این مرکز نیست",
                status_code=422,
                error_code="member_center_mismatch",
            )
        if body.membership_id:
            membership = await self.memberships.get(tenant_id, body.membership_id)
            if membership is None:
                raise NotFoundError("عضویت یافت نشد", error_code="membership_not_found")
        kind = validate_card_kind(body.kind)
        card_number = (
            validate_code(body.card_number, field="card_number")
            if body.card_number
            else _next_card_number()
        )
        if await self.repo.get_by_card_number(tenant_id, card_number):
            raise AppError(
                "شماره کارت تکراری است",
                status_code=409,
                error_code="card_number_exists",
            )
        issued_on = body.issued_on or date.today()
        qr_payload = None
        barcode_payload = None
        if kind in (CardKind.QR, CardKind.DIGITAL):
            qr_payload = f"sc:{tenant_id}:{member.id}:{card_number}"
        if kind == CardKind.BARCODE:
            barcode_payload = card_number
        entity = MembershipCard(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            member_id=body.member_id,
            membership_id=body.membership_id,
            card_number=card_number,
            kind=kind,
            status=validate_card_status(body.status),
            qr_payload=qr_payload,
            barcode_payload=barcode_payload,
            issued_on=issued_on,
            expires_on=body.expires_on,
            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="membership_card",
            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.MEMBERSHIP_CARD_ISSUED,
            aggregate_type="membership_card",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "member_id": str(entity.member_id),
                "card_number": entity.card_number,
                "kind": entity.kind.value,
            },
        )
        return entity

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: MembershipCardUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> MembershipCard:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "status" in data and data["status"] is not None:
            data["status"] = validate_card_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="membership_card",
            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 revoke(
        self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None
    ) -> MembershipCard:
        entity = await self.get(tenant_id, entity_id)
        entity.status = CardStatus.REVOKED
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="membership_card",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=entity.updated_by,
            message="card revoked",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=SportsCenterEventType.MEMBERSHIP_CARD_REVOKED,
            aggregate_type="membership_card",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"card_number": entity.card_number},
        )
        return entity

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

    async def list_by_member(
        self, tenant_id: UUID, member_id: UUID, *, offset: int, limit: int
    ):
        await self._require_member(tenant_id, member_id)
        return await self.repo.list_by_member(
            tenant_id, member_id, offset=offset, limit=limit
        )


class DigitalMembershipService(_MemberBase):
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        super().__init__(session, publisher)
        self.repo = DigitalMembershipRepository(session)
        self.memberships = MembershipRepository(session)

    async def create(
        self,
        tenant_id: UUID,
        body: DigitalMembershipCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> DigitalMembership:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        await self._require_member(tenant_id, body.member_id)
        if body.membership_id:
            membership = await self.memberships.get(tenant_id, body.membership_id)
            if membership is None:
                raise NotFoundError("عضویت یافت نشد", error_code="membership_not_found")
        pass_code = (
            validate_code(body.pass_code, field="pass_code")
            if body.pass_code
            else _next_pass_code()
        )
        if await self.repo.get_by_pass_code(tenant_id, pass_code):
            raise AppError(
                "کد پاس دیجیتال تکراری است",
                status_code=409,
                error_code="pass_code_exists",
            )
        entity = DigitalMembership(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            member_id=body.member_id,
            membership_id=body.membership_id,
            pass_code=pass_code,
            status=validate_card_status(body.status),
            token_ref=body.token_ref,
            deep_link=body.deep_link,
            issued_at=datetime.now(timezone.utc),
            expires_at=body.expires_at,
            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="digital_membership",
            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.DIGITAL_MEMBERSHIP_ISSUED,
            aggregate_type="digital_membership",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"member_id": str(entity.member_id), "pass_code": entity.pass_code},
        )
        return entity

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: DigitalMembershipUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> DigitalMembership:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "status" in data and data["status"] is not None:
            data["status"] = validate_card_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="digital_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)
        return entity

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

    async def list_by_member(
        self, tenant_id: UUID, member_id: UUID, *, offset: int, limit: int
    ):
        await self._require_member(tenant_id, member_id)
        return await self.repo.list_by_member(
            tenant_id, member_id, offset=offset, limit=limit
        )


class WaiverService(_MemberBase):
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        super().__init__(session, publisher)
        self.repo = WaiverRepository(session)

    async def create(
        self, tenant_id: UUID, body: WaiverCreate, *, actor: CurrentUser | None = None
    ) -> Waiver:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        await self._require_member(tenant_id, body.member_id)
        entity = Waiver(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            member_id=body.member_id,
            membership_id=body.membership_id,
            title=validate_non_empty(body.title, field="title"),
            waiver_version=validate_non_empty(body.waiver_version, field="waiver_version"),
            status=validate_waiver_status(body.status),
            expires_on=body.expires_on,
            document_file_ref=(
                validate_file_ref(body.document_file_ref)
                if body.document_file_ref
                else None
            ),
            notes=body.notes,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="waiver",
            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.WAIVER_CREATED,
            aggregate_type="waiver",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"member_id": str(entity.member_id), "title": entity.title},
        )
        return entity

    async def sign(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: WaiverSignRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> Waiver:
        entity = await self.get(tenant_id, entity_id)
        if entity.status == WaiverStatus.SIGNED:
            raise AppError(
                "تعهدنامه قبلاً امضا شده است",
                status_code=409,
                error_code="waiver_already_signed",
            )
        entity.status = WaiverStatus.SIGNED
        entity.signed_at = datetime.now(timezone.utc)
        if body.signature_ref:
            entity.signature_ref = validate_file_ref(
                body.signature_ref, field="signature_ref"
            )
        if body.document_file_ref:
            entity.document_file_ref = validate_file_ref(body.document_file_ref)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="waiver",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=entity.updated_by,
            message="waiver signed",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=SportsCenterEventType.WAIVER_SIGNED,
            aggregate_type="waiver",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"member_id": str(entity.member_id)},
        )
        return entity

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: WaiverUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> Waiver:
        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_waiver_status(data["status"])
        if "document_file_ref" in data and data["document_file_ref"] is not None:
            data["document_file_ref"] = validate_file_ref(data["document_file_ref"])
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="waiver",
            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) -> Waiver:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("تعهدنامه یافت نشد", error_code="waiver_not_found")
        return entity

    async def list_by_member(
        self, tenant_id: UUID, member_id: UUID, *, offset: int, limit: int
    ):
        await self._require_member(tenant_id, member_id)
        return await self.repo.list_by_member(
            tenant_id, member_id, offset=offset, limit=limit
        )


class MemberDocumentService(_MemberBase):
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        super().__init__(session, publisher)
        self.repo = MemberDocumentRepository(session)

    async def create(
        self,
        tenant_id: UUID,
        body: MemberDocumentCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> MemberDocument:
        actor_id = actor.user_id if actor else None
        await self._require_center(tenant_id, body.sports_center_id)
        await self._require_member(tenant_id, body.member_id)
        size = (
            validate_non_negative_int(body.size_bytes, field="size_bytes")
            if body.size_bytes is not None
            else None
        )
        entity = MemberDocument(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            member_id=body.member_id,
            title=validate_non_empty(body.title, field="title"),
            kind=validate_document_kind(body.kind),
            file_ref=validate_file_ref(body.file_ref),
            mime_type=body.mime_type,
            size_bytes=size,
            notes=body.notes,
            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="member_document",
            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.MEMBER_DOCUMENT_ATTACHED,
            aggregate_type="member_document",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"member_id": str(entity.member_id), "file_ref": entity.file_ref},
        )
        return entity

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: MemberDocumentUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> MemberDocument:
        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 "kind" in data and data["kind"] is not None:
            data["kind"] = validate_document_kind(data["kind"])
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="member_document",
            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) -> MemberDocument:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("سند عضو یافت نشد", error_code="member_document_not_found")
        return entity

    async def list_by_member(
        self, tenant_id: UUID, member_id: UUID, *, offset: int, limit: int
    ):
        await self._require_member(tenant_id, member_id)
        return await self.repo.list_by_member(
            tenant_id, member_id, offset=offset, limit=limit
        )

    async def soft_delete(
        self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None
    ) -> MemberDocument:
        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="member_document",
            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
