"""Modelos Pydantic usados para payloads persistibles y respuestas serializables."""

from __future__ import annotations

from typing import Any, Mapping

from pydantic import BaseModel, ConfigDict, Field

from app.batch_processing.domain.models import (
    ASSOCIATION_SOURCE_AUTO,
    ASSOCIATION_SOURCE_MANUAL,
    BATCH_STATUS_RECIBIDO,
    EPICRISIS_STATUS_COMPLETADO_CON_ERRORES,
    CLINICAL_STATUS_PENDIENTE,
    EPICRISIS_STATUS_PENDIENTE,
    FILE_STATUS_PENDIENTE,
)
from app.batch_processing.domain.models import AssociationDecision
from app.batch_processing.application.utils import normalize_case_key_list


def _string(value: Any, *, strip: bool = True) -> str:
    """Convierte valores arbitrarios en texto sin propagar `None`."""

    if value is None:
        return ""
    text = str(value)
    return text.strip() if strip else text


def _int_value(value: Any) -> int:
    """Normaliza enteros tolerando strings y valores vacíos."""

    try:
        return int(value or 0)
    except (TypeError, ValueError):
        return 0


def _float_value(value: Any) -> float:
    """Normaliza flotantes tolerando strings y valores vacíos."""

    try:
        return float(value or 0.0)
    except (TypeError, ValueError):
        return 0.0


def _list_value(value: Any) -> list[Any]:
    """Garantiza una lista para consumo de respuestas serializadas."""

    return list(value or [])


def _dict_value(value: Any) -> dict[str, Any]:
    """Garantiza un diccionario para consumo de respuestas serializadas."""

    return dict(value or {})


class ApplicationModel(BaseModel):
    """Base homogénea para modelos de la capa de aplicación."""

    model_config = ConfigDict(extra="ignore")

    def to_document(self, *, exclude_none: bool = True) -> dict[str, Any]:
        """Serializa el modelo a un diccionario listo para persistencia o respuesta."""

        return self.model_dump(mode="python", exclude_none=exclude_none)


class BatchBulkEpicrisisPayload(ApplicationModel):
    """Estado agregado del procesamiento masivo de epicrisis dentro del lote."""

    bulk_epicrisis_status: str = EPICRISIS_STATUS_PENDIENTE
    bulk_epicrisis_job_id: str = ""
    bulk_epicrisis_requested_at: str = ""
    bulk_epicrisis_total_target: int = 0
    bulk_epicrisis_completed_count: int = 0
    bulk_epicrisis_failed_count: int = 0
    bulk_epicrisis_skipped_count: int = 0
    bulk_epicrisis_case_keys: list[str] = Field(default_factory=list)

    @classmethod
    def from_batch_record(cls, batch: Mapping[str, Any] | None) -> "BatchBulkEpicrisisPayload":
        """Extrae solo los campos del agregado de epicrisis desde el documento del lote."""

        batch_data = batch or {}
        return cls(
            bulk_epicrisis_status=_string(
                batch_data.get("bulk_epicrisis_status")
            )
            or EPICRISIS_STATUS_PENDIENTE,
            bulk_epicrisis_job_id=_string(batch_data.get("bulk_epicrisis_job_id")),
            bulk_epicrisis_requested_at=_string(batch_data.get("bulk_epicrisis_requested_at")),
            bulk_epicrisis_total_target=_int_value(batch_data.get("bulk_epicrisis_total_target")),
            bulk_epicrisis_completed_count=_int_value(
                batch_data.get("bulk_epicrisis_completed_count")
            ),
            bulk_epicrisis_failed_count=_int_value(
                batch_data.get("bulk_epicrisis_failed_count")
            ),
            bulk_epicrisis_skipped_count=_int_value(
                batch_data.get("bulk_epicrisis_skipped_count")
            ),
            bulk_epicrisis_case_keys=normalize_case_key_list(
                batch_data.get("bulk_epicrisis_case_keys")
            ),
        )


class BatchCreatePayload(ApplicationModel):
    """Documento inicial persistido para un lote recién recibido."""

    usuario: str
    nombre_archivo: str
    status: str = BATCH_STATUS_RECIBIDO
    created_at: str
    updated_at: str
    total_files: int = 0
    processed_files: int = 0
    failed_files: int = 0
    pending_validation_files: int = 0
    associated_files: int = 0
    clinical_processed_files: int = 0
    clinical_failed_files: int = 0
    clinical_pending_files: int = 0
    bulk_epicrisis_status: str = EPICRISIS_STATUS_PENDIENTE
    bulk_epicrisis_job_id: str = ""
    bulk_epicrisis_requested_at: str = ""
    bulk_epicrisis_total_target: int = 0
    bulk_epicrisis_completed_count: int = 0
    bulk_epicrisis_failed_count: int = 0
    bulk_epicrisis_skipped_count: int = 0
    bulk_epicrisis_case_keys: list[str] = Field(default_factory=list)
    excel_epicrisis_status: str = EPICRISIS_STATUS_PENDIENTE
    excel_epicrisis_job_id: str = ""
    excel_epicrisis_requested_at: str = ""
    excel_epicrisis_generated_at: str = ""
    excel_epicrisis_error: str = ""
    excel_epicrisis_filename: str = ""
    excel_epicrisis_download_url: str = ""
    excel_epicrisis_included_count: int = 0
    excel_epicrisis_omitted_count: int = 0
    excel_epicrisis_path: str = ""
    error: str = ""


class BatchUpdatePayload(ApplicationModel):
    """Actualización parcial de un lote."""

    archive_path: str | None = None
    status: str | None = None
    error: str | None = None
    updated_at: str | None = None
    total_files: int | None = None
    processed_files: int | None = None
    failed_files: int | None = None
    pending_validation_files: int | None = None
    associated_files: int | None = None
    clinical_processed_files: int | None = None
    clinical_failed_files: int | None = None
    clinical_pending_files: int | None = None
    bulk_epicrisis_status: str | None = None
    bulk_epicrisis_job_id: str | None = None
    bulk_epicrisis_requested_at: str | None = None
    bulk_epicrisis_total_target: int | None = None
    bulk_epicrisis_completed_count: int | None = None
    bulk_epicrisis_failed_count: int | None = None
    bulk_epicrisis_skipped_count: int | None = None
    bulk_epicrisis_case_keys: list[str] | None = None
    excel_epicrisis_status: str | None = None
    excel_epicrisis_job_id: str | None = None
    excel_epicrisis_requested_at: str | None = None
    excel_epicrisis_generated_at: str | None = None
    excel_epicrisis_error: str | None = None
    excel_epicrisis_filename: str | None = None
    excel_epicrisis_download_url: str | None = None
    excel_epicrisis_included_count: int | None = None
    excel_epicrisis_omitted_count: int | None = None
    excel_epicrisis_path: str | None = None


class BatchFileCreatePayload(ApplicationModel):
    """Documento inicial persistido para cada archivo extraído del ZIP."""

    batch_id: str
    original_name: str
    relative_path: str
    stored_path: str
    status: str = FILE_STATUS_PENDIENTE
    detected_type: str = ""
    patient_name: str = ""
    patient_id: str = ""
    case_number: str = ""
    service_date: str = ""
    procedure_code: str = ""
    procedure_description: str = ""
    case_key: str = ""
    associated_user: str = ""
    association_source: str = ASSOCIATION_SOURCE_AUTO
    confidence: float = 0.0
    evidence: list[str] = Field(default_factory=list)
    score_breakdown: dict[str, Any] = Field(default_factory=dict)
    top_candidates: list[dict[str, Any]] = Field(default_factory=list)
    manual_resolution: dict[str, Any] = Field(default_factory=dict)
    text_preview: str = ""
    extracted_text: str = ""
    clinical_status: str = CLINICAL_STATUS_PENDIENTE
    clinical_job_id: str = ""
    clinical_error: str = ""
    clinical_processed_at: str = ""
    analysis_document_id: str = ""
    error: str = ""
    created_at: str
    updated_at: str


class BatchFileUpdatePayload(ApplicationModel):
    """Actualización parcial del documento batch por archivo."""

    status: str | None = None
    stored_path: str | None = None
    detected_type: str | None = None
    patient_name: str | None = None
    patient_id: str | None = None
    case_number: str | None = None
    service_date: str | None = None
    procedure_code: str | None = None
    procedure_description: str | None = None
    case_key: str | None = None
    associated_user: str | None = None
    association_source: str | None = None
    confidence: float | None = None
    evidence: list[str] | None = None
    score_breakdown: dict[str, Any] | None = None
    top_candidates: list[dict[str, Any]] | None = None
    manual_resolution: dict[str, Any] | None = None
    text_preview: str | None = None
    extracted_text: str | None = None
    clinical_status: str | None = None
    clinical_job_id: str | None = None
    clinical_error: str | None = None
    clinical_processed_at: str | None = None
    analysis_document_id: str | None = None
    error: str | None = None
    updated_at: str | None = None

    @classmethod
    def from_association_decision(
        cls,
        decision: AssociationDecision,
        *,
        updated_at: str,
    ) -> "BatchFileUpdatePayload":
        """Convierte una decisión de asociación en un payload de actualización."""

        payload = cls(
            status=decision.status,
            case_key=decision.case_key,
            confidence=round(decision.confidence, 4),
            evidence=list(decision.evidence),
            patient_name=decision.patient_name,
            patient_id=decision.patient_id,
            case_number=decision.case_number,
            procedure_code=decision.procedure_code,
            procedure_description=decision.procedure_description,
            associated_user=decision.associated_user,
            association_source=decision.association_source,
            score_breakdown=dict(decision.score_breakdown),
            top_candidates=list(decision.top_candidates),
            updated_at=updated_at,
        )
        if decision.association_source != ASSOCIATION_SOURCE_MANUAL:
            payload.manual_resolution = {}
        return payload


class ManualResolutionMetadata(ApplicationModel):
    """Metadatos de auditoría para una resolución manual."""

    resolved_by: str
    resolved_at: str
    reason: str


class BatchCaseRecord(ApplicationModel):
    """Agregado persistido de documentos asociados a un mismo caso."""

    batch_id: str
    usuario: str = ""
    case_key: str
    patient_name: str = ""
    patient_id: str = ""
    case_number: str = ""
    service_date: str = ""
    procedure_code: str = ""
    procedure_description: str = ""
    associated_user: str = ""
    file_ids: list[str] = Field(default_factory=list)
    analysis_document_ids: list[str] = Field(default_factory=list)
    detected_types: list[str] = Field(default_factory=list)
    document_count: int = 0
    processed_document_count: int = 0
    clinical_failed_count: int = 0
    ready_for_epicrisis: bool = False
    epicrisis_status: str = EPICRISIS_STATUS_PENDIENTE
    epicrisis_job_id: str = ""
    epicrisis_error: str = ""
    epicrisis_url: str = ""
    updated_at: str = ""

    @classmethod
    def from_existing(
        cls,
        *,
        batch_id: str,
        username: str,
        case_key: str,
        existing_case: Mapping[str, Any] | None,
    ) -> "BatchCaseRecord":
        """Inicializa el agregado con el estado previo ya persistido del caso."""

        current = existing_case or {}
        epicrisis_url = _string(current.get("epicrisis_url")) or (
            f"/epicrisis?case_key={case_key}" if case_key else ""
        )
        return cls(
            batch_id=batch_id,
            usuario=username,
            case_key=case_key,
            epicrisis_status=_string(current.get("epicrisis_status")) or EPICRISIS_STATUS_PENDIENTE,
            epicrisis_job_id=_string(current.get("epicrisis_job_id")),
            epicrisis_error=_string(current.get("epicrisis_error")),
            epicrisis_url=epicrisis_url,
            updated_at=_string(current.get("updated_at")),
        )


class BatchCaseUpdatePayload(ApplicationModel):
    """Actualización parcial del agregado de caso."""

    epicrisis_status: str | None = None
    epicrisis_job_id: str | None = None
    epicrisis_error: str | None = None
    epicrisis_url: str | None = None
    updated_at: str | None = None


class BatchUploadAcceptedView(ApplicationModel):
    """Respuesta de aceptación del lote para polling asíncrono."""

    batch_id: str
    status: str
    created_at: str
    poll_url: str


class MaterializationResultView(ApplicationModel):
    """Respuesta del intento de materialización clínica por archivo."""

    file_id: str
    analysis_document_id: str = ""
    clinical_status: str = CLINICAL_STATUS_PENDIENTE
    clinical_error: str = ""


class BatchStatusView(ApplicationModel):
    """Resumen estable del lote consumido por la UI."""

    batch_id: str
    usuario: str = ""
    status: str = BATCH_STATUS_RECIBIDO
    total_files: int = 0
    processed_files: int = 0
    failed_files: int = 0
    pending_validation_files: int = 0
    associated_files: int = 0
    clinical_processed_files: int = 0
    clinical_failed_files: int = 0
    clinical_pending_files: int = 0
    bulk_epicrisis_status: str = EPICRISIS_STATUS_PENDIENTE
    bulk_epicrisis_job_id: str = ""
    bulk_epicrisis_requested_at: str = ""
    bulk_epicrisis_total_target: int = 0
    bulk_epicrisis_completed_count: int = 0
    bulk_epicrisis_failed_count: int = 0
    bulk_epicrisis_skipped_count: int = 0
    excel_epicrisis_status: str = EPICRISIS_STATUS_PENDIENTE
    excel_epicrisis_job_id: str = ""
    excel_epicrisis_requested_at: str = ""
    excel_epicrisis_generated_at: str = ""
    excel_epicrisis_error: str = ""
    excel_epicrisis_filename: str = ""
    excel_epicrisis_download_url: str = ""
    excel_epicrisis_included_count: int = 0
    excel_epicrisis_omitted_count: int = 0
    created_at: str = ""
    updated_at: str = ""
    nombre_archivo: str = ""
    error: str = ""

    @classmethod
    def from_record(cls, batch: Mapping[str, Any]) -> "BatchStatusView":
        """Normaliza un documento de lote persistido al contrato de salida."""

        return cls(
            batch_id=_string(batch.get("_id")),
            usuario=_string(batch.get("usuario")),
            status=_string(batch.get("status")) or BATCH_STATUS_RECIBIDO,
            total_files=_int_value(batch.get("total_files")),
            processed_files=_int_value(batch.get("processed_files")),
            failed_files=_int_value(batch.get("failed_files")),
            pending_validation_files=_int_value(batch.get("pending_validation_files")),
            associated_files=_int_value(batch.get("associated_files")),
            clinical_processed_files=_int_value(batch.get("clinical_processed_files")),
            clinical_failed_files=_int_value(batch.get("clinical_failed_files")),
            clinical_pending_files=_int_value(batch.get("clinical_pending_files")),
            bulk_epicrisis_status=_string(batch.get("bulk_epicrisis_status"))
            or EPICRISIS_STATUS_PENDIENTE,
            bulk_epicrisis_job_id=_string(batch.get("bulk_epicrisis_job_id")),
            bulk_epicrisis_requested_at=_string(batch.get("bulk_epicrisis_requested_at")),
            bulk_epicrisis_total_target=_int_value(batch.get("bulk_epicrisis_total_target")),
            bulk_epicrisis_completed_count=_int_value(
                batch.get("bulk_epicrisis_completed_count")
            ),
            bulk_epicrisis_failed_count=_int_value(batch.get("bulk_epicrisis_failed_count")),
            bulk_epicrisis_skipped_count=_int_value(batch.get("bulk_epicrisis_skipped_count")),
            excel_epicrisis_status=_string(batch.get("excel_epicrisis_status"))
            or EPICRISIS_STATUS_PENDIENTE,
            excel_epicrisis_job_id=_string(batch.get("excel_epicrisis_job_id")),
            excel_epicrisis_requested_at=_string(batch.get("excel_epicrisis_requested_at")),
            excel_epicrisis_generated_at=_string(batch.get("excel_epicrisis_generated_at")),
            excel_epicrisis_error=_string(batch.get("excel_epicrisis_error")),
            excel_epicrisis_filename=_string(batch.get("excel_epicrisis_filename")),
            excel_epicrisis_download_url=_string(batch.get("excel_epicrisis_download_url")),
            excel_epicrisis_included_count=_int_value(batch.get("excel_epicrisis_included_count")),
            excel_epicrisis_omitted_count=_int_value(batch.get("excel_epicrisis_omitted_count")),
            created_at=_string(batch.get("created_at")),
            updated_at=_string(batch.get("updated_at")),
            nombre_archivo=_string(batch.get("nombre_archivo")),
            error=_string(batch.get("error")),
        )


class UserBatchSummaryView(ApplicationModel):
    """Resumen corto de lote para el historial del usuario."""

    batch_id: str
    status: str = BATCH_STATUS_RECIBIDO
    nombre_archivo: str = ""
    created_at: str = ""
    updated_at: str = ""
    total_files: int = 0
    processed_files: int = 0
    associated_files: int = 0
    pending_validation_files: int = 0
    failed_files: int = 0
    clinical_processed_files: int = 0
    clinical_failed_files: int = 0
    clinical_pending_files: int = 0
    excel_epicrisis_status: str = EPICRISIS_STATUS_PENDIENTE
    excel_epicrisis_generated_at: str = ""
    excel_epicrisis_filename: str = ""
    excel_epicrisis_download_url: str = ""
    error: str = ""

    @classmethod
    def from_record(cls, batch: Mapping[str, Any]) -> "UserBatchSummaryView":
        """Normaliza un documento de lote al formato del historial resumido."""

        return cls(
            batch_id=_string(batch.get("_id")),
            status=_string(batch.get("status")) or BATCH_STATUS_RECIBIDO,
            nombre_archivo=_string(batch.get("nombre_archivo")),
            created_at=_string(batch.get("created_at")),
            updated_at=_string(batch.get("updated_at")),
            total_files=_int_value(batch.get("total_files")),
            processed_files=_int_value(batch.get("processed_files")),
            associated_files=_int_value(batch.get("associated_files")),
            pending_validation_files=_int_value(batch.get("pending_validation_files")),
            failed_files=_int_value(batch.get("failed_files")),
            clinical_processed_files=_int_value(batch.get("clinical_processed_files")),
            clinical_failed_files=_int_value(batch.get("clinical_failed_files")),
            clinical_pending_files=_int_value(batch.get("clinical_pending_files")),
            excel_epicrisis_status=_string(batch.get("excel_epicrisis_status"))
            or EPICRISIS_STATUS_PENDIENTE,
            excel_epicrisis_generated_at=_string(batch.get("excel_epicrisis_generated_at")),
            excel_epicrisis_filename=_string(batch.get("excel_epicrisis_filename")),
            excel_epicrisis_download_url=_string(batch.get("excel_epicrisis_download_url")),
            error=_string(batch.get("error")),
        )


class BatchFileView(ApplicationModel):
    """Detalle serializado de un archivo dentro del lote."""

    file_id: str
    original_name: str = ""
    detected_type: str = ""
    status: str = FILE_STATUS_PENDIENTE
    patient_name: str = ""
    patient_id: str = ""
    case_number: str = ""
    case_key: str = ""
    associated_user: str = ""
    association_source: str = ASSOCIATION_SOURCE_AUTO
    confidence: float = 0.0
    evidence: list[str] = Field(default_factory=list)
    score_breakdown: dict[str, Any] = Field(default_factory=dict)
    top_candidates: list[dict[str, Any]] = Field(default_factory=list)
    manual_resolution: dict[str, Any] = Field(default_factory=dict)
    clinical_status: str = CLINICAL_STATUS_PENDIENTE
    analysis_document_id: str = ""
    clinical_error: str = ""
    clinical_processed_at: str = ""
    job_id: str = ""
    error: str = ""

    @classmethod
    def from_record(cls, item: Mapping[str, Any]) -> "BatchFileView":
        """Normaliza un documento batch por archivo para la UI de seguimiento."""

        return cls(
            file_id=_string(item.get("_id")),
            original_name=_string(item.get("original_name")),
            detected_type=_string(item.get("detected_type")),
            status=_string(item.get("status")) or FILE_STATUS_PENDIENTE,
            patient_name=_string(item.get("patient_name")),
            patient_id=_string(item.get("patient_id")),
            case_number=_string(item.get("case_number")),
            case_key=_string(item.get("case_key")),
            associated_user=_string(item.get("associated_user")),
            association_source=_string(item.get("association_source")) or ASSOCIATION_SOURCE_AUTO,
            confidence=_float_value(item.get("confidence")),
            evidence=[_string(value) for value in _list_value(item.get("evidence"))],
            score_breakdown=_dict_value(item.get("score_breakdown")),
            top_candidates=[
                dict(candidate)
                for candidate in _list_value(item.get("top_candidates"))
                if isinstance(candidate, Mapping)
            ],
            manual_resolution=_dict_value(item.get("manual_resolution")),
            clinical_status=_string(item.get("clinical_status")) or CLINICAL_STATUS_PENDIENTE,
            analysis_document_id=_string(item.get("analysis_document_id")),
            clinical_error=_string(item.get("clinical_error")),
            clinical_processed_at=_string(item.get("clinical_processed_at")),
            job_id=_string(item.get("clinical_job_id")),
            error=_string(item.get("error")),
        )


class BatchCaseView(ApplicationModel):
    """Vista serializable de un caso consolidado del lote."""

    case_key: str
    patient_name: str = ""
    patient_id: str = ""
    case_number: str = ""
    procedure_code: str = ""
    procedure_description: str = ""
    associated_user: str = ""
    file_ids: list[str] = Field(default_factory=list)
    analysis_document_ids: list[str] = Field(default_factory=list)
    document_count: int = 0
    processed_document_count: int = 0
    clinical_failed_count: int = 0
    detected_types: list[str] = Field(default_factory=list)
    ready_for_epicrisis: bool = False
    epicrisis_status: str = EPICRISIS_STATUS_PENDIENTE
    epicrisis_job_id: str = ""
    epicrisis_error: str = ""
    epicrisis_url: str = ""

    @classmethod
    def from_record(cls, item: Mapping[str, Any]) -> "BatchCaseView":
        """Normaliza un agregado persistido del caso al contrato de salida."""

        return cls(
            case_key=_string(item.get("case_key")),
            patient_name=_string(item.get("patient_name")),
            patient_id=_string(item.get("patient_id")),
            case_number=_string(item.get("case_number")),
            procedure_code=_string(item.get("procedure_code")),
            procedure_description=_string(item.get("procedure_description")),
            associated_user=_string(item.get("associated_user")),
            file_ids=[_string(value) for value in _list_value(item.get("file_ids"))],
            analysis_document_ids=[
                _string(value) for value in _list_value(item.get("analysis_document_ids"))
            ],
            document_count=_int_value(item.get("document_count")),
            processed_document_count=_int_value(item.get("processed_document_count")),
            clinical_failed_count=_int_value(item.get("clinical_failed_count")),
            detected_types=[
                _string(value) for value in _list_value(item.get("detected_types"))
            ],
            ready_for_epicrisis=bool(item.get("ready_for_epicrisis")),
            epicrisis_status=_string(item.get("epicrisis_status")) or EPICRISIS_STATUS_PENDIENTE,
            epicrisis_job_id=_string(item.get("epicrisis_job_id")),
            epicrisis_error=_string(item.get("epicrisis_error")),
            epicrisis_url=_string(item.get("epicrisis_url")),
        )


class QueueBatchEpicrisisResult(ApplicationModel):
    """Respuesta del encolado masivo de epicrisis para el lote."""

    batch_id: str
    job_id: str = ""
    status: str = EPICRISIS_STATUS_PENDIENTE
    queued_count: int = 0
    skipped_completed_count: int = 0
    skipped_inflight_count: int = 0
    skipped_not_ready_count: int = 0
    case_keys: list[str] = Field(default_factory=list)


class QueueBatchEpicrisisExcelResult(ApplicationModel):
    """Respuesta del encolado de exportación Excel de epicrisis para el lote."""

    batch_id: str
    job_id: str = ""
    status: str = EPICRISIS_STATUS_PENDIENTE
    eligible_count: int = 0
    filename: str = ""


class ManualAssociationResultView(ApplicationModel):
    """Respuesta de la resolución manual de una asociación ambigua."""

    file_id: str
    status: str = FILE_STATUS_PENDIENTE
    case_key: str = ""
    association_source: str = ASSOCIATION_SOURCE_MANUAL
    manual_resolution: dict[str, Any] = Field(default_factory=dict)
    confidence: float = 0.0

    @classmethod
    def from_record(cls, item: Mapping[str, Any]) -> "ManualAssociationResultView":
        """Normaliza el archivo actualizado a la respuesta esperada por la UI."""

        return cls(
            file_id=_string(item.get("_id")),
            status=_string(item.get("status")) or FILE_STATUS_PENDIENTE,
            case_key=_string(item.get("case_key")),
            association_source=_string(item.get("association_source"))
            or ASSOCIATION_SOURCE_MANUAL,
            manual_resolution=_dict_value(item.get("manual_resolution")),
            confidence=_float_value(item.get("confidence")),
        )
