"""Módulo de integração com a API do AnaBlock (https://anablock.net.br).

Baixa a lista oficial de domínios com ordem de bloqueio no Brasil e mantém a
base local sincronizada, sem nunca tocar nos bloqueios manuais do provedor.

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

import json
import sqlite3
from dataclasses import dataclass

import requests

from . import audit, rpz
from .db import get_db

ORIGEM = "anablock"
PROCESSO = "Ordem AnaBlock"
DEFAULT_URL = "https://api.anablock.net.br/api/domain/all"
FALLBACK_URL = "https://api.anablock.net.br/domains/all"
TIMEOUT = 60


class AnaBlockError(RuntimeError):
    pass


@dataclass
class SyncResult:
    total_remoto: int
    incluidos: int
    removidos: int
    mensagem: str


def get_settings() -> dict:
    db = get_db()
    try:
        row = db.execute("SELECT * FROM config_anablock WHERE id = 1").fetchone()
    except sqlite3.Error:
        row = None
    if row is None:
        return {
            "enabled": 0,
            "token": "",
            "api_url": DEFAULT_URL,
            "frequencia": 360,
            "remover_ausentes": 1,
            "ultima_sincronizacao": None,
            "ultimo_status": "",
            "total_importados": 0,
        }
    return dict(row)


def save_settings(
    *,
    enabled: bool,
    token: str,
    api_url: str,
    frequencia: int,
    remover_ausentes: bool,
) -> None:
    db = get_db()
    db.execute(
        "INSERT INTO config_anablock (id, enabled, token, api_url, frequencia, remover_ausentes) "
        "VALUES (1, ?, ?, ?, ?, ?) "
        "ON CONFLICT(id) DO UPDATE SET enabled = excluded.enabled, token = excluded.token, "
        "api_url = excluded.api_url, frequencia = excluded.frequencia, "
        "remover_ausentes = excluded.remover_ausentes",
        (
            1 if enabled else 0,
            token,
            api_url or DEFAULT_URL,
            max(int(frequencia), 15),
            1 if remover_ausentes else 0,
        ),
    )
    db.commit()


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


def parse_payload(text: str) -> list[str]:
    """Aceita JSON (lista de strings ou objetos) ou texto com 1 domínio por linha."""
    raw: list[str] = []
    stripped = text.strip()
    if stripped[:1] in ("[", "{"):
        try:
            data = json.loads(stripped)
        except json.JSONDecodeError:
            data = None
        if isinstance(data, dict):
            for key in ("domains", "data", "objects", "result", "items"):
                if isinstance(data.get(key), list):
                    data = data[key]
                    break
        if isinstance(data, list):
            for item in data:
                if isinstance(item, str):
                    raw.append(item)
                elif isinstance(item, dict):
                    for key in ("domain", "dominio", "name", "object", "value"):
                        if isinstance(item.get(key), str):
                            raw.append(item[key])
                            break
    if not raw:
        for line in stripped.splitlines():
            line = line.strip()
            if not line or line.startswith(("#", ";", "//")):
                continue
            # formatos unbound/bind9 vem com aspas e diretivas
            if '"' in line:
                line = line.split('"')[1]
            raw.append(line.split()[0] if line.split() else line)

    domains: list[str] = []
    seen: set[str] = set()
    for value in raw:
        domain = rpz.normalize_domain(value)
        if domain and domain not in seen:
            seen.add(domain)
            domains.append(domain)
    return domains


def fetch_domains(settings: dict | None = None) -> list[str]:
    cfg = settings or get_settings()
    token = (cfg.get("token") or "").strip()
    urls = [cfg.get("api_url") or DEFAULT_URL]
    if FALLBACK_URL not in urls:
        urls.append(FALLBACK_URL)

    headers = {"User-Agent": "DNS-Shield-ISP/1.1.0"}
    if token:
        headers["Authorization"] = f"Bearer {token}"
        headers["X-API-Token"] = token

    last_error = ""
    for url in urls:
        try:
            resp = requests.get(url, headers=headers, timeout=TIMEOUT)
        except requests.RequestException as exc:
            last_error = f"{url}: {exc}"
            continue
        if resp.status_code in (401, 403):
            raise AnaBlockError(
                "O AnaBlock recusou a credencial (HTTP "
                f"{resp.status_code}). Verifique o token da API em Configurações."
            )
        if resp.status_code != 200:
            last_error = f"{url}: HTTP {resp.status_code}"
            continue
        domains = parse_payload(resp.text)
        if domains:
            return domains
        last_error = f"{url}: resposta sem domínios válidos"

    raise AnaBlockError(f"Não foi possível obter a lista do AnaBlock. {last_error}")


def sync() -> SyncResult:
    """Sincroniza a base local com a lista do AnaBlock e regrava a zona RPZ."""
    from . import licenca

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

    settings = get_settings()
    try:
        remote = fetch_domains(settings)
    except AnaBlockError as exc:
        _record_status(f"erro: {exc}")
        audit.log("anablock_erro", str(exc))
        raise

    db = get_db()
    existing = {
        row["domain"]: row
        for row in db.execute(
            "SELECT id, domain, active FROM blocked_domains WHERE origem = ?", (ORIGEM,)
        ).fetchall()
    }
    remote_set = set(remote)
    incluidos = 0
    removidos = 0

    for domain in remote:
        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, "Importado da API do AnaBlock", "anablock", ORIGEM),
            )
            incluidos += 1
        except sqlite3.IntegrityError:
            # já existe como bloqueio manual do provedor: preserva a origem manual
            continue

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

    db.commit()

    mensagem = (
        f"{len(remote)} domínios na lista do AnaBlock · "
        f"{incluidos} incluídos · {removidos} removidos"
    )
    _record_status(f"ok: {mensagem}", len(remote))
    audit.log("anablock_sync", mensagem)

    try:
        rpz.sync_zone()
    except rpz.RPZError as exc:
        audit.log("rpz_erro", f"sincronização AnaBlock: {exc}")
        mensagem += f" · atenção: a zona RPZ não recarregou ({exc})"

    return SyncResult(len(remote), incluidos, removidos, mensagem)
