"""
Patient service handling registration, queries, and business rules.
"""

import asyncio
import math
from datetime import date, datetime
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.core.exceptions import AlreadyExistsException, NotFoundException
from app.core.pagination import PaginationParams
from app.core.responses import PaginatedResponse, PaginationMeta
from app.models.audit import AuditCategory
from app.models.user import User
from app.repositories.lab_repository import LabRepository
from app.repositories.patient_repository import PatientRepository
from app.schemas.patient import PatientCreate, PatientResponse, PatientUpdate
from app.services.audit_service import AuditService


class PatientService:
    """Business logic for patient operations."""

    def __init__(
        self, repo: PatientRepository | None = None, audit_service: AuditService | None = None
    ) -> None:
        self.repo = repo or PatientRepository()
        self.audit = audit_service or AuditService()

    async def _record_event(
        self,
        db: AsyncSession,
        *,
        action: str,
        description: str,
        lab_id: UUID,
        actor: User | None,
        target_id: UUID,
        details: dict[str, object] | None = None,
    ) -> None:
        role = getattr(actor.role, "value", actor.role) if actor else "system"
        await self.audit.record_event(
            db,
            action=action,
            description=description,
            category=AuditCategory.CLINICAL.value,
            actor_name=actor.name if actor else "System",
            actor_role=str(role),
            actor_id=actor.id if actor else None,
            target_type="patient",
            target_id=str(target_id),
            ip_address=None,
            details=details,
            lab_id=lab_id,
        )

    async def list_patients(
        self,
        db: AsyncSession,
        pagination: PaginationParams,
        lab_id: UUID | None = None,
        q: str | None = None,
        created_from: datetime | None = None,
        created_to: datetime | None = None,
        activity_date: date | None = None,
    ) -> PaginatedResponse[PatientResponse]:
        """Retrieve paginated patients with optional search and registration-date range."""
        patients, total = await self.repo.list_patients(
            db,
            limit=pagination.limit,
            offset=pagination.offset,
            lab_id=lab_id,
            q=q,
            created_from=created_from,
            created_to=created_to,
            activity_date=activity_date,
        )
        items = [PatientResponse.model_validate(p) for p in patients]
        pages = math.ceil(total / pagination.limit) if total > 0 else 1
        return PaginatedResponse(
            data=items,
            meta=PaginationMeta(
                total=total,
                page=pagination.page,
                limit=pagination.limit,
                pages=pages,
            ),
        )

    async def get_patient(
        self, db: AsyncSession, patient_id: UUID, lab_id: UUID | None = None
    ) -> PatientResponse:
        """Fetch patient details by ID."""
        patient = await self.repo.get_by_id(db, patient_id, lab_id=lab_id)
        if not patient:
            raise NotFoundException(message=f"Patient with id '{patient_id}' was not found.")
        return PatientResponse.model_validate(patient)

    async def create_patient(
        self, db: AsyncSession, data: PatientCreate, actor: User | None = None
    ) -> PatientResponse:
        """Register a new patient ensuring phone uniqueness per branch."""
        if data.lab_id is None:
            data.lab_id = await LabRepository().get_default_lab_id(db)

        existing = await self.repo.get_by_lab_and_phone(db, data.lab_id, data.phone)
        if existing:
            raise AlreadyExistsException(
                message=f"A patient with phone '{data.phone}' is already registered at this lab branch."
            )

        code = await self.repo.get_next_patient_code(db, data.lab_id)
        patient = await self.repo.create(db, data, patient_code=code)
        await self._record_event(
            db,
            action="PATIENT_CREATED",
            description="Registered a patient record.",
            lab_id=patient.lab_id,
            actor=actor,
            target_id=patient.id,
        )
        return PatientResponse.model_validate(patient)

    async def update_patient(
        self,
        db: AsyncSession,
        patient_id: UUID,
        data: PatientUpdate,
        actor: User | None = None,
        lab_id: UUID | None = None,
    ) -> PatientResponse:
        """Update demographic or contact details for a patient."""
        patient = await self.repo.get_by_id(db, patient_id, lab_id=lab_id)
        if not patient:
            raise NotFoundException(message=f"Patient with id '{patient_id}' was not found.")

        if data.phone and data.phone != patient.phone:
            existing = await self.repo.get_by_lab_and_phone(db, patient.lab_id, data.phone)
            if existing and existing.id != patient.id:
                raise AlreadyExistsException(
                    message=f"A patient with phone '{data.phone}' already exists at this branch."
                )

        changed_fields = [
            key
            for key, value in data.model_dump(exclude_unset=True).items()
            if getattr(patient, key) != value
        ]
        updated = await self.repo.update(db, patient, data)
        if changed_fields:
            await self._record_event(
                db,
                action="PATIENT_UPDATED",
                description="Updated patient profile fields.",
                lab_id=patient.lab_id,
                actor=actor,
                target_id=patient.id,
                details={"changed_fields": changed_fields},
            )
        return PatientResponse.model_validate(updated)

    async def reset_portal_credentials(
        self,
        db: AsyncSession,
        patient_id: UUID,
        phone: str | None,
        rotate_pin: bool,
        actor: User | None = None,
        lab_id: UUID | None = None,
    ) -> tuple[PatientResponse, str | None]:
        """Desk reset of portal login credentials (reception/superadmin).

        Returns the updated patient plus the fresh PIN when rotated — the
        plain PIN is shown once and never stored.
        """
        from app.core.portal_pin import encrypt_pin, generate_pin
        from app.core.security import hash_password

        patient = await self.repo.get_by_id(db, patient_id, lab_id=lab_id)
        if not patient:
            raise NotFoundException(message=f"Patient with id '{patient_id}' was not found.")

        phone_changed = bool(phone and phone != patient.phone)
        if phone_changed and phone:
            existing = await self.repo.get_by_lab_and_phone(db, patient.lab_id, phone)
            if existing and existing.id != patient.id:
                raise AlreadyExistsException(
                    message=f"A patient with phone '{phone}' already exists at this branch."
                )
            patient.phone = phone

        fresh_pin: str | None = None
        if rotate_pin:
            fresh_pin = generate_pin()
            patient.pin_hash = await asyncio.to_thread(hash_password, fresh_pin)
            patient.portal_pin_enc = encrypt_pin(fresh_pin)

        await self._record_event(
            db,
            action="PATIENT_PORTAL_CREDENTIALS_RESET",
            description="Updated patient portal credentials.",
            lab_id=patient.lab_id,
            actor=actor,
            target_id=patient.id,
            details={"phone_changed": phone_changed, "pin_rotated": rotate_pin},
        )
        await db.commit()
        await db.refresh(patient)
        return PatientResponse.model_validate(patient), fresh_pin

    async def delete_patient(
        self,
        db: AsyncSession,
        patient_id: UUID,
        actor: User | None = None,
        lab_id: UUID | None = None,
    ) -> None:
        """Soft-delete a patient."""
        patient = await self.repo.get_by_id(db, patient_id, lab_id=lab_id)
        if not patient:
            raise NotFoundException(message=f"Patient with id '{patient_id}' was not found.")
        await self.repo.delete(db, patient)
        await self._record_event(
            db,
            action="PATIENT_DEACTIVATED",
            description="Deactivated a patient record.",
            lab_id=patient.lab_id,
            actor=actor,
            target_id=patient.id,
        )
