"""Federação DNS Shield ISP.

Permite que instalações do DNS Shield ISP em provedores diferentes troquem
listas de bloqueio e de liberação judicial entre si, no mesmo modelo do
AnaBlock: cada servidor pode publicar as próprias listas (API protegida por
token) e consumir as listas de parceiros cadastrados.

Regras invioláveis:
  * bloqueios manuais do provedor local nunca são apagados pela federação;
  * liberações locais (passthrough) continuam com prioridade absoluta;
  * cada parceiro tem origem própria (``federacao:<id>``), então a remoção de
    domínios ausentes afeta apenas o que veio daquele parceiro.

Desenvolvedor: Euquias Junior <junior@jbinfo.com.br>
"""
from __future__ import annotations

import secrets
import sqlite3
from dataclasses import dataclass

import requests

from . import audit, rpz
from .db import get_db, get_provider

TIMEOUT = 60
USER_AGENT = "DNS-Shield-ISP-Federacao/1.3.0"
PROCESSO_PADRAO = "Federação DNS Shield ISP"


class FederacaoError(RuntimeError):
    pass


@dataclass
class PeerResult:
    peer_id: int
    nome: str
    total_remoto: int
    incluidos: int
    removidos: int
    liberacoes: int
    mensagem: str


def origem_peer(peer_id: int) -> str:
    return f"federacao:{peer_id}"


# ------------------------------------------------------------------ publicação

def get_publicacao() -> dict:
    db = get_db()
    try:
        row = db.execute("SELECT * FROM federacao_publicacao WHERE id = 1").fetchone()
    except sqlite3.Error:
        row = None
    if row is None:
        return {
            "publicar": 0,
            "token_publicacao": "",
            "publicar_liberacoes": 1,
            "consumir": 0,
            "frequencia": 360,
            "remover_ausentes": 0,
            "ultima_sincronizacao": None,
            "ultimo_status": "",
        }
    return dict(row)


def save_publicacao(
    *,
    publicar: bool,
    publicar_liberacoes: bool,
    consumir: bool,
    frequencia: int,
    remover_ausentes: bool,
    token: str | None = None,
) -> None:
    atual = get_publicacao()
    novo_token = token if token else (atual.get("token_publicacao") or "")
    if publicar and not novo_token:
        novo_token = gerar_token()
    db = get_db()
    db.execute(
        "INSERT INTO federacao_publicacao (id, publicar, token_publicacao, "
        "publicar_liberacoes, consumir, frequencia, remover_ausentes) "
        "VALUES (1, ?, ?, ?, ?, ?, ?) "
        "ON CONFLICT(id) DO UPDATE SET publicar = excluded.publicar, "
        "token_publicacao = excluded.token_publicacao, "
        "publicar_liberacoes = excluded.publicar_liberacoes, "
        "consumir = excluded.consumir, frequencia = excluded.frequencia, "
        "remover_ausentes = excluded.remover_ausentes",
        (
            1 if publicar else 0,
            novo_token,
            1 if publicar_liberacoes else 0,
            1 if consumir else 0,
            max(int(frequencia), 15),
            1 if remover_ausentes else 0,
        ),
    )
    db.commit()


def gerar_token() -> str:
    return secrets.token_urlsafe(32)


def rotacionar_token() -> str:
    token = gerar_token()
    db = get_db()
    db.execute(
        "INSERT INTO federacao_publicacao (id, token_publicacao) VALUES (1, ?) "
        "ON CONFLICT(id) DO UPDATE SET token_publicacao = excluded.token_publicacao",
        (token,),
    )
    db.commit()
    return token


def payload_publicacao() -> dict:
    """Monta o JSON entregue aos provedores parceiros."""
    cfg = get_publicacao()
    db = get_db()
    bloqueios = db.execute(
        "SELECT domain, process_no, origem FROM blocked_domains "
        "WHERE active = 1 ORDER BY domain"
    ).fetchall()
    data = {
        "schema": 1,
        "gerado_por": "DNS Shield ISP",
        "provedor": get_provider().get("trade_name") or "",
        "bloqueios": [
            {"domain": r["domain"], "processo": r["process_no"], "origem": r["origem"]}
            for r in bloqueios
        ],
        "liberacoes": [],
    }
    if int(cfg.get("publicar_liberacoes") or 0):
        liberacoes = db.execute(
            "SELECT domain, processo FROM liberacoes WHERE active = 1 ORDER BY domain"
        ).fetchall()
        data["liberacoes"] = [
            {"domain": r["domain"], "processo": r["processo"]} for r in liberacoes
        ]
    return data


# ---------------------------------------------------------------------- parceiros

def listar_peers() -> list[dict]:
    try:
        rows = get_db().execute(
            "SELECT * FROM federacao_peers ORDER BY nome COLLATE NOCASE"
        ).fetchall()
    except sqlite3.Error:
        return []
    return [dict(r) for r in rows]


def get_peer(peer_id: int) -> dict | None:
    row = get_db().execute(
        "SELECT * FROM federacao_peers WHERE id = ?", (peer_id,)
    ).fetchone()
    return dict(row) if row else None


def adicionar_peer(
    *, nome: str, url: str, token: str, importar_liberacoes: bool, enabled: bool
) -> int:
    db = get_db()
    try:
        cur = db.execute(
            "INSERT INTO federacao_peers (nome, url, token, enabled, importar_liberacoes) "
            "VALUES (?, ?, ?, ?, ?)",
            (nome, url.rstrip("/"), token, 1 if enabled else 0, 1 if importar_liberacoes else 0),
        )
        db.commit()
        return int(cur.lastrowid or 0)
    except sqlite3.IntegrityError as exc:
        raise FederacaoError("Já existe um parceiro cadastrado com esta URL.") from exc


def remover_peer(peer_id: int) -> dict | None:
    peer = get_peer(peer_id)
    if peer is None:
        return None
    db = get_db()
    db.execute("DELETE FROM blocked_domains WHERE origem = ?", (origem_peer(peer_id),))
    db.execute("DELETE FROM federacao_peers WHERE id = ?", (peer_id,))
    db.commit()
    return peer


def _peer_status(peer_id: int, status: str, total: int | None = None) -> None:
    db = get_db()
    if total is None:
        db.execute(
            "UPDATE federacao_peers SET ultimo_status = ?, "
            "ultima_sincronizacao = datetime('now') WHERE id = ?",
            (status[:300], peer_id),
        )
    else:
        db.execute(
            "UPDATE federacao_peers SET ultimo_status = ?, total_importados = ?, "
            "ultima_sincronizacao = datetime('now') WHERE id = ?",
            (status[:300], total, peer_id),
        )
    db.commit()


def _record_status(status: str) -> None:
    db = get_db()
    db.execute(
        "UPDATE federacao_publicacao SET ultimo_status = ?, "
        "ultima_sincronizacao = datetime('now') WHERE id = 1",
        (status[:300],),
    )
    db.commit()


# -------------------------------------------------------------------- consumo

def fetch_peer(peer: dict) -> dict:
    url = (peer.get("url") or "").rstrip("/")
    if not url.startswith(("http://", "https://")):
        raise FederacaoError("URL do parceiro inválida.")
    headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
    token = (peer.get("token") or "").strip()
    if token:
        headers["Authorization"] = f"Bearer {token}"
    try:
        resp = requests.get(url, headers=headers, timeout=TIMEOUT)
    except requests.RequestException as exc:
        raise FederacaoError(f"falha de conexão: {exc}") from exc
    if resp.status_code in (401, 403):
        raise FederacaoError(
            f"o parceiro recusou a credencial (HTTP {resp.status_code}). "
            "Confirme o token de federação com o provedor parceiro."
        )
    if resp.status_code != 200:
        raise FederacaoError(f"HTTP {resp.status_code} ao consultar o parceiro.")
    try:
        data = resp.json()
    except ValueError as exc:
        raise FederacaoError("o parceiro respondeu em formato inválido (JSON esperado).") from exc
    if not isinstance(data, dict):
        raise FederacaoError("o parceiro respondeu em formato inesperado.")
    return data


def _extrair(itens: object) -> list[tuple[str, str]]:
    out: list[tuple[str, str]] = []
    seen: set[str] = set()
    if not isinstance(itens, list):
        return out
    for item in itens:
        raw = ""
        processo = ""
        if isinstance(item, str):
            raw = item
        elif isinstance(item, dict):
            for key in ("domain", "dominio", "name", "value"):
                if isinstance(item.get(key), str):
                    raw = item[key]
                    break
            for key in ("processo", "process_no", "process"):
                if isinstance(item.get(key), str):
                    processo = item[key]
                    break
        domain = rpz.normalize_domain(raw)
        if domain and domain not in seen:
            seen.add(domain)
            out.append((domain, processo.strip()))
    return out


def sync_peer(peer: dict, *, remover_ausentes: bool | None = None) -> PeerResult:
    peer_id = int(peer["id"])
    origem = origem_peer(peer_id)
    if remover_ausentes is None:
        remover_ausentes = bool(int(get_publicacao().get("remover_ausentes") or 0))

    try:
        data = fetch_peer(peer)
    except FederacaoError as exc:
        _peer_status(peer_id, f"erro: {exc}")
        raise

    bloqueios = _extrair(data.get("bloqueios") or data.get("blocks") or data.get("domains"))
    liberacoes = (
        _extrair(data.get("liberacoes") or data.get("allowlist") or [])
        if int(peer.get("importar_liberacoes") or 0)
        else []
    )

    db = get_db()
    locais_liberados = {
        r["domain"] for r in db.execute("SELECT domain FROM liberacoes WHERE active = 1")
    }
    existing = {
        r["domain"]: r
        for r in db.execute(
            "SELECT id, domain, active FROM blocked_domains WHERE origem = ?", (origem,)
        ).fetchall()
    }
    remoto = {d for d, _ in bloqueios}
    incluidos = 0
    removidos = 0

    for domain, processo in bloqueios:
        if domain in locais_liberados:
            continue
        row = existing.get(domain)
        if row is not None:
            if row["active"] != 1:
                db.execute("UPDATE blocked_domains SET active = 1 WHERE id = ?", (row["id"],))
                incluidos += 1
            continue
        try:
            db.execute(
                "INSERT INTO blocked_domains (domain, process_no, notes, created_by, origem) "
                "VALUES (?, ?, ?, ?, ?)",
                (
                    domain,
                    processo or PROCESSO_PADRAO,
                    f"Recebido da federação: {peer.get('nome')}",
                    "federacao",
                    origem,
                ),
            )
            incluidos += 1
        except sqlite3.IntegrityError:
            # já existe como bloqueio manual/AnaBlock: preserva a origem local
            continue

    if remover_ausentes:
        for domain, row in existing.items():
            if domain not in remoto and row["active"] == 1:
                db.execute("DELETE FROM blocked_domains WHERE id = ?", (row["id"],))
                removidos += 1

    # liberações recebidas entram como sugestão ativa de passthrough
    liberadas = 0
    for domain, processo in liberacoes:
        try:
            db.execute(
                "INSERT INTO liberacoes (domain, processo, usuario, notes) VALUES (?, ?, ?, ?)",
                (
                    domain,
                    processo or PROCESSO_PADRAO,
                    "federacao",
                    f"Recebido da federação: {peer.get('nome')}",
                ),
            )
            liberadas += 1
        except sqlite3.IntegrityError:
            continue

    db.commit()

    mensagem = (
        f"{len(bloqueios)} bloqueios no parceiro · {incluidos} incluídos · "
        f"{removidos} removidos · {liberadas} liberação(ões) importada(s)"
    )
    _peer_status(peer_id, f"ok: {mensagem}", len(bloqueios))
    return PeerResult(peer_id, peer.get("nome") or "", len(bloqueios), incluidos, removidos, liberadas, mensagem)


def sync_all(*, apenas_habilitados: bool = True) -> list[PeerResult]:
    """Sincroniza todos os parceiros e regrava a zona RPZ uma única vez."""
    from . import licenca

    try:
        licenca.garantir_modulo("federacao")
    except licenca.LicencaError as exc:
        _record_status(f"bloqueado: {exc}")
        raise FederacaoError(str(exc)) from exc

    peers = [p for p in listar_peers() if not apenas_habilitados or int(p.get("enabled") or 0)]
    if not peers:
        _record_status("nenhum parceiro habilitado")
        raise FederacaoError("Nenhum provedor parceiro habilitado para sincronizar.")

    resultados: list[PeerResult] = []
    erros: list[str] = []
    for peer in peers:
        try:
            resultados.append(sync_peer(peer))
        except FederacaoError as exc:
            erros.append(f"{peer.get('nome')}: {exc}")
            _peer_status(int(peer["id"]), f"erro: {exc}")

    total_inc = sum(r.incluidos for r in resultados)
    total_rem = sum(r.removidos for r in resultados)
    resumo = (
        f"{len(resultados)}/{len(peers)} parceiro(s) sincronizado(s) · "
        f"{total_inc} incluídos · {total_rem} removidos"
    )
    if erros:
        resumo += " · falhas: " + "; ".join(erros)[:200]
    _record_status(("ok: " if resultados else "erro: ") + resumo)
    audit.log("federacao_sync", resumo)

    if resultados:
        try:
            rpz.sync_zone()
        except rpz.RPZError as exc:
            audit.log("rpz_erro", f"sincronização da federação: {exc}")
    if not resultados:
        raise FederacaoError(resumo)
    return resultados
