"""
Universal LSL ingestion — Tier 1 of the device matrix (see HARDWARE.md).

KEY FACT: the recorder (`recorder.py`) already resolves and records EVERY LSL
outlet on the network. So any device whose vendor app emits an LSL stream
(Smart Eye, Enobio/NIC2, actiCHamp, Shimmer's LSL app, Plux OpenSignals,
Artinis OxySoft, Pupil LSL Relay, the Gazepoint->LSL tool, ...) is captured with
*zero* adapter code. That is what makes biosync hardware-agnostic, exactly like
iMotions — and it's the same property the synthetic sources already prove.

This module adds the two things you still want around that fact:

  * `discover()`  — enumerate what's currently on the network (for a UI / CLI /
                    pre-flight check that a device is actually streaming).
  * `LSLSource`   — an OPTIONAL managed bridge: subscribe to an existing upstream
                    LSL stream, optionally transform each sample, and re-publish
                    it under a biosync-owned StreamSpec. Use it only when you need
                    to relabel, down-select channels, or apply light DSP before
                    recording. For a plain native-LSL device you do NOT need this —
                    just start the vendor app and record.

Nothing here touches `recorder.py` or `analysis/`. The invariant holds.
"""

from __future__ import annotations

from typing import Callable, Sequence

from pylsl import StreamInlet, resolve_streams, resolve_byprop

from ..core.source import Source, StreamSpec


def discover(timeout: float = 2.0) -> list[dict]:
    """List LSL outlets currently visible on the network.

    Returns one dict per stream with the fields a picker UI needs. Handy as a
    pre-flight: confirm a Tier-1 device is streaming before you hit record.
    """
    out = []
    for info in resolve_streams(timeout):
        out.append({
            "name": info.name(),
            "type": info.type(),
            "channel_count": info.channel_count(),
            "nominal_srate": info.nominal_srate(),
            "source_id": info.source_id(),
            "format": info.channel_format(),
        })
    return out


class LSLSource(Source):
    """
    OPTIONAL bridge: consume an upstream LSL stream and re-publish it as a
    biosync-managed Source (so it starts/stops with everything else and carries a
    known StreamSpec). Supply a `transform` to relabel/clean/down-select.

    Resolve the upstream by `name` or by `stype` (content-type). If you give a
    `transform`, also pass the output `out_channels` it produces.

    NOTE: if the raw upstream stays on the network, the recorder will capture both
    the raw and this re-published copy. That's intended when `transform` adds a
    processed variant; if you only want the processed one, run the vendor app such
    that the raw isn't separately resolvable, or filter at the recorder.
    """

    def __init__(
        self,
        *,
        name: str | None = None,
        stype: str | None = None,
        out_name: str | None = None,
        out_stype: str | None = None,
        out_channels: Sequence[str] | None = None,
        transform: Callable[[list], list] | None = None,
        resolve_timeout: float = 5.0,
        pull_timeout: float = 0.5,
    ):
        if not (name or stype):
            raise ValueError("give either name= or stype= to resolve the upstream")
        self._sel_name = name
        self._sel_stype = stype
        self._resolve_timeout = resolve_timeout
        self._pull_timeout = pull_timeout
        self._transform = transform

        info = self._resolve_upstream()
        labels = _channel_labels(info)
        is_string = info.channel_format() == 3  # cf_string
        self._inlet = StreamInlet(info, max_buflen=360, recover=True)

        spec = StreamSpec(
            name=out_name or info.name(),
            stype=out_stype or info.type(),
            channels=list(out_channels) if out_channels else labels,
            nominal_srate=info.nominal_srate(),
            channel_format="string" if is_string else "float32",
            source_id=f"biosync-lslbridge-{info.source_id() or info.name()}",
        )
        super().__init__(spec)

    def _resolve_upstream(self):
        prop = "name" if self._sel_name else "type"
        value = self._sel_name or self._sel_stype
        results = resolve_byprop(prop, value, timeout=self._resolve_timeout)
        if not results:
            raise RuntimeError(f"no upstream LSL stream with {prop}={value!r}")
        return results[0]

    def read(self):
        # Pull from upstream, optionally transform, and yield with the upstream's
        # own timestamp so the recorder's time_correction keeps everything aligned.
        while not self.stopping:
            sample, ts = self._inlet.pull_sample(timeout=self._pull_timeout)
            if sample is None:
                continue
            if self._transform is not None:
                sample = self._transform(sample)
            yield sample, ts


def _channel_labels(info) -> list[str]:
    labels, ch = [], info.desc().child("channels").child("channel")
    for i in range(info.channel_count()):
        labels.append(ch.child_value("label") or f"ch{i}")
        ch = ch.next_sibling()
    return labels
