"""Casos de uso para la carga individual asíncrona."""

from __future__ import annotations

import hashlib
import logging
from dataclasses import dataclass
from datetime import tzinfo
from difflib import SequenceMatcher
from typing import Any, Protocol

from app.batch_processing.application.utils import now_iso
from app.batch_processing.domain.ports import (
    CaseAssociationService,
    DocumentClassifier,
    TextExtractor,
)
from app.core.logging import bind_log_context, get_audit_logger
from app.individual_ingestion.application.models import (
    IndividualUploadAcceptedView,
    IndividualUploadCreatePayload,
    IndividualUploadStatusView,
    IndividualUploadUpdatePayload,
)
from app.individual_ingestion.domain.models import (
    PENDING_ACTION_CONFIRM_CASE_IDENTITY,
    PENDING_ACTION_CONFIRM_CASE_OVERRIDE,
    PENDING_ACTION_CONFIRM_TYPE_OVERRIDE,
    SUPPORTED_INDIVIDUAL_SUPPORT_TYPES,
    UPLOAD_STATUS_CANCELADO,
    UPLOAD_STATUS_COMPLETADO,
    UPLOAD_STATUS_ESPERANDO_CONFIRMACION,
    UPLOAD_STATUS_FALLIDO,
    UPLOAD_STATUS_LISTO_PARA_PROCESAR,
    UPLOAD_STATUS_PRECHECK_EN_COLA,
    UPLOAD_STATUS_PRECHECK_PROCESANDO,
    UPLOAD_STATUS_PROCESANDO,
)
from app.services.clinical_document_service import ClinicalDocumentRequest


logger = logging.getLogger(__name__)
audit_logger = get_audit_logger()


class IndividualUploadDispatcher(Protocol):
    def dispatch_precheck(self, upload_id: str) -> None: ...

    def dispatch_materialization(self, upload_id: str) -> None: ...


class IndividualClinicalDocumentService(Protocol):
    def resolve_case_identity(self, request: ClinicalDocumentRequest) -> Any: ...

    def get_user_case_context(self, username: str, case_key: str) -> dict[str, Any]: ...

    def process_and_persist(self, request: ClinicalDocumentRequest) -> dict[str, Any]: ...


def _compute_file_hash(contents: bytes) -> str:
    return hashlib.sha256(contents).hexdigest()


def _impact_summary(selected_type: str, detected_type: str) -> list[str]:
    if not selected_type or not detected_type or selected_type == detected_type:
        return []
    impact_map = {
        "historia_clinica": [
            "Puede alterar la base clínica del caso y la extracción de diagnósticos, procedimientos y medicamentos.",
            "Puede afectar la elegibilidad y consistencia de epicrisis, RIPS y RDA.",
        ],
        "factura": [
            "Puede alterar la extracción administrativa y la consistencia de cobros frente al caso clínico.",
            "Puede afectar validaciones de soportes y hallazgos posteriores.",
        ],
        "quirurgico": [
            "Puede alterar procedimientos extraídos y codificación CUPS/CIE-10 quirúrgica.",
            "Puede afectar la consistencia de artefactos derivados del caso.",
        ],
        "laboratorio": [
            "Puede alterar ayudas diagnósticas y hallazgos consolidados del caso.",
        ],
        "radiologia": [
            "Puede alterar ayudas diagnósticas y hallazgos consolidados del caso.",
        ],
        "generico": [
            "Puede ocultar reglas específicas del tipo documental real y afectar trazabilidad.",
        ],
    }
    return list(impact_map.get(selected_type, []))


def _is_terminal_status(status: str) -> bool:
    return status in {UPLOAD_STATUS_COMPLETADO, UPLOAD_STATUS_CANCELADO, UPLOAD_STATUS_FALLIDO}


def _normalize_type(value: Any) -> str:
    return str(value or "").strip()


def _resolve_effective_document_type(
    *,
    selected_type: str,
    detected_type: str,
    requested_type: str = "",
) -> str:
    normalized_selected = _normalize_type(selected_type)
    normalized_detected = _normalize_type(detected_type)
    normalized_requested = _normalize_type(requested_type)
    if normalized_requested and normalized_requested in {normalized_selected, normalized_detected}:
        return normalized_requested
    return normalized_detected or normalized_selected or "generico"


def _name_similarity(left: str, right: str) -> float:
    normalized_left = _normalize_type(left).lower()
    normalized_right = _normalize_type(right).lower()
    if not normalized_left or not normalized_right:
        return 0.0
    return SequenceMatcher(a=normalized_left, b=normalized_right).ratio()


def _sanitize_provisional_identity_value(value: Any, *, unknown_tokens: set[str] | None = None) -> str:
    normalized = _normalize_type(value)
    if not normalized:
        return ""
    lowered = normalized.lower()
    if unknown_tokens and lowered in unknown_tokens:
        return ""
    return normalized


def _detect_case_contradictions(
    *,
    extracted_patient_id: str,
    preferred_patient_id: str,
    extracted_case_number: str,
    preferred_case_number: str,
    extracted_patient_name: str,
    preferred_patient_name: str,
) -> list[str]:
    contradictions: list[str] = []
    if extracted_patient_id and preferred_patient_id and extracted_patient_id != preferred_patient_id:
        contradictions.append(
            f"Identificación extraída ({extracted_patient_id}) no coincide con el caso seleccionado ({preferred_patient_id})."
        )
    if extracted_case_number and preferred_case_number and extracted_case_number != preferred_case_number:
        contradictions.append(
            f"Número de caso extraído ({extracted_case_number}) no coincide con el caso seleccionado ({preferred_case_number})."
        )
    if extracted_patient_name and preferred_patient_name and _name_similarity(extracted_patient_name, preferred_patient_name) < 0.7:
        contradictions.append(
            f"Nombre de paciente extraído ({extracted_patient_name}) no coincide con el caso seleccionado ({preferred_patient_name})."
        )
    return contradictions


@dataclass
class CreateIndividualUploadUseCase:
    repository: Any
    file_store: Any
    dispatcher: IndividualUploadDispatcher
    colombia_tz: tzinfo

    def execute(
        self,
        *,
        filename: str,
        contents: bytes,
        username: str,
        selected_document_type: str,
        provided_case_key: str = "",
    ) -> dict[str, Any]:
        normalized_filename = str(filename or "").strip()
        normalized_type = str(selected_document_type or "").strip()
        normalized_case_key = str(provided_case_key or "").strip()
        if not normalized_filename or not normalized_filename.lower().endswith(".pdf"):
            raise ValueError("Solo se permiten archivos PDF.")
        if not contents:
            raise ValueError("El archivo PDF está vacío.")
        if normalized_type == "historia_clinica" and normalized_case_key:
            raise ValueError("La historia clínica individual no debe llegar con case_key preexistente.")
        if normalized_type != "historia_clinica":
            if normalized_type not in SUPPORTED_INDIVIDUAL_SUPPORT_TYPES:
                raise ValueError("Tipo de soporte individual no soportado.")
            if not normalized_case_key:
                raise ValueError("Debes seleccionar un caso existente para adjuntar un soporte.")

        file_hash = _compute_file_hash(contents)
        duplicate = self.repository.find_latest_by_hash(username, file_hash)
        if duplicate and not _is_terminal_status(str(duplicate.get("status") or "")):
            upload_id = str(duplicate.get("_id") or "")
            return IndividualUploadAcceptedView(
                upload_id=upload_id,
                status=str(duplicate.get("status") or UPLOAD_STATUS_PRECHECK_EN_COLA),
                created_at=str(duplicate.get("created_at") or ""),
                poll_url=f"/api/cargue-individual/{upload_id}",
                duplicate=True,
                message="El documento ya está en curso.",
            ).to_document()
        if duplicate and str(duplicate.get("clinical_document_id") or "").strip():
            upload_id = str(duplicate.get("_id") or "")
            return IndividualUploadAcceptedView(
                upload_id=upload_id,
                status=str(duplicate.get("status") or UPLOAD_STATUS_COMPLETADO),
                created_at=str(duplicate.get("created_at") or ""),
                poll_url=f"/api/cargue-individual/{upload_id}",
                duplicate=True,
                message="El documento ya fue cargado previamente.",
            ).to_document()

        created_at = now_iso(self.colombia_tz)
        payload = IndividualUploadCreatePayload(
            usuario=username,
            original_name=normalized_filename,
            selected_document_type=normalized_type,
            provided_case_key=normalized_case_key,
            source_file_hash=file_hash,
            stored_path="",
            file_size=len(contents),
            created_at=created_at,
            updated_at=created_at,
            effective_document_type=normalized_type,
        )
        upload_id = self.repository.create_upload(payload.to_document())
        stored_path = self.file_store.save_file(upload_id, normalized_filename, contents)
        self.repository.update_upload(
            upload_id,
            IndividualUploadUpdatePayload(stored_path=stored_path, updated_at=created_at).to_document(),
        )
        self.dispatcher.dispatch_precheck(upload_id)
        return IndividualUploadAcceptedView(
            upload_id=upload_id,
            status=UPLOAD_STATUS_PRECHECK_EN_COLA,
            created_at=created_at,
            poll_url=f"/api/cargue-individual/{upload_id}",
            duplicate=False,
            message="Documento aceptado para precheck.",
        ).to_document()


@dataclass
class GetIndividualUploadStatusUseCase:
    repository: Any

    def execute(self, upload_id: str, *, username: str) -> dict[str, Any] | None:
        record = self.repository.get_upload(upload_id)
        if not record or str(record.get("usuario") or "").strip() != username:
            return None
        return IndividualUploadStatusView.from_record(upload_id, record).to_document()


@dataclass
class GetLatestPendingIndividualUploadUseCase:
    repository: Any

    def execute(self, username: str) -> dict[str, Any] | None:
        record = self.repository.find_latest_active(username)
        if not record:
            return None
        upload_id = str(record.get("_id") or "")
        return IndividualUploadStatusView.from_record(upload_id, record).to_document()


@dataclass
class RunIndividualPrecheckUseCase:
    repository: Any
    file_store: Any
    text_extractor: TextExtractor
    classifier: DocumentClassifier
    clinical_document_service: IndividualClinicalDocumentService
    case_association_service: CaseAssociationService
    dispatcher: IndividualUploadDispatcher
    colombia_tz: tzinfo

    def execute(self, upload_id: str) -> dict[str, Any]:
        record = self.repository.get_upload(upload_id) or {}
        if not record:
            return {}
        bind_log_context(
            username=record.get("usuario", ""),
            file_id=upload_id,
            document_type=record.get("selected_document_type", ""),
        )
        current_status = str(record.get("status") or "")
        if current_status not in {UPLOAD_STATUS_PRECHECK_EN_COLA, UPLOAD_STATUS_PRECHECK_PROCESANDO}:
            return record
        self.repository.update_upload(
            upload_id,
            IndividualUploadUpdatePayload(
                status=UPLOAD_STATUS_PRECHECK_PROCESANDO,
                error="",
                updated_at=now_iso(self.colombia_tz),
            ).to_document(),
        )
        record = self.repository.get_upload(upload_id) or {}

        try:
            raw_text = self.text_extractor.extract_text(str(record.get("stored_path") or ""))
            selected_type = str(record.get("selected_document_type") or "").strip()
            detected_type = self.classifier.classify(str(record.get("original_name") or ""), raw_text)
            pending_actions: list[str] = []
            warnings: list[str] = []
            contradictions: list[str] = []
            provisional_case_number = ""
            provisional_patient_id = ""
            provisional_patient_name = ""
            case_key = str(record.get("provided_case_key") or "").strip()

            if selected_type == "historia_clinica":
                resolution = self.clinical_document_service.resolve_case_identity(
                    ClinicalDocumentRequest(
                        raw_text=raw_text,
                        detected_type="historia_clinica",
                        username=str(record.get("usuario") or ""),
                        original_name=str(record.get("original_name") or ""),
                    )
                )
                provisional_case_number = resolution.case_number
                provisional_patient_id = _sanitize_provisional_identity_value(resolution.patient_id)
                provisional_patient_name = _sanitize_provisional_identity_value(
                    resolution.patient_name,
                    unknown_tokens={"desconocido"},
                )
                warnings.extend(list(resolution.review_messages or []))
                if not provisional_patient_id:
                    message = "No fue posible identificar con confianza la identificación del paciente."
                    if message not in warnings:
                        warnings.append(message)
                if not provisional_patient_name:
                    message = "No fue posible identificar con confianza el nombre del paciente."
                    if message not in warnings:
                        warnings.append(message)
                pending_actions.append(PENDING_ACTION_CONFIRM_CASE_IDENTITY)
            else:
                case_context = self.clinical_document_service.get_user_case_context(
                    str(record.get("usuario") or ""),
                    case_key,
                )
                if not case_context:
                    raise ValueError("No se encontró el caso destino para el soporte.")
                signals = self.case_association_service.extract_signals(
                    str(record.get("original_name") or ""),
                    raw_text,
                    detected_type,
                )
                contradictions = _detect_case_contradictions(
                    extracted_patient_id=str(signals.patient_id or ""),
                    preferred_patient_id=str(case_context.get("patient_id") or ""),
                    extracted_case_number=str(signals.case_number or ""),
                    preferred_case_number=str(case_context.get("case_number") or ""),
                    extracted_patient_name=str(signals.patient_name or ""),
                    preferred_patient_name=str(case_context.get("patient_name") or ""),
                )
                if contradictions:
                    pending_actions.append(PENDING_ACTION_CONFIRM_CASE_OVERRIDE)

            if detected_type != selected_type:
                pending_actions.append(PENDING_ACTION_CONFIRM_TYPE_OVERRIDE)
                warnings.append(
                    f"El documento parece ser '{detected_type}' pero fue cargado como '{selected_type}'."
                )

            next_status = (
                UPLOAD_STATUS_ESPERANDO_CONFIRMACION if pending_actions else UPLOAD_STATUS_LISTO_PARA_PROCESAR
            )
            impact_summary = _impact_summary(selected_type, detected_type)
            effective_document_type = _resolve_effective_document_type(
                selected_type=selected_type,
                detected_type=detected_type,
            )
            self.repository.update_upload(
                upload_id,
                IndividualUploadUpdatePayload(
                    status=next_status,
                    detected_document_type=detected_type,
                    effective_document_type=effective_document_type,
                    extracted_text=raw_text,
                    pending_actions=pending_actions,
                    warnings=warnings,
                    contradictions=contradictions,
                    impact_summary=impact_summary,
                    provisional_case_number=provisional_case_number,
                    provisional_patient_id=provisional_patient_id,
                    provisional_patient_name=provisional_patient_name,
                    case_key=case_key,
                    precheck_completed_at=now_iso(self.colombia_tz),
                    updated_at=now_iso(self.colombia_tz),
                ).to_document(),
            )
            updated = self.repository.get_upload(upload_id) or {}
            if next_status == UPLOAD_STATUS_LISTO_PARA_PROCESAR:
                self.dispatcher.dispatch_materialization(upload_id)
            return updated
        except Exception as exc:
            logger.exception("Error en precheck de carga individual %s", upload_id)
            self.repository.update_upload(
                upload_id,
                IndividualUploadUpdatePayload(
                    status=UPLOAD_STATUS_FALLIDO,
                    error=str(exc),
                    failed_at=now_iso(self.colombia_tz),
                    updated_at=now_iso(self.colombia_tz),
                ).to_document(),
            )
            return self.repository.get_upload(upload_id) or {}


@dataclass
class ConfirmIndividualUploadUseCase:
    repository: Any
    dispatcher: IndividualUploadDispatcher
    colombia_tz: tzinfo

    def execute(
        self,
        *,
        upload_id: str,
        username: str,
        confirmed_case_number: str = "",
        confirmed_patient_id: str = "",
        confirmed_patient_name: str = "",
        confirmed_effective_document_type: str = "",
    ) -> dict[str, Any]:
        record = self.repository.get_upload(upload_id) or {}
        if not record or str(record.get("usuario") or "").strip() != username:
            raise ValueError("Carga individual no encontrada.")
        if str(record.get("status") or "") != UPLOAD_STATUS_ESPERANDO_CONFIRMACION:
            raise ValueError("La carga no está esperando confirmación.")

        pending_actions = list(record.get("pending_actions") or [])
        normalized_case_number = str(
            confirmed_case_number or record.get("provisional_case_number") or ""
        ).strip()
        normalized_patient_id = str(
            confirmed_patient_id or record.get("provisional_patient_id") or ""
        ).strip()
        normalized_patient_name = str(
            confirmed_patient_name or record.get("provisional_patient_name") or ""
        ).strip()

        if PENDING_ACTION_CONFIRM_CASE_IDENTITY in pending_actions and not (
            normalized_case_number and normalized_patient_id and normalized_patient_name
        ):
            raise ValueError(
                "Debes confirmar número de caso, identificación y nombre completo antes de continuar."
            )

        selected_type = str(record.get("selected_document_type") or "").strip()
        detected_type = str(record.get("detected_document_type") or "").strip()
        effective_type = _resolve_effective_document_type(
            selected_type=selected_type,
            detected_type=detected_type,
            requested_type=confirmed_effective_document_type
            or str(record.get("effective_document_type") or ""),
        )
        selected_override_confirmed = bool(
            selected_type
            and detected_type
            and selected_type != detected_type
            and effective_type == selected_type
        )
        case_override_confirmed = PENDING_ACTION_CONFIRM_CASE_OVERRIDE in pending_actions
        override_audit = {}
        if selected_type != detected_type or case_override_confirmed:
            override_audit = {
                "confirmed_by": username,
                "confirmed_at": now_iso(self.colombia_tz),
                "selected_document_type": selected_type,
                "detected_document_type": detected_type,
                "effective_document_type": effective_type,
                "pending_actions": pending_actions,
                "contradictions": list(record.get("contradictions") or []),
                "warnings": list(record.get("warnings") or []),
            }

        self.repository.update_upload(
            upload_id,
            IndividualUploadUpdatePayload(
                status=UPLOAD_STATUS_LISTO_PARA_PROCESAR,
                pending_actions=[],
                confirmed_case_number=normalized_case_number,
                confirmed_patient_id=normalized_patient_id,
                confirmed_patient_name=normalized_patient_name,
                effective_document_type=effective_type,
                selected_type_override_confirmed=selected_override_confirmed,
                case_override_confirmed=case_override_confirmed,
                override_audit=override_audit,
                updated_at=now_iso(self.colombia_tz),
            ).to_document(),
        )
        self.dispatcher.dispatch_materialization(upload_id)
        return self.repository.get_upload(upload_id) or {}


@dataclass
class CancelIndividualUploadUseCase:
    repository: Any
    file_store: Any
    colombia_tz: tzinfo

    def execute(self, *, upload_id: str, username: str, reason: str = "") -> dict[str, Any]:
        record = self.repository.get_upload(upload_id) or {}
        if not record or str(record.get("usuario") or "").strip() != username:
            raise ValueError("Carga individual no encontrada.")
        current_status = str(record.get("status") or "")
        if current_status in {UPLOAD_STATUS_COMPLETADO, UPLOAD_STATUS_CANCELADO}:
            raise ValueError("La carga ya no se puede cancelar.")
        self.file_store.delete_file(str(record.get("stored_path") or ""))
        audit_logger.business_event(
            event_type="individual.upload_cancelled",
            action="cancel_upload",
            outcome="success",
            service="individual_ingestion",
            resource={"upload_id": upload_id, "document_type": record.get("selected_document_type", "")},
            metrics={"file_size": int(record.get("file_size") or 0)},
            error={"reason": str(reason or "cancelled_by_user")},
        )
        self.repository.update_upload(
            upload_id,
            IndividualUploadUpdatePayload(
                status=UPLOAD_STATUS_CANCELADO,
                cancelled_at=now_iso(self.colombia_tz),
                stored_path="",
                extracted_text="",
                pending_actions=[],
                warnings=[],
                contradictions=[],
                impact_summary=[],
                error="",
                audit_only=True,
                updated_at=now_iso(self.colombia_tz),
            ).to_document(),
        )
        return self.repository.get_upload(upload_id) or {}


@dataclass
class RunIndividualMaterializationUseCase:
    repository: Any
    file_store: Any
    clinical_document_service: IndividualClinicalDocumentService
    colombia_tz: tzinfo

    def execute(self, upload_id: str) -> dict[str, Any]:
        record = self.repository.get_upload(upload_id) or {}
        if not record:
            return {}
        bind_log_context(
            username=record.get("usuario", ""),
            file_id=upload_id,
            case_key=record.get("provided_case_key", ""),
            document_type=record.get("selected_document_type", ""),
        )
        current_status = str(record.get("status") or "")
        if current_status not in {UPLOAD_STATUS_LISTO_PARA_PROCESAR, UPLOAD_STATUS_PROCESANDO}:
            return record
        self.repository.update_upload(
            upload_id,
            IndividualUploadUpdatePayload(
                status=UPLOAD_STATUS_PROCESANDO,
                materialization_started_at=now_iso(self.colombia_tz),
                updated_at=now_iso(self.colombia_tz),
                error="",
            ).to_document(),
        )
        record = self.repository.get_upload(upload_id) or {}
        try:
            final_case_number = str(
                record.get("confirmed_case_number") or record.get("provisional_case_number") or ""
            ).strip()
            final_patient_id = str(
                record.get("confirmed_patient_id") or record.get("provisional_patient_id") or ""
            ).strip()
            final_patient_name = str(
                record.get("confirmed_patient_name") or record.get("provisional_patient_name") or ""
            ).strip()
            selected_type = str(record.get("selected_document_type") or "").strip()
            detected_type = str(record.get("detected_document_type") or "").strip()
            effective_type = _resolve_effective_document_type(
                selected_type=selected_type,
                detected_type=detected_type,
                requested_type=str(record.get("effective_document_type") or ""),
            )
            result = self.clinical_document_service.process_and_persist(
                ClinicalDocumentRequest(
                    raw_text=str(record.get("extracted_text") or ""),
                    detected_type=effective_type,
                    username=str(record.get("usuario") or ""),
                    original_name=str(record.get("original_name") or ""),
                    case_key=str(record.get("provided_case_key") or ""),
                    case_number=final_case_number,
                    patient_id=final_patient_id,
                    batch_id="",
                    batch_file_id=f"individual:{upload_id}",
                    ingestion_source="individual",
                    provided_patient_name=final_patient_name,
                    provided_patient_id=final_patient_id,
                    provided_case_number=final_case_number,
                    selected_document_type=selected_type,
                    detected_document_type=detected_type,
                    effective_document_type=effective_type,
                    source_file_hash=str(record.get("source_file_hash") or ""),
                    category_override_confirmed=bool(record.get("selected_type_override_confirmed", False)),
                    override_audit=dict(record.get("override_audit") or {}),
                )
            )
            self.file_store.delete_file(str(record.get("stored_path") or ""))
            self.repository.update_upload(
                upload_id,
                IndividualUploadUpdatePayload(
                    status=UPLOAD_STATUS_COMPLETADO,
                    stored_path="",
                    extracted_text="",
                    clinical_document_id=str(result.get("id_documento") or ""),
                    case_key=str(result.get("case_key") or record.get("provided_case_key") or ""),
                    completed_at=now_iso(self.colombia_tz),
                    updated_at=now_iso(self.colombia_tz),
                ).to_document(),
            )
            return self.repository.get_upload(upload_id) or {}
        except Exception as exc:
            logger.exception("Error materializando carga individual %s", upload_id)
            self.repository.update_upload(
                upload_id,
                IndividualUploadUpdatePayload(
                    status=UPLOAD_STATUS_FALLIDO,
                    error=str(exc),
                    failed_at=now_iso(self.colombia_tz),
                    updated_at=now_iso(self.colombia_tz),
                ).to_document(),
            )
            return self.repository.get_upload(upload_id) or {}
