"""Doctor panel application services — Phase 13.2."""
from __future__ import annotations

from datetime import datetime, timezone
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.doctor_panel import VisitNote, VisitSession
from app.models.types import (
    AppointmentLifecycleAction,
    AppointmentStatus,
    AuditAction,
    VisitSessionAction,
    VisitSessionStatus,
)
from app.policies.doctor_panel import VisitSessionPolicy
from app.repositories.appointments import AppointmentRepository
from app.repositories.doctor_panel import VisitNoteRepository, VisitSessionRepository
from app.repositories.foundation import ClinicRepository
from app.repositories.profiles import DoctorRepository, PatientRepository
from app.schemas.appointments import AppointmentLifecycleRequest
from app.schemas.doctor_panel import (
    DoctorDashboardRead,
    DoctorQueueRead,
    VisitNoteCreate,
    VisitNoteUpdate,
    VisitSessionActionRequest,
    VisitSessionCreate,
)
from app.services.appointments import AppointmentService
from app.services.audit_service import AuditService
from app.services.foundation import _actor_id, _jsonable_changes
from app.specifications.appointments import AppointmentListSpec
from app.specifications.doctor_panel import VisitSessionListSpec
from app.validators import ensure_optimistic_version, validate_code
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


class VisitSessionService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = VisitSessionRepository(session)
        self.appointments = AppointmentRepository(session)
        self.appointment_service = AppointmentService(session)
        self.clinics = ClinicRepository(session)
        self.doctors = DoctorRepository(session)
        self.patients = PatientRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()
        self.policy = VisitSessionPolicy()

    async def _validate_refs(self, tenant_id: UUID, body: VisitSessionCreate) -> 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("بیمار یافت نشد")
        appt = await self.appointments.get(tenant_id, body.appointment_id)
        if appt is None:
            raise NotFoundError("نوبت یافت نشد")
        if appt.doctor_id != body.doctor_id or appt.patient_id != body.patient_id:
            raise AppError(
                "نوبت با پزشک/بیمار مطابقت ندارد",
                status_code=422,
                error_code="appointment_mismatch",
            )

    async def create(
        self, tenant_id: UUID, body: VisitSessionCreate, *, actor: CurrentUser | None = None
    ) -> VisitSession:
        await self._validate_refs(tenant_id, body)
        if await self.repo.get_by_appointment(tenant_id, body.appointment_id):
            raise AppError(
                "جلسه ویزیت برای این نوبت وجود دارد",
                status_code=409,
                error_code="visit_session_exists",
            )
        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 = VisitSession(
            tenant_id=tenant_id,
            clinic_id=body.clinic_id,
            doctor_id=body.doctor_id,
            patient_id=body.patient_id,
            appointment_id=body.appointment_id,
            code=code,
            status=VisitSessionStatus.QUEUED,
            queue_position=body.queue_position,
            metadata_json=body.metadata_json,
            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="visit_session",
            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.VISIT_SESSION_CREATED,
            aggregate_type="visit_session",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "doctor_id": str(entity.doctor_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, visit_session_id: UUID) -> VisitSession:
        entity = await self.repo.get(tenant_id, visit_session_id)
        if entity is None:
            raise NotFoundError("جلسه ویزیت یافت نشد")
        return entity

    async def list(
        self,
        tenant_id: UUID,
        *,
        offset: int = 0,
        limit: int = 20,
        spec: VisitSessionListSpec | None = None,
    ) -> list[VisitSession]:
        return list(await self.repo.list_filtered(tenant_id, offset=offset, limit=limit, spec=spec))

    async def _apply_action(
        self,
        tenant_id: UUID,
        visit_session_id: UUID,
        action: VisitSessionAction,
        body: VisitSessionActionRequest,
        *,
        actor: CurrentUser | None = None,
        event_type: HealthcareEventType,
    ) -> VisitSession:
        entity = await self.get(tenant_id, visit_session_id)
        ensure_optimistic_version(entity, body.version)
        new_status = self.policy.ensure_transition(action=action, current=entity.status)
        now = datetime.now(timezone.utc)
        if action == VisitSessionAction.START:
            entity.started_at = now
            appt = await self.appointments.get(tenant_id, entity.appointment_id)
            if appt and appt.status == AppointmentStatus.CONFIRMED:
                await self.appointment_service._apply_lifecycle(
                    tenant_id,
                    entity.appointment_id,
                    AppointmentLifecycleAction.CHECK_IN,
                    AppointmentLifecycleRequest(version=appt.version),
                    actor=actor,
                    event_type=HealthcareEventType.APPOINTMENT_CHECKED_IN,
                )
        elif action == VisitSessionAction.FINISH:
            entity.finished_at = now
            appt = await self.appointments.get(tenant_id, entity.appointment_id)
            if appt and appt.status in (
                AppointmentStatus.CONFIRMED,
                AppointmentStatus.CHECKED_IN,
            ):
                await self.appointment_service._apply_lifecycle(
                    tenant_id,
                    entity.appointment_id,
                    AppointmentLifecycleAction.COMPLETE,
                    AppointmentLifecycleRequest(version=appt.version),
                    actor=actor,
                    event_type=HealthcareEventType.APPOINTMENT_COMPLETED,
                )
        entity.status = new_status
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="visit_session",
            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="visit_session",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "status": new_status.value},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def start(
        self,
        tenant_id: UUID,
        visit_session_id: UUID,
        body: VisitSessionActionRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> VisitSession:
        return await self._apply_action(
            tenant_id,
            visit_session_id,
            VisitSessionAction.START,
            body,
            actor=actor,
            event_type=HealthcareEventType.VISIT_SESSION_STARTED,
        )

    async def finish(
        self,
        tenant_id: UUID,
        visit_session_id: UUID,
        body: VisitSessionActionRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> VisitSession:
        return await self._apply_action(
            tenant_id,
            visit_session_id,
            VisitSessionAction.FINISH,
            body,
            actor=actor,
            event_type=HealthcareEventType.VISIT_SESSION_FINISHED,
        )

    async def defer(
        self,
        tenant_id: UUID,
        visit_session_id: UUID,
        body: VisitSessionActionRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> VisitSession:
        return await self._apply_action(
            tenant_id,
            visit_session_id,
            VisitSessionAction.DEFER,
            body,
            actor=actor,
            event_type=HealthcareEventType.VISIT_SESSION_DEFERRED,
        )

    async def call_next(
        self,
        tenant_id: UUID,
        visit_session_id: UUID,
        body: VisitSessionActionRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> VisitSession:
        entity = await self.get(tenant_id, visit_session_id)
        ensure_optimistic_version(entity, body.version)
        self.policy.ensure_transition(action=VisitSessionAction.CALL_NEXT, current=entity.status)
        entity.queue_position = 1
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        self.events.publish(
            event_type=HealthcareEventType.VISIT_SESSION_CALLED,
            aggregate_type="visit_session",
            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 dashboard(self, tenant_id: UUID, *, doctor_id: UUID) -> DoctorDashboardRead:
        if await self.doctors.get(tenant_id, doctor_id) is None:
            raise NotFoundError("پزشک یافت نشد")
        queued = await self.repo.count_by_status(
            tenant_id, doctor_id=doctor_id, status=VisitSessionStatus.QUEUED
        )
        in_progress = await self.repo.count_by_status(
            tenant_id, doctor_id=doctor_id, status=VisitSessionStatus.IN_PROGRESS
        )
        completed_today = await self.repo.count_completed_today(tenant_id, doctor_id=doctor_id)
        appt_spec = AppointmentListSpec(doctor_id=doctor_id, sort_by="scheduled_start", sort_dir="asc")
        upcoming, _ = await self.appointment_service.list(
            tenant_id, offset=0, limit=10, spec=appt_spec
        )
        return DoctorDashboardRead(
            doctor_id=doctor_id,
            queued_count=queued,
            in_progress_count=in_progress,
            completed_today_count=completed_today,
            upcoming_appointments=upcoming,
        )

    async def queue(self, tenant_id: UUID, *, doctor_id: UUID) -> DoctorQueueRead:
        spec = VisitSessionListSpec(
            doctor_id=doctor_id,
            status=VisitSessionStatus.QUEUED,
            sort_by="queue_position",
            sort_dir="asc",
        )
        items = await self.list(tenant_id, offset=0, limit=100, spec=spec)
        return DoctorQueueRead(doctor_id=doctor_id, items=items)


class VisitNoteService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = VisitNoteRepository(session)
        self.sessions = VisitSessionRepository(session)
        self.doctors = DoctorRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self, tenant_id: UUID, body: VisitNoteCreate, *, actor: CurrentUser | None = None
    ) -> VisitNote:
        if await self.sessions.get(tenant_id, body.visit_session_id) is None:
            raise NotFoundError("جلسه ویزیت یافت نشد")
        if await self.doctors.get(tenant_id, body.doctor_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 = VisitNote(
            tenant_id=tenant_id,
            visit_session_id=body.visit_session_id,
            doctor_id=body.doctor_id,
            code=code,
            title=body.title,
            body=body.body,
            storage_file_ref=body.storage_file_ref,
            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.VISIT_NOTE_CREATED,
            aggregate_type="visit_note",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "visit_session_id": str(entity.visit_session_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def update(
        self,
        tenant_id: UUID,
        note_id: UUID,
        body: VisitNoteUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> VisitNote:
        entity = await self.repo.get(tenant_id, note_id)
        if entity is None:
            raise NotFoundError("یادداشت ویزیت یافت نشد")
        ensure_optimistic_version(entity, body.version)
        if body.title is not None:
            entity.title = body.title
        if body.body is not None:
            entity.body = body.body
        if body.storage_file_ref is not None:
            entity.storage_file_ref = body.storage_file_ref
        if body.metadata_json is not None:
            entity.metadata_json = body.metadata_json
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        self.events.publish(
            event_type=HealthcareEventType.VISIT_NOTE_UPDATED,
            aggregate_type="visit_note",
            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_by_session(self, tenant_id: UUID, visit_session_id: UUID) -> list[VisitNote]:
        if await self.sessions.get(tenant_id, visit_session_id) is None:
            raise NotFoundError("جلسه ویزیت یافت نشد")
        return list(await self.repo.list_by_session(tenant_id, visit_session_id))
