"""Consolidated live leak audit.

Used by the Privacy tab "Run audit" button. Probes every surface that can
deanonymize the host even when the chain is healthy:

  • IP            — exit IP through chain vs direct IP (subnet leak)
  • DNS           — system resolvers, whether they're public
  • IPv6 binding  — adapters with IPv6 active
  • WPAD/PAC      — leftover AutoConfigURL or AutoDetect
  • Policy locks  — Group Policy proxy keys that beat our settings
  • PerUser flag  — Windows Server ProxySettingsPerUser
  • LAN broadcast — LLMNR / NetBIOS / mDNS status
  • VPN           — adapter presence
  • WebRTC        — Chrome/Edge policy presence
  • System proxy  — what registry says we're set to
  • TLS exit      — quick categorisation hint (datacenter / residential)

Each check has its own short timeout so a slow probe never blocks the rest.
"""
from __future__ import annotations

import asyncio
import logging
import subprocess
import winreg
from dataclasses import dataclass, field

from .dns_leak import get_system_dns_servers
from .firewall import is_admin
from .privacy_lan import lan_status
from .sysproxy import detect_policy_overrides, is_system_proxy_set
from .validator import check_chain_exit_ip, get_direct_ip
from .vpn_detect import detect_vpn

log = logging.getLogger(__name__)


@dataclass
class AuditFinding:
    name: str
    ok: bool
    value: str
    note: str = ""


@dataclass
class AuditReport:
    findings: list[AuditFinding] = field(default_factory=list)
    overall_ok: bool = True

    def add(self, name: str, ok: bool, value: str, note: str = "") -> None:
        self.findings.append(AuditFinding(name, ok, value, note))
        if not ok:
            self.overall_ok = False


def _check_ipv6_active() -> tuple[bool, str]:
    """True/'string' if any 'Up' adapter has IPv6 binding enabled.

    PowerShell 3.0+ path; falls back to ipconfig parsing (locale-tolerant
    enough — looks for hex colons).
    """
    try:
        r = subprocess.run(
            ["powershell", "-NoProfile", "-NonInteractive", "-Command",
             "Get-NetAdapterBinding -ComponentID ms_tcpip6 | "
             "Where-Object { $_.Enabled } | "
             "Select-Object -ExpandProperty Name"],
            capture_output=True, text=True, timeout=10,
            creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
        )
        names = [n.strip() for n in (r.stdout or "").splitlines() if n.strip()]
        if names:
            return True, ", ".join(names[:3]) + (f" +{len(names)-3}" if len(names) > 3 else "")
        # Empty stdout could mean PS3 cmdlet missing — try ipconfig fallback.
        if r.returncode != 0:
            raise RuntimeError("PS3 net cmdlets unavailable")
        return False, "no adapters bound to IPv6"
    except Exception:
        try:
            r = subprocess.run(
                ["ipconfig"], capture_output=True, text=True, timeout=8,
                creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
            )
            has_v6 = any(
                "IPv6" in ln and ":" in ln.split(":", 1)[1]
                for ln in (r.stdout or "").splitlines()
            )
            return has_v6, ("IPv6 address present" if has_v6 else "no IPv6 address")
        except Exception as e:
            return False, f"unknown ({e})"


def _check_wpad() -> tuple[bool, str]:
    path = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings"
    autoconf = ""
    autodet = 0
    try:
        with winreg.OpenKey(
            winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_QUERY_VALUE
        ) as key:
            try:
                autoconf = str(winreg.QueryValueEx(key, "AutoConfigURL")[0] or "")
            except OSError:
                pass
            try:
                autodet = int(winreg.QueryValueEx(key, "AutoDetect")[0])
            except OSError:
                autodet = 0
    except OSError:
        pass
    leaking = bool(autoconf) or autodet == 1
    if leaking:
        bits = []
        if autoconf:
            bits.append(f"AutoConfigURL={autoconf}")
        if autodet:
            bits.append(f"AutoDetect={autodet}")
        return False, ", ".join(bits)
    return True, "no WPAD/PAC override"


def _check_per_user_flag() -> tuple[bool, str]:
    try:
        with winreg.OpenKey(
            winreg.HKEY_LOCAL_MACHINE,
            r"SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings",
            0, winreg.KEY_QUERY_VALUE,
        ) as key:
            v = int(winreg.QueryValueEx(key, "ProxySettingsPerUser")[0])
            if v == 0:
                return False, "ProxySettingsPerUser=0 (HKCU IGNORED)"
            return True, "ProxySettingsPerUser=1"
    except OSError:
        return True, "unset (default → HKCU honored)"


def _check_webrtc_policy() -> tuple[bool, str]:
    """OK means the policy IS set (browsers won't leak non-proxied UDP)."""
    paths = (
        r"SOFTWARE\Policies\Google\Chrome",
        r"SOFTWARE\Policies\Microsoft\Edge",
    )
    _DWORD_DISABLE = 3  # disable_non_proxied_udp
    _STR_KEY = "WebRtcIPHandling"
    _STR_DISABLE = "disable_non_proxied_udp"
    found = []
    for p in paths:
        try:
            with winreg.OpenKey(
                winreg.HKEY_LOCAL_MACHINE, p, 0, winreg.KEY_QUERY_VALUE
            ) as key:
                ok = False
                try:
                    v = int(winreg.QueryValueEx(key, "DefaultWebRtcIpHandlingPolicy")[0])
                    if v == _DWORD_DISABLE:
                        ok = True
                except OSError:
                    pass
                try:
                    v_str = str(winreg.QueryValueEx(key, _STR_KEY)[0]).strip().lower()
                    if v_str == _STR_DISABLE:
                        ok = True
                except OSError:
                    pass
                if ok:
                    found.append(p.split("\\")[-1])
        except OSError:
            continue
    if found:
        return True, "policy set: " + ", ".join(found)
    return False, "no WebRTC policy — Chrome/Edge may leak local IPs"


def _categorize_exit(ip: str | None) -> str:
    """Cheap categorization hint based on common RIR / ASN heuristics.

    No external lookup — just shape-based hints. Real classification can be
    added later via offline GeoIP DBs.
    """
    if not ip:
        return "unknown"
    try:
        a = int(ip.split(".")[0])
    except Exception:
        return "non-IPv4"
    if a in (10,) or ip.startswith(("192.168.", "172.16.", "172.17.", "172.18.",
                                    "172.19.", "172.2", "172.30.", "172.31.")):
        return "PRIVATE — chain broken"
    if a == 127:
        return "LOOPBACK — chain broken"
    return "public"


async def run_audit(
    listen_proxy: str,
    ip_check_url: str,
    timeout_seconds: float = 10.0,
) -> AuditReport:
    """Run every check in parallel where safe; return structured report."""
    rep = AuditReport()
    chain_running = is_system_proxy_set()

    # Network probes in parallel
    direct_task = asyncio.create_task(get_direct_ip(ip_check_url, timeout_seconds))
    chain_task = (
        asyncio.create_task(
            check_chain_exit_ip(listen_proxy, ip_check_url, timeout_seconds, chain_hops=1)
        )
        if chain_running else None
    )

    direct_ip = await direct_task
    exit_ip = await chain_task if chain_task else None

    if direct_ip:
        rep.add("Direct IP (no chain)", True, direct_ip, "")
    else:
        rep.add("Direct IP (no chain)", False, "unreachable", "no internet?")

    if chain_running:
        if exit_ip:
            cat = _categorize_exit(exit_ip)
            same = bool(direct_ip and exit_ip == direct_ip)
            rep.add(
                "Chain exit IP",
                not same and cat == "public",
                f"{exit_ip} ({cat})",
                "matches direct IP — chain not forwarding" if same else "",
            )
        else:
            rep.add("Chain exit IP", False, "unreachable through chain", "")
    else:
        rep.add("Chain exit IP", True, "chain not running", "skipped")

    # DNS resolvers
    dns_servers = get_system_dns_servers()
    private_prefixes = ("127.", "10.", "192.168.", "172.16.", "172.17.", "172.18.",
                        "172.19.", "172.2", "172.30.", "172.31.")
    public_dns = [d for d in dns_servers if not d.startswith(private_prefixes)]
    if not dns_servers:
        rep.add("DNS resolvers", True, "none reported (DHCP)", "")
    elif public_dns:
        rep.add(
            "DNS resolvers",
            False,
            ", ".join(dns_servers),
            f"{len(public_dns)} public — DNS may bypass chain",
        )
    else:
        rep.add("DNS resolvers", True, ", ".join(dns_servers), "all private/local")

    # IPv6 binding
    v6_on, v6_msg = _check_ipv6_active()
    rep.add("IPv6 binding", not v6_on, v6_msg, "IPv6 active = potential leak past v4 proxies" if v6_on else "")

    # WPAD
    wpad_ok, wpad_msg = _check_wpad()
    rep.add("WPAD / PAC", wpad_ok, wpad_msg)

    # Per-User flag
    pu_ok, pu_msg = _check_per_user_flag()
    rep.add("ProxySettingsPerUser", pu_ok, pu_msg,
            "needs Admin fix on this box" if not pu_ok else "")

    # Policy locks
    pol = detect_policy_overrides()
    if pol:
        rep.add("Group Policy proxy locks", False, f"{len(pol)} entries",
                "policy keys override our proxy")
    else:
        rep.add("Group Policy proxy locks", True, "none")

    # LAN-scope broadcasts
    lan = lan_status()
    lan_clean = "OFF" in lan["llmnr"] and "OFF" in lan["mdns"] and "NICs with NetBIOS disabled" in lan["netbios"]
    rep.add(
        "LAN broadcast (LLMNR/NetBIOS/mDNS)",
        lan_clean,
        f"LLMNR={lan['llmnr']}  NetBIOS={lan['netbios']}  mDNS={lan['mdns']}",
        "hostname leaks to local network" if not lan_clean else "",
    )

    # VPN
    vpn = detect_vpn()
    rep.add(
        "VPN adapter",
        True,
        f"{vpn.label}" + (f" ({vpn.adapter})" if vpn.adapter else ""),
        "informational",
    )

    # WebRTC policy
    rtc_ok, rtc_msg = _check_webrtc_policy()
    rep.add("Browser WebRTC policy", rtc_ok, rtc_msg)

    # Admin status (a lot of fixes require it)
    rep.add(
        "Administrator privileges",
        is_admin(),
        "Yes" if is_admin() else "No",
        "MAC/IPv6/LAN/HKLM fixes need elevation" if not is_admin() else "",
    )

    return rep


def run_audit_sync(
    listen_proxy: str,
    ip_check_url: str,
    timeout_seconds: float = 10.0,
) -> AuditReport:
    return asyncio.run(run_audit(listen_proxy, ip_check_url, timeout_seconds))
