"""MAC sensor generator.

Emits NDJSON and CSV. The CSV header is VERBATIM the user-provided real device
header:

  sessionStart,messageCount,onlineDurationSeconds,sessionEnd,processingTimestamp,
  deviceId,version,macAddress,averageSignalStrength,deviceManufacturer,
  ingestion_ts,status
"""
from __future__ import annotations

import random
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Any

from .common import (
    EARTH_R_M, KN_MS,
    epoch_ms, iso_utc,
    haversine_m, rssi_from_distance,
    sensor_lookup, load_personas,
)

MAC_CSV_HEADER = [
    "sessionStart", "messageCount", "onlineDurationSeconds", "sessionEnd",
    "processingTimestamp", "deviceId", "version", "macAddress",
    "averageSignalStrength", "deviceManufacturer", "ingestion_ts", "status",
]


@dataclass
class MacObservation:
    """One emission session of a single MAC at a single sensor."""
    sensor_id: str
    mac: str
    session_start: datetime | None
    session_end: datetime | None
    message_count: int
    avg_rssi: float
    manufacturer: str | None
    status: str = "active"
    version: str = "fw-2.4.1"

    def to_ndjson(self) -> dict[str, Any]:
        proc_ts = self.session_end or (self.session_start + timedelta(seconds=60) if self.session_start else datetime.utcnow())
        return {
            "sessionStart": iso_utc(self.session_start) if self.session_start else None,
            "messageCount": int(self.message_count),
            "onlineDurationSeconds": int((self.session_end - self.session_start).total_seconds()) if (self.session_start and self.session_end) else None,
            "sessionEnd": iso_utc(self.session_end) if self.session_end else None,
            "processingTimestamp": iso_utc(proc_ts),
            "deviceId": self.sensor_id,
            "version": self.version,
            "macAddress": self.mac.lower(),
            "averageSignalStrength": round(self.avg_rssi, 2),
            "deviceManufacturer": self.manufacturer,
            "ingestion_ts": epoch_ms(proc_ts) + 12,
            "status": self.status,
        }

    def to_csv_row(self) -> list[Any]:
        nd = self.to_ndjson()
        return [
            nd["sessionStart"] if nd["sessionStart"] else "None",
            nd["messageCount"],
            nd["onlineDurationSeconds"] if nd["onlineDurationSeconds"] is not None else "None",
            nd["sessionEnd"] if nd["sessionEnd"] else "None",
            nd["processingTimestamp"],
            nd["deviceId"],
            nd["version"],
            nd["macAddress"],
            nd["averageSignalStrength"],
            nd["deviceManufacturer"] if nd["deviceManufacturer"] else "None",
            nd["ingestion_ts"],
            nd["status"],
        ]


@dataclass
class MovingMacEmitter:
    """A MAC that moves along a track (e.g. crew device riding a ship)."""
    mac: str
    manufacturer: str | None
    waypoints: list[tuple[datetime, float, float]]
    active_windows: list[tuple[datetime, datetime]] | None = None
    seed: int | None = None


@dataclass
class StaticMacEmitter:
    """A MAC that emits from a fixed point (e.g. background coastal devices)."""
    mac: str
    manufacturer: str | None
    lat: float
    lon: float
    active_windows: list[tuple[datetime, datetime]]
    seed: int | None = None


def _track_pos(track: list[tuple[datetime, float, float]], t: datetime) -> tuple[float, float]:
    if t <= track[0][0]:
        return track[0][1], track[0][2]
    if t >= track[-1][0]:
        return track[-1][1], track[-1][2]
    for i in range(len(track) - 1):
        a, b = track[i], track[i + 1]
        if a[0] <= t <= b[0]:
            span = (b[0] - a[0]).total_seconds()
            f = 0.0 if span <= 0 else (t - a[0]).total_seconds() / span
            return a[1] + f * (b[1] - a[1]), a[2] + f * (b[2] - a[2])
    return track[-1][1], track[-1][2]


def simulate_moving_mac(
    em: MovingMacEmitter,
    sensors: dict[str, dict[str, Any]] | None = None,
    *,
    rssi_threshold: float = -105.0,
    session_window_s: float = 60.0,
) -> list[MacObservation]:
    """For each (sensor, time-window) where the device is within range,
    emit a MacObservation session aggregating messages."""
    sensors = sensors or sensor_lookup()
    rng = random.Random(em.seed)
    out: list[MacObservation] = []
    active = em.active_windows or [(em.waypoints[0][0], em.waypoints[-1][0])]
    for win_start, win_end in active:
        t = win_start
        while t < win_end:
            win = (t, min(win_end, t + timedelta(seconds=session_window_s)))
            for sid, sensor in sensors.items():
                if sensor.get("kind") != "mac":
                    continue
                if sensor.get("subtype") in ("airborne",):
                    # Airborne sensors are handled separately if their own track is provided
                    continue
                slat, slon = sensor["lat"], sensor["lon"]
                # Sample distance at window midpoint
                tm = win[0] + (win[1] - win[0]) / 2
                lat, lon = _track_pos(em.waypoints, tm)
                d = haversine_m(slat, slon, lat, lon)
                rssi = rssi_from_distance(d, rng=rng)
                if rssi < rssi_threshold:
                    continue
                msg_count = max(1, int(rng.gauss(40, 12)))
                out.append(MacObservation(
                    sensor_id=sid,
                    mac=em.mac,
                    session_start=win[0],
                    session_end=win[1],
                    message_count=msg_count,
                    avg_rssi=rssi,
                    manufacturer=em.manufacturer,
                ))
            t = win[1]
    return out


def simulate_airborne_mac_custom(
    airborne_sensor_id: str,
    sensor_track: list[tuple[datetime, float, float]],
    target_macs: list[MovingMacEmitter],
    *,
    tx_dbm: float = -15.0,
    n: float = 2.0,
    noise_db: float = 2.0,
    rssi_threshold: float = -90.0,
    session_window_s: float = 5.0,
    seed: int = 7777,
) -> list[MacObservation]:
    """Airborne MAC sensor with tunable path-loss / noise parameters.

    Walks `sensor_track` in `session_window_s` slices. For each target MAC
    within hearing range, emits a `MacObservation`. The path-loss parameters
    (`tx_dbm`, `n`, `noise_db`) are forwarded to `rssi_from_distance` so
    callers can model different airborne payloads (e.g. high-gain drone radio
    vs. plane sensor)."""
    out: list[MacObservation] = []
    rng = random.Random(seed)
    t0 = sensor_track[0][0]
    t1 = sensor_track[-1][0]
    t = t0
    while t < t1:
        win = (t, min(t1, t + timedelta(seconds=session_window_s)))
        tm = win[0] + (win[1] - win[0]) / 2
        slat, slon = _track_pos(sensor_track, tm)
        for em in target_macs:
            tlat, tlon = _track_pos(em.waypoints, tm)
            d = haversine_m(slat, slon, tlat, tlon)
            rssi = rssi_from_distance(d, rng=rng, tx_dbm=tx_dbm, n=n, noise_db=noise_db)
            if rssi < rssi_threshold:
                continue
            out.append(MacObservation(
                sensor_id=airborne_sensor_id,
                mac=em.mac,
                session_start=win[0],
                session_end=win[1],
                message_count=max(1, int(rng.gauss(8, 3))),
                avg_rssi=rssi,
                manufacturer=em.manufacturer,
            ))
        t = win[1]
    return out


def simulate_airborne_mac(
    airborne_sensor_id: str,
    sensor_track: list[tuple[datetime, float, float]],
    target_macs: list[MovingMacEmitter],
    *,
    rssi_threshold: float = -90.0,
    session_window_s: float = 5.0,
) -> list[MacObservation]:
    """Thin wrapper around `simulate_airborne_mac_custom` with the historical
    airborne defaults (tx_dbm=-15, n=2.0, noise_db=2.0)."""
    return simulate_airborne_mac_custom(
        airborne_sensor_id,
        sensor_track,
        target_macs,
        tx_dbm=-15.0,
        n=2.0,
        noise_db=2.0,
        rssi_threshold=rssi_threshold,
        session_window_s=session_window_s,
    )


def simulate_static_mac(em: StaticMacEmitter, sensors: dict[str, dict[str, Any]] | None = None,
                        *, session_window_s: float = 120.0) -> list[MacObservation]:
    sensors = sensors or sensor_lookup()
    rng = random.Random(em.seed)
    out: list[MacObservation] = []
    for win_start, win_end in em.active_windows:
        t = win_start
        while t < win_end:
            win = (t, min(win_end, t + timedelta(seconds=session_window_s)))
            for sid, sensor in sensors.items():
                if sensor.get("kind") != "mac" or sensor.get("subtype") == "airborne":
                    continue
                d = haversine_m(sensor["lat"], sensor["lon"], em.lat, em.lon)
                rssi = rssi_from_distance(d, rng=rng, n=2.8, noise_db=2.5)
                if rssi < -108:
                    continue
                out.append(MacObservation(
                    sensor_id=sid,
                    mac=em.mac,
                    session_start=win[0],
                    session_end=win[1],
                    message_count=max(1, int(rng.gauss(15, 6))),
                    avg_rssi=rssi,
                    manufacturer=em.manufacturer,
                ))
            t = win[1]
    return out


# ---------- Background noise ----------

def generate_background_macs(
    sensors: dict[str, dict[str, Any]] | None,
    start: datetime,
    end: datetime,
    *,
    mac_count: int = 20,
    cadence_s: float = 300.0,
    seed: int = 42,
) -> list[MacObservation]:
    """Generate background consumer-device MAC observations at each coastal/port sensor
    to mimic real ambient device traffic. Uses real OUI prefixes from personas catalog."""
    sensors = sensors or sensor_lookup()
    personas = load_personas()
    rng = random.Random(seed)
    oui_choices = []
    for vendor, prefixes in personas["oui_vendors_real"].items():
        for p in prefixes:
            oui_choices.append((vendor, p))
    macs = []
    for _ in range(mac_count):
        vendor, prefix = rng.choice(oui_choices)
        suffix = ":".join(f"{rng.randint(0,255):02X}" for _ in range(3))
        macs.append((f"{prefix}:{suffix}", vendor))
    out: list[MacObservation] = []
    t = start
    while t < end:
        for sid, sensor in sensors.items():
            if sensor.get("kind") != "mac" or sensor.get("subtype") == "airborne":
                continue
            n = rng.randint(0, 3)
            chosen = rng.sample(macs, k=min(n, len(macs)))
            for mac, vendor in chosen:
                rssi = rng.uniform(-105, -60)
                # Real coastal MAC sensors frequently emit observations with
                # missing fields (device offline mid-session, MAC randomization
                # at vendor level, etc.). Inject realistic nulls so downstream
                # consumers don't assume every field is populated.
                obs_session_start = t
                obs_session_end = t + timedelta(seconds=cadence_s)
                obs_manufacturer = vendor
                if rng.random() < 0.15:
                    obs_session_start = None
                if rng.random() < 0.15:
                    obs_session_end = None
                if rng.random() < 0.30:
                    obs_manufacturer = None
                out.append(MacObservation(
                    sensor_id=sid,
                    mac=mac,
                    session_start=obs_session_start,
                    session_end=obs_session_end,
                    message_count=rng.randint(1, 50),
                    avg_rssi=rssi,
                    manufacturer=obs_manufacturer,
                ))
        t = t + timedelta(seconds=cadence_s)
    return out
