#!/usr/bin/env python3
"""Bridge module — transparent inline network bridge.

Creates a transparent L2 bridge between the onboard ethernet adapter and a
USB ethernet adapter.  The bridge has NO IP address — it is invisible on the
network and can only be detected by physical inspection.

Provides 802.1X bypass via silent bridge (EAP passthrough) and suppresses
STP/BPDU/CDP/LLDP frames via ebtables to avoid triggering network monitoring.

A C watchdog thread monitors bridge health and restores direct connectivity
within 15 seconds if the bridge fails.
"""

import logging
import os
import subprocess
import threading
import time
from pathlib import Path
from typing import Optional

from modules.base import BaseModule

logger = logging.getLogger(__name__)

BRIDGE_NAME = "br0"
WATCHDOG_INTERVAL = 5.0  # seconds between health checks
WATCHDOG_MAX_FAILURES = 3  # consecutive failures before teardown
FAILSAFE_TIMEOUT = 15  # seconds — must restore connectivity within this


class Bridge(BaseModule):
    """Transparent inline bridge — invisible on the network."""

    name = "bridge"
    module_type = "connectivity"
    priority = -300
    requires_root = True

    def __init__(self, bus, state, config, engine=None):
        super().__init__(bus, state, config, engine)
        self._if1: Optional[str] = None
        self._if2: Optional[str] = None
        self._bridge_up = False
        self._watchdog_thread: Optional[threading.Thread] = None
        self._consecutive_failures = 0
        self._scripts_dir: Optional[str] = None

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

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

        br_cfg = self.config.get("connectivity", {}).get("bridge", {})
        if not br_cfg:
            logger.error("No bridge config in bigbrother.yaml")
            self.state.set_module_status(self.name, "error")
            return

        self._if1 = br_cfg.get("interface1")
        self._if2 = br_cfg.get("interface2")

        if not self._if1 or not self._if2:
            logger.error("Bridge requires interface1 and interface2 in config")
            self.state.set_module_status(self.name, "error")
            return

        # Locate scripts directory
        install_path = self.config.get("device", {}).get("install_path", "/opt/.cache/bb")
        self._scripts_dir = os.path.join(install_path, "scripts")

        if not self.setup_bridge(self._if1, self._if2):
            self.state.set_module_status(self.name, "error")
            return

        # Start watchdog thread
        self._running = True
        self._pid = os.getpid()
        self._start_time = time.time()
        self._bridge_up = True

        self._watchdog_thread = threading.Thread(
            target=self._watchdog_loop, daemon=True, name="sensor-bridge-watchdog",
        )
        self._watchdog_thread.start()

        self.state.set_module_status(self.name, "running", pid=self._pid)
        self.bus.emit("CONNECTIVITY_CHANGED",
                      {"module": self.name, "state": "up",
                       "bridge": BRIDGE_NAME, "if1": self._if1, "if2": self._if2},
                      source_module=self.name)
        logger.info("Bridge %s active: %s <-> %s", BRIDGE_NAME, self._if1, self._if2)

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

        self._running = False

        if self._watchdog_thread and self._watchdog_thread.is_alive():
            self._watchdog_thread.join(timeout=5.0)

        self.teardown_bridge()
        self._bridge_up = False

        self.state.set_module_status(self.name, "stopped")
        self.bus.emit("CONNECTIVITY_CHANGED",
                      {"module": self.name, "state": "down"},
                      source_module=self.name)
        logger.info("Bridge torn down")

    def status(self) -> dict:
        return {
            "running": self._running,
            "pid": self._pid,
            "uptime": time.time() - self._start_time if self._start_time else 0,
            "bridge_up": self._bridge_up,
            "bridge_name": BRIDGE_NAME,
            "interface1": self._if1,
            "interface2": self._if2,
            "healthy": self.is_healthy(),
            "consecutive_failures": self._consecutive_failures,
        }

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

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

    # ------------------------------------------------------------------
    # Bridge operations
    # ------------------------------------------------------------------

    def setup_bridge(self, if1: str, if2: str) -> bool:
        """Create transparent bridge between two interfaces.

        Tries the shell script first; falls back to inline commands if
        the script is missing.
        """
        script = os.path.join(self._scripts_dir, "setup_bridge.sh") if self._scripts_dir else None

        if script and os.path.isfile(script):
            try:
                result = subprocess.run(
                    ["bash", script, if1, if2],
                    capture_output=True, text=True, timeout=30,
                )
                if result.returncode == 0:
                    logger.info("Bridge setup via script succeeded")
                    return True
                logger.warning("Bridge script failed: %s", result.stderr.strip())
            except (subprocess.TimeoutExpired, OSError) as exc:
                logger.warning("Bridge script error: %s", exc)

        # Fallback: inline bridge setup
        return self._setup_bridge_inline(if1, if2)

    def teardown_bridge(self) -> bool:
        """Remove bridge and restore interfaces."""
        script = os.path.join(self._scripts_dir, "teardown_bridge.sh") if self._scripts_dir else None

        if script and os.path.isfile(script):
            try:
                result = subprocess.run(
                    ["bash", script],
                    capture_output=True, text=True, timeout=30,
                )
                if result.returncode == 0:
                    logger.info("Bridge teardown via script succeeded")
                    return True
                logger.warning("Teardown script failed: %s", result.stderr.strip())
            except (subprocess.TimeoutExpired, OSError) as exc:
                logger.warning("Teardown script error: %s", exc)

        # Fallback: inline teardown
        return self._teardown_bridge_inline()

    def is_healthy(self) -> bool:
        """Check if the bridge interface exists and both ports are attached."""
        try:
            # Check bridge exists
            result = subprocess.run(
                ["ip", "link", "show", BRIDGE_NAME],
                capture_output=True, timeout=5,
            )
            if result.returncode != 0:
                return False

            # Check both interfaces are in the bridge
            result = subprocess.run(
                ["bridge", "link", "show", "dev", self._if1],
                capture_output=True, text=True, timeout=5,
            )
            if BRIDGE_NAME not in result.stdout:
                return False

            result = subprocess.run(
                ["bridge", "link", "show", "dev", self._if2],
                capture_output=True, text=True, timeout=5,
            )
            if BRIDGE_NAME not in result.stdout:
                return False

            return True

        except (subprocess.TimeoutExpired, OSError):
            return False

    # ------------------------------------------------------------------
    # Inline bridge commands (fallback if scripts missing)
    # ------------------------------------------------------------------

    def _setup_bridge_inline(self, if1: str, if2: str) -> bool:
        """Create bridge using ip/bridge/ebtables commands directly."""
        commands = [
            # Create bridge, no IP
            ["ip", "link", "add", "name", BRIDGE_NAME, "type", "bridge"],
            # Disable STP
            ["ip", "link", "set", BRIDGE_NAME, "type", "bridge", "stp_state", "0"],
            # Disable forwarding delay
            ["ip", "link", "set", BRIDGE_NAME, "type", "bridge", "forward_delay", "0"],
            # Set promiscuous mode on interfaces
            ["ip", "link", "set", if1, "promisc", "on"],
            ["ip", "link", "set", if2, "promisc", "on"],
            # Flush any existing IP addresses
            ["ip", "addr", "flush", "dev", if1],
            ["ip", "addr", "flush", "dev", if2],
            # Add interfaces to bridge
            ["ip", "link", "set", if1, "master", BRIDGE_NAME],
            ["ip", "link", "set", if2, "master", BRIDGE_NAME],
            # Bring everything up
            ["ip", "link", "set", "up", "dev", if1],
            ["ip", "link", "set", "up", "dev", if2],
            ["ip", "link", "set", "up", "dev", BRIDGE_NAME],
        ]

        for cmd in commands:
            try:
                subprocess.run(cmd, check=True, capture_output=True, timeout=10)
            except subprocess.CalledProcessError as exc:
                logger.error("Bridge setup command failed: %s — %s",
                             " ".join(cmd), exc.stderr.decode(errors='replace'))
                self._teardown_bridge_inline()
                return False
            except subprocess.TimeoutExpired:
                logger.error("Bridge setup command timed out: %s", " ".join(cmd))
                self._teardown_bridge_inline()
                return False

        # Suppress STP/BPDU/CDP/LLDP via ebtables
        self._apply_ebtables_rules(if1, if2)

        logger.info("Inline bridge setup complete: %s <-> %s via %s", if1, if2, BRIDGE_NAME)
        return True

    def _teardown_bridge_inline(self) -> bool:
        """Remove bridge and restore interfaces."""
        # Remove ebtables rules (best-effort)
        self._remove_ebtables_rules()

        commands = [
            ["ip", "link", "set", "down", "dev", BRIDGE_NAME],
        ]

        # Remove interfaces from bridge
        if self._if1:
            commands.append(["ip", "link", "set", self._if1, "nomaster"])
            commands.append(["ip", "link", "set", self._if1, "promisc", "off"])
        if self._if2:
            commands.append(["ip", "link", "set", self._if2, "nomaster"])
            commands.append(["ip", "link", "set", self._if2, "promisc", "off"])

        commands.append(["ip", "link", "del", BRIDGE_NAME])

        for cmd in commands:
            try:
                subprocess.run(cmd, capture_output=True, timeout=10)
            except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
                pass  # Best-effort teardown

        logger.info("Inline bridge teardown complete")
        return True

    def _apply_ebtables_rules(self, if1: str, if2: str) -> None:
        """Apply ebtables rules to suppress discovery protocols."""
        # STP/BPDU destination: 01:80:C2:00:00:00
        # CDP/VTP: 01:00:0C:CC:CC:CC
        # LLDP: 01:80:C2:00:00:0E
        suppress_macs = [
            "01:80:C2:00:00:00",  # STP/BPDU
            "01:00:0C:CC:CC:CC",  # CDP/VTP
            "01:80:C2:00:00:0E",  # LLDP
            "01:00:0C:CC:CC:CD",  # Cisco PVST+
        ]

        for mac in suppress_macs:
            for direction in ["FORWARD", "OUTPUT"]:
                cmd = ["ebtables", "-A", direction, "-d", mac, "-j", "DROP"]
                try:
                    subprocess.run(cmd, capture_output=True, timeout=5)
                except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
                    logger.debug("ebtables rule failed (may not be installed): %s", " ".join(cmd))

    def _remove_ebtables_rules(self) -> None:
        """Flush ebtables rules."""
        try:
            subprocess.run(["ebtables", "-F"], capture_output=True, timeout=5)
            subprocess.run(["ebtables", "-X"], capture_output=True, timeout=5)
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
            pass

    # ------------------------------------------------------------------
    # Watchdog thread
    # ------------------------------------------------------------------

    def _watchdog_loop(self) -> None:
        """Monitor bridge health; restore direct connectivity on failure."""
        while self._running:
            time.sleep(WATCHDOG_INTERVAL)
            if not self._running:
                break

            if self.is_healthy():
                self._consecutive_failures = 0
                continue

            self._consecutive_failures += 1
            logger.warning("Bridge health check failed (%d/%d)",
                           self._consecutive_failures, WATCHDOG_MAX_FAILURES)

            if self._consecutive_failures >= WATCHDOG_MAX_FAILURES:
                logger.error("Bridge failsafe triggered — restoring direct connectivity")
                self.bus.emit("MODULE_ERROR",
                              {"module": self.name,
                               "error": "bridge_failsafe_triggered",
                               "consecutive_failures": self._consecutive_failures},
                              source_module=self.name)

                # Teardown broken bridge and restore direct connection
                self._teardown_bridge_inline()
                self._bridge_up = False

                # Try to re-establish bridge
                if self._if1 and self._if2:
                    time.sleep(2.0)
                    if self._setup_bridge_inline(self._if1, self._if2):
                        self._bridge_up = True
                        self._consecutive_failures = 0
                        logger.info("Bridge recovered after failsafe")
                    else:
                        logger.error("Bridge recovery failed — operating without bridge")
                        self._running = False
