"""Scheduling & Booking services — Phase 9.4."""
from __future__ import annotations

from datetime import 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.booking import Booking, Equipment, ResourceReservation, SportsSession, WaitingListEntry
from app.models.types import AuditAction, BookingStatus, LifecycleStatus, WaitlistStatus
from app.repositories.booking import (
    BookingRepository,
    EquipmentRepository,
    ResourceReservationRepository,
    SportsSessionRepository,
    WaitingListEntryRepository,
)
from app.repositories.foundation import SportsCenterRepository
from app.repositories.members import MemberRepository
from app.schemas.booking import (
    BookingCancelRequest,
    BookingCreate,
    EquipmentCreate,
    EquipmentUpdate,
    ResourceReservationCreate,
    SportsSessionCreate,
    SportsSessionUpdate,
    WaitingListCreate,
)
from app.services.audit_service import AuditService
from app.validators import ensure_optimistic_version, validate_code, validate_non_empty, validate_non_negative_int
from app.validators.staff import validate_datetime_window
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


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


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


class _BookingBase:
    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)


class EquipmentService(_BookingBase):
    def __init__(self, session: AsyncSession, publisher=None) -> None:
        super().__init__(session, publisher)
        self.repo = EquipmentRepository(session)

    async def create(self, tenant_id: UUID, body: EquipmentCreate, *, actor: CurrentUser):
        await self._require_center(tenant_id, body.sports_center_id)
        validate_code(body.code)
        validate_non_empty(body.name, field="name")
        entity = Equipment(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            branch_id=body.branch_id,
            facility_id=body.facility_id,
            code=body.code,
            name=body.name,
            description=body.description,
            status=body.status,
            quantity_total=body.quantity_total,
            quantity_available=body.quantity_available,
            attributes=body.attributes,
            created_by=actor.user_id if actor else None,
            updated_by=actor.user_id if actor else None,
        )
        await self.repo.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            tenant_id=tenant_id,
            event_type=SportsCenterEventType.EQUIPMENT_CREATED,
            aggregate_type="equipment",
            aggregate_id=entity.id,
            payload={"code": body.code},
        )
        return entity

    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 list(self, tenant_id: UUID, *, offset=0, limit=20):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    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("تجهیزات یافت نشد", error_code="equipment_not_found")
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: EquipmentUpdate, *, actor: CurrentUser):
        entity = await self.get(tenant_id, entity_id)
        _apply_update(entity, body.model_dump(exclude_unset=True))
        entity.updated_by = actor.user_id if actor else None
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser):
        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.session.commit()
        return entity


class SportsSessionService(_BookingBase):
    def __init__(self, session: AsyncSession, publisher=None) -> None:
        super().__init__(session, publisher)
        self.repo = SportsSessionRepository(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 _ensure_no_conflicts(
        self,
        tenant_id: UUID,
        sports_center_id: UUID,
        starts_at: datetime,
        ends_at: datetime,
        *,
        coach_id=None,
        facility_id=None,
        exclude_id=None,
    ) -> None:
        validate_datetime_window(starts_at, ends_at)
        conflicts = await self.repo.count_conflicts(
            tenant_id,
            sports_center_id,
            starts_at,
            ends_at,
            coach_id=coach_id,
            facility_id=facility_id,
            exclude_id=exclude_id,
        )
        if conflicts:
            raise AppError("تداخل زمانی جلسه وجود دارد", status_code=409, error_code="session_conflict")

    async def create(self, tenant_id: UUID, body: SportsSessionCreate, *, actor: CurrentUser):
        await self._require_center(tenant_id, body.sports_center_id)
        validate_code(body.code)
        validate_non_empty(body.title, field="title")
        validate_non_negative_int(body.capacity, field="capacity")
        await self._ensure_no_conflicts(
            tenant_id,
            body.sports_center_id,
            body.starts_at,
            body.ends_at,
            coach_id=body.coach_id,
            facility_id=body.facility_id,
        )
        entity = SportsSession(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            branch_id=body.branch_id,
            code=body.code,
            title=body.title,
            description=body.description,
            session_kind=body.session_kind,
            sport_id=body.sport_id,
            coach_id=body.coach_id,
            facility_id=body.facility_id,
            court_id=body.court_id,
            room_id=body.room_id,
            starts_at=body.starts_at,
            ends_at=body.ends_at,
            capacity=body.capacity,
            recurrence_kind=body.recurrence_kind,
            recurrence_rule=body.recurrence_rule,
            status=body.status,
            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.repo.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            tenant_id=tenant_id,
            event_type=SportsCenterEventType.SESSION_CREATED,
            aggregate_type="sports_session",
            aggregate_id=entity.id,
            payload={"code": body.code},
        )
        return entity

    async def list(self, tenant_id: UUID, sports_center_id: UUID, *, offset=0, limit=20):
        return await self.repo.list_by_center(tenant_id, sports_center_id, offset=offset, limit=limit)

    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("جلسه یافت نشد", error_code="session_not_found")
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: SportsSessionUpdate, *, actor: CurrentUser):
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        starts = data.get("starts_at", entity.starts_at)
        ends = data.get("ends_at", entity.ends_at)
        coach_id = data.get("coach_id", entity.coach_id)
        facility_id = data.get("facility_id", entity.facility_id)
        await self._ensure_no_conflicts(
            tenant_id,
            entity.sports_center_id,
            starts,
            ends,
            coach_id=coach_id,
            facility_id=facility_id,
            exclude_id=entity.id,
        )
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        entity.version += 1
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            tenant_id=tenant_id,
            event_type=SportsCenterEventType.SESSION_UPDATED,
            aggregate_type="sports_session",
            aggregate_id=entity.id,
            payload={"code": entity.code},
        )
        return entity

    async def delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser):
        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.session.commit()
        return entity


class BookingService(_BookingBase):
    def __init__(self, session: AsyncSession, publisher=None) -> None:
        super().__init__(session, publisher)
        self.repo = BookingRepository(session)
        self.sessions = SportsSessionRepository(session)
        self.waitlist = WaitingListEntryRepository(session)
        self.members = MemberRepository(session)

    async def create(self, tenant_id: UUID, body: BookingCreate, *, actor: CurrentUser):
        session = await self.sessions.get(tenant_id, body.session_id)
        if session is None:
            raise NotFoundError("جلسه یافت نشد", error_code="session_not_found")
        member = await self.members.get(tenant_id, body.member_id)
        if member is None:
            raise NotFoundError("عضو یافت نشد", error_code="member_not_found")
        active = await self.repo.count_active_for_session(tenant_id, body.session_id)
        if session.capacity > 0 and active >= session.capacity:
            raise AppError("ظرفیت جلسه تکمیل است", error_code="session_full")
        entity = Booking(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            session_id=body.session_id,
            member_id=body.member_id,
            membership_id=body.membership_id,
            booking_number=_next_booking_number(),
            status=BookingStatus.PENDING,
            notes=body.notes,
            created_by=actor.user_id if actor else None,
            updated_by=actor.user_id if actor else None,
        )
        await self.repo.add(entity)
        session.booked_count = active + 1
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            tenant_id=tenant_id,
            event_type=SportsCenterEventType.BOOKING_CREATED,
            aggregate_type="booking",
            aggregate_id=entity.id,
            payload={"session_id": str(body.session_id), "member_id": str(body.member_id)},
        )
        return entity

    async def confirm(self, tenant_id: UUID, booking_id: UUID, *, actor: CurrentUser):
        entity = await self.get(tenant_id, booking_id)
        if entity.status != BookingStatus.PENDING:
            raise AppError("رزرو قابل تأیید نیست", error_code="invalid_booking_status")
        entity.status = BookingStatus.CONFIRMED
        entity.confirmed_at = datetime.now(timezone.utc)
        entity.updated_by = actor.user_id if actor else None
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            tenant_id=tenant_id,
            event_type=SportsCenterEventType.BOOKING_CONFIRMED,
            aggregate_type="booking",
            aggregate_id=entity.id,
            payload={"booking_number": entity.booking_number},
        )
        return entity

    async def cancel(
        self,
        tenant_id: UUID,
        booking_id: UUID,
        body: BookingCancelRequest,
        *,
        actor: CurrentUser,
    ):
        entity = await self.get(tenant_id, booking_id)
        if entity.status in (BookingStatus.CANCELLED, BookingStatus.COMPLETED):
            raise AppError("رزرو قابل لغو نیست", error_code="invalid_booking_status")
        entity.status = BookingStatus.CANCELLED
        entity.cancelled_at = datetime.now(timezone.utc)
        entity.cancellation_reason = body.cancellation_reason
        entity.updated_by = actor.user_id if actor else None
        session = await self.sessions.get(tenant_id, entity.session_id)
        if session and session.booked_count > 0:
            session.booked_count -= 1
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            tenant_id=tenant_id,
            event_type=SportsCenterEventType.BOOKING_CANCELLED,
            aggregate_type="booking",
            aggregate_id=entity.id,
            payload={"booking_number": entity.booking_number},
        )
        return entity

    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("رزرو یافت نشد", error_code="booking_not_found")
        return entity

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


class WaitingListService(_BookingBase):
    def __init__(self, session: AsyncSession, publisher=None) -> None:
        super().__init__(session, publisher)
        self.repo = WaitingListEntryRepository(session)
        self.sessions = SportsSessionRepository(session)
        self.members = MemberRepository(session)

    async def join(self, tenant_id: UUID, body: WaitingListCreate, *, actor: CurrentUser):
        session = await self.sessions.get(tenant_id, body.session_id)
        if session is None:
            raise NotFoundError("جلسه یافت نشد", error_code="session_not_found")
        member = await self.members.get(tenant_id, body.member_id)
        if member is None:
            raise NotFoundError("عضو یافت نشد", error_code="member_not_found")
        position = await self.repo.next_position(tenant_id, body.session_id)
        entity = WaitingListEntry(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            session_id=body.session_id,
            member_id=body.member_id,
            position=position,
            status=WaitlistStatus.WAITING,
            notes=body.notes,
            created_by=actor.user_id if actor else None,
            updated_by=actor.user_id if actor else None,
        )
        await self.repo.add(entity)
        session.waitlist_count += 1
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            tenant_id=tenant_id,
            event_type=SportsCenterEventType.WAITING_LIST_JOINED,
            aggregate_type="waiting_list_entry",
            aggregate_id=entity.id,
            payload={"session_id": str(body.session_id), "position": position},
        )
        return entity

    async def list(self, tenant_id: UUID, session_id: UUID, *, offset=0, limit=20):
        stmt_entries = await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)
        return [e for e in stmt_entries if e.session_id == session_id]


class ResourceReservationService(_BookingBase):
    def __init__(self, session: AsyncSession, publisher=None) -> None:
        super().__init__(session, publisher)
        self.repo = ResourceReservationRepository(session)

    async def create(self, tenant_id: UUID, body: ResourceReservationCreate, *, actor: CurrentUser):
        validate_datetime_window(body.starts_at, body.ends_at)
        conflicts = await self.repo.count_conflicts(
            tenant_id,
            body.starts_at,
            body.ends_at,
            facility_id=body.facility_id,
            equipment_id=body.equipment_id,
        )
        if conflicts:
            raise AppError("تداخل رزرو منبع وجود دارد", error_code="resource_conflict")
        entity = ResourceReservation(
            tenant_id=tenant_id,
            sports_center_id=body.sports_center_id,
            session_id=body.session_id,
            booking_id=body.booking_id,
            facility_id=body.facility_id,
            equipment_id=body.equipment_id,
            starts_at=body.starts_at,
            ends_at=body.ends_at,
            quantity=body.quantity,
            status=BookingStatus.CONFIRMED,
            notes=body.notes,
            created_by=actor.user_id if actor else None,
            updated_by=actor.user_id if actor else None,
        )
        await self.repo.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

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

    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("رزرو منبع یافت نشد", error_code="resource_reservation_not_found")
        return entity

    async def delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser):
        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.session.commit()
        return entity
