"""Shared constants and functions for scope patch extraction and signal labels.

This module is the single source of truth for parameters that must stay
synchronised between the training pipeline (scripts/model_definition.py)
and the runtime classifier (lib/signal/signal_classifier.py).

scripts/model_definition.py imports from this module via a try/except
fallback so that it works both in the project repo (where lib/ is on
sys.path) and in the deployed environment (~/Pycom/signals/scripts/,
where patch_config.py is copied alongside model_definition.py).
"""

import numpy as np


# ---------------------------------------------------------------------------
# Scope / patch geometry
# ---------------------------------------------------------------------------

SCOPE_HISTORY_ROWS = 100
SCOPE_HISTORY_COLS = 475
SCOPE_PATCH_ROWS = 100
SCOPE_PATCH_COLS = 64
INPUT_CHANNELS = 1
SCOPE_AMP_MAX = 160
PATCH_MIN_MARGIN_BINS = 20
PATCH_MARGIN_FRACTION = 0.15
PATCH_STRATEGY = "centered"

# Auxiliary input dimension (span, ref_level, noise_floor, bandwidth_bins,
# center_freq_offset_hz, snr_db)
AUX_DIM = 6

AUX_FEATURE_NAMES = [
    "span_hz",
    "reference_level_db",
    "noise_floor",
    "bandwidth_bins",
    "center_freq_offset_hz",
    "snr_db",
]

# ---------------------------------------------------------------------------
# Signal labels
# ---------------------------------------------------------------------------

# Model labels: the 15 classes the classifier is trained on.
# This list MUST match model/signal_labels.json at training time.
SIGNAL_LABELS = [
    "CW",
    "FM",
    "AM",
    "SSB",
    "RTTY",
    "FT8",
    "FT4",
    "FT2",
    "Q65",
    "JT65",
    "AFSK",
    "PSK",
    "FSK",
    "MFSK",
    "MSK",
]

# Capture labels: model labels plus "Unknown" for human annotation
# in the capture manager UI.
CAPTURE_LABELS = SIGNAL_LABELS + ["Unknown"]

# Fallback used at runtime when the labels JSON is missing or corrupt.
SIGNAL_LABELS_FALLBACK = ["Unknown"]


# ---------------------------------------------------------------------------
# Auxiliary feature normalisation
# ---------------------------------------------------------------------------

def prepare_aux_features(
    span_hz: float = 100000,
    reference_level_db: float = 0,
    noise_floor: float = 10,
    bandwidth_bins: int = 5,
    center_freq_offset_hz: float = 0,
    snr_db: float = 0,
) -> np.ndarray:
    """Normalize auxiliary features to [0, 1] or [-1, 1] for the model.

    Parameters
    ----------
    span_hz : float
        Scope span in Hz. Log-scaled to [0, 1] (1 Hz -> 0, 1 MHz -> 1).
    reference_level_db : float
        Scope reference level in dB. Maps [-20, 60] to [0, 1].
    noise_floor : float
        Noise floor in raw scope units (0-160). Maps to [0, 1].
    bandwidth_bins : int
        Signal width in scope bins (0-475). Maps to [0, 1].
    center_freq_offset_hz : float
        Offset from center frequency in Hz. Maps to [-1, 1].
    snr_db : float
        Signal-to-noise ratio in dB. Maps [0, 40] to [0, 1].

    Returns
    -------
    np.ndarray
        Shape (AUX_DIM,) float32 with normalized features.
    """
    norm_span = min(max(np.log10(max(span_hz, 1)) / 6.0, 0.0), 1.0)
    norm_ref = min(max((reference_level_db + 20) / 80.0, 0.0), 1.0)
    norm_noise = min(max(noise_floor / float(SCOPE_AMP_MAX), 0.0), 1.0)
    norm_bw = min(max(bandwidth_bins / float(SCOPE_PATCH_COLS), 0.0), 1.0)
    norm_offset = min(max(center_freq_offset_hz / max(span_hz / 2, 1), -1.0), 1.0)
    norm_snr = min(max(snr_db / 40.0, 0.0), 1.0)
    return np.array(
        [norm_span, norm_ref, norm_noise, norm_bw, norm_offset, norm_snr],
        dtype=np.float32,
    )
