#!/usr/bin/env bash
set -euo pipefail

if [ "$(id -u)" -ne 0 ]; then
  echo "Start deze installer met sudo/root."
  exit 1
fi

DEFAULT_PORTAL_URL="${REMOTE_PLATFORM_URL:-https://remote.no-li.nl}"
INSTALL_VERSION="0.9.0"

cat >/tmp/rp_installer.py <<'END_RP_INSTALLER'
import json, os, platform, getpass, subprocess, urllib.request, urllib.error, time, ipaddress, shutil
from pathlib import Path

PORTAL_URL = os.environ.get("REMOTE_PLATFORM_URL", "https://remote.no-li.nl").rstrip("/")
INSTALL_VERSION = "0.9.0"
AGENT_BIN = Path("/usr/local/bin/gateway-agent")
CONFIG_DIR = Path("/etc/remote-platform/agent")
CONFIG_FILE = CONFIG_DIR / "config.json"
SERVICE_FILE = Path("/etc/systemd/system/remote-platform-agent.service")
OLD_SERVICE_FILE = Path("/etc/systemd/system/gateway-agent.service")
WG_CLIENT = Path("/etc/wireguard/rpwg-client.conf")

# NIEUW: niet-interactieve herstel-modus, bedoeld om vanuit de portal/agent
# zelf getriggerd te worden (geen mens die op een prompt zit te wachten).
# ask() geeft dan meteen de default terug in plaats van op invoer te wachten --
# de rest van het script hoeft daardoor NIET herschreven te worden, elke
# bestaande prompt "doet gewoon het voor herstel voor de hand liggende ding".
NONINTERACTIVE = os.environ.get("RP_NONINTERACTIVE", "") == "1"

def ask(prompt, default=None, secret=False):
    if NONINTERACTIVE:
        return default or ""
    suffix = f" [{default}]" if default not in (None, "") else ""
    v = getpass.getpass(f"{prompt}{suffix}: ") if secret else input(f"{prompt}{suffix}: ")
    v = v.strip()
    return v if v else (default or "")

def req(method, url, token=None, data=None):
    body = json.dumps(data).encode("utf-8") if data is not None else None
    r = urllib.request.Request(url, data=body, method=method)
    if token:
        r.add_header("Authorization", "Bearer " + token)
    if data is not None:
        r.add_header("Content-Type", "application/json")
    with urllib.request.urlopen(r) as x:
        payload = x.read()
        return json.loads(payload.decode("utf-8")) if payload else {}

def run(cmd, check=True):
    print("+", cmd)
    return subprocess.run(cmd, shell=True, check=check)

def output(cmd):
    try:
        return subprocess.check_output(cmd, shell=True, text=True, stderr=subprocess.DEVNULL)
    except Exception:
        return ""

def service_exists(name):
    return subprocess.run(f"systemctl list-unit-files {name} --no-legend 2>/dev/null | grep -q '^{name}'", shell=True).returncode == 0

def stop_services():
    for svc in ["remote-platform-agent.service", "gateway-agent.service", "wg-quick@rpwg-client.service"]:
        run(f"systemctl stop {svc} 2>/dev/null || true", check=False)

def disable_services():
    for svc in ["remote-platform-agent.service", "gateway-agent.service", "wg-quick@rpwg-client.service"]:
        run(f"systemctl disable {svc} 2>/dev/null || true", check=False)

def load_existing_config():
    if not CONFIG_FILE.exists():
        return {}
    try:
        return json.loads(CONFIG_FILE.read_text())
    except Exception:
        return {}

def backup_existing_config():
    if CONFIG_FILE.exists():
        backup = CONFIG_FILE.with_suffix(".json.backup-" + time.strftime("%Y%m%d-%H%M%S"))
        shutil.copy2(CONFIG_FILE, backup)
        print("Bestaande config backup:", backup)

def find_existing_gateway_id(cfg, token):
    gid = cfg.get("gateway_id") or cfg.get("gatewayId") or ""
    if gid:
        return gid
    serial = cfg.get("gateway_serial") or cfg.get("serial")
    if serial:
        try:
            gateways = req("GET", PORTAL_URL + "/gateways", token=token)
            for g in gateways:
                if g.get("serial") == serial:
                    return g.get("id") or ""
        except Exception as e:
            print("Kon gateway niet op serial zoeken:", e)
    return ""

def existing_markers():
    markers = []
    if AGENT_BIN.exists(): markers.append(str(AGENT_BIN))
    if CONFIG_FILE.exists(): markers.append(str(CONFIG_FILE))
    if SERVICE_FILE.exists() or service_exists("remote-platform-agent.service"): markers.append("remote-platform-agent.service")
    if OLD_SERVICE_FILE.exists() or service_exists("gateway-agent.service"): markers.append("gateway-agent.service")
    if WG_CLIENT.exists(): markers.append(str(WG_CLIENT))
    return markers

def remove_local_files(remove_config_dir=True):
    stop_services()
    disable_services()
    backup_existing_config()

    for f in [SERVICE_FILE, OLD_SERVICE_FILE]:
        if f.exists():
            f.unlink()

    if AGENT_BIN.exists():
        AGENT_BIN.unlink()

    if WG_CLIENT.exists():
        WG_CLIENT.unlink()

    if remove_config_dir and CONFIG_DIR.exists():
        shutil.rmtree(CONFIG_DIR, ignore_errors=True)

    run("systemctl daemon-reload", check=False)

def choose_existing_action(token):
    markers = existing_markers()
    cfg = load_existing_config()
    if not markers:
        if NONINTERACTIVE:
            raise SystemExit("Niet-interactieve herstel-modus vereist een bestaande installatie -- die is hier niet gevonden.")
        return "new", cfg
    print("\nBestaande installatie gevonden:")
    for m in markers:
        print(" -", m)
    print("\nKies actie:")
    print("1. Herstellen/updaten bestaande installatie")
    print("2. Opnieuw installeren")
    print("3. Volledig verwijderen (ook uit portal indien gevonden)")
    print("4. Afbreken")
    choice = ask("Keuze", "1")
    if choice == "1":
        stop_services()
        if AGENT_BIN.exists(): AGENT_BIN.unlink()
        return "repair", cfg
    if choice == "2":
        remove_local_files(remove_config_dir=False)
        return "reinstall", cfg
    if choice == "3":
        gid = find_existing_gateway_id(cfg, token)
        if gid:
            confirm = ask(f"Type VERWIJDER om gateway {gid} ook uit portal te verwijderen", "")
            if confirm == "VERWIJDER":
                req("DELETE", PORTAL_URL + f"/gateways/{gid}", token=token)
                print("Gateway uit portal verwijderd:", gid)
        else:
            print("Geen bestaande gateway_id gevonden; portal verwijderen overgeslagen.")
        remove_local_files()
        raise SystemExit("Volledig verwijderd.")
    raise SystemExit("Afgebroken.")

def detect_subnets(include_advanced=False):
    # Geeft nu (netwerk, interface)-paren terug in plaats van alleen het
    # netwerk -- nodig om straks automatisch de juiste lan_interface voor
    # NAT/masquerade te kunnen meesturen, in plaats van dat handmatig achteraf
    # te moeten repareren (zoals bij TvKantoor en S010HMI gebeurde).
    out = output("ip -4 route show scope link")
    found = []
    for line in out.splitlines():
        parts = line.split()
        if not parts: continue
        net = parts[0]
        dev = ""
        if "dev" in parts:
            try: dev = parts[parts.index("dev")+1]
            except Exception: dev = ""
        if net == "default" or net.startswith("169.254.") or "/" not in net:
            continue
        if not include_advanced:
            if dev.startswith(("docker", "br-", "veth", "wg", "rpwg")): continue
            if net.startswith(("172.17.", "172.18.", "172.19.", "10.99.")): continue
        try:
            n = ipaddress.ip_network(net, strict=False)
        except Exception:
            continue
        if n.is_loopback or n.is_link_local: continue
        s = str(n)
        if s not in [f[0] for f in found]:
            found.append((s, dev))
    return found


def choose_reuse_gateway(old_cfg):
    gid = old_cfg.get("gateway_id") or old_cfg.get("gatewayId") or ""
    if not gid:
        return ""
    print("\nBestaande gateway gevonden in config:")
    print(gid)
    print("\nKies actie:")
    print("1. Deze bestaande gateway hergebruiken")
    print("2. Nieuwe gateway aanmaken")
    c = ask("Keuze", "1")
    if c == "1":
        return gid
    return ""

def fmt_net(net):
    """Toont een netwerk als 'adres masker 255.x.x.x' i.p.v. CIDR-notatie."""
    try:
        n = ipaddress.ip_network(str(net), strict=False)
        return f"{n.network_address} masker {n.netmask}"
    except Exception:
        return str(net)

def mask_to_prefix(v):
    """Accepteert een subnetmasker in 255.x.x.x-notatie (of een kaal getal) en
    geeft de prefixlengte terug. '255.255.255.0' -> 24, '255.255.0.0' -> 16."""
    v = str(v).strip()
    if not v:
        return None
    if "." in v:
        try:
            return ipaddress.IPv4Network(f"0.0.0.0/{v}").prefixlen
        except Exception:
            return None
    try:
        n = int(v)
        return n if 0 <= n <= 32 else None
    except Exception:
        return None

def prefix_to_mask(p):
    """Prefixlengte -> masker in 255.x.x.x-notatie. 24 -> '255.255.255.0'."""
    try:
        return str(ipaddress.IPv4Network(f"0.0.0.0/{int(p)}").netmask)
    except Exception:
        return "255.255.255.0"

def _network_obj(net):
    return ipaddress.ip_network(net, strict=False)

def ip_in_use(ip, iface):
    """Best-effort ARP/ping-check of een adres al in gebruik is op het
    machinenetwerk. Gebruikt eerst 'arping' (betrouwbaarst op L2), valt
    terug op een korte ping. Geen uitsluitsel = we melden 'vrij' maar
    waarschuwen niet hard, zodat de installatie niet vastloopt."""
    if not iface:
        return False
    # arping -c1 -w1 : 1 poging, 1s wachten
    r = subprocess.run(f"arping -c1 -w1 -I {iface} {ip}", shell=True,
                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    if r.returncode == 0:
        return True  # kreeg antwoord -> in gebruik
    # ping-fallback (kan ook 'in gebruik' aantonen als arping ontbreekt)
    r = subprocess.run(f"ping -c1 -W1 {ip}", shell=True,
                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    return r.returncode == 0

def propose_gateway_ip(net, iface):
    """Stelt een hoog, waarschijnlijk vrij adres voor in het netwerk
    (begint bij .240 en telt af), en controleert met ARP of het vrij is.
    Geeft een voorstel-string terug (leeg als niks vrij lijkt)."""
    try:
        n = _network_obj(net)
    except Exception:
        return ""
    hosts = list(n.hosts())
    if not hosts:
        return ""
    # kandidaten: bij voorkeur hoog in het bereik (.240..)
    candidates = [h for h in hosts if int(str(h).split(".")[-1]) >= 240] or hosts[-16:]
    for cand in reversed(candidates):
        if not ip_in_use(str(cand), iface):
            return str(cand)
    return str(candidates[-1]) if candidates else ""

def ask_gateway_ip(net, iface):
    """Vraagt per netwerk een gateway-IP. Leeg = alleen L2 (geen L3-routing
    voor dit netwerk). Voorstel is een ARP-gecheckt hoog adres."""
    mask = str(_network_obj(net).netmask)
    proposal = propose_gateway_ip(net, iface)
    print(f"\n  Netwerk {fmt_net(net)}")
    print("  Gateway-IP in dit netwerk = het adres dat DEZE gateway op de")
    print("  machinezijde krijgt, nodig om online te kunnen gaan (L3/S7comm).")
    print("  Leeg laten = alleen apparaten zoeken (L2/DCP), geen L3-routing.")
    gip = ask("  Gateway-IP (leeg = alleen L2)", proposal)
    gip = gip.strip()
    if not gip:
        return ""
    # validatie: moet in het netwerk vallen
    try:
        if ipaddress.ip_address(gip) not in _network_obj(net):
            print(f"  LET OP: {gip} valt niet in {net} -- toch opgeslagen zoals ingevoerd.")
    except Exception:
        print(f"  Ongeldig IP: {gip} -- overgeslagen (alleen L2 voor dit netwerk).")
        return ""
    if iface and ip_in_use(gip, iface):
        keep = ask(f"  WAARSCHUWING: {gip} lijkt al in gebruik. Toch gebruiken? j/n", "n")
        if not keep.lower().startswith("j"):
            return ask_gateway_ip(net, iface)
    return gip

def list_local_interfaces():
    """Alle bruikbare fysieke interfaces (geen lo/docker/veth/wg/rpwg)."""
    out = output("ip -o link show")
    ifaces = []
    for line in out.splitlines():
        # formaat: "2: eth0: <...>"
        parts = line.split(":")
        if len(parts) < 2:
            continue
        name = parts[1].strip().split("@")[0]
        if not name or name == "lo":
            continue
        if name.startswith(("docker","br-","veth","wg","rpwg","gtap")):
            continue
        ifaces.append(name)
    return ifaces

def iface_has_ipv4(iface):
    return bool(output(f"ip -4 -o addr show dev {iface}").strip())

FW_PROFILES = [
    ("smb",    "Verkenner / bestandsdeling (SMB)"),
    ("rdp",    "Extern bureaublad (RDP)"),
    ("vnc",    "VNC"),
    ("s7comm", "PLC / S7comm (poort 102)"),
    ("web",    "Web (80/443)"),
    ("dns",    "DNS"),
    ("ntp",    "Tijd (NTP)"),
    ("ping",   "Ping"),
]

def configure_dhcp(iface_cfg):
    """Optionele DHCP-server op de machinekant.

    Alleen mogelijk als de machine-interface een ANDERE is dan de uplink --
    anders zou de DHCP-server ook op het klantnetwerk antwoorden en daar de
    DHCP-server van de klant verstoren."""
    machine_ifaces = [c for c in iface_cfg if c["role"] == "machine"]
    both_ifaces    = [c for c in iface_cfg if c["role"] == "both"]

    if not machine_ifaces:
        if both_ifaces:
            print("\n=== DHCP-server ===")
            print("Niet beschikbaar: uplink en machinekant zitten op dezelfde")
            print("netwerkkaart. Een DHCP-server zou dan ook op het kantoornetwerk")
            print("antwoorden. Gebruik gescheiden kaarten om DHCP te kunnen gebruiken.")
        return None

    mi = machine_ifaces[0]
    print("\n=== DHCP-server (optioneel) ===")
    print(f"Op de machinekant ({mi['iface']}) kan de gateway zelf adressen uitdelen.")
    print("Handig voor een monteurslaptop of tijdelijk apparaat in het machinenetwerk.")
    if NONINTERACTIVE:
        return None
    if not ask("DHCP-server aanzetten? j/n", "n").lower().startswith("j"):
        return None

    base = mi.get("ip_addr") or ""
    suggest_start, suggest_end = "", ""
    if base:
        try:
            parts = base.split(".")
            suggest_start = ".".join(parts[:3] + ["100"])
            suggest_end   = ".".join(parts[:3] + ["150"])
        except Exception:
            pass

    rs = ask("  Eerste adres van het uitdeelbereik", suggest_start).strip()
    re_ = ask("  Laatste adres van het uitdeelbereik", suggest_end).strip()
    nm = ask("  Subnetmasker", prefix_to_mask(mi.get("prefix_len") or 24)).strip()
    lh = ask("  Leaseduur in uren", "12").strip()
    try:
        lh = int(lh)
    except Exception:
        lh = 12

    pg = ask("  Default gateway meegeven (clients kunnen dan naar buiten)? j/n", "n").lower().startswith("j")
    pd = ask("  DNS meegeven? j/n", "n").lower().startswith("j")
    dns = ""
    if pd:
        dns = ask("  DNS-server(s), komma-gescheiden (leeg = de gateway zelf)", "").strip()
    dom = ask("  Domeinnaam (optioneel)", "").strip()

    return {"enabled":True,"iface":mi["iface"],"range_start":rs,"range_end":re_,
            "netmask":nm,"lease_hours":lh,"provide_gateway":pg,"provide_dns":pd,
            "dns_servers":dns or None,"domain_name":dom or None}

def configure_firewall():
    """Zet de harde firewall aan en vraagt eventuele uitzonderingen.

    BASIS (altijd): al het doorgaande verkeer tussen klantnetwerk en
    machinenetwerk is DICHT. Alleen de tunnel mag bij het machinenetwerk.
    De gateway zelf blijft normaal bereikbaar (SSH) en de agent mag naar de hub.
    """
    print("\n=== Firewall ===")
    print("Standaard staat ALLE verkeer tussen klantnetwerk en machinenetwerk DICHT.")
    print("Alleen via de beveiligde tunnel is de machinekant bereikbaar.")
    print("De gateway zelf blijft gewoon bereikbaar en kan bij de hub.")
    if NONINTERACTIVE:
        return {"firewall_enabled": True, "rules": []}

    on = ask("Firewall aanzetten (sterk aanbevolen)? j/n", "j").lower().startswith("j")
    if not on:
        print("  LET OP: firewall UIT -- doorgaand verkeer is dan niet beperkt.")
        return {"firewall_enabled": False, "rules": []}

    rules = []
    print("\nUitzonderingen toevoegen? Bijvoorbeeld: een PC in het machinenetwerk")
    print("die via Verkenner bij een bestandsserver op het kantoornetwerk moet.")
    while True:
        add = ask("Uitzondering toevoegen? j/n", "n")
        if not add.lower().startswith("j"):
            break
        print("\n  Richting:")
        print("   1. machinenetwerk -> kantoornetwerk  (bijv. PLC/PC naar bestandsserver)")
        print("   2. kantoornetwerk -> machinenetwerk  (bijv. vaste werkplek naar de machine)")
        print("   3. machinenetwerk -> internet        (bijv. tijdserver, updates)")
        d = ask("  Kies 1/2/3", "1").strip()
        direction = {"1":"machine_to_client","2":"client_to_machine","3":"machine_to_internet"}.get(d,"machine_to_client")

        src = ask("  Bron (IP, netwerk, bereik a-b, of 'alles')", "alles").strip() or "any"
        dst = ask("  Doel (IP, netwerk, bereik a-b, of 'alles')", "alles").strip() or "any"

        print("\n  Profielen (meerdere mogelijk, komma-gescheiden, leeg = geen):")
        for i,(k,label) in enumerate(FW_PROFILES,1):
            print(f"   {i}. {label}")
        sel = ask("  Kies nummer(s)", "").strip()
        profs = []
        for item in [x.strip() for x in sel.split(",") if x.strip()]:
            try:
                profs.append(FW_PROFILES[int(item)-1][0])
            except Exception:
                pass
        profiles = ",".join(profs)

        proto, ports = "any", "any"
        if not profiles:
            proto = ask("  Protocol: tcp / udp / icmp / alles", "tcp").strip().lower() or "any"
            if proto in ("alles","any"): proto = "any"
            ports = ask("  Poort(en): bijv. 445, 5900-5910, 80,443 of 'alles'", "alles").strip() or "any"
            if ports.lower() in ("alles","any"): ports = "any"

        desc = ask("  Omschrijving (optioneel)", "").strip() or None
        rules.append({"direction":direction,"src":src if src.lower() not in ("alles","any") else "any",
                      "dst":dst if dst.lower() not in ("alles","any") else "any",
                      "protocol":proto,"ports":ports,"profiles":profiles,
                      "description":desc,"enabled":True,"sort_order":100+len(rules)})
        print(f"  Toegevoegd: {direction}, profielen: {profiles or '-'}, poorten: {ports}")

    return {"firewall_enabled": True, "rules": rules}

def configure_interfaces(subnet_pairs):
    """Vraagt per RELEVANTE fysieke interface de rol (machine/uplink/both) en
    IP-methode (dhcp/static). Ondersteunt alle in/uit-combinaties.

    Relevant = de interface heeft een IPv4-adres, OF hoort bij een gekozen
    machinesubnet. Ongebruikte kaarten (bijv. een lege eth0 waar niks op zit)
    worden overgeslagen -- daar hoef je niks over te beslissen.

    Slimme defaults: interface bij een gekozen machinesubnet -> 'machine';
    de interface met de default route -> 'uplink'; is er maar één relevante
    interface -> 'both'. static zonder IP wordt nooit verstuurd (dat gaf eerder
    een 422 die de hele config liet mislukken): geen IP => terugval naar dhcp
    of overslaan."""
    all_ifaces = list_local_interfaces()
    if not all_ifaces:
        return []

    # interface van de default route = uplink-kandidaat
    default_dev = ""
    for line in output("ip -4 route show default").splitlines():
        toks = line.split()
        if "dev" in toks:
            default_dev = toks[toks.index("dev")+1]
            break

    # machine-interfaces = die bij de gekozen subnetten horen (met bekende dev)
    machine_ifaces = set(dev for _net, dev, _gip in subnet_pairs if dev)

    # Bepaal de MACHINE-interface voor handmatig toegevoegde subnetten (dev="").
    # Bij alles-op-één (in=uit) is dat de uplink-interface zelf; anders de
    # niet-uplink interface met IPv4, of de enige interface met IPv4.
    ipv4_ifaces = [i for i in all_ifaces if iface_has_ipv4(i)]
    machine_iface_for_manual = ""
    non_uplink = [i for i in ipv4_ifaces if i != default_dev]
    if non_uplink:
        machine_iface_for_manual = non_uplink[0]      # aparte machine-NIC
    elif len(ipv4_ifaces) == 1:
        machine_iface_for_manual = ipv4_ifaces[0]     # alles op één kaart (in=uit)
    elif default_dev:
        machine_iface_for_manual = default_dev

    # gateway-IP-voorstel per interface. Automatisch gedetecteerde subnetten
    # hebben een dev; handmatige subnetten (dev="") koppelen we aan de
    # machine-interface hierboven. De uplink krijgt nooit een gateway-IP.
    gip_by_iface = {}
    for net, dev, gip in subnet_pairs:
        if not gip:
            continue
        target = dev if dev else machine_iface_for_manual
        if target and target not in gip_by_iface:
            gip_by_iface[target] = (gip, str(_network_obj(net).prefixlen))

    # FILTER: alleen relevante interfaces. Een kaart is relevant als hij een
    # IPv4 heeft (dus in gebruik is) of bij een gekozen machinesubnet hoort.
    relevant = [i for i in all_ifaces if iface_has_ipv4(i) or i in machine_ifaces]
    if not relevant:
        # niets zinnigs te configureren -> laat de agent het via lan_interfaces doen
        return []

    print("\n=== Interface-rollen instellen ===")
    print("Per netwerkkaart: MACHINE-kant (naar de PLC), UPLINK (naar de hub),")
    print("BEIDE (alles op één kaart), of SKIP om deze kaart over te slaan.")
    print("Kaarten zonder adres en zonder gekozen subnet slaan we zelf al over.")

    single = len(relevant) == 1

    def default_role_for(iface):
        if single:
            return "both"
        if iface in machine_ifaces and iface == default_dev:
            return "both"
        if iface in machine_ifaces:
            return "machine"
        if iface == default_dev:
            return "uplink"
        return "machine"

    result = []
    for iface in relevant:
        has_ip = iface_has_ipv4(iface)
        gip, pfx = gip_by_iface.get(iface, ("", "24"))
        default_role = default_role_for(iface)

        if NONINTERACTIVE:
            role = default_role
        else:
            print(f"\nInterface {iface}" + (" (heeft nu IPv4)" if has_ip else " (geen IPv4)"))
            role = ask(f"  Rol voor {iface}: machine / uplink / both / skip (overslaan)", default_role).strip().lower()
            # expliciet overslaan: deze kaart wordt helemaal niet geconfigureerd
            if role in ("skip","s","overslaan","x","-","geen","none"):
                print(f"  {iface} overgeslagen -- wordt niet geconfigureerd.")
                continue
            if role not in ("machine","uplink","both"):
                role = default_role

        # IP-methode bepalen. Belangrijk: heeft de interface al DHCP (has_ip)
        # dan is dhcp de veilige default voor de UPLINK-kant. Voor de
        # MACHINE-kant is een gateway-IP nodig -> static met dat IP.
        is_machine = role in ("machine","both")
        ip_addr = None
        prefix_len = None

        if is_machine and gip:
            # machinezijde met bekend gateway-IP -> static
            method = "static"
            ip_addr = gip
            try:
                prefix_len = int(pfx)
            except Exception:
                prefix_len = 24
        elif has_ip:
            # heeft al een adres (DHCP) -> dhcp, niks te vragen
            method = "dhcp"
        else:
            # geen adres en geen gateway-IP bekend -> vraag (of skip)
            if NONINTERACTIVE:
                method = "dhcp"
            else:
                method = ask(f"  IP voor {iface}: dhcp / static", "dhcp").strip().lower()
                if method not in ("dhcp","static"):
                    method = "dhcp"
                if method == "static":
                    ip_addr = ask(f"  Vast IP voor {iface} (leeg = toch dhcp)", "").strip() or None
                    if ip_addr:
                        _dflt_mask = prefix_to_mask(pfx or 24)
                        pfx_in = ask(f"  Subnetmasker voor {iface}", _dflt_mask).strip()
                        prefix_len = mask_to_prefix(pfx_in)
                        if prefix_len is None:
                            prefix_len = 24
                    else:
                        # static zonder IP -> terugval naar dhcp (voorkomt 422)
                        method = "dhcp"

        result.append({"iface":iface,"role":role,"ip_method":method,
                       "ip_addr":ip_addr,"prefix_len":prefix_len})

    # veiligheidsnet: verwijder alle static-items zonder ip_addr (mag nooit)
    result = [r for r in result if not (r["ip_method"]=="static" and not r["ip_addr"])]
    return result

def choose_subnets():
    advanced = ask("Geavanceerde/VPN/Docker netwerken tonen? j/n", "n").lower().startswith("j")
    detected = detect_subnets(include_advanced=advanced)  # lijst van (netwerk, interface)
    print("\nGevonden netwerken:")
    if detected:
        # markeer welk netwerk de UPLINK is (netwerk waar het huidige adres van
        # de default-route-interface in valt) -- dat is meestal GEEN machinenet.
        _up_dev = ""
        _up_ip = ""
        for _l in output("ip -4 route show default").splitlines():
            _t = _l.split()
            if "dev" in _t:
                _up_dev = _t[_t.index("dev")+1]
            if "src" in _t:
                _up_ip = _t[_t.index("src")+1]
            if _up_dev:
                break
        if _up_dev and not _up_ip:
            for _l in output(f"ip -4 -o addr show dev {_up_dev}").splitlines():
                _p = _l.split()
                if "inet" in _p:
                    _up_ip = _p[_p.index("inet")+1].split("/")[0]
                    break
        for i, (net, dev) in enumerate(detected, 1):
            _tag = ""
            try:
                # exacte match: alleen het netwerk dat ECHT op de uplink-interface
                # staat is de uplink (een breed machinenet kan het uplink-adres
                # omvatten zonder de uplink te zijn)
                if _up_dev and dev == _up_dev and _up_ip:
                    _n = ipaddress.ip_network(net, strict=False)
                    if ipaddress.ip_address(_up_ip) in _n and _n.prefixlen >= 24:
                        _tag = "  <-- UPLINK (waarschijnlijk NIET je machinenetwerk)"
            except Exception:
                pass
            print(f"{i}. {fmt_net(net)} (via {dev or '?'}){_tag}")
    else:
        print("Geen bruikbare IPv4-linknetwerken gevonden.")

    selected = []
    if NONINTERACTIVE:
        # Herstel-modus: gewoon alles wat nu gedetecteerd wordt opnieuw
        # meenemen, geen mens die hoeft te kiezen.
        selected = list(detected)
    else:
        # Consistent: ALTIJD leeg kunnen laten (= niets selecteren), ook als er
        # maar één netwerk gedetecteerd is. Dat is nodig wanneer het enige
        # gedetecteerde netwerk de UPLINK is (bijv. het kantoornetwerk) en het
        # echte machinenetwerk handmatig toegevoegd wordt.
        if len(detected) >= 1:
            choice = ask("Selecteer subnetnummer(s), komma-gescheiden. Leeg = niets selecteren", "")
        else:
            choice = ""
        for item in [x.strip() for x in choice.split(",") if x.strip()]:
            selected.append(detected[int(item)-1])
        while True:
            manual = ask("Handmatig netwerk toevoegen? bijv. 192.168.0.0 255.255.0.0, leeg = klaar", "")
            if not manual: break
            try:
                # accepteer zowel 'adres masker' als 'adres/prefix'
                _parts = manual.replace(",", " ").split()
                if len(_parts) == 2:
                    _pfx = mask_to_prefix(_parts[1])
                    if _pfx is None:
                        raise ValueError(f"ongeldig masker: {_parts[1]}")
                    manual = f"{_parts[0]}/{_pfx}"
                net = str(ipaddress.ip_network(manual, strict=False))
                selected.append((net, ""))  # interface onbekend voor handmatig ingevoerde subnetten
            except Exception as e:
                print(f"  Ongeldige invoer ({e}).")
                print("  Verwacht: netwerkadres en masker, bijv. 192.168.0.0 255.255.0.0")

    # Bepaal welk netwerk de UPLINK is: dat is het netwerk waar de default
    # route doorheen loopt (de interface met de default route, en waarvan het
    # huidige IP-adres van de gateway in dat netwerk valt). Voor de uplink
    # vragen we GEEN gateway-IP -- die heeft al een adres (DHCP/statisch) en
    # hoort de machinezijde niet te zijn.
    uplink_dev = ""
    uplink_gw_ip = ""
    for line in output("ip -4 route show default").splitlines():
        toks = line.split()
        if "dev" in toks:
            uplink_dev = toks[toks.index("dev")+1]
        # 'default via X dev Y src Z' -- pak src indien aanwezig
        if "src" in toks:
            uplink_gw_ip = toks[toks.index("src")+1]
        if uplink_dev:
            break
    # val terug op het eerste IPv4-adres van de uplink-interface
    if uplink_dev and not uplink_gw_ip:
        for l in output(f"ip -4 -o addr show dev {uplink_dev}").splitlines():
            parts = l.split()
            if "inet" in parts:
                uplink_gw_ip = parts[parts.index("inet")+1].split("/")[0]
                break

    # Het EXACTE netwerk waarin de uplink-interface zit (adres + masker), niet
    # zomaar "het adres valt erin". Dat onderscheid is cruciaal: een handmatig
    # machinenetwerk met een BREED masker (bijv. 192.168.0.0 255.255.0.0)
    # OMVAT het uplink-adres 192.168.1.75, maar is een ander netwerk. Alleen
    # een exacte match (zelfde netwerkadres en zelfde masker) is de uplink.
    uplink_networks = set()
    if uplink_dev:
        for _l in output(f"ip -4 -o addr show dev {uplink_dev}").splitlines():
            _p = _l.split()
            if "inet" in _p:
                try:
                    uplink_networks.add(str(ipaddress.ip_network(_p[_p.index("inet")+1], strict=False)))
                except Exception:
                    pass

    def is_uplink_net(net, dev):
        # exacte match met een netwerk dat daadwerkelijk op de uplink-interface
        # geconfigureerd staat -- alleen dan is het de uplink
        try:
            return str(_network_obj(net)) in uplink_networks
        except Exception:
            return False

    # Ontdubbel, en vraag per uniek netwerk een gateway-IP (leeg = alleen L2).
    # De uplink slaan we over -- geen gateway-IP nodig.
    out = []
    seen = set()
    print("\nGateway-IP per netwerk (voor online gaan / L3). Enter = voorstel overnemen, leeg = alleen L2.")
    for net, dev in selected:
        if net in seen:
            continue
        seen.add(net)
        if is_uplink_net(net, dev):
            print(f"\n  Netwerk {fmt_net(net)} is de UPLINK (heeft al adres {uplink_gw_ip}) -- geen gateway-IP nodig, overgeslagen.")
            out.append((net, dev, ""))
            continue
        gip = "" if NONINTERACTIVE else ask_gateway_ip(net, dev)
        out.append((net, dev, gip))
    if not out: raise SystemExit("Er is geen subnet gekozen.")
    return out

def choose_org(options):
    orgs = options.get("organizations", [])
    if not orgs: raise SystemExit("Geen klanten beschikbaar voor deze gebruiker.")
    print("\nKlanten:")
    for i,o in enumerate(orgs,1): print(f"{i}. {o['name']}")
    if options.get("can_create_organization"): print("N. Nieuwe klant")
    c = ask("Kies klantnummer" + (" of N" if options.get("can_create_organization") else ""))
    if c.lower()=="n":
        if not options.get("can_create_organization"): raise SystemExit("Deze gebruiker mag geen klanten aanmaken.")
        return req("POST", PORTAL_URL+"/installer/organizations", token=TOKEN, data={"name":ask("Nieuwe klantnaam")})["id"]
    return orgs[int(c)-1]["id"]

def choose_site(options, org_id):
    sites = [s for s in options.get("sites", []) if s["organization_id"] == org_id]
    print("\nLocatie:")
    print("0. Geen locatie / direct onder klant")
    if len(sites)==1:
        print(f"1. {sites[0]['name']} (voorgesteld)")
        print("N. Nieuwe locatie")
        c = ask("Kies locatie", "1")
    elif len(sites)>1:
        for i,s in enumerate(sites,1): print(f"{i}. {s['name']}")
        print("N. Nieuwe locatie")
        c = ask("Kies locatie")
    else:
        print("Geen locaties gevonden.")
        print("N. Nieuwe locatie")
        c = ask("Kies 0 of N", "0")
    if c == "0": return None
    if c.lower()=="n":
        return req("POST", PORTAL_URL+"/installer/sites", token=TOKEN, data={"organization_id":org_id,"name":ask("Nieuwe locatienaam")})["id"]
    return sites[int(c)-1]["id"]


def register_wayvnc_service(gw_id, token):
    # NIEUW: als wayvnc lokaal gedetecteerd en compatibel gemaakt is, meteen
    # een service in de portal registreren zodat 'ie direct bruikbaar is --
    # in plaats van dat iemand dat later handmatig moet toevoegen.
    try:
        req("POST", PORTAL_URL + f"/gateways/{gw_id}/services", token=token, data={
            "service_type": "vnc_local",
            "name": "wayvnc (lokaal scherm)",
            "target_host": "127.0.0.1",
            "target_port": 5900,
            "enabled": True,
            "auth_enabled": False,
            "username": None,
            "password": None,
            "view_only": False,
            "shared_session": True,
            "settings": {}
        })
        print("wayvnc-service geregistreerd in de portal.")
    except Exception as e:
        print("wayvnc-service registreren mislukt (ga verder zonder):", e)


def configure_wayvnc_compat_if_needed(gw_id=None, token=None):
    has_wayvnc = subprocess.run("command -v wayvnc >/dev/null 2>&1", shell=True).returncode == 0
    running = subprocess.run("pgrep -x wayvnc >/dev/null 2>&1", shell=True).returncode == 0
    if not has_wayvnc or not running:
        return

    print("\nwayvnc gedetecteerd.")
    print("Standaard wayvnc kan in noVNC de fout geven: Unsupported security types (types: 262).")
    print("Remote Platform kan wayvnc lokaal-only noVNC-compatible zetten:")
    print(" - bind op 127.0.0.1")
    print(" - poort 5900")
    print(" - auth uit, omdat toegang via gateway-tunnel loopt")
    choice = ask("wayvnc compatibel maken voor browser/noVNC? j/n", "j")
    if not choice.lower().startswith("j"):
        return

    script_url = PORTAL_URL + "/downloads/tools/fix-wayvnc-novnc.sh"
    tmp_script = "/tmp/fix-wayvnc-novnc.sh"
    applied = False
    try:
        urllib.request.urlretrieve(script_url, tmp_script)
        run(f"chmod +x {tmp_script}")
        run(f"{tmp_script}", check=False)
        applied = True
    except Exception as e:
        print("wayvnc compatibility stap mislukt:", e)

    if applied and gw_id:
        register_wayvnc_service(gw_id, token)


def install_service():
    SERVICE_FILE.write_text("""[Unit]
Description=Remote Platform Gateway Agent
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/gateway-agent -config /etc/remote-platform/agent/config.json
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
""")
    run("systemctl daemon-reload")
    run("systemctl enable --now remote-platform-agent.service")

print("Remote Platform Gateway Installer", INSTALL_VERSION, "(niet-interactief)" if NONINTERACTIVE else "")
print("Portal:", PORTAL_URL)

TOKEN = None
if not NONINTERACTIVE:
    print("\nInloggen")
    username = ask("Gebruiker/email")
    password = ask("Wachtwoord", secret=True)
    try:
        login = req("POST", PORTAL_URL+"/installer/login", data={"username":username,"password":password})
    except urllib.error.HTTPError as e:
        raise SystemExit(f"Login mislukt: HTTP {e.code}")
    TOKEN = login["access_token"]

mode, old_cfg = choose_existing_action(token=TOKEN)
reuse_gateway_id = choose_reuse_gateway(old_cfg) if mode in ("repair", "reinstall") else ""

if reuse_gateway_id:
    org_id = None
    site_id = None
    gateway_name = ask("Gateway naam", platform.node())
else:
    options = req("GET", PORTAL_URL+"/installer/options", token=TOKEN)
    org_id = choose_org(options)
    options = req("GET", PORTAL_URL+"/installer/options", token=TOKEN)
    site_id = choose_site(options, org_id)
    gateway_name = ask("Gateway naam", platform.node())

subnet_pairs = choose_subnets()          # lijst van (netwerk, interface, gateway_ip)
subnets = [s[0] for s in subnet_pairs]
primary_subnet = subnets[0]
# gateway-IP per netwerk (kan leeg zijn = alleen L2 voor dat netwerk)
gateway_ips = {s[0]: s[2] for s in subnet_pairs}
# NIEUW: verzamel ALLE unieke interfaces die bij de gekozen subnetten horen
# -- niet alleen die van het eerste subnet. Anders krijgt bijvoorbeeld
# wlan0 geen NAT-regel als eth0 toevallig als eerste is gekozen, terwijl
# beide netwerken wel degelijk bereikbaar moeten zijn (precies wat er bij
# S010HMI misging: 192.168.1.x via wlan0 werkte niet omdat alleen eth0
# een regel kreeg).
lan_interfaces = []
for _net, dev, _gip in subnet_pairs:
    if dev and dev not in lan_interfaces:
        lan_interfaces.append(dev)

print("\nSamenvatting:")
print("Actie:", mode)
print("Gateway:", gateway_name)
if reuse_gateway_id:
    print("Hergebruik gateway_id:", reuse_gateway_id)
else:
    print("Locatie:", site_id or "direct onder klant")
print("Netwerken:", ", ".join(fmt_net(x) for x in subnets))
print("LAN-interface(s) (voor NAT):", ", ".join(lan_interfaces) if lan_interfaces else "(onbekend -- NAT wordt overgeslagen)")
_gip_lines = [f"  {n} -> {g}" for n, g in gateway_ips.items() if g]
print("Gateway-IP per netwerk:", ("\n" + "\n".join(_gip_lines)) if _gip_lines else "(geen -- alleen L2/DCP)")
if not ask("Installeren? j/n", "j").lower().startswith("j"):
    raise SystemExit("Afgebroken.")

arch = platform.machine().lower()
if arch in ("aarch64","arm64"): bin_url = PORTAL_URL+"/downloads/bin/gateway-agent-linux-arm64"
elif arch in ("x86_64","amd64"): bin_url = PORTAL_URL+"/downloads/bin/gateway-agent-linux-amd64"
else: raise SystemExit("Niet ondersteunde architectuur: "+arch)

# 'iptables' stond hier niet bij -- zonder dat pakket kan NAT/masquerade
# nooit ingesteld worden, en bleef elke nieuwe gateway zonder verkeer naar
# het echte klantnetwerk zitten totdat iemand dat achteraf handmatig fixte.
run("apt-get update && apt-get install -y curl ca-certificates wireguard python3 iproute2 iptables iputils-arping iputils-ping")
tmp_bin = "/tmp/gateway-agent.download"
print("Agent downloaden:", bin_url)
urllib.request.urlretrieve(bin_url, tmp_bin)
run(f"install -m 0755 {tmp_bin} /usr/local/bin/gateway-agent")
run(f"rm -f {tmp_bin}", check=False)

if reuse_gateway_id:
    gw = {
        "id": reuse_gateway_id,
        "serial": old_cfg.get("gateway_serial") or old_cfg.get("serial") or ("GW-REUSE-" + reuse_gateway_id[:8])
    }
    cfg = dict(old_cfg)
    cfg["server_url"] = PORTAL_URL
    cfg["gateway_id"] = reuse_gateway_id
    cfg["machine_subnet"] = primary_subnet
    if "gateway_serial" not in cfg:
        cfg["gateway_serial"] = gw["serial"]
else:
    reg = req("POST", PORTAL_URL+"/installer/register-gateway", token=TOKEN, data={
        "organization_id": org_id,
        "site_id": site_id,
        "gateway_name": gateway_name,
        "machine_subnet": primary_subnet,
        "hostname": platform.node(),
        "os": "linux",
        "arch": arch,
        "installer_version": INSTALL_VERSION
    })
    gw = reg["gateway"]
    cfg = reg["agent_config"]
    cfg["server_url"] = PORTAL_URL
    cfg["gateway_id"] = gw["id"]

CONFIG_DIR.mkdir(parents=True, exist_ok=True)
CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
run("chmod 600 /etc/remote-platform/agent/config.json")
install_service()
configure_wayvnc_compat_if_needed(gw_id=gw["id"], token=TOKEN)

for net in subnets:
    try:
        req("POST", PORTAL_URL+f"/gateways/{gw['id']}/routes", token=TOKEN, data={"network":net,"gateway_ip":gateway_ips.get(net,"") or None,"description":"Selected during installer","source":"installer","published":True,"enabled":True})
    except Exception as e:
        print("Route registratie overgeslagen/fout:", net, e)

# NIEUW: volledige interface-configuratie (rol machine/uplink/both + IP-methode
# dhcp/static) opslaan via PUT /gateways/{id}/interfaces. Dit ondersteunt alle
# in/uit-combinaties. De agent leest dit bij sessie-open en zet ip_forward +
# masquerade + eventuele statische IP's op de juiste interface(s).
try:
    iface_cfg = configure_interfaces(subnet_pairs)
    if iface_cfg:
        req("PUT", PORTAL_URL+f"/gateways/{gw['id']}/interfaces", token=TOKEN, data={"interfaces": iface_cfg})
        print("Interface-configuratie opgeslagen:")
        for ic in iface_cfg:
            desc = ic["role"]
            if ic["ip_method"] == "static":
                desc += f", static {ic['ip_addr']} masker {prefix_to_mask(ic['prefix_len'])}"
            else:
                desc += ", dhcp"
            print(f"  {ic['iface']}: {desc}")
except Exception as e:
    print("Interface-configuratie overgeslagen/fout:", e)

# NIEUW: firewall configureren en opslaan. Standaard staat alles tussen
# klant- en machinenetwerk dicht; alleen de tunnel mag erbij.
try:
    fw = configure_firewall()
    req("PUT", PORTAL_URL+f"/gateways/{gw['id']}/firewall", token=TOKEN, data=fw)
    if fw["firewall_enabled"]:
        print(f"Firewall AAN opgeslagen ({len(fw['rules'])} uitzondering(en)).")
    else:
        print("Firewall UIT opgeslagen.")
    # direct toepassen zodat de gateway niet tot de eerste sessie open staat
    try:
        req("POST", PORTAL_URL+f"/gateways/{gw['id']}/firewall/apply", token=TOKEN, data={})
        print("Firewall wordt nu toegepast op de gateway.")
    except Exception as e:
        print("Firewall direct toepassen overgeslagen (gebeurt bij eerste sessie):", e)
except Exception as e:
    print("Firewall-configuratie overgeslagen/fout:", e)

# NIEUW: optionele DHCP-server op de machinekant (alleen bij gescheiden kaarten)
try:
    dhcp_cfg = configure_dhcp(iface_cfg if 'iface_cfg' in dir() else [])
    if dhcp_cfg:
        req("PUT", PORTAL_URL+f"/gateways/{gw['id']}/dhcp", token=TOKEN, data=dhcp_cfg)
        req("POST", PORTAL_URL+f"/gateways/{gw['id']}/dhcp/apply", token=TOKEN, data={})
        print(f"DHCP-server AAN op {dhcp_cfg['iface']}: {dhcp_cfg['range_start']} - {dhcp_cfg['range_end']}")
except Exception as e:
    print("DHCP-configuratie overgeslagen/fout:", e)

# NIEUW: handmatig ingevoerde subnetten (geen bekende interface, dev="")
# krijgen anders NOOIT een lokale kernel-route op de gateway zelf -- ze
# vallen dan stil terug op de standaardroute (vaak de WiFi-uplink!) en
# zijn zo onbereikbaar. Dit veroorzaakte op 23 juli een echte storing bij
# S010HMI (172.27.111.0/24 werd handmatig toegevoegd, kreeg nooit een
# lokale route, en verkeer ernaartoe verdween in het niets via wlan0).
# Aanname: een handmatig subnet zit op hetzelfde fysieke segment als het
# eerste automatisch gedetecteerde subnet -- redelijk voor platte
# industriële netwerken met meerdere bereiken op één kabel, en bevestigd
# door het S010HMI-incident zelf.
primary_iface_for_manual = next((dev for _net, dev, _gip in subnet_pairs if dev), None)
if primary_iface_for_manual:
    manual_routes = [net for net, dev, _gip in subnet_pairs if not dev]
    for net in manual_routes:
        run(f"ip route replace {net} dev {primary_iface_for_manual}", check=False)
        print(f"Lokale route toegevoegd voor handmatig subnet {net} via {primary_iface_for_manual}")

    if manual_routes:
        route_cmds = "\n".join(f"ExecStart=/sbin/ip route replace {n} dev {primary_iface_for_manual}" for n in manual_routes)
        Path("/etc/systemd/system/rp-manual-routes.service").write_text(f"""[Unit]
Description=Remote Platform: herstel handmatig toegevoegde subnet-routes na opstarten
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
{route_cmds}

[Install]
WantedBy=multi-user.target
""")
        run("systemctl daemon-reload", check=False)
        run("systemctl enable rp-manual-routes.service", check=False)
        print("rp-manual-routes.service aangemaakt -- herstelt deze route(s) automatisch na elke opstart.")

try: req("POST", PORTAL_URL+f"/gateways/{gw['id']}/vpn/generate-keys")
except Exception as e: print("Generate keys melding:", e)
time.sleep(8)

host = PORTAL_URL.replace("http://","").replace("https://","").split("/")[0].split(":")[0]
endpoint = os.environ.get("REMOTE_PLATFORM_VPN_ENDPOINT", host+":51820")
try:
    out = req("POST", PORTAL_URL+f"/gateways/{gw['id']}/vpn/bootstrap", data={
        "server_code": "primary",
        "endpoint": endpoint,
        "transport_mode": "udp",
        "machine_subnets": subnets,
        "lan_interfaces": lan_interfaces,
        "gateway_ips": gateway_ips
    })
    print(json.dumps(out, indent=2))
except Exception as e:
    print("VPN bootstrap melding:", e)

print("\nAgent status:")
subprocess.call("systemctl status remote-platform-agent.service --no-pager -l || true", shell=True)
print("\nKlaar.")
print("Gateway ID:", gw["id"])
print("Serial:", gw["serial"])
END_RP_INSTALLER

REMOTE_PLATFORM_URL="${DEFAULT_PORTAL_URL}" python3 /tmp/rp_installer.py
