"""Atualização do DNS Shield ISP pelo serviço de licenças do desenvolvedor.

O painel do provedor não fala com o GitHub: ele consulta o ``licenca-server``
para saber qual versão está publicada e baixa o tarball. As atualizações do
sistema são livres; não exigem CNPJ/chave nem contrato ativo. A integridade do
pacote é garantida pelo SHA-256 publicado.

Apenas os módulos AnaBlock e Federação continuam sendo liberados por licença.

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

import hashlib
import re
import shutil
import subprocess
import tarfile
import tempfile
import time
from pathlib import Path

import requests

from . import licenca
from .config import APP_VERSION, get_config

ENDPOINT_VERSAO = "/api/pacote/versao"
ENDPOINT_ARQUIVO = "/api/pacote/arquivo"
TIMEOUT = 30
DOWNLOAD_TIMEOUT = 600


def _parse(version: str) -> tuple[int, ...]:
    nums = re.findall(r"\d+", version or "")
    return tuple(int(n) for n in nums[:4]) or (0,)


def local_version() -> str:
    return APP_VERSION


def _base_url() -> str:
    cfg_licenca = licenca.get_settings()
    return (cfg_licenca.get("url") or get_config().licenca_url or licenca.DEFAULT_URL).rstrip("/")


def _params_extra() -> dict[str, str]:
    """Envia CNPJ/chave como telemetria, se existirem; não exige."""
    cfg = licenca.get_settings()
    cnpj = licenca.somente_digitos(cfg.get("cnpj") or "")
    chave = (cfg.get("chave") or "").strip()
    out: dict[str, str] = {"versao": APP_VERSION}
    if cnpj:
        out["cnpj"] = cnpj
    if chave:
        out["chave"] = chave
    return out


def check() -> dict:
    """Consulta a versão publicada no serviço de licenças."""
    base = _base_url()
    result = {
        "local_version": APP_VERSION,
        "remote_version": None,
        "update_available": False,
        "changelog": "",
        "error": None,
        "checked_at": time.strftime("%d/%m/%Y %H:%M:%S"),
        "servico": base,
    }

    try:
        resp = requests.get(
            f"{base}{ENDPOINT_VERSAO}",
            params=_params_extra(),
            timeout=TIMEOUT,
            headers={"User-Agent": f"DNS-Shield-ISP/{APP_VERSION}"},
        )
    except requests.RequestException as exc:
        result["error"] = f"Serviço de atualizações inacessível: {exc}"
        return result

    if resp.status_code != 200:
        result["error"] = f"Serviço de atualizações respondeu HTTP {resp.status_code}."
        return result

    try:
        data = resp.json()
    except ValueError:
        result["error"] = "Resposta do serviço de atualizações em formato inválido."
        return result

    remote = str(data.get("versao") or "").lstrip("vV")
    result["remote_version"] = remote or None
    result["changelog"] = str(data.get("notas") or "")
    result["sha256"] = str(data.get("sha256") or "")
    if remote:
        result["update_available"] = _parse(remote) > _parse(APP_VERSION)
    else:
        result["error"] = "Nenhuma versão publicada no serviço de atualizações."
    return result



def _run(cmd: list[str], cwd: str | None = None) -> tuple[int, str]:
    res = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=600)
    return res.returncode, ((res.stdout or "") + (res.stderr or "")).strip()


def backup_database() -> str:
    cfg = get_config()
    src = Path(cfg.db_path)
    if not src.exists():
        return ""
    dest_dir = src.parent / "backups"
    dest_dir.mkdir(parents=True, exist_ok=True)
    dest = dest_dir / f"{src.stem}-{time.strftime('%Y%m%d-%H%M%S')}.db"
    try:
        subprocess.run(
            ["sqlite3", str(src), f".backup '{dest}'"],
            capture_output=True, text=True, timeout=120, check=True,
        )
    except (subprocess.SubprocessError, FileNotFoundError):
        shutil.copy2(src, dest)
    return str(dest)


def _baixar_pacote(destino: Path) -> tuple[bool, str]:
    base = _base_url()
    try:
        with requests.get(
            f"{base}{ENDPOINT_ARQUIVO}",
            params=_params_extra(),
            timeout=DOWNLOAD_TIMEOUT,
            stream=True,
            headers={"User-Agent": f"DNS-Shield-ISP/{APP_VERSION}"},
        ) as resp:
            if resp.status_code != 200:
                return False, f"Serviço de atualizações respondeu HTTP {resp.status_code}."
            with destino.open("wb") as fh:
                for chunk in resp.iter_content(chunk_size=65536):
                    if chunk:
                        fh.write(chunk)
    except requests.RequestException as exc:
        return False, f"Falha no download: {exc}"
    return True, f"{destino.stat().st_size} bytes"


def _sha256(caminho: Path) -> str:
    h = hashlib.sha256()
    with caminho.open("rb") as fh:
        for bloco in iter(lambda: fh.read(65536), b""):
            h.update(bloco)
    return h.hexdigest()



def _raiz_da_aplicacao(extraido: Path) -> Path | None:
    if (extraido / "app.py").exists():
        return extraido
    for candidato in sorted(extraido.rglob("app.py")):
        if (candidato.parent / "dnsshield").is_dir():
            return candidato.parent
    return None


def apply_update() -> dict:
    """Baixa o pacote publicado, valida o checksum e aplica preservando o SQLite."""
    cfg = get_config()
    steps: list[dict] = []

    info = check()
    steps.append({
        "step": "Versão publicada",
        "ok": not info.get("error"),
        "output": info.get("error") or f"{info['local_version']} → {info.get('remote_version')}",
    })
    if info.get("error"):
        return {"success": False, "steps": steps}

    backup = backup_database()
    steps.append({
        "step": "Backup do banco",
        "ok": True,
        "output": backup or "banco inexistente (nada a copiar)",
    })

    with tempfile.TemporaryDirectory(prefix="dnsshield-update-") as tmp:
        tmpdir = Path(tmp)
        pacote = tmpdir / "dns-shield-isp.tar.gz"
        ok, out = _baixar_pacote(pacote)
        steps.append({"step": "Download do pacote", "ok": ok, "output": out})
        if not ok:
            return {"success": False, "steps": steps}

        esperado = (info.get("sha256") or "").lower()
        atual = _sha256(pacote)
        if esperado:
            conferiu = esperado == atual
            steps.append({
                "step": "Checksum SHA-256",
                "ok": conferiu,
                "output": atual if conferiu else f"esperado {esperado}, obtido {atual}",
            })
            if not conferiu:
                return {"success": False, "steps": steps}
        else:
            steps.append({
                "step": "Checksum SHA-256",
                "ok": True,
                "output": f"{atual} (sem checksum publicado para comparar)",
            })


        destino = tmpdir / "extraido"
        try:
            with tarfile.open(pacote, "r:gz") as tf:
                tf.extractall(destino)
        except (tarfile.TarError, OSError) as exc:
            steps.append({"step": "Extração do pacote", "ok": False, "output": str(exc)})
            return {"success": False, "steps": steps}

        raiz = _raiz_da_aplicacao(destino)
        steps.append({
            "step": "Extração do pacote",
            "ok": raiz is not None,
            "output": str(raiz) if raiz else "app.py não encontrado no pacote",
        })
        if raiz is None:
            return {"success": False, "steps": steps}

        install_dir = Path(cfg.install_dir)
        backup_dir = Path(f"{cfg.install_dir}.bak")
        try:
            if install_dir.exists():
                if backup_dir.exists():
                    shutil.rmtree(backup_dir)
                shutil.copytree(
                    install_dir, backup_dir, symlinks=True,
                    ignore=shutil.ignore_patterns("venv"),
                )
            for item in raiz.iterdir():
                if item.name == "venv":
                    continue
                alvo = install_dir / item.name
                if item.is_dir():
                    if alvo.exists():
                        shutil.rmtree(alvo)
                    shutil.copytree(item, alvo, symlinks=True)
                else:
                    shutil.copy2(item, alvo)
        except OSError as exc:
            steps.append({"step": "Aplicação dos arquivos", "ok": False, "output": str(exc)})
            return {"success": False, "steps": steps}

        steps.append({
            "step": "Aplicação dos arquivos",
            "ok": True,
            "output": f"copiado para {install_dir} (backup em {backup_dir})",
        })

    pip = str(Path(cfg.install_dir) / "venv" / "bin" / "pip")
    if Path(pip).exists():
        code, out = _run([pip, "install", "-r", "requirements.txt"], cwd=cfg.install_dir)
        steps.append({"step": "Dependências Python", "ok": code == 0, "output": out[-2000:]})

    from .db import connect, migrate

    conn = connect()
    try:
        version = migrate(conn)
        steps.append({
            "step": "Migração do banco",
            "ok": True,
            "output": f"schema na versão {version}",
        })
    except Exception as exc:  # noqa: BLE001
        steps.append({"step": "Migração do banco", "ok": False, "output": str(exc)})
        return {"success": False, "steps": steps}
    finally:
        conn.close()

    code, out = _run(["systemctl", "restart", cfg.panel_service])
    steps.append({"step": "Reinício do serviço", "ok": code == 0, "output": out or "ok"})
    return {"success": all(s["ok"] for s in steps), "steps": steps}
