"""
Pre-processa a planilha bruta de voos de 2025.

Entrada:
  db/raw/Planilha 2025.xlsx

Saidas:
  db/pre_process/registro_voo_2025/registro_voo_2025_limpo.xlsx
  db/pre_process/registro_voo_2025/registro_voo_2025_limpo.csv
  db/pre_process/registro_voo_2025/missoes_oamrp_2025.csv
  db/pre_process/registro_voo_2025/resumo_registro_voo_2025.json
  db/processed/registro_voo_2025.csv
"""

from __future__ import annotations

import csv
import json
from collections import Counter
from datetime import date, datetime, time, timedelta
from pathlib import Path
from typing import Any

import openpyxl


ANO = 2025
BASE_DIR = Path(__file__).resolve().parents[3]
RAW_PATH = BASE_DIR / "db" / "raw" / "Planilha 2025.xlsx"
PREPROCESS_DIR = BASE_DIR / "db" / "pre_process" / "registro_voo_2025"
PROCESSED_DIR = BASE_DIR / "db" / "processed"

LIMPO_XLSX = PREPROCESS_DIR / "registro_voo_2025_limpo.xlsx"
LIMPO_CSV = PREPROCESS_DIR / "registro_voo_2025_limpo.csv"
CONSOLIDADO_CSV = PREPROCESS_DIR / "registro_voo_2025_consolidado.csv"
MISSOES_CSV = PREPROCESS_DIR / "missoes_oamrp_2025.csv"
MISSOES_CONSOLIDADAS_CSV = PREPROCESS_DIR / "missoes_oamrp_2025_consolidado.csv"
RESUMO_JSON = PREPROCESS_DIR / "resumo_registro_voo_2025.json"
PROCESSED_CSV = PROCESSED_DIR / "registro_voo_2025.csv"
PROCESSED_CONSOLIDADO_CSV = PROCESSED_DIR / "registro_voo_2025_consolidado.csv"

MES_MAP = {
    "JAN": 1,
    "FEV": 2,
    "MAR": 3,
    "ABR": 4,
    "MAI": 5,
    "JUN": 6,
    "JUL": 7,
    "AGO": 8,
    "SET": 9,
    "OUT": 10,
    "NOV": 11,
    "DEZ": 12,
}
MES_ABBR = {numero: nome for nome, numero in MES_MAP.items()}
LINHAS_ORIGEM_EXCLUIDAS = {793}

COLUNAS_REGISTRO = [
    "id_registro",
    "linha_origem",
    "data",
    "data_iso",
    "mes",
    "mes_numero",
    "om",
    "aeronave",
    "etapa",
    "localidade_dep",
    "localidade_arr",
    "horario_z_dep",
    "horario_z_arr",
    "partida_utc",
    "chegada_utc",
    "tempo_voo",
    "tempo_voo_min",
    "segmento",
    "pousos",
    "missao",
    "periodo",
    "cruza_meia_noite",
]

COLUNAS_OAMRP = [
    "id",
    "orig",
    "dest",
    "partida_utc",
    "chegada_utc",
    "prioridade",
]

COLUNAS_CONSOLIDADAS = [
    "id_registro",
    "linhas_origem",
    "data",
    "data_iso",
    "mes",
    "mes_numero",
    "om",
    "aeronave",
    "etapa_inicio",
    "etapa_fim",
    "localidade_dep",
    "localidade_arr",
    "horario_z_dep",
    "horario_z_arr",
    "partida_utc",
    "chegada_utc",
    "tempo_voo",
    "tempo_voo_min",
    "segmento_inicio",
    "segmento_fim",
    "pousos",
    "missao",
    "periodo",
    "cruza_meia_noite",
    "qtd_linhas_originais",
    "tem_rota_intermediaria",
]


def texto_limpo(valor: Any) -> str:
    if valor is None:
        return ""
    return str(valor).strip()


def normalizar_mes(valor: Any) -> str:
    texto = texto_limpo(valor).upper().replace(".", "")
    return texto


def parse_data(valor_data: Any, valor_mes: Any) -> date:
    if isinstance(valor_data, datetime):
        return valor_data.date()
    if isinstance(valor_data, date):
        return valor_data

    texto = texto_limpo(valor_data).lower().replace(".", "")
    if "/" not in texto:
        raise ValueError(f"Data invalida: {valor_data!r}")
    dia_txt, mes_txt = texto.split("/", 1)
    mes_key = normalizar_mes(mes_txt or valor_mes)
    if mes_key not in MES_MAP:
        raise ValueError(f"Mes invalido: data={valor_data!r}, mes={valor_mes!r}")
    return date(ANO, MES_MAP[mes_key], int(dia_txt))


def minutos_do_dia(valor: Any) -> tuple[int, int]:
    """Retorna (minutos no dia, deslocamento em dias informado pelo Excel)."""
    if isinstance(valor, datetime):
        return valor.hour * 60 + valor.minute, 0
    if isinstance(valor, time):
        return valor.hour * 60 + valor.minute, 0
    if isinstance(valor, timedelta):
        total_min = int(round(valor.total_seconds() / 60))
        return total_min % (24 * 60), total_min // (24 * 60)
    if isinstance(valor, (int, float)):
        total_min = int(round(float(valor) * 24 * 60))
        return total_min % (24 * 60), total_min // (24 * 60)

    texto = texto_limpo(valor)
    if ":" in texto:
        partes = texto.split(":")
        return int(partes[0]) * 60 + int(partes[1]), 0
    raise ValueError(f"Horario invalido: {valor!r}")


def minutos_duracao(valor: Any) -> int:
    if isinstance(valor, datetime):
        return valor.hour * 60 + valor.minute
    if isinstance(valor, time):
        return valor.hour * 60 + valor.minute
    if isinstance(valor, timedelta):
        return int(round(valor.total_seconds() / 60))
    if isinstance(valor, (int, float)):
        return int(round(float(valor) * 24 * 60))

    texto = texto_limpo(valor)
    if ":" in texto:
        horas, minutos = texto.split(":", 1)
        return int(horas) * 60 + int(minutos)
    raise ValueError(f"Duracao invalida: {valor!r}")


def hhmm(minutos: int) -> str:
    minutos = minutos % (24 * 60)
    return f"{minutos // 60:02d}:{minutos % 60:02d}"


def duracao_hhmm(minutos: int) -> str:
    return f"{minutos // 60:02d}:{minutos % 60:02d}"


def iso_utc(data_voo: date, minutos: int, deslocamento_dias: int = 0) -> str:
    data_hora = datetime.combine(data_voo, time()) + timedelta(
        days=deslocamento_dias,
        minutes=minutos,
    )
    return data_hora.isoformat(timespec="minutes") + ":00Z"


def atualizar_datas_do_registro(
    registro: dict[str, Any],
    partida: datetime,
    chegada: datetime,
) -> None:
    data_voo = partida.date()
    registro["data"] = data_voo.strftime("%d/%m")
    registro["data_iso"] = data_voo.isoformat()
    registro["mes_numero"] = data_voo.month
    registro["mes"] = MES_ABBR[data_voo.month]
    registro["partida_utc"] = partida.isoformat(timespec="minutes").replace("+00:00", "") + ":00Z"
    registro["chegada_utc"] = chegada.isoformat(timespec="minutes").replace("+00:00", "") + ":00Z"
    registro["cruza_meia_noite"] = chegada.date() > partida.date()


def id_registro(linha: dict[str, Any]) -> str:
    om = texto_limpo(linha["om"]) or "SEMOM"
    etapa = texto_limpo(linha["etapa"]) or "SEMETAPA"
    return f"RV2025_OM{om}_E{etapa}_L{linha['linha_origem']}"


def linha_tem_voo(ws: openpyxl.worksheet.worksheet.Worksheet, row: int) -> bool:
    return any(ws.cell(row, col).value not in (None, "") for col in range(1, 13))


def extrair_registros() -> list[dict[str, Any]]:
    wb = openpyxl.load_workbook(RAW_PATH, data_only=True)
    ws = wb.active

    registros: list[dict[str, Any]] = []
    for row in range(4, ws.max_row + 1):
        if row in LINHAS_ORIGEM_EXCLUIDAS:
            continue
        if not linha_tem_voo(ws, row):
            continue

        data_voo = parse_data(ws.cell(row, 1).value, ws.cell(row, 2).value)
        mes = normalizar_mes(ws.cell(row, 2).value)
        dep_min, dep_offset = minutos_do_dia(ws.cell(row, 8).value)
        arr_min, arr_offset_excel = minutos_do_dia(ws.cell(row, 9).value)
        arr_offset = max(arr_offset_excel, 1 if arr_min <= dep_min else 0)
        tempo_min = minutos_duracao(ws.cell(row, 10).value)

        registro = {
            "linha_origem": row,
            "data": data_voo.strftime("%d/%m"),
            "data_iso": data_voo.isoformat(),
            "mes": mes,
            "mes_numero": MES_MAP.get(mes, ""),
            "om": ws.cell(row, 3).value,
            "aeronave": ws.cell(row, 4).value,
            "etapa": ws.cell(row, 5).value,
            "localidade_dep": texto_limpo(ws.cell(row, 6).value).upper(),
            "localidade_arr": texto_limpo(ws.cell(row, 7).value).upper(),
            "horario_z_dep": hhmm(dep_min),
            "horario_z_arr": hhmm(arr_min),
            "partida_utc": iso_utc(data_voo, dep_min, dep_offset),
            "chegada_utc": iso_utc(data_voo, arr_min, arr_offset),
            "tempo_voo": duracao_hhmm(tempo_min),
            "tempo_voo_min": tempo_min,
            "segmento": ws.cell(row, 11).value,
            "pousos": ws.cell(row, 12).value,
            "missao": texto_limpo(ws.cell(row, 35).value).upper(),
            "periodo": texto_limpo(ws.cell(row, 36).value).upper(),
            "cruza_meia_noite": arr_offset > dep_offset,
        }
        registro["id_registro"] = id_registro(registro)
        registros.append(registro)

    corrigir_continuidade_temporal(registros)
    return registros


def corrigir_continuidade_temporal(registros: list[dict[str, Any]]) -> None:
    grupos: dict[tuple[str, str], list[dict[str, Any]]] = {}
    for registro in registros:
        chave = (texto_limpo(registro["om"]), texto_limpo(registro["aeronave"]))
        grupos.setdefault(chave, []).append(registro)

    for linhas in grupos.values():
        linhas.sort(key=lambda registro: registro["linha_origem"])
        chegada_anterior: datetime | None = None
        for registro in linhas:
            partida = parse_iso_utc(registro["partida_utc"])
            chegada = parse_iso_utc(registro["chegada_utc"])
            while chegada_anterior is not None and partida < chegada_anterior:
                partida += timedelta(days=1)
                chegada += timedelta(days=1)
            atualizar_datas_do_registro(registro, partida, chegada)
            chegada_anterior = chegada


def escrever_csv(caminho: Path, colunas: list[str], linhas: list[dict[str, Any]]) -> None:
    with caminho.open("w", newline="", encoding="utf-8-sig") as arquivo:
        escritor = csv.DictWriter(arquivo, fieldnames=colunas)
        escritor.writeheader()
        escritor.writerows(linhas)


def escrever_xlsx(caminho: Path, colunas: list[str], linhas: list[dict[str, Any]]) -> None:
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "registro_voo_2025"
    ws.append(colunas)
    for linha in linhas:
        ws.append([linha[coluna] for coluna in colunas])
    ws.freeze_panes = "A2"
    ws.auto_filter.ref = ws.dimensions
    for column_cells in ws.columns:
        letter = column_cells[0].column_letter
        max_len = max(len(texto_limpo(cell.value)) for cell in column_cells)
        ws.column_dimensions[letter].width = min(max(max_len + 2, 10), 28)
    wb.save(caminho)


def preparar_oamrp(registros: list[dict[str, Any]]) -> list[dict[str, Any]]:
    linhas = []
    for registro in registros:
        linhas.append(
            {
                "id": registro["id_registro"],
                "orig": registro["localidade_dep"],
                "dest": registro["localidade_arr"],
                "partida_utc": registro["partida_utc"],
                "chegada_utc": registro["chegada_utc"],
                "prioridade": 1,
            }
        )
    return linhas


def toca_rota(registro: dict[str, Any]) -> bool:
    return "ROTA" in {registro["localidade_dep"], registro["localidade_arr"]}


def parse_iso_utc(valor: str) -> datetime:
    return datetime.fromisoformat(valor.replace("Z", "+00:00"))


def consolidar_grupo(grupo: list[dict[str, Any]]) -> dict[str, Any]:
    primeiro = grupo[0]
    ultimo = grupo[-1]
    tempo_total = sum(int(registro["tempo_voo_min"]) for registro in grupo)
    pousos_total = sum(int(registro["pousos"] or 0) for registro in grupo)
    missoes = sorted({registro["missao"] for registro in grupo if registro["missao"]})
    periodos = sorted({registro["periodo"] for registro in grupo if registro["periodo"]})
    linhas = [str(registro["linha_origem"]) for registro in grupo]
    cruza_meia_noite = parse_iso_utc(ultimo["chegada_utc"]).date() > parse_iso_utc(
        primeiro["partida_utc"]
    ).date()

    return {
        "id_registro": f"{primeiro['id_registro']}_C{ultimo['linha_origem']}",
        "linhas_origem": ";".join(linhas),
        "data": primeiro["data"],
        "data_iso": primeiro["data_iso"],
        "mes": primeiro["mes"],
        "mes_numero": primeiro["mes_numero"],
        "om": primeiro["om"],
        "aeronave": primeiro["aeronave"],
        "etapa_inicio": primeiro["etapa"],
        "etapa_fim": ultimo["etapa"],
        "localidade_dep": primeiro["localidade_dep"],
        "localidade_arr": ultimo["localidade_arr"],
        "horario_z_dep": primeiro["horario_z_dep"],
        "horario_z_arr": ultimo["horario_z_arr"],
        "partida_utc": primeiro["partida_utc"],
        "chegada_utc": ultimo["chegada_utc"],
        "tempo_voo": duracao_hhmm(tempo_total),
        "tempo_voo_min": tempo_total,
        "segmento_inicio": primeiro["segmento"],
        "segmento_fim": ultimo["segmento"],
        "pousos": pousos_total,
        "missao": ";".join(missoes),
        "periodo": ";".join(periodos),
        "cruza_meia_noite": cruza_meia_noite,
        "qtd_linhas_originais": len(grupo),
        "tem_rota_intermediaria": any(toca_rota(registro) for registro in grupo),
    }


def consolidar_transicoes_rota(registros: list[dict[str, Any]]) -> list[dict[str, Any]]:
    grupos: dict[tuple[str, str], list[dict[str, Any]]] = {}
    for registro in registros:
        chave = (texto_limpo(registro["om"]), texto_limpo(registro["aeronave"]))
        grupos.setdefault(chave, []).append(registro)

    consolidados: list[dict[str, Any]] = []
    for chave in sorted(grupos):
        linhas = sorted(
            grupos[chave],
            key=lambda registro: (parse_iso_utc(registro["partida_utc"]), registro["linha_origem"]),
        )
        idx = 0
        while idx < len(linhas):
            atual = linhas[idx]
            if not toca_rota(atual):
                consolidados.append(consolidar_grupo([atual]))
                idx += 1
                continue

            grupo = [atual]
            idx += 1
            while (
                idx < len(linhas)
                and toca_rota(linhas[idx])
                and grupo[-1]["localidade_arr"] == linhas[idx]["localidade_dep"]
            ):
                grupo.append(linhas[idx])
                idx += 1
                if grupo[-1]["localidade_arr"] != "ROTA":
                    break
            consolidados.append(consolidar_grupo(grupo))

    return sorted(consolidados, key=lambda registro: parse_iso_utc(registro["partida_utc"]))


def preparar_oamrp_consolidado(registros: list[dict[str, Any]]) -> list[dict[str, Any]]:
    linhas = []
    for registro in registros:
        linhas.append(
            {
                "id": registro["id_registro"],
                "orig": registro["localidade_dep"],
                "dest": registro["localidade_arr"],
                "partida_utc": registro["partida_utc"],
                "chegada_utc": registro["chegada_utc"],
                "prioridade": 1,
            }
        )
    return linhas


def gerar_resumo(
    registros: list[dict[str, Any]],
    registros_consolidados: list[dict[str, Any]],
) -> dict[str, Any]:
    faltantes = {
        coluna: sum(registro[coluna] in (None, "") for registro in registros)
        for coluna in COLUNAS_REGISTRO
    }
    return {
        "arquivo_origem": str(RAW_PATH.relative_to(BASE_DIR)),
        "ano_assumido": ANO,
        "linhas_origem_excluidas": sorted(LINHAS_ORIGEM_EXCLUIDAS),
        "registros_extraidos": len(registros),
        "registros_consolidados": len(registros_consolidados),
        "linhas_com_cruzamento_de_meia_noite": sum(
            1 for registro in registros if registro["cruza_meia_noite"]
        ),
        "linhas_com_rota": sum(1 for registro in registros if toca_rota(registro)),
        "trechos_consolidados_com_rota": sum(
            1 for registro in registros_consolidados if registro["tem_rota_intermediaria"]
        ),
        "aeronaves": dict(sorted(Counter(r["aeronave"] for r in registros).items())),
        "periodos": dict(Counter(r["periodo"] for r in registros)),
        "meses": dict(Counter(r["mes"] for r in registros)),
        "campos_faltantes": {k: v for k, v in faltantes.items() if v},
        "observacoes": [
            "Colunas REAL ate COM2 descartadas conforme criterio informado.",
            "Horarios tratados como UTC/Z; nao houve conversao de fuso.",
            "Chegadas iguais ou anteriores a partida foram deslocadas para o dia seguinte.",
            "SEGMTO foi preservado da planilha original, sem recalculo.",
            "Transicoes com ROTA tambem foram consolidadas em arquivo separado.",
            "Prioridade OAMRP definida como 1 por falta de coluna de prioridade na origem.",
        ],
    }


def main() -> None:
    PREPROCESS_DIR.mkdir(parents=True, exist_ok=True)
    PROCESSED_DIR.mkdir(parents=True, exist_ok=True)

    registros = extrair_registros()
    registros_consolidados = consolidar_transicoes_rota(registros)
    escrever_csv(LIMPO_CSV, COLUNAS_REGISTRO, registros)
    escrever_csv(PROCESSED_CSV, COLUNAS_REGISTRO, registros)
    escrever_xlsx(LIMPO_XLSX, COLUNAS_REGISTRO, registros)
    escrever_csv(CONSOLIDADO_CSV, COLUNAS_CONSOLIDADAS, registros_consolidados)
    escrever_csv(PROCESSED_CONSOLIDADO_CSV, COLUNAS_CONSOLIDADAS, registros_consolidados)
    escrever_csv(MISSOES_CSV, COLUNAS_OAMRP, preparar_oamrp(registros))
    escrever_csv(
        MISSOES_CONSOLIDADAS_CSV,
        COLUNAS_OAMRP,
        preparar_oamrp_consolidado(registros_consolidados),
    )

    with RESUMO_JSON.open("w", encoding="utf-8") as arquivo:
        json.dump(
            gerar_resumo(registros, registros_consolidados),
            arquivo,
            ensure_ascii=False,
            indent=2,
        )
        arquivo.write("\n")

    print(f"Registros extraidos: {len(registros)}")
    print(f"Registros consolidados: {len(registros_consolidados)}")
    print(f"CSV final: {PROCESSED_CSV.relative_to(BASE_DIR)}")
    print(f"CSV consolidado: {PROCESSED_CONSOLIDADO_CSV.relative_to(BASE_DIR)}")
    print(f"CSV OAMRP: {MISSOES_CSV.relative_to(BASE_DIR)}")
    print(f"CSV OAMRP consolidado: {MISSOES_CONSOLIDADAS_CSV.relative_to(BASE_DIR)}")


if __name__ == "__main__":
    main()
