# ============================================
# Proyección top-down visitantes del parque
# - Coeficiente de captura (k)
# - Estacionalidad mensual (S_mes)
# - Proyección 2025–2027 (punto y rango)
# Requisitos: pandas, numpy
# ============================================

import pandas as pd
import numpy as np
from pathlib import Path

# ------------------------------------------------------------
# 0) SECCIÓN DE ENTRADA: pega/lee tus datos reales AQUÍ
# ------------------------------------------------------------
# Estructuras mínimas requeridas:
#   turistas_ciudad: columnas ["fecha","turistas_totales"]
#   visitantes_parque: columnas ["fecha","visitantes_totales"]
# Notas:
#   - 'fecha' en formato AAAA-MM (o cualquier fecha; se redondea a inicio de mes)
#   - Deben existir meses con ambos datos (para calibrar k y S_mes)
#   - Para proyectar: turistas_ciudad debe incluir 2025-01 ... 2027-12 (forecast)


raw = [
    # 2022
    ("ene-22", 252670,  47555),
    ("feb-22", 187397,  33752),
    ("mar-22", 195151,  50401),
    ("abr-22", 259592,  88518),
    ("may-22", 286375,  52899),
    ("jun-22", 269962,  74406),
    ("jul-22", 278759, 117898),
    ("ago-22", 280636, 106035),
    ("sep-22", 278508,  48593),
    ("oct-22", 203882,  45272),
    ("nov-22", 216805,  45395),
    ("dic-22", 215150,  60313),
    # 2023
    ("ene-23", 199069,      0),  # sin registro (se convierte a NaN abajo)
    ("feb-23", 191296,      0),  # sin registro
    ("mar-23", 206714,      0),  # sin registro
    ("abr-23", 220838,      0),  # sin registro
    ("may-23", 246132,  65396),
    ("jun-23", 241114,  95181),
    ("jul-23", 236356, 165041),
    ("ago-23", 252871, 140610),
    ("sep-23", 247014,  57745),
    ("oct-23", 198486,  49379),
    ("nov-23", 224430,  53081),
    ("dic-23", 237070,  72613),
    # 2024
    ("ene-24", 223116,  49328),
    ("feb-24", 186327,  43293),
    ("mar-24", 191706,  70971),
    ("abr-24", 240656,  76619),
    ("may-24", 232581,  58704),
    ("jun-24", 233522,  69427),
    ("jul-24", 245496, 123249),
    ("ago-24", 277021,  97003),
    ("sep-24", 254396,  36239),
    ("oct-24", 169104,  23176),
    ("nov-24", 151484,  24855),
    ("dic-24", 160698,  31070),
    # 2025 (proyección interna del parque)
    ("ene-25", 159184,  23727),
    ("feb-25", 111926,  20156),
    ("mar-25", 119146,  29739),
    ("abr-25", 148154,  43406),
    ("may-25", 173372,  38977),
    ("jun-25", 165415,  50204),
    ("jul-25", 180548,  93354),
    ("ago-25", 208976,  78757),
    ("sep-25", 191711,  34471),
    ("oct-25", 153369,  32263),
    ("nov-25", 134198,  37575),
    ("dic-25", 153369,  54967),
    # 2026 (proyección interna del parque)
    ("ene-26", 146496,  28473),
    ("feb-26", 103005,  24188),
    ("mar-26", 109649,  35687),
    ("abr-26", 136345,  52088),
    ("may-26", 159553,  46773),
    ("jun-26", 152230,  60245),
    ("jul-26", 166157, 112025),
    ("ago-26", 192319,  94508),
    ("sep-26", 176430,  41365),
    ("oct-26", 141144,  38715),
    ("nov-26", 123501,  45090),
    ("dic-26", 141144,  65960),
    # 2027 (proyección interna del parque)
    ("ene-27", 133895,  34167),
    ("feb-27",  94145,  29025),
    ("mar-27", 100218,  42824),
    ("abr-27", 124617,  62505),
    ("may-27", 145829,  56127),
    ("jun-27", 139136,  72294),
    ("jul-27", 151865, 134430),
    ("ago-27", 175777, 113410),
    ("sep-27", 161255,  49638),
    ("oct-27", 129004,  46458),
    ("nov-27", 112878,  54108),
    ("dic-27", 129004,  79152),
]

# -------------------------
# Helpers para parsear "ene-22" -> Timestamp(2022-01-01)
# -------------------------
MES_MAP = {"ene":1,"feb":2,"mar":3,"abr":4,"may":5,"jun":6,"jul":7,"ago":8,"sep":9,"oct":10,"nov":11,"dic":12}
def parse_fecha(s):
    s = s.strip().lower()
    m_abbr, yy = s.split("-")
    year = 2000 + int(yy)
    month = MES_MAP[m_abbr]
    return pd.Timestamp(year, month, 1)

# -------------------------
# Construcción de DataFrame maestro
# -------------------------
df_all = pd.DataFrame(
    [{"fecha": parse_fecha(f), "turistas_totales": t, "visitantes_totales": v} for f, t, v in raw]
).sort_values("fecha").reset_index(drop=True)

# -------------------------
# 1) turistas_ciudad (2022–2027)
# -------------------------
turistas_ciudad = df_all[["fecha", "turistas_totales"]].copy()

# -------------------------
# 2) visitantes_parque (SOLO reales hasta 2024-12)
#    - Los “0” de ene–abr/2023 se vuelven NaN (sin registro)
# -------------------------
visitantes_parque = df_all.loc[df_all["fecha"] <= pd.Timestamp(2025,8,1), ["fecha","visitantes_totales"]].copy()

mask_sin_registro = visitantes_parque["fecha"].isin(pd.date_range("2023-01-01","2023-04-01", freq="MS"))

visitantes_parque.loc[mask_sin_registro, "visitantes_totales"] = np.nan

# -------------------------
# 3) proyeccion_interna (ene-2025 a dic-2027)
# -------------------------
mask_interna = (df_all["fecha"] >= pd.Timestamp(2025,9,1)) & (df_all["fecha"] <= pd.Timestamp(2027,12,1))
proyeccion_interna = (
    df_all.loc[mask_interna, ["fecha"]]
          .assign(visitantes_interna = df_all.loc[mask_interna, "visitantes_totales"].values)
    .reset_index(drop=True)
)

# ------------------------------------------------------------
# 1) FUNCIONES AUXILIARES
# ------------------------------------------------------------
def _to_month_start(s):
    """Convierte una serie/columna de fechas a primer día de mes (timestamp)."""
    s = pd.to_datetime(s, errors="coerce")
    return s.values.astype("datetime64[M]").astype("datetime64[ns]")

def preparar_bases(turistas_df: pd.DataFrame, parque_df: pd.DataFrame) -> pd.DataFrame:
    """
    Unifica las dos fuentes en una sola tabla mensual.
    Salida: DataFrame con ['fecha','turistas_totales','visitantes_totales']
    """
    a = turistas_df.rename(columns=str.lower).copy()
    b = parque_df.rename(columns=str.lower).copy()
    assert {"fecha", "turistas_totales"}.issubset(a.columns), "Faltan columnas en turistas_ciudad"
    assert {"fecha", "visitantes_totales"}.issubset(b.columns), "Faltan columnas en visitantes_parque"

    a["fecha"] = _to_month_start(a["fecha"])
    b["fecha"] = _to_month_start(b["fecha"])
    a = a.sort_values("fecha").groupby("fecha", as_index=False)["turistas_totales"].sum(min_count=1)
    b = b.sort_values("fecha").groupby("fecha", as_index=False)["visitantes_totales"].sum(min_count=1)

    df = pd.merge(a, b, on="fecha", how="outer").sort_values("fecha").reset_index(drop=True)
    return df

def calcular_k(df: pd.DataFrame, metodo_outliers: str = "p10p90"):
    """
    Calcula coeficiente de captura k (mediana de visitantes/turistas) y su rango intercuartílico.
    - Excluye meses con turistas<=0 o visitantes<0.
    - Control simple de outliers por percentiles (p10-p90) o IQR.
    Retorna: dict con k, k_low, k_high, corr, df_trim (con columnas ratio_captura)
    """
    base = df.dropna(subset=["turistas_totales", "visitantes_totales"]).copy()
    base = base[(base["turistas_totales"] > 0) & (base["visitantes_totales"] >= 0)].copy()
    if base.empty:
        raise ValueError("No hay meses con ambos datos para calcular k.")

    base["ratio_captura"] = base["visitantes_totales"] / base["turistas_totales"]

    # Control de outliers:
    if metodo_outliers == "p10p90":
        lo, hi = base["ratio_captura"].quantile([0.10, 0.90])
        f = base[(base["ratio_captura"] >= lo) & (base["ratio_captura"] <= hi)].copy()
    else:  # IQR
        q1, q3 = base["ratio_captura"].quantile([0.25, 0.75])
        iqr = q3 - q1
        lo, hi = q1 - 1.5 * iqr, q3 + 1.5 * iqr
        f = base[(base["ratio_captura"] >= lo) & (base["ratio_captura"] <= hi)].copy()

    if f.empty:
        f = base.copy()  # si el filtro dejó vacío, usa todo

    k = f["ratio_captura"].median()
    k_low = f["ratio_captura"].quantile(0.25)
    k_high = f["ratio_captura"].quantile(0.75)

    # Correlación Turistas vs Visitantes (en meses válidos)
    corr = f[["turistas_totales", "visitantes_totales"]].corr().iloc[0, 1]

    return {
        "k": float(k),
        "k_low": float(k_low),
        "k_high": float(k_high),
        "corr": float(corr),
        "df_trim": f.sort_values("fecha").reset_index(drop=True)
    }

def calcular_estacionalidad(df_trim: pd.DataFrame, k: float) -> pd.Series:
    """
    Estima el índice estacional mensual normalizado (promedio anual = 1.00).
    Usa mediana de: visitantes / (k * turistas), agrupado por mes (1..12).
    Devuelve una Serie indexada por mes (1..12) con factores S_mes.
    """
    tmp = df_trim.copy()
    tmp = tmp[(tmp["turistas_totales"] > 0) & (tmp["visitantes_totales"] >= 0)]
    tmp["mes"] = tmp["fecha"].dt.month
    tmp["ratio_estacional"] = tmp["visitantes_totales"] / (k * tmp["turistas_totales"])

    s = tmp.groupby("mes")["ratio_estacional"].median()
    # Reindex a 1..12 y rellena con 1.0 si faltó algún mes
    s = s.reindex(range(1, 13), fill_value=1.0)
    s = s / s.mean()  # normaliza a promedio 1.0
    return s

def proyectar_visitantes(
    turistas_df: pd.DataFrame,
    k: float, k_low: float, k_high: float,
    S_mes: pd.Series,
    inicio: str = "2025-01",
    fin: str = "2027-12"
) -> pd.DataFrame:
    """
    Proyecta visitantes del parque a partir de turistas proyectados y estacionalidad.
    turistas_df: DataFrame con ['fecha','turistas_totales'] que incluya el periodo a proyectar.
    Retorna: DataFrame con columnas:
      fecha, turistas, S_mes, visitantes_base, visitantes_low, visitantes_high
    """
    a = turistas_df.rename(columns=str.lower).copy()
    assert {"fecha", "turistas_totales"}.issubset(a.columns), "turistas_df inválido"
    a["fecha"] = _to_month_start(a["fecha"])
    # Filtra al rango solicitado
    mask = (a["fecha"] >= pd.to_datetime(inicio)) & (a["fecha"] <= pd.to_datetime(fin))
    a = a.loc[mask].copy().sort_values("fecha")
    if a.empty:
        raise ValueError("No hay turistas proyectados en el rango solicitado.")

    a["mes"] = a["fecha"].dt.month
    a["S_mes"] = a["mes"].map(S_mes).astype(float)

    a["visitantes_base"] = k * a["turistas_totales"] * a["S_mes"]
    a["visitantes_low"] = k_low * a["turistas_totales"] * a["S_mes"]
    a["visitantes_high"] = k_high * a["turistas_totales"] * a["S_mes"]

    cols = ["fecha", "turistas_totales", "S_mes", "visitantes_base", "visitantes_low", "visitantes_high"]
    return a[cols].rename(columns={"turistas_totales": "turistas"})

def agregar_proyeccion_interna(df_proj: pd.DataFrame, interna_df: pd.DataFrame) -> pd.DataFrame:
    """
    (Opcional) Une la proyección interna del parque y calcula diferencias.
    interna_df: ['fecha','visitantes_interna']
    """
    if interna_df is None or interna_df.empty:
        return df_proj

    z = interna_df.rename(columns=str.lower).copy()
    assert {"fecha", "visitantes_interna"}.issubset(z.columns), "proyeccion_interna inválida"
    z["fecha"] = _to_month_start(z["fecha"])

    out = df_proj.merge(z, on="fecha", how="left")
    if "visitantes_interna" in out.columns:
        out["dif_%_vs_interna"] = (out["visitantes_base"] - out["visitantes_interna"]) / out["visitantes_interna"] * 100
    return out

def resumen_anual(df_proj: pd.DataFrame) -> pd.DataFrame:
    """Suma anual de turistas y visitantes proyectados (base/low/high)."""
    x = df_proj.copy()
    x["anio"] = x["fecha"].dt.year
    agg = (x.groupby("anio")[["turistas", "visitantes_base", "visitantes_low", "visitantes_high"]]
             .sum(min_count=1)
             .reset_index())
    # Crece vs año previo (base)
    agg["crec_%_visitantes_base_vs_año_prev"] = agg["visitantes_base"].pct_change() * 100
    return agg


# ------------------------------------------------------------
# 2) PIPELINE
# ------------------------------------------------------------
def main():
    # 2.1) Preparar tabla unificada
    df = preparar_bases(turistas_ciudad, visitantes_parque)

    # 2.2) Calcular k y depurar outliers
    k_result = calcular_k(df, metodo_outliers="p10p90")
    k, k_low, k_high, corr = k_result["k"], k_result["k_low"], k_result["k_high"], k_result["corr"]
    df_trim = k_result["df_trim"]

    print(f"Coeficiente de captura (k): {k:,.6f}  | Rango IQR: [{k_low:,.6f}, {k_high:,.6f}]  | Corr Turistas~Visitantes: {corr:0.3f}")

    # 2.3) Estacionalidad
    S_mes = calcular_estacionalidad(df_trim, k)
    print("\nÍndice estacional mensual (normalizado, promedio=1.0):")
    print(S_mes.round(3))

    # 2.4) Proyección 2025–2027
    proy = proyectar_visitantes(
        turistas_df=turistas_ciudad,
        k=k, k_low=k_low, k_high=k_high,
        S_mes=S_mes,
        inicio="2025-01", fin="2027-12"
    )

    # 2.5) (Opcional) unir proyección interna del parque y calcular diferencias
    proy_cmp = agregar_proyeccion_interna(proy, proyeccion_interna)

    # 2.6) Resumen anual
    anual = resumen_anual(proy_cmp)

    # 2.7) Exportar (opcional)
    # Path("output").mkdir(parents=True, exist_ok=True)
    # proy_cmp.to_excel("output/proyeccion_parque_2025_2027.xlsx", index=False)
    # anual.to_excel("output/resumen_anual_2025_2027.xlsx", index=False)

    # 2.8) Mostrar primeras filas
    print("\nProyección mensual (primeras filas):")
    print(proy_cmp.head(12).round(0))

    print("\nResumen anual:")
    print(anual.round(0))


if __name__ == "__main__":
    main()
