"""
ERGON F11 - Motor de reglas de alertas (Sprint 3 2026-04-20).

Evalua 8 reglas contra la data de una obra y devuelve alertas + metadata
de calibracion (Valor 2 manual v1.0). On-demand: no persiste en DB.

Reglas implementadas:
    R1 AVANCE_DESVIO         - por rubro, atraso en pp vs umbral_desvio       -> AMARILLO
    R2 AVANCE_CRITICO        - por rubro, atraso en pp vs umbral_critico      -> ROJO
    R3 SOBRECOSTO            - por rubro, ac > pv*(1+umbral_desvio/100)       -> AMARILLO
    R4 SOBRECOSTO_CRITICO    - por rubro, ac > pv*(1+umbral_critico/100)      -> ROJO
    R5 FECHA_FIN_RIESGO      - por obra, EAC_date > fecha_fin_plan + 30 dias  -> AMARILLO
    R6 FECHA_FIN_CRITICO     - por obra, EAC_date > fecha_fin_plan + 60 dias  -> ROJO
    R7 CONTRATO_VENCIDO      - por contrato, fecha_vence < corte y pendiente  -> ROJO
    R8 CONTRATO_SIN_FIRMA    - por contrato, emision > 15d sin firma          -> AMARILLO

Severidad agregada:
    Solo alertas AMARILLO/ROJO se listan. Los rubros/contratos en VERDE no
    aparecen en el payload (consistente con get_alertas pre-F11).

Calibracion honesta (Valor 2):
    - R5/R6 solo se evaluan con >=3 meses de data real. Sin esa base,
      el motor reporta 'calibracion.fecha_fin = "insuficiente"' y saltea.
    - El banner usa la banda del EVM (ya calibrada por meses de data).

Uso:
    import alertas_engine
    data = alertas_engine.evaluar(conn, "TDG1")
    # {alertas, total, por_severidad, reglas_aplicadas, calibracion, corte_mes,
    #  umbrales}
"""
from __future__ import annotations

from datetime import date, timedelta
from typing import Any  # noqa: F401


SEVERIDAD_AMARILLO = "AMARILLO"
SEVERIDAD_ROJO = "ROJO"

# Umbrales hard-codeados por regla (no se exponen en UI sprint 3).
# R5/R6 comparan EAC_date vs fecha_fin_plan + delta_dias.
DELTA_FECHA_FIN_RIESGO_DIAS = 30
DELTA_FECHA_FIN_CRITICO_DIAS = 60
# R8 compara dias transcurridos desde emision sin firmar.
DIAS_MAX_SIN_FIRMA = 15
# R5/R6 requieren esta cantidad minima de meses con data real para ser honestos.
MIN_MESES_PARA_EAC_DATE = 3


def _parse_iso(s: str) -> date | None:
    if not s:
        return None
    try:
        return date.fromisoformat(s[:10])
    except (ValueError, TypeError):
        return None


def _eac_fecha_fin(conn, codigo: str, obra: dict) -> tuple[date | None, dict]:
    """Proyecta la fecha de fin usando SPI (formula PMI clasica).

    EAC_date = fecha_inicio + (fecha_fin_plan - fecha_inicio) / SPI

    Honestidad: si hay < 3 meses de data real, no proyecta — devuelve None y
    deja constancia en calibracion. No inventa fechas (scar_008).
    """
    meses_con_real = conn.execute(
        """
        SELECT COUNT(DISTINCT mes) AS n
        FROM avance_mensual
        WHERE obra_id = ? AND real_acum_pct IS NOT NULL
        """,
        (obra["id"],),
    ).fetchone()["n"]

    if meses_con_real < MIN_MESES_PARA_EAC_DATE:
        return None, {
            "evaluada": False,
            "motivo": (
                f"Insuficiente: {meses_con_real} mes(es) de data real "
                f"(minimo {MIN_MESES_PARA_EAC_DATE} para proyectar honestamente)."
            ),
            "meses_con_data": meses_con_real,
        }

    # SPI en el mes de corte (ponderado por pesos de rubros).
    corte_mes = obra["fecha_corte_actual"][:7]
    row = conn.execute(
        """
        SELECT
          ROUND(SUM(a.plan_acum_pct * r.peso_pct / 100.0), 6) AS plan_global,
          ROUND(SUM(CASE WHEN a.real_acum_pct IS NOT NULL
                         THEN a.real_acum_pct * r.peso_pct / 100.0
                         ELSE 0 END), 6) AS real_global
        FROM avance_mensual a
        JOIN rubros_obra r ON r.id = a.rubro_id
        WHERE a.obra_id = ? AND a.mes = ?
        """,
        (obra["id"], corte_mes),
    ).fetchone()

    if not row or not row["plan_global"]:
        return None, {
            "evaluada": False,
            "motivo": f"Sin avance ponderado al corte {corte_mes}.",
            "meses_con_data": meses_con_real,
        }

    spi = float(row["real_global"] or 0) / float(row["plan_global"])
    if spi <= 0:
        return None, {
            "evaluada": False,
            "motivo": "SPI <= 0 — proyeccion no es fisica.",
            "meses_con_data": meses_con_real,
            "spi": round(spi, 4),
        }

    f_ini = _parse_iso(obra["fecha_inicio"])
    f_fin = _parse_iso(obra["fecha_fin_plan"])
    if not f_ini or not f_fin or f_fin <= f_ini:
        return None, {
            "evaluada": False,
            "motivo": "Fechas de la obra inconsistentes.",
        }

    total_plan_days = (f_fin - f_ini).days
    eac_days = total_plan_days / spi
    eac_date = f_ini + timedelta(days=round(eac_days))

    return eac_date, {
        "evaluada": True,
        "meses_con_data": meses_con_real,
        "spi": round(spi, 4),
        "fecha_fin_plan": f_fin.isoformat(),
        "eac_date": eac_date.isoformat(),
        "delta_dias": (eac_date - f_fin).days,
    }


def _evaluar_rubros(conn, obra: dict, u_desv: float, u_crit: float) -> list[dict]:
    """R1-R4: por cada rubro, avance (pp) y sobrecosto (%) al corte."""
    corte_mes = obra["fecha_corte_actual"][:7]
    rows = conn.execute(
        """
        SELECT r.id, r.orden, r.nombre, r.peso_pct,
               a.plan_acum_pct, a.real_acum_pct,
               (SELECT SUM(pm.plan) FROM presupuesto_mensual pm
                WHERE pm.rubro_id = r.id AND pm.mes <= ?) AS pv_rubro,
               (SELECT SUM(pm.real) FROM presupuesto_mensual pm
                WHERE pm.rubro_id = r.id AND pm.mes <= ?) AS ac_rubro
        FROM rubros_obra r
        LEFT JOIN avance_mensual a
          ON a.rubro_id = r.id AND a.obra_id = r.obra_id AND a.mes = ?
        WHERE r.obra_id = ?
        ORDER BY r.orden
        """,
        (corte_mes, corte_mes, corte_mes, obra["id"]),
    ).fetchall()

    alertas: list[dict] = []
    for r in rows:
        plan = r["plan_acum_pct"]
        real = r["real_acum_pct"]
        # R1/R2: avance
        if plan is not None and real is not None:
            desvio_pp = round(float(plan) - float(real), 2)
            if desvio_pp > u_crit:
                alertas.append({
                    "regla_id": "R2",
                    "tipo": "AVANCE_CRITICO",
                    "severidad": SEVERIDAD_ROJO,
                    "rubro": r["nombre"],
                    "mensaje": (
                        f"{r['nombre']}: atraso {desvio_pp:.1f} pp "
                        f"(critico > {u_crit:.0f})"
                    ),
                    "contexto": {
                        "rubro_id": r["id"], "orden": r["orden"],
                        "plan_acum_pct": float(plan),
                        "real_acum_pct": float(real),
                        "desvio_pp": desvio_pp,
                    },
                })
            elif desvio_pp > u_desv:
                alertas.append({
                    "regla_id": "R1",
                    "tipo": "AVANCE_DESVIO",
                    "severidad": SEVERIDAD_AMARILLO,
                    "rubro": r["nombre"],
                    "mensaje": (
                        f"{r['nombre']}: atraso {desvio_pp:.1f} pp "
                        f"(desvio > {u_desv:.0f})"
                    ),
                    "contexto": {
                        "rubro_id": r["id"], "orden": r["orden"],
                        "plan_acum_pct": float(plan),
                        "real_acum_pct": float(real),
                        "desvio_pp": desvio_pp,
                    },
                })

        # R3/R4: sobrecosto por rubro
        pv = r["pv_rubro"]
        ac = r["ac_rubro"]
        if pv is not None and ac is not None and float(pv) > 0:
            ratio = float(ac) / float(pv)  # 1.15 = 15% sobrecosto
            sobrecosto_pct = round((ratio - 1.0) * 100.0, 2)
            if sobrecosto_pct > u_crit:
                alertas.append({
                    "regla_id": "R4",
                    "tipo": "SOBRECOSTO_CRITICO",
                    "severidad": SEVERIDAD_ROJO,
                    "rubro": r["nombre"],
                    "mensaje": (
                        f"{r['nombre']}: sobrecosto {sobrecosto_pct:.1f}% "
                        f"(critico > {u_crit:.0f}%)"
                    ),
                    "contexto": {
                        "rubro_id": r["id"], "orden": r["orden"],
                        "pv_rubro": float(pv), "ac_rubro": float(ac),
                        "sobrecosto_pct": sobrecosto_pct,
                    },
                })
            elif sobrecosto_pct > u_desv:
                alertas.append({
                    "regla_id": "R3",
                    "tipo": "SOBRECOSTO",
                    "severidad": SEVERIDAD_AMARILLO,
                    "rubro": r["nombre"],
                    "mensaje": (
                        f"{r['nombre']}: sobrecosto {sobrecosto_pct:.1f}% "
                        f"(desvio > {u_desv:.0f}%)"
                    ),
                    "contexto": {
                        "rubro_id": r["id"], "orden": r["orden"],
                        "pv_rubro": float(pv), "ac_rubro": float(ac),
                        "sobrecosto_pct": sobrecosto_pct,
                    },
                })
    return alertas


def _evaluar_cronograma(conn, obra: dict) -> tuple[list[dict], dict]:
    """R5/R6: EAC_date vs fecha_fin_plan + delta."""
    eac_date, calib = _eac_fecha_fin(conn, obra["codigo"], obra)
    alertas: list[dict] = []
    if eac_date is None:
        return alertas, calib

    f_fin = _parse_iso(obra["fecha_fin_plan"])
    if not f_fin:
        return alertas, calib

    delta = (eac_date - f_fin).days
    if delta > DELTA_FECHA_FIN_CRITICO_DIAS:
        alertas.append({
            "regla_id": "R6",
            "tipo": "FECHA_FIN_CRITICO",
            "severidad": SEVERIDAD_ROJO,
            "mensaje": (
                f"Fecha estimada de fin ({eac_date.isoformat()}) supera en "
                f"{delta} dias la planificada ({f_fin.isoformat()})"
            ),
            "contexto": {
                "eac_date": eac_date.isoformat(),
                "fecha_fin_plan": f_fin.isoformat(),
                "delta_dias": delta,
                "spi": calib.get("spi"),
            },
        })
    elif delta > DELTA_FECHA_FIN_RIESGO_DIAS:
        alertas.append({
            "regla_id": "R5",
            "tipo": "FECHA_FIN_RIESGO",
            "severidad": SEVERIDAD_AMARILLO,
            "mensaje": (
                f"Fecha estimada de fin ({eac_date.isoformat()}) supera en "
                f"{delta} dias la planificada ({f_fin.isoformat()})"
            ),
            "contexto": {
                "eac_date": eac_date.isoformat(),
                "fecha_fin_plan": f_fin.isoformat(),
                "delta_dias": delta,
                "spi": calib.get("spi"),
            },
        })
    return alertas, calib


def _evaluar_contratos(conn, obra: dict) -> list[dict]:
    """R7: vencido sin resolver. R8: emision + 15d sin firma."""
    corte = _parse_iso(obra["fecha_corte_actual"])
    if not corte:
        return []

    rows = conn.execute(
        """
        SELECT c.id, c.tipo, c.descripcion, c.estado,
               c.fecha_emision, c.fecha_firma, c.fecha_vence,
               s.nombre AS subcontratista
        FROM contratos c
        LEFT JOIN subcontratistas s ON s.id = c.contratista_id
        WHERE c.obra_id = ?
        ORDER BY c.fecha_emision, c.id
        """,
        (obra["id"],),
    ).fetchall()

    alertas: list[dict] = []
    umbral_sin_firma = corte - timedelta(days=DIAS_MAX_SIN_FIRMA)

    for c in rows:
        estado = (c["estado"] or "").upper()
        f_emi = _parse_iso(c["fecha_emision"])
        f_firma = _parse_iso(c["fecha_firma"]) if c["fecha_firma"] else None
        f_vence = _parse_iso(c["fecha_vence"]) if c["fecha_vence"] else None
        tipo = c["tipo"]
        desc = c["descripcion"] or tipo
        sub = c["subcontratista"] or "sin asignar"

        # R7: vencido y aun pendiente/observado
        if f_vence and f_vence < corte and estado in ("PENDIENTE", "OBSERVADO"):
            alertas.append({
                "regla_id": "R7",
                "tipo": "CONTRATO_VENCIDO",
                "severidad": SEVERIDAD_ROJO,
                "contrato": desc,
                "mensaje": (
                    f"{tipo} ({sub}) vencio el {f_vence.isoformat()} "
                    f"y sigue en estado {estado}"
                ),
                "contexto": {
                    "contrato_id": c["id"],
                    "subcontratista": sub,
                    "fecha_vence": f_vence.isoformat(),
                    "estado": estado,
                },
            })

        # R8: emision sin firma > 15 dias (solo si no esta firmado)
        if f_emi and f_emi < umbral_sin_firma and f_firma is None:
            dias = (corte - f_emi).days
            alertas.append({
                "regla_id": "R8",
                "tipo": "CONTRATO_SIN_FIRMA",
                "severidad": SEVERIDAD_AMARILLO,
                "contrato": desc,
                "mensaje": (
                    f"{tipo} ({sub}) emitido el {f_emi.isoformat()} "
                    f"lleva {dias} dias sin firma"
                ),
                "contexto": {
                    "contrato_id": c["id"],
                    "subcontratista": sub,
                    "fecha_emision": f_emi.isoformat(),
                    "dias_sin_firma": dias,
                    "estado": estado,
                },
            })

    return alertas


def evaluar(conn, codigo: str) -> dict:
    """Evalua las 8 reglas contra la obra `codigo`. Devuelve payload API-ready.

    Estructura:
        {
          alertas: [...],              # lista ordenada por severidad desc, regla asc
          total: int,
          por_severidad: {ROJO: int, AMARILLO: int},
          reglas_aplicadas: ["R1", "R3", ...],  # ordenado
          corte_mes: "YYYY-MM",
          umbrales: {desvio_pct, critico_pct},
          calibracion: {
            nota: str (Valor 2),
            fecha_fin: {evaluada, motivo?, meses_con_data, spi?, ...},
          },
        }
    """
    obra = conn.execute(
        "SELECT * FROM obras WHERE codigo = ?", (codigo,),
    ).fetchone()
    if not obra:
        return {
            "alertas": [], "total": 0,
            "por_severidad": {SEVERIDAD_ROJO: 0, SEVERIDAD_AMARILLO: 0},
            "reglas_aplicadas": [],
            "corte_mes": None,
            "umbrales": None,
            "calibracion": {"nota": f"Obra {codigo} no existe.", "fecha_fin": None},
        }
    obra = {k: obra[k] for k in obra.keys()}

    u_desv = float(obra["umbral_desvio_pct"])
    u_crit = float(obra["umbral_critico_pct"])

    alertas: list[dict] = []
    alertas.extend(_evaluar_rubros(conn, obra, u_desv, u_crit))
    a_cron, calib_fecha = _evaluar_cronograma(conn, obra)
    alertas.extend(a_cron)
    alertas.extend(_evaluar_contratos(conn, obra))

    # Orden: ROJO antes que AMARILLO; dentro del mismo nivel por regla_id.
    orden_sev = {SEVERIDAD_ROJO: 0, SEVERIDAD_AMARILLO: 1}
    alertas.sort(key=lambda a: (orden_sev.get(a["severidad"], 9), a["regla_id"]))

    por_severidad = {
        SEVERIDAD_ROJO: sum(1 for a in alertas if a["severidad"] == SEVERIDAD_ROJO),
        SEVERIDAD_AMARILLO: sum(1 for a in alertas if a["severidad"] == SEVERIDAD_AMARILLO),
    }
    reglas_aplicadas = sorted({a["regla_id"] for a in alertas})

    # Banner calibracion (Valor 2 manual v1.0). Honesto con los limites del motor.
    meses_data = calib_fecha.get("meses_con_data", 0)
    nota = (
        "Motor de reglas F11 (sprint 3): heuristicas deterministas sobre avance, "
        "presupuesto, cronograma y contratos. La proyeccion de fecha de fin usa "
        "SPI clasico (PMI); su banda se estrecha con mas meses de data. "
        f"Esta obra tiene {meses_data} mes(es) de data real. "
        "El motor predictivo con intervalos calibrados via IA llega en fase 2."
    )

    return {
        "alertas": alertas,
        "total": len(alertas),
        "por_severidad": por_severidad,
        "reglas_aplicadas": reglas_aplicadas,
        "corte_mes": obra["fecha_corte_actual"][:7],
        "umbrales": {"desvio_pct": u_desv, "critico_pct": u_crit},
        "calibracion": {"nota": nota, "fecha_fin": calib_fecha},
    }
