"""Validação retrospectiva usando voos históricos."""

from __future__ import annotations

from datetime import datetime
from pathlib import Path

import numpy as np
import pandas as pd
from scipy.optimize import Bounds, LinearConstraint, milp

from .data_io import carregar_cadastro
from .utils import normalizar_texto


COLUNAS_VOOS_2025 = ["data", "aeronave", "tipo_escala", "tripulante", "funcao", "oi", "horas_voadas", "sbv"]
COLUNAS_OPCIONAIS_VOOS_2025 = ["slot_id"]


def criar_modelo_voos_2025(path: Path) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    if path.exists():
        return
    pd.DataFrame(columns=COLUNAS_VOOS_2025).to_csv(path, index=False)


def carregar_voos_2025(path: Path) -> pd.DataFrame:
    df = pd.read_csv(path)
    for col in COLUNAS_VOOS_2025:
        if col not in df.columns:
            df[col] = pd.NA
    for col in COLUNAS_OPCIONAIS_VOOS_2025:
        if col not in df.columns:
            df[col] = pd.NA
    df = df[COLUNAS_OPCIONAIS_VOOS_2025 + COLUNAS_VOOS_2025].copy()
    df["data"] = pd.to_datetime(df["data"], errors="coerce").dt.date
    for col in ["aeronave", "tipo_escala", "tripulante", "funcao", "oi"]:
        df[col] = df[col].map(normalizar_texto)
    df["horas_voadas"] = pd.to_numeric(df["horas_voadas"], errors="coerce").fillna(0)
    df["sbv"] = pd.to_numeric(df["sbv"], errors="coerce").fillna(0)
    return df.dropna(subset=["data"]).reset_index(drop=True)


def _montar_slots(voos: pd.DataFrame) -> pd.DataFrame:
    voados = voos[voos["horas_voadas"] > 0].copy()
    if voados.empty:
        return pd.DataFrame(columns=["slot_id", "data", "aeronave", "tipo_escala", "horas_voadas"])
    if "slot_id" in voados.columns and not voados["slot_id"].isna().all():
        voos_unicos = (
            voados.groupby(["slot_id", "data", "aeronave", "tipo_escala"], as_index=False)
            .agg(
                horas_voadas=("horas_voadas", "max"),
                tripulantes_reais=("tripulante", lambda s: ", ".join(sorted(set(s)))),
            )
        )
        slots = (
            voos_unicos.groupby(["data", "aeronave", "tipo_escala"], as_index=False)
            .agg(
                horas_voadas=("horas_voadas", "sum"),
                tripulantes_reais=("tripulantes_reais", lambda s: ", ".join(sorted(set(", ".join(s).split(", "))))),
            )
            .sort_values(["data", "aeronave", "tipo_escala"])
            .reset_index(drop=True)
        )
    else:
        slots = (
            voados.groupby(["data", "aeronave", "tipo_escala"], as_index=False)
            .agg(horas_voadas=("horas_voadas", "max"), tripulantes_reais=("tripulante", lambda s: ", ".join(sorted(set(s)))))
            .sort_values(["data", "aeronave", "tipo_escala"])
            .reset_index(drop=True)
        )
    slots["slot_id"] = slots.index + 1
    return slots[["slot_id", "data", "aeronave", "tipo_escala", "horas_voadas", "tripulantes_reais"]]


def _metricas(horas: pd.Series, meta_horas: float) -> dict:
    horas = horas.astype(float)
    return {
        "tripulantes": int(len(horas)),
        "horas_total": round(float(horas.sum()), 2),
        "horas_media": round(float(horas.mean()), 2) if len(horas) else 0,
        "desvio_padrao": round(float(horas.std(ddof=0)), 2) if len(horas) else 0,
        "menor_horas": round(float(horas.min()), 2) if len(horas) else 0,
        "maior_horas": round(float(horas.max()), 2) if len(horas) else 0,
        "abaixo_meta": int((horas < meta_horas).sum()),
        "desvio_medio_meta": round(float((horas - meta_horas).abs().mean()), 2) if len(horas) else 0,
    }


def otimizar_equalizacao_50(
    base_dir: Path,
    voos: pd.DataFrame,
    meta_horas: float = 50.0,
    tempo_limite_segundos: float = 180.0,
    gap_relativo: float = 0.05,
) -> dict:
    historico_vazio = pd.DataFrame(columns=["tripulante", "horas_voadas", "sbv"])
    cadastro = carregar_cadastro(base_dir, historico_vazio)
    trips_cadastro = set(cadastro["tripulante"])
    if "slot_id" in voos.columns and not voos["slot_id"].isna().all():
        slots_validos = (
            voos.groupby("slot_id")["tripulante"]
            .apply(lambda s: set(s).issubset(trips_cadastro))
            .loc[lambda s: s]
            .index
        )
        voos = voos[voos["slot_id"].isin(slots_validos)].copy()
    else:
        voos = voos[voos["tripulante"].isin(trips_cadastro)].copy()

    slots = _montar_slots(voos)
    if slots.empty:
        raise RuntimeError("A base de 2025 não possui linhas com horas_voadas maiores que zero.")

    trips = sorted(set(voos["tripulante"]) & trips_cadastro)
    if not trips:
        raise RuntimeError("Não há tripulantes da base de 2025 presentes no cadastro atual.")

    pares = []
    for s_idx, slot in slots.iterrows():
        qt_col = f"qt_{slot['aeronave'].lower()}"
        for t_idx, trip in enumerate(trips):
            linha = cadastro[cadastro["tripulante"] == trip]
            if not linha.empty and bool(linha.iloc[0].get(qt_col, False)):
                pares.append((s_idx, t_idx, float(slot["horas_voadas"])))
    if not pares:
        raise RuntimeError("Não há candidatos compatíveis entre aeronaves dos voos 2025 e cadastro.")

    n_x = len(pares)
    n_t = len(trips)
    hmax_idx = n_x + 2 * n_t
    hmin_idx = hmax_idx + 1
    n_vars = hmin_idx + 1
    c = np.zeros(n_vars)
    c[n_x : n_x + n_t] = 1
    c[n_x + n_t :] = 1
    c[hmax_idx] = 100
    c[hmin_idx] = -100
    integrality = np.zeros(n_vars)
    integrality[:n_x] = 1
    bounds = Bounds(np.zeros(n_vars), np.r_[np.ones(n_x), np.full(2 * n_t + 2, np.inf)])

    constraints = []
    for s_idx in range(len(slots)):
        row = np.zeros(n_vars)
        for var_idx, (slot_i, _, _) in enumerate(pares):
            if slot_i == s_idx:
                row[var_idx] = 1
        constraints.append(LinearConstraint(row, 2, 2))

    for data in sorted(slots["data"].unique()):
        slots_dia = set(slots.index[slots["data"] == data])
        for t_idx in range(n_t):
            row = np.zeros(n_vars)
            for var_idx, (slot_i, trip_i, _) in enumerate(pares):
                if slot_i in slots_dia and trip_i == t_idx:
                    row[var_idx] = 1
            constraints.append(LinearConstraint(row, -np.inf, 1))

    for t_idx in range(n_t):
        row = np.zeros(n_vars)
        for var_idx, (_, trip_i, horas) in enumerate(pares):
            if trip_i == t_idx:
                row[var_idx] = horas
        row[n_x + t_idx] = -1
        row[n_x + n_t + t_idx] = 1
        constraints.append(LinearConstraint(row, meta_horas, meta_horas))

        row = np.zeros(n_vars)
        for var_idx, (_, trip_i, horas) in enumerate(pares):
            if trip_i == t_idx:
                row[var_idx] = horas
        row[hmax_idx] = -1
        constraints.append(LinearConstraint(row, -np.inf, 0))

        row = np.zeros(n_vars)
        for var_idx, (_, trip_i, horas) in enumerate(pares):
            if trip_i == t_idx:
                row[var_idx] = horas
        row[hmin_idx] = -1
        constraints.append(LinearConstraint(row, 0, np.inf))

    result = milp(
        c=c,
        integrality=integrality,
        bounds=bounds,
        constraints=constraints,
        options={"time_limit": tempo_limite_segundos, "mip_rel_gap": gap_relativo},
    )
    if result.x is None:
        raise RuntimeError(f"Validação MILP inviável: {result.message}")

    escolhidos = result.x[:n_x] > 0.5
    linhas = []
    for var_idx, escolhido in enumerate(escolhidos):
        if not escolhido:
            continue
        s_idx, t_idx, horas = pares[var_idx]
        slot = slots.iloc[s_idx]
        linhas.append(
            {
                "slot_id": int(slot["slot_id"]),
                "data": slot["data"],
                "aeronave": slot["aeronave"],
                "tipo_escala": slot["tipo_escala"],
                "tripulante_otimizado": trips[t_idx],
                "horas_voadas": horas,
                "tripulantes_reais": slot["tripulantes_reais"],
            }
        )
    otimizados = pd.DataFrame(linhas)
    horas_reais = voos.groupby("tripulante")["horas_voadas"].sum().reindex(trips, fill_value=0)
    horas_otimizadas = otimizados.groupby("tripulante_otimizado")["horas_voadas"].sum().reindex(trips, fill_value=0)
    comparativo = pd.DataFrame(
        {
            "tripulante": trips,
            "horas_reais_2025": horas_reais.to_numpy(),
            "horas_otimizadas_meta_50": horas_otimizadas.to_numpy(),
        }
    )
    comparativo["delta_otimizado_menos_real"] = comparativo["horas_otimizadas_meta_50"] - comparativo["horas_reais_2025"]

    return {
        "slots": slots,
        "otimizados": otimizados,
        "comparativo": comparativo,
        "metricas": pd.DataFrame(
            [
                {"cenario": "real_2025", **_metricas(comparativo["horas_reais_2025"], meta_horas)},
                {"cenario": "otimizado_meta_50", **_metricas(comparativo["horas_otimizadas_meta_50"], meta_horas)},
            ]
        ),
        "status_solver": {
            "sucesso": bool(result.success),
            "mensagem": str(result.message),
            "fun_objetivo": float(result.fun) if result.fun is not None else None,
        },
    }


def salvar_relatorio_validacao(base_dir: Path, resultado: dict, meta_horas: float = 50.0) -> Path:
    out_dir = base_dir / "resultados" / "validacao"
    out_dir.mkdir(parents=True, exist_ok=True)
    arquivo = out_dir / f"validacao_2025_meta_{int(meta_horas)}_{datetime.now():%Y%m%d_%H%M%S}.xlsx"
    with pd.ExcelWriter(arquivo, engine="openpyxl") as writer:
        pd.DataFrame([resultado.get("status_solver", {})]).to_excel(writer, sheet_name="solver", index=False)
        resultado["metricas"].to_excel(writer, sheet_name="metricas", index=False)
        resultado["comparativo"].to_excel(writer, sheet_name="comparativo_trips", index=False)
        resultado["slots"].to_excel(writer, sheet_name="voos_2025_slots", index=False)
        resultado["otimizados"].to_excel(writer, sheet_name="escala_otimizada", index=False)
    return arquivo
