"""Network chain manager.

Manages multi-hop traffic routing through service containers,
providing defense in depth with policy enforcement at each hop.
"""

from __future__ import annotations

import subprocess
from datetime import datetime
from pathlib import Path

from gaol.domains.exceptions import (
    ChainExistsError,
    ChainHopError,
    ChainNotFoundError,
    ChainStartError,
)
from gaol.domains.models import (
    ChainConfig,
    ChainHopState,
    ChainHopType,
    ChainState,
)
from gaol.domains.store import DomainsFileStore, get_domain_store


class ChainManager:
    """Manage network chains for multi-hop traffic routing.

    Chains define a sequence of containers that traffic flows through,
    similar to security domain network chains (app → firewall → gateway).

    Example:
        >>> manager = ChainManager()
        >>> config = ChainConfig(
        ...     name="secure-browsing",
        ...     app_container="my-browser",
        ...     hops=[
        ...         ChainHop(hop_type=ChainHopType.FIREWALL, container_name="sys-firewall", position=1),
        ...         ChainHop(hop_type=ChainHopType.GATEWAY, container_name="sys-tor", position=2),
        ...     ],
        ... )
        >>> manager.create(config)
        >>> manager.start("secure-browsing")
    """

    def __init__(self, store: DomainsFileStore | None = None) -> None:
        """Initialize the chain 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: ChainConfig) -> ChainState:
        """Create a new network chain.

        Args:
            config: Chain configuration.

        Returns:
            New chain state.

        Raises:
            ChainExistsError: If chain already exists.
        """
        if self._store.chain_exists(config.name):
            raise ChainExistsError(config.name)

        # Validate that all containers exist
        self._validate_containers(config)

        # Create hop states
        hop_states = []
        for hop in config.hops:
            hop_state = ChainHopState(
                container_name=hop.container_name,
                hop_type=hop.hop_type,
                position=hop.position,
                status="stopped",
            )
            hop_states.append(hop_state)

        state = ChainState(
            name=config.name,
            app_container=config.app_container,
            status="stopped",
            created_at=datetime.now(),
            hops=hop_states,
            description=config.description,
        )

        self._store.save_chain(state)
        return state

    def _validate_containers(self, config: ChainConfig) -> None:
        """Validate that all containers in the chain exist."""
        # Check app container
        if not self._container_exists(config.app_container):
            raise ChainStartError(
                config.name, f"App container not found: {config.app_container}"
            )

        # Check each hop
        for hop in config.hops:
            if not self._container_exists(hop.container_name):
                raise ChainStartError(
                    config.name, f"Hop container not found: {hop.container_name}"
                )

    def _container_exists(self, container_name: str) -> bool:
        """Check if a container exists."""
        # Check service containers
        service = self._store.get_service(container_name)
        if service:
            return True

        # Check regular containers via machinectl
        machine_path = Path(f"/var/lib/machines/{container_name}")
        if machine_path.exists():
            return True

        # Check with svc- prefix (service containers use this)
        svc_path = Path(f"/var/lib/machines/svc-{container_name}")
        return svc_path.exists()

    def start(self, name: str) -> ChainState:
        """Start a network chain.

        This configures routing through all hops and enables the chain.

        Args:
            name: Chain name.

        Returns:
            Updated chain state.
        """
        state = self._store.get_chain(name)
        if not state:
            raise ChainNotFoundError(name)

        try:
            state.status = "starting"
            self._store.save_chain(state)

            # Start each hop in order (from last to first)
            for hop in sorted(state.hops, key=lambda h: h.position, reverse=True):
                self._start_hop(state, hop)

            # Configure routing from app through the chain
            self._configure_chain_routing(state)

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

            # Get public IP at exit point
            if state.hops:
                last_hop = max(state.hops, key=lambda h: h.position)
                state.public_ip = self._get_public_ip(last_hop.container_name)

            self._store.save_chain(state)
            return state

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

    def _start_hop(self, _chain: ChainState, hop: ChainHopState) -> None:
        """Start a single hop in the chain."""
        # Check if it's a service container
        service = self._store.get_service(hop.container_name)

        if service:
            # Start the service container if not running
            if service.status != "running":
                from gaol.domains.service_manager import get_service_container_manager
                manager = get_service_container_manager()
                manager.start(hop.container_name)
                service = self._store.get_service(hop.container_name)

            hop.status = "running"
            hop.ip_address = self._get_container_ip(f"svc-{hop.container_name}")
        else:
            # Regular container - just check it's running
            container_name = hop.container_name
            result = self._run_cmd(
                ["sudo", "machinectl", "show", container_name, "--property=State"],
                check=False,
            )
            if result.returncode == 0 and "running" in result.stdout.lower():
                hop.status = "running"
                hop.ip_address = self._get_container_ip(container_name)
            else:
                # Try to start it
                self._run_cmd(["sudo", "machinectl", "start", container_name], check=False)
                hop.status = "running"
                hop.ip_address = self._get_container_ip(container_name)

    def _get_container_ip(self, container_name: str) -> str | None:
        """Get the IP address of a container."""
        result = self._run_cmd(
            ["sudo", "machinectl", "show", container_name, "--property=IPAddress"],
            check=False,
        )
        if result.returncode == 0 and "=" in result.stdout:
            ip = result.stdout.strip().split("=")[1]
            if ip:
                return ip

        # Try via hostname -I inside container
        result = self._run_cmd(
            ["sudo", "machinectl", "shell", f"root@{container_name}", "--", "hostname", "-I"],
            check=False,
        )
        if result.returncode == 0 and result.stdout.strip():
            return result.stdout.strip().split()[0]

        return None

    def _configure_chain_routing(self, state: ChainState) -> None:
        """Configure routing from app container through all hops."""
        if not state.hops:
            return

        # Sort hops by position
        sorted_hops = sorted(state.hops, key=lambda h: h.position)

        # The app container should route to the first hop
        first_hop = sorted_hops[0]
        if not first_hop.ip_address:
            raise ChainHopError(
                state.name, first_hop.container_name, "Could not determine hop IP address"
            )

        # Configure app container's routing
        app_container = state.app_container
        self._configure_container_routing(app_container, first_hop)

        # Configure each hop to route to the next
        for i in range(len(sorted_hops) - 1):
            current_hop = sorted_hops[i]
            next_hop = sorted_hops[i + 1]

            if not next_hop.ip_address:
                raise ChainHopError(
                    state.name, next_hop.container_name, "Could not determine hop IP address"
                )

            self._configure_hop_routing(current_hop, next_hop)

    def _configure_container_routing(
        self, container_name: str, first_hop: ChainHopState
    ) -> None:
        """Configure a container to route through the first hop."""
        hop_ip = first_hop.ip_address

        if first_hop.hop_type == ChainHopType.GATEWAY:
            # For gateway hops (Tor, VPN), use transparent proxy rules
            script = f"""
# Kill switch - block direct internet access
iptables -P OUTPUT DROP
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -d {hop_ip} -j ACCEPT

# Redirect all traffic through the hop
iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination {hop_ip}:9040
iptables -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination {hop_ip}:5353
"""
        else:
            # For firewall/proxy hops, set default gateway
            script = f"""
# Route all traffic through the hop
ip route del default 2>/dev/null || true
ip route add default via {hop_ip}
"""

        self._run_cmd(
            ["sudo", "machinectl", "shell", f"root@{container_name}", "--", "bash", "-c", script],
            check=False,
        )

    def _configure_hop_routing(
        self, current_hop: ChainHopState, next_hop: ChainHopState
    ) -> None:
        """Configure a hop to route to the next hop."""
        container_name = current_hop.container_name

        # Check if it's a service container
        service = self._store.get_service(container_name)
        if service:
            container_name = f"svc-{container_name}"

        next_ip = next_hop.ip_address

        # Configure forwarding to next hop
        script = f"""
# Enable IP forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward

# NAT traffic to next hop
iptables -t nat -A POSTROUTING -j MASQUERADE
iptables -A FORWARD -j ACCEPT

# Set default route to next hop
ip route del default 2>/dev/null || true
ip route add default via {next_ip}
"""
        self._run_cmd(
            ["sudo", "machinectl", "shell", f"root@{container_name}", "--", "bash", "-c", script],
            check=False,
        )

    def _get_public_ip(self, container_name: str) -> str | None:
        """Get the public IP from the exit point."""
        # Check if it's a service container
        service = self._store.get_service(container_name)
        if service:
            container_name = f"svc-{container_name}"

        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) -> ChainState:
        """Stop a network chain.

        This removes routing configuration but doesn't stop the hop containers.

        Args:
            name: Chain name.

        Returns:
            Updated chain state.
        """
        state = self._store.get_chain(name)
        if not state:
            raise ChainNotFoundError(name)

        # Remove routing configuration from app container
        self._remove_container_routing(state.app_container)

        # Update hop states
        for hop in state.hops:
            hop.status = "stopped"

        state.status = "stopped"
        state.kill_switch_active = False
        state.public_ip = None

        self._store.save_chain(state)
        return state

    def _remove_container_routing(self, container_name: str) -> None:
        """Remove chain routing configuration from a container."""
        script = """
# Flush iptables rules
iptables -F
iptables -t nat -F
iptables -P OUTPUT ACCEPT

# Restore default routing (DHCP will fix this on restart)
"""
        self._run_cmd(
            ["sudo", "machinectl", "shell", f"root@{container_name}", "--", "bash", "-c", script],
            check=False,
        )

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

        Args:
            name: Chain name.
            force: Force deletion even if running.
        """
        state = self._store.get_chain(name)
        if not state:
            raise ChainNotFoundError(name)

        if state.status == "running" and not force:
            raise RuntimeError(
                f"Chain '{name}' is running. Use --force to delete anyway."
            )

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

        self._store.delete_chain(name)

    def status(self, name: str) -> ChainState:
        """Get chain status.

        Args:
            name: Chain name.

        Returns:
            Chain state with updated hop statuses.
        """
        state = self._store.get_chain(name)
        if not state:
            raise ChainNotFoundError(name)

        # Update hop statuses
        all_running = True
        any_error = False

        for hop in state.hops:
            container_name = hop.container_name

            # Check if it's a service container
            service = self._store.get_service(container_name)
            if service:
                container_name = f"svc-{container_name}"

            result = self._run_cmd(
                ["sudo", "machinectl", "show", container_name, "--property=State"],
                check=False,
            )

            if result.returncode == 0 and "running" in result.stdout.lower():
                hop.status = "running"
                hop.ip_address = self._get_container_ip(container_name)
            else:
                hop.status = "stopped"
                all_running = False

            if hop.status == "error":
                any_error = True

        # Update chain status based on hops
        if state.status == "running":
            if any_error:
                state.status = "degraded"
                state.kill_switch_active = True
            elif not all_running:
                state.status = "degraded"

        self._store.save_chain(state)
        return state

    def activate_kill_switch(self, name: str) -> ChainState:
        """Manually activate kill switch to block all traffic.

        Args:
            name: Chain name.

        Returns:
            Updated chain state.
        """
        state = self._store.get_chain(name)
        if not state:
            raise ChainNotFoundError(name)

        # Block all outbound traffic from app container
        script = """
iptables -P OUTPUT DROP
iptables -P INPUT DROP
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
"""
        self._run_cmd(
            ["sudo", "machinectl", "shell", f"root@{state.app_container}", "--", "bash", "-c", script],
            check=False,
        )

        state.kill_switch_active = True
        self._store.save_chain(state)

        return state

    def deactivate_kill_switch(self, name: str) -> ChainState:
        """Deactivate kill switch and restore routing.

        Args:
            name: Chain name.

        Returns:
            Updated chain state.
        """
        state = self._store.get_chain(name)
        if not state:
            raise ChainNotFoundError(name)

        # Restore routing
        self._configure_chain_routing(state)

        state.kill_switch_active = False
        self._store.save_chain(state)

        return state

    def list_chains(self) -> list[ChainState]:
        """List all chains.

        Returns:
            List of chain states.
        """
        return self._store.list_chains()

    def get_chains_for_container(self, container_name: str) -> list[ChainState]:
        """Get all chains involving a container.

        Args:
            container_name: Container to search for.

        Returns:
            List of chains involving this container.
        """
        return self._store.get_chains_for_container(container_name)


# Global instance
_manager: ChainManager | None = None


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