"""Appointment application services — Phase 13.1."""
from __future__ import annotations

from datetime import datetime, timezone
from typing import Any
from uuid import UUID

from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import get_event_publisher
from app.events.types import HealthcareEventType
from app.models.appointments import (
    Appointment,
    AppointmentType,
    AvailabilityWindow,
    Schedule,
    WaitingListEntry,
)
from app.models.types import AppointmentLifecycleAction, AppointmentStatus, AuditAction
from app.policies.appointments import AppointmentLifecyclePolicy
from app.providers.mocks import MockCommunicationProvider
from app.repositories.appointments import (
    AppointmentRepository,
    AppointmentTypeRepository,
    AvailabilityWindowRepository,
    ScheduleRepository,
    WaitingListEntryRepository,
)
from app.repositories.foundation import ClinicRepository, DepartmentRepository
from app.repositories.profiles import DoctorRepository, PatientRepository
from app.schemas.appointments import (
    AppointmentCreate,
    AppointmentLifecycleRequest,
    AppointmentReschedule,
    AppointmentTypeCreate,
    AvailabilityWindowCreate,
    ScheduleCreate,
    WaitingListEntryCreate,
)
from app.services.audit_service import AuditService
from app.services.foundation import _actor_id, _jsonable_changes
from app.specifications.appointments import AppointmentListSpec
from app.validators import ensure_optimistic_version, validate_code
from app.validators.appointments import validate_schedule_times, validate_time_window
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


class ScheduleService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ScheduleRepository(session)
        self.clinics = ClinicRepository(session)
        self.doctors = DoctorRepository(session)
        self.departments = DepartmentRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self, tenant_id: UUID, body: ScheduleCreate, *, actor: CurrentUser | None = None
    ) -> Schedule:
        if await self.clinics.get(tenant_id, body.clinic_id) is None:
            raise NotFoundError("کلینیک یافت نشد")
        if await self.doctors.get(tenant_id, body.doctor_id) is None:
            raise NotFoundError("پزشک یافت نشد")
        if body.department_id and await self.departments.get(tenant_id, body.department_id) is None:
            raise NotFoundError("بخش یافت نشد")
        code = validate_code(body.code)
        validate_schedule_times(start_time=body.start_time, end_time=body.end_time)
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError("کد تکراری است", error_code="duplicate_code", status_code=409)
        entity = Schedule(
            tenant_id=tenant_id,
            clinic_id=body.clinic_id,
            doctor_id=body.doctor_id,
            department_id=body.department_id,
            code=code,
            name=body.name,
            day_of_week=body.day_of_week,
            start_time=body.start_time,
            end_time=body.end_time,
            slot_duration_minutes=body.slot_duration_minutes,
            is_active=body.is_active,
            metadata_json=body.metadata_json,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.repo.add(entity)
        self.events.publish(
            event_type=HealthcareEventType.SCHEDULE_CREATED,
            aggregate_type="schedule",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list(self, tenant_id: UUID, *, offset: int = 0, limit: int = 20) -> list[Schedule]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))


class AvailabilityWindowService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = AvailabilityWindowRepository(session)
        self.clinics = ClinicRepository(session)
        self.doctors = DoctorRepository(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: AvailabilityWindowCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> AvailabilityWindow:
        if await self.clinics.get(tenant_id, body.clinic_id) is None:
            raise NotFoundError("کلینیک یافت نشد")
        if await self.doctors.get(tenant_id, body.doctor_id) is None:
            raise NotFoundError("پزشک یافت نشد")
        code = validate_code(body.code)
        validate_time_window(starts_at=body.starts_at, ends_at=body.ends_at)
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError("کد تکراری است", error_code="duplicate_code", status_code=409)
        entity = AvailabilityWindow(
            tenant_id=tenant_id,
            clinic_id=body.clinic_id,
            doctor_id=body.doctor_id,
            code=code,
            starts_at=body.starts_at,
            ends_at=body.ends_at,
            is_bookable=body.is_bookable,
            metadata_json=body.metadata_json,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.repo.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[AvailabilityWindow]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))


class AppointmentTypeService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = AppointmentTypeRepository(session)
        self.clinics = ClinicRepository(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: AppointmentTypeCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> AppointmentType:
        if await self.clinics.get(tenant_id, body.clinic_id) is None:
            raise NotFoundError("کلینیک یافت نشد")
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError("کد تکراری است", error_code="duplicate_code", status_code=409)
        entity = AppointmentType(
            tenant_id=tenant_id,
            clinic_id=body.clinic_id,
            code=code,
            name=body.name,
            description=body.description,
            default_duration_minutes=body.default_duration_minutes,
            status=body.status,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.repo.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[AppointmentType]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))


class AppointmentService:
    def __init__(
        self,
        session: AsyncSession,
        *,
        communication: MockCommunicationProvider | None = None,
    ) -> None:
        self.session = session
        self.repo = AppointmentRepository(session)
        self.clinics = ClinicRepository(session)
        self.doctors = DoctorRepository(session)
        self.patients = PatientRepository(session)
        self.departments = DepartmentRepository(session)
        self.types = AppointmentTypeRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()
        self.policy = AppointmentLifecyclePolicy()
        self.communication = communication or MockCommunicationProvider()

    async def _validate_refs(self, tenant_id: UUID, body: AppointmentCreate) -> None:
        if await self.clinics.get(tenant_id, body.clinic_id) is None:
            raise NotFoundError("کلینیک یافت نشد")
        if await self.doctors.get(tenant_id, body.doctor_id) is None:
            raise NotFoundError("پزشک یافت نشد")
        if await self.patients.get(tenant_id, body.patient_id) is None:
            raise NotFoundError("بیمار یافت نشد")
        if body.department_id and await self.departments.get(tenant_id, body.department_id) is None:
            raise NotFoundError("بخش یافت نشد")
        if body.appointment_type_id and await self.types.get(tenant_id, body.appointment_type_id) is None:
            raise NotFoundError("نوع نوبت یافت نشد")

    async def _ensure_no_conflict(
        self,
        tenant_id: UUID,
        *,
        doctor_id: UUID,
        scheduled_start: datetime,
        scheduled_end: datetime,
        exclude_id: UUID | None = None,
    ) -> None:
        conflicts = await self.repo.find_conflicts(
            tenant_id,
            doctor_id=doctor_id,
            scheduled_start=scheduled_start,
            scheduled_end=scheduled_end,
            exclude_id=exclude_id,
        )
        if conflicts:
            raise AppError(
                "تداخل زمانی با نوبت دیگر",
                status_code=409,
                error_code="appointment_conflict",
                details={"conflicting_ids": [str(c.id) for c in conflicts]},
            )

    async def book(
        self, tenant_id: UUID, body: AppointmentCreate, *, actor: CurrentUser | None = None
    ) -> Appointment:
        validate_time_window(starts_at=body.scheduled_start, ends_at=body.scheduled_end)
        await self._validate_refs(tenant_id, body)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError("کد تکراری است", error_code="duplicate_code", status_code=409)
        await self._ensure_no_conflict(
            tenant_id,
            doctor_id=body.doctor_id,
            scheduled_start=body.scheduled_start,
            scheduled_end=body.scheduled_end,
        )
        entity = Appointment(
            tenant_id=tenant_id,
            clinic_id=body.clinic_id,
            doctor_id=body.doctor_id,
            patient_id=body.patient_id,
            department_id=body.department_id,
            appointment_type_id=body.appointment_type_id,
            code=code,
            status=AppointmentStatus.DRAFT,
            scheduled_start=body.scheduled_start,
            scheduled_end=body.scheduled_end,
            notes=body.notes,
            metadata_json=body.metadata_json,
            status_changed_at=datetime.now(timezone.utc),
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        try:
            await self.repo.add(entity)
        except IntegrityError as exc:
            raise AppError("کد تکراری است", error_code="duplicate_code", status_code=409) from exc
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="appointment",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=HealthcareEventType.APPOINTMENT_BOOKED,
            aggregate_type="appointment",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "status": entity.status.value},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, appointment_id: UUID) -> Appointment:
        entity = await self.repo.get(tenant_id, appointment_id)
        if entity is None:
            raise NotFoundError("نوبت یافت نشد")
        return entity

    async def list(
        self,
        tenant_id: UUID,
        *,
        offset: int = 0,
        limit: int = 20,
        spec: AppointmentListSpec | None = None,
    ) -> tuple[list[Appointment], int]:
        items = list(await self.repo.list_filtered(tenant_id, offset=offset, limit=limit, spec=spec))
        total = await self.repo.count_filtered(tenant_id, spec=spec)
        return items, total

    async def _apply_lifecycle(
        self,
        tenant_id: UUID,
        appointment_id: UUID,
        action: AppointmentLifecycleAction,
        body: AppointmentLifecycleRequest,
        *,
        actor: CurrentUser | None = None,
        event_type: HealthcareEventType,
    ) -> Appointment:
        entity = await self.get(tenant_id, appointment_id)
        ensure_optimistic_version(entity, body.version)
        new_status = self.policy.ensure_transition(action=action, current=entity.status)
        entity.status = new_status
        entity.status_reason = body.reason
        entity.status_changed_at = datetime.now(timezone.utc)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="appointment",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=_actor_id(actor),
            changes={"action": action.value, "status": new_status.value},
        )
        self.events.publish(
            event_type=event_type,
            aggregate_type="appointment",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "status": new_status.value},
        )
        if action == AppointmentLifecycleAction.CONFIRM and not entity.reminder_scheduled:
            patient = await self.patients.get(tenant_id, entity.patient_id)
            to = (patient.contact or {}).get("phone", "unknown") if patient else "unknown"
            await self.communication.send(
                tenant_id=tenant_id,
                channel="sms",
                template_key="healthcare.appointment.reminder",
                to=to,
                payload={"appointment_id": str(entity.id), "code": entity.code},
            )
            entity.reminder_scheduled = True
            self.events.publish(
                event_type=HealthcareEventType.APPOINTMENT_REMINDER_SCHEDULED,
                aggregate_type="appointment",
                aggregate_id=entity.id,
                tenant_id=tenant_id,
                payload={"code": entity.code},
            )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def confirm(
        self, tenant_id: UUID, appointment_id: UUID, body: AppointmentLifecycleRequest, *, actor=None
    ) -> Appointment:
        return await self._apply_lifecycle(
            tenant_id,
            appointment_id,
            AppointmentLifecycleAction.CONFIRM,
            body,
            actor=actor,
            event_type=HealthcareEventType.APPOINTMENT_CONFIRMED,
        )

    async def cancel(
        self, tenant_id: UUID, appointment_id: UUID, body: AppointmentLifecycleRequest, *, actor=None
    ) -> Appointment:
        return await self._apply_lifecycle(
            tenant_id,
            appointment_id,
            AppointmentLifecycleAction.CANCEL,
            body,
            actor=actor,
            event_type=HealthcareEventType.APPOINTMENT_CANCELLED,
        )

    async def mark_no_show(
        self, tenant_id: UUID, appointment_id: UUID, body: AppointmentLifecycleRequest, *, actor=None
    ) -> Appointment:
        return await self._apply_lifecycle(
            tenant_id,
            appointment_id,
            AppointmentLifecycleAction.MARK_NO_SHOW,
            body,
            actor=actor,
            event_type=HealthcareEventType.APPOINTMENT_NO_SHOW,
        )

    async def reschedule(
        self, tenant_id: UUID, appointment_id: UUID, body: AppointmentReschedule, *, actor=None
    ) -> Appointment:
        entity = await self.get(tenant_id, appointment_id)
        ensure_optimistic_version(entity, body.version)
        self.policy.ensure_transition(
            action=AppointmentLifecycleAction.RESCHEDULE, current=entity.status
        )
        validate_time_window(starts_at=body.scheduled_start, ends_at=body.scheduled_end)
        await self._ensure_no_conflict(
            tenant_id,
            doctor_id=entity.doctor_id,
            scheduled_start=body.scheduled_start,
            scheduled_end=body.scheduled_end,
            exclude_id=entity.id,
        )
        entity.scheduled_start = body.scheduled_start
        entity.scheduled_end = body.scheduled_end
        entity.status_reason = body.reason
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        self.events.publish(
            event_type=HealthcareEventType.APPOINTMENT_RESCHEDULED,
            aggregate_type="appointment",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class WaitingListService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = WaitingListEntryRepository(session)
        self.clinics = ClinicRepository(session)
        self.patients = PatientRepository(session)

    async def create(
        self,
        tenant_id: UUID,
        body: WaitingListEntryCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> WaitingListEntry:
        if await self.clinics.get(tenant_id, body.clinic_id) is None:
            raise NotFoundError("کلینیک یافت نشد")
        if await self.patients.get(tenant_id, body.patient_id) is None:
            raise NotFoundError("بیمار یافت نشد")
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError("کد تکراری است", error_code="duplicate_code", status_code=409)
        entity = WaitingListEntry(
            tenant_id=tenant_id,
            clinic_id=body.clinic_id,
            doctor_id=body.doctor_id,
            patient_id=body.patient_id,
            appointment_type_id=body.appointment_type_id,
            code=code,
            preferred_date=body.preferred_date,
            notes=body.notes,
            priority=body.priority,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.repo.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[WaitingListEntry]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))
