from dataclasses import dataclass
from typing import cast

import pytz
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

from app.config import config
from app.llm import LLMTask, LLMTextRequest
from app.llm.providers import GeminiAdapter, GroqAdapter, OpenAIAdapterStub
from app.llm.renderers import render_clinical_document_html
from app.llm.routing import DefaultModelRouter, DefaultModelSelectionPolicy
from app.llm.schemas import HistoriaClinicaStructured, dump_clinical_model
from app.services.clinical_structured_extraction import ClinicalStructuredExtractionService
from app.services.context_budgeting import deduplicate_texts, limit_items, truncate_text
from app.services.deterministic_signals import extract_deterministic_signals
from app.services.historia_summary import (
    choose_historia_resumen,
    extract_historia_diagnosticos_text,
    extract_historia_medicamentos_text,
    extract_historia_motivo_text,
    extract_historia_procedimientos_text,
    extract_historia_resumen_text,
)
from app.services.llm_task_cache import build_llm_task_fingerprint


app = FastAPI()
colombia_tz = pytz.timezone("America/Bogota")
_HISTORIA_SUMMARY_PROMPT_VERSION = "v3"
_HISTORIA_PROMPT_PACK_VERSION = "v2"


@dataclass(frozen=True)
class PreparedHistoriaPrompt:
    prompt_text: str
    detailed_summary: str = ""


def _build_historia_llm_router() -> DefaultModelRouter:
    return DefaultModelRouter(
        providers={
            "gemini": GeminiAdapter(
                api_key=config.GEMINI_API_KEY,
                default_model=config.GEMINI_MODEL_DEFAULT,
                service="historia_clinica_gemini_provider",
            ),
            "groq": GroqAdapter(
                api_key=config.GROQ_API_KEY,
                default_model=config.GROQ_MODEL_DEFAULT,
                service="historia_clinica_groq_provider",
            ),
            "openai": OpenAIAdapterStub(enabled=False),
        },
        policy=DefaultModelSelectionPolicy(),
        service="historia_clinica_router",
    )


def _chunk_text(text: str, size: int) -> list[str]:
    return [text[i : i + size] for i in range(0, len(text), size)]


def _budget_historia_text(text: str, *, max_chars: int) -> str:
    lines, _ = deduplicate_texts(str(text or "").splitlines())
    budgeted_lines, _ = limit_items(lines, max_items=max(1, max_chars // 40) if max_chars else None)
    compacted = "\n".join(budgeted_lines)
    truncated, _ = truncate_text(compacted, max_chars=max_chars)
    return truncated


def _format_section(title: str, value: str) -> str:
    normalized = str(value or "").strip()
    if not normalized:
        return ""
    return f"[{title}]\n{normalized}"


def _build_identity_section(snapshot) -> str:
    values = []
    if snapshot.patient_name:
        values.append(f"Nombre del paciente: {snapshot.patient_name}")
    if snapshot.patient_id:
        values.append(f"Documento: {snapshot.patient_id}")
    if snapshot.case_number:
        values.append(f"Caso: {snapshot.case_number}")
    if snapshot.dates:
        values.append("Fechas relevantes: " + ", ".join(snapshot.dates[:5]))
    return "\n".join(values)


def _build_remaining_context(compacted_text: str, prioritized_sections: list[str], *, max_chars: int) -> str:
    if not compacted_text:
        return ""
    prioritized_lines = {
        line.strip().casefold()
        for section in prioritized_sections
        for line in section.splitlines()
        if line.strip() and not line.startswith("[")
    }
    remaining_lines = []
    for line in compacted_text.splitlines():
        stripped = line.strip()
        if not stripped:
            continue
        if stripped.casefold() in prioritized_lines:
            continue
        remaining_lines.append(stripped)
    remaining_text, _ = truncate_text("\n".join(remaining_lines), max_chars=max_chars)
    return remaining_text


def _should_expand_historia_summary(explicit_summary: str) -> bool:
    normalized = str(explicit_summary or "").strip()
    if not normalized:
        return False
    word_count = len(normalized.split())
    return word_count <= 45 or normalized == normalized.upper()


def _build_historia_prompt_pack(
    descripcion: str,
    *,
    snapshot,
    compacted_text: str,
    max_chars: int,
    previous_summary: str = "",
) -> str:
    identity_section = _build_identity_section(snapshot)
    motivo_section = extract_historia_motivo_text(descripcion)
    resumen_section = extract_historia_resumen_text(descripcion)
    diagnosticos_section = extract_historia_diagnosticos_text(descripcion)
    procedimientos_section = extract_historia_procedimientos_text(descripcion)
    medicamentos_section = extract_historia_medicamentos_text(descripcion)

    prioritized_sections = [
        _format_section("PACK HISTORIA CLINICA", f"version={_HISTORIA_PROMPT_PACK_VERSION}"),
        _format_section("IDENTIDAD CLINICA BASICA", identity_section),
        _format_section("MOTIVO DE CONSULTA", motivo_section),
        _format_section("RESUMEN CLINICO", resumen_section),
        _format_section("DIAGNOSTICOS", diagnosticos_section),
        _format_section("PROCEDIMIENTOS", procedimientos_section),
        _format_section("MEDICAMENTOS", medicamentos_section),
    ]
    if previous_summary:
        prioritized_sections.append(_format_section("RESUMEN PREVIO DEL DOCUMENTO COMPLETO", previous_summary))
    prioritized_sections = [section for section in prioritized_sections if section]
    fallback_context = _build_remaining_context(
        compacted_text,
        prioritized_sections,
        max_chars=max(800, max_chars // 3),
    )
    if fallback_context:
        prioritized_sections.append(_format_section("CONTEXTO ADICIONAL PRIORIZADO", fallback_context))
    packed_text = "\n\n".join(prioritized_sections)
    budgeted_prompt, _ = truncate_text(packed_text, max_chars=max_chars)
    return budgeted_prompt


def _resolve_historia_cache_context(*, llm_router, task: LLMTask, username: str) -> dict[str, str] | None:
    normalized_username = str(username or "").strip()
    if not normalized_username or llm_router is None or not hasattr(llm_router, "_resolve_route"):
        return None
    route = llm_router._resolve_route(task)
    return {"username": normalized_username, "provider": route.provider, "model": route.model}


def _generate_historia_summary_with_cache(
    *,
    llm_router,
    prompt: str,
    task: LLMTask,
    cache_repository=None,
    username: str = "",
) -> str:
    cache_context = _resolve_historia_cache_context(llm_router=llm_router, task=task, username=username)
    fingerprint = None
    if cache_repository is not None and cache_context is not None:
        fingerprint = build_llm_task_fingerprint(
            task=task.value,
            provider=cache_context["provider"],
            model=cache_context["model"],
            cache_version=getattr(cache_repository, "cache_version", "v1"),
            prompt_version=_HISTORIA_SUMMARY_PROMPT_VERSION,
            payload={"prompt": prompt},
        )
        cached = cache_repository.get(
            username=cache_context["username"],
            task=task.value,
            fingerprint=fingerprint,
        )
        if cached and isinstance(cached.get("payload"), dict):
            cached_text = str((cached["payload"] or {}).get("text") or "").strip()
            if cached_text:
                return cached_text

    response = llm_router.generate_text(
        LLMTextRequest(
            task=task,
            prompt=prompt,
            system_prompt="Eres un analista médico experto. Responde en texto plano controlado.",
        )
    )
    result_text = str(response.content or "").strip()
    if (
        result_text
        and cache_repository is not None
        and cache_context is not None
        and fingerprint is not None
    ):
        cache_repository.upsert(
            username=cache_context["username"],
            task=task.value,
            fingerprint=fingerprint,
            provider=cache_context["provider"],
            model=cache_context["model"],
            payload={"text": result_text},
            metadata={
                "prompt_version": _HISTORIA_SUMMARY_PROMPT_VERSION,
                "response_provider": response.provider,
                "response_model": response.model,
            },
        )
    return result_text


def _resumir_historia_larga(
    texto: str,
    *,
    summary_chunk: int,
    llm_router,
    cache_repository=None,
    username: str = "",
) -> str:
    instrucciones = (
        "Resume la historia clinica manteniendo todos los datos clinicos críticos: "
        "diagnosticos, procedimientos, medicamentos (con dosis/via/frecuencia si aparece), "
        "fechas, números, hallazgos y contexto relevante. No inventes datos. "
        "Entrega el resumen en espanol, en texto plano con secciones claras."
    )
    partes = _chunk_text(texto, summary_chunk)
    res_parcial = []
    for idx, parte in enumerate(partes, start=1):
        prompt_resumen = f"{instrucciones}\n\nParte {idx} de {len(partes)}:\n{parte}"
        res_text = _generate_historia_summary_with_cache(
            llm_router=llm_router,
            prompt=prompt_resumen,
            task=LLMTask.HISTORIA_CHUNK_SUMMARY,
            cache_repository=cache_repository,
            username=username,
        )
        if res_text:
            res_parcial.append(res_text.strip())

    if not res_parcial:
        return ""

    consolidado = "\n\n".join(res_parcial)
    prompt_final = (
        "Consolida los siguientes resúmenes parciales en un único resumen clínico "
        "detallado, coherente y completo. Preserva diagnósticos, procedimientos, "
        "medicamentos, fechas, cifras, hallazgos, antecedentes y evolución relevante. "
        "Corrige ortografía y puntuación. Entrega un texto narrativo clínico de uno o dos "
        "párrafos, sin viñetas ni encabezados, y no omitas datos relevantes.\n\n"
        f"{consolidado}"
    )
    return _generate_historia_summary_with_cache(
        llm_router=llm_router,
        prompt=prompt_final,
        task=LLMTask.HISTORIA_FINAL_SUMMARY,
        cache_repository=cache_repository,
        username=username,
    )


def _prepare_historia_prompt_payload(
    descripcion: str,
    *,
    summary_threshold: int,
    summary_chunk: int,
    max_chars: int,
    llm_router=None,
    cache_repository=None,
    username: str = "",
) -> PreparedHistoriaPrompt:
    snapshot = extract_deterministic_signals(
        descripcion,
        "historia_clinica",
        summary_threshold=summary_threshold,
        max_chars=max_chars,
    )
    compact_budget = min(max_chars or config.HISTORIA_MAX_CHARS, config.HISTORIA_COMPACT_MAX_CHARS)
    base_text = _budget_historia_text(
        snapshot.compacted_text or (descripcion or ""),
        max_chars=compact_budget,
    )
    explicit_summary = extract_historia_resumen_text(descripcion)

    resumen_previo = ""
    should_generate_summary = (
        summary_threshold
        and len(base_text) > summary_threshold
        and (not snapshot.should_skip_historia_summary or _should_expand_historia_summary(explicit_summary))
    )
    if should_generate_summary:
        try:
            router = llm_router or _build_historia_llm_router()
            resumen_previo = _resumir_historia_larga(
                base_text,
                summary_chunk=summary_chunk,
                llm_router=router,
                cache_repository=cache_repository,
                username=username,
            )
        except Exception:
            resumen_previo = ""

    return PreparedHistoriaPrompt(
        prompt_text=_build_historia_prompt_pack(
            descripcion,
            snapshot=snapshot,
            compacted_text=base_text,
            max_chars=max_chars,
            previous_summary=resumen_previo,
        ),
        detailed_summary=resumen_previo,
    )


def _prepare_historia_prompt_text(
    descripcion: str,
    *,
    summary_threshold: int,
    summary_chunk: int,
    max_chars: int,
    llm_router=None,
    cache_repository=None,
    username: str = "",
) -> str:
    return _prepare_historia_prompt_payload(
        descripcion,
        summary_threshold=summary_threshold,
        summary_chunk=summary_chunk,
        max_chars=max_chars,
        llm_router=llm_router,
        cache_repository=cache_repository,
        username=username,
    ).prompt_text


class HistoriaClinicaRequest(BaseModel):
    descripcion: str

    @staticmethod
    def extraer_historia_estructurada(
        descripcion: str,
        *,
        llm_router=None,
        cache_repository=None,
        username: str = "",
    ):
        descripcion = descripcion or ""
        summary_threshold = config.HISTORIA_SUMMARY_THRESHOLD
        summary_chunk = max(500, config.HISTORIA_SUMMARY_CHUNK)
        max_chars = config.HISTORIA_MAX_CHARS
        router = llm_router or _build_historia_llm_router()
        prepared_prompt = _prepare_historia_prompt_payload(
            descripcion,
            summary_threshold=summary_threshold,
            summary_chunk=summary_chunk,
            max_chars=max_chars,
            llm_router=router,
            cache_repository=cache_repository,
            username=username,
        )

        try:
            result = ClinicalStructuredExtractionService(
                llm_router=router,
                cache_repository=cache_repository,
            ).extract(
                raw_text=descripcion,
                document_type="historia_clinica",
                summary_text=prepared_prompt.prompt_text,
            )
            analysis_model = cast(HistoriaClinicaStructured, result.analysis_model)
            analysis_model.resumen_clinico = choose_historia_resumen(
                analysis_model.resumen_clinico,
                detailed_candidate=prepared_prompt.detailed_summary,
                raw_text=descripcion,
                analisis_html=result.rendered_html,
            )
            result.analysis_structured = dump_clinical_model(analysis_model)
            result.rendered_html = render_clinical_document_html(analysis_model)
            return result
        except Exception as e:
            proveedor = "llm_router"
            if hasattr(router, "_resolve_route"):
                try:
                    proveedor = router._resolve_route(LLMTask.CLINICAL_DOCUMENT_EXTRACT).provider
                except Exception:
                    proveedor = "llm_router"
            raise HTTPException(status_code=500, detail=f"Error usando {proveedor}: {e}")

    @staticmethod
    def analizar_historiaclinica(descripcion: str) -> str:
        return HistoriaClinicaRequest.extraer_historia_estructurada(descripcion).rendered_html

    @staticmethod
    def guardar_historia(descripcion: str, analisis: str) -> str:
        # Desactivado: esta colección no se usa en flujos posteriores.
        return ""
