from __future__ import annotations

import json
from dataclasses import dataclass
from typing import Any

from app.core.logging import get_log_context
from app.llm import LLMOutputKind, LLMStructuredRequest, LLMTask
from app.llm.renderers import render_clinical_document_html
from app.llm.schemas import (
    ClinicalStructuredModel,
    dump_clinical_model,
    get_clinical_document_model,
    get_clinical_schema_name,
)
from app.services.llm_task_cache import build_llm_task_fingerprint


_DOCUMENT_INSTRUCTIONS: dict[str, str] = {
    "historia_clinica": (
        "Extrae metadatos clínicos completos, resumen, diagnósticos, procedimientos y medicamentos. "
        "No inventes códigos. Si un código o fecha no aparece, omítelo o usa texto mínimo claro."
    ),
    "laboratorio": (
        "Extrae nombre del paciente, fecha del examen, tipo de examen, tabla de resultados, valores alterados, "
        "interpretación y observaciones."
    ),
    "radiologia": (
        "Extrae nombre del paciente, fecha del estudio, tipo de estudio, región anatómica, técnica, hallazgos y conclusión."
    ),
    "prescripcion": "Extrae fecha, medicamentos formulados y observaciones de la prescripción.",
    "quirurgico": (
        "Extrae fecha del procedimiento, diagnósticos, procedimientos realizados, hallazgos, descripción del procedimiento, "
        "complicaciones y conclusión."
    ),
    "factura": (
        "Extrae una factura clínica electrónica. Separa estrictamente proveedor, datos de factura, pagador, "
        "paciente, líneas facturadas y resumen financiero. En proveedor usa el nombre institucional y el NIT "
        "correctos. En datos de factura toma el número de factura únicamente del bloque Prefijo/No. Factura, "
        "no de resoluciones DIAN. Extrae Caso No., Convenio, Fecha Ingreso, Fecha Egreso, Vencimiento y el "
        "valor en letras solo si están explícitos. En las líneas facturadas conserva descripciones reales, "
        "códigos, cantidades, valores y clasifica sin inventar entre procedimientos, laboratorio, imagenología, "
        "hospitalización, honorarios, medicamentos y otros servicios."
    ),
    "generico": "Resume el documento y registra referencias diagnósticas relevantes sin inventar datos.",
}
_CLINICAL_EXTRACT_PROMPT_VERSION = "v1"
_DOCUMENT_PROMPT_VERSIONS: dict[str, str] = {
    "factura": "v2",
}


@dataclass(slots=True)
class StructuredClinicalAnalysisResult:
    document_type: str
    analysis_model: ClinicalStructuredModel
    analysis_structured: dict[str, Any]
    rendered_html: str
    provider: str
    model: str
    error_analisis: str | None = None
    metrics: dict[str, Any] | None = None


class ClinicalStructuredExtractionService:
    def __init__(self, *, llm_router: Any, cache_repository: Any = None) -> None:
        self._llm_router = llm_router
        self._cache_repository = cache_repository

    def extract(
        self,
        *,
        raw_text: str,
        document_type: str,
        summary_text: str | None = None,
    ) -> StructuredClinicalAnalysisResult:
        normalized_type = str(document_type or "generico").strip() or "generico"
        model_cls = get_clinical_document_model(normalized_type)
        schema_name = get_clinical_schema_name(normalized_type)
        schema_version = str(model_cls.model_fields["schema_version"].default)
        prompt_version = self._get_prompt_version(normalized_type)
        prompt = self._build_prompt(
            document_type=normalized_type,
            raw_text=raw_text,
            summary_text=summary_text,
            model_cls=model_cls,
            schema_name=schema_name,
        )
        cache_context = self._build_cache_context(
            task=LLMTask.CLINICAL_DOCUMENT_EXTRACT,
            document_type=normalized_type,
            schema_name=schema_name,
            schema_version=schema_version,
            source_text=summary_text or raw_text,
            prompt_version=prompt_version,
        )
        if cache_context is not None:
            cached = self._cache_repository.get(
                username=cache_context["username"],
                task=LLMTask.CLINICAL_DOCUMENT_EXTRACT.value,
                fingerprint=cache_context["fingerprint"],
            )
            if cached and isinstance(cached.get("payload"), dict):
                return self._hydrate_cached_result(
                    document_type=normalized_type,
                    payload=cached["payload"],
                    provider=str(cached.get("provider") or cache_context["provider"]),
                    model=str(cached.get("model") or cache_context["model"]),
                )
        response = self._llm_router.generate_structured(
            LLMStructuredRequest(
                task=LLMTask.CLINICAL_DOCUMENT_EXTRACT,
                prompt=prompt,
                output_model=model_cls,
                output_kind=LLMOutputKind.STRUCTURED_OBJECT,
                system_prompt=(
                    "Eres un extractor clínico. Responde únicamente un objeto JSON válido que cumpla el esquema indicado. "
                    "No uses Markdown, HTML ni texto adicional."
                ),
                metadata={"document_type": normalized_type, "schema_name": schema_name},
            )
        )
        analysis_model = model_cls.model_validate(response.content)
        rendered_html = render_clinical_document_html(analysis_model)
        result = StructuredClinicalAnalysisResult(
            document_type=normalized_type,
            analysis_model=analysis_model,
            analysis_structured=dump_clinical_model(analysis_model),
            rendered_html=rendered_html,
            provider=response.provider,
            model=response.model,
            metrics=response.metrics,
        )
        if cache_context is not None:
            self._cache_repository.upsert(
                username=cache_context["username"],
                task=LLMTask.CLINICAL_DOCUMENT_EXTRACT.value,
                fingerprint=cache_context["fingerprint"],
                provider=cache_context["provider"],
                model=cache_context["model"],
                payload={
                    "analysis_structured": result.analysis_structured,
                    "rendered_html": result.rendered_html,
                    "analysis_schema": analysis_model.__class__.__name__,
                    "analysis_schema_version": analysis_model.schema_version,
                    "analysis_render_version": "v1",
                },
                metadata={
                    "document_type": normalized_type,
                    "schema_name": schema_name,
                    "schema_version": schema_version,
                    "prompt_version": prompt_version,
                    "response_provider": response.provider,
                    "response_model": response.model,
                },
            )
        return result

    def _build_prompt(
        self,
        *,
        document_type: str,
        raw_text: str,
        summary_text: str | None,
        model_cls: type[ClinicalStructuredModel],
        schema_name: str,
    ) -> str:
        source_text = str(summary_text or raw_text or "").strip()
        schema_json = json.dumps(model_cls.model_json_schema(by_alias=True), ensure_ascii=False, separators=(",", ":"))
        instructions = _DOCUMENT_INSTRUCTIONS.get(document_type, _DOCUMENT_INSTRUCTIONS["generico"])
        return (
            f"Tarea: extraer {document_type}.\n"
            f"Versión de prompt: {self._get_prompt_version(document_type)}.\n"
            f"Contrato: devuelve un único JSON válido para el esquema {schema_name}.\n"
            f"Reglas: {instructions}\n"
            "Omite campos desconocidos, no inventes datos, usa listas vacías si no hay elementos.\n"
            f"Esquema JSON: {schema_json}\n\n"
            f"Documento fuente:\n{source_text}"
        )

    def _build_cache_context(
        self,
        *,
        task: LLMTask,
        document_type: str,
        schema_name: str,
        schema_version: str,
        source_text: str | None,
        prompt_version: str,
    ) -> dict[str, str] | None:
        username = str(get_log_context().get("username") or "").strip()
        if not username or self._cache_repository is None:
            return None
        route = self._llm_router._resolve_route(task) if hasattr(self._llm_router, "_resolve_route") else None
        if route is None:
            return None
        return {
            "username": username,
            "provider": route.provider,
            "model": route.model,
            "fingerprint": build_llm_task_fingerprint(
                task=task.value,
                provider=route.provider,
                model=route.model,
                cache_version=getattr(self._cache_repository, "cache_version", "v1"),
                prompt_version=prompt_version,
                schema_version=schema_version,
                payload={
                    "document_type": document_type,
                    "schema_name": schema_name,
                    "source_text": source_text or "",
                },
            ),
        }

    def _get_prompt_version(self, document_type: str) -> str:
        normalized = str(document_type or "").strip().lower() or "generico"
        return _DOCUMENT_PROMPT_VERSIONS.get(normalized, _CLINICAL_EXTRACT_PROMPT_VERSION)

    def _hydrate_cached_result(
        self,
        *,
        document_type: str,
        payload: dict[str, Any],
        provider: str,
        model: str,
    ) -> StructuredClinicalAnalysisResult:
        analysis_structured = dict(payload.get("analysis_structured") or {})
        model_cls = get_clinical_document_model(document_type)
        analysis_model = model_cls.model_validate(analysis_structured)
        rendered_html = str(payload.get("rendered_html") or "") or render_clinical_document_html(analysis_model)
        return StructuredClinicalAnalysisResult(
            document_type=document_type,
            analysis_model=analysis_model,
            analysis_structured=dump_clinical_model(analysis_model),
            rendered_html=rendered_html,
            provider=provider,
            model=model,
            metrics={"source": "cache"},
        )
