"""WireGuard interface management via wg-quick."""

from __future__ import annotations

import os
import re
import shutil
import subprocess
from pathlib import Path

from .config import WG_DIR
from .exceptions import BeaconSystemError

WG_CONF_DIR = WG_DIR


def _sudo() -> list[str]:
    """Return ['sudo'] only when not root and sudo is available."""
    return ["sudo"] if _sudo_available() else []


# Config file

def write_config(interface_name: str, config_content: str) -> Path:
    """Write the WireGuard config to the state directory and return its path."""
    WG_CONF_DIR.mkdir(parents=True, exist_ok=True)
    conf_path = WG_CONF_DIR / f"{interface_name}.conf"
    conf_path.write_text(config_content)
    conf_path.chmod(0o600)
    return conf_path


def remove_config(interface_name: str) -> None:
    conf_path = WG_CONF_DIR / f"{interface_name}.conf"
    conf_path.unlink(missing_ok=True)


# Interface lifecycle

def up(interface_name: str) -> None:
    """Bring up a WireGuard interface using wg-quick."""
    conf_path = WG_CONF_DIR / f"{interface_name}.conf"
    _run([*_sudo(), "wg-quick", "up", str(conf_path)], action="bring up")


def down(interface_name: str) -> None:
    """Bring down a WireGuard interface.

    Tries ``wg-quick down`` first (clean teardown with iptables rules removed).
    Falls back to ``ip link delete`` if the config file no longer exists (e.g.
    after a container recreate where the state dir is somehow missing).
    """
    conf_path = WG_CONF_DIR / f"{interface_name}.conf"
    if conf_path.exists():
        _run([*_sudo(), "wg-quick", "down", str(conf_path)], action="bring down")
    else:
        try:
            subprocess.run(
                [*_sudo(), "ip", "link", "delete", interface_name],
                capture_output=True,
                check=True,
            )
        except (subprocess.CalledProcessError, FileNotFoundError) as exc:
            raise BeaconSystemError(
                f"Failed to bring down WireGuard interface: {exc}"
            ) from exc


def get_active_interfaces() -> list[str]:
    """Return names of all currently active WireGuard interfaces.

    Uses ``ip link show type wireguard`` which works even when no wg-quick
    config files exist (e.g. after a container recreate).
    """
    try:
        out = subprocess.check_output(
            ["ip", "link", "show", "type", "wireguard"],
            stderr=subprocess.DEVNULL,
            text=True,
        )
        interfaces = []
        for line in out.splitlines():
            m = re.match(r"^\d+:\s+(\S+?)[@:]", line)
            if m:
                interfaces.append(m.group(1))
        return interfaces
    except (subprocess.CalledProcessError, FileNotFoundError):
        return []


def teardown_all() -> None:
    """Delete every active WireGuard interface via ``ip link delete``.

    Used before creating a new beacon to clear stale interfaces from previous
    runs (e.g. Docker restart where the network namespace outlives the container
    and no wg-quick config files exist to run ``wg-quick down``).
    """
    for iface in get_active_interfaces():
        try:
            subprocess.run(
                [*_sudo(), "ip", "link", "delete", iface],
                capture_output=True,
                check=True,
            )
        except (subprocess.CalledProcessError, FileNotFoundError):
            pass


def is_up(interface_name: str) -> bool:
    """Return True if the interface is currently active."""
    try:
        out = subprocess.check_output(
            ["wg", "show", interface_name],
            stderr=subprocess.DEVNULL,
            text=True,
        )
        return "interface:" in out.lower()
    except (subprocess.CalledProcessError, FileNotFoundError):
        return False


def get_status(interface_name: str) -> dict:
    """
    Return a dict with WireGuard interface status:
      { active: bool, peers: int, latest_handshake: str | None }
    """
    try:
        out = subprocess.check_output(
            ["wg", "show", interface_name],
            stderr=subprocess.DEVNULL,
            text=True,
        )
    except (subprocess.CalledProcessError, FileNotFoundError):
        return {"active": False, "peers": 0, "latest_handshake": None}

    peers = len(re.findall(r"^peer:", out, re.MULTILINE))
    handshake_match = re.search(r"latest handshake: (.+)", out)
    handshake = handshake_match.group(1).strip() if handshake_match else None

    return {
        "active": "interface:" in out.lower(),
        "peers": peers,
        "latest_handshake": handshake,
    }


# System tweaks

def enable_ip_forwarding() -> None:
    """Enable IPv4 forwarding (requires root)."""
    try:
        subprocess.run(
            [*_sudo(), "sysctl", "-w", "net.ipv4.ip_forward=1"],
            check=True,
            capture_output=True,
        )
    except (subprocess.CalledProcessError, FileNotFoundError):
        pass  # Not fatal - WireGuard may handle this via PostUp rules


def is_root() -> bool:
    """Return True if the current process is running as root (uid 0)."""
    return os.getuid() == 0


def _sudo_available() -> bool:
    """Return True if sudo should be used.

    Requires all of: not already root, sudo is installed, and an interactive
    TTY is present (so the user can enter a password if needed). In
    non-interactive environments (Docker, CI, cron, tests) the process either
    runs with the necessary capabilities (CAP_NET_ADMIN) or as root, so sudo
    is unnecessary and would only cause hangs or errors.
    """
    import sys
    return (
        os.getuid() != 0
        and shutil.which("sudo") is not None
        and sys.stdin.isatty()
    )


def is_installed() -> bool:
    """Check both wg and wg-quick are available."""
    return shutil.which("wg") is not None and shutil.which("wg-quick") is not None


def is_iptables_available() -> bool:
    """Check if iptables is available on the system."""
    return shutil.which("iptables") is not None


def prompt_sudo() -> None:
    """Ensure the user has an active sudo session (prompt now, before spinners).

    No-op when already root or when sudo is not installed (e.g. Docker with
    NET_ADMIN capability). Otherwise calls ``sudo -v`` which validates
    credentials and refreshes the timestamp. Must be called before any Rich
    live-display context (Status/spinner) so the password prompt is visible.
    """
    if not _sudo_available():
        return
    try:
        subprocess.run(["sudo", "-v"], check=True)
    except (subprocess.CalledProcessError, FileNotFoundError):
        raise BeaconSystemError("sudo is required to manage WireGuard interfaces.")


# Internal

def _run(cmd: list[str], action: str) -> None:
    try:
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            detail = result.stderr.strip() or result.stdout.strip()
            raise BeaconSystemError(
                f"Failed to {action} WireGuard interface: {detail}"
            )
    except FileNotFoundError:
        raise BeaconSystemError(
            "wg-quick not found. Install WireGuard: https://www.wireguard.com/install/"
        )
