#!/usr/bin/env python3
"""
SYLink HoneyBot — mini UI web port 8080.

Permet à l'admin client de :
  - voir l'URL d'activation complète + QR code scannable
  - cliquer directement le lien pour aller sur le portail UniSOC
  - configurer le réseau (DHCP / IP statique) AVANT l'activation
  - apply netplan localement

Design aligné avec le portail UniSOC (thème clair, cards rounded, palette slate/orange).
Auto-shutdown : quand license.json apparaît, re-run init.py pour basculer le banner
en mode COUVERTURE Windows BackupServer, puis stop ce service.
"""
from __future__ import annotations

import base64
import json
import os
import socket
import subprocess
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlparse, parse_qs

CONFIG_DIR = Path("/opt/sylink-honeypot/etc")
PORT = int(os.environ.get("SETUP_UI_PORT", "8080"))


# ──────────────────────────────────────────────────────────────────────────────
# Helpers state + réseau local
# ──────────────────────────────────────────────────────────────────────────────

def load_state() -> dict:
    state = {"activated": False}
    for fname in ("enroll.json", "license.json"):
        f = CONFIG_DIR / fname
        if not f.exists():
            continue
        try:
            data = json.loads(f.read_text())
        except Exception:
            continue
        if fname == "license.json" and (data.get("license_token") or data.get("api_key")):
            state["activated"] = True
            state["tenant_id"] = data.get("tenant_id")
            state["honeypot_id"] = data.get("honeypot_id")
        else:
            state.update(data)
    return state


def primary_iface() -> str:
    try:
        out = subprocess.run(["ip", "-4", "route", "show", "default"],
                             capture_output=True, text=True, timeout=4).stdout
        for line in out.splitlines():
            parts = line.split()
            if "dev" in parts:
                return parts[parts.index("dev") + 1]
    except Exception:
        pass
    return "ens192"


def current_network() -> dict:
    iface = primary_iface()
    info = {"interface": iface, "mode": "dhcp", "ip_cidr": "", "gateway": "", "dns": []}
    try:
        out = subprocess.run(["ip", "-4", "-o", "addr", "show", "dev", iface],
                             capture_output=True, text=True, timeout=4).stdout
        for line in out.splitlines():
            parts = line.split()
            for i, p in enumerate(parts):
                if p == "inet" and i + 1 < len(parts):
                    info["ip_cidr"] = parts[i + 1]
                    break
    except Exception:
        pass
    try:
        out = subprocess.run(["ip", "-4", "route", "show", "default"],
                             capture_output=True, text=True, timeout=4).stdout
        for line in out.splitlines():
            parts = line.split()
            if "via" in parts:
                info["gateway"] = parts[parts.index("via") + 1]
            if "proto" in parts:
                info["mode"] = parts[parts.index("proto") + 1]
    except Exception:
        pass
    try:
        with open("/etc/resolv.conf") as f:
            info["dns"] = [l.split()[1] for l in f if l.startswith("nameserver")][:4]
    except Exception:
        pass
    return info


def render_qr_png_b64(data: str) -> str:
    """Génère un QR PNG via qrencode et le renvoie en base64 (pour <img src='data:image/png;base64,...'/>)."""
    try:
        r = subprocess.run(
            ["qrencode", "-t", "PNG", "-s", "5", "-m", "2", "-o", "-", data],
            capture_output=True, timeout=5, check=True,
        )
        return base64.b64encode(r.stdout).decode("ascii")
    except Exception:
        return ""


def detect_network_system() -> str:
    """Renvoie 'netplan' (Ubuntu) ou 'ifupdown' (Debian) selon ce qui est installé/utilisé."""
    if Path("/etc/netplan").is_dir() and subprocess.run(["which", "netplan"],
                                                        capture_output=True).returncode == 0:
        return "netplan"
    return "ifupdown"


def apply_network_static(iface: str, ip_cidr: str, gateway: str, dns: list[str]) -> tuple[bool, str]:
    if not ip_cidr or not gateway:
        return False, "IP et passerelle requises."
    if "/" not in ip_cidr:
        return False, "Format IP attendu : 192.168.1.222/24 (avec CIDR)."
    dns_clean = [d for d in dns if d] or ["8.8.8.8", "1.1.1.1"]

    sys_type = detect_network_system()

    if sys_type == "netplan":
        yaml = f"""# UniSOC honeypot — config statique appliquée depuis :8080
network:
  version: 2
  ethernets:
    {iface}:
      dhcp4: no
      addresses:
        - {ip_cidr}
      routes:
        - to: default
          via: {gateway}
      nameservers:
        addresses: [{', '.join(dns_clean)}]
"""
        try:
            target = Path("/etc/netplan/99-unisoc-honeypot.yaml")
            target.write_text(yaml)
            os.chmod(target, 0o600)
            for f in Path("/etc/netplan").glob("*.yaml"):
                if f.name != target.name:
                    f.rename(f.with_suffix(".yaml.unisoc-bak"))
            subprocess.run(["netplan", "generate"], check=True, timeout=15)
            subprocess.Popen(["bash", "-c", "sleep 2 && netplan apply"])
            return True, f"[netplan] Config posée. IP {ip_cidr} dans ~5 s."
        except Exception as e:
            return False, f"netplan échec : {e}"

    # ─── ifupdown (Debian) ──────────────────────────────────────────────
    try:
        # Backup initial /etc/network/interfaces (1 seule fois)
        interfaces_path = Path("/etc/network/interfaces")
        backup_path = Path("/etc/network/interfaces.unisoc-bak")
        if not backup_path.exists() and interfaces_path.exists():
            backup_path.write_text(interfaces_path.read_text())

        # Réécrit /etc/network/interfaces en minimal + source des .d
        interfaces_path.write_text("""# Géré par UniSOC honeypot — voir /etc/network/interfaces.d/
source /etc/network/interfaces.d/*

auto lo
iface lo inet loopback
""")

        # Écrit la config statique dans le drop-in
        dropin = Path(f"/etc/network/interfaces.d/99-unisoc-honeypot")
        cfg = f"""# UniSOC honeypot — config statique appliquée depuis :8080
allow-hotplug {iface}
iface {iface} inet static
    address {ip_cidr}
    gateway {gateway}
    dns-nameservers {' '.join(dns_clean)}
"""
        dropin.write_text(cfg)
        os.chmod(dropin, 0o644)

        # Apply en background pour ne pas couper la réponse HTTP
        subprocess.Popen([
            "bash", "-c",
            f"sleep 2 && ifdown {iface} 2>/dev/null ; ifup {iface}"
        ])
        return True, f"[ifupdown] Config posée. IP {ip_cidr} dans ~5 s — rechargez sur la nouvelle IP."
    except Exception as e:
        return False, f"ifupdown échec : {e}"


def apply_network_dhcp(iface: str) -> tuple[bool, str]:
    sys_type = detect_network_system()

    if sys_type == "netplan":
        try:
            f_unisoc = Path("/etc/netplan/99-unisoc-honeypot.yaml")
            if f_unisoc.exists():
                f_unisoc.unlink()
            for f in Path("/etc/netplan").glob("*.yaml.unisoc-bak"):
                f.rename(f.with_suffix(""))
            if not list(Path("/etc/netplan").glob("*.yaml")):
                Path("/etc/netplan/01-dhcp.yaml").write_text(
                    f"network:\n  version: 2\n  ethernets:\n    {iface}:\n      dhcp4: true\n"
                )
                os.chmod("/etc/netplan/01-dhcp.yaml", 0o600)
            subprocess.run(["netplan", "generate"], check=True, timeout=15)
            subprocess.Popen(["bash", "-c", "sleep 2 && netplan apply"])
            return True, "[netplan] DHCP restauré."
        except Exception as e:
            return False, str(e)

    # ─── ifupdown ───────────────────────────────────────────────────────
    try:
        dropin = Path(f"/etc/network/interfaces.d/99-unisoc-honeypot")
        if dropin.exists():
            dropin.unlink()
        # Restore le backup s'il existe, sinon génère un default DHCP
        backup_path = Path("/etc/network/interfaces.unisoc-bak")
        interfaces_path = Path("/etc/network/interfaces")
        if backup_path.exists():
            interfaces_path.write_text(backup_path.read_text())
        else:
            interfaces_path.write_text(f"""source /etc/network/interfaces.d/*

auto lo
iface lo inet loopback

allow-hotplug {iface}
iface {iface} inet dhcp
""")
        subprocess.Popen([
            "bash", "-c",
            f"sleep 2 && ifdown {iface} 2>/dev/null ; ifup {iface}"
        ])
        return True, "[ifupdown] DHCP restauré. Re-bail dans ~5 s."
    except Exception as e:
        return False, str(e)


# ──────────────────────────────────────────────────────────────────────────────
# HTML — design aligné portail UniSOC (light, slate, primary orange)
# ──────────────────────────────────────────────────────────────────────────────

HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="fr"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>SYLink HoneyBot — Activation</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
:root {{
  --bg: #f8fafc;
  --bg-card: #ffffff;
  --border: #e2e8f0;
  --border-strong: #cbd5e1;
  --fg: #0f172a;
  --fg-mute: #64748b;
  --primary: #ea580c;
  --primary-h: #c2410c;
  --primary-soft: #fff7ed;
  --success: #16a34a;
  --success-soft: #f0fdf4;
  --warn: #d97706;
  --danger: #dc2626;
  --danger-soft: #fef2f2;
}}
html,body {{ background: var(--bg); color: var(--fg); font-family: -apple-system, BlinkMacSystemFont, system-ui, "Segoe UI", Roboto, sans-serif; min-height: 100%; line-height: 1.5; }}
body {{ padding: 32px 40px; }}
.container {{ max-width: 1440px; margin: 0 auto; }}

/* Header */
header {{ display: flex; align-items: center; gap: 14px; margin-bottom: 32px; padding-bottom: 18px; border-bottom: 1px solid var(--border); }}
.logo-mark {{ width: 44px; height: 44px; border-radius: 10px; background: linear-gradient(135deg, var(--primary), #9a3412); display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:18px;letter-spacing:-0.5px; }}
h1 {{ font-size: 22px; font-weight: 700; color: var(--fg); }}
.sub {{ color: var(--fg-mute); font-size: 13px; margin-top: 2px; }}

/* Banner top */
.banner {{ display: flex; gap: 16px; padding: 18px 20px; border-radius: 12px; margin-bottom: 24px; border: 1px solid var(--border); align-items: center; }}
.banner.activated {{ background: var(--success-soft); border-color: rgba(22,163,74,0.3); }}
.banner.pending {{ background: var(--primary-soft); border-color: rgba(234,88,12,0.3); }}
.banner-icon {{ width: 44px; height: 44px; border-radius: 50%; display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:700;flex-shrink:0; }}
.banner.activated .banner-icon {{ background: var(--success); color: #fff; }}
.banner.pending .banner-icon {{ background: var(--primary); color: #fff; }}
.banner h2 {{ font-size: 17px; font-weight: 700; color: var(--fg); }}
.banner p {{ color: var(--fg-mute); font-size: 14px; margin-top: 2px; }}

/* Grid 2 cols */
.grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; }}
@media (max-width: 720px) {{ .grid {{ grid-template-columns: 1fr; }} }}

/* Card */
.card {{ background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 22px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }}
.card h3 {{ font-size: 12px; font-weight: 700; color: var(--primary); margin-bottom: 14px; text-transform: uppercase; letter-spacing: 0.6px; }}

/* Big primary action button */
.btn {{ padding: 11px 18px; background: var(--primary); color: #fff; border: 0; border-radius: 8px; font-weight: 600; cursor: pointer; font-size: 14px; text-decoration: none; display: inline-block; transition: background 0.15s; }}
.btn:hover {{ background: var(--primary-h); }}
.btn.secondary {{ background: var(--bg); border: 1px solid var(--border-strong); color: var(--fg); }}
.btn.secondary:hover {{ background: #f1f5f9; }}
.btn.big {{ display: block; width: 100%; padding: 14px; font-size: 15px; text-align: center; }}

/* QR */
.qr-block {{ display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 8px 0 16px; }}
.qr-block img {{ background: #fff; padding: 10px; border-radius: 10px; border: 1px solid var(--border); max-width: 220px; }}
.qr-block .qr-caption {{ font-size: 12px; color: var(--fg-mute); text-align: center; }}

/* Code */
.code {{ font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 24px; font-weight: 800; color: var(--primary); letter-spacing: 6px; padding: 14px 22px; background: var(--primary-soft); border: 1px solid rgba(234,88,12,0.25); border-radius: 8px; display: inline-block; text-align: center; }}

/* URL row + copy */
.url-row {{ display: flex; gap: 8px; align-items: stretch; margin-top: 8px; }}
.url-box {{ flex: 1; padding: 12px 14px; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; font-family: ui-monospace, monospace; font-size: 11px; word-break: break-all; color: var(--fg); text-decoration: none; }}
.url-box:hover {{ border-color: var(--primary); color: var(--primary); }}
.btn.copy {{ flex: 0 0 auto; }}

/* Form */
.field {{ margin-bottom: 12px; }}
.field label {{ display: block; font-size: 12px; font-weight: 600; color: var(--fg-mute); margin-bottom: 4px; }}
input[type=text] {{ width: 100%; padding: 10px 12px; background: var(--bg-card); border: 1px solid var(--border-strong); color: var(--fg); border-radius: 8px; font-family: ui-monospace, monospace; font-size: 13px; }}
input[type=text]:focus {{ outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(234,88,12,0.12); }}

/* Mode tabs */
.mode-tabs {{ display: flex; gap: 8px; margin-bottom: 14px; }}
.mode-tab {{ flex: 1; padding: 10px 14px; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; color: var(--fg-mute); cursor: pointer; text-align: center; font-size: 13px; font-weight: 600; transition: all 0.15s; }}
.mode-tab.active {{ background: var(--primary); color: #fff; border-color: var(--primary); }}
.mode-tab:not(.active):hover {{ border-color: var(--border-strong); }}

/* Messages */
.msg {{ padding: 11px 14px; border-radius: 8px; margin-top: 14px; font-size: 13px; }}
.msg.ok {{ background: var(--success-soft); border: 1px solid rgba(22,163,74,0.3); color: #166534; }}
.msg.err {{ background: var(--danger-soft); border: 1px solid rgba(220,38,38,0.3); color: #991b1b; }}

.net-status {{ background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; margin-bottom: 14px; font-size: 13px; color: var(--fg-mute); }}
.net-status strong {{ color: var(--fg); font-family: ui-monospace, monospace; }}

.hint {{ font-size: 11px; color: var(--warn); margin-top: 8px; }}

/* Specs */
dl > div {{ display: flex; justify-content: space-between; padding: 9px 0; border-bottom: 1px solid var(--border); font-size: 13px; }}
dl > div:last-child {{ border-bottom: none; }}
dt {{ color: var(--fg-mute); font-weight: 500; }}
dd {{ font-family: ui-monospace, monospace; color: var(--fg); }}

.footer {{ margin-top: 28px; padding-top: 18px; text-align: center; color: var(--fg-mute); font-size: 12px; border-top: 1px solid var(--border); }}
.footer a {{ color: var(--primary); text-decoration: none; }}
</style></head>
<body>
<div class="container">

<header>
  <div class="logo-mark">SY</div>
  <div>
    <h1>SYLink HoneyBot — Activation</h1>
    <div class="sub">VM honeypot interne · 14 services leurres · piloté par UniSOC AI</div>
  </div>
</header>

{status_banner}

{activation_section}

{network_card}

<div class="card" style="margin-top:16px;">
  <h3>Spécifications</h3>
  <dl>
    <div><dt>Hostname</dt><dd>{hostname}</dd></div>
    <div><dt>Fingerprint VM</dt><dd>{fp_short}…</dd></div>
    <div><dt>API</dt><dd>{api_base}</dd></div>
    <div><dt>Portail SOC</dt><dd><a href="{portal_base}" target="_blank" style="color:var(--primary);text-decoration:none;">{portal_base}</a></dd></div>
  </dl>
</div>

<div class="footer">
  Documentation : <a href="{portal_base}/docs/honeybot-setup" target="_blank">guide d'installation</a>
  &nbsp;·&nbsp; Support : <a href="mailto:contact@unisoc.fr">contact@unisoc.fr</a>
</div>

</div>

<script>
function copyUrl(){{
  const u = document.getElementById('activation-url');
  if (!u) return;
  navigator.clipboard.writeText(u.dataset.url).then(()=>{{
    const b = document.getElementById('copy-btn');
    const old = b.textContent;
    b.textContent = '✓ Copié';
    b.style.background = '#16a34a';
    setTimeout(()=> {{ b.textContent = old; b.style.background = ''; }}, 1800);
  }});
}}
function setMode(m){{
  document.getElementById('mode').value = m;
  document.getElementById('tab-dhcp').classList.toggle('active', m==='dhcp');
  document.getElementById('tab-static').classList.toggle('active', m==='static');
  document.getElementById('static-fields').style.display = (m==='static') ? 'block' : 'none';
}}
</script>
</body></html>"""


def render_status_banner(state: dict) -> str:
    if state.get("activated"):
        return f"""<div class="banner activated">
<div class="banner-icon">✓</div>
<div><h2>HoneyBot activé</h2>
<p>Tenant <strong>{state.get('tenant_id', '?')}</strong> · Honeypot ID <strong>{state.get('honeypot_id', '?')}</strong>
&nbsp;·&nbsp; Cette page va se fermer automatiquement (la VM doit rester discrète).</p>
</div></div>"""
    return """<div class="banner pending">
<div class="banner-icon">⏳</div>
<div><h2>En attente d'activation</h2>
<p>Activez cette VM dans votre espace client UniSOC pour qu'elle bascule en mode honeypot actif.</p>
</div></div>"""


def render_activation_section(state: dict) -> str:
    if state.get("activated"):
        return ""
    url = state.get("qr_url", "#")
    code = state.get("short_code", "—")
    qr_b64 = render_qr_png_b64(url)
    qr_img = (f'<img src="data:image/png;base64,{qr_b64}" alt="QR code activation"/>'
              if qr_b64 else '<div style="color:var(--fg-mute);font-size:12px;">QR indisponible — utilisez le lien ci-contre</div>')

    return f"""<div class="grid">

<div class="card">
  <h3>🔗 Activation en 1 clic</h3>
  <a class="btn big" href="{url}" target="_blank">▶ Ouvrir la page d'activation UniSOC</a>

  <div class="field" style="margin-top:18px;">
    <label>Lien complet (cliquable ou à copier)</label>
    <div class="url-row">
      <a id="activation-url" class="url-box" href="{url}" target="_blank" data-url="{url}">{url}</a>
      <button id="copy-btn" class="btn copy" type="button" onclick="copyUrl()">Copier</button>
    </div>
  </div>

  <div class="field">
    <label>Ou code court à saisir sur {state.get('portal_base','https://client.unisoc.fr')}/honeypot/activate</label>
    <div class="code">{code}</div>
  </div>
</div>

<div class="card">
  <h3>📱 Activation par QR Code</h3>
  <div class="qr-block">
    {qr_img}
    <div class="qr-caption">Scannez avec un smartphone connecté à Internet,<br>
    vous serez redirigé vers la page d'activation UniSOC.</div>
  </div>
</div>

</div>"""


def render_network_card(state: dict) -> str:
    if state.get("activated"):
        return ""
    net = current_network()
    msg = state.get("_network_msg", "")
    msg_html = ""
    if msg:
        kind = "ok" if state.get("_network_ok") else "err"
        msg_html = f'<div class="msg {kind}">{msg}</div>'
    return f"""<div class="card">
<h3>🌐 Configuration réseau (avant activation)</h3>

<div class="net-status">
  Actuellement&nbsp;:
  <strong>{net.get('mode','?')}</strong> &nbsp;·&nbsp;
  IP <strong>{net.get('ip_cidr','—')}</strong> &nbsp;·&nbsp;
  GW <strong>{net.get('gateway','—')}</strong> &nbsp;·&nbsp;
  iface <strong>{net.get('interface','—')}</strong>
</div>

<form method="POST" action="/apply-network">
  <input type="hidden" id="mode" name="mode" value="dhcp">
  <div class="mode-tabs">
    <div id="tab-dhcp" class="mode-tab active" onclick="setMode('dhcp')">Garder DHCP</div>
    <div id="tab-static" class="mode-tab" onclick="setMode('static')">Fixer IP statique</div>
  </div>

  <div id="static-fields" style="display:none;">
    <div class="field">
      <label>Adresse IP / CIDR</label>
      <input type="text" name="ip_cidr" value="{net.get('ip_cidr','')}" placeholder="192.168.1.222/24">
    </div>
    <div class="field">
      <label>Passerelle</label>
      <input type="text" name="gateway" value="{net.get('gateway','')}" placeholder="192.168.1.1">
    </div>
    <div class="field">
      <label>DNS (séparés par virgule)</label>
      <input type="text" name="dns" value="{', '.join(net.get('dns',[]) or ['8.8.8.8','1.1.1.1'])}" placeholder="8.8.8.8, 1.1.1.1">
    </div>
    <input type="hidden" name="interface" value="{net.get('interface','')}">

    <p class="hint">⚠ Si la nouvelle IP devient injoignable, reconnectez-vous depuis la console VMware pour corriger.</p>
  </div>

  <button class="btn big" type="submit">Appliquer la configuration réseau</button>
</form>
{msg_html}
</div>"""


def render_page(extra_state: dict | None = None) -> str:
    state = load_state()
    if extra_state:
        state.update(extra_state)
    return HTML_TEMPLATE.format(
        hostname=os.uname().nodename,
        fp_short=(state.get("machine_fingerprint") or "—")[:24],
        api_base=state.get("hp_api_base", "https://api.unisoc.fr"),
        portal_base=state.get("portal_base", "https://client.unisoc.fr"),
        status_banner=render_status_banner(state),
        activation_section=render_activation_section(state),
        network_card=render_network_card(state),
    )


# ──────────────────────────────────────────────────────────────────────────────
# Auto-shutdown : quand license.json apparaît, re-run init.py pour basculer
# /etc/issue en banner COUVERTURE Windows BackupServer, puis stop ce service.
# ──────────────────────────────────────────────────────────────────────────────

def auto_shutdown_watcher():
    license_file = CONFIG_DIR / "license.json"
    while True:
        time.sleep(15)
        if license_file.exists():
            try:
                lic = json.loads(license_file.read_text())
                if lic.get("license_token") or lic.get("api_key"):
                    print("[setup-ui] license.json détecté — bascule banner couverture + auto-stop dans 20s")
                    time.sleep(20)
                    try:
                        subprocess.run(["/opt/sylink-honeypot/bin/init.py"], timeout=15)
                    except Exception as e:
                        print(f"[setup-ui] init.py rerun échec : {e}")
                    subprocess.run(["systemctl", "disable", "--now", "sylink-honeypot-setup-ui.service"],
                                   timeout=20)
                    return
            except Exception as e:
                print(f"[setup-ui] erreur watch license : {e}")


# ──────────────────────────────────────────────────────────────────────────────
# HTTP handlers
# ──────────────────────────────────────────────────────────────────────────────

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if urlparse(self.path).path != "/":
            self.send_response(404); self.end_headers(); return
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.end_headers()
        self.wfile.write(render_page().encode())

    def do_POST(self):
        if urlparse(self.path).path != "/apply-network":
            self.send_response(404); self.end_headers(); return
        try:
            n = int(self.headers.get("Content-Length", 0))
            body = self.rfile.read(n).decode("utf-8", errors="replace")
            form = {k: v[0] for k, v in parse_qs(body).items()}
            mode = form.get("mode", "dhcp")
            iface = form.get("interface") or primary_iface()
            if mode == "static":
                dns = [d.strip() for d in (form.get("dns") or "").split(",") if d.strip()]
                ok, msg = apply_network_static(iface, form.get("ip_cidr", "").strip(),
                                                form.get("gateway", "").strip(), dns)
            else:
                ok, msg = apply_network_dhcp(iface)
            page = render_page({"_network_ok": ok, "_network_msg": msg})
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.end_headers()
            self.wfile.write(page.encode())
        except Exception as e:
            self.send_response(500); self.end_headers()
            self.wfile.write(f"erreur : {e}".encode())

    def log_message(self, format, *args):
        pass


def main():
    threading.Thread(target=auto_shutdown_watcher, daemon=True).start()
    server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
    print(f"sylink-honeypot-setup-ui listening on 0.0.0.0:{PORT}")
    server.serve_forever()


if __name__ == "__main__":
    main()
