from collections.abc import Callable
from dataclasses import dataclass
from time import sleep
from typing import Any, cast

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

from app.clinical_pipeline.domain.errors import HistoriaSummaryCoverageError
from app.clinical_pipeline.domain.models import (
    HistoriaProcessingMetrics,
    ProcessingFailureSource,
)
from app.config import config
from app.llm import LLMProviderError, 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_extraction_quality import assess_clinical_extraction
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_antecedentes import (
    antecedentes_to_prompt_text,
    extract_historia_antecedentes_structured,
)
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 = "v5"
_HISTORIA_PROMPT_PACK_VERSION = "v5"


@dataclass(frozen=True)
class PreparedHistoriaPrompt:
    prompt_text: str
    detailed_summary: str = ""
    processing_metrics: HistoriaProcessingMetrics | None = None


@dataclass(frozen=True)
class _SummaryGenerationFailure:
    source: ProcessingFailureSource
    provider: str
    model: str
    error_kind: str
    retryable: bool
    attempts: int
    retry_after_seconds: float = 0.0


def _parse_retry_delays(value: Any) -> list[float]:
    candidates = value if isinstance(value, (list, tuple)) else str(value or "").split(",")
    delays: list[float] = []
    for candidate in candidates:
        try:
            delays.append(max(0.0, float(candidate)))
        except (TypeError, ValueError):
            continue
    return delays or [2.0, 5.0]


def _resolve_summary_route(llm_router: Any, task: LLMTask) -> tuple[str, str]:
    if llm_router is None or not hasattr(llm_router, "_resolve_route"):
        return "gemini", ""
    try:
        route = llm_router._resolve_route(task)
        return str(route.provider or ""), str(route.model or "")
    except Exception:
        return "gemini", ""


def _retry_after_seconds(error: LLMProviderError) -> float:
    try:
        return max(0.0, float(error.details.get("retry_after_seconds") or 0))
    except (TypeError, ValueError):
        return 0.0


def _generate_historia_summary_with_retries(
    *,
    llm_router: Any,
    prompt: str,
    task: LLMTask,
    cache_repository: Any = None,
    username: str = "",
    max_retries: int,
    retry_delays: list[float],
    sleep_fn: Callable[[float], None],
) -> tuple[str, _SummaryGenerationFailure | None]:
    provider, model = _resolve_summary_route(llm_router, task)
    last_failure: _SummaryGenerationFailure | None = None
    total_attempts = max(0, max_retries) + 1
    for attempt_index in range(total_attempts):
        if attempt_index > 0:
            delay_index = min(attempt_index - 1, len(retry_delays) - 1)
            configured_delay = retry_delays[delay_index] if retry_delays else 0.0
            retry_after = last_failure.retry_after_seconds if last_failure else 0.0
            sleep_fn(max(configured_delay, retry_after))
        try:
            result = _generate_historia_summary_with_cache(
                llm_router=llm_router,
                prompt=prompt,
                task=task,
                cache_repository=cache_repository,
                username=username,
            )
            if result:
                return result, None
            last_failure = _SummaryGenerationFailure(
                source="external_provider",
                provider=provider,
                model=model,
                error_kind="empty_response",
                retryable=True,
                attempts=attempt_index + 1,
            )
        except LLMProviderError as exc:
            retry_after = _retry_after_seconds(exc)
            external = exc.kind.value not in {
                "invalid_configuration",
                "unsupported_capability",
            }
            last_failure = _SummaryGenerationFailure(
                source="external_provider" if external else "internal",
                provider=exc.provider or provider,
                model=exc.model or model,
                error_kind=exc.kind.value,
                retryable=bool(external and exc.retryable),
                attempts=attempt_index + 1,
                retry_after_seconds=retry_after,
            )
            if not external or not exc.retryable:
                break
        except Exception as exc:
            last_failure = _SummaryGenerationFailure(
                source="internal",
                provider=provider,
                model=model,
                error_kind=exc.__class__.__name__,
                retryable=False,
                attempts=attempt_index + 1,
            )
            break
    return "", last_failure


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


def _chunk_text(text: str, size: int, *, overlap: int = 0) -> list[str]:
    """Divide cronológicamente priorizando límites de página o párrafo."""

    source = str(text or "")
    if not source:
        return []
    normalized_size = max(500, int(size or 0))
    normalized_overlap = min(max(0, int(overlap or 0)), normalized_size // 4)
    chunks: list[str] = []
    start = 0
    while start < len(source):
        hard_end = min(len(source), start + normalized_size)
        end = hard_end
        if hard_end < len(source):
            search_start = start + int(normalized_size * 0.7)
            candidates = [
                source.rfind("\n\f\n", search_start, hard_end),
                source.rfind("\n\n", search_start, hard_end),
                source.rfind("\n", search_start, hard_end),
            ]
            boundary = max(candidates)
            if boundary > start:
                end = boundary + 1
        chunk = source[start:end]
        if chunk:
            chunks.append(chunk)
        if end >= len(source):
            break
        next_start = max(start + 1, end - normalized_overlap)
        start = next_start
    return chunks


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)
    antecedentes_section = antecedentes_to_prompt_text(extract_historia_antecedentes_structured(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("ANTECEDENTES ESTRUCTURADOS DETERMINISTAS", antecedentes_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 = "",
    return_metrics: bool = False,
    chunk_overlap: int | None = None,
    max_retries: int | None = None,
    retry_delays: list[float] | None = None,
    sleep_fn: Callable[[float], None] = sleep,
) -> str | tuple[str, HistoriaProcessingMetrics]:
    instrucciones = (
        "Resume la historia clinica manteniendo todos los datos clinicos críticos: "
        "diagnosticos, procedimientos, medicamentos (separando nombre, dosis, vía, frecuencia, duración, cantidad "
        "y estado administrado/formulado/ordenado cuando aparezcan), "
        "fechas, números, hallazgos y contexto relevante. No inventes datos. "
        "Entrega el resumen en espanol, en texto plano con secciones claras."
    )
    overlap = config.HISTORIA_SUMMARY_CHUNK_OVERLAP if chunk_overlap is None else chunk_overlap
    retries = config.HISTORIA_SUMMARY_MAX_RETRIES if max_retries is None else max_retries
    delays = _parse_retry_delays(
        config.HISTORIA_SUMMARY_RETRY_DELAYS_SECONDS if retry_delays is None else retry_delays
    )
    partes = _chunk_text(texto, summary_chunk, overlap=overlap)
    res_parcial = []
    failed_chunks: list[int] = []
    chunk_failures: list[_SummaryGenerationFailure] = []
    for idx, parte in enumerate(partes, start=1):
        prompt_resumen = f"{instrucciones}\n\nParte {idx} de {len(partes)}:\n{parte}"
        res_text, failure = _generate_historia_summary_with_retries(
            llm_router=llm_router,
            prompt=prompt_resumen,
            task=LLMTask.HISTORIA_CHUNK_SUMMARY,
            cache_repository=cache_repository,
            username=username,
            max_retries=retries,
            retry_delays=delays,
            sleep_fn=sleep_fn,
        )
        if res_text:
            res_parcial.append(res_text.strip())
        else:
            failed_chunks.append(idx)
            if failure:
                chunk_failures.append(failure)

    if failed_chunks:
        primary_failure = chunk_failures[0] if chunk_failures else None
        raise HistoriaSummaryCoverageError(
            "No fue posible resumir todos los bloques de la historia clínica.",
            failed_chunks=failed_chunks,
            stage=LLMTask.HISTORIA_CHUNK_SUMMARY.value,
            attempts=max((item.attempts for item in chunk_failures), default=max(0, retries) + 1),
            provider=primary_failure.provider if primary_failure else "",
            model=primary_failure.model if primary_failure else "",
            error_kind=primary_failure.error_kind if primary_failure else "",
            retryable=bool(chunk_failures) and all(item.retryable for item in chunk_failures),
            source=primary_failure.source if primary_failure else "internal",
        )

    if not res_parcial:
        raise HistoriaSummaryCoverageError("La historia clínica no produjo bloques resumibles.")

    consolidation_levels = 0
    current_summaries = list(res_parcial)
    reduce_budget = max(1000, int(config.HISTORIA_SUMMARY_REDUCE_MAX_CHARS or 0))
    final_instruction = (
        "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"
    )
    while len(current_summaries) > 1 or consolidation_levels == 0:
        groups: list[list[str]] = []
        current_group: list[str] = []
        current_chars = 0
        for summary in current_summaries:
            added_chars = len(summary) + (2 if current_group else 0)
            if current_group and current_chars + added_chars > reduce_budget:
                groups.append(current_group)
                current_group = []
                current_chars = 0
            current_group.append(summary)
            current_chars += added_chars
        if current_group:
            groups.append(current_group)
        if len(current_summaries) > 1 and len(groups) >= len(current_summaries):
            groups = [current_summaries[index : index + 2] for index in range(0, len(current_summaries), 2)]

        next_summaries: list[str] = []
        for group in groups:
            prompt_final = final_instruction + "\n\n".join(group)
            consolidated, failure = _generate_historia_summary_with_retries(
                llm_router=llm_router,
                prompt=prompt_final,
                task=LLMTask.HISTORIA_FINAL_SUMMARY,
                cache_repository=cache_repository,
                username=username,
                max_retries=retries,
                retry_delays=delays,
                sleep_fn=sleep_fn,
            )
            if not consolidated:
                raise HistoriaSummaryCoverageError(
                    "No fue posible consolidar los resúmenes de la historia clínica.",
                    stage=LLMTask.HISTORIA_FINAL_SUMMARY.value,
                    attempts=failure.attempts if failure else max(0, retries) + 1,
                    provider=failure.provider if failure else "",
                    model=failure.model if failure else "",
                    error_kind=failure.error_kind if failure else "",
                    retryable=bool(failure and failure.retryable),
                    source=failure.source if failure else "internal",
                )
            next_summaries.append(consolidated.strip())
        consolidation_levels += 1
        current_summaries = next_summaries

    metrics = HistoriaProcessingMetrics(
        source_characters=len(texto),
        covered_characters=len(texto),
        chunks_total=len(partes),
        chunks_succeeded=len(res_parcial),
        consolidation_levels=consolidation_levels,
        chunk_size=summary_chunk,
        chunk_overlap=overlap,
        chunk_model=config.GEMINI_MODEL_EXTRACT_LOW_RISK,
        final_model=config.GEMINI_MODEL_REASONING,
        failed_chunks=[],
    )
    result = current_summaries[0]
    return (result, metrics) if return_metrics else result


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,
    )
    resumen_previo = ""
    processing_metrics = HistoriaProcessingMetrics(
        source_characters=len(descripcion),
        covered_characters=len(descripcion),
        chunks_total=0,
        chunks_succeeded=0,
        chunk_size=summary_chunk,
        chunk_model=config.GEMINI_MODEL_EXTRACT_LOW_RISK,
        final_model=config.GEMINI_MODEL_REASONING,
        chunk_overlap=config.HISTORIA_SUMMARY_CHUNK_OVERLAP,
    )
    should_generate_summary = bool(summary_threshold and len(descripcion) > summary_threshold)
    if should_generate_summary:
        router = llm_router or _build_historia_llm_router()
        summary_result = _resumir_historia_larga(
            descripcion,
            summary_chunk=summary_chunk,
            llm_router=router,
            cache_repository=cache_repository,
            username=username,
            return_metrics=True,
        )
        if isinstance(summary_result, tuple):
            resumen_previo, processing_metrics = summary_result
        else:
            resumen_previo = str(summary_result or "")

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


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,
            )
            route_metadata = dict(getattr(result, "route_metadata", {}) or {})
            if prepared_prompt.processing_metrics is not None:
                route_metadata["historia_processing"] = prepared_prompt.processing_metrics.to_document()
            result.route_metadata = route_metadata
            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)
            result.quality_metadata = assess_clinical_extraction(
                document_type="historia_clinica",
                raw_text=descripcion,
                analysis_model=analysis_model,
            ).to_payload()
            return result
        except HistoriaSummaryCoverageError:
            raise
        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 ""
