"""
predictor.py — Calculadora Nivel 1 de ypN0 (respuesta axilar completa)
=======================================================================

Modelo logistico simple para predecir la probabilidad de respuesta
patologica completa axilar (ypN0) tras neoadyuvancia en cancer de mama
con axila clinicamente positiva (cN+) al diagnostico.

Coeficientes derivados de:
- van Amstel BJS 2024 (PMID 39302345)  -> efecto subtipo
- Boughey 2022 Ki67 (PMID 35569077)    -> Ki67 en luminales
- Boughey 2022 genomico (PMID 35876925)-> riesgo genomico en HR+/HER2-
- An NCDB 2025 (PMID 40684017)         -> edad, inmunoterapia
- KEYNOTE-522 (Schmid 2020)            -> pembrolizumab en TNBC
- TRAIN-2 / Chowdhary                  -> pertuzumab en HER2+
- NeoSTOP / GeparSixto                 -> carboplatino en TNBC
- Cabioglu NEOSENTITURK 2025           -> cN2-3 vs cN1
- Samiei JAMA Surg 2021                -> tasas basales por subtipo

NIVEL 1 = combinacion deterministica de ORs publicados, sin
recalibracion con pacientes individuales. NO sustituye al juicio
clinico ni a herramientas validadas externamente.
"""

from __future__ import annotations

import math
from typing import Optional


# ---------------------------------------------------------------------------
# COEFICIENTES (log-odds) DERIVADOS DE ORs PUBLICADOS
# ---------------------------------------------------------------------------
# El intercepto se calibra para que el paciente "ancla" (HR+/HER2-, cN1,
# edad 50, Ki67 20%, NAC estandar sin agentes especiales, riesgo genomico
# desconocido) tenga una probabilidad de ypN0 ~10% (consistente con la
# tasa baja en luminales -- Samiei 2021 reporta ~18% global, van Amstel
# RISAS reporta 7% en HR+/HER2- en cN+).
#
# log(odds_ancla) = log(0.10/0.90) = -2.197

_COEFFICIENTS = {
    # Intercepto (paciente ancla)
    "intercept": math.log(0.10 / 0.90),  # -2.197

    # Subtipo molecular (referencia: HR+/HER2-) -- van Amstel BJS 2024
    "subtype": {
        "HR+/HER2-": math.log(1.0),     # 0 (referencia)
        "HR+/HER2+": math.log(14.82),   # 2.696
        "HR-/HER2+": math.log(40.0),    # 3.689
        "TNBC":      math.log(6.91),    # 1.933
    },

    # Ki67 -- solo aplica en HR+/HER2- (Boughey 2022, PMID 35569077)
    # Tasas brutas 28.4% (Ki67>=20%) vs 8.1% (<20%) en HR+/HER2-
    # OR derivado ~4.5; usamos OR=3 conservador (consistente con prompt)
    "ki67_high_luminal": math.log(3.0),

    # Edad: OR 0.99 por anio (An NCDB 2025) -- centrada en 50
    "age_per_year": math.log(0.99),

    # cN: OR 0.65 para cN2-3 vs cN1
    "cN23_vs_cN1": math.log(0.65),

    # Regimenes adicionales (aditivos en log-odds)
    "pertuzumab_in_HER2pos": math.log(1.5),   # TRAIN-2 / Chowdhary
    "carboplatin_in_TNBC":  math.log(1.4),    # NeoSTOP / GeparSixto
    # KEYNOTE-522: nodal pCR 76.7% vs 69.9% en TNBC cN+
    # OR = (0.767/0.233)/(0.699/0.301) = 3.292/2.322 = 1.418 -> ~1.42
    "pembrolizumab_in_TNBC": math.log(1.42),

    # Riesgo genomico (solo HR+/HER2-) -- Boughey 2022 PMID 35876925
    # Alto vs bajo: OR ~2 (mas pCR), bajo vs alto: OR 0.49
    "genomic_high_luminal": math.log(2.0),
    "genomic_low_luminal":  math.log(0.5),
}


# ---------------------------------------------------------------------------
# VALORES VALIDOS PARA LAS ENTRADAS
# ---------------------------------------------------------------------------
VALID_SUBTYPES = ("HR+/HER2-", "HR+/HER2+", "HR-/HER2+", "TNBC")
VALID_CN = ("cN1", "cN2", "cN3")
VALID_REGIMENS = ("standard_NAC", "anthra_taxane", "TC", "TCH", "AC_T",
                  "dose_dense_AC_T", "other")
VALID_GENOMIC = ("unknown", "low", "high")


# ---------------------------------------------------------------------------
# CLASE PRINCIPAL
# ---------------------------------------------------------------------------
class LogisticPredictor:
    """
    Predictor logistico de ypN0 (nivel 1).

    Uso:
        pred = LogisticPredictor()
        out = pred.predict(
            subtype="TNBC", ki67_pct=70, age=45, cN_stage="cN1",
            regimen="AC_T", has_pertuzumab=False, has_carboplatin=True,
            has_pembrolizumab=True, genomic_risk="unknown"
        )
        print(out["probability"], out["risk_band"], out["confidence"])
    """

    def __init__(self) -> None:
        self.coef = _COEFFICIENTS

    # ------------------------------------------------------------------
    # Metodo principal
    # ------------------------------------------------------------------
    def predict(
        self,
        subtype: str,
        ki67_pct: Optional[float],
        age: float,
        cN_stage: str,
        regimen: str,
        has_pertuzumab: bool,
        has_carboplatin: bool,
        has_pembrolizumab: bool,
        genomic_risk: str = "unknown",
    ) -> dict:
        """Devuelve dict con probability, risk_band, confidence y detalles."""

        # --- Validacion -------------------------------------------------
        if subtype not in VALID_SUBTYPES:
            raise ValueError(f"subtype invalido: {subtype}. Validos: {VALID_SUBTYPES}")
        if cN_stage not in VALID_CN:
            raise ValueError(f"cN_stage invalido: {cN_stage}. Validos: {VALID_CN}")
        if genomic_risk not in VALID_GENOMIC:
            raise ValueError(f"genomic_risk invalido: {genomic_risk}. Validos: {VALID_GENOMIC}")
        if age < 18 or age > 100:
            raise ValueError(f"edad fuera de rango (18-100): {age}")

        # --- Suma log-odds ---------------------------------------------
        logit = self.coef["intercept"]
        contributions: dict[str, float] = {"intercept (ancla 10%)": logit}

        # 1) Subtipo
        c_subtype = self.coef["subtype"][subtype]
        logit += c_subtype
        contributions[f"subtipo ({subtype})"] = c_subtype

        # 2) Ki67 (solo HR+/HER2-)
        if subtype == "HR+/HER2-" and ki67_pct is not None and ki67_pct >= 20:
            c = self.coef["ki67_high_luminal"]
            logit += c
            contributions["Ki67 >=20% (luminal)"] = c

        # 3) Edad (centrada en 50)
        c_age = (age - 50.0) * self.coef["age_per_year"]
        logit += c_age
        contributions[f"edad ({age:.0f}, vs 50)"] = c_age

        # 4) cN
        if cN_stage in ("cN2", "cN3"):
            c = self.coef["cN23_vs_cN1"]
            logit += c
            contributions[f"{cN_stage} (vs cN1)"] = c

        # 5) Regimenes -- aplican solo si el subtipo corresponde
        is_HER2pos = subtype in ("HR+/HER2+", "HR-/HER2+")
        is_TNBC = subtype == "TNBC"

        if has_pertuzumab and is_HER2pos:
            c = self.coef["pertuzumab_in_HER2pos"]
            logit += c
            contributions["pertuzumab (HER2+)"] = c

        if has_carboplatin and is_TNBC:
            c = self.coef["carboplatin_in_TNBC"]
            logit += c
            contributions["carboplatino (TNBC)"] = c

        if has_pembrolizumab and is_TNBC:
            c = self.coef["pembrolizumab_in_TNBC"]
            logit += c
            contributions["pembrolizumab (TNBC, KEYNOTE-522)"] = c

        # 6) Riesgo genomico (solo HR+/HER2-)
        if subtype == "HR+/HER2-":
            if genomic_risk == "high":
                c = self.coef["genomic_high_luminal"]
                logit += c
                contributions["riesgo genomico alto"] = c
            elif genomic_risk == "low":
                c = self.coef["genomic_low_luminal"]
                logit += c
                contributions["riesgo genomico bajo"] = c

        # --- Probabilidad ---------------------------------------------
        prob = 1.0 / (1.0 + math.exp(-logit))

        # --- Banda de riesgo -------------------------------------------
        if prob < 0.20:
            band = "low"
        elif prob < 0.50:
            band = "medium"
        else:
            band = "high"

        # --- Confianza (data quality heuristic) -----------------------
        # Reglas: cN3 o TNBC con genomic_risk = confianza media-baja;
        # subtipo HR-/HER2+ tiene IC amplios (OR=40, sin IC en abstract).
        confidence = "medium"
        if subtype == "HR-/HER2+":
            # OR=40 es muy alto y van Amstel no reporta IC; baja confianza
            confidence = "low"
        elif subtype == "HR+/HER2-" and (
            ki67_pct is None or genomic_risk == "unknown"
        ):
            confidence = "medium"
        elif subtype in ("HR+/HER2+", "TNBC") and cN_stage == "cN1":
            confidence = "high"
        if cN_stage == "cN3":
            confidence = "low"

        return {
            "probability": round(prob, 4),
            "probability_pct": f"{prob * 100:.1f}%",
            "risk_band": band,
            "confidence": confidence,
            "log_odds": round(logit, 4),
            "contributions_logodds": {k: round(v, 4) for k, v in contributions.items()},
        }


# ---------------------------------------------------------------------------
# VINETAS CLINICAS
# ---------------------------------------------------------------------------
_VIGNETTES = [
    {
        "label": "1. HR+/HER2- luminal A, edad 65, Ki67 10%, cN1, NAC estandar",
        "args": dict(subtype="HR+/HER2-", ki67_pct=10, age=65, cN_stage="cN1",
                     regimen="AC_T", has_pertuzumab=False,
                     has_carboplatin=False, has_pembrolizumab=False,
                     genomic_risk="unknown"),
    },
    {
        "label": "2. HR+/HER2- luminal B, edad 45, Ki67 30%, cN1, NAC estandar",
        "args": dict(subtype="HR+/HER2-", ki67_pct=30, age=45, cN_stage="cN1",
                     regimen="AC_T", has_pertuzumab=False,
                     has_carboplatin=False, has_pembrolizumab=False,
                     genomic_risk="high"),
    },
    {
        "label": "3. TNBC, edad 45, cN1, AC-T + carbo + pembrolizumab (KN522)",
        "args": dict(subtype="TNBC", ki67_pct=70, age=45, cN_stage="cN1",
                     regimen="AC_T", has_pertuzumab=False,
                     has_carboplatin=True, has_pembrolizumab=True,
                     genomic_risk="unknown"),
    },
    {
        "label": "4. TNBC, edad 60, cN2, AC-T sin agentes especiales",
        "args": dict(subtype="TNBC", ki67_pct=60, age=60, cN_stage="cN2",
                     regimen="AC_T", has_pertuzumab=False,
                     has_carboplatin=False, has_pembrolizumab=False,
                     genomic_risk="unknown"),
    },
    {
        "label": "5. HR-/HER2+, edad 50, cN1, TCH + pertuzumab",
        "args": dict(subtype="HR-/HER2+", ki67_pct=40, age=50, cN_stage="cN1",
                     regimen="TCH", has_pertuzumab=True,
                     has_carboplatin=False, has_pembrolizumab=False,
                     genomic_risk="unknown"),
    },
    {
        "label": "6. HR+/HER2+, edad 55, cN2, TCH + pertuzumab",
        "args": dict(subtype="HR+/HER2+", ki67_pct=25, age=55, cN_stage="cN2",
                     regimen="TCH", has_pertuzumab=True,
                     has_carboplatin=False, has_pembrolizumab=False,
                     genomic_risk="unknown"),
    },
    {
        "label": "7. HR+/HER2+, edad 70, cN1, TCH sin pertuzumab",
        "args": dict(subtype="HR+/HER2+", ki67_pct=15, age=70, cN_stage="cN1",
                     regimen="TCH", has_pertuzumab=False,
                     has_carboplatin=False, has_pembrolizumab=False,
                     genomic_risk="unknown"),
    },
    {
        "label": "8. HR+/HER2- joven, riesgo genomico bajo (Oncotype <=25), cN1",
        "args": dict(subtype="HR+/HER2-", ki67_pct=15, age=42, cN_stage="cN1",
                     regimen="AC_T", has_pertuzumab=False,
                     has_carboplatin=False, has_pembrolizumab=False,
                     genomic_risk="low"),
    },
    {
        "label": "9. TNBC, edad 38, cN3, AC-T + carbo + pembro",
        "args": dict(subtype="TNBC", ki67_pct=80, age=38, cN_stage="cN3",
                     regimen="AC_T", has_pertuzumab=False,
                     has_carboplatin=True, has_pembrolizumab=True,
                     genomic_risk="unknown"),
    },
    {
        "label": "10. HR-/HER2+, edad 35, cN3, TCHP (taxano+carbo+H+P)",
        "args": dict(subtype="HR-/HER2+", ki67_pct=60, age=35, cN_stage="cN3",
                     regimen="TCH", has_pertuzumab=True,
                     has_carboplatin=False, has_pembrolizumab=False,
                     genomic_risk="unknown"),
    },
]


def _print_vignettes() -> None:
    pred = LogisticPredictor()
    print("=" * 78)
    print("CALCULADORA ypN0 NIVEL 1 -- 10 vinetas clinicas")
    print("=" * 78)
    for v in _VIGNETTES:
        out = pred.predict(**v["args"])
        print(f"\n{v['label']}")
        print(f"   -> ypN0 = {out['probability_pct']}  "
              f"[banda: {out['risk_band']}]  "
              f"[confianza: {out['confidence']}]")
    print("\n" + "=" * 78)
    print("CASOS DE VERIFICACION")
    print("=" * 78)
    cases = [
        ("V1. TNBC, cN1, edad 45, pembrolizumab (esperado >=60%)",
         dict(subtype="TNBC", ki67_pct=70, age=45, cN_stage="cN1",
              regimen="AC_T", has_pertuzumab=False,
              has_carboplatin=True, has_pembrolizumab=True,
              genomic_risk="unknown")),
        ("V2. HR+/HER2-, cN1, edad 65, Ki67 10%, sin agentes (esperado <=15%)",
         dict(subtype="HR+/HER2-", ki67_pct=10, age=65, cN_stage="cN1",
              regimen="AC_T", has_pertuzumab=False,
              has_carboplatin=False, has_pembrolizumab=False,
              genomic_risk="unknown")),
        ("V3. HR-/HER2+, cN1, edad 50, pertuzumab (esperado >=70%)",
         dict(subtype="HR-/HER2+", ki67_pct=40, age=50, cN_stage="cN1",
              regimen="TCH", has_pertuzumab=True,
              has_carboplatin=False, has_pembrolizumab=False,
              genomic_risk="unknown")),
        ("V4. HR+/HER2+, cN2, edad 55, pertuzumab (esperado >=40%)",
         dict(subtype="HR+/HER2+", ki67_pct=25, age=55, cN_stage="cN2",
              regimen="TCH", has_pertuzumab=True,
              has_carboplatin=False, has_pembrolizumab=False,
              genomic_risk="unknown")),
    ]
    for label, args in cases:
        out = pred.predict(**args)
        print(f"\n{label}")
        print(f"   -> ypN0 = {out['probability_pct']}  "
              f"[banda: {out['risk_band']}]")


if __name__ == "__main__":
    _print_vignettes()
