"""Aplica config recebida do servidor: regera /etc/dnsmasq.d/judges.conf,
limpa o set ipset/nftset, ajusta política do nftables, recarrega servicos.

Funciona tanto com systemd quanto em container (sem systemd).
"""
from __future__ import annotations

import hashlib
import json
import logging
import os
import subprocess
from pathlib import Path

log = logging.getLogger(__name__)


DNSMASQ_HEADER = """# Gerado automaticamente pelo ppci-agent — NAO editar manualmente.
no-resolv
listen-address=127.0.0.1
bind-interfaces
cache-size=2000
neg-ttl=60
log-facility=/var/log/dnsmasq.log

# Bloqueio padrao: tudo cai em 127.0.0.1 / ::1
address=/#/127.0.0.1
address=/#/::1

# DNS-pin do servidor PPCI (sessao 14): resiliencia se o DNS autoritativo do
# campus cair. address= e mais especifico que /#/ e tem precedencia sobre server=;
# o IP e liberado pela regra estatica do nftables (ip daddr 200.134.31.237 accept).
# Re-emitido aqui em todo apply para SOBREVIVER ao resync do OTA.
address=/maratona.td.utfpr.edu.br/200.134.31.237
"""


def render_dnsmasq_conf(dominios: list[dict]) -> str:
    """Recebe lista [{'dominio': 'codeforces.com', 'categoria': 'externo'}, ...]
    e devolve o conteúdo completo de /etc/dnsmasq.d/judges.conf.
    """
    lines = [DNSMASQ_HEADER, "# === whitelist ==="]
    for d in dominios:
        dom = d.get("dominio", "").strip()
        if not dom:
            continue
        lines.append(f"server=/{dom}/1.1.1.1")
        lines.append(f"server=/{dom}/8.8.8.8")
        lines.append(f"nftset=/{dom}/4#inet#ppci#judges_ok,6#inet#ppci#judges_ok_v6")
    return "\n".join(lines) + "\n"


def compute_local_hash(modo: str, dominios: list[dict]) -> str:
    """Mesma logica de hash do servidor (Prova.config_hash)."""
    payload = {
        "modo": modo,
        "dominios": sorted([d["dominio"] for d in dominios if d.get("dominio")]),
    }
    raw = json.dumps(payload, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(raw.encode()).hexdigest()[:16]


def write_atomic(path: Path, content: str, mode: int = 0o644) -> None:
    """Escrita atomica: tmp -> rename."""
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(content)
    os.chmod(tmp, mode)
    tmp.replace(path)


def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess:
    log.debug("run: %s", " ".join(cmd))
    return subprocess.run(cmd, check=check, capture_output=True, text=True)


def reload_dnsmasq(no_systemd: bool) -> None:
    if no_systemd:
        # Mata e reinicia em foreground (container)
        run(["pkill", "-x", "dnsmasq"], check=False)
        run([
            "dnsmasq",
            "--conf-file=/etc/dnsmasq.conf",
            "--conf-dir=/etc/dnsmasq.d/,*.conf",
        ])
    else:
        # restart (não reload): SIGHUP do reload NÃO relê os arquivos de config
        # (judges.conf com as diretivas nftset=). Só restart aplica a whitelist nova.
        run(["systemctl", "restart", "dnsmasq"])


def flush_nftables_sets() -> None:
    """Limpa os sets para forçar repopulação via dnsmasq."""
    for set_name in ("judges_ok", "judges_ok_v6"):
        run(["nft", "flush", "set", "inet", "ppci", set_name], check=False)


def ensure_codigo_mount() -> None:
    """Monta a pasta de código compartilhada com o host (virtfs do launcher).

    O ppci-launcher passa `-virtfs ... mount_tag=codigo`. Aqui montamos esse
    9p em /home/ppci/Codigo. Idempotente: não faz nada se já montado, e
    silencia se o virtfs não existir (ex.: VM rodando sem o launcher).
    """
    dest = Path("/home/ppci/Codigo")
    already = subprocess.run(["mountpoint", "-q", str(dest)], capture_output=True)
    if already.returncode == 0:
        return
    try:
        dest.mkdir(parents=True, exist_ok=True)
        subprocess.run(
            ["mount", "-t", "9p", "-o",
             "trans=virtio,version=9p2000.L,msize=104857600",
             "codigo", str(dest)],
            check=True, capture_output=True, text=True,
        )
        subprocess.run(["chown", "ppci:ppci", str(dest)], check=False)
        log.info("pasta de código montada em %s", dest)
    except (subprocess.CalledProcessError, OSError):
        log.debug("virtfs 'codigo' indisponível — pulando mount da pasta de código")


def _reload_wallpaper_live(wp_path: Path, warns: list[str]) -> None:
    """Recarrega o fundo de tela ao vivo no LXQt (pcmanfm-qt).

    O agente roda como root, mas o desktop roda como o usuário 'ppci'.
    Executamos o pcmanfm-qt no contexto gráfico do ppci (DISPLAY/DBus).
    Sem ambiente gráfico (ex.: container Docker) → no-op silencioso.

    O pcmanfm-qt cacheia por caminho: setar a MESMA path não recarrega.
    Por isso fazemos toggle: set num arquivo tmp e volta pro canônico
    (termina apontando pro canônico, bom para o próximo boot).
    """
    import shutil
    if not shutil.which("pcmanfm-qt"):
        return
    try:
        import pwd
        uid = pwd.getpwnam("ppci").pw_uid
    except KeyError:
        return
    env = (
        f"DISPLAY=:0 XDG_RUNTIME_DIR=/run/user/{uid} "
        f"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{uid}/bus"
    )
    tmp = wp_path.with_name("wp-reload-tmp.png")
    try:
        shutil.copyfile(wp_path, tmp)
        os.chmod(tmp, 0o644)
        try:
            shutil.chown(tmp, user="ppci")
        except Exception:  # noqa: BLE001
            pass
        run(["su", "ppci", "-c", f"{env} pcmanfm-qt --set-wallpaper={tmp}"], check=False)
        run(["su", "ppci", "-c", f"{env} pcmanfm-qt --set-wallpaper={wp_path}"], check=False)
    except Exception as e:  # noqa: BLE001
        warns.append(f"wallpaper reload: {e}")
    finally:
        try:
            tmp.unlink()
        except OSError:
            pass


def apply_so_config(so: dict, server_url: str, token: str) -> list[str]:
    """Aplica configurações de SO: wallpaper, BOCA.desktop, resolução.

    Retorna lista de warnings (vazia se tudo ok).
    """
    warns: list[str] = []
    if not so:
        return warns

    # ---- BOCA.desktop ----
    boca_url = (so.get("boca_url") or "").strip()
    if boca_url:
        desktop_path = Path("/home/ppci/Desktop/BOCA.desktop")
        try:
            desktop_path.parent.mkdir(parents=True, exist_ok=True)
            desktop_path.write_text(
                "[Desktop Entry]\n"
                "Name=BOCA\n"
                "Comment=Juiz da maratona\n"
                f"Exec=falkon {boca_url}\n"
                "Type=Application\n"
                "Icon=falkon\n"
                "Terminal=false\n"
            )
            try:
                os.chmod(desktop_path, 0o755)
            except OSError:
                pass
        except Exception as e:  # noqa: BLE001
            warns.append(f"BOCA.desktop: {e}")

    # ---- Wallpaper ----
    wp_url = (so.get("wallpaper_url") or "").strip()
    if wp_url:
        wp_path = Path("/home/ppci/Imagens/wallpaper-ppci-linux.png")
        try:
            wp_path.parent.mkdir(parents=True, exist_ok=True)
            import requests
            headers = {"Authorization": f"Bearer {token}"} if "/media/" in wp_url else {}
            r = requests.get(wp_url, headers=headers, timeout=15)
            r.raise_for_status()
            new_bytes = r.content
            old_bytes = wp_path.read_bytes() if wp_path.exists() else b""
            # Só reescreve+recarrega se o wallpaper realmente mudou — evita
            # a tela "piscar" a cada resync disparado por outra config.
            if new_bytes != old_bytes:
                wp_path.write_bytes(new_bytes)
                try:
                    os.chmod(wp_path, 0o644)
                    import shutil
                    shutil.chown(wp_path, user="ppci")
                except Exception:  # noqa: BLE001
                    pass
                # O reload ao vivo é feito por ensure_so_live() — chamado a cada
                # tick até a sessão LXQt subir, corrigindo a corrida do 1º boot.
        except Exception as e:  # noqa: BLE001
            warns.append(f"wallpaper: {e}")

    # ---- Resolução (xrandr) — só tenta se xrandr existe ----
    res = (so.get("resolucao") or "").strip()
    if res and Path("/usr/bin/xrandr").exists():
        try:
            run(["xrandr", "-s", res], check=False)
        except Exception as e:  # noqa: BLE001
            warns.append(f"xrandr: {e}")

    return warns


def _session_ready() -> bool:
    """True se a sessão gráfica do ppci já está de pé (DBus do usuário existe)."""
    try:
        import pwd
        uid = pwd.getpwnam("ppci").pw_uid
    except KeyError:
        return False
    return Path(f"/run/user/{uid}/bus").exists()


def ensure_so_live(so: dict) -> bool:
    """Re-afirma o SO config (wallpaper/BOCA) no contexto da sessão gráfica.

    Chamado a cada tick até a sessão LXQt estar de pé. Corrige a corrida do 1º
    boot: o agente (systemd) sobe antes do desktop, então o set-wallpaper inicial
    falha e o BOCA não é pego pelo scan. Os ARQUIVOS já foram escritos por
    apply_so_config; aqui só fazemos a parte que exige a sessão (DISPLAY/DBus).
    Retorna True quando conseguiu aplicar ao vivo (sessão pronta).
    """
    if not so:
        return True
    if not _session_ready():
        return False
    warns: list[str] = []
    if (so.get("wallpaper_url") or "").strip():
        wp_path = Path("/home/ppci/Imagens/wallpaper-ppci-linux.png")
        if wp_path.exists():
            _reload_wallpaper_live(wp_path, warns)
    if (so.get("boca_url") or "").strip():
        boca = Path("/home/ppci/Desktop/BOCA.desktop")
        if boca.exists():
            # dono ppci + executável → o desktop confia/exibe o atalho
            try:
                import shutil
                shutil.chown(boca, user="ppci", group="ppci")
                os.chmod(boca, 0o755)
            except Exception:  # noqa: BLE001
                pass
    if warns:
        log.warning("SO live warnings: %s", "; ".join(warns))
    return True


def apply_config(payload: dict, no_systemd: bool, server_url: str = "", token: str = "") -> tuple[str, str]:
    """Aplica config inteira; retorna (hash_aplicado, erro_msg|'').

    payload tem o formato:
      {
        "prova": "slug" | None,
        "modo": "treino" | "prova",
        "dominios": [{"dominio": "...", "categoria": "..."}],
        "so": {"wallpaper_url": "...", "boca_url": "...", "resolucao": "..."},
        "config_hash": "abc...",
      }
    """
    dominios = payload.get("dominios") or []
    modo = payload.get("modo") or "treino"
    so = payload.get("so") or {}
    # Hash veio do servidor; usar esse para garantir bate-bate no painel.
    server_hash = payload.get("config_hash") or ""

    try:
        # 1. Regerar dnsmasq.conf
        conf = render_dnsmasq_conf(dominios)
        write_atomic(Path("/etc/dnsmasq.d/judges.conf"), conf)

        # 2. Recarregar dnsmasq
        reload_dnsmasq(no_systemd)

        # 3. Limpar sets (sera repopulado conforme resolves acontecam)
        flush_nftables_sets()

        # 4. Aplicar config do SO (wallpaper, BOCA.desktop, resolução)
        so_warns = apply_so_config(so, server_url, token)
        if so_warns:
            log.warning("SO config warnings: %s", "; ".join(so_warns))

        applied_hash = server_hash or compute_local_hash(modo, dominios)
        log.info("Config aplicada: %d dominios, hash=%s, so_warns=%d",
                 len(dominios), applied_hash, len(so_warns))
        return applied_hash, ""
    except subprocess.CalledProcessError as e:
        err = f"{e.cmd[0]} falhou: {(e.stderr or e.stdout or str(e)).strip()[:500]}"
        log.error(err)
        return "", err
    except Exception as e:  # noqa: BLE001
        log.exception("erro aplicando config")
        return "", str(e)[:500]
