import os
import time
import typing as T
import pandas as pd
import numpy as np
from dotenv import load_dotenv
import re
import json
import requests



load_dotenv()


def fetch_pib_const_nacional(debug_json: bool = False) -> pd.DataFrame:
    """
    Descarga la serie histórica del PIB nacional trimestral a precios constantes base 2018 (INEGI/BIE).
    Devuelve DF con columnas: period_str ("YYYYTn"), period (pd.Period 'Q'), date (fin de trimestre), value (float).
    Lanza RuntimeError con mensaje claro si no hay datos.
    """
    token = os.getenv("INEGI_TOKEN", "")
    if not token:
        raise RuntimeError("Falta INEGI_TOKEN en el .env")

    INDICADOR_PIB_CONST = 735879  # PIB nacional a precios constantes 2018 (trimestral, BIE)
    area_geo = "0700"             # Nacional (para este endpoint)
    url = (
        f"https://www.inegi.org.mx/app/api/indicadores/desarrolladores/jsonxml/"
        f"INDICATOR/{INDICADOR_PIB_CONST}/es/{area_geo}/false/BIE/2.0/{token}?type=json"
    )

    r = requests.get(url, timeout=25)
    r.raise_for_status()
    data = r.json()

    # -- Depuración opcional --
    if debug_json:
        # Muestra primeras 1200 chars de JSON para evitar saturar
        preview = json.dumps(data, ensure_ascii=False)[:1200]
        print("DEBUG INEGI JSON PREVIEW:", preview)

    # --- Detectar estructura ---
    observations = None

    # Estructura 1 (la más común en Indicadores): Series[0].OBSERVATIONS
    try:
        series = data.get("Series", [])
        if series and isinstance(series, list) and series[0].get("OBSERVATIONS"):
            observations = series[0]["OBSERVATIONS"]
    except Exception:
        pass

    # Estructura 2 (algunos endpoints devuelven Data.Observations)
    if observations is None:
        try:
            data_node = data.get("Data") or {}
            obs_alt = data_node.get("Observations")
            if isinstance(obs_alt, list) and obs_alt:
                # Normalizar a las mismas claves que usamos abajo
                observations = [
                    {
                        "TIME_PERIOD": o.get("TimePeriod") or o.get("TIME_PERIOD"),
                        "OBS_VALUE": o.get("OBS_VALUE") or o.get("Value"),
                    }
                    for o in obs_alt
                ]
        except Exception:
            pass

    if not observations:
        # Trata de exponer mensaje de error de la API si viene presente
        msg_api = data.get("Message") or data.get("Nota") or data.get("message") or "Sin observaciones en la respuesta"
        raise RuntimeError(f"INEGI sin datos: {msg_api}")

    # --- Parseador robusto de trimestre ---
    def _parse_period_to_q(p: str) -> pd.Period:
        s = str(p).strip()
        # Acepta '2025T1', '2025 t1', '2025/1', '2025-Q1', etc.
        m = re.search(r"(\d{4}).*?([1-4])", s)
        if not m:
            raise ValueError(f"No puedo interpretar periodo: {p}")
        y, q = int(m.group(1)), int(m.group(2))
        return pd.Period(f"{y}Q{q}", freq="Q")

    rows = []
    for o in observations:
        tp = o.get("TIME_PERIOD")
        val = o.get("OBS_VALUE")
        if tp is None or val is None:
            continue
        # Limpiar valores tipo "123,456.7" o similares
        if isinstance(val, str):
            val = val.replace(",", "").strip()
        value = pd.to_numeric(val, errors="coerce")
        if pd.isna(value):
            continue
        try:
            per = _parse_period_to_q(tp)
        except Exception:
            continue
        rows.append({"period": per, "value": float(value)})

    df = pd.DataFrame(rows).dropna(subset=["period", "value"])
    if df.empty:
        raise RuntimeError("La serie regresó vacía después de parsear observaciones.")

    df = df.sort_values("period")
    df["period_str"] = df["period"].astype(str).str.replace("Q", "T", regex=False)
    df["date"] = df["period"].dt.to_timestamp(how="end")

    # Validación final antes de devolver (evita el error ['period','value'])
    expected = {"period_str", "period", "date", "value"}
    missing = expected - set(df.columns)
    if missing:
        raise RuntimeError(f"Faltan columnas en el DataFrame final: {missing}")

    return df[["period_str", "period", "date", "value"]].reset_index(drop=True)

def _fix_mojibake(text: str) -> str:
    """
    Si el CSV se leyó con el encoding incorrecto y trae 'Ã­' en vez de 'í',
    intentamos reparar con round-trip latin1->utf8. Si no hay error, devolvemos arreglado.
    """
    if not isinstance(text, str):
        return text
    try:
        # Si ya está bien, esto fallará y simplemente devolvemos el original
        repaired = text.encode("latin1").decode("utf-8")
        # Heurística: si al reparar aparecen caracteres con tildes válidos, usa repaired
        if "Ã" in text and ("á" in repaired or "é" in repaired or "í" in repaired or "ó" in repaired or "ú" in repaired or "ñ" in repaired):
            return repaired
    except Exception:
        pass
    return text

def load_gdp_by_sector_csv(
    path: str,
    value_col: str = "GDP",   # O "Time" si prefieres esa columna
) -> pd.DataFrame:
    """
    Lee el CSV descargado de DataMéxico (PIB trimestral por actividad), limpia y devuelve
    un DataFrame largo listo para graficar:
      columnas -> ['sector','period','period_str','date','value']

    - Corrige encoding (intenta 'utf-8-sig' y fallback a 'latin-1')
    - Repara mojibake (Ã­ -> í) si hace falta
    - Convierte 'Quarter' a Period trimestral y a fecha (fin de trimestre)
    - Convierte la columna de valores (GDP o Time) a float
    - Agrega por duplicados si existieran
    """
    # 1) Leer con utf-8-sig y fallback a latin-1
    try:
        df = pd.read_csv(path, encoding="utf-8-sig")
    except UnicodeDecodeError:
        df = pd.read_csv(path, encoding="latin-1")

    # 3) Repara mojibake en nombres de sector si hace falta
    if "Sector" in df.columns:
        df["Sector"] = df["Sector"].astype(str).map(_fix_mojibake)

    # 5) Parsear Quarter a Period trimestral
    # Quarter viene tipo "1993-T1", "2000-T4", etc.
    def parse_quarter_to_period(q: str) -> pd.Period | None:
        if pd.isna(q):
            return None
        s = str(q).strip()
        m = re.search(r"(\d{4}).*?([1-4])", s)
        if not m:
            return None
        y, t = int(m.group(1)), int(m.group(2))
        return pd.Period(f"{y}Q{t}", freq="Q")

    df["period"] = df.get("Quarter", df.get("Quarter", None)).map(parse_quarter_to_period)
    df = df.dropna(subset=["period"])

    # 6) Convertir valores a float (soporta notación científica tipo '7.2855E+11' y enteros grandes)
    def to_float(x):
        try:
            # Quita comas si las hubiera
            return float(str(x).replace(",", ""))
        except Exception:
            return np.nan

    df["value"] = df['GDP'].map(to_float)
    df = df.dropna(subset=["value"])

    # 7) Ordenar y generar auxiliares
    df = df.sort_values(["Sector", "period"])
    df["period_str"] = df["period"].astype(str).str.replace("Q", "T", regex=False)
    df["date"] = df["period"].dt.to_timestamp(how="end")

    # 8) Devolver formato largo requerido y agregar por si hay duplicados
    out = (df.groupby(["Sector", "period", "period_str", "date"], as_index=False)["value"].sum())
    return out[["Sector", "period", "period_str", "date", "value"]]





def get_inflacion_anual(token: str, fecha_inicio: str = None, fecha_fin: str = None) -> pd.DataFrame:
    """
        Descarga la serie SP30578 del Banxico (INPC Variación mensual)
        en formato DataFrame.
        
        Parámetros:
            token (str): Tu token del Banxico.
            fecha_inicio (str): Fecha inicial (opcional, 'YYYY-MM-DD')
            fecha_fin (str): Fecha final (opcional, 'YYYY-MM-DD')
            
        Retorna:
            pd.DataFrame con columnas ['fecha', 'valor']
        """
   
    seie_id = "SP30578"

    # URL base para la API del SIE Banxico
    BASE_URL = f"https://www.banxico.org.mx/SieAPIRest/service/v1/series/{seie_id}/datos"

    # Construcción de URL con fechas opcionales
    if fecha_inicio and fecha_fin:
        url = f"{BASE_URL}/{fecha_inicio}/{fecha_fin}?token={token}"
    else:
        url = f"{BASE_URL}?token={token}"

    # Petición a la API
    resp = requests.get(url)
    resp.raise_for_status()  # Lanza error si algo salió mal

    data = resp.json()

    # Extraer la lista de datos de la respuesta
    serie_datos = data['bmx']['series'][0]['datos']

    # Convertir a DataFrame
    df = pd.DataFrame(serie_datos)
    df.rename(columns={'fecha': 'Fecha', 'dato': 'Valor'}, inplace=True)

    # Convertir tipos
    df['Fecha'] = pd.to_datetime(df['Fecha'], dayfirst=True)
    df['Valor'] = pd.to_numeric(df['Valor'], errors='coerce')

    return df.sort_values('Fecha').reset_index(drop=True)

def get_prev_value(df_sorted: pd.DataFrame, current_date: pd.Timestamp) -> float | None:
    """Devuelve la variación del mes anterior disponible (pudiera no ser exactamente -1 mes si hay huecos)."""
    prev = df_sorted.loc[df_sorted["date"] < current_date]
    if prev.empty:
        return None
    return prev.iloc[-1]["Inflacion"]

def get_yoy_value(df_sorted: pd.DataFrame, current_date: pd.Timestamp) -> float | None:
    """Devuelve la variación del mismo mes del año previo (si existe exactamente esa fecha)."""
    target = current_date - pd.DateOffset(years=1)
    # Buscar coincidencia exacta por año/mes (ignora día)
    yoy = df_sorted[(df_sorted["date"].dt.year == target.year) &
                    (df_sorted["date"].dt.month == target.month)]
    if yoy.empty:
        return None
    return yoy.iloc[-1]["Inflacion"]

def fmt_pp(x):
    return f"{x:+.2f} pp"  # puntos porcentuales con signo

def inflacion_acumulada(df_periodo: pd.DataFrame) -> float:
    """
    Inflación acumulada compuesta a partir de variaciones mensuales (%).
    Fórmula: Π(1 + r_i/100) - 1  -> en %
    """
    if df_periodo.empty:
        return np.nan
    # Paso 1: Convertir tasas anuales a mensuales efectivas
    # Fórmula: r_mensual = (1 + r_anual)^(1/12) - 1
    df_periodo['Tasa_Mensual'] = df_periodo['Inflacion'].apply(lambda x: (1 + x/100)**(1/12) - 1)

    # Paso 2: Calcular factores de crecimiento mensual (1 + r_mensual)
    df_periodo['Factor_Crecimiento'] = 1 + df_periodo['Tasa_Mensual']

    # Paso 3: Producto compuesto de los factores de crecimiento
    producto_compuesto = np.prod(df_periodo['Factor_Crecimiento'])

    # Paso 4: Calcular inflación acumulada y convertir a porcentaje
    inflacion_acumulada = (producto_compuesto - 1) * 100
    return (inflacion_acumulada) 






def get_settings() -> dict:
    return {
        "INEGI_TOKEN": os.getenv("INEGI_TOKEN", ""),
        "BANXICO_TOKEN": os.getenv("BANXICO_TOKEN", ""),
        "DB_URL": os.getenv("DB_URL", ""),
        "CACHE_TTL": 3600,
    }

def last_common_quarter(df: pd.DataFrame) -> str | None:
    if df.empty: return None
    last_by_state = df.groupby("cve_ent")["periodo"].max()
    return last_by_state.min()

def compute_qoq(df: pd.DataFrame) -> pd.DataFrame:
    df = df.sort_values(["cve_ent", "periodo"]).copy()
    df["valor_lag"] = df.groupby("cve_ent")["valor"].shift(1)
    df["qoq_pct"] = 100 * (df["valor"] - df["valor_lag"]) / df["valor_lag"]
    return df

def fetch_itaee_mock() -> tuple[pd.DataFrame, dict]:
    estados = [f"{i:02d}" for i in range(1, 33)]
    nombres = [
        "Aguascalientes","Baja California","Baja California Sur","Campeche","Coahuila","Colima",
        "Chiapas","Chihuahua","Ciudad de México","Durango","Guanajuato","Guerrero","Hidalgo",
        "Jalisco","México","Michoacán","Morelos","Nayarit","Nuevo León","Oaxaca","Puebla",
        "Querétaro","Quintana Roo","San Luis Potosí","Sinaloa","Sonora","Tabasco","Tamaulipas",
        "Tlaxcala","Veracruz","Yucatán","Zacatecas"
    ]
    periods = ["2024T4","2025T1"]

    rows = []
    rng = np.random.default_rng(42)
    for cve, nom in zip(estados, nombres):
        base = rng.uniform(95, 105)
        for i, p in enumerate(periods):
            rows.append({"cve_ent": cve, "estado": nom, "periodo": p, "valor": base + i * rng.uniform(-1, 2)})
    df = pd.DataFrame(rows)

    meta = {
        "source": "api",
        "fetched_at": pd.Timestamp.utcnow().strftime("%Y-%m-%d %H:%M UTC"),
        "note": "MOCK de arranque; sustituir por API INEGI/BD",
    }
    return df, meta