#!/usr/bin/env python3
"""
Shared RAW-pulse encoder for the 1D-CNN (Phase 3B).

A single source of truth for turning a Flipper RAW pulse array into the fixed
normalized tensor the CNN consumes — imported by BOTH the trainer
(``scripts/train_cnn_classifier.py``) and the inference wrapper
(``src/matcher/cnn_classifier.py``) so the two can never drift.

Encoding:
    * take the first ``length`` signed durations (µs; +high / -low),
    * scale by the sequence's *median absolute* duration — a robust, TE-like
      unit so a short pulse is ~±1 and a long pulse ~±2-3 (the ratios that carry
      the protocol identity), while absolute gain / capture level is normalised
      out. Using the median instead of the max is deliberate: a single huge
      inter-packet gap (~10 000 µs) would otherwise squash every informative
      short pulse toward zero and blind the CNN.
    * clip to ±``CLIP`` and rescale to [-1, 1] so outsized gaps saturate the
      ceiling instead of dominating the dynamic range,
    * right-pad with zeros to ``length``.

Returns a float32 array of shape ``(length,)`` or ``None`` when the pulse train
is empty or degenerate (all-zero).
"""

from typing import List, Optional

import numpy as np

PULSE_LEN = 512
CLIP = 8.0  # in median-pulse units; gaps beyond this saturate to ±1


def encode_pulses(pulses: List[int], length: int = PULSE_LEN) -> Optional[np.ndarray]:
    if pulses is None or len(pulses) == 0:
        return None
    arr = np.asarray(pulses[:length], dtype=np.float32)
    nz = np.abs(arr[arr != 0])
    if nz.size == 0:
        return None
    scale = float(np.median(nz))
    if scale <= 0.0:
        scale = float(np.max(np.abs(arr)))
    if scale <= 0.0:
        return None
    arr = np.clip(arr / scale, -CLIP, CLIP) / CLIP
    if arr.size < length:
        arr = np.pad(arr, (0, length - arr.size))
    return arr.astype(np.float32)
