"""WireGuard mesh network manager.

This module provides the WireGuardMeshManager for managing WireGuard
mesh networks between containers.
"""

from __future__ import annotations

import ipaddress
import shutil
from datetime import datetime
from pathlib import Path

import yaml

from gaol.privacy.exceptions import (
    WireGuardMeshAlreadyExistsError,
    WireGuardMeshNotFoundError,
    WireGuardPeerError,
)
from gaol.privacy.models import (
    PrivacyStatus,
    WireGuardInterface,
    WireGuardMeshConfig,
    WireGuardMeshState,
    WireGuardPeer,
)
from gaol.privacy.wireguard.backend import WireGuardBackend


class WireGuardMeshManager:
    """Manage WireGuard mesh networks.

    Handles creating, configuring, and managing WireGuard mesh networks
    for secure communication between containers.

    Example:
        >>> manager = WireGuardMeshManager()
        >>> mesh = manager.create("mymesh", subnet="10.200.0.0/24")
        >>> print(f"Interface: {mesh.interface_name}")
        >>> manager.add_peer("mymesh", "peer1", "PUBLIC_KEY", "10.200.0.2")
        >>> manager.delete("mymesh")
    """

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

        Args:
            state_dir: Directory for state files. Defaults to
                       ~/.local/state/gaol/wireguard
        """
        if state_dir is None:
            state_dir = Path.home() / ".local" / "state" / "gaol" / "wireguard"
        self.state_dir = state_dir
        self.state_dir.mkdir(parents=True, exist_ok=True)
        self._store_path = self.state_dir / "meshes.yaml"
        self._backend = WireGuardBackend(state_dir)

    def create(
        self,
        name: str,
        subnet: str = "10.200.0.0/24",
        listen_port: int = 51820,
        interface_name: str | None = None,
    ) -> WireGuardMeshState:
        """Create a new WireGuard mesh network.

        Creates a new mesh network with the first address in the subnet
        assigned to the local interface.

        Args:
            name: Unique name for this mesh.
            subnet: Subnet for the mesh (CIDR notation).
            listen_port: UDP port for incoming connections.
            interface_name: Interface name (defaults to wg-{name}).

        Returns:
            WireGuardMeshState with mesh information.

        Raises:
            WireGuardMeshAlreadyExistsError: If mesh with name exists.
        """
        # Check for existing
        existing = self.get(name)
        if existing and existing.status == PrivacyStatus.RUNNING:
            raise WireGuardMeshAlreadyExistsError(name)

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

        if interface_name is None:
            interface_name = f"wg-{name[:12]}"

        # Generate keypair for local interface
        private_key, public_key = self._backend.generate_keypair()

        # Calculate local address (first usable in subnet)
        network = ipaddress.ip_network(subnet, strict=False)
        hosts = list(network.hosts())
        if not hosts:
            raise ValueError(f"Subnet {subnet} has no usable addresses")
        local_address = f"{hosts[0]}/{network.prefixlen}"

        # Create interface
        interface = WireGuardInterface(
            private_key=private_key,
            address=local_address,
            listen_port=listen_port,
        )

        # Create config file
        config_file = self._backend.create_interface(
            interface_name,
            private_key,
            local_address,
            listen_port,
        )

        # Create state
        state = WireGuardMeshState(
            name=name,
            interface=interface,
            peers=[],
            interface_name=interface_name,
            status=PrivacyStatus.STOPPED,
            config_file=config_file,
            created_at=datetime.now(),
        )

        # Store public key for sharing with peers
        pubkey_file = self.state_dir / name / "public_key"
        pubkey_file.write_text(public_key)

        self._save_state(state)
        return state

    def start(self, name: str) -> WireGuardMeshState:
        """Start (bring up) a mesh network.

        Args:
            name: Name of the mesh to start.

        Returns:
            Updated mesh state.

        Raises:
            WireGuardMeshNotFoundError: If mesh not found.
        """
        state = self.get(name)
        if not state:
            raise WireGuardMeshNotFoundError(name)

        if state.config_file:
            self._backend.interface_up(state.config_file)
            state.status = PrivacyStatus.RUNNING
            self._save_state(state)

        return state

    def stop(self, name: str) -> WireGuardMeshState:
        """Stop (bring down) a mesh network.

        Args:
            name: Name of the mesh to stop.

        Returns:
            Updated mesh state.

        Raises:
            WireGuardMeshNotFoundError: If mesh not found.
        """
        state = self.get(name)
        if not state:
            raise WireGuardMeshNotFoundError(name)

        if state.config_file:
            self._backend.interface_down(state.config_file)
            state.status = PrivacyStatus.STOPPED
            self._save_state(state)

        return state

    def delete(self, name: str) -> None:
        """Delete a mesh network.

        Args:
            name: Name of the mesh to delete.

        Raises:
            WireGuardMeshNotFoundError: If mesh not found.
        """
        state = self.get(name)
        if not state:
            raise WireGuardMeshNotFoundError(name)

        # Bring down interface if running
        if state.status == PrivacyStatus.RUNNING and state.config_file:
            self._backend.interface_down(state.config_file)

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

        # Remove state
        self._remove_state(name)

    def add_peer(
        self,
        mesh_name: str,
        peer_name: str,
        public_key: str,
        allowed_ips: str | list[str],
        endpoint: str | None = None,
        persistent_keepalive: int = 25,
    ) -> WireGuardMeshState:
        """Add a peer to a mesh network.

        Args:
            mesh_name: Name of the mesh.
            peer_name: Name for this peer.
            public_key: Peer's public key.
            allowed_ips: IP address(es) allowed for this peer.
            endpoint: Peer's endpoint (host:port) for outbound connections.
            persistent_keepalive: Keepalive interval (0 to disable).

        Returns:
            Updated mesh state.

        Raises:
            WireGuardMeshNotFoundError: If mesh not found.
            WireGuardPeerError: If peer already exists.
        """
        state = self.get(mesh_name)
        if not state:
            raise WireGuardMeshNotFoundError(mesh_name)

        # Check if peer exists
        for peer in state.peers:
            if peer.name == peer_name:
                raise WireGuardPeerError(peer_name, "Peer already exists")
            if peer.public_key == public_key:
                raise WireGuardPeerError(peer_name, "Public key already in use")

        # Normalize allowed_ips to list
        if isinstance(allowed_ips, str):
            allowed_ips = [allowed_ips]

        # Ensure CIDR notation
        normalized_ips = []
        for ip in allowed_ips:
            if "/" not in ip:
                ip = f"{ip}/32"
            normalized_ips.append(ip)

        # Create peer
        peer = WireGuardPeer(
            name=peer_name,
            public_key=public_key,
            endpoint=endpoint,
            allowed_ips=normalized_ips,
            persistent_keepalive=persistent_keepalive,
        )

        # Add to config file
        if state.config_file:
            self._backend.add_peer_to_config(
                state.config_file,
                public_key,
                normalized_ips,
                endpoint,
                persistent_keepalive,
            )

        # Update state
        state.peers.append(peer)
        self._save_state(state)

        # If running, reload interface
        if state.status == PrivacyStatus.RUNNING:
            self._backend.interface_down(state.config_file)
            self._backend.interface_up(state.config_file)

        return state

    def remove_peer(self, mesh_name: str, peer_name: str) -> WireGuardMeshState:
        """Remove a peer from a mesh network.

        Args:
            mesh_name: Name of the mesh.
            peer_name: Name of the peer to remove.

        Returns:
            Updated mesh state.

        Raises:
            WireGuardMeshNotFoundError: If mesh not found.
            WireGuardPeerError: If peer not found.
        """
        state = self.get(mesh_name)
        if not state:
            raise WireGuardMeshNotFoundError(mesh_name)

        # Find and remove peer
        peer_found = False
        new_peers = []
        for peer in state.peers:
            if peer.name == peer_name:
                peer_found = True
            else:
                new_peers.append(peer)

        if not peer_found:
            raise WireGuardPeerError(peer_name, "Peer not found")

        state.peers = new_peers

        # Regenerate config file
        if state.config_file and state.interface:
            self._regenerate_config(state)

        self._save_state(state)

        # If running, reload interface
        if state.status == PrivacyStatus.RUNNING and state.config_file:
            self._backend.interface_down(state.config_file)
            self._backend.interface_up(state.config_file)

        return state

    def _regenerate_config(self, state: WireGuardMeshState) -> None:
        """Regenerate the WireGuard config file.

        Args:
            state: Mesh state.
        """
        if not state.config_file or not state.interface:
            return

        # Generate base config
        config_content = f"""[Interface]
PrivateKey = {state.interface.private_key}
Address = {state.interface.address}
ListenPort = {state.interface.listen_port}
"""

        # Add peers
        for peer in state.peers:
            config_content += f"""
[Peer]
PublicKey = {peer.public_key}
AllowedIPs = {", ".join(peer.allowed_ips)}
"""
            if peer.endpoint:
                config_content += f"Endpoint = {peer.endpoint}\n"
            if peer.persistent_keepalive > 0:
                config_content += f"PersistentKeepalive = {peer.persistent_keepalive}\n"
            if peer.preshared_key:
                config_content += f"PresharedKey = {peer.preshared_key}\n"

        state.config_file.write_text(config_content)
        state.config_file.chmod(0o600)

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

        Args:
            name: Mesh name.

        Returns:
            WireGuardMeshState if found, None otherwise.
        """
        states = self._load_states()
        for state in states:
            if state.name == name:
                # Update status
                if self._backend.is_interface_up(state.interface_name):
                    state.status = PrivacyStatus.RUNNING
                else:
                    state.status = PrivacyStatus.STOPPED
                return state
        return None

    def list(self) -> list[WireGuardMeshState]:
        """List all mesh networks.

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

        # Update statuses
        for state in states:
            if self._backend.is_interface_up(state.interface_name):
                state.status = PrivacyStatus.RUNNING
            else:
                state.status = PrivacyStatus.STOPPED

        return states

    def get_public_key(self, name: str) -> str | None:
        """Get the public key for a mesh network.

        This can be shared with peers to add this node.

        Args:
            name: Mesh name.

        Returns:
            Public key, or None if mesh not found.
        """
        pubkey_file = self.state_dir / name / "public_key"
        if pubkey_file.exists():
            return pubkey_file.read_text().strip()
        return None

    def status(self, name: str) -> dict | None:
        """Get detailed status of a mesh network.

        Args:
            name: Mesh name.

        Returns:
            Status dictionary with interface and peer info.
        """
        state = self.get(name)
        if not state:
            return None

        # Get interface status from WireGuard
        wg_status = self._backend.get_interface_status(state.interface_name)

        return {
            "name": name,
            "status": state.status.value,
            "interface": state.interface_name,
            "address": state.interface.address if state.interface else None,
            "listen_port": state.interface.listen_port if state.interface else None,
            "public_key": self.get_public_key(name),
            "peers": [
                {
                    "name": p.name,
                    "public_key": p.public_key[:20] + "...",
                    "allowed_ips": p.allowed_ips,
                    "endpoint": p.endpoint,
                }
                for p in state.peers
            ],
            "wg_status": wg_status,
        }

    def _save_state(self, state: WireGuardMeshState) -> None:
        """Save mesh 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 mesh state.

        Args:
            name: Name of mesh 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[WireGuardMeshState]:
        """Load mesh states from store.

        Returns:
            List of mesh 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 "meshes" not in data:
                return []
            return [WireGuardMeshState.model_validate(m) for m in data["meshes"]]
        except Exception:
            return []

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

        Args:
            states: List of states to write.
        """
        self._store_path.parent.mkdir(parents=True, exist_ok=True)
        data = {"meshes": [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)
