"""Table Service & Reservations application services — Phase 12.3.

Dine-in workflows only: reservations, table assignments, in-service
requests, and waitlist. No POS / kitchen / payment posting.
"""
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 HospitalityEventType
from app.models.table_service import (
    Reservation,
    ServiceRequest,
    TableAssignment,
    WaitlistEntry,
)
from app.models.types import (
    AuditAction,
    ReservationStatus,
    ServiceRequestStatus,
    TableAssignmentStatus,
    WaitlistStatus,
)
from app.repositories.foundation import (
    BranchRepository,
    DiningAreaRepository,
    DiningTableRepository,
    VenueRepository,
)
from app.repositories.qr import QrOrderingSessionRepository
from app.repositories.table_service import (
    ReservationRepository,
    ServiceRequestRepository,
    TableAssignmentRepository,
    WaitlistEntryRepository,
)
from app.schemas.table_service import (
    ReservationCreate,
    ServiceRequestCreate,
    TableAssignmentCreate,
    WaitlistEntryCreate,
)
from app.services.audit_service import AuditService
from app.validators import (
    validate_code,
    validate_non_empty,
    validate_positive_int,
    validate_reservation_status,
    validate_reservation_window,
    validate_service_request_kind,
    validate_service_request_status,
    validate_waitlist_status,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser

RESERVATION_TERMINAL_STATUSES = frozenset(
    {ReservationStatus.COMPLETED, ReservationStatus.CANCELLED, ReservationStatus.NO_SHOW}
)
SERVICE_REQUEST_TERMINAL_STATUSES = frozenset(
    {ServiceRequestStatus.CLOSED, ServiceRequestStatus.CANCELLED}
)
WAITLIST_TERMINAL_STATUSES = frozenset(
    {WaitlistStatus.SEATED, WaitlistStatus.CANCELLED, WaitlistStatus.EXPIRED}
)


class _TableServiceBase:
    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.venues = VenueRepository(session)
        self.tables = DiningTableRepository(session)

    async def _require_venue(self, tenant_id: UUID, venue_id: UUID):
        venue = await self.venues.get(tenant_id, venue_id)
        if venue is None:
            raise NotFoundError("واحد پذیرایی یافت نشد", error_code="venue_not_found")
        return venue

    async def _require_table(self, tenant_id: UUID, table_id: UUID):
        table = await self.tables.get(tenant_id, table_id)
        if table is None:
            raise NotFoundError("میز یافت نشد", error_code="table_not_found")
        return table


class ReservationService(_TableServiceBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = ReservationRepository(session)
        self.branches = BranchRepository(session)
        self.dining_areas = DiningAreaRepository(session)

    async def create(
        self, tenant_id: UUID, body: ReservationCreate, *, actor: CurrentUser | None = None
    ) -> Reservation:
        await self._require_venue(tenant_id, body.venue_id)
        if body.branch_id is not None and await self.branches.get(tenant_id, body.branch_id) is None:
            raise NotFoundError("شعبه یافت نشد", error_code="branch_not_found")
        if (
            body.dining_area_id is not None
            and await self.dining_areas.get(tenant_id, body.dining_area_id) is None
        ):
            raise NotFoundError("فضای پذیرایی یافت نشد", error_code="dining_area_not_found")
        if body.table_id is not None:
            await self._require_table(tenant_id, body.table_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.venue_id, code) is not None:
            raise AppError(
                "کد رزرو تکراری است", status_code=409, error_code="reservation_code_exists"
            )
        party_size = validate_positive_int(body.party_size, field="party_size")
        reserved_from, reserved_to = validate_reservation_window(
            body.reserved_from, body.reserved_to
        )
        actor_id = actor.user_id if actor else None
        entity = Reservation(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            branch_id=body.branch_id,
            dining_area_id=body.dining_area_id,
            table_id=body.table_id,
            code=code,
            guest_name=validate_non_empty(body.guest_name, field="guest_name"),
            guest_phone=body.guest_phone,
            party_size=party_size,
            status=ReservationStatus.PENDING,
            reserved_from=reserved_from,
            reserved_to=reserved_to,
            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="reservation",
            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=HospitalityEventType.RESERVATION_CREATED,
            aggregate_type="reservation",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "status": entity.status.value},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Reservation:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("رزرو یافت نشد", error_code="reservation_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 change_status(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        new_status: ReservationStatus | str,
        *,
        actor: CurrentUser | None = None,
    ) -> Reservation:
        entity = await self.get(tenant_id, entity_id)
        resolved = validate_reservation_status(new_status)
        if entity.status in RESERVATION_TERMINAL_STATUSES:
            raise AppError(
                "وضعیت رزرو نهایی شده و قابل تغییر نیست",
                status_code=409,
                error_code="reservation_status_final",
            )
        actor_id = actor.user_id if actor else None
        entity.status = resolved
        entity.updated_by = actor_id
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="reservation",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=actor_id,
            changes={"status": resolved},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=HospitalityEventType.RESERVATION_STATUS_CHANGED,
            aggregate_type="reservation",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"status": entity.status.value},
        )
        return entity


class TableAssignmentService(_TableServiceBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = TableAssignmentRepository(session)
        self.reservations = ReservationRepository(session)
        self.ordering_sessions = QrOrderingSessionRepository(session)

    async def create(
        self, tenant_id: UUID, body: TableAssignmentCreate, *, actor: CurrentUser | None = None
    ) -> TableAssignment:
        await self._require_venue(tenant_id, body.venue_id)
        await self._require_table(tenant_id, body.table_id)
        if body.reservation_id is not None:
            if await self.reservations.get(tenant_id, body.reservation_id) is None:
                raise NotFoundError("رزرو یافت نشد", error_code="reservation_not_found")
        if body.ordering_session_id is not None:
            if await self.ordering_sessions.get(tenant_id, body.ordering_session_id) is None:
                raise NotFoundError(
                    "جلسه سفارش‌گیری یافت نشد", error_code="qr_ordering_session_not_found"
                )
        active = await self.repo.list_active_for_table(tenant_id, body.table_id)
        if active:
            raise AppError(
                "میز در حال حاضر تخصیص فعال دارد",
                status_code=409,
                error_code="table_assignment_active_exists",
            )
        actor_id = actor.user_id if actor else None
        entity = TableAssignment(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            table_id=body.table_id,
            reservation_id=body.reservation_id,
            ordering_session_id=body.ordering_session_id,
            status=TableAssignmentStatus.ACTIVE,
            assigned_at=datetime.now(timezone.utc),
            guest_label=body.guest_label,
            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="table_assignment",
            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=HospitalityEventType.TABLE_ASSIGNMENT_CREATED,
            aggregate_type="table_assignment",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"table_id": str(entity.table_id), "status": entity.status.value},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> TableAssignment:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("تخصیص میز یافت نشد", error_code="table_assignment_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 release(
        self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None
    ) -> TableAssignment:
        entity = await self.get(tenant_id, entity_id)
        if entity.status != TableAssignmentStatus.ACTIVE:
            raise AppError(
                "تخصیص میز فعال نیست", status_code=409, error_code="table_assignment_not_active"
            )
        actor_id = actor.user_id if actor else None
        entity.status = TableAssignmentStatus.RELEASED
        entity.released_at = datetime.now(timezone.utc)
        entity.updated_by = actor_id
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="table_assignment",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=actor_id,
            changes={"status": entity.status},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class ServiceRequestService(_TableServiceBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = ServiceRequestRepository(session)
        self.table_assignments = TableAssignmentRepository(session)

    async def create(
        self, tenant_id: UUID, body: ServiceRequestCreate, *, actor: CurrentUser | None = None
    ) -> ServiceRequest:
        await self._require_venue(tenant_id, body.venue_id)
        await self._require_table(tenant_id, body.table_id)
        if body.table_assignment_id is not None:
            if await self.table_assignments.get(tenant_id, body.table_assignment_id) is None:
                raise NotFoundError(
                    "تخصیص میز یافت نشد", error_code="table_assignment_not_found"
                )
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.venue_id, code) is not None:
            raise AppError(
                "کد درخواست خدمت تکراری است",
                status_code=409,
                error_code="service_request_code_exists",
            )
        kind = validate_service_request_kind(body.kind)
        actor_id = actor.user_id if actor else None
        entity = ServiceRequest(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            table_id=body.table_id,
            table_assignment_id=body.table_assignment_id,
            code=code,
            kind=kind,
            status=ServiceRequestStatus.OPEN,
            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="service_request",
            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=HospitalityEventType.SERVICE_REQUEST_CREATED,
            aggregate_type="service_request",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"kind": entity.kind.value, "table_id": str(entity.table_id)},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> ServiceRequest:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("درخواست خدمت یافت نشد", error_code="service_request_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 change_status(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        new_status,
        *,
        actor: CurrentUser | None = None,
    ) -> ServiceRequest:
        entity = await self.get(tenant_id, entity_id)
        resolved = validate_service_request_status(new_status)
        if entity.status in SERVICE_REQUEST_TERMINAL_STATUSES:
            raise AppError(
                "وضعیت درخواست خدمت نهایی شده و قابل تغییر نیست",
                status_code=409,
                error_code="service_request_status_final",
            )
        actor_id = actor.user_id if actor else None
        entity.status = resolved
        entity.updated_by = actor_id
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="service_request",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=actor_id,
            changes={"status": resolved},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class WaitlistEntryService(_TableServiceBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = WaitlistEntryRepository(session)

    async def create(
        self, tenant_id: UUID, body: WaitlistEntryCreate, *, actor: CurrentUser | None = None
    ) -> WaitlistEntry:
        await self._require_venue(tenant_id, body.venue_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.venue_id, code) is not None:
            raise AppError(
                "کد لیست انتظار تکراری است",
                status_code=409,
                error_code="waitlist_entry_code_exists",
            )
        party_size = validate_positive_int(body.party_size, field="party_size")
        actor_id = actor.user_id if actor else None
        entity = WaitlistEntry(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            code=code,
            guest_name=validate_non_empty(body.guest_name, field="guest_name"),
            party_size=party_size,
            status=WaitlistStatus.WAITING,
            phone=body.phone,
            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="waitlist_entry",
            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=HospitalityEventType.WAITLIST_ENTRY_CREATED,
            aggregate_type="waitlist_entry",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "party_size": entity.party_size},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> WaitlistEntry:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("مورد لیست انتظار یافت نشد", error_code="waitlist_entry_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 change_status(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        new_status,
        *,
        actor: CurrentUser | None = None,
    ) -> WaitlistEntry:
        entity = await self.get(tenant_id, entity_id)
        resolved = validate_waitlist_status(new_status)
        if entity.status in WAITLIST_TERMINAL_STATUSES:
            raise AppError(
                "وضعیت لیست انتظار نهایی شده و قابل تغییر نیست",
                status_code=409,
                error_code="waitlist_entry_status_final",
            )
        actor_id = actor.user_id if actor else None
        entity.status = resolved
        entity.updated_by = actor_id
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="waitlist_entry",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=actor_id,
            changes={"status": resolved},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity
