#!/usr/bin/env python3
"""Mullvad exit node manager for Tailscale on macOS.

Usage:
  mullvad on <city or country>   - connect to Mullvad exit node
  mullvad off                     - disconnect Mullvad exit node
  mullvad status                  - show current Mullvad status
"""

import subprocess
import sys
import re
import random


def _run_tailscale(args, timeout=30):
    """Run a tailscale command, suppressing version mismatch warnings."""
    result = subprocess.run(
        ["tailscale"] + args,
        capture_output=True,
        text=True,
        timeout=timeout,
    )
    output = result.stdout
    if result.returncode != 0:
        if result.stderr:
            print(result.stderr.strip(), file=sys.stderr)
        result.check_returncode()
    return output


def _run_tailscale_live(args, timeout=30):
    """Run tailscale command with live output (for up/down)."""
    result = subprocess.run(
        ["tailscale"] + args,
        timeout=timeout,
    )
    if result.returncode != 0:
        sys.exit(result.returncode)


def get_exit_nodes():
    """Get all available exit nodes including Mullvad ones."""
    output = _run_tailscale(["exit-node", "list"])
    nodes = []

    for line in output.split("\n"):
        line = line.strip()
        if not line:
            continue
        if line.startswith("#") or line.startswith("Warning"):
            continue
        if line.startswith("IP "):
            continue

        match = re.match(
            r"^(\S+)\s+(\S+\.ts\.net)\.?\s+(.+?)\s{2,}(.+?)\s{2,}(\S+)$",
            line,
        )
        if match:
            ip, hostname, country, city, status = match.groups()
            hostname = hostname.rstrip(".")
            nodes.append(
                {
                    "ip": ip,
                    "hostname": hostname,
                    "country": country.strip(),
                    "city": city.strip(),
                    "status": status,
                }
            )
    return nodes


def find_nodes(nodes, location):
    """Find Mullvad nodes matching location (case-insensitive on city/country/hostname)."""
    location_lower = location.lower()
    matches = []
    for node in nodes:
        is_mullvad = "mullvad" in node["hostname"].lower()
        if not is_mullvad:
            continue

        if (
            location_lower in node["city"].lower()
            or location_lower in node["country"].lower()
            or location_lower in node["hostname"].lower()
        ):
            matches.append(node)

    return matches


def get_current_exit_node():
    """Get the currently active exit node hostname, or None."""
    try:
        output = _run_tailscale(["exit-node", "list"], timeout=10)
    except subprocess.CalledProcessError:
        return None
    for line in output.split("\n"):
        if "selected" in line:
            parts = line.split()
            if len(parts) >= 2:
                return parts[1].rstrip(".")
            return line.strip()
    return None


def show_status():
    """Show current Mullvad status."""
    current = get_current_exit_node()
    if current:
        if "mullvad" in current.lower():
            print(f"Mullvad: ON  -> {current}")
        else:
            print(f"Exit node: {current} (not Mullvad)")
    else:
        print("Mullvad: OFF")


def cmd_on(location):
    """Connect to a Mullvad exit node matching the given location."""
    nodes = get_exit_nodes()
    matches = find_nodes(nodes, location)

    if not matches:
        print(f"No Mullvad exit nodes found for '{location}'")
        print("\nAvailable Mullvad locations:")
        shown = set()
        for node in nodes:
            is_mullvad = "mullvad" in node["hostname"].lower()
            if is_mullvad:
                key = f"{node['country']} - {node['city']}"
                if key not in shown:
                    print(f"  {key}")
                    shown.add(key)
        sys.exit(1)

    node = random.choice(matches)
    hostname = node["hostname"]

    print(f"Connecting to Mullvad: {node['country']} - {node['city']} ({hostname})")
    print("Tearing down Tailscale...")
    _run_tailscale_live(["down", "--accept-risk=lose-ssh"])
    print("Bringing up Tailscale with Mullvad exit node...")
    _run_tailscale_live(
        ["up", "--reset", "--accept-risk=lose-ssh", f"--exit-node={hostname}", "--exit-node-allow-lan-access=false", "--accept-routes"]
    )
    print(f"Mullvad: ON  -> {hostname}  ({node['country']}, {node['city']})")


def cmd_off():
    """Disconnect from the Mullvad exit node."""
    print("Tearing down Tailscale...")
    _run_tailscale_live(["down", "--accept-risk=lose-ssh"])
    print("Bringing up Tailscale without exit node...")
    _run_tailscale_live(["up", "--reset", "--accept-risk=lose-ssh", "--accept-routes"])
    print("Mullvad: OFF")


def main():
    if len(sys.argv) < 2:
        print("Usage: mullvad on <city or country> | mullvad off | mullvad status")
        sys.exit(1)

    cmd = sys.argv[1].lower()

    if cmd == "on":
        if len(sys.argv) < 3:
            print("Usage: mullvad on <city or country>")
            print("  e.g. mullvad on amsterdam")
            print("  e.g. mullvad on netherlands")
            sys.exit(1)
        cmd_on(sys.argv[2])

    elif cmd == "off":
        cmd_off()

    elif cmd == "status":
        show_status()

    else:
        print(f"Unknown command: {cmd}")
        print("Usage: mullvad on <city or country> | mullvad off | mullvad status")
        sys.exit(1)


if __name__ == "__main__":
    main()
