"""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 uuid import uuid4

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,
    IndividualUploadSessionView,
    IndividualUploadStatusView,
    IndividualUploadUpdatePayload,
)
from app.individual_ingestion.domain.models import (
    ACTIVE_SESSION_STATUSES,
    PENDING_ACTION_CONFIRM_CASE_IDENTITY,
    PENDING_ACTION_CONFIRM_CASE_OVERRIDE,
    PENDING_ACTION_CONFIRM_TYPE_OVERRIDE,
    SESSION_STATUS_ACTIVE,
    SESSION_STATUS_BLOCKED,
    SESSION_STATUS_CANCELLED,
    SESSION_STATUS_COMPLETED,
    SUPPORTED_INDIVIDUAL_SUPPORT_TYPES,
    TERMINAL_UPLOAD_STATUSES,
    UPLOAD_STATUS_BLOCKED_BASE_FAILED,
    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,
    UPLOAD_STATUS_WAITING_BASE_MATERIALIZATION,
)
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 _is_base_terminal_failure(status: str) -> bool:
    return status in {UPLOAD_STATUS_CANCELADO, UPLOAD_STATUS_FALLIDO, UPLOAD_STATUS_BLOCKED_BASE_FAILED}


def _is_story_type(document_type: str) -> bool:
    return _normalize_type(document_type) == "historia_clinica"


def _create_session_id() -> str:
    return f"manual-{uuid4().hex}"


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


def _needs_support_identity_warning(
    *,
    detected_type: str,
    extracted_patient_id: str,
    extracted_case_number: str,
    extracted_patient_name: str,
) -> bool:
    normalized_type = _normalize_type(detected_type)
    if normalized_type != "factura":
        return False
    return not any(
        [
            _normalize_type(extracted_patient_id),
            _normalize_type(extracted_case_number),
            _normalize_type(extracted_patient_name),
        ]
    )


def _serialize_session(session: dict[str, Any] | None) -> dict[str, Any] | None:
    if not session:
        return None
    uploads = []
    for item in session.get("uploads") or []:
        upload_id = str(item.get("_id") or "").strip()
        if not upload_id:
            continue
        uploads.append(IndividualUploadStatusView.from_record(upload_id, item).to_document())
    return IndividualUploadSessionView(
        session_id=str(session.get("session_id") or "").strip(),
        active_case_key=str(session.get("active_case_key") or "").strip(),
        base_upload_id=str(session.get("base_upload_id") or "").strip(),
        session_status=str(session.get("session_status") or "").strip(),
        base_ready=bool(session.get("base_ready")),
        blocked_by_base_failure=bool(session.get("blocked_by_base_failure")),
        updated_at=str(session.get("updated_at") or "").strip(),
        uploads=uploads,
    ).to_document()


@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 = "",
        session_id: str = "",
        lock_case_selection: bool = False,
    ) -> 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()
        normalized_session_id = str(session_id 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)
        requested_session = (
            self.repository.get_session(username, normalized_session_id) if normalized_session_id else None
        ) or {}
        requested_session_status = str(requested_session.get("session_status") or "").strip()
        use_existing_session = bool(
            not _is_story_type(normalized_type)
            and normalized_session_id
            and requested_session
            and requested_session_status in ACTIVE_SESSION_STATUSES
        )
        effective_session_id = normalized_session_id if use_existing_session else ""
        base_upload_id = ""
        depends_on_upload_id = ""
        session_case_key = ""
        session_status = ""
        if _is_story_type(normalized_type):
            effective_session_id = _create_session_id()
        elif use_existing_session:
            if bool(requested_session.get("blocked_by_base_failure")):
                raise ValueError("La sesión manual seleccionada está bloqueada por falla de la historia base.")
            base_upload_id = str(requested_session.get("base_upload_id") or "").strip()
            depends_on_upload_id = base_upload_id
            session_case_key = str(requested_session.get("active_case_key") or normalized_case_key).strip()
            session_status = requested_session_status or SESSION_STATUS_ACTIVE

        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,
            session_id=effective_session_id,
            session_status=SESSION_STATUS_ACTIVE if effective_session_id else session_status,
            session_origin="manual_upload_dialog" if effective_session_id else "",
            base_upload_id=base_upload_id,
            depends_on_upload_id=depends_on_upload_id,
            session_case_key=session_case_key,
            case_selection_locked=bool(lock_case_selection and normalized_case_key),
        )
        upload_id = self.repository.create_upload(payload.to_document())
        if _is_story_type(normalized_type):
            self.repository.update_upload(
                upload_id,
                IndividualUploadUpdatePayload(
                    base_upload_id=upload_id,
                    updated_at=created_at,
                ).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 GetActiveIndividualUploadSessionUseCase:
    repository: Any

    def execute(self, username: str) -> dict[str, Any] | None:
        session = self.repository.get_active_session(username)
        return _serialize_session(session)


@dataclass
class GetIndividualUploadSessionUseCase:
    repository: Any

    def execute(self, username: str, *, session_id: str) -> dict[str, Any] | None:
        session = self.repository.get_session(username, session_id)
        return _serialize_session(session)


@dataclass
class ListIndividualUploadSessionsUseCase:
    repository: Any

    def execute(self, username: str, *, include_terminal: bool = False) -> list[dict[str, Any]]:
        sessions = self.repository.list_sessions(username, include_terminal=include_terminal)
        return [serialized for item in sessions if (serialized := _serialize_session(item)) is not None]


@dataclass
class CloseIndividualUploadSessionUseCase:
    repository: Any
    colombia_tz: tzinfo

    def execute(self, *, username: str, session_id: str) -> dict[str, Any]:
        session = self.repository.get_session(username, session_id) or {}
        if not session:
            raise ValueError("Sesión manual no encontrada.")
        uploads = list(session.get("uploads") or [])
        if not uploads:
            raise ValueError("La sesión manual no contiene cargas.")
        non_terminal_uploads = [
            item
            for item in uploads
            if str(item.get("status") or "").strip() not in TERMINAL_UPLOAD_STATUSES
        ]
        if non_terminal_uploads:
            raise ValueError("Debes cancelar o completar todas las cargas antes de cerrar la sesión manual.")
        updated_at = now_iso(self.colombia_tz)
        for item in uploads:
            upload_id = str(item.get("_id") or "").strip()
            if not upload_id:
                continue
            self.repository.update_upload(
                upload_id,
                IndividualUploadUpdatePayload(
                    session_status=SESSION_STATUS_COMPLETED,
                    updated_at=updated_at,
                ).to_document(),
            )
        return (
            self.repository.get_session(username, session_id)
            or {
                "session_id": session_id,
                "session_status": SESSION_STATUS_COMPLETED,
                "uploads": [],
            }
        )


@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"},
                )
                case_key = str(getattr(resolution, "case_key", "") or "").strip()
                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:
                    base_upload_id = str(
                        record.get("depends_on_upload_id") or record.get("base_upload_id") or ""
                    ).strip()
                    base_upload = self.repository.get_upload(base_upload_id) or {}
                    if base_upload:
                        case_context = {
                            "case_key": str(
                                base_upload.get("case_key")
                                or base_upload.get("session_case_key")
                                or record.get("provided_case_key")
                                or case_key
                            ).strip(),
                            "case_number": str(
                                base_upload.get("confirmed_case_number")
                                or base_upload.get("provisional_case_number")
                                or ""
                            ).strip(),
                            "patient_id": str(
                                base_upload.get("confirmed_patient_id")
                                or base_upload.get("provisional_patient_id")
                                or ""
                            ).strip(),
                            "patient_name": str(
                                base_upload.get("confirmed_patient_name")
                                or base_upload.get("provisional_patient_name")
                                or ""
                            ).strip(),
                        }
                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 _needs_support_identity_warning(
                    detected_type=detected_type,
                    extracted_patient_id=str(signals.patient_id or ""),
                    extracted_case_number=str(signals.case_number or ""),
                    extracted_patient_name=str(signals.patient_name or ""),
                ):
                    warnings.append(
                        "No se pudo validar con confianza la identidad extraída de la factura contra el caso seleccionado."
                    )
                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,
                    session_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,
                case_key=str(record.get("case_key") or ""),
                session_case_key=str(record.get("case_key") or record.get("session_case_key") or ""),
                session_status=SESSION_STATUS_ACTIVE if str(record.get("session_id") or "").strip() else None,
                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,
                session_status=SESSION_STATUS_CANCELLED
                if str(record.get("base_upload_id") or "") == upload_id
                else None,
                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(),
        )
        if str(record.get("base_upload_id") or "").strip() == upload_id:
            self._block_dependent_uploads(username=username, base_upload_id=upload_id)
        return self.repository.get_upload(upload_id) or {}

    def _block_dependent_uploads(self, *, username: str, base_upload_id: str) -> None:
        for dependent in self.repository.list_dependent_uploads(username, base_upload_id):
            dependent_id = str(dependent.get("_id") or "").strip()
            if not dependent_id:
                continue
            if str(dependent.get("status") or "").strip() in TERMINAL_UPLOAD_STATUSES:
                continue
            self.file_store.delete_file(str(dependent.get("stored_path") or ""))
            self.repository.update_upload(
                dependent_id,
                IndividualUploadUpdatePayload(
                    status=UPLOAD_STATUS_BLOCKED_BASE_FAILED,
                    waiting_for_base_upload_id=base_upload_id,
                    blocked_by_base_failure=True,
                    session_status=SESSION_STATUS_BLOCKED,
                    stored_path="",
                    extracted_text="",
                    error="La historia clínica base de esta sesión fue cancelada o falló.",
                    failed_at=now_iso(self.colombia_tz),
                    updated_at=now_iso(self.colombia_tz),
                ).to_document(),
            )


@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,
            UPLOAD_STATUS_WAITING_BASE_MATERIALIZATION,
        }:
            return record
        if not _is_story_type(str(record.get("selected_document_type") or "")):
            dependency_result = self._guard_support_dependency(record)
            if dependency_result is not None:
                return dependency_result
        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,
                    session_status=SESSION_STATUS_ACTIVE
                    if str(record.get("session_id") or "").strip()
                    else str(record.get("session_status") or ""),
                    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 ""),
                    session_case_key=str(result.get("case_key") or record.get("session_case_key") or ""),
                    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 str(record.get("base_upload_id") or "").strip() == upload_id:
                self._release_dependent_uploads(updated)
            return updated
        except Exception as exc:
            logger.exception("Error materializando carga individual %s", upload_id)
            self.repository.update_upload(
                upload_id,
                IndividualUploadUpdatePayload(
                    status=UPLOAD_STATUS_FALLIDO,
                    session_status=SESSION_STATUS_BLOCKED
                    if str(record.get("base_upload_id") or "").strip() == upload_id
                    else str(record.get("session_status") or ""),
                    error=str(exc),
                    failed_at=now_iso(self.colombia_tz),
                    updated_at=now_iso(self.colombia_tz),
                ).to_document(),
            )
            failed = self.repository.get_upload(upload_id) or {}
            if str(record.get("base_upload_id") or "").strip() == upload_id:
                self._block_dependent_uploads(failed)
            return failed

    def _guard_support_dependency(self, record: dict[str, Any]) -> dict[str, Any] | None:
        username = str(record.get("usuario") or "").strip()
        base_upload_id = str(record.get("depends_on_upload_id") or record.get("base_upload_id") or "").strip()
        if not username or not base_upload_id:
            return None
        base_upload = self.repository.get_upload(base_upload_id) or {}
        base_status = str(base_upload.get("status") or "").strip()
        upload_id = str(record.get("_id") or "").strip()
        if base_status == UPLOAD_STATUS_COMPLETADO:
            if str(record.get("case_key") or "").strip() != str(base_upload.get("case_key") or "").strip():
                self.repository.update_upload(
                    upload_id,
                    IndividualUploadUpdatePayload(
                        case_key=str(base_upload.get("case_key") or ""),
                        session_case_key=str(base_upload.get("case_key") or ""),
                        updated_at=now_iso(self.colombia_tz),
                    ).to_document(),
                )
            return None
        if _is_base_terminal_failure(base_status):
            self.repository.update_upload(
                upload_id,
                IndividualUploadUpdatePayload(
                    status=UPLOAD_STATUS_BLOCKED_BASE_FAILED,
                    waiting_for_base_upload_id=base_upload_id,
                    blocked_by_base_failure=True,
                    session_status=SESSION_STATUS_BLOCKED,
                    error="La historia clínica base de esta sesión fue cancelada o falló.",
                    failed_at=now_iso(self.colombia_tz),
                    updated_at=now_iso(self.colombia_tz),
                ).to_document(),
            )
            return self.repository.get_upload(upload_id) or {}
        self.repository.update_upload(
            upload_id,
            IndividualUploadUpdatePayload(
                status=UPLOAD_STATUS_WAITING_BASE_MATERIALIZATION,
                waiting_for_base_upload_id=base_upload_id,
                blocked_by_base_failure=False,
                session_status=SESSION_STATUS_ACTIVE,
                updated_at=now_iso(self.colombia_tz),
                error="",
            ).to_document(),
        )
        return self.repository.get_upload(upload_id) or {}

    def _release_dependent_uploads(self, base_record: dict[str, Any]) -> None:
        username = str(base_record.get("usuario") or "").strip()
        base_upload_id = str(base_record.get("_id") or "").strip()
        if not username or not base_upload_id:
            return
        for dependent in self.repository.list_dependent_uploads(username, base_upload_id):
            dependent_id = str(dependent.get("_id") or "").strip()
            if not dependent_id:
                continue
            if str(dependent.get("status") or "").strip() not in {
                UPLOAD_STATUS_WAITING_BASE_MATERIALIZATION,
                UPLOAD_STATUS_LISTO_PARA_PROCESAR,
            }:
                continue
            self.repository.update_upload(
                dependent_id,
                IndividualUploadUpdatePayload(
                    status=UPLOAD_STATUS_LISTO_PARA_PROCESAR,
                    case_key=str(base_record.get("case_key") or ""),
                    session_case_key=str(base_record.get("case_key") or ""),
                    waiting_for_base_upload_id="",
                    blocked_by_base_failure=False,
                    error="",
                    updated_at=now_iso(self.colombia_tz),
                ).to_document(),
            )
            current = self.repository.get_upload(dependent_id) or {}
            if str(current.get("status") or "").strip() == UPLOAD_STATUS_LISTO_PARA_PROCESAR:
                self.execute(dependent_id)

    def _block_dependent_uploads(self, base_record: dict[str, Any]) -> None:
        username = str(base_record.get("usuario") or "").strip()
        base_upload_id = str(base_record.get("_id") or "").strip()
        if not username or not base_upload_id:
            return
        for dependent in self.repository.list_dependent_uploads(username, base_upload_id):
            dependent_id = str(dependent.get("_id") or "").strip()
            if not dependent_id:
                continue
            if str(dependent.get("status") or "").strip() in TERMINAL_UPLOAD_STATUSES:
                continue
            self.file_store.delete_file(str(dependent.get("stored_path") or ""))
            self.repository.update_upload(
                dependent_id,
                IndividualUploadUpdatePayload(
                    status=UPLOAD_STATUS_BLOCKED_BASE_FAILED,
                    waiting_for_base_upload_id=base_upload_id,
                    blocked_by_base_failure=True,
                    session_status=SESSION_STATUS_BLOCKED,
                    stored_path="",
                    extracted_text="",
                    error="La historia clínica base de esta sesión fue cancelada o falló.",
                    failed_at=now_iso(self.colombia_tz),
                    updated_at=now_iso(self.colombia_tz),
                ).to_document(),
            )
