"""
Layer 3: RF Perturbation Sensor
Detects perturbations in ambient EMF field — not signal extraction,
but field texture reading via RTL-SDR or sound card + wire coil.

States: CALM, STIR, STORM, DEAD
Metrics: amplitude_rms, spectral_centroid, spectral_spread, zero_crossing_rate
"""
import os
import json
import time
import numpy as np
from pathlib import Path
from datetime import datetime
from typing import Literal, Optional

BASE_DIR = Path(__file__).parent.parent
MEMORY_DIR = BASE_DIR / "memory"
DATA_DIR = BASE_DIR / "data" / "emf"

FieldState = Literal["CALM", "STIR", "STORM", "DEAD"]

# Threshold configuration
THRESHOLDS = {
    "calm_max_dbfs": -60.0,
    "stir_max_dbfs": -40.0,
    "storm_max_dbfs": -20.0,
}


class PerturbationSensor:
    """
    RF perturbation sensor using RTL-SDR or sound card + wire coil.
    Reads ambient EMF field texture — not demodulated signals,
    but field characteristics: amplitude, spectral color, texture, activity.
    """

    def __init__(
        self,
        source: Literal["rtlsdr", "soundcard", "simulation"] = "simulation",
        sample_rate: int = 250000,
        center_freq: int = 100000000,
        gain: int = 40,
    ):
        self.source = source
        self.sample_rate = sample_rate
        self.center_freq = center_freq
        self.gain = gain
        self.state: FieldState = "DEAD"
        self.metrics = {
            "amplitude_rms_dbfs": -80.0,
            "spectral_centroid_hz": 0,
            "spectral_spread_hz": 0,
            "zero_crossing_rate": 0.0,
            "dominant_frequency_hz": 0,
            "perturbation_count_60s": 0,
        }
        self.sources_detected: list[str] = []
        self.interpretation: str = "no field detectable"
        self._perturbation_history: list[tuple[float, float]] = []  # (timestamp, amplitude)
        self._last_sample_time: Optional[float] = None

    def _read_rtlsdr(self) -> Optional[np.ndarray]:
        """Read samples from RTL-SDR dongle."""
        try:
            from rtlsdr import RtlSdr
            sdr = RtlSdr()
            sdr.sample_rate = self.sample_rate
            sdr.center_freq = self.center_freq
            sdr.gain = self.gain
            samples = sdr.read_samples(256 * 1024)
            sdr.close()
            return samples
        except Exception:
            return None

    def _read_soundcard(self) -> Optional[np.ndarray]:
        """Read samples from sound card line-in (wire coil interface)."""
        try:
            import pyaudio
            p = pyaudio.PyAudio()
            stream = p.open(
                format=pyaudio.paInt16,
                channels=1,
                rate=44100,
                input=True,
                input_device_index=None,
                frames_per_buffer=1024,
            )
            data = stream.read(1024)
            stream.stop_stream()
            p.terminate()
            # Convert to numpy array, normalize
            audio_data = np.frombuffer(data, dtype=np.int16)
            return audio_data.astype(np.float32) / 32768.0
        except Exception:
            return None

    def _simulate_field(self) -> np.ndarray:
        """Generate simulated RF field for testing without hardware."""
        # Power line hum + occasional perturbations
        t = time.time()
        freq_60hz = 60.0
        freq_180hz = 180.0  # 3rd harmonic
        n = 1024
        ts = np.linspace(t, t + n / 44100, n)
        signal = (
            0.3 * np.sin(2 * np.pi * freq_60hz * ts)
            + 0.1 * np.sin(2 * np.pi * freq_180hz * ts)
            + 0.05 * np.random.randn(n)
        )
        # Occasional perturbation burst
        if int(t) % 30 < 2:
            burst_start = int(t) % 10
            signal[burst_start : burst_start + 50] += 0.8 * np.random.randn(50)
        return signal.astype(np.float32)

    def _compute_metrics(self, samples: np.ndarray) -> dict:
        """Compute perturbation metrics from raw samples."""
        if len(samples) < 2:
            return self.metrics

        # RMS amplitude
        rms = np.sqrt(np.mean(samples**2))
        rms_dbfs = 20 * np.log10(rms + 1e-12)

        # Zero crossing rate
        zcr = np.sum(np.diff(np.sign(samples)) != 0) / len(samples)

        # Spectral analysis via FFT
        fft_vals = np.fft.rfft(samples)
        fft_mag = np.abs(fft_vals)
        freqs = np.fft.rfftfreq(len(samples), 1.0 / 44100)

        # Spectral centroid
        if fft_mag.sum() > 0:
            spectral_centroid = np.sum(freqs * fft_mag) / np.sum(fft_mag)
        else:
            spectral_centroid = 0.0

        # Spectral spread
        if fft_mag.sum() > 0:
            spectral_spread = np.sqrt(
                np.sum(((freqs - spectral_centroid) ** 2) * fft_mag) / np.sum(fft_mag)
            )
        else:
            spectral_spread = 0.0

        # Dominant frequency
        dom_idx = np.argmax(fft_mag)
        dominant_freq = freqs[dom_idx]

        # Perturbation count in last 60s
        now = time.time()
        self._perturbation_history = [
            (ts, amp) for ts, amp in self._perturbation_history if now - ts < 60
        ]
        if rms_dbfs > THRESHOLDS["calm_max_dbfs"]:
            self._perturbation_history.append((now, rms_dbfs))
        perturbation_count = len(self._perturbation_history)

        return {
            "amplitude_rms_dbfs": float(rms_dbfs),
            "spectral_centroid_hz": float(spectral_centroid),
            "spectral_spread_hz": float(spectral_spread),
            "zero_crossing_rate": float(zcr),
            "dominant_frequency_hz": float(dominant_freq),
            "perturbation_count_60s": perturbation_count,
        }

    def _detect_sources(self, metrics: dict) -> list[str]:
        """Infer likely EMF sources from spectral characteristics."""
        sources = []
        centroid = metrics["spectral_centroid_hz"]
        dom_freq = metrics["dominant_frequency_hz"]
        zcr = metrics["zero_crossing_rate"]

        if dom_freq < 70 and dom_freq > 50:
            sources.append("power_line_60hz")
        if dom_freq > 150 and dom_freq < 220:
            sources.append("power_line_60hz_harmonics")
        if centroid > 800000 and centroid < 900000:
            sources.append("cellular_band_850mhz")
        if centroid > 2400000000 and centroid < 2500000000:
            sources.append("wifi_2.4ghz")
        if centroid > 88000000 and centroid < 108000000:
            sources.append("fm_radio_88-108mhz")
        if zcr > 0.3:
            sources.append("switch_mode_psu_noise")
        if metrics["perturbation_count_60s"] > 10:
            sources.append("lightning_sferics")

        return sources

    def _classify_state(self, metrics: dict) -> FieldState:
        """Classify field state from metrics."""
        rms = metrics["amplitude_rms_dbfs"]
        perturb = metrics["perturbation_count_60s"]

        if rms < -80.0 or perturb == 0:
            return "DEAD"
        elif rms < THRESHOLDS["calm_max_dbfs"]:
            return "CALM"
        elif rms < THRESHOLDS["stir_max_dbfs"]:
            return "STIR"
        else:
            return "STORM"

    def _interpret(self, state: FieldState, metrics: dict, sources: list[str]) -> str:
        """Generate human-readable interpretation."""
        interpretations = {
            "DEAD": "no field detectable — antenna disconnected or shielded environment",
            "CALM": "rural/suburban field — minimal human EM activity",
            "STIR": "urban field — moderate human EM activity, cellular and WiFi present",
            "STORM": "dense EM environment — strong RF sources nearby, possible lightning",
        }
        base = interpretations.get(state, "unknown")
        if sources:
            base += f". Detected: {', '.join(sources)}."
        return base

    def read(self) -> dict:
        """Take one reading: acquire samples, compute metrics, classify state."""
        # Acquire samples based on source
        if self.source == "rtlsdr":
            samples = self._read_rtlsdr()
        elif self.source == "soundcard":
            samples = self._read_soundcard()
        else:
            samples = self._simulate_field()

        if samples is None:
            self.state = "DEAD"
            self.metrics = {k: 0.0 for k in self.metrics}
            self.sources_detected = []
            self.interpretation = "sensor unavailable"
        else:
            self.metrics = self._compute_metrics(samples)
            self.sources_detected = self._detect_sources(self.metrics)
            self.state = self._classify_state(self.metrics)
            self.interpretation = self._interpret(self.state, self.metrics, self.sources_detected)

        return self.current_reading()

    def current_reading(self) -> dict:
        """Return current reading as structured dict."""
        return {
            "timestamp": datetime.now().isoformat(),
            "source": self.source,
            "field_state": self.state,
            "metrics": self.metrics,
            "sources_detected": self.sources_detected,
            "interpretation": self.interpretation,
        }

    def write(self, output_dir: Path = MEMORY_DIR) -> Path:
        """Write perturbation.md to memory directory."""
        output_dir.mkdir(parents=True, exist_ok=True)
        path = output_dir / "perturbation.md"
        reading = self.current_reading()
        m = reading["metrics"]
        content = f"""# Brain Perturbation — RF Layer

## Timestamp
 {reading['timestamp']}

## Source
 {reading['source']}

## Field State
 {reading['field_state']}

## Metrics
| Metric | Value |
|--------|-------|
| Amplitude RMS | {m['amplitude_rms_dbfs']:.1f} dBFS |
| Spectral Centroid | {m['spectral_centroid_hz']:.0f} Hz |
| Spectral Spread | {m['spectral_spread_hz']:.0f} Hz |
| Dominant Frequency | {m['dominant_frequency_hz']:.0f} Hz |
| Perturbation Count (60s) | {m['perturbation_count_60s']} |

## Sources Detected
{chr(10).join(f"- {s}" for s in reading['sources_detected']) if reading['sources_detected'] else "- none"}

## Interpretation
{reading['interpretation']}

## States
- CALM: field stable below -60 dBFS
- STIR: minor perturbations (-60 to -40 dBFS)
- STORM: major perturbations (-40 to -20 dBFS)
- DEAD: no field detectable
"""
        path.write_text(content)
        # Also write JSON for programmatic access
        json_path = DATA_DIR / "perturbation_latest.json"
        json_path.parent.mkdir(parents=True, exist_ok=True)
        json_path.write_text(json.dumps(reading, indent=2))
        return path


def daemon(sensor: PerturbationSensor, interval_s: float = 5.0):
    """Run perturbation sensor as daemon."""
    print(f"Perturbation sensor starting — source: {sensor.source}, interval: {interval_s}s")
    while True:
        sensor.read()
        sensor.write()
        print(f"[{datetime.now().isoformat()}] {sensor.state} | {sensor.metrics['amplitude_rms_dbfs']:.1f} dBFS")
        time.sleep(interval_s)
