"""
AUTARCH WireGuard VPN Manager
Server management, client/peer CRUD, remote ADB (TCP/IP + USB/IP).

Integrates /home/snake/wg_setec/ functionality into the AUTARCH framework
with added remote ADB and USB/IP support for Android device management
over WireGuard tunnels.
"""

import io
import json
import re
import subprocess
import time
import uuid
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, List, Any, Tuple

from core.paths import get_data_dir, find_tool
from core.daemon import root_exec


class WireGuardManager:
    """WireGuard VPN + Remote ADB manager."""

    def __init__(self, config=None):
        self._wg_bin = find_tool('wg')
        self._wg_quick = find_tool('wg-quick')
        self._usbip_bin = find_tool('usbip')

        self._data_dir = get_data_dir() / 'wireguard'
        self._data_dir.mkdir(parents=True, exist_ok=True)
        self._clients_file = self._data_dir / 'clients.json'
        self._last_ip_file = self._data_dir / 'last_ip'

        # Config from autarch_settings.conf [wireguard] section
        self._config = config or {}
        self._wg_config_path = self._config.get('config_path', '/etc/wireguard/wg0.conf')
        self._interface = self._config.get('interface', 'wg0')
        self._subnet = self._config.get('subnet', '10.1.0.0/24')
        self._server_address = self._config.get('server_address', '10.1.0.1')
        self._listen_port = self._config.get('listen_port', '51820')
        self._default_dns = self._config.get('default_dns', '1.1.1.1, 8.8.8.8')
        self._default_allowed_ips = self._config.get('default_allowed_ips', '0.0.0.0/0, ::/0')

    # ── Helpers ──────────────────────────────────────────────────────

    def _run_wg(self, args, timeout=10):
        """Run wg command, return (stdout, stderr, rc)."""
        if not self._wg_bin:
            return ('', 'wg binary not found', 1)
        cmd = [self._wg_bin] + args
        try:
            proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
            return (proc.stdout, proc.stderr, proc.returncode)
        except subprocess.TimeoutExpired:
            return ('', 'Command timed out', 1)
        except Exception as e:
            return ('', str(e), 1)

    def _run_wg_sudo(self, args, timeout=10):
        """Run wg command with root privileges via daemon, return (stdout, stderr, rc)."""
        if not self._wg_bin:
            return ('', 'wg binary not found', 1)
        cmd = [str(self._wg_bin)] + args
        r = root_exec(cmd, timeout=timeout)
        return (r['stdout'], r['stderr'], r['code'])

    def _run_cmd(self, cmd, timeout=10, input_data=None):
        """Run arbitrary command, return (stdout, stderr, rc).
        Commands starting with 'sudo' are routed through the privileged daemon."""
        # Strip sudo and route through daemon
        if cmd and cmd[0] == 'sudo':
            r = root_exec(cmd[1:], timeout=timeout, stdin=input_data)
            return (r['stdout'], r['stderr'], r['code'])
        try:
            proc = subprocess.run(
                cmd, capture_output=True, text=True,
                timeout=timeout, input=input_data
            )
            return (proc.stdout, proc.stderr, proc.returncode)
        except subprocess.TimeoutExpired:
            return ('', 'Command timed out', 1)
        except Exception as e:
            return ('', str(e), 1)

    def _load_clients(self):
        """Load clients from JSON file."""
        if not self._clients_file.exists():
            return {}
        try:
            with open(self._clients_file, 'r') as f:
                return json.load(f)
        except (json.JSONDecodeError, OSError):
            return {}

    def _save_clients(self, data):
        """Save clients to JSON file."""
        with open(self._clients_file, 'w') as f:
            json.dump(data, f, indent=2)

    def _get_server_public_key(self):
        """Read server public key."""
        # Try file first
        key_path = Path('/etc/wireguard/server_public.key')
        if key_path.exists():
            try:
                return key_path.read_text().strip()
            except OSError:
                pass
        # Try wg show
        stdout, _, rc = self._run_wg_sudo(['show', self._interface, 'public-key'])
        if rc == 0 and stdout.strip():
            return stdout.strip()
        return ''

    def _get_server_endpoint(self):
        """Read server public IP/endpoint."""
        ip_path = Path('/etc/wireguard/server_public_ip')
        if ip_path.exists():
            try:
                return ip_path.read_text().strip()
            except OSError:
                pass
        return ''

    def _adb_bin(self):
        """Get ADB binary path."""
        return find_tool('adb')

    def _run_adb(self, args, timeout=30):
        """Run ADB command, return (stdout, stderr, rc)."""
        adb = self._adb_bin()
        if not adb:
            return ('', 'adb binary not found', 1)
        cmd = [adb] + args
        try:
            proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
            return (proc.stdout, proc.stderr, proc.returncode)
        except subprocess.TimeoutExpired:
            return ('', 'Command timed out', 1)
        except Exception as e:
            return ('', str(e), 1)

    # ── Server Management ────────────────────────────────────────────

    def is_available(self):
        """Check if wg binary exists."""
        return self._wg_bin is not None

    def get_server_status(self):
        """Parse wg show for interface info."""
        stdout, stderr, rc = self._run_wg_sudo(['show', self._interface])
        if rc != 0:
            return {
                'running': False,
                'interface': self._interface,
                'error': stderr.strip() if stderr else 'Interface not running',
            }

        info = {
            'interface': self._interface,
            'running': True,
            'public_key': self._get_server_public_key(),
            'endpoint': f'{self._get_server_endpoint()}:{self._listen_port}',
            'listen_port': self._listen_port,
        }

        for line in stdout.split('\n'):
            line = line.strip()
            if line.startswith('listening port:'):
                info['listen_port'] = line.split(':', 1)[1].strip()
            elif line.startswith('public key:'):
                info['public_key'] = line.split(':', 1)[1].strip()

        # Count peers
        peer_count = stdout.count('peer:')
        info['peer_count'] = peer_count

        return info

    def start_interface(self):
        """Start WireGuard interface with wg-quick."""
        if not self._wg_quick:
            return {'ok': False, 'error': 'wg-quick not found'}
        stdout, stderr, rc = self._run_cmd(
            ['sudo', self._wg_quick, 'up', self._interface], timeout=15)
        if rc == 0:
            return {'ok': True, 'message': f'{self._interface} started'}
        # Already running is not an error
        if 'already exists' in stderr:
            return {'ok': True, 'message': f'{self._interface} already running'}
        return {'ok': False, 'error': stderr.strip() or 'Failed to start'}

    def stop_interface(self):
        """Stop WireGuard interface with wg-quick."""
        if not self._wg_quick:
            return {'ok': False, 'error': 'wg-quick not found'}
        stdout, stderr, rc = self._run_cmd(
            ['sudo', self._wg_quick, 'down', self._interface], timeout=15)
        if rc == 0:
            return {'ok': True, 'message': f'{self._interface} stopped'}
        if 'is not a WireGuard interface' in stderr:
            return {'ok': True, 'message': f'{self._interface} already stopped'}
        return {'ok': False, 'error': stderr.strip() or 'Failed to stop'}

    def restart_interface(self):
        """Restart WireGuard interface."""
        self.stop_interface()
        time.sleep(1)
        return self.start_interface()

    # ── Key Generation ───────────────────────────────────────────────

    def generate_keypair(self):
        """Generate WireGuard keypair. Returns (private_key, public_key)."""
        priv_out, priv_err, priv_rc = self._run_wg(['genkey'])
        if priv_rc != 0:
            return (None, None)
        private_key = priv_out.strip()
        pub_out, pub_err, pub_rc = self._run_wg(['pubkey'], timeout=5)
        # pubkey reads from stdin, need to pipe
        proc = subprocess.run(
            [self._wg_bin, 'pubkey'], input=private_key,
            capture_output=True, text=True, timeout=5
        )
        if proc.returncode != 0:
            return (None, None)
        public_key = proc.stdout.strip()
        return (private_key, public_key)

    def generate_preshared_key(self):
        """Generate WireGuard preshared key."""
        stdout, _, rc = self._run_wg(['genpsk'])
        if rc == 0:
            return stdout.strip()
        return None

    # ── IP Assignment ────────────────────────────────────────────────

    def get_next_ip(self):
        """Get next available client IP in the subnet."""
        try:
            if self._last_ip_file.exists():
                last_octet = int(self._last_ip_file.read_text().strip())
            else:
                last_octet = 1
        except (ValueError, OSError):
            last_octet = 1

        next_octet = last_octet + 1
        self._last_ip_file.write_text(str(next_octet))

        # Extract subnet prefix (e.g. "10.1.0" from "10.1.0.0/24")
        prefix = '.'.join(self._subnet.split('.')[:3])
        return f'{prefix}.{next_octet}'

    # ── Client/Peer Management ───────────────────────────────────────

    def create_client(self, name, dns=None, allowed_ips=None):
        """Create a new WireGuard client/peer."""
        private_key, public_key = self.generate_keypair()
        if not private_key:
            return {'ok': False, 'error': 'Failed to generate keypair'}

        preshared_key = self.generate_preshared_key()
        assigned_ip = self.get_next_ip()

        client_id = str(uuid.uuid4())[:8]
        client = {
            'id': client_id,
            'name': name,
            'private_key': private_key,
            'public_key': public_key,
            'preshared_key': preshared_key or '',
            'assigned_ip': assigned_ip,
            'dns': dns or self._default_dns,
            'allowed_ips': allowed_ips or self._default_allowed_ips,
            'enabled': True,
            'created_at': datetime.now().isoformat(),
        }

        # Add to live WireGuard
        try:
            self._add_peer_to_wg(public_key, preshared_key, assigned_ip)
        except Exception as e:
            return {'ok': False, 'error': f'Failed to add peer to WG: {e}'}

        # Add to config file
        try:
            self._append_peer_to_config(public_key, preshared_key, assigned_ip, name)
        except Exception as e:
            pass  # Non-fatal, peer is live

        # Save to JSON store
        clients = self._load_clients()
        clients[client_id] = client
        self._save_clients(clients)

        return {'ok': True, 'client': client}

    def delete_client(self, client_id):
        """Delete a client/peer."""
        clients = self._load_clients()
        client = clients.get(client_id)
        if not client:
            return {'ok': False, 'error': 'Client not found'}

        # Remove from live WG
        self._remove_peer_from_wg(client['public_key'])

        # Remove from config file
        try:
            self._remove_peer_from_config(client['public_key'])
        except Exception:
            pass

        # Remove from JSON
        del clients[client_id]
        self._save_clients(clients)

        return {'ok': True, 'message': f'Client {client["name"]} deleted'}

    def toggle_client(self, client_id, enabled):
        """Enable or disable a client."""
        clients = self._load_clients()
        client = clients.get(client_id)
        if not client:
            return {'ok': False, 'error': 'Client not found'}

        if enabled and not client.get('enabled', True):
            # Re-add peer
            self._add_peer_to_wg(
                client['public_key'], client.get('preshared_key', ''),
                client['assigned_ip'])
        elif not enabled and client.get('enabled', True):
            # Remove peer
            self._remove_peer_from_wg(client['public_key'])

        client['enabled'] = enabled
        self._save_clients(clients)
        action = 'enabled' if enabled else 'disabled'
        return {'ok': True, 'message': f'Client {client["name"]} {action}'}

    def get_all_clients(self):
        """Get list of all clients."""
        clients = self._load_clients()
        return list(clients.values())

    def get_client(self, client_id):
        """Get single client by ID."""
        clients = self._load_clients()
        return clients.get(client_id)

    def get_peer_status(self):
        """Parse wg show for per-peer stats. Returns dict keyed by public key."""
        stdout, _, rc = self._run_wg_sudo(['show', self._interface])
        if rc != 0:
            return {}

        peers = {}
        current_peer = None

        for line in stdout.split('\n'):
            line = line.strip()
            if line.startswith('peer:'):
                current_peer = line.split(':', 1)[1].strip()
                peers[current_peer] = {
                    'public_key': current_peer,
                    'latest_handshake': None,
                    'latest_handshake_str': '',
                    'transfer_rx': 0,
                    'transfer_tx': 0,
                    'transfer_rx_str': '',
                    'transfer_tx_str': '',
                    'allowed_ips': '',
                    'endpoint': '',
                }
            elif current_peer:
                if line.startswith('latest handshake:'):
                    hs_str = line.split(':', 1)[1].strip()
                    peers[current_peer]['latest_handshake'] = _parse_handshake(hs_str)
                    peers[current_peer]['latest_handshake_str'] = hs_str
                elif line.startswith('transfer:'):
                    transfer = line.split(':', 1)[1].strip()
                    parts = transfer.split(',')
                    if len(parts) == 2:
                        peers[current_peer]['transfer_rx'] = _parse_transfer(parts[0].strip())
                        peers[current_peer]['transfer_tx'] = _parse_transfer(parts[1].strip())
                        peers[current_peer]['transfer_rx_str'] = parts[0].strip().replace('received', '').strip()
                        peers[current_peer]['transfer_tx_str'] = parts[1].strip().replace('sent', '').strip()
                elif line.startswith('allowed ips:'):
                    peers[current_peer]['allowed_ips'] = line.split(':', 1)[1].strip()
                elif line.startswith('endpoint:'):
                    peers[current_peer]['endpoint'] = line.split(':', 1)[1].strip()

        return peers

    def _add_peer_to_wg(self, public_key, preshared_key, allowed_ip):
        """Add peer to live WireGuard interface."""
        if preshared_key:
            stdout, stderr, rc = self._run_cmd(
                ['sudo', self._wg_bin, 'set', self._interface,
                 'peer', public_key,
                 'preshared-key', '/dev/stdin',
                 'allowed-ips', f'{allowed_ip}/32'],
                input_data=preshared_key, timeout=10
            )
        else:
            stdout, stderr, rc = self._run_wg_sudo(
                ['set', self._interface,
                 'peer', public_key,
                 'allowed-ips', f'{allowed_ip}/32'])
        if rc != 0:
            raise RuntimeError(f'wg set failed: {stderr}')

    def _remove_peer_from_wg(self, public_key):
        """Remove peer from live WireGuard interface."""
        self._run_wg_sudo(
            ['set', self._interface, 'peer', public_key, 'remove'])

    def _append_peer_to_config(self, public_key, preshared_key, allowed_ip, name=''):
        """Append [Peer] block to wg0.conf."""
        config_path = Path(self._wg_config_path)
        if not config_path.exists():
            return
        content = config_path.read_text()
        timestamp = time.strftime('%c')
        block = f'\n# Client: {name} - Added {timestamp}\n[Peer]\n'
        block += f'PublicKey = {public_key}\n'
        if preshared_key:
            block += f'PresharedKey = {preshared_key}\n'
        block += f'AllowedIPs = {allowed_ip}/32\n'
        # Write via sudo tee
        self._run_cmd(
            ['sudo', 'tee', '-a', self._wg_config_path],
            input_data=block, timeout=5)

    def _remove_peer_from_config(self, public_key):
        """Remove [Peer] block from wg0.conf."""
        config_path = Path(self._wg_config_path)
        if not config_path.exists():
            return
        # Read via sudo
        stdout, _, rc = self._run_cmd(['sudo', 'cat', self._wg_config_path])
        if rc != 0:
            return
        content = stdout

        lines = content.split('\n')
        new_lines = []
        i = 0
        while i < len(lines):
            line = lines[i]
            # Check comment line preceding peer block
            if line.strip().startswith('# Client:') and i + 1 < len(lines):
                block_lines = [line]
                j = i + 1
                while j < len(lines):
                    if (lines[j].strip() == '' or
                        (lines[j].strip().startswith('[') and lines[j].strip() != '[Peer]') or
                        lines[j].strip().startswith('# Client:')):
                        break
                    block_lines.append(lines[j])
                    j += 1
                if public_key in '\n'.join(block_lines):
                    i = j
                    continue
            elif line.strip() == '[Peer]':
                block_lines = [line]
                j = i + 1
                while j < len(lines):
                    if (lines[j].strip() == '' or
                        (lines[j].strip().startswith('[') and lines[j].strip() != '[Peer]') or
                        lines[j].strip().startswith('# Client:')):
                        break
                    block_lines.append(lines[j])
                    j += 1
                if public_key in '\n'.join(block_lines):
                    i = j
                    continue
            new_lines.append(line)
            i += 1

        cleaned = re.sub(r'\n{3,}', '\n\n', '\n'.join(new_lines))
        # Write back via sudo tee
        self._run_cmd(
            ['sudo', 'tee', self._wg_config_path],
            input_data=cleaned, timeout=5)

    def import_existing_peers(self):
        """Parse wg0.conf and import existing peers into JSON store."""
        stdout, _, rc = self._run_cmd(['sudo', 'cat', self._wg_config_path])
        if rc != 0:
            return {'ok': False, 'error': 'Cannot read WG config', 'imported': 0}

        lines = stdout.split('\n')
        peers = []
        current_peer = None
        pending_name = None

        for line in lines:
            stripped = line.strip()
            name_match = re.match(r'# Client:\s*(.+?)(?:\s*-\s*Added|$)', stripped)
            if name_match:
                pending_name = name_match.group(1).strip()
                continue
            if stripped == '[Peer]':
                current_peer = {'name': pending_name}
                peers.append(current_peer)
                pending_name = None
                continue
            if stripped.startswith('['):
                current_peer = None
                pending_name = None
                continue
            if current_peer is not None and '=' in stripped:
                key, val = stripped.split('=', 1)
                current_peer[key.strip()] = val.strip()

        clients = self._load_clients()
        existing_keys = {c['public_key'] for c in clients.values()}
        imported = 0

        for peer in peers:
            public_key = peer.get('PublicKey')
            allowed_ip = peer.get('AllowedIPs', '').replace('/32', '')
            preshared_key = peer.get('PresharedKey', '')
            name = peer.get('name') or 'legacy-client'

            if not public_key or not allowed_ip:
                continue
            if public_key in existing_keys:
                continue

            # Ensure unique name
            existing_names = {c['name'] for c in clients.values()}
            final_name = name
            counter = 1
            while final_name in existing_names:
                final_name = f'{name}-{counter}'
                counter += 1

            client_id = str(uuid.uuid4())[:8]
            clients[client_id] = {
                'id': client_id,
                'name': final_name,
                'private_key': '',
                'public_key': public_key,
                'preshared_key': preshared_key,
                'assigned_ip': allowed_ip,
                'dns': self._default_dns,
                'allowed_ips': self._default_allowed_ips,
                'enabled': True,
                'created_at': datetime.now().isoformat(),
                'imported': True,
            }
            existing_keys.add(public_key)
            imported += 1

        self._save_clients(clients)
        return {'ok': True, 'imported': imported}

    # ── Client Config Generation ─────────────────────────────────────

    def generate_client_config(self, client):
        """Build the .conf file content for a client."""
        server_pubkey = self._get_server_public_key()
        server_endpoint = self._get_server_endpoint()

        lines = ['[Interface]']
        if client.get('private_key'):
            lines.append(f"PrivateKey = {client['private_key']}")
        lines.append(f"Address = {client['assigned_ip']}/32")
        lines.append(f"DNS = {client.get('dns', self._default_dns)}")
        lines.append('')
        lines.append('[Peer]')
        lines.append(f'PublicKey = {server_pubkey}')
        if client.get('preshared_key'):
            lines.append(f"PresharedKey = {client['preshared_key']}")
        lines.append(f'Endpoint = {server_endpoint}:{self._listen_port}')
        lines.append(f"AllowedIPs = {client.get('allowed_ips', self._default_allowed_ips)}")
        lines.append('PersistentKeepalive = 25')
        lines.append('')
        return '\n'.join(lines)

    def generate_qr_code(self, config_text):
        """Generate QR code PNG bytes from config text."""
        try:
            import qrcode
            qr = qrcode.QRCode(
                version=1, box_size=10, border=4,
                error_correction=qrcode.constants.ERROR_CORRECT_L)
            qr.add_data(config_text)
            qr.make(fit=True)
            img = qr.make_image(fill_color='black', back_color='white')
            buf = io.BytesIO()
            img.save(buf, format='PNG')
            buf.seek(0)
            return buf.getvalue()
        except ImportError:
            return None

    # ── Remote ADB — TCP/IP ──────────────────────────────────────────

    def adb_connect(self, client_ip):
        """Connect to device via ADB TCP/IP over WireGuard tunnel."""
        stdout, stderr, rc = self._run_adb(
            ['connect', f'{client_ip}:5555'], timeout=15)
        output = (stdout + stderr).strip()
        if 'connected' in output.lower():
            return {'ok': True, 'message': output}
        return {'ok': False, 'error': output or 'Connection failed'}

    def adb_disconnect(self, client_ip):
        """Disconnect ADB TCP/IP device."""
        stdout, stderr, rc = self._run_adb(
            ['disconnect', f'{client_ip}:5555'], timeout=10)
        return {'ok': rc == 0, 'message': (stdout + stderr).strip()}

    def get_adb_remote_devices(self):
        """Filter adb devices for WireGuard subnet IPs."""
        stdout, _, rc = self._run_adb(['devices', '-l'], timeout=10)
        if rc != 0:
            return []
        # Extract WG subnet prefix
        prefix = '.'.join(self._subnet.split('.')[:3]) + '.'
        devices = []
        for line in stdout.strip().split('\n')[1:]:  # skip header
            line = line.strip()
            if not line or 'List of' in line:
                continue
            parts = line.split()
            if parts and parts[0].startswith(prefix):
                serial = parts[0]
                state = parts[1] if len(parts) > 1 else 'unknown'
                model = ''
                for p in parts[2:]:
                    if p.startswith('model:'):
                        model = p.split(':', 1)[1]
                devices.append({
                    'serial': serial,
                    'state': state,
                    'model': model,
                    'ip': serial.split(':')[0],
                })
        return devices

    def auto_connect_peers(self):
        """Try ADB connect on all active WG peers."""
        peer_status = self.get_peer_status()
        clients = self._load_clients()
        results = []

        for client in clients.values():
            if not client.get('enabled', True):
                continue
            # Check if peer has recent handshake
            pub_key = client['public_key']
            peer = peer_status.get(pub_key, {})
            hs = peer.get('latest_handshake')
            if hs is not None and hs < 180:  # Active within 3 minutes
                ip = client['assigned_ip']
                result = self.adb_connect(ip)
                results.append({
                    'name': client['name'],
                    'ip': ip,
                    'result': result,
                })

        return {'ok': True, 'results': results, 'attempted': len(results)}

    # ── Remote ADB — USB/IP ──────────────────────────────────────────

    def usbip_available(self):
        """Check if usbip binary exists."""
        return self._usbip_bin is not None

    def check_usbip_modules(self):
        """Check if vhci-hcd kernel module is loaded."""
        stdout, _, rc = self._run_cmd(['lsmod'], timeout=5)
        return 'vhci_hcd' in stdout

    def load_usbip_modules(self):
        """Load vhci-hcd kernel module."""
        stdout, stderr, rc = self._run_cmd(
            ['sudo', 'modprobe', 'vhci-hcd'], timeout=10)
        if rc == 0:
            return {'ok': True, 'message': 'vhci-hcd module loaded'}
        return {'ok': False, 'error': stderr.strip() or 'Failed to load module'}

    def usbip_list_remote(self, client_ip):
        """List exportable USB devices on remote host."""
        if not self._usbip_bin:
            return {'ok': False, 'error': 'usbip not found', 'devices': []}
        stdout, stderr, rc = self._run_cmd(
            ['sudo', self._usbip_bin, 'list', '-r', client_ip], timeout=15)
        if rc != 0:
            return {'ok': False, 'error': stderr.strip() or 'Failed to list',
                    'devices': []}

        devices = []
        current = None
        for line in stdout.split('\n'):
            line = line.strip()
            # Parse device lines like: "1-1: vendor:product ..."
            m = re.match(r'(\d+-[\d.]+):\s*(.+)', line)
            if m:
                current = {
                    'busid': m.group(1),
                    'description': m.group(2).strip(),
                }
                devices.append(current)
            elif current and ':' in line and not line.startswith('usbip'):
                # Additional info lines
                current['description'] += f' | {line}'

        return {'ok': True, 'devices': devices}

    def usbip_attach(self, client_ip, busid):
        """Attach remote USB device via USB/IP."""
        if not self._usbip_bin:
            return {'ok': False, 'error': 'usbip not found'}
        stdout, stderr, rc = self._run_cmd(
            ['sudo', self._usbip_bin, 'attach', '-r', client_ip, '-b', busid],
            timeout=15)
        if rc == 0:
            return {'ok': True, 'message': f'Attached {busid} from {client_ip}'}
        return {'ok': False, 'error': stderr.strip() or 'Failed to attach'}

    def usbip_detach(self, port):
        """Detach USB/IP virtual device by port number."""
        if not self._usbip_bin:
            return {'ok': False, 'error': 'usbip not found'}
        stdout, stderr, rc = self._run_cmd(
            ['sudo', self._usbip_bin, 'detach', '-p', str(port)], timeout=10)
        if rc == 0:
            return {'ok': True, 'message': f'Detached port {port}'}
        return {'ok': False, 'error': stderr.strip() or 'Failed to detach'}

    def usbip_port_status(self):
        """List imported virtual USB devices."""
        if not self._usbip_bin:
            return {'ok': False, 'error': 'usbip not found', 'ports': []}
        stdout, stderr, rc = self._run_cmd(
            ['sudo', self._usbip_bin, 'port'], timeout=10)
        if rc != 0:
            return {'ok': False, 'error': stderr.strip(), 'ports': []}

        ports = []
        current = None
        for line in stdout.split('\n'):
            line = line.strip()
            m = re.match(r'Port\s+(\d+):\s*(.+)', line)
            if m:
                current = {
                    'port': m.group(1),
                    'status': m.group(2).strip(),
                }
                ports.append(current)
            elif current and line and not line.startswith('Port'):
                current['detail'] = line

        return {'ok': True, 'ports': ports}

    def get_usbip_status(self):
        """Combined USB/IP status."""
        available = self.usbip_available()
        modules_loaded = self.check_usbip_modules() if available else False
        ports = self.usbip_port_status() if available else {'ports': []}
        return {
            'available': available,
            'modules_loaded': modules_loaded,
            'active_imports': len(ports.get('ports', [])),
            'ports': ports.get('ports', []),
        }

    # ── UPnP Integration ─────────────────────────────────────────────

    def refresh_upnp_mapping(self):
        """Ensure port 51820/UDP is UPnP-mapped."""
        try:
            from core.upnp import get_upnp_manager
            mgr = get_upnp_manager()
            result = mgr.add_mapping(
                int(self._listen_port), 'UDP',
                f'WireGuard VPN (port {self._listen_port})')
            return result
        except Exception as e:
            return {'ok': False, 'error': str(e)}


# ── Utility Functions ────────────────────────────────────────────────

def _parse_handshake(hs_str):
    """Parse handshake time string into seconds ago, or None."""
    total_seconds = 0
    parts = hs_str.replace(' ago', '').split(',')
    for part in parts:
        part = part.strip()
        match = re.match(r'(\d+)\s+(second|minute|hour|day)', part)
        if match:
            val = int(match.group(1))
            unit = match.group(2)
            if unit == 'second':
                total_seconds += val
            elif unit == 'minute':
                total_seconds += val * 60
            elif unit == 'hour':
                total_seconds += val * 3600
            elif unit == 'day':
                total_seconds += val * 86400
    return total_seconds if total_seconds > 0 else None


def _parse_transfer(s):
    """Parse transfer string like '1.5 MiB' into bytes."""
    match = re.match(r'([\d.]+)\s*(\w+)', s)
    if not match:
        return 0
    val = float(match.group(1))
    unit = match.group(2)
    multipliers = {
        'B': 1, 'KiB': 1024, 'MiB': 1024**2,
        'GiB': 1024**3, 'TiB': 1024**4
    }
    return int(val * multipliers.get(unit, 1))


# ── Singleton ────────────────────────────────────────────────────────

_manager = None

def get_wireguard_manager(config=None):
    global _manager
    if _manager is None:
        # Load config from autarch_settings.conf
        if config is None:
            try:
                from core.config import get_config
                cfg = get_config()
                config = {
                    'config_path': cfg.get('wireguard', 'config_path',
                                           fallback='/etc/wireguard/wg0.conf'),
                    'interface': cfg.get('wireguard', 'interface', fallback='wg0'),
                    'subnet': cfg.get('wireguard', 'subnet', fallback='10.1.0.0/24'),
                    'server_address': cfg.get('wireguard', 'server_address',
                                              fallback='10.1.0.1'),
                    'listen_port': cfg.get('wireguard', 'listen_port', fallback='51820'),
                    'default_dns': cfg.get('wireguard', 'default_dns',
                                           fallback='1.1.1.1, 8.8.8.8'),
                    'default_allowed_ips': cfg.get('wireguard', 'default_allowed_ips',
                                                   fallback='0.0.0.0/0, ::/0'),
                }
            except Exception:
                config = {}
        _manager = WireGuardManager(config)
    return _manager
