"""Service container manager.

Manages security domain-style service containers (sys-tor, sys-vpn, sys-firewall)
that provide network services to other containers.
"""

from __future__ import annotations

import subprocess
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING

from gaol.domains.exceptions import DomainsError
from gaol.domains.models import (
    ClientAttachment,
    ServiceContainerConfig,
    ServiceContainerState,
    ServiceContainerType,
)
from gaol.domains.store import DomainsFileStore, get_domain_store

if TYPE_CHECKING:
    pass


# =============================================================================
# Exceptions
# =============================================================================


class ServiceContainerError(DomainsError):
    """Base exception for service container operations."""


class ServiceExistsError(ServiceContainerError):
    """Service container already exists."""

    def __init__(self, name: str) -> None:
        self.name = name
        super().__init__(f"Service container already exists: {name}")


class ServiceNotFoundError(ServiceContainerError):
    """Service container not found."""

    def __init__(self, name: str) -> None:
        self.name = name
        super().__init__(f"Service container not found: {name}")


class ServiceStartError(ServiceContainerError):
    """Failed to start service container."""

    def __init__(self, name: str, reason: str) -> None:
        self.name = name
        self.reason = reason
        super().__init__(f"Failed to start service '{name}': {reason}")


class AttachmentError(ServiceContainerError):
    """Failed to attach client to service."""

    def __init__(self, client: str, service: str, reason: str) -> None:
        self.client = client
        self.service = service
        self.reason = reason
        super().__init__(f"Failed to attach '{client}' to '{service}': {reason}")


# =============================================================================
# Service Container Manager
# =============================================================================


class ServiceContainerManager:
    """Manage service containers (sys-tor, sys-vpn, sys-firewall).

    Service containers provide network services to other containers,
    similar to security domain sys-net, sys-firewall, sys-whonix.

    Example:
        >>> manager = ServiceContainerManager()
        >>> config = ServiceContainerConfig(
        ...     name="sys-tor",
        ...     service_type=ServiceContainerType.TOR_GATEWAY,
        ... )
        >>> manager.create(config)
        >>> manager.start("sys-tor")
        >>> manager.attach("my-browser", "sys-tor")
    """

    def __init__(self, store: DomainsFileStore | None = None) -> None:
        """Initialize the service container manager."""
        self._store = store or get_domain_store()

    def _run_cmd(
        self,
        cmd: list[str],
        *,
        check: bool = True,
        capture_output: bool = True,
    ) -> subprocess.CompletedProcess[str]:
        """Run a command and return the result."""
        try:
            result = subprocess.run(
                cmd,
                check=check,
                capture_output=capture_output,
                text=True,
            )
            return result
        except subprocess.CalledProcessError as e:
            raise RuntimeError(f"Command failed: {' '.join(cmd)}\n{e.stderr}") from e

    def create(self, config: ServiceContainerConfig) -> ServiceContainerState:
        """Create a new service container.

        Args:
            config: Service container configuration.

        Returns:
            New service container state.

        Raises:
            ServiceExistsError: If service already exists.
        """
        if self._store.service_exists(config.name):
            raise ServiceExistsError(config.name)

        # Create the underlying nspawn container
        container_name = f"svc-{config.name}"

        try:
            self._create_container(container_name, config)
            self._install_service_packages(container_name, config)
            self._configure_service(container_name, config)

            state = ServiceContainerState(
                name=config.name,
                service_type=config.service_type,
                container_name=container_name,
                status="stopped",
                created_at=datetime.now(),
            )

            self._store.save_service(state)
            return state

        except Exception as e:
            # Clean up on failure
            self._cleanup_container(container_name)
            raise ServiceContainerError(f"Failed to create service: {e}") from e

    def _create_container(
        self, container_name: str, config: ServiceContainerConfig
    ) -> None:
        """Create the underlying nspawn container."""
        from gaol.models.container import ContainerConfig, NetworkConfig, NetworkMode
        from gaol.runtimes.nspawn import NspawnRuntime

        runtime = NspawnRuntime()

        # Service containers need network access via bridged networking
        # This connects to br0 bridge like existing mullvad/tor containers
        container_config = ContainerConfig(
            name=container_name,
            runtime="nspawn",
            distro=config.distro,
            network=NetworkConfig(mode=NetworkMode.BRIDGED),
            private_users=False,  # Need root for network configuration
        )

        runtime.create(container_config)

    def _setup_container_dns(self, container_name: str) -> None:
        """Set up DNS resolution in a container.

        When using systemd-nspawn -D without boot mode, the container
        doesn't have access to the host's systemd-resolved stub resolver.
        This method configures DNS to use public resolvers.

        systemd-resolved's postinst script converts /etc/resolv.conf to a
        symlink pointing to /run/systemd/resolve/stub-resolv.conf. Since
        systemd-nspawn mounts /run as a fresh tmpfs, the symlink target
        doesn't exist. We must:
        1. Remove the symlink
        2. Create a real /etc/resolv.conf file with public DNS servers
        """
        machine_path = Path(f"/var/lib/machines/{container_name}")
        resolv_conf = machine_path / "etc" / "resolv.conf"

        # DNS content with public resolvers
        dns_content = "nameserver 8.8.8.8\nnameserver 1.1.1.1\n"

        # Always remove existing file/symlink and create a real file
        # This handles both symlinks (from systemd-resolved) and regular files
        self._run_cmd(["sudo", "rm", "-f", str(resolv_conf)], check=False)
        subprocess.run(
            ["sudo", "tee", str(resolv_conf)],
            input=dns_content,
            text=True,
            capture_output=True,
        )

    def _install_service_packages(
        self, container_name: str, config: ServiceContainerConfig
    ) -> None:
        """Install required packages for the service type."""
        packages = ["iptables", "iproute2", "curl", "dnsutils"]

        if config.service_type == ServiceContainerType.TOR_GATEWAY:
            packages.extend(["tor", "torsocks"])
        elif config.service_type == ServiceContainerType.VPN_GATEWAY:
            if config.vpn_provider == "wireguard":
                packages.extend(["wireguard-tools"])
            else:
                packages.extend(["openvpn"])
        elif config.service_type == ServiceContainerType.FIREWALL:
            packages.extend(["nftables"])

        # Ensure DNS is configured before running apt-get
        self._setup_container_dns(container_name)

        # Temporarily move the nspawn config to use host networking during setup
        # The nspawn config sets up bridged networking which requires DHCP, but
        # systemd-nspawn -D mode doesn't run systemd services like networkd
        nspawn_config = Path(f"/etc/systemd/nspawn/{container_name}.nspawn")
        nspawn_backup = Path(f"/tmp/{container_name}.nspawn.bak")
        if nspawn_config.exists():
            self._run_cmd(["sudo", "mv", str(nspawn_config), str(nspawn_backup)], check=False)

        try:
            # Install packages using host networking
            machine_path = Path(f"/var/lib/machines/{container_name}")
            self._run_cmd(
                [
                    "sudo", "systemd-nspawn", "-D", str(machine_path),
                    "--resolv-conf=off",
                    "apt-get", "update",
                ],
                capture_output=False,
            )
            self._run_cmd(
                [
                    "sudo", "systemd-nspawn", "-D", str(machine_path),
                    "--resolv-conf=off",
                    "env", "DEBIAN_FRONTEND=noninteractive",
                    "apt-get", "install", "-y", *packages,
                ],
                capture_output=False,
            )
        finally:
            # Restore the nspawn config for bridged networking when container runs
            if nspawn_backup.exists():
                self._run_cmd(["sudo", "mv", str(nspawn_backup), str(nspawn_config)], check=False)

    def _configure_service(
        self, container_name: str, config: ServiceContainerConfig
    ) -> None:
        """Configure the service inside the container."""
        machine_path = Path(f"/var/lib/machines/{container_name}")

        if config.service_type == ServiceContainerType.TOR_GATEWAY:
            self._configure_tor(machine_path, config)
        elif config.service_type == ServiceContainerType.VPN_GATEWAY:
            self._configure_vpn(machine_path, config)
        elif config.service_type == ServiceContainerType.FIREWALL:
            self._configure_firewall(machine_path, config)

    def _configure_tor(self, machine_path: Path, config: ServiceContainerConfig) -> None:
        """Configure Tor service."""
        torrc_content = """# Gaol Tor Gateway Configuration
SocksPort 0.0.0.0:9050
DNSPort 0.0.0.0:5353
TransPort 0.0.0.0:9040

# Safety settings
SafeLogging 1
AvoidDiskWrites 1
"""
        if config.tor_exit_nodes:
            torrc_content += f"ExitNodes {{{','.join(config.tor_exit_nodes)}}}\n"
        if config.tor_exclude_nodes:
            torrc_content += f"ExcludeNodes {{{','.join(config.tor_exclude_nodes)}}}\n"
        if config.tor_bridges:
            torrc_content += "UseBridges 1\n"
            for bridge in config.tor_bridges:
                torrc_content += f"Bridge {bridge}\n"

        torrc_path = machine_path / "etc" / "tor" / "torrc"
        torrc_path.parent.mkdir(parents=True, exist_ok=True)

        # Write via sudo
        subprocess.run(
            ["sudo", "tee", str(torrc_path)],
            input=torrc_content,
            text=True,
            capture_output=True,
        )

    def _configure_vpn(self, machine_path: Path, config: ServiceContainerConfig) -> None:
        """Configure VPN service."""
        if not config.vpn_config_file:
            return

        # Copy VPN config file into container
        vpn_config_path = Path(config.vpn_config_file)
        if vpn_config_path.exists():
            dest_dir = machine_path / "etc" / "wireguard"
            if config.vpn_provider == "openvpn":
                dest_dir = machine_path / "etc" / "openvpn"

            self._run_cmd(["sudo", "mkdir", "-p", str(dest_dir)])
            self._run_cmd([
                "sudo", "cp", str(vpn_config_path),
                str(dest_dir / vpn_config_path.name),
            ])

    def _configure_firewall(
        self, machine_path: Path, _config: ServiceContainerConfig
    ) -> None:
        """Configure firewall service."""
        # Create nftables config
        nft_content = """#!/usr/sbin/nft -f
# Gaol Firewall Gateway Configuration

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        iif lo accept
        ct state established,related accept
        ct state invalid drop
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
        ct state established,related accept
        ct state invalid drop
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}
"""
        nft_path = machine_path / "etc" / "nftables.conf"
        subprocess.run(
            ["sudo", "tee", str(nft_path)],
            input=nft_content,
            text=True,
            capture_output=True,
        )

    def _cleanup_container(self, container_name: str) -> None:
        """Clean up a failed container creation."""
        machine_path = Path(f"/var/lib/machines/{container_name}")
        if machine_path.exists():
            self._run_cmd(["sudo", "rm", "-rf", str(machine_path)], check=False)

        nspawn_path = Path(f"/etc/systemd/nspawn/{container_name}.nspawn")
        if nspawn_path.exists():
            self._run_cmd(["sudo", "rm", str(nspawn_path)], check=False)

    def start(self, name: str) -> ServiceContainerState:
        """Start a service container.

        Args:
            name: Service container name.

        Returns:
            Updated service state.

        Raises:
            ServiceNotFoundError: If service doesn't exist.
            ServiceStartError: If start fails.
        """
        state = self._store.get_service(name)
        if not state:
            raise ServiceNotFoundError(name)

        try:
            # Start the container
            self._run_cmd(["sudo", "machinectl", "start", state.container_name])

            # Wait for it to be running
            self._wait_for_container(state.container_name)

            # Start the specific service
            self._start_service_daemon(state)

            state.status = "running"
            state.started_at = datetime.now()

            # Get public IP for VPN/Tor
            if state.service_type in (
                ServiceContainerType.TOR_GATEWAY,
                ServiceContainerType.VPN_GATEWAY,
            ):
                state.public_ip = self._get_public_ip(state.container_name)

            self._store.save_service(state)
            return state

        except Exception as e:
            state.status = "error"
            state.error_message = str(e)
            self._store.save_service(state)
            raise ServiceStartError(name, str(e)) from e

    def _wait_for_container(self, container_name: str, timeout: int = 30) -> None:
        """Wait for a container to be running."""
        import time

        start = time.time()
        while time.time() - start < timeout:
            result = self._run_cmd(
                ["sudo", "machinectl", "show", container_name, "--property=State"],
                check=False,
            )
            if result.returncode == 0 and "running" in result.stdout.lower():
                return
            time.sleep(0.5)

        raise RuntimeError(f"Container {container_name} failed to start")

    def _start_service_daemon(self, state: ServiceContainerState) -> None:
        """Start the appropriate daemon inside the container."""
        if state.service_type == ServiceContainerType.TOR_GATEWAY:
            self._run_cmd([
                "sudo", "machinectl", "shell", f"root@{state.container_name}",
                "--", "systemctl", "start", "tor",
            ])
            # Wait for bootstrap
            self._wait_for_tor_bootstrap(state)

        elif state.service_type == ServiceContainerType.VPN_GATEWAY:
            # Start WireGuard or OpenVPN
            # The config should be in place from _configure_vpn
            self._run_cmd([
                "sudo", "machinectl", "shell", f"root@{state.container_name}",
                "--", "wg-quick", "up", "wg0",
            ], check=False)

    def _wait_for_tor_bootstrap(
        self, state: ServiceContainerState, timeout: int = 120
    ) -> None:
        """Wait for Tor to bootstrap."""
        import time

        start = time.time()
        while time.time() - start < timeout:
            result = self._run_cmd(
                [
                    "sudo", "machinectl", "shell", f"root@{state.container_name}",
                    "--", "bash", "-c",
                    "echo 'GETINFO status/bootstrap-phase' | nc localhost 9051 2>/dev/null || echo 'not ready'",
                ],
                check=False,
            )
            if "BOOTSTRAP PROGRESS=100" in result.stdout:
                state.tor_bootstrap_progress = 100
                return

            # Parse progress
            if "PROGRESS=" in result.stdout:
                import re
                match = re.search(r"PROGRESS=(\d+)", result.stdout)
                if match:
                    state.tor_bootstrap_progress = int(match.group(1))

            time.sleep(2)

        raise RuntimeError("Tor bootstrap timeout")

    def _get_public_ip(self, container_name: str) -> str | None:
        """Get the public IP address from inside the container."""
        result = self._run_cmd(
            [
                "sudo", "machinectl", "shell", f"root@{container_name}",
                "--", "curl", "-s", "--max-time", "10", "https://api.ipify.org",
            ],
            check=False,
        )
        if result.returncode == 0 and result.stdout.strip():
            return result.stdout.strip()
        return None

    def stop(self, name: str) -> ServiceContainerState:
        """Stop a service container.

        Args:
            name: Service container name.

        Returns:
            Updated service state.
        """
        state = self._store.get_service(name)
        if not state:
            raise ServiceNotFoundError(name)

        self._run_cmd(
            ["sudo", "machinectl", "stop", state.container_name],
            check=False,
        )

        state.status = "stopped"
        state.public_ip = None
        self._store.save_service(state)

        return state

    def delete(self, name: str, force: bool = False) -> None:
        """Delete a service container.

        Args:
            name: Service container name.
            force: Force deletion even if clients are attached.

        Raises:
            ServiceNotFoundError: If service doesn't exist.
        """
        state = self._store.get_service(name)
        if not state:
            raise ServiceNotFoundError(name)

        # Check for attached clients
        if state.attached_clients and not force:
            raise ServiceContainerError(
                f"Service '{name}' has {len(state.attached_clients)} attached clients. "
                "Use --force to delete anyway."
            )

        # Detach all clients
        import contextlib
        for client in list(state.attached_clients):
            with contextlib.suppress(Exception):
                self.detach(client, name)

        # Stop if running
        if state.status == "running":
            self.stop(name)

        # Delete container
        self._cleanup_container(state.container_name)

        # Remove from store
        self._store.delete_service(name)

    def attach(
        self,
        client_name: str,
        service_name: str,
        routing_mode: str = "all",
    ) -> ClientAttachment:
        """Attach a client container to route through a service.

        Args:
            client_name: Name of the client container.
            service_name: Name of the service container.
            routing_mode: 'all' or 'selected' for specific ports.

        Returns:
            Client attachment record.

        Raises:
            ServiceNotFoundError: If service doesn't exist.
            AttachmentError: If attachment fails.
        """
        state = self._store.get_service(service_name)
        if not state:
            raise ServiceNotFoundError(service_name)

        if state.status != "running":
            raise AttachmentError(
                client_name, service_name, "Service is not running"
            )

        try:
            # Configure client container to route through service
            self._configure_client_routing(client_name, state)

            attachment = ClientAttachment(
                client_name=client_name,
                service_name=service_name,
                routing_mode=routing_mode,
            )

            self._store.add_client_attachment(attachment)
            return attachment

        except Exception as e:
            raise AttachmentError(client_name, service_name, str(e)) from e

    def _configure_client_routing(
        self, client_name: str, service: ServiceContainerState
    ) -> None:
        """Configure a client container to route traffic through service."""
        # Get the service container's IP
        result = self._run_cmd(
            [
                "sudo", "machinectl", "show", service.container_name,
                "--property=IPAddress",
            ],
            check=False,
        )

        service_ip = None
        if result.returncode == 0 and "=" in result.stdout:
            service_ip = result.stdout.strip().split("=")[1]

        if not service_ip:
            # Try to get IP via nsenter
            result = self._run_cmd(
                [
                    "sudo", "machinectl", "shell", f"root@{service.container_name}",
                    "--", "hostname", "-I",
                ],
                check=False,
            )
            if result.returncode == 0:
                service_ip = result.stdout.strip().split()[0]

        if not service_ip:
            raise RuntimeError("Could not determine service container IP")

        # Configure client's routing based on service type
        if service.service_type == ServiceContainerType.TOR_GATEWAY:
            self._configure_tor_client(client_name, service_ip)
        elif service.service_type == ServiceContainerType.VPN_GATEWAY:
            self._configure_vpn_client(client_name, service_ip)

    def _configure_tor_client(self, client_name: str, tor_ip: str) -> None:
        """Configure client to use Tor gateway."""
        # Set up transparent proxy rules in client
        script = f"""
# Redirect DNS to Tor
iptables -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination {tor_ip}:5353
iptables -t nat -A OUTPUT -p tcp --dport 53 -j DNAT --to-destination {tor_ip}:5353

# Redirect all TCP through Tor TransPort
iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination {tor_ip}:9040
"""
        self._run_cmd(
            [
                "sudo", "machinectl", "shell", f"root@{client_name}",
                "--", "bash", "-c", script,
            ],
        )

    def _configure_vpn_client(self, client_name: str, vpn_ip: str) -> None:
        """Configure client to route through VPN gateway."""
        # Set default gateway to VPN container
        script = f"""
# Route all traffic through VPN gateway
ip route del default 2>/dev/null || true
ip route add default via {vpn_ip}
"""
        self._run_cmd(
            [
                "sudo", "machinectl", "shell", f"root@{client_name}",
                "--", "bash", "-c", script,
            ],
        )

    def detach(self, client_name: str, service_name: str) -> bool:
        """Detach a client from a service.

        Args:
            client_name: Name of the client container.
            service_name: Name of the service container.

        Returns:
            True if detached successfully.
        """
        # Remove routing configuration
        self._run_cmd(
            [
                "sudo", "machinectl", "shell", f"root@{client_name}",
                "--", "iptables", "-t", "nat", "-F",
            ],
            check=False,
        )

        return self._store.remove_client_attachment(client_name, service_name)

    def status(self, name: str) -> ServiceContainerState:
        """Get the status of a service container.

        Args:
            name: Service container name.

        Returns:
            Service container state.
        """
        state = self._store.get_service(name)
        if not state:
            raise ServiceNotFoundError(name)

        # Update status from runtime
        result = self._run_cmd(
            ["sudo", "machinectl", "show", state.container_name, "--property=State"],
            check=False,
        )

        if result.returncode == 0 and "running" in result.stdout.lower():
            state.status = "running"
            # Update public IP
            state.public_ip = self._get_public_ip(state.container_name)
        else:
            state.status = "stopped"
            state.public_ip = None

        self._store.save_service(state)
        return state

    def list_services(
        self, service_type: ServiceContainerType | None = None
    ) -> list[ServiceContainerState]:
        """List all service containers.

        Args:
            service_type: Optional filter by type.

        Returns:
            List of service container states.
        """
        return self._store.list_services(service_type)


# Global instance
_manager: ServiceContainerManager | None = None


def get_service_container_manager() -> ServiceContainerManager:
    """Get the global ServiceContainerManager instance."""
    global _manager
    if _manager is None:
        _manager = ServiceContainerManager()
    return _manager
