"""
RSSI data collection from Linux WiFi interfaces.

Provides two concrete collectors:
    - LinuxWifiCollector: reads real RSSI from /proc/net/wireless and iw commands
    - SimulatedCollector: produces deterministic synthetic signals for testing

Both share the same WifiSample dataclass and thread-safe ring buffer.
"""

from __future__ import annotations

import logging
import math
import re
import subprocess
import threading
import time
from collections import deque
from dataclasses import dataclass, field
from typing import Deque, List, Optional, Protocol

import numpy as np

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Data types
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class WifiSample:
    """A single WiFi measurement sample."""

    timestamp: float          # UNIX epoch seconds (time.time())
    rssi_dbm: float           # Received signal strength in dBm
    noise_dbm: float          # Noise floor in dBm
    link_quality: float       # Link quality 0-1 (normalised)
    tx_bytes: int             # Cumulative TX bytes
    rx_bytes: int             # Cumulative RX bytes
    retry_count: int          # Cumulative retry count
    interface: str            # WiFi interface name


# ---------------------------------------------------------------------------
# Thread-safe ring buffer
# ---------------------------------------------------------------------------

class RingBuffer:
    """Thread-safe fixed-size ring buffer for WifiSample objects."""

    def __init__(self, max_size: int) -> None:
        self._buf: Deque[WifiSample] = deque(maxlen=max_size)
        self._lock = threading.Lock()

    def append(self, sample: WifiSample) -> None:
        with self._lock:
            self._buf.append(sample)

    def get_all(self) -> List[WifiSample]:
        """Return a snapshot of all samples (oldest first)."""
        with self._lock:
            return list(self._buf)

    def get_last_n(self, n: int) -> List[WifiSample]:
        """Return the most recent *n* samples."""
        with self._lock:
            items = list(self._buf)
            return items[-n:] if n < len(items) else items

    def __len__(self) -> int:
        with self._lock:
            return len(self._buf)

    def clear(self) -> None:
        with self._lock:
            self._buf.clear()


# ---------------------------------------------------------------------------
# Collector protocol
# ---------------------------------------------------------------------------

class WifiCollector(Protocol):
    """Protocol that all WiFi collectors must satisfy."""

    def start(self) -> None: ...
    def stop(self) -> None: ...
    def get_samples(self, n: Optional[int] = None) -> List[WifiSample]: ...
    @property
    def sample_rate_hz(self) -> float: ...


# ---------------------------------------------------------------------------
# Linux WiFi collector (real hardware)
# ---------------------------------------------------------------------------

class LinuxWifiCollector:
    """
    Collects real RSSI data from a Linux WiFi interface.

    Data sources:
        - /proc/net/wireless  (RSSI, noise, link quality)
        - iw dev <iface> station dump  (TX/RX bytes, retry count)

    Parameters
    ----------
    interface : str
        WiFi interface name, e.g. ``"wlan0"``.
    sample_rate_hz : float
        Target sampling rate in Hz (default 10).
    buffer_seconds : int
        How many seconds of history to keep in the ring buffer (default 120).
    """

    def __init__(
        self,
        interface: str = "wlan0",
        sample_rate_hz: float = 10.0,
        buffer_seconds: int = 120,
    ) -> None:
        self._interface = interface
        self._rate = sample_rate_hz
        self._buffer = RingBuffer(max_size=int(sample_rate_hz * buffer_seconds))
        self._running = False
        self._thread: Optional[threading.Thread] = None

    # -- public API ----------------------------------------------------------

    @property
    def sample_rate_hz(self) -> float:
        return self._rate

    def start(self) -> None:
        """Start the background sampling thread."""
        if self._running:
            return
        self._validate_interface()
        self._running = True
        self._thread = threading.Thread(
            target=self._sample_loop, daemon=True, name="wifi-rssi-collector"
        )
        self._thread.start()
        logger.info(
            "LinuxWifiCollector started on %s at %.1f Hz",
            self._interface,
            self._rate,
        )

    def stop(self) -> None:
        """Stop the background sampling thread."""
        self._running = False
        if self._thread is not None:
            self._thread.join(timeout=2.0)
            self._thread = None
        logger.info("LinuxWifiCollector stopped")

    def get_samples(self, n: Optional[int] = None) -> List[WifiSample]:
        """
        Return collected samples.

        Parameters
        ----------
        n : int or None
            If given, return only the most recent *n* samples.
        """
        if n is not None:
            return self._buffer.get_last_n(n)
        return self._buffer.get_all()

    def collect_once(self) -> WifiSample:
        """Collect a single sample right now (blocking)."""
        return self._read_sample()

    # -- internals -----------------------------------------------------------

    def _validate_interface(self) -> None:
        """Check that the interface exists on this machine."""
        try:
            with open("/proc/net/wireless", "r") as f:
                content = f.read()
            if self._interface not in content:
                raise RuntimeError(
                    f"WiFi interface '{self._interface}' not found in "
                    f"/proc/net/wireless. Available interfaces may include: "
                    f"{self._parse_interface_names(content)}. "
                    f"Ensure the interface is up and associated with an AP."
                )
        except FileNotFoundError:
            raise RuntimeError(
                "Cannot read /proc/net/wireless. "
                "This collector requires a Linux system with wireless-extensions support. "
                "If running in a container or VM without WiFi hardware, use "
                "SimulatedCollector instead."
            )

    @staticmethod
    def _parse_interface_names(proc_content: str) -> List[str]:
        """Extract interface names from /proc/net/wireless content."""
        names: List[str] = []
        for line in proc_content.splitlines()[2:]:  # skip header lines
            parts = line.split(":")
            if len(parts) >= 2:
                names.append(parts[0].strip())
        return names

    def _sample_loop(self) -> None:
        interval = 1.0 / self._rate
        while self._running:
            t0 = time.monotonic()
            try:
                sample = self._read_sample()
                self._buffer.append(sample)
            except Exception:
                logger.exception("Error reading WiFi sample")
            elapsed = time.monotonic() - t0
            sleep_time = max(0.0, interval - elapsed)
            if sleep_time > 0:
                time.sleep(sleep_time)

    def _read_sample(self) -> WifiSample:
        """Read one sample from the OS."""
        rssi, noise, quality = self._read_proc_wireless()
        tx_bytes, rx_bytes, retries = self._read_iw_station()
        return WifiSample(
            timestamp=time.time(),
            rssi_dbm=rssi,
            noise_dbm=noise,
            link_quality=quality,
            tx_bytes=tx_bytes,
            rx_bytes=rx_bytes,
            retry_count=retries,
            interface=self._interface,
        )

    def _read_proc_wireless(self) -> tuple[float, float, float]:
        """Parse /proc/net/wireless for the configured interface."""
        try:
            with open("/proc/net/wireless", "r") as f:
                for line in f:
                    if self._interface in line:
                        # Format: iface: status quality signal noise ...
                        parts = line.split()
                        # parts[0] = "wlan0:", parts[2]=quality, parts[3]=signal, parts[4]=noise
                        quality_raw = float(parts[2].rstrip("."))
                        signal_raw = float(parts[3].rstrip("."))
                        noise_raw = float(parts[4].rstrip("."))
                        # Normalise quality to 0..1 (max is typically 70)
                        quality = min(1.0, max(0.0, quality_raw / 70.0))
                        return signal_raw, noise_raw, quality
        except (FileNotFoundError, IndexError, ValueError) as exc:
            raise RuntimeError(
                f"Failed to read /proc/net/wireless for {self._interface}: {exc}"
            ) from exc
        raise RuntimeError(
            f"Interface {self._interface} not found in /proc/net/wireless"
        )

    def _read_iw_station(self) -> tuple[int, int, int]:
        """Run ``iw dev <iface> station dump`` and parse TX/RX/retries."""
        try:
            result = subprocess.run(
                ["iw", "dev", self._interface, "station", "dump"],
                capture_output=True,
                text=True,
                timeout=2.0,
            )
            text = result.stdout

            tx_bytes = self._extract_int(text, r"tx bytes:\s*(\d+)")
            rx_bytes = self._extract_int(text, r"rx bytes:\s*(\d+)")
            retries = self._extract_int(text, r"tx retries:\s*(\d+)")
            return tx_bytes, rx_bytes, retries
        except (FileNotFoundError, subprocess.TimeoutExpired):
            # iw not installed or timed out -- degrade gracefully
            return 0, 0, 0

    @staticmethod
    def _extract_int(text: str, pattern: str) -> int:
        m = re.search(pattern, text)
        return int(m.group(1)) if m else 0


# ---------------------------------------------------------------------------
# Simulated collector (deterministic, for testing)
# ---------------------------------------------------------------------------

class SimulatedCollector:
    """
    Deterministic simulated WiFi collector for testing.

    Generates a synthetic RSSI signal composed of:
        - A constant baseline (-50 dBm default)
        - An optional sinusoidal component (configurable frequency/amplitude)
        - Optional step-change injection (for change-point testing)
        - Deterministic noise from a seeded PRNG

    This is explicitly a test/development tool and makes no attempt to
    appear as real hardware.

    Parameters
    ----------
    seed : int
        Random seed for deterministic output.
    sample_rate_hz : float
        Target sampling rate in Hz (default 10).
    buffer_seconds : int
        Ring buffer capacity in seconds (default 120).
    baseline_dbm : float
        RSSI baseline in dBm (default -50).
    sine_freq_hz : float
        Frequency of the sinusoidal RSSI component (default 0.3 Hz, breathing band).
    sine_amplitude_dbm : float
        Amplitude of the sinusoidal component (default 2.0 dBm).
    noise_std_dbm : float
        Standard deviation of additive Gaussian noise (default 0.5 dBm).
    step_change_at : float or None
        If set, inject a step change of ``step_change_dbm`` at this time offset
        (seconds from start).
    step_change_dbm : float
        Magnitude of the step change (default -10 dBm).
    """

    def __init__(
        self,
        seed: int = 42,
        sample_rate_hz: float = 10.0,
        buffer_seconds: int = 120,
        baseline_dbm: float = -50.0,
        sine_freq_hz: float = 0.3,
        sine_amplitude_dbm: float = 2.0,
        noise_std_dbm: float = 0.5,
        step_change_at: Optional[float] = None,
        step_change_dbm: float = -10.0,
    ) -> None:
        self._rate = sample_rate_hz
        self._buffer = RingBuffer(max_size=int(sample_rate_hz * buffer_seconds))
        self._rng = np.random.default_rng(seed)

        self._baseline = baseline_dbm
        self._sine_freq = sine_freq_hz
        self._sine_amp = sine_amplitude_dbm
        self._noise_std = noise_std_dbm
        self._step_at = step_change_at
        self._step_dbm = step_change_dbm

        self._running = False
        self._thread: Optional[threading.Thread] = None
        self._start_time: float = 0.0
        self._sample_index: int = 0

    # -- public API ----------------------------------------------------------

    @property
    def sample_rate_hz(self) -> float:
        return self._rate

    def start(self) -> None:
        if self._running:
            return
        self._running = True
        self._start_time = time.time()
        self._sample_index = 0
        self._thread = threading.Thread(
            target=self._sample_loop, daemon=True, name="sim-rssi-collector"
        )
        self._thread.start()
        logger.info("SimulatedCollector started at %.1f Hz (seed reused from init)", self._rate)

    def stop(self) -> None:
        self._running = False
        if self._thread is not None:
            self._thread.join(timeout=2.0)
            self._thread = None

    def get_samples(self, n: Optional[int] = None) -> List[WifiSample]:
        if n is not None:
            return self._buffer.get_last_n(n)
        return self._buffer.get_all()

    def generate_samples(self, duration_seconds: float) -> List[WifiSample]:
        """
        Generate a batch of samples without the background thread.

        Useful for unit tests that need a known signal without timing jitter.

        Parameters
        ----------
        duration_seconds : float
            How many seconds of signal to produce.

        Returns
        -------
        list of WifiSample
        """
        n_samples = int(duration_seconds * self._rate)
        samples: List[WifiSample] = []
        base_time = time.time()
        for i in range(n_samples):
            t = i / self._rate
            sample = self._make_sample(base_time + t, t, i)
            samples.append(sample)
        return samples

    # -- internals -----------------------------------------------------------

    def _sample_loop(self) -> None:
        interval = 1.0 / self._rate
        while self._running:
            t0 = time.monotonic()
            now = time.time()
            t_offset = now - self._start_time
            sample = self._make_sample(now, t_offset, self._sample_index)
            self._buffer.append(sample)
            self._sample_index += 1
            elapsed = time.monotonic() - t0
            sleep_time = max(0.0, interval - elapsed)
            if sleep_time > 0:
                time.sleep(sleep_time)

    def _make_sample(self, timestamp: float, t_offset: float, index: int) -> WifiSample:
        """Build one deterministic sample."""
        # Sinusoidal component
        sine = self._sine_amp * math.sin(2.0 * math.pi * self._sine_freq * t_offset)

        # Deterministic Gaussian noise (uses the seeded RNG)
        noise = self._rng.normal(0.0, self._noise_std)

        # Step change
        step = 0.0
        if self._step_at is not None and t_offset >= self._step_at:
            step = self._step_dbm

        rssi = self._baseline + sine + noise + step

        return WifiSample(
            timestamp=timestamp,
            rssi_dbm=float(rssi),
            noise_dbm=-95.0,
            link_quality=max(0.0, min(1.0, (rssi + 100.0) / 60.0)),
            tx_bytes=index * 1500,
            rx_bytes=index * 3000,
            retry_count=max(0, index // 100),
            interface="sim0",
        )
