"""Tinc VPN management for RedundaNet."""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING

from jinja2 import Template

from redundanet.core.exceptions import VPNError
from redundanet.utils.files import ensure_dir, read_file, write_file
from redundanet.utils.logging import get_logger
from redundanet.utils.process import is_command_available, run_command

if TYPE_CHECKING:
    from redundanet.core.config import NodeConfig

logger = get_logger(__name__)

# Tinc configuration templates
TINC_CONF_TEMPLATE = """# Tinc configuration for {{ network_name }}
# Generated by RedundaNet

Name = {{ node_name }}
Mode = switch
Device = /dev/net/tun
DeviceType = tun
AddressFamily = ipv4
Port = {{ port }}

{% for connect_node in connect_to %}
ConnectTo = {{ connect_node }}
{% endfor %}

# Security settings
Compression = 9
Cipher = aes-256-cbc
Digest = sha256

# Process settings
ProcessPriority = high
"""

TINC_UP_TEMPLATE = """#!/bin/bash
# Tinc-up script for {{ network_name }}
# Generated by RedundaNet

ip link set $INTERFACE up
ip addr add {{ vpn_ip }}/32 dev $INTERFACE
ip route add {{ vpn_network }} dev $INTERFACE
"""

TINC_DOWN_TEMPLATE = """#!/bin/bash
# Tinc-down script for {{ network_name }}
# Generated by RedundaNet

ip route del {{ vpn_network }} dev $INTERFACE 2>/dev/null || true
ip addr del {{ vpn_ip }}/32 dev $INTERFACE 2>/dev/null || true
ip link set $INTERFACE down
"""

HOST_FILE_TEMPLATE = """# Host file for {{ node_name }}
# Generated by RedundaNet

Subnet = {{ vpn_ip }}/32
{% if public_ip %}Address = {{ public_ip }}{% endif %}
Port = {{ port }}

{{ public_key }}
"""


@dataclass
class TincConfig:
    """Configuration for Tinc VPN."""

    network_name: str = "redundanet"
    node_name: str = ""
    vpn_ip: str = ""
    vpn_network: str = "10.100.0.0/16"
    port: int = 655
    public_ip: str | None = None
    connect_to: list[str] = field(default_factory=list)
    config_dir: Path = field(default_factory=lambda: Path("/etc/tinc"))

    @property
    def network_dir(self) -> Path:
        """Get the network-specific configuration directory."""
        return self.config_dir / self.network_name

    @property
    def hosts_dir(self) -> Path:
        """Get the hosts directory."""
        return self.network_dir / "hosts"

    @classmethod
    def from_node_config(cls, node: NodeConfig, network_name: str = "redundanet") -> TincConfig:
        """Create TincConfig from a NodeConfig."""
        return cls(
            network_name=network_name,
            node_name=node.name,
            vpn_ip=node.vpn_ip or node.internal_ip,
            port=node.ports.tinc,
            public_ip=node.public_ip,
        )


class TincManager:
    """Manages Tinc VPN configuration and operations."""

    def __init__(self, config: TincConfig) -> None:
        self.config = config
        self._private_key_path = self.config.network_dir / "rsa_key.priv"
        self._public_key_path = self.config.hosts_dir / self.config.node_name

    def ensure_tinc_installed(self) -> bool:
        """Check if tinc is installed."""
        if not is_command_available("tincd"):
            raise VPNError("Tinc is not installed. Please install tinc first.")
        return True

    def setup(self, peers: list[NodeConfig] | None = None) -> None:
        """Set up the complete Tinc configuration.

        Args:
            peers: List of peer nodes to connect to
        """
        logger.info("Setting up Tinc VPN", node=self.config.node_name)

        # Create directories
        ensure_dir(self.config.network_dir, mode=0o755)
        ensure_dir(self.config.hosts_dir, mode=0o755)

        # Determine which nodes to connect to
        connect_nodes = []
        if peers:
            for peer in peers:
                if peer.is_publicly_accessible and peer.name != self.config.node_name:
                    connect_nodes.append(peer.name)
        self.config.connect_to = connect_nodes

        # Generate configuration files
        self._write_tinc_conf()
        self._write_tinc_up()
        self._write_tinc_down()

        # Generate keys if they don't exist
        if not self._private_key_path.exists():
            self.generate_keys()

        # Write peer host files
        if peers:
            for peer in peers:
                if peer.name != self.config.node_name:
                    self._write_host_file(peer)

        logger.info("Tinc VPN setup complete", node=self.config.node_name)

    def _write_tinc_conf(self) -> None:
        """Write the main tinc.conf file."""
        template = Template(TINC_CONF_TEMPLATE)
        content = template.render(
            network_name=self.config.network_name,
            node_name=self.config.node_name,
            port=self.config.port,
            connect_to=self.config.connect_to,
        )

        conf_path = self.config.network_dir / "tinc.conf"
        write_file(conf_path, content, mode=0o644)
        logger.debug("Wrote tinc.conf", path=str(conf_path))

    def _write_tinc_up(self) -> None:
        """Write the tinc-up script."""
        template = Template(TINC_UP_TEMPLATE)
        content = template.render(
            network_name=self.config.network_name,
            vpn_ip=self.config.vpn_ip,
            vpn_network=self.config.vpn_network,
        )

        script_path = self.config.network_dir / "tinc-up"
        write_file(script_path, content, executable=True)
        logger.debug("Wrote tinc-up", path=str(script_path))

    def _write_tinc_down(self) -> None:
        """Write the tinc-down script."""
        template = Template(TINC_DOWN_TEMPLATE)
        content = template.render(
            network_name=self.config.network_name,
            vpn_ip=self.config.vpn_ip,
            vpn_network=self.config.vpn_network,
        )

        script_path = self.config.network_dir / "tinc-down"
        write_file(script_path, content, executable=True)
        logger.debug("Wrote tinc-down", path=str(script_path))

    def _write_host_file(
        self,
        node: NodeConfig,
        public_key: str | None = None,
    ) -> None:
        """Write a host file for a node."""
        template = Template(HOST_FILE_TEMPLATE)
        content = template.render(
            node_name=node.name,
            vpn_ip=node.vpn_ip or node.internal_ip,
            public_ip=node.public_ip if node.is_publicly_accessible else None,
            port=node.ports.tinc,
            public_key=public_key or "",
        )

        host_path = self.config.hosts_dir / node.name
        write_file(host_path, content, mode=0o644)
        logger.debug("Wrote host file", node=node.name, path=str(host_path))

    def generate_keys(self) -> str:
        """Generate Tinc RSA keypair.

        Returns:
            The public key as a string
        """
        logger.info("Generating Tinc keys", node=self.config.node_name)

        # Create empty host file for key generation
        host_file = self.config.hosts_dir / self.config.node_name
        if not host_file.exists():
            header = f"# Host file for {self.config.node_name}\n"
            header += f"Subnet = {self.config.vpn_ip}/32\n"
            if self.config.public_ip:
                header += f"Address = {self.config.public_ip}\n"
            header += f"Port = {self.config.port}\n"
            write_file(host_file, header, mode=0o644)

        # Generate keys using tincd
        result = run_command(
            f"tincd -n {self.config.network_name} -K4096",
            input_text="\n",  # Accept defaults
        )

        if not result.success:
            raise VPNError(f"Failed to generate keys: {result.stderr}")

        # Read and return the public key
        return self.get_public_key()

    def get_public_key(self) -> str:
        """Get the public key for this node."""
        host_file = self.config.hosts_dir / self.config.node_name
        if not host_file.exists():
            raise VPNError(f"Host file not found: {host_file}")

        content = read_file(host_file)

        # Extract the public key portion
        lines = content.split("\n")
        in_key = False
        key_lines = []

        for line in lines:
            if "BEGIN RSA PUBLIC KEY" in line:
                in_key = True
            if in_key:
                key_lines.append(line)
            if "END RSA PUBLIC KEY" in line:
                break

        if not key_lines:
            raise VPNError("No public key found in host file")

        return "\n".join(key_lines)

    def start(self) -> bool:
        """Start the Tinc VPN daemon."""
        logger.info("Starting Tinc VPN", network=self.config.network_name)

        result = run_command(f"tincd -n {self.config.network_name}")
        if not result.success:
            logger.error("Failed to start Tinc", error=result.stderr)
            return False

        logger.info("Tinc VPN started")
        return True

    def stop(self) -> bool:
        """Stop the Tinc VPN daemon."""
        logger.info("Stopping Tinc VPN", network=self.config.network_name)

        result = run_command(f"tincd -n {self.config.network_name} -k")
        if not result.success:
            logger.error("Failed to stop Tinc", error=result.stderr)
            return False

        logger.info("Tinc VPN stopped")
        return True

    def reload(self) -> bool:
        """Reload Tinc VPN configuration."""
        logger.info("Reloading Tinc VPN", network=self.config.network_name)

        result = run_command(f"tincd -n {self.config.network_name} -kHUP")
        return result.success

    def is_running(self) -> bool:
        """Check if Tinc is running."""
        pid_file = Path(f"/var/run/tinc.{self.config.network_name}.pid")
        if not pid_file.exists():
            return False

        pid = int(pid_file.read_text().strip())
        return Path(f"/proc/{pid}").exists()

    def get_status(self) -> dict[str, object]:
        """Get the current status of Tinc VPN."""
        return {
            "running": self.is_running(),
            "network": self.config.network_name,
            "node": self.config.node_name,
            "vpn_ip": self.config.vpn_ip,
            "config_dir": str(self.config.network_dir),
        }
