"""
============================================================================
 AIRCRAFT ROUTING - ESQUADRAO ARARA - V2
============================================================================
 Primeira versao do modelo OAMRP usando dados reais pre-processados do
 registro de voo 2025.

 Diferencas em relacao ao v1:
   - le o CSV consolidado gerado a partir da Planilha 2025;
   - filtra uma janela de datas, com janeiro/2025 como padrao;
   - usa aeronaves reais observadas no registro: 2800, 2803, 2809 e 2811;
   - gera um CSV de resultado em db/processed/.

 Esta versao ainda e uma rodada exploratoria. Todas as missoes recebem
 prioridade 1 e a base de manutencao considerada e SBMN.
============================================================================
"""

from __future__ import annotations

import argparse
import csv
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path

import pulp


BASE_DIR = Path(__file__).resolve().parents[1]
CSV_REGISTRO_PADRAO = BASE_DIR / "db" / "processed" / "registro_voo_2025_consolidado.csv"
CSV_RESULTADO_PADRAO = BASE_DIR / "db" / "processed" / "resultado_oamrp_jan_2025.csv"

DATA_INICIO_PADRAO = "2025-01-01"
DATA_FIM_PADRAO = "2025-02-01"  # exclusivo

BASE_MANUTENCAO = "SBMN"
BASE_INICIAL = BASE_MANUTENCAO
TURNAROUND_MIN_H = 1.0
LIMITE_HORAS_F = 300.0
PRIORIDADE_PADRAO = 1

AERONAVES = [
    # id    base inicial  horas acumuladas desde ultima inspecao
    ("2800", BASE_INICIAL, 0.0),
    ("2803", BASE_INICIAL, 0.0),
    ("2809", BASE_INICIAL, 0.0),
    ("2811", BASE_INICIAL, 0.0),
]


@dataclass(frozen=True)
class Missao:
    id: str
    orig: str
    dest: str
    partida_utc: datetime
    chegada_utc: datetime
    prioridade: int
    periodo: str
    linhas_origem: str
    aeronave_real: str
    om: str

    @property
    def dur_h(self) -> float:
        return (self.chegada_utc - self.partida_utc).total_seconds() / 3600


def parse_utc(valor: str) -> datetime:
    texto = str(valor).strip()
    if texto.endswith("Z"):
        texto = texto[:-1] + "+00:00"
    dt = datetime.fromisoformat(texto)
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc)


def parse_data_inicio(valor: str) -> datetime:
    return datetime.fromisoformat(valor).replace(tzinfo=timezone.utc)


def carregar_missoes(caminho: Path, inicio: datetime, fim: datetime) -> list[Missao]:
    missoes: list[Missao] = []
    with caminho.open(newline="", encoding="utf-8-sig") as arquivo:
        leitor = csv.DictReader(arquivo)
        for linha in leitor:
            partida = parse_utc(linha["partida_utc"])
            chegada = parse_utc(linha["chegada_utc"])
            if not (inicio <= partida < fim):
                continue
            if chegada <= partida:
                raise ValueError(f"Missao {linha['id_registro']}: chegada antes da partida")
            missoes.append(
                Missao(
                    id=linha["id_registro"],
                    orig=linha["localidade_dep"],
                    dest=linha["localidade_arr"],
                    partida_utc=partida,
                    chegada_utc=chegada,
                    prioridade=PRIORIDADE_PADRAO,
                    periodo=linha.get("periodo", ""),
                    linhas_origem=linha.get("linhas_origem", ""),
                    aeronave_real=linha.get("aeronave", ""),
                    om=linha.get("om", ""),
                )
            )
    return sorted(missoes, key=lambda m: (m.partida_utc, m.id))


def horas_relativas(missoes: list[Missao]) -> tuple[dict[str, dict[str, object]], datetime]:
    horizonte_inicio = min(m.partida_utc for m in missoes)
    dados = {}
    for missao in missoes:
        dados[missao.id] = {
            "orig": missao.orig,
            "dest": missao.dest,
            "part": (missao.partida_utc - horizonte_inicio).total_seconds() / 3600,
            "cheg": (missao.chegada_utc - horizonte_inicio).total_seconds() / 3600,
            "prio": missao.prioridade,
            "dur": missao.dur_h,
            "obj": missao,
        }
    return dados, horizonte_inicio


def resolver(missoes: list[Missao]) -> dict[str, object]:
    if not missoes:
        raise ValueError("Nenhuma missao encontrada na janela informada")

    miss, horizonte_inicio = horas_relativas(missoes)
    acft = {a[0]: {"base": a[1], "f": a[2]} for a in AERONAVES}
    ids = list(miss.keys())
    ks = list(acft.keys())

    conex = [
        (i, j)
        for i in ids
        for j in ids
        if i != j
        and miss[i]["dest"] == miss[j]["orig"]
        and miss[j]["part"] >= miss[i]["cheg"] + TURNAROUND_MIN_H
    ]

    def pode_iniciar(k: str, i: str) -> bool:
        return miss[i]["orig"] == acft[k]["base"]

    mdl = pulp.LpProblem("ARARA_OAMRP_v2", pulp.LpMaximize)

    x = {(k, i): pulp.LpVariable(f"x_{k}_{i}", cat="Binary") for k in ks for i in ids}
    s = {
        (k, i): pulp.LpVariable(f"s_{k}_{i}", cat="Binary")
        for k in ks
        for i in ids
        if pode_iniciar(k, i)
    }
    c = {
        (k, i, j): pulp.LpVariable(f"c_{k}_{i}_{j}", cat="Binary")
        for k in ks
        for (i, j) in conex
    }
    z = {i: pulp.LpVariable(f"z_{i}", cat="Binary") for i in ids}

    mdl += pulp.lpSum(miss[i]["prio"] * z[i] for i in ids), "missoes_cumpridas_ponderadas"

    for i in ids:
        mdl += z[i] == pulp.lpSum(x[(k, i)] for k in ks), f"def_z_{i}"

    for k in ks:
        for i in ids:
            entradas = pulp.lpSum(c[(k, jj, i)] for (jj, ii) in conex if ii == i)
            inicio = s[(k, i)] if (k, i) in s else 0
            mdl += inicio + entradas == x[(k, i)], f"in_{k}_{i}"

    for k in ks:
        for i in ids:
            saidas = pulp.lpSum(c[(k, i, jj)] for (ii, jj) in conex if ii == i)
            mdl += saidas <= x[(k, i)], f"out_{k}_{i}"

    for k in ks:
        inicios = [s[(k, i)] for i in ids if (k, i) in s]
        if inicios:
            mdl += pulp.lpSum(inicios) <= 1, f"start_{k}"

    for k in ks:
        mdl += (
            pulp.lpSum(miss[i]["dur"] * x[(k, i)] for i in ids)
            <= LIMITE_HORAS_F - acft[k]["f"]
        ), f"horas_{k}"

    solver = pulp.PULP_CBC_CMD(msg=False)
    mdl.solve(solver)
    prio_otima = int(round(pulp.value(mdl.objective) or 0))

    # Criterio secundario: entre solucoes com a mesma prioridade, minimiza horas
    # totais atribuidas. Neste v2 isso evita voos extras irrelevantes se houver
    # empate em prioridade.
    mdl += pulp.lpSum(miss[i]["prio"] * z[i] for i in ids) == prio_otima, "fixa_prioridade"
    mdl.sense = pulp.LpMinimize
    mdl.setObjective(pulp.lpSum(miss[i]["dur"] * x[(k, i)] for k in ks for i in ids))
    mdl.solve(solver)

    return {
        "modelo": mdl,
        "miss": miss,
        "acft": acft,
        "ids": ids,
        "ks": ks,
        "conex": conex,
        "x": x,
        "s": s,
        "c": c,
        "z": z,
        "horizonte_inicio": horizonte_inicio,
        "prioridade_otima": prio_otima,
    }


def rota_da_aeronave(sol: dict[str, object], k: str) -> list[str]:
    ids = sol["ids"]
    conex = sol["conex"]
    s = sol["s"]
    c = sol["c"]
    inicio = [
        i
        for i in ids
        if (k, i) in s and s[(k, i)].value() is not None and s[(k, i)].value() > 0.5
    ]
    if not inicio:
        return []

    seq = [inicio[0]]
    while True:
        atual = seq[-1]
        prox = [
            j
            for (ii, j) in conex
            if ii == atual and c[(k, atual, j)].value() is not None and c[(k, atual, j)].value() > 0.5
        ]
        if not prox:
            break
        seq.append(prox[0])
    return seq


def escrever_resultado(sol: dict[str, object], caminho: Path) -> None:
    caminho.parent.mkdir(parents=True, exist_ok=True)
    miss = sol["miss"]
    z = sol["z"]
    linhas = []
    for k in sol["ks"]:
        for ordem, mid in enumerate(rota_da_aeronave(sol, k), start=1):
            obj = miss[mid]["obj"]
            linhas.append(
                {
                    "aeronave_modelo": k,
                    "ordem": ordem,
                    "id": mid,
                    "om": obj.om,
                    "orig": obj.orig,
                    "dest": obj.dest,
                    "partida_utc": obj.partida_utc.isoformat().replace("+00:00", "Z"),
                    "chegada_utc": obj.chegada_utc.isoformat().replace("+00:00", "Z"),
                    "dur_h": f"{obj.dur_h:.2f}",
                    "prioridade": obj.prioridade,
                    "periodo": obj.periodo,
                    "aeronave_real_2025": obj.aeronave_real,
                    "linhas_origem": obj.linhas_origem,
                    "status": "cumprida",
                }
            )

    for mid in sol["ids"]:
        if not z[mid].value() or z[mid].value() <= 0.5:
            obj = miss[mid]["obj"]
            linhas.append(
                {
                    "aeronave_modelo": "",
                    "ordem": "",
                    "id": mid,
                    "om": obj.om,
                    "orig": obj.orig,
                    "dest": obj.dest,
                    "partida_utc": obj.partida_utc.isoformat().replace("+00:00", "Z"),
                    "chegada_utc": obj.chegada_utc.isoformat().replace("+00:00", "Z"),
                    "dur_h": f"{obj.dur_h:.2f}",
                    "prioridade": obj.prioridade,
                    "periodo": obj.periodo,
                    "aeronave_real_2025": obj.aeronave_real,
                    "linhas_origem": obj.linhas_origem,
                    "status": "nao_cumprida",
                }
            )

    colunas = [
        "status",
        "aeronave_modelo",
        "ordem",
        "id",
        "om",
        "orig",
        "dest",
        "partida_utc",
        "chegada_utc",
        "dur_h",
        "prioridade",
        "periodo",
        "aeronave_real_2025",
        "linhas_origem",
    ]
    with caminho.open("w", newline="", encoding="utf-8-sig") as arquivo:
        escritor = csv.DictWriter(arquivo, fieldnames=colunas)
        escritor.writeheader()
        escritor.writerows(linhas)


def imprimir_relatorio(sol: dict[str, object], resultado_csv: Path) -> None:
    miss = sol["miss"]
    z = sol["z"]
    ids = sol["ids"]
    cobertas = [i for i in ids if z[i].value() is not None and z[i].value() > 0.5]
    prioridade_total = sum(int(miss[i]["prio"]) for i in ids)
    prioridade_obtida = sum(int(miss[i]["prio"]) for i in cobertas)

    print("=" * 72)
    print(" OAMRP V2 - Registro de voo 2025 consolidado")
    print("=" * 72)
    print(f" Status solver: {pulp.LpStatus[sol['modelo'].status]}")
    print(f" Horizonte inicia em UTC: {sol['horizonte_inicio'].isoformat().replace('+00:00', 'Z')}")
    print(f" Missoes cumpridas: {len(cobertas)}/{len(ids)}")
    print(f" Prioridade obtida: {prioridade_obtida}/{prioridade_total}")
    print(f" Resultado CSV: {resultado_csv}")

    for k in sol["ks"]:
        seq = rota_da_aeronave(sol, k)
        horas = sum(float(miss[i]["dur"]) for i in seq)
        print(f"\n Aeronave {k}: {len(seq)} missoes | {horas:.1f}h")
        for mid in seq[:12]:
            obj = miss[mid]["obj"]
            print(
                f"   {obj.partida_utc:%d/%m %H:%M}Z "
                f"{obj.orig}->{obj.dest} OM {obj.om} ({mid})"
            )
        if len(seq) > 12:
            print(f"   ... mais {len(seq) - 12} missoes")

    nao_cumpridas = [i for i in ids if i not in cobertas]
    if nao_cumpridas:
        print("\n Missoes nao cumpridas (primeiras 20):")
        for mid in nao_cumpridas[:20]:
            obj = miss[mid]["obj"]
            print(f"   {obj.partida_utc:%d/%m %H:%M}Z {obj.orig}->{obj.dest} OM {obj.om} ({mid})")
        if len(nao_cumpridas) > 20:
            print(f"   ... mais {len(nao_cumpridas) - 20} missoes")


def main() -> None:
    parser = argparse.ArgumentParser(description="Resolve OAMRP v2 com registro de voo real.")
    parser.add_argument("--csv", type=Path, default=CSV_REGISTRO_PADRAO)
    parser.add_argument("--inicio", default=DATA_INICIO_PADRAO)
    parser.add_argument("--fim", default=DATA_FIM_PADRAO)
    parser.add_argument("--resultado", type=Path, default=CSV_RESULTADO_PADRAO)
    args = parser.parse_args()

    inicio = parse_data_inicio(args.inicio)
    fim = parse_data_inicio(args.fim)
    missoes = carregar_missoes(args.csv, inicio, fim)
    sol = resolver(missoes)
    escrever_resultado(sol, args.resultado)
    imprimir_relatorio(sol, args.resultado)


if __name__ == "__main__":
    main()
