from __future__ import annotations

import asyncio
from dataclasses import dataclass
from types import SimpleNamespace

import pandas as pd
import pytest

from app.case_epicrisis.application.curation import (
    apply_curation_decision,
    build_document_objective_data,
    curation_context_payload,
    dump_curation_model,
    reconcile_case_objective_data,
)
from app.case_epicrisis.domain.curation import (
    CandidateOrigin,
    CatalogReference,
    CodeSystem,
    CurationDecisionType,
    CurationStatus,
    ObjectiveData,
    ObjectiveDiagnosis,
    ProcedureState,
)
from app.routes.pages import epicrisis as epicrisis_routes
from app.routes.pages.epicrisis_pdf_cache import guardar_epicrisis_pdf_cache
from app.services.clinical_document_service import ClinicalDocumentService
from app.services.deterministic_signals import DeterministicSignalSnapshot


@dataclass
class FakeCatalogRegistry:
    codes: dict[CodeSystem, set[str]]

    def reference(self, system: CodeSystem) -> CatalogReference:
        return CatalogReference(
            system=system,
            index_name=f"{system.value}_fixture",
            fingerprint=f"fingerprint-{system.value}",
            source_name="fixture",
        )

    def references(self) -> list[CatalogReference]:
        return [
            self.reference(CodeSystem.CIE10),
            self.reference(CodeSystem.CUPS),
            self.reference(CodeSystem.SOAT),
        ]

    def contains(self, system: CodeSystem, code: str) -> bool:
        return code in self.codes.get(system, set())

    def description_matches(self, system: CodeSystem, code: str, description: str) -> bool:
        return self.contains(system, code) and bool(description.strip())


def _registry() -> FakeCatalogRegistry:
    return FakeCatalogRegistry(
        {
            CodeSystem.CIE10: {"S422", "S423"},
            CodeSystem.CUPS: {
                "793101",
                "808112",
                "804001",
                "817204",
                "793203",
                "836301",
                "765401",
                "818307",
            },
            CodeSystem.SOAT: set(),
        }
    )


def _signals(*cups: tuple[str, int]) -> DeterministicSignalSnapshot:
    return DeterministicSignalSnapshot(
        document_type="quirurgico",
        inline_cups=[
            {
                "codigo_cups": code,
                "procedimiento": f"Procedimiento explícito {code}",
                "page": page,
                "excerpt": f"{code} procedimiento quirúrgico",
            }
            for code, page in cups
        ]
    )


def test_julio_preserves_explicit_surgical_cups_and_invoice_differences():
    registry = _registry()
    qx = {
        "_id": "julio-qx",
        "tipo_documento": "quirurgico",
        "nombre_archivo": "cirugia-julio.pdf",
        "codigos_cups": [
            {"codigo_cups": code, "procedimiento": f"Procedimiento explícito {code}"}
            for code in ("793101", "808112", "804001", "817204")
        ],
    }
    qx_objective = build_document_objective_data(
        qx,
        catalog_registry=registry,
        deterministic_signals=_signals(
            ("793101", 5),
            ("808112", 5),
            ("804001", 6),
            ("817204", 6),
        ),
    )

    assert {item.codigo_cups for item in qx_objective.procedures} == {
        "793101",
        "808112",
        "804001",
        "817204",
    }
    assert all(item.status == CurationStatus.AUTO_VALIDATED for item in qx_objective.procedures)
    assert {item.evidence[0].page for item in qx_objective.procedures} == {5, 6}

    invoice = {
        "_id": "julio-invoice",
        "tipo_documento": "factura",
        "nombre_archivo": "factura-julio.pdf",
        "factura_json": {
            "servicios_procedimientos": {
                "procedimientos_quirurgicos": [
                    {
                        "codigo_cups": code,
                        "codigo_facturacion": f"INT-{code}",
                        "descripcion": f"Procedimiento explícito {code}",
                    }
                    for code in ("793101", "817204", "765401", "818307")
                ]
            }
        },
    }
    invoice_objective = build_document_objective_data(invoice, catalog_registry=registry)
    case = reconcile_case_objective_data(
        [
            {"datos_objetivos": dump_curation_model(qx_objective)},
            {"datos_objetivos": dump_curation_model(invoice_objective)},
        ]
    )

    assert case is not None
    merged = {item.codigo_cups: item for item in case.procedures}
    assert set(merged) == {"793101", "808112", "804001", "817204", "765401", "818307"}
    assert set(merged["793101"].states) == {ProcedureState.PERFORMED, ProcedureState.BILLED}
    assert merged["793101"].codigo_facturacion == "INT-793101"
    assert ProcedureState.BILLED not in merged["808112"].states


def test_diagnosis_rejects_cross_system_codes():
    with pytest.raises(ValueError, match="CIE-10"):
        ObjectiveDiagnosis(
            item_id="invalid-system",
            code_system=CodeSystem.CUPS,
            code="793101",
            description="Procedimiento numérico usado como diagnóstico",
            origin=CandidateOrigin.LLM,
            status=CurationStatus.PENDING_REVIEW,
        )


def test_monica_anatomy_conflict_blocks_formal_publication():
    registry = _registry()
    objective = ObjectiveData(
        diagnoses=[
            ObjectiveDiagnosis(
                item_id="dx-proximal",
                code="S422",
                description="Fractura de la epífisis superior del húmero",
                original_text="fractura de húmero proximal",
                origin=CandidateOrigin.EXPLICIT,
                status=CurationStatus.AUTO_VALIDATED,
                catalog=registry.reference(CodeSystem.CIE10),
            ),
            ObjectiveDiagnosis(
                item_id="dx-diaphysis",
                code="S423",
                description="Fractura de la diáfisis del húmero",
                original_text="fractura de la diáfisis del húmero",
                origin=CandidateOrigin.EXPLICIT,
                status=CurationStatus.AUTO_VALIDATED,
                catalog=registry.reference(CodeSystem.CIE10),
            ),
        ],
        catalogs=registry.references(),
    )
    case = reconcile_case_objective_data([{"datos_objetivos": dump_curation_model(objective)}])

    assert case is not None
    assert case.generation_blocked is True
    assert case.conflicts[0].blocking is True
    assert {item.status for item in case.diagnoses} == {CurationStatus.CONFLICT}
    assert curation_context_payload(case)["pdf_primary_diagnosticos"] == []


def test_external_cups_failure_preserves_uncoded_procedures_as_retryable():
    objective = build_document_objective_data(
        {
            "_id": "1390913-history",
            "tipo_documento": "historia_clinica",
            "procedimientos_extraidos": ["Reducción de fractura", "Inmovilización"],
            "error_cups": "503 Service Unavailable",
        },
        catalog_registry=_registry(),
    )

    assert objective.processing_status == "partial"
    assert objective.retryable_errors == ["cups_provider_unavailable"]
    assert len(objective.procedures) == 2
    assert all(
        item.status == CurationStatus.EXTERNAL_SERVICE_PENDING for item in objective.procedures
    )


def test_human_decision_is_auditable_and_uses_optimistic_version():
    objective = ObjectiveData(
        diagnoses=[
            ObjectiveDiagnosis(
                item_id="dx-review",
                code="S423",
                description="Fractura de húmero",
                origin=CandidateOrigin.RAG,
                status=CurationStatus.PENDING_REVIEW,
            )
        ]
    )
    case = reconcile_case_objective_data([{"datos_objetivos": dump_curation_model(objective)}])
    assert case is not None

    with pytest.raises(ValueError, match="recarga"):
        apply_curation_decision(
            case,
            item_id="dx-review",
            decision=CurationDecisionType.APPROVE,
            actor="auditor",
            expected_version="stale",
        )

    previous_version = case.version
    updated = apply_curation_decision(
        case,
        item_id="dx-review",
        decision=CurationDecisionType.CORRECT,
        actor="auditor",
        expected_version=previous_version,
        corrected_code="S422",
        corrected_description="Fractura proximal del húmero",
    )
    diagnosis = updated.diagnoses[0]
    assert diagnosis.status == CurationStatus.CORRECTED
    assert diagnosis.code == "S422"
    assert diagnosis.decisions[0].original_code == "S423"
    assert diagnosis.decisions[0].actor == "auditor"
    assert updated.version != previous_version


def test_transient_cups_provider_retries_without_real_sleep():
    class TransientRetriever:
        def __init__(self):
            self.calls = 0

        def asignar_codigos(self, procedures):
            self.calls += 1
            if self.calls < 3:
                raise RuntimeError("503 Service Unavailable")
            return pd.DataFrame([{"codigo_cups": "793101", "procedimiento": procedures[0]}])

    service = object.__new__(ClinicalDocumentService)
    service.cups_retriever = TransientRetriever()
    delays = []
    service.sleep_fn = delays.append

    result = service._assign_cups_with_retry(["Reducción abierta"])

    assert result.iloc[0]["codigo_cups"] == "793101"
    assert service.cups_retriever.calls == 3
    assert delays == [2, 8]


def test_pdf_cache_route_preserves_user_selection_without_consulting_curation():
    inserted_documents = []

    class CacheCollection:
        def insert_one(self, document):
            inserted_documents.append(document)
            return SimpleNamespace(inserted_id="pdf-cache-1")

    class CaseService:
        def get_cached_case_context(self, username, case_key):
            raise AssertionError("La generación del PDF no debe consultar la curación.")

    class JsonRequest:
        async def json(self):
            return {
                "html_pdf": "<html><body>Epicrisis</body></html>",
                "nombre_paciente": "Paciente",
                "case_key": "monica",
                "diagnosticos": ["T870 - Diagnóstico agregado manualmente"],
                "procedimientos": [
                    {"codigo_cups": "", "descripcion": "Procedimiento agregado manualmente"}
                ],
            }

    services = SimpleNamespace(
        case_epicrisis_service=CaseService(),
        mongo_analyses=SimpleNamespace(
            collection=SimpleNamespace(
                database={"epicrisis_pdf_cache": CacheCollection()}
            )
        ),
    )

    result = asyncio.run(
        guardar_epicrisis_pdf_cache(
            JsonRequest(),
            SimpleNamespace(username="auditor"),
            services,
        )
    )

    assert result["cache_id"] == "pdf-cache-1"
    assert inserted_documents[0]["diagnosticos"] == ["T870 - Diagnóstico agregado manualmente"]
    assert inserted_documents[0]["procedimientos"] == ["Procedimiento agregado manualmente"]


def test_pdf_cache_route_allows_unresolved_curation_without_publishable_clinical_items():
    inserted_documents = []

    class CacheCollection:
        def insert_one(self, document):
            inserted_documents.append(document)
            return SimpleNamespace(inserted_id="pdf-cache-empty")

    class CaseService:
        def get_cached_case_context(self, username, case_key):
            return {
                "contexto": {
                    "curation_schema_version": "v1",
                    "curation_generation_blocked": True,
                    "pdf_primary_diagnosticos": [],
                    "pdf_primary_procedimientos": [],
                }
            }

    class JsonRequest:
        async def json(self):
            return {
                "html_pdf": "<html><body>Resumen clínico</body></html>",
                "nombre_paciente": "Paciente",
                "case_key": "monica",
                "diagnosticos": [],
                "procedimientos": [],
            }

    services = SimpleNamespace(
        case_epicrisis_service=CaseService(),
        mongo_analyses=SimpleNamespace(
            collection=SimpleNamespace(
                database={"epicrisis_pdf_cache": CacheCollection()}
            )
        ),
    )

    result = asyncio.run(
        guardar_epicrisis_pdf_cache(
            JsonRequest(),
            SimpleNamespace(username="auditor"),
            services,
        )
    )

    assert result["cache_id"] == "pdf-cache-empty"
    assert inserted_documents[0]["diagnosticos"] == []
    assert inserted_documents[0]["procedimientos"] == []


def test_curation_routes_forward_versioned_human_decisions(monkeypatch):
    calls = []

    class CaseService:
        def get_case_curation(self, username, case_key):
            return {"curation_version": "v-current", "diagnosticos_curados": []}

        def update_case_curation_item(self, username, case_key, **kwargs):
            calls.append((username, case_key, kwargs))
            return {"version": "v-next"}

    class JsonRequest:
        async def json(self):
            return {
                "decision": "correct",
                "expected_version": "v-current",
                "corrected_code": "S422",
                "corrected_description": "Fractura proximal",
            }

    monkeypatch.setattr(
        epicrisis_routes,
        "_resolve_real_case_key",
        lambda **kwargs: f"real-{kwargs['visible_case_key']}",
    )
    services = SimpleNamespace(case_epicrisis_service=CaseService())
    user = SimpleNamespace(username="auditor")

    queried = asyncio.run(
        epicrisis_routes.obtener_curacion_epicrisis(
            "visible-case",
            current_user=user,
            services=services,
        )
    )
    updated = asyncio.run(
        epicrisis_routes.actualizar_item_curacion_epicrisis(
            "visible-case",
            "dx-1",
            JsonRequest(),
            current_user=user,
            services=services,
        )
    )

    assert queried["curation_version"] == "v-current"
    assert updated["curation"]["version"] == "v-next"
    assert calls[0][1] == "real-visible-case"
    assert calls[0][2]["expected_version"] == "v-current"
    assert calls[0][2]["corrected_code"] == "S422"
