"""Network leak monitoring for privacy containers.

Monitors containers for traffic that bypasses the expected privacy tunnel
(Tor, VPN, WireGuard). Detects and logs potential leaks from the host side.
"""

from __future__ import annotations

import json
import os
import re
import signal
import subprocess
import threading
import time
from collections.abc import Callable
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from gaol.runtimes.base import ContainerRuntime


class PrivacyType(str, Enum):
    """Type of privacy tunnel being monitored."""

    TOR = "tor"
    VPN = "vpn"
    WIREGUARD = "wireguard"


class LeakType(str, Enum):
    """Type of network leak detected."""

    DNS = "dns"  # DNS query bypassing tunnel
    DIRECT_IP = "direct_ip"  # Direct IP connection
    IPV6 = "ipv6"  # IPv6 traffic (often leaks)
    WEBRTC = "webrtc"  # WebRTC leak (browser-specific)
    CLEARTEXT = "cleartext"  # Unencrypted traffic on tunnel interface


@dataclass
class LeakEvent:
    """Represents a detected network leak."""

    timestamp: datetime
    container: str
    leak_type: LeakType
    source_ip: str | None = None
    dest_ip: str | None = None
    dest_port: int | None = None
    protocol: str | None = None
    details: str | None = None
    raw_packet: str | None = None

    def to_dict(self) -> dict:
        """Convert to dictionary for JSON serialization."""
        return {
            "timestamp": self.timestamp.isoformat(),
            "container": self.container,
            "leak_type": self.leak_type.value,
            "source_ip": self.source_ip,
            "dest_ip": self.dest_ip,
            "dest_port": self.dest_port,
            "protocol": self.protocol,
            "details": self.details,
        }

    def to_json(self) -> str:
        """Convert to JSON string."""
        return json.dumps(self.to_dict())


@dataclass
class MonitorConfig:
    """Configuration for leak monitoring."""

    privacy_type: PrivacyType
    """Type of privacy tunnel being monitored."""

    allowed_ports: list[int] = field(default_factory=list)
    """Ports that are expected to have direct traffic (e.g., VPN port)."""

    allowed_ips: list[str] = field(default_factory=list)
    """IPs that are allowed to receive direct traffic (e.g., VPN server)."""

    allowed_networks: list[str] = field(default_factory=lambda: [
        "127.0.0.0/8",
        "10.0.0.0/8",
        "172.16.0.0/12",
        "192.168.0.0/16",
    ])
    """Local networks that are allowed."""

    tor_user: str = "debian-tor"
    """Username running the Tor daemon (for Tor containers)."""

    tor_socks_port: int = 9050
    """Tor SOCKS port."""

    tor_trans_port: int = 9040
    """Tor transparent proxy port."""

    tor_dns_port: int = 5353
    """Tor DNS port."""

    vpn_interface: str = "tun0"
    """VPN tunnel interface name."""

    wg_interface: str = "wg0"
    """WireGuard interface name."""

    log_file: Path | None = None
    """Path to log file for leak events."""

    alert_callback: Callable[[LeakEvent], None] | None = None
    """Callback function for leak alerts."""

    check_ipv6: bool = True
    """Whether to monitor for IPv6 leaks."""


class LeakMonitor:
    """Monitors a container for network leaks from the host side."""

    def __init__(
        self,
        container_name: str,
        config: MonitorConfig,
        runtime: ContainerRuntime | None = None,
    ) -> None:
        """Initialize leak monitor.

        Args:
            container_name: Name of the container to monitor
            config: Monitoring configuration
            runtime: Optional container runtime for additional checks
        """
        self.container_name = container_name
        self.config = config
        self.runtime = runtime

        self._running = False
        self._tcpdump_process: subprocess.Popen | None = None
        self._monitor_thread: threading.Thread | None = None
        self._leaks: list[LeakEvent] = []
        self._lock = threading.Lock()

    @property
    def is_running(self) -> bool:
        """Check if monitor is running."""
        return self._running

    @property
    def leaks(self) -> list[LeakEvent]:
        """Get list of detected leaks."""
        with self._lock:
            return list(self._leaks)

    def _get_container_interface(self) -> str | None:
        """Get the veth interface for the container on the host side.

        Returns:
            Interface name (e.g., 've-tor-browser') or None if not found
        """
        # For nspawn containers, the interface is typically ve-<container>
        veth_name = f"ve-{self.container_name}"

        # Check if interface exists
        result = subprocess.run(
            ["ip", "link", "show", veth_name],
            capture_output=True,
            text=True,
        )
        if result.returncode == 0:
            return veth_name

        # Try to find it from container's network namespace
        result = subprocess.run(
            ["sudo", "machinectl", "show", self.container_name, "-p", "Leader"],
            capture_output=True,
            text=True,
        )
        if result.returncode == 0:
            pid = result.stdout.strip().split("=")[-1]
            # Look for veth pair
            result = subprocess.run(
                ["ip", "link"],
                capture_output=True,
                text=True,
            )
            for line in result.stdout.split("\n"):
                if f"ve-{self.container_name}" in line or f"vb-{self.container_name}" in line:
                    match = re.search(r"^\d+:\s+(\S+)[@:]", line)
                    if match:
                        return match.group(1)

        return None

    def _get_container_pid(self) -> int | None:
        """Get the leader PID of the container.

        Returns:
            PID or None if container not running
        """
        result = subprocess.run(
            ["sudo", "machinectl", "show", self.container_name, "-p", "Leader", "--value"],
            capture_output=True,
            text=True,
        )
        if result.returncode == 0 and result.stdout.strip():
            try:
                return int(result.stdout.strip())
            except ValueError:
                return None
        return None

    def _build_tcpdump_filter(self) -> str:
        """Build tcpdump filter expression based on privacy type.

        Returns:
            BPF filter string for tcpdump
        """
        filters = []

        if self.config.privacy_type == PrivacyType.TOR:
            # For Tor: capture traffic not going to localhost
            # (legitimate Tor traffic goes through local SOCKS/transparent proxy)
            filters.append("not host 127.0.0.1")
            filters.append("not host ::1")

            # Exclude allowed local networks
            for net in self.config.allowed_networks:
                filters.append(f"not net {net}")

            # DNS leak detection - capture any DNS not to Tor DNS
            # (handled in packet analysis)

        elif self.config.privacy_type in (PrivacyType.VPN, PrivacyType.WIREGUARD):
            # For VPN: capture traffic not going through tunnel
            # We monitor the physical interface, not the tunnel interface

            # Exclude VPN server traffic (that's expected)
            for ip in self.config.allowed_ips:
                filters.append(f"not host {ip}")

            # Exclude VPN ports
            for port in self.config.allowed_ports:
                filters.append(f"not port {port}")

            # Exclude local networks
            for net in self.config.allowed_networks:
                filters.append(f"not net {net}")

        # IPv6 monitoring
        if self.config.check_ipv6:
            # IPv6 is often a leak vector - capture it separately
            pass  # We'll detect IPv6 in packet analysis

        return " and ".join(filters) if filters else ""

    def _analyze_packet(self, packet_line: str) -> LeakEvent | None:
        """Analyze a tcpdump packet line for leaks.

        Args:
            packet_line: Output line from tcpdump

        Returns:
            LeakEvent if leak detected, None otherwise
        """
        # Parse tcpdump output
        # Format: timestamp IP src.port > dst.port: details
        # Or: timestamp IP6 src > dst: details

        timestamp = datetime.now()
        leak_type = None
        source_ip = None
        dest_ip = None
        dest_port = None
        protocol = None
        details = None

        # Check for IPv6 traffic
        if " IP6 " in packet_line:
            leak_type = LeakType.IPV6
            protocol = "IPv6"
            details = "IPv6 traffic detected (potential leak)"

        # Check for DNS queries
        if ".53:" in packet_line or " > .53:" in packet_line:
            # Extract destination
            match = re.search(r">\s+(\S+)\.53:", packet_line)
            if match:
                dest_ip = match.group(1)
                # Check if this DNS is going outside the tunnel
                if self.config.privacy_type == PrivacyType.TOR:
                    if dest_ip not in ("127.0.0.1", "127.0.0.53"):
                        leak_type = LeakType.DNS
                        dest_port = 53
                        protocol = "UDP"
                        details = f"DNS query to {dest_ip} bypassing Tor"

        # Parse source/destination for IP traffic
        ip_match = re.search(
            r"IP\s+(\d+\.\d+\.\d+\.\d+)(?:\.(\d+))?\s+>\s+(\d+\.\d+\.\d+\.\d+)(?:\.(\d+))?",
            packet_line
        )
        if ip_match:
            source_ip = ip_match.group(1)
            dest_ip = ip_match.group(3)
            if ip_match.group(4):
                dest_port = int(ip_match.group(4))

            # Check for direct connections (not through tunnel)
            if leak_type is None and dest_ip:
                # Check if destination is in allowed networks
                is_allowed = False
                for net in self.config.allowed_networks:
                    if self._ip_in_network(dest_ip, net):
                        is_allowed = True
                        break

                if not is_allowed and dest_ip not in self.config.allowed_ips:
                    leak_type = LeakType.DIRECT_IP
                    protocol = "TCP" if "Flags" in packet_line else "UDP"
                    details = f"Direct connection to {dest_ip}:{dest_port}"

        if leak_type:
            return LeakEvent(
                timestamp=timestamp,
                container=self.container_name,
                leak_type=leak_type,
                source_ip=source_ip,
                dest_ip=dest_ip,
                dest_port=dest_port,
                protocol=protocol,
                details=details,
                raw_packet=packet_line[:500],  # Truncate long packets
            )

        return None

    def _ip_in_network(self, ip: str, network: str) -> bool:
        """Check if an IP is in a network.

        Args:
            ip: IP address to check
            network: Network in CIDR notation

        Returns:
            True if IP is in network
        """
        try:
            import ipaddress
            return ipaddress.ip_address(ip) in ipaddress.ip_network(network, strict=False)
        except (ValueError, ImportError):
            return False

    def _handle_leak(self, event: LeakEvent) -> None:
        """Handle a detected leak event.

        Args:
            event: The leak event
        """
        with self._lock:
            self._leaks.append(event)

        # Log to file if configured
        if self.config.log_file:
            try:
                with open(self.config.log_file, "a") as f:
                    f.write(event.to_json() + "\n")
            except OSError:
                pass

        # Call alert callback if configured
        if self.config.alert_callback:
            try:
                self.config.alert_callback(event)
            except Exception:
                pass

    def _monitor_loop(self) -> None:
        """Main monitoring loop using tcpdump."""
        interface = self._get_container_interface()
        if not interface:
            # Fall back to monitoring from inside container's netns
            pid = self._get_container_pid()
            if not pid:
                return
            cmd = [
                "sudo", "nsenter", "-t", str(pid), "-n",
                "tcpdump", "-l", "-n", "-q",
            ]
        else:
            cmd = ["sudo", "tcpdump", "-i", interface, "-l", "-n", "-q"]

        bpf_filter = self._build_tcpdump_filter()
        if bpf_filter:
            cmd.extend(bpf_filter.split())

        try:
            self._tcpdump_process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.DEVNULL,
                text=True,
            )

            while self._running and self._tcpdump_process.poll() is None:
                line = self._tcpdump_process.stdout.readline()
                if not line:
                    continue

                event = self._analyze_packet(line.strip())
                if event:
                    self._handle_leak(event)

        except Exception:
            pass
        finally:
            if self._tcpdump_process:
                self._tcpdump_process.terminate()
                self._tcpdump_process = None

    def start(self) -> None:
        """Start monitoring for leaks."""
        if self._running:
            return

        self._running = True
        self._monitor_thread = threading.Thread(
            target=self._monitor_loop,
            daemon=True,
        )
        self._monitor_thread.start()

    def stop(self) -> None:
        """Stop monitoring."""
        self._running = False

        if self._tcpdump_process:
            try:
                self._tcpdump_process.terminate()
                self._tcpdump_process.wait(timeout=2)
            except subprocess.TimeoutExpired:
                self._tcpdump_process.kill()
            self._tcpdump_process = None

        if self._monitor_thread:
            self._monitor_thread.join(timeout=2)
            self._monitor_thread = None

    def run_leak_test(self) -> list[LeakEvent]:
        """Run active leak tests against the container.

        Performs several tests:
        1. DNS leak test - tries to resolve a domain
        2. Direct IP test - tries to connect to a known IP
        3. IPv6 leak test - checks for IPv6 connectivity

        Returns:
            List of leak events detected during testing
        """
        events = []
        pid = self._get_container_pid()
        if not pid:
            return events

        def run_in_container(cmd: list[str]) -> tuple[int, str, str]:
            """Run a command inside the container."""
            full_cmd = ["sudo", "nsenter", "-t", str(pid), "-n", "-m", "-u", "-i", "-p", "--"]
            full_cmd.extend(cmd)
            result = subprocess.run(
                full_cmd,
                capture_output=True,
                text=True,
                timeout=10,
            )
            return result.returncode, result.stdout, result.stderr

        # Test 1: DNS leak - try to use external DNS
        if self.config.privacy_type == PrivacyType.TOR:
            try:
                rc, stdout, stderr = run_in_container([
                    "dig", "+short", "@8.8.8.8", "whoami.akamai.net"
                ])
                if rc == 0 and stdout.strip():
                    events.append(LeakEvent(
                        timestamp=datetime.now(),
                        container=self.container_name,
                        leak_type=LeakType.DNS,
                        dest_ip="8.8.8.8",
                        dest_port=53,
                        protocol="UDP",
                        details=f"DNS query succeeded to 8.8.8.8: {stdout.strip()}",
                    ))
            except (subprocess.TimeoutExpired, Exception):
                pass  # Blocked as expected

        # Test 2: Direct IP connection
        try:
            # Try to connect directly to a known IP (should be blocked)
            rc, stdout, stderr = run_in_container([
                "curl", "-s", "--connect-timeout", "5",
                "--socks5", "", "http://1.1.1.1/cdn-cgi/trace"
            ])
            if rc == 0:
                events.append(LeakEvent(
                    timestamp=datetime.now(),
                    container=self.container_name,
                    leak_type=LeakType.DIRECT_IP,
                    dest_ip="1.1.1.1",
                    dest_port=80,
                    protocol="TCP",
                    details="Direct HTTP connection succeeded",
                ))
        except (subprocess.TimeoutExpired, Exception):
            pass

        # Test 3: IPv6 leak
        if self.config.check_ipv6:
            try:
                rc, stdout, stderr = run_in_container([
                    "curl", "-6", "-s", "--connect-timeout", "5",
                    "http://ipv6.icanhazip.com/"
                ])
                if rc == 0 and stdout.strip():
                    events.append(LeakEvent(
                        timestamp=datetime.now(),
                        container=self.container_name,
                        leak_type=LeakType.IPV6,
                        protocol="IPv6",
                        details=f"IPv6 connectivity detected: {stdout.strip()}",
                    ))
            except (subprocess.TimeoutExpired, Exception):
                pass

        return events

    def check_iptables_rules(self) -> dict:
        """Check if proper iptables rules are in place.

        Returns:
            Dict with rule status information
        """
        pid = self._get_container_pid()
        if not pid:
            return {"error": "Container not running"}

        result = {
            "has_tor_redirect": False,
            "has_dns_block": False,
            "has_kill_switch": False,
            "has_logging": False,
            "rules": [],
        }

        try:
            # Get iptables rules from container
            cmd = ["sudo", "nsenter", "-t", str(pid), "-n", "iptables-save"]
            proc = subprocess.run(cmd, capture_output=True, text=True)

            if proc.returncode == 0:
                rules = proc.stdout

                # Check for Tor transparent proxy rules
                if "REDIRECT" in rules and "9040" in rules:
                    result["has_tor_redirect"] = True

                # Check for DNS redirection/blocking
                if "53" in rules and ("REDIRECT" in rules or "DROP" in rules):
                    result["has_dns_block"] = True

                # Check for kill switch (DROP as default)
                if "-j REJECT" in rules or "-j DROP" in rules:
                    result["has_kill_switch"] = True

                # Check for logging rules
                if "-j LOG" in rules:
                    result["has_logging"] = True

                result["rules"] = [
                    line.strip() for line in rules.split("\n")
                    if line.strip() and not line.startswith("#") and not line.startswith("*")
                ]

        except Exception as e:
            result["error"] = str(e)

        return result

    def get_summary(self) -> dict:
        """Get a summary of monitoring status.

        Returns:
            Dict with monitoring summary
        """
        return {
            "container": self.container_name,
            "privacy_type": self.config.privacy_type.value,
            "is_running": self._running,
            "total_leaks": len(self._leaks),
            "leaks_by_type": {
                lt.value: sum(1 for l in self._leaks if l.leak_type == lt)
                for lt in LeakType
            },
            "recent_leaks": [l.to_dict() for l in self._leaks[-10:]],
        }


def create_tor_monitor(
    container_name: str,
    log_file: Path | None = None,
    alert_callback: Callable[[LeakEvent], None] | None = None,
) -> LeakMonitor:
    """Create a leak monitor configured for Tor containers.

    Args:
        container_name: Name of the container
        log_file: Optional log file path
        alert_callback: Optional callback for alerts

    Returns:
        Configured LeakMonitor
    """
    config = MonitorConfig(
        privacy_type=PrivacyType.TOR,
        tor_user="debian-tor",
        tor_socks_port=9050,
        tor_trans_port=9040,
        tor_dns_port=5353,
        log_file=log_file,
        alert_callback=alert_callback,
    )
    return LeakMonitor(container_name, config)


def create_vpn_monitor(
    container_name: str,
    vpn_server_ip: str,
    vpn_port: int = 1194,
    interface: str = "tun0",
    log_file: Path | None = None,
    alert_callback: Callable[[LeakEvent], None] | None = None,
) -> LeakMonitor:
    """Create a leak monitor configured for VPN containers.

    Args:
        container_name: Name of the container
        vpn_server_ip: IP address of VPN server
        vpn_port: VPN port (default OpenVPN port)
        interface: VPN interface name
        log_file: Optional log file path
        alert_callback: Optional callback for alerts

    Returns:
        Configured LeakMonitor
    """
    config = MonitorConfig(
        privacy_type=PrivacyType.VPN,
        allowed_ips=[vpn_server_ip],
        allowed_ports=[vpn_port],
        vpn_interface=interface,
        log_file=log_file,
        alert_callback=alert_callback,
    )
    return LeakMonitor(container_name, config)


def create_wireguard_monitor(
    container_name: str,
    wg_endpoint: str,
    wg_port: int = 51820,
    interface: str = "wg0",
    log_file: Path | None = None,
    alert_callback: Callable[[LeakEvent], None] | None = None,
) -> LeakMonitor:
    """Create a leak monitor configured for WireGuard containers.

    Args:
        container_name: Name of the container
        wg_endpoint: WireGuard endpoint IP
        wg_port: WireGuard port
        interface: WireGuard interface name
        log_file: Optional log file path
        alert_callback: Optional callback for alerts

    Returns:
        Configured LeakMonitor
    """
    config = MonitorConfig(
        privacy_type=PrivacyType.WIREGUARD,
        allowed_ips=[wg_endpoint],
        allowed_ports=[wg_port],
        wg_interface=interface,
        log_file=log_file,
        alert_callback=alert_callback,
    )
    return LeakMonitor(container_name, config)
