"""Attendance & Access Control services — Phase 9.5."""
from __future__ import annotations

from datetime import datetime, timezone
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.attendance import AccessLog, AttendanceRecord
from app.models.types import AccessResult, AttendanceKind, AttendanceStatus, AuditAction, MembershipStatus
from app.repositories.attendance import AccessLogRepository, AttendanceRecordRepository
from app.repositories.foundation import MembershipRepository, SportsCenterRepository
from app.repositories.members import MemberRepository, MembershipCardRepository
from app.schemas.attendance import AttendanceRecordCreate, CheckInRequest, CheckOutRequest
from app.services.audit_service import AuditService
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


class AttendanceService:
    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)
        self.memberships = MembershipRepository(session)
        self.cards = MembershipCardRepository(session)
        self.records = AttendanceRecordRepository(session)
        self.access_logs = AccessLogRepository(session)

    async def _validate_membership(self, tenant_id: UUID, member_id: UUID, membership_id: UUID | None):
        if membership_id is None:
            return None
        membership = await self.memberships.get(tenant_id, membership_id)
        if membership is None or membership.member_id != member_id:
            raise AppError("عضویت معتبر نیست", error_code="invalid_membership")
        if membership.status not in (MembershipStatus.ACTIVE, MembershipStatus.FROZEN):
            raise AppError("عضویت فعال نیست", error_code="membership_not_active")
        return membership

    async def _log_access(
        self,
        tenant_id: UUID,
        *,
        sports_center_id: UUID,
        member_id: UUID | None,
        membership_id: UUID | None,
        device_id: UUID | None,
        gateway_id: UUID | None,
        access_result: AccessResult,
        access_method: str | None,
        credential_ref: str | None,
        denial_reason: str | None = None,
        actor: CurrentUser,
    ) -> AccessLog:
        log = AccessLog(
            tenant_id=tenant_id,
            sports_center_id=sports_center_id,
            member_id=member_id,
            membership_id=membership_id,
            device_id=device_id,
            gateway_id=gateway_id,
            access_result=access_result,
            access_method=access_method,
            credential_ref=credential_ref,
            recorded_at=datetime.now(timezone.utc),
            denial_reason=denial_reason,
            created_by=actor.user_id if actor else None,
            updated_by=actor.user_id if actor else None,
        )
        await self.access_logs.add(log)
        event = (
            SportsCenterEventType.ACCESS_GRANTED
            if access_result == AccessResult.GRANTED
            else SportsCenterEventType.ACCESS_DENIED
        )
        self.publisher.publish(
            tenant_id=tenant_id,
            event_type=event,
            aggregate_type="access_log",
            aggregate_id=log.id,
            payload={"member_id": str(member_id) if member_id else None, "result": access_result.value},
        )
        return log

    async def check_in(self, tenant_id: UUID, body: CheckInRequest, *, actor: CurrentUser):
        center = await self.centers.get(tenant_id, body.sports_center_id)
        if center is None:
            raise NotFoundError("مرکز ورزشی یافت نشد", error_code="sports_center_not_found")
        member = await self.members.get(tenant_id, body.member_id)
        if member is None:
            await self._log_access(
                tenant_id,
                sports_center_id=body.sports_center_id,
                member_id=None,
                membership_id=None,
                device_id=body.device_id,
                gateway_id=body.gateway_id,
                access_result=AccessResult.DENIED,
                access_method="check_in",
                credential_ref=body.pass_code,
                denial_reason="member_not_found",
                actor=actor,
            )
            await self.session.commit()
            raise NotFoundError("عضو یافت نشد", error_code="member_not_found")
        try:
            await self._validate_membership(tenant_id, body.member_id, body.membership_id)
        except AppError as exc:
            await self._log_access(
                tenant_id,
                sports_center_id=body.sports_center_id,
                member_id=body.member_id,
                membership_id=body.membership_id,
                device_id=body.device_id,
                gateway_id=body.gateway_id,
                access_result=AccessResult.DENIED,
                access_method="check_in",
                credential_ref=body.pass_code,
                denial_reason=exc.error_code,
                actor=actor,
            )
            await self.session.commit()
            raise
        now = datetime.now(timezone.utc)
        record = AttendanceRecord(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            member_id=body.member_id,
            membership_id=body.membership_id,
            session_id=body.session_id,
            attendance_kind=AttendanceKind.CHECK_IN,
            status=AttendanceStatus.PRESENT,
            recorded_at=now,
            device_id=body.device_id,
            gateway_id=body.gateway_id,
            card_id=body.card_id,
            pass_code=body.pass_code,
            notes=body.notes,
            created_by=actor.user_id if actor else None,
            updated_by=actor.user_id if actor else None,
        )
        await self.records.add(record)
        await self._log_access(
            tenant_id,
            sports_center_id=body.sports_center_id,
            member_id=body.member_id,
            membership_id=body.membership_id,
            device_id=body.device_id,
            gateway_id=body.gateway_id,
            access_result=AccessResult.GRANTED,
            access_method="check_in",
            credential_ref=body.pass_code,
            actor=actor,
        )
        await self.session.commit()
        await self.session.refresh(record)
        self.publisher.publish(
            tenant_id=tenant_id,
            event_type=SportsCenterEventType.CHECK_IN_COMPLETED,
            aggregate_type="attendance_record",
            aggregate_id=record.id,
            payload={"member_id": str(body.member_id)},
        )
        self.publisher.publish(
            tenant_id=tenant_id,
            event_type=SportsCenterEventType.ATTENDANCE_RECORDED,
            aggregate_type="attendance_record",
            aggregate_id=record.id,
            payload={"kind": AttendanceKind.CHECK_IN.value},
        )
        return record

    async def check_out(self, tenant_id: UUID, body: CheckOutRequest, *, actor: CurrentUser):
        member = await self.members.get(tenant_id, body.member_id)
        if member is None:
            raise NotFoundError("عضو یافت نشد", error_code="member_not_found")
        now = datetime.now(timezone.utc)
        record = AttendanceRecord(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            member_id=body.member_id,
            membership_id=body.membership_id,
            attendance_kind=AttendanceKind.CHECK_OUT,
            status=AttendanceStatus.PRESENT,
            recorded_at=now,
            device_id=body.device_id,
            gateway_id=body.gateway_id,
            notes=body.notes,
            created_by=actor.user_id if actor else None,
            updated_by=actor.user_id if actor else None,
        )
        await self.records.add(record)
        await self.session.commit()
        await self.session.refresh(record)
        self.publisher.publish(
            tenant_id=tenant_id,
            event_type=SportsCenterEventType.CHECK_OUT_COMPLETED,
            aggregate_type="attendance_record",
            aggregate_id=record.id,
            payload={"member_id": str(body.member_id)},
        )
        return record

    async def create_manual(
        self, tenant_id: UUID, body: AttendanceRecordCreate, *, actor: CurrentUser
    ):
        member = await self.members.get(tenant_id, body.member_id)
        if member is None:
            raise NotFoundError("عضو یافت نشد", error_code="member_not_found")
        record = AttendanceRecord(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            branch_id=body.branch_id,
            member_id=body.member_id,
            membership_id=body.membership_id,
            session_id=body.session_id,
            attendance_kind=body.attendance_kind,
            status=body.status,
            recorded_at=body.recorded_at,
            device_id=body.device_id,
            gateway_id=body.gateway_id,
            card_id=body.card_id,
            pass_code=body.pass_code,
            notes=body.notes,
            metadata_json=body.metadata_json,
            created_by=actor.user_id if actor else None,
            updated_by=actor.user_id if actor else None,
        )
        await self.records.add(record)
        await self.session.commit()
        await self.session.refresh(record)
        self.publisher.publish(
            tenant_id=tenant_id,
            event_type=SportsCenterEventType.ATTENDANCE_RECORDED,
            aggregate_type="attendance_record",
            aggregate_id=record.id,
            payload={"kind": body.attendance_kind.value},
        )
        return record

    async def list_records(self, tenant_id: UUID, member_id: UUID, *, offset=0, limit=20):
        return await self.records.list_by_member(tenant_id, member_id, offset=offset, limit=limit)

    async def list_access_logs(self, tenant_id: UUID, member_id: UUID, *, offset=0, limit=20):
        return await self.access_logs.list_by_member(tenant_id, member_id, offset=offset, limit=limit)
