#!/usr/bin/env python3
"""Export UniFi Network (Integration API) — lecture seule.

Env:
  UNIFY_API_NETWORK_KEY  (clé locale CG Fiber, pas unifi.ui.com)
  UNIFI_BASE_URL         défaut https://192.168.50.1/proxy/network/integration/v1

Sortie:
  stdout = résumé Markdown
  --json-dir DIR         dumps JSON bruts (souvent gitignorés)

Pas de secrets dans le Markdown. Ne committe pas les dumps bruts.
"""
from __future__ import annotations

import argparse
import json
import os
import ssl
import sys
import urllib.error
import urllib.request
from datetime import date
from pathlib import Path

DEFAULT_BASE = "https://192.168.50.1/proxy/network/integration/v1"


def get_json(base: str, path: str, key: str, ctx: ssl.SSLContext) -> dict:
    req = urllib.request.Request(
        f"{base.rstrip('/')}{path}",
        headers={"X-API-KEY": key, "Accept": "application/json"},
    )
    with urllib.request.urlopen(req, context=ctx, timeout=30) as resp:
        return json.load(resp)


def zone_name(zones: dict[str, str], zid: str | None) -> str:
    return zones.get(zid or "", zid or "?")


def describe_filter(tf: dict | None, nets: dict[str, dict]) -> str:
    if not tf:
        return "ANY"
    t = tf.get("type")
    if t == "NETWORK":
        nf = tf.get("networkFilter") or {}
        ids = nf.get("networkIds") or []
        names = []
        for i in ids:
            n = nets.get(i)
            if n:
                names.append(f"{n['name']}(vlan{n['vlanId']})")
            else:
                names.append(i[:8])
        prefix = "NOT " if nf.get("matchOpposite") else ""
        return prefix + (",".join(names) if names else "NETWORK?")
    if t == "IP_ADDRESS":
        items = (tf.get("ipAddressFilter") or {}).get("items") or []
        return ",".join(str(i.get("value")) for i in items) or "IP?"
    if t == "PORT":
        ports = (tf.get("portFilter") or {}).get("items") or []
        return "port:" + ",".join(str(i.get("value")) for i in ports)
    return str(t)


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--json-dir", type=Path, help="Écrire dumps JSON bruts")
    ap.add_argument("--insecure", action="store_true", default=True, help="TLS sans vérif (défaut)")
    args = ap.parse_args()

    key = os.environ.get("UNIFY_API_NETWORK_KEY") or os.environ.get("UNIFI_API_NETWORK_KEY")
    if not key:
        print("UNIFY_API_NETWORK_KEY manquant", file=sys.stderr)
        return 1
    base = os.environ.get("UNIFI_BASE_URL", DEFAULT_BASE)
    ctx = ssl._create_unverified_context() if args.insecure else ssl.create_default_context()

    try:
        info = get_json(base, "/info", key, ctx)
        sites = get_json(base, "/sites", key, ctx)["data"]
    except urllib.error.HTTPError as e:
        print(f"HTTP {e.code}: {e.read()[:200]!r}", file=sys.stderr)
        return 1

    if not sites:
        print("Aucun site", file=sys.stderr)
        return 1
    site = sites[0]
    site_id = site["id"]
    sp = f"/sites/{site_id}"

    networks = get_json(base, f"{sp}/networks", key, ctx)["data"]
    nets = {n["id"]: n for n in networks}
    # détails subnet
    details = []
    for n in networks:
        d = get_json(base, f"{sp}/networks/{n['id']}", key, ctx)
        details.append(d)
        nets[n["id"]] = d

    zones_raw = get_json(base, f"{sp}/firewall/zones", key, ctx)["data"]
    zones = {z["id"]: z["name"] for z in zones_raw}
    policies = get_json(base, f"{sp}/firewall/policies", key, ctx)["data"]

    if args.json_dir:
        args.json_dir.mkdir(parents=True, exist_ok=True)
        for name, obj in [
            ("info.json", info),
            ("sites.json", sites),
            ("networks.json", details),
            ("firewall_zones.json", zones_raw),
            ("firewall_policies.json", policies),
        ]:
            (args.json_dir / name).write_text(json.dumps(obj, indent=2) + "\n")

    user = [p for p in policies if (p.get("metadata") or {}).get("origin") == "USER_DEFINED"]
    today = date.today().isoformat()

    print(f"# UniFi export (lecture seule) — {today}")
    print()
    print(f"- Application : `{info.get('applicationVersion', '?')}`")
    print(f"- Site : `{site.get('name')}` (`{site.get('internalReference', '')}`)")
    print(f"- Policies : {len(policies)} total, **{len(user)} USER_DEFINED**")
    print()
    print("## Networks")
    print()
    print("| VLAN | Nom | Gateway | Zone | isolation | internet | DHCP DNS |")
    print("|------|-----|---------|------|-----------|----------|----------|")
    for d in sorted(details, key=lambda x: x.get("vlanId") or 0):
        ipv4 = d.get("ipv4Configuration") or {}
        gw = ipv4.get("hostIpAddress", "")
        pref = ipv4.get("prefixLength", "")
        subnet = f"{gw}/{pref}" if gw else "?"
        dhcp = ipv4.get("dhcpConfiguration") or ipv4.get("dhcp") or {}
        dns_list = dhcp.get("dnsServerIpAddressesOverride") if isinstance(dhcp, dict) else None
        dns = ", ".join(dns_list) if dns_list else "(gateway / défaut UniFi)"
        print(
            f"| {d.get('vlanId')} | {d.get('name')} | `{subnet}` | "
            f"{zone_name(zones, d.get('zoneId'))} | "
            f"{d.get('isolationEnabled')} | {d.get('internetAccessEnabled')} | {dns} |"
        )
    print()
    print("## Firewall USER_DEFINED")
    print()
    print("| Index | Nom | Action | Source | Destination |")
    print("|-------|-----|--------|--------|-------------|")
    for p in sorted(user, key=lambda x: x.get("index") or 0):
        src = p.get("source") or {}
        dst = p.get("destination") or {}
        act = (p.get("action") or {}).get("type")
        print(
            f"| {p.get('index')} | {p.get('name')} | {act} | "
            f"{zone_name(zones, src.get('zoneId'))} / {describe_filter(src.get('trafficFilter'), nets)} | "
            f"{zone_name(zones, dst.get('zoneId'))} / {describe_filter(dst.get('trafficFilter'), nets)} |"
        )
    print()
    print("## Notes auto")
    print()
    print("- Catch-all système Internal→Internal `Allow All Traffic` : toujours présent côté UniFi.")
    print("- Les règles USER sans filtre PORT = allow **tout** le trafic (pas « DNS only »).")
    print("- Regenerer : `python3 unifi_export_network.py`")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
