"""Time-series store: per-channel ring buffers + min/max, with an optional
recorder for full-session capture/playback.

The acquisition thread pushes samples here; the GUI (or terminal) only reads.
This decoupling is what keeps the UI smooth while the ELM327 plods along at
~7-15 reads/sec.  Nothing here touches serial or Qt -- pure data.
"""
import threading
from collections import deque


class Channel:
    """One PID's rolling history plus session min/max.

    Lock-guarded so the acquisition thread can push while the GUI thread
    reads series()/snapshots without a 'deque mutated during iteration' race.
    """

    def __init__(self, key, maxlen=3600):
        self.key = key
        self.buf = deque(maxlen=maxlen)     # (t, value); value may be None
        self.lo = None
        self.hi = None
        self.last_t = None
        self.last_v = None
        self._lock = threading.Lock()

    def push(self, t, v):
        with self._lock:
            self.buf.append((t, v))
            self.last_t, self.last_v = t, v
            if v is not None:
                self.lo = v if self.lo is None else min(self.lo, v)
                self.hi = v if self.hi is None else max(self.hi, v)

    def reset_minmax(self):
        with self._lock:
            self.lo = self.hi = self.last_v

    def series(self, since=None):
        """Return [(t, v), ...]; if since given, only samples with t >= since."""
        with self._lock:
            items = list(self.buf)          # snapshot under lock
        if since is None:
            return items
        return [(t, v) for (t, v) in items if t >= since]


class TimeSeriesStore:
    """Thread-safe collection of Channels keyed by PID key."""

    def __init__(self, maxlen=3600):
        self._ch = {}
        self._maxlen = maxlen
        self._lock = threading.Lock()
        self.recorder = None                # set to a recorder with .write(key, t, v)

    def channel(self, key):
        with self._lock:
            c = self._ch.get(key)
            if c is None:
                c = Channel(key, self._maxlen)
                self._ch[key] = c
            return c

    def push(self, key, t, v):
        self.channel(key).push(t, v)
        rec = self.recorder
        if rec is not None:
            rec.write(key, t, v)

    def latest(self, key):
        c = self._ch.get(key)
        return None if c is None else c.last_v

    def minmax(self, key):
        c = self._ch.get(key)
        return (None, None) if c is None else (c.lo, c.hi)

    def reset_minmax(self, keys=None):
        with self._lock:
            for k, c in self._ch.items():
                if keys is None or k in keys:
                    c.reset_minmax()

    def keys(self):
        with self._lock:
            return list(self._ch.keys())

    def clear(self):
        """Empty every channel's history + min/max (start a fresh capture)."""
        with self._lock:
            chans = list(self._ch.values())
        for c in chans:
            with c._lock:
                c.buf.clear()
                c.lo = c.hi = c.last_v = c.last_t = None

    def snapshot(self):
        """Return {key: [(t, v), ...]} of all current channel history."""
        with self._lock:
            chans = dict(self._ch)
        return {k: c.series() for k, c in chans.items()}


def export_csv(store, path):
    """Write a store's current buffers to a long-format CSV (t,key,value)."""
    rows = []
    for key, series in store.snapshot().items():
        for t, v in series:
            rows.append((t, key, v))
    rows.sort(key=lambda r: r[0])
    with open(path, "w") as f:
        f.write("t,key,value\n")
        for t, key, v in rows:
            f.write(f"{t:.3f},{key},{'' if v is None else v}\n")
    return path


class CsvRecorder:
    """Long-format session recorder: one row per sample (t,key,value).

    Long format (vs wide) tolerates per-PID poll rates and PIDs appearing
    mid-session.  Replay re-pushes rows into a fresh store in t order.
    """

    def __init__(self, path):
        self._f = open(path, "w")
        self._f.write("t,key,value\n")
        self._lock = threading.Lock()

    def write(self, key, t, v):
        with self._lock:
            self._f.write(f"{t:.3f},{key},{'' if v is None else v}\n")

    def close(self):
        with self._lock:
            self._f.close()


def replay_csv(path, store):
    """Load a CsvRecorder file back into a store (for playback)."""
    with open(path) as f:
        next(f, None)                       # header
        for line in f:
            parts = line.rstrip("\n").split(",", 2)
            if len(parts) != 3:
                continue
            t, key, v = parts
            try:
                t = float(t)
            except ValueError:
                continue
            val = None if v == "" else float(v) if _is_num(v) else v
            store.push(key, t, val)
    return store


def _is_num(s):
    try:
        float(s)
        return True
    except ValueError:
        return False
