"""Funções compartilhadas pelos scripts de coleta e tratamento.

Só biblioteca padrão (decisão D-004).
"""

import csv
import datetime as dt
import hashlib
import json
import time
import urllib.error
import urllib.request
from pathlib import Path

RAIZ = Path(__file__).resolve().parent.parent
BRUTOS = RAIZ / "dados" / "brutos"
TRATADOS = RAIZ / "dados" / "tratados"
FICHAS = RAIZ / "fontes" / "fichas"
METODOLOGIA = RAIZ / "metodologia"

HOJE = dt.date.today()
USER_AGENT = "Mozilla/5.0 (projeto comparacao-governos; coleta de dados publicos)"


# ---------------------------------------------------------------- fichas

def ler_ficha(caminho):
    """Lê o cabeçalho (front matter) de uma ficha: linhas `chave: valor` entre `---`."""
    linhas = caminho.read_text(encoding="utf-8").splitlines()
    if not linhas or linhas[0].strip() != "---":
        raise ValueError(f"{caminho} não começa com front matter")
    meta = {}
    for linha in linhas[1:]:
        if linha.strip() == "---":
            break
        if ":" in linha:
            chave, valor = linha.split(":", 1)
            meta[chave.strip()] = valor.strip()
    meta["_arquivo"] = caminho
    return meta


def ler_fichas(tema):
    pasta = FICHAS / tema
    return [ler_ficha(p) for p in sorted(pasta.glob("*.md"))]


def consultas(ficha, campo="consulta"):
    """Um campo pode listar várias consultas separadas por `|`."""
    return [c.strip() for c in ficha.get(campo, "").split("|") if c.strip()]


# ---------------------------------------------------------------- mandatos

def ler_mandatos():
    with open(METODOLOGIA / "mandatos.csv", encoding="utf-8") as f:
        mandatos = list(csv.DictReader(f))
    for m in mandatos:
        m["inicio"] = dt.date.fromisoformat(m["inicio"])
        m["fim"] = dt.date.fromisoformat(m["fim"])
    return mandatos


def anos_do_mandato(m):
    """Anos civis inteiros dentro do mandato (D-006)."""
    return [a for a in range(m["inicio"].year, m["fim"].year + 1)
            if m["inicio"] <= dt.date(a, 1, 1) and dt.date(a, 12, 31) <= m["fim"]]


def ultimo_dia_do_mes(ano, mes):
    return dt.date(ano + mes // 12, mes % 12 + 1, 1) - dt.timedelta(days=1)


def meses_do_mandato(m):
    """Meses cujo último dia cai dentro do mandato (D-006), como 'AAAA-MM'."""
    meses = []
    ano, mes = m["inicio"].year, m["inicio"].month
    while dt.date(ano, mes, 1) <= m["fim"]:
        if m["inicio"] <= ultimo_dia_do_mes(ano, mes) <= m["fim"]:
            meses.append(f"{ano:04d}-{mes:02d}")
        ano, mes = (ano + 1, 1) if mes == 12 else (ano, mes + 1)
    return meses


# ---------------------------------------------------------------- download

def baixar(url, tentativas=4, aceitar_erro=()):
    """Baixa uma URL e devolve os bytes. Repete em erro temporário (502, 503, 504, timeout)."""
    ultimo_erro = None
    for i in range(tentativas):
        req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
        try:
            with urllib.request.urlopen(req, timeout=120) as r:
                return r.read()
        except urllib.error.HTTPError as e:
            if e.code in aceitar_erro:
                raise
            ultimo_erro = e
            if e.code not in (429, 500, 502, 503, 504):
                raise
        except (urllib.error.URLError, TimeoutError) as e:
            ultimo_erro = e
        time.sleep(3 * (i + 1))
    raise RuntimeError(f"falha ao baixar {url}: {ultimo_erro}")


def salvar_bruto(fonte, nome, conteudo, url, ext):
    """Grava o arquivo bruto com a data do dia no nome e registra no manifesto da pasta.

    Nunca sobrescreve: se já houver arquivo idêntico do mesmo dia, não grava de novo;
    se houver um diferente, grava com sufixo.
    """
    pasta = BRUTOS / fonte
    pasta.mkdir(parents=True, exist_ok=True)
    sha = hashlib.sha256(conteudo).hexdigest()
    base = f"{nome}_{HOJE.isoformat()}"
    caminho = pasta / f"{base}.{ext}"
    n = 1
    while caminho.exists():
        if hashlib.sha256(caminho.read_bytes()).hexdigest() == sha:
            return caminho
        n += 1
        caminho = pasta / f"{base}-{n}.{ext}"
    caminho.write_bytes(conteudo)

    manifesto = pasta / "manifesto.csv"
    novo = not manifesto.exists()
    with open(manifesto, "a", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        if novo:
            w.writerow(["arquivo", "consulta_url", "data_extracao", "sha256", "bytes"])
        w.writerow([caminho.name, url, dt.datetime.now().isoformat(timespec="seconds"), sha, len(conteudo)])
    return caminho


def brutos_mais_recentes(fonte, nome):
    """Arquivos brutos da extração mais recente de uma série.

    Nome dos arquivos: `<nome>[_<fatia>]_<AAAA-MM-DD>[-n].<ext>`. Devolve um arquivo por
    fatia (consultas longas são fatiadas), todos da data mais recente; se houver mais de
    um no mesmo dia, vale o de maior sufixo.
    """
    pasta = BRUTOS / fonte
    escolhidos = {}
    for p in pasta.glob(f"{nome}_*"):
        if p.name == "manifesto.csv":
            continue
        prefixo, carimbo = p.stem.rsplit("_", 1)
        chave = (carimbo[:10], int(carimbo[11:] or 1))  # "2026-10-05" ou "2026-10-05-2"
        if prefixo not in escolhidos or chave > escolhidos[prefixo][0]:
            escolhidos[prefixo] = (chave, p)
    if not escolhidos:
        return []
    ultima = max(chave[0] for chave, _ in escolhidos.values())
    return sorted(p for chave, p in escolhidos.values() if chave[0] == ultima)


def ler_manifesto(fonte):
    caminho = BRUTOS / fonte / "manifesto.csv"
    if not caminho.exists():
        return {}
    with open(caminho, encoding="utf-8") as f:
        return {linha["arquivo"]: linha for linha in csv.DictReader(f)}


def ler_json(caminho):
    return json.loads(Path(caminho).read_text(encoding="utf-8"))


# ---------------------------------------------------------------- csv

def escrever_csv(caminho, cabecalho, linhas):
    caminho.parent.mkdir(parents=True, exist_ok=True)
    with open(caminho, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow(cabecalho)
        w.writerows(linhas)


def ler_csv(caminho):
    with open(caminho, encoding="utf-8") as f:
        return list(csv.DictReader(f))
