"""Patient portal application services — Phase 13.4."""
from __future__ import annotations

from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import get_event_publisher
from app.events.types import HealthcareEventType
from app.models.patient_portal import (
    PatientDocumentRef,
    PatientNotificationPreference,
    PatientPortalProfile,
)
from app.repositories.foundation import ClinicRepository
from app.repositories.patient_portal import (
    PatientDocumentRefRepository,
    PatientNotificationPreferenceRepository,
    PatientPortalProfileRepository,
)
from app.repositories.profiles import PatientRepository
from app.schemas.appointments import AppointmentCreate, AppointmentLifecycleRequest
from app.schemas.patient_portal import (
    PatientDocumentRefCreate,
    PatientNotificationPreferenceUpsert,
    PatientPortalProfileCreate,
    PatientPortalProfileUpdate,
)
from app.services.appointments import AppointmentService
from app.services.foundation import _actor_id
from app.specifications.appointments import AppointmentListSpec
from app.validators import ensure_optimistic_version, validate_code
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


class PatientPortalService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.profiles = PatientPortalProfileRepository(session)
        self.documents = PatientDocumentRefRepository(session)
        self.prefs = PatientNotificationPreferenceRepository(session)
        self.patients = PatientRepository(session)
        self.clinics = ClinicRepository(session)
        self.appointments = AppointmentService(session)
        self.events = get_event_publisher()

    async def get_or_create_profile(
        self, tenant_id: UUID, patient_id: UUID, *, actor: CurrentUser | None = None
    ) -> PatientPortalProfile:
        existing = await self.profiles.get_by_patient(tenant_id, patient_id)
        if existing:
            return existing
        body = PatientPortalProfileCreate(patient_id=patient_id)
        return await self.create_profile(tenant_id, body, actor=actor)

    async def create_profile(
        self, tenant_id: UUID, body: PatientPortalProfileCreate, *, actor: CurrentUser | None = None
    ) -> PatientPortalProfile:
        if await self.patients.get(tenant_id, body.patient_id) is None:
            raise NotFoundError("بیمار یافت نشد")
        if await self.profiles.get_by_patient(tenant_id, body.patient_id):
            raise AppError("پروفایل پرتال وجود دارد", status_code=409, error_code="profile_exists")
        if body.preferred_clinic_id and await self.clinics.get(tenant_id, body.preferred_clinic_id) is None:
            raise NotFoundError("کلینیک یافت نشد")
        entity = PatientPortalProfile(
            tenant_id=tenant_id,
            patient_id=body.patient_id,
            preferred_clinic_id=body.preferred_clinic_id,
            preferred_language=body.preferred_language,
            emergency_contact_ref=body.emergency_contact_ref,
            notification_prefs=body.notification_prefs,
            metadata_json=body.metadata_json,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.profiles.add(entity)
        self.events.publish(
            event_type=HealthcareEventType.PATIENT_PORTAL_PROFILE_CREATED,
            aggregate_type="patient_portal_profile",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"patient_id": str(entity.patient_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def update_profile(
        self,
        tenant_id: UUID,
        patient_id: UUID,
        body: PatientPortalProfileUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> PatientPortalProfile:
        entity = await self.profiles.get_by_patient(tenant_id, patient_id)
        if entity is None:
            raise NotFoundError("پروفایل پرتال یافت نشد")
        ensure_optimistic_version(entity, body.version)
        if body.preferred_clinic_id is not None:
            if body.preferred_clinic_id and await self.clinics.get(tenant_id, body.preferred_clinic_id) is None:
                raise NotFoundError("کلینیک یافت نشد")
            entity.preferred_clinic_id = body.preferred_clinic_id
        if body.preferred_language is not None:
            entity.preferred_language = body.preferred_language
        if body.emergency_contact_ref is not None:
            entity.emergency_contact_ref = body.emergency_contact_ref
        if body.notification_prefs is not None:
            entity.notification_prefs = body.notification_prefs
        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.PATIENT_PORTAL_PROFILE_UPDATED,
            aggregate_type="patient_portal_profile",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"patient_id": str(entity.patient_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_appointments(self, tenant_id: UUID, patient_id: UUID):
        spec = AppointmentListSpec(patient_id=patient_id, sort_by="scheduled_start", sort_dir="desc")
        return await self.appointments.list(tenant_id, offset=0, limit=50, spec=spec)

    async def book_appointment(
        self, tenant_id: UUID, patient_id: UUID, body: AppointmentCreate, *, actor=None
    ):
        if body.patient_id != patient_id:
            raise AppError("دسترسی به بیمار دیگر مجاز نیست", status_code=403, error_code="patient_mismatch")
        return await self.appointments.book(tenant_id, body, actor=actor)

    async def cancel_appointment(
        self,
        tenant_id: UUID,
        patient_id: UUID,
        appointment_id: UUID,
        body: AppointmentLifecycleRequest,
        *,
        actor=None,
    ):
        appt = await self.appointments.get(tenant_id, appointment_id)
        if appt.patient_id != patient_id:
            raise AppError("دسترسی به نوبت دیگر مجاز نیست", status_code=403, error_code="patient_mismatch")
        return await self.appointments.cancel(tenant_id, appointment_id, body, actor=actor)

    async def create_document(
        self, tenant_id: UUID, patient_id: UUID, body: PatientDocumentRefCreate, *, actor=None
    ) -> PatientDocumentRef:
        code = validate_code(body.code)
        if await self.documents.get_by_code(tenant_id, code):
            raise AppError("کد تکراری است", error_code="duplicate_code", status_code=409)
        entity = PatientDocumentRef(
            tenant_id=tenant_id,
            patient_id=patient_id,
            code=code,
            title=body.title,
            storage_file_ref=body.storage_file_ref,
            document_type=body.document_type,
            metadata_json=body.metadata_json,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.documents.add(entity)
        self.events.publish(
            event_type=HealthcareEventType.PATIENT_DOCUMENT_REF_CREATED,
            aggregate_type="patient_document_ref",
            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_documents(self, tenant_id: UUID, patient_id: UUID):
        return list(await self.documents.list_by_patient(tenant_id, patient_id))

    async def upsert_notification_pref(
        self,
        tenant_id: UUID,
        patient_id: UUID,
        body: PatientNotificationPreferenceUpsert,
        *,
        actor=None,
    ) -> PatientNotificationPreference:
        existing = await self.prefs.get_pref(
            tenant_id, patient_id, body.channel, body.template_key
        )
        if existing:
            existing.enabled = body.enabled
            existing.metadata_json = body.metadata_json
            existing.updated_by = _actor_id(actor)
            entity = existing
        else:
            entity = PatientNotificationPreference(
                tenant_id=tenant_id,
                patient_id=patient_id,
                channel=body.channel,
                template_key=body.template_key,
                enabled=body.enabled,
                metadata_json=body.metadata_json,
                created_by=_actor_id(actor),
                updated_by=_actor_id(actor),
            )
            await self.prefs.add(entity)
        self.events.publish(
            event_type=HealthcareEventType.PATIENT_NOTIFICATION_PREF_UPDATED,
            aggregate_type="patient_notification_preference",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"channel": entity.channel, "template_key": entity.template_key},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_notification_prefs(self, tenant_id: UUID, patient_id: UUID):
        return list(await self.prefs.list_by_patient(tenant_id, patient_id))
