"""VPN gateway manager.

This module provides the VPNGatewayManager for managing VPN connections
that route container traffic through VPN tunnels.
"""

from __future__ import annotations

import os
import shutil
import subprocess
import time
from datetime import datetime
from pathlib import Path

import yaml

from gaol.privacy.exceptions import (
    VPNConfigError,
    VPNConnectionError,
    VPNGatewayAlreadyExistsError,
    VPNGatewayNotFoundError,
)
from gaol.privacy.firewall import PrivacyFirewall
from gaol.privacy.models import (
    PrivacyStatus,
    VPNGatewayConfig,
    VPNGatewayState,
    VPNProvider,
)


class VPNGatewayManager:
    """Manage VPN gateway connections.

    Handles starting, stopping, and tracking VPN connections
    that route container traffic through VPN tunnels.

    Supports:
    - OpenVPN
    - WireGuard

    Example:
        >>> manager = VPNGatewayManager()
        >>> config = VPNGatewayConfig(
        ...     name="myvpn",
        ...     provider=VPNProvider.OPENVPN,
        ...     config_file=Path("~/vpn/client.ovpn"),
        ... )
        >>> gateway = manager.create(config)
        >>> manager.delete("myvpn")
    """

    def __init__(self, state_dir: Path | None = None) -> None:
        """Initialize the VPN gateway manager.

        Args:
            state_dir: Directory for state files. Defaults to
                       ~/.local/state/gaol/vpn
        """
        if state_dir is None:
            state_dir = Path.home() / ".local" / "state" / "gaol" / "vpn"
        self.state_dir = state_dir
        self.state_dir.mkdir(parents=True, exist_ok=True)
        self._store_path = self.state_dir / "gateways.yaml"
        self._firewall = PrivacyFirewall()

    def create(self, config: VPNGatewayConfig) -> VPNGatewayState:
        """Create and start a VPN gateway.

        Args:
            config: Gateway configuration.

        Returns:
            VPNGatewayState with connection information.

        Raises:
            VPNGatewayAlreadyExistsError: If gateway with name already exists.
            VPNConfigError: If configuration is invalid.
            VPNConnectionError: If connection fails.
        """
        # Check for existing
        existing = self.get(config.name)
        if existing and existing.status == PrivacyStatus.RUNNING:
            raise VPNGatewayAlreadyExistsError(config.name)

        # Clean up stale state
        if existing:
            self._remove_state(config.name)

        # Validate config file
        config_path = Path(config.config_file).expanduser()
        if not config_path.exists():
            raise VPNConfigError(f"Config file not found: {config_path}")

        # Check for required tools
        if config.provider == VPNProvider.OPENVPN:
            if not shutil.which("openvpn"):
                raise VPNConfigError("openvpn not found. Install with: apt install openvpn")
        elif config.provider == VPNProvider.WIREGUARD:
            if not shutil.which("wg-quick"):
                raise VPNConfigError("wg-quick not found. Install with: apt install wireguard-tools")

        # Create state
        state = VPNGatewayState(
            name=config.name,
            provider=config.provider,
            status=PrivacyStatus.STARTING,
            config_file=config_path,
            connected_at=datetime.now(),
        )

        # Start VPN
        try:
            if config.provider == VPNProvider.OPENVPN:
                self._start_openvpn(config, state)
            else:
                self._start_wireguard(config, state)
        except Exception as e:
            state.status = PrivacyStatus.ERROR
            self._save_state(state)
            raise VPNConnectionError(str(e)) from e

        # Set up kill switch if enabled
        if config.kill_switch and state.interface:
            try:
                self._firewall.setup_kill_switch(state.interface)
            except Exception:
                pass  # Don't fail if firewall setup fails

        self._save_state(state)
        return state

    def _start_openvpn(self, config: VPNGatewayConfig, state: VPNGatewayState) -> None:
        """Start OpenVPN connection.

        Args:
            config: Gateway configuration.
            state: State to update.
        """
        # Create working directory
        work_dir = self.state_dir / config.name
        work_dir.mkdir(parents=True, exist_ok=True)

        # Copy config to working directory
        config_path = Path(config.config_file).expanduser()
        local_config = work_dir / "client.ovpn"
        shutil.copy(config_path, local_config)

        # Create log file
        log_file = work_dir / "openvpn.log"

        # Start OpenVPN in daemon mode
        cmd = [
            "sudo", "openvpn",
            "--config", str(local_config),
            "--daemon",
            "--log", str(log_file),
            "--writepid", str(work_dir / "openvpn.pid"),
            "--dev", f"tun-{config.name[:8]}",
        ]

        # Add DNS options
        if config.dns_servers:
            for dns in config.dns_servers:
                cmd.extend(["--dhcp-option", f"DNS {dns}"])

        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            raise VPNConnectionError(f"OpenVPN failed: {result.stderr}")

        # Wait for connection
        time.sleep(3)

        # Read PID
        pid_file = work_dir / "openvpn.pid"
        if pid_file.exists():
            state.pid = int(pid_file.read_text().strip())

        # Get interface name
        state.interface = f"tun-{config.name[:8]}"

        # Check if process is running
        if state.pid and self._is_process_running(state.pid):
            state.status = PrivacyStatus.RUNNING
        else:
            state.status = PrivacyStatus.ERROR

    def _start_wireguard(self, config: VPNGatewayConfig, state: VPNGatewayState) -> None:
        """Start WireGuard connection.

        Args:
            config: Gateway configuration.
            state: State to update.
        """
        config_path = Path(config.config_file).expanduser()

        # Use wg-quick to bring up interface
        # Interface name comes from config file name
        interface_name = config_path.stem

        cmd = ["sudo", "wg-quick", "up", str(config_path)]
        result = subprocess.run(cmd, capture_output=True, text=True)

        if result.returncode != 0:
            raise VPNConnectionError(f"wg-quick failed: {result.stderr}")

        state.interface = interface_name
        state.status = PrivacyStatus.RUNNING

    def delete(self, name: str) -> None:
        """Stop and remove a VPN gateway.

        Args:
            name: Name of the gateway to delete.

        Raises:
            VPNGatewayNotFoundError: If gateway is not found.
        """
        state = self.get(name)
        if not state:
            raise VPNGatewayNotFoundError(name)

        # Stop the connection
        if state.provider == VPNProvider.OPENVPN:
            self._stop_openvpn(state)
        else:
            self._stop_wireguard(state)

        # Clean up firewall rules
        self._firewall.cleanup()

        # Clean up working directory
        work_dir = self.state_dir / name
        if work_dir.exists():
            shutil.rmtree(work_dir)

        # Remove state
        self._remove_state(name)

    def _stop_openvpn(self, state: VPNGatewayState) -> None:
        """Stop OpenVPN connection.

        Args:
            state: Gateway state.
        """
        if state.pid:
            try:
                os.kill(state.pid, 15)  # SIGTERM
                time.sleep(1)
                # Force kill if still running
                if self._is_process_running(state.pid):
                    os.kill(state.pid, 9)  # SIGKILL
            except (ProcessLookupError, PermissionError):
                pass

    def _stop_wireguard(self, state: VPNGatewayState) -> None:
        """Stop WireGuard connection.

        Args:
            state: Gateway state.
        """
        if state.interface:
            cmd = ["sudo", "wg-quick", "down", state.interface]
            subprocess.run(cmd, capture_output=True, text=True)

    def get(self, name: str) -> VPNGatewayState | None:
        """Get a gateway by name.

        Args:
            name: Gateway name.

        Returns:
            VPNGatewayState if found, None otherwise.
        """
        states = self._load_states()
        for state in states:
            if state.name == name:
                # Update status
                self._update_status(state)
                return state
        return None

    def list(self) -> list[VPNGatewayState]:
        """List all VPN gateways.

        Returns:
            List of gateway states.
        """
        states = self._load_states()

        # Update statuses
        for state in states:
            self._update_status(state)

        return states

    def status(self, name: str) -> VPNGatewayState | None:
        """Get detailed status of a gateway.

        Args:
            name: Gateway name.

        Returns:
            VPNGatewayState with current status, None if not found.
        """
        state = self.get(name)
        if not state:
            return None

        # Get public IP if running
        if state.status == PrivacyStatus.RUNNING:
            state.public_ip = self._get_public_ip()

        return state

    def _update_status(self, state: VPNGatewayState) -> None:
        """Update gateway status.

        Args:
            state: State to update.
        """
        if state.provider == VPNProvider.OPENVPN:
            if state.pid and self._is_process_running(state.pid):
                state.status = PrivacyStatus.RUNNING
            else:
                state.status = PrivacyStatus.STOPPED
        elif state.provider == VPNProvider.WIREGUARD:
            if state.interface and self._is_interface_up(state.interface):
                state.status = PrivacyStatus.RUNNING
            else:
                state.status = PrivacyStatus.STOPPED

    def _is_process_running(self, pid: int) -> bool:
        """Check if a process is running.

        Args:
            pid: Process ID.

        Returns:
            True if process is running.
        """
        try:
            os.kill(pid, 0)
            return True
        except (ProcessLookupError, PermissionError):
            return False

    def _is_interface_up(self, interface: str) -> bool:
        """Check if a network interface is up.

        Args:
            interface: Interface name.

        Returns:
            True if interface is up.
        """
        try:
            result = subprocess.run(
                ["ip", "link", "show", interface],
                capture_output=True,
                text=True,
            )
            return "UP" in result.stdout
        except Exception:
            return False

    def _get_public_ip(self) -> str | None:
        """Get current public IP address.

        Returns:
            Public IP address or None.
        """
        try:
            result = subprocess.run(
                ["curl", "-s", "https://api.ipify.org"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            if result.returncode == 0:
                return result.stdout.strip()
        except Exception:
            pass
        return None

    def _save_state(self, state: VPNGatewayState) -> None:
        """Save gateway state.

        Args:
            state: State to save.
        """
        states = self._load_states()
        states = [s for s in states if s.name != state.name]
        states.append(state)
        self._write_store(states)

    def _remove_state(self, name: str) -> None:
        """Remove gateway state.

        Args:
            name: Name of gateway to remove.
        """
        states = self._load_states()
        states = [s for s in states if s.name != name]
        self._write_store(states)

    def _load_states(self) -> list[VPNGatewayState]:
        """Load gateway states from store.

        Returns:
            List of gateway states.
        """
        if not self._store_path.exists():
            return []

        try:
            with open(self._store_path) as f:
                data = yaml.safe_load(f)
            if not data or "gateways" not in data:
                return []
            return [VPNGatewayState.model_validate(g) for g in data["gateways"]]
        except Exception:
            return []

    def _write_store(self, states: list[VPNGatewayState]) -> None:
        """Write gateway states to store.

        Args:
            states: List of states to write.
        """
        self._store_path.parent.mkdir(parents=True, exist_ok=True)
        data = {"gateways": [s.model_dump(mode="json") for s in states]}
        with open(self._store_path, "w") as f:
            yaml.safe_dump(data, f, default_flow_style=False)
