"""Geração da zona RPZ e recarga do BIND9 sem downtime."""
from __future__ import annotations

import re
import sqlite3
import shutil
import subprocess
import tempfile
import time
from pathlib import Path

from .config import get_config
from .db import get_db

DOMAIN_RE = re.compile(
    r"^(?=.{1,253}$)(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-))+$"
)


class RPZError(RuntimeError):
    pass


def normalize_domain(raw: str) -> str | None:
    """Normaliza entradas como 'https://WWW.Site.com/path' -> 'site.com'."""
    if not raw:
        return None
    value = raw.strip().strip('"').strip("'").lower()
    value = re.sub(r"^[a-z]+://", "", value)
    value = value.split("/")[0].split("?")[0].split("#")[0]
    value = value.split("@")[-1]
    value = value.split(":")[0]
    value = value.rstrip(".")
    if value.startswith("www."):
        value = value[4:]
    if not value or not DOMAIN_RE.match(value):
        return None
    try:
        value.encode("ascii")
    except UnicodeEncodeError:
        return None
    return value


def allowed_domains() -> list[str]:
    """Domínios liberados judicialmente (passthrough). Têm prioridade absoluta."""
    try:
        rows = get_db().execute(
            "SELECT domain FROM liberacoes WHERE active = 1 ORDER BY domain"
        ).fetchall()
    except sqlite3.Error:
        return []
    return [r["domain"] for r in rows]


def active_domains() -> list[str]:
    rows = get_db().execute(
        "SELECT domain FROM blocked_domains WHERE active = 1 ORDER BY domain"
    ).fetchall()
    allowed = set(allowed_domains())
    return [r["domain"] for r in rows if r["domain"] not in allowed]


def _serial() -> str:
    return str(int(time.time()))


def render_zone(domains: list[str], passthrough: list[str] | None = None) -> str:
    cfg = get_config()
    passthrough = passthrough or []
    allowed = set(passthrough)
    lines = [
        "; DNS Shield ISP - zona RPZ gerada automaticamente. NAO EDITE MANUALMENTE.",
        "; Desenvolvedor: Euquias Junior <junior@jbinfo.com.br>",
        "$TTL 60",
        f"@   IN  SOA localhost. admin.{cfg.rpz_zone}. (",
        f"        {_serial()} ; serial",
        "        300        ; refresh",
        "        60         ; retry",
        "        604800     ; expire",
        "        60 )       ; minimum",
        "    IN  NS  localhost.",
        "",
    ]
    if passthrough:
        lines.append("; --- Liberacoes judiciais (passthrough): prioridade sobre bloqueios ---")
        for domain in passthrough:
            lines.append(f"{domain}    IN  CNAME rpz-passthru.")
            lines.append(f"*.{domain}  IN  CNAME rpz-passthru.")
        lines.append("")
        lines.append("; --- Bloqueios judiciais ---")
    for domain in domains:
        if domain in allowed:
            continue
        lines.append(f"{domain}    IN  A     {cfg.sinkhole_ip}")
        lines.append(f"*.{domain}  IN  A     {cfg.sinkhole_ip}")
        if cfg.sinkhole_ipv6:
            lines.append(f"{domain}    IN  AAAA  {cfg.sinkhole_ipv6}")
            lines.append(f"*.{domain}  IN  AAAA  {cfg.sinkhole_ipv6}")
    lines.append("")
    return "\n".join(lines)



def _run(cmd: list[str]) -> subprocess.CompletedProcess:
    try:
        return subprocess.run(cmd, capture_output=True, text=True, timeout=60)
    except FileNotFoundError as exc:
        nome = Path(cmd[0]).name
        raise RPZError(
            f"Comando '{cmd[0]}' não encontrado. Verifique se o BIND9 (bind9utils) "
            f"está instalado neste servidor. Dica: apt install bind9-utils. Detalhe: {exc}"
        ) from exc
    except subprocess.SubprocessError as exc:
        raise RPZError(f"Falha ao executar '{cmd[0]}': {exc}") from exc



def sync_zone() -> str:
    """Regrava a zona, valida e recarrega. Rollback automático em caso de erro."""
    cfg = get_config()
    if cfg.server_role != "primary":
        raise RPZError("Este servidor é secundário; a zona é recebida via AXFR.")

    target = Path(cfg.rpz_file)
    target.parent.mkdir(parents=True, exist_ok=True)
    backup = target.with_suffix(target.suffix + ".bak")
    if target.exists():
        shutil.copy2(target, backup)

    content = render_zone(active_domains(), allowed_domains())
    tmp_path = None
    try:
        with tempfile.NamedTemporaryFile(
            "w", dir=str(target.parent), delete=False, prefix=".rpz-", encoding="utf-8"
        ) as tmp:
            tmp.write(content)
            tmp_path = Path(tmp.name)

        check = _run([cfg.named_checkzone_bin, cfg.rpz_zone, str(tmp_path)])
        if check.returncode != 0:
            raise RPZError(f"named-checkzone rejeitou a zona: {check.stderr or check.stdout}")

        tmp_path.replace(target)
        tmp_path = None
        try:
            target.chmod(0o644)
        except PermissionError:
            pass

        reload_res = _run([cfg.rndc_bin, "reload", cfg.rpz_zone])
        if reload_res.returncode != 0:
            reload_res = _run([cfg.rndc_bin, "reload"])
        if reload_res.returncode != 0:
            if backup.exists():
                shutil.copy2(backup, target)
                _run([cfg.rndc_bin, "reload", cfg.rpz_zone])
            raise RPZError(f"Falha no rndc reload: {reload_res.stderr or reload_res.stdout}")

        return (reload_res.stdout or "zona recarregada").strip()
    finally:
        if tmp_path is not None and tmp_path.exists():
            tmp_path.unlink(missing_ok=True)
