# gui/components/data/decode_raw_data.py
# =============================================================================
# Purpose
# -------
# Decode raw rows from **one session** in 'telemetry' into 'decoded_telemetry'.
#
# Scope
# -----
# - This decoder processes **only a single session_id**.
# - If session_id is not provided, it auto-selects the **most recent** session
#   present in 'telemetry' (by MAX(t_ns)).
#
# What it does
# ------------
# - Reads rows from telemetry for the chosen session_id.
# - Copies session metadata **exactly** from telemetry:
#     session_id, logger_session, logger_session_number.
# - Decodes UART 14B frames (EF FE ... EE), Big Endian pairs:
#     [2..3]=u16 MotorCurrent
#     [4..5]=s16 EncoderValue
#     [6..7]=s16 RelativeEncoderValue
#     [8..9]=s16 V24PecDiff
#     [10..11]=s16 Reserved0
#     [12]=u8  PWM
# - Computes time_ms per (session_id, logger_session_number) relative to first t_ns.
# - Parses I2C 2B angle (AS5048A) into:
#     i2c_raw14, i2c_zero_raw14 (first seen per (session_id, logger_session_number)),
#     i2c_delta_raw14 (signed wrap),
#     i2c_angle_deg (-(delta*360/16384)),
#     i2c_zero_angle_deg (zero*360/16384)
# - Upserts decoded rows into 'decoded_telemetry' (no i2c_raw stored).
#
# Entry points
# ------------
# - decode_into_decoded_telemetry(session_id: str|None = None, batch_size: int = 1000) -> dict
# - run(session_id: str|None = None, **_) -> dict          (alias; ignores other filters)
# - main([session_id]) -> int                               (simple CLI)
# =============================================================================

from __future__ import annotations
from typing import Optional, Tuple, Dict, Any, List

# DB helpers (centralized)
from components.data.db import (
    get_connection,
    ensure_decoded_schema,
    upsert_decoded_rows,
)

# -----------------------------
# UART framing & decode helpers
# -----------------------------
FRAME_LEN = 14
START0 = 0xEF
START1 = 0xFE
END = 0xEE


def _parse_hex_bytes(s: Optional[str]) -> Optional[bytes]:
    """Accepts 'EF FE ... EE' or 'EFFEEE...' (spaces optional). Returns None if invalid/missing."""
    if not s:
        return None
    s2 = s.strip().replace(" ", "")
    if len(s2) % 2 != 0:
        return None
    try:
        return bytes.fromhex(s2)
    except ValueError:
        return None


def _be_u16(b0: int, b1: int) -> int:
    return (b0 << 8) | b1


def _be_s16(b0: int, b1: int) -> int:
    u = (b0 << 8) | b1
    return u - 0x10000 if (u & 0x8000) else u


def _decode_uart_frame(
    raw: Optional[bytes],
) -> Optional[Tuple[int, int, int, int, int, int]]:
    if raw is None or len(raw) != FRAME_LEN:
        return None
    if not (raw[0] == START0 and raw[1] == START1 and raw[13] == END):
        return None

    # motor_current must be signed s16 (two's complement, big endian)
    mc = _be_s16(raw[2], raw[3])
    enc = _be_s16(raw[4], raw[5])
    rel = _be_s16(raw[6], raw[7])
    v24 = _be_s16(raw[8], raw[9])
    res0 = _be_s16(raw[10], raw[11])
    pwm = int(raw[12])  # u8 (0..100)

    # Optional sanity guard to drop obvious misframes (tune bounds to your hw)
    if not (-40000 <= mc <= 40000 and 0 <= pwm <= 100):
        return None

    return (mc, enc, rel, v24, res0, pwm)


# -----------------------------
# I2C decode helpers (AS5048A)
# -----------------------------
def _i2c_raw14(i2c_raw: Optional[str]) -> Optional[int]:
    """Decode 2-byte I2C payload into 14-bit angle (0..16383)."""
    b = _parse_hex_bytes(i2c_raw)
    if b is None or len(b) != 2:
        return None
    msb, lsb = b[0], b[1]
    return ((msb << 6) | (lsb & 0x3F)) & 0x3FFF


def _wrap14_delta(cur: int, zero: int) -> int:
    """Signed wrap-around delta (cur - zero) mapped to [-8192..+8191] in 14-bit space."""
    return ((cur - zero + 8192) % 16384) - 8192


# -----------------------------
# Session selection helpers
# -----------------------------
def _resolve_session_id() -> Optional[str]:
    """
    Pick the most recent session_id present in telemetry (by MAX(t_ns)).
    Returns None if telemetry is empty.
    """
    with get_connection() as con:
        cur = con.cursor()
        cur.execute(
            """
            SELECT session_id
            FROM telemetry
            WHERE t_ns IS NOT NULL
            GROUP BY session_id
            ORDER BY MAX(t_ns) DESC
            LIMIT 1
            """
        )
        row = cur.fetchone()
        return row[0] if row else None


# -----------------------------
# Internal: load rows for one session
# -----------------------------
_SELECT_FOR_SESSION = """
SELECT
  id, session_id, logger_session, logger_session_number,
  t_ns, uart_raw, i2c_raw
FROM telemetry
WHERE session_id = ?
ORDER BY logger_session_number, t_ns
"""


# -----------------------------
# Public: main decode entry
# -----------------------------
def decode_into_decoded_telemetry(
    session_id: Optional[str] = None,
    *,
    batch_size: int = 1000,
) -> Dict[str, int]:
    ensure_decoded_schema()

    sess = session_id or _resolve_session_id()
    if not sess:
        return {"processed": 0, "decoded": 0, "skipped_invalid_uart": 0}

    # Load rows for this session
    with get_connection() as con:
        cur = con.cursor()
        cur.execute(_SELECT_FOR_SESSION, (sess,))
        rows = cur.fetchall()

    processed = decoded = skipped = 0

    first_ns_map: Dict[Tuple[str, int], int] = {}
    i2c_zero_map: Dict[Tuple[str, int], int] = {}

    try:
        import config

        _CONFIG_ZERO14 = (
            ((int(config.SESSION_ZERO[0]) & 0xFF) << 8)
            | (int(config.SESSION_ZERO[1]) & 0xFF)
        ) & 0x3FFF
    except Exception:
        _CONFIG_ZERO14 = None

    # 1) PASS: decode & COLLECT per run
    from collections import defaultdict

    grp_rows: Dict[Tuple[str, int], List[Dict[str, Any]]] = defaultdict(list)

    for id_, sess_id, log_tag, log_no, t_ns, uart_raw, i2c_raw in rows:
        processed += 1
        grp = (sess_id, int(log_no))

        if _CONFIG_ZERO14 is not None:
            i2c_zero_map.setdefault(grp, _CONFIG_ZERO14)

        # baseline for time_ms
        if grp not in first_ns_map and t_ns is not None:
            first_ns_map[grp] = t_ns
        t0 = first_ns_map.get(grp)
        time_ms = (
            int((t_ns - t0) / 1_000_000)
            if (t0 is not None and t_ns is not None)
            else None
        )

        # UART decode
        frame = _decode_uart_frame(_parse_hex_bytes(uart_raw))
        if frame is None:
            skipped += 1
            continue
        mc, enc, rel, v24, res0, pwm = frame

        # I2C decode/angles
        raw14 = _i2c_raw14(i2c_raw)
        zero14 = i2c_zero_map.get(grp)
        if raw14 is not None and zero14 is None:
            zero14 = raw14
            i2c_zero_map[grp] = zero14

        if raw14 is not None and zero14 is not None:
            delta14 = _wrap14_delta(raw14, zero14)
            angle_deg = -(delta14 * 360.0 / 16384.0)
            zero_angle_deg = zero14 * 360.0 / 16384.0
        else:
            delta14 = None
            angle_deg = None
            zero_angle_deg = (zero14 * 360.0 / 16384.0) if zero14 is not None else None

        grp_rows[grp].append(
            {
                "id": id_,
                "session_id": sess_id,
                "logger_session": log_tag,
                "logger_session_number": int(log_no),
                "t_ns": t_ns,
                "time_ms": time_ms,
                "motor_current": mc,
                "encoder_value": enc,
                "relative_encoder_value": rel,
                "v24_pec_diff": v24,
                "pwm": pwm,
                "i2c_raw14": raw14,
                "i2c_zero_raw14": zero14,
                "i2c_delta_raw14": delta14,
                "i2c_angle_deg": angle_deg,
                "i2c_zero_angle_deg": zero_angle_deg,
                "angular_velocity": None,
            }
        )

    # 2) PASS: compute angular_velocity per group using Kalman RTS on angle vs time
    for grp, items in grp_rows.items():
        # Build time in seconds with full precision; keep your existing time_ms for storage
        time_s_list = []
        angle_deg_list = []
        for row in items:
            t_ns = row["t_ns"]
            # use high-precision seconds internally
            time_s_list.append(
                (t_ns - items[0]["t_ns"]) * 1e-9
                if (t_ns is not None and items[0]["t_ns"] is not None)
                else None
            )
            angle_deg_list.append(
                row["i2c_angle_deg"]
            )  # may be None; RTS handles segments

        omega_deg_s = _kalman_velocity_over_series(
            time_s_list, angle_deg_list, gap_threshold_s=0.1
        )

        # write back
        for row, w in zip(items, omega_deg_s):
            row["angular_velocity"] = w

    # 3) UPSERT in batches (same API)
    out_batch: List[Dict[str, Any]] = []

    def _flush_batch():
        nonlocal out_batch, decoded
        if not out_batch:
            return
        decoded += upsert_decoded_rows(out_batch)
        out_batch = []

    for items in grp_rows.values():
        for row in items:
            out_batch.append(row)
            if len(out_batch) >= batch_size:
                _flush_batch()
    _flush_batch()

    return {"processed": processed, "decoded": decoded, "skipped_invalid_uart": skipped}


# -----------------------------
# Kalman RTS (constant-velocity) helpers
# -----------------------------
def _robust_var(values):
    """Robust variance via MAD; returns a small floor if data is tiny."""
    xs = [v for v in values if v is not None]
    if len(xs) < 5:
        return 1e-6
    xs_sorted = sorted(xs)
    m = xs_sorted[len(xs) // 2]
    abs_dev = [abs(v - m) for v in xs_sorted]
    mad = sorted(abs_dev)[len(abs_dev) // 2] or 1e-9
    # 1.4826 * MAD ~ sigma (Gaussian)
    sigma = 1.4826 * mad
    v = sigma * sigma
    return v if v > 1e-6 else 1e-6


def _kalman_rts_velocity_segment(t_s, y_deg, r_var=None, q_pow=None):
    """
    One contiguous segment (strictly increasing times, no big gaps).
    State x=[theta, omega], units: theta=deg, omega=deg/s.
    F_k = [[1, dt],[0,1]]
    Q_k = q * [[dt^3/3, dt^2/2],[dt^2/2, dt]]
    H   = [1, 0], R = r.
    Returns (theta_s, omega_s) lists (len = len(y_deg))
    """
    n = len(y_deg)
    if n == 0:
        return [], []
    # Estimate R (measurement variance) if not provided
    if r_var is None:
        r_var = _robust_var(y_deg)
    # Estimate process noise power q if not provided (based on rough accel scale)
    if q_pow is None:
        # crude accel estimate from finite differences
        dtheta = [(y_deg[i + 1] - y_deg[i]) for i in range(n - 1)]
        dt = [(t_s[i + 1] - t_s[i]) for i in range(n - 1)]
        dv = []
        for i in range(len(dtheta) - 1):
            dt1 = dt[i] if dt[i] > 1e-6 else 1e-6
            dt2 = dt[i + 1] if dt[i + 1] > 1e-6 else 1e-6
            v1 = dtheta[i] / dt1
            v2 = dtheta[i + 1] / dt2
            dv.append(
                (v2 - v1) / max((dt1 + dt2) / 2.0, 1e-6)
            )  # deg/s^2 change per second
        # variance-like scale of acceleration
        acc_var = _robust_var(dv) if dv else 1.0
        med_dt = sorted(dt)[len(dt) // 2] if dt else 0.01
        q_pow = max(acc_var * med_dt * 1e-3, 1e-6)  # conservative floor

    # Allocate
    x_f = [[0.0, 0.0] for _ in range(n)]
    P_f = [[[0.0, 0.0], [0.0, 0.0]] for _ in range(n)]
    x_p = [[0.0, 0.0] for _ in range(n)]
    P_p = [[[0.0, 0.0], [0.0, 0.0]] for _ in range(n)]

    # Init
    x = [y_deg[0], 0.0]
    P = [[1.0, 0.0], [0.0, 1000.0]]

    H = [1.0, 0.0]
    R = r_var

    def mat_add(A, B):
        return [
            [A[0][0] + B[0][0], A[0][1] + B[0][1]],
            [A[1][0] + B[1][0], A[1][1] + B[1][1]],
        ]

    def mat_mul(A, B):
        return [
            [
                A[0][0] * B[0][0] + A[0][1] * B[1][0],
                A[0][0] * B[0][1] + A[0][1] * B[1][1],
            ],
            [
                A[1][0] * B[0][0] + A[1][1] * B[1][0],
                A[1][0] * B[0][1] + A[1][1] * B[1][1],
            ],
        ]

    def mat_transpose(A):
        return [[A[0][0], A[1][0]], [A[0][1], A[1][1]]]

    def mat_vec(A, v):
        return [A[0][0] * v[0] + A[0][1] * v[1], A[1][0] * v[0] + A[1][1] * v[1]]

    def mat_inv_2x2(A):
        det = A[0][0] * A[1][1] - A[0][1] * A[1][0]
        if abs(det) < 1e-12:
            det = 1e-12
        inv = [[A[1][1] / det, -A[0][1] / det], [-A[1][0] / det, A[0][0] / det]]
        return inv

    # FILTER
    for k in range(n):
        if k == 0:
            x_p[k] = [x[0], x[1]]
            P_p[k] = [[P[0][0], P[0][1]], [P[1][0], P[1][1]]]
        else:
            dt = t_s[k] - t_s[k - 1]
            F = [[1.0, dt], [0.0, 1.0]]
            q11 = (dt**3) / 3.0
            q12 = (dt**2) / 2.0
            q22 = dt
            Q = [[q_pow * q11, q_pow * q12], [q_pow * q12, q_pow * q22]]
            # Predict
            x = [x[0] + dt * x[1], x[1]]
            FP = mat_mul(F, P)
            FT = mat_transpose(F)
            P = mat_add(mat_mul(FP, FT), Q)
            x_p[k] = [x[0], x[1]]
            P_p[k] = [[P[0][0], P[0][1]], [P[1][0], P[1][1]]]

        # Update with measurement y_deg[k]
        z = y_deg[k]
        # S = H*P*H^T + R  (scalar)
        S = P[0][0] + R
        K0 = P[0][0] / S  # first row, col 0 of K
        K1 = P[1][0] / S  # second row, col 0 of K
        innov = z - x[0]
        x = [x[0] + K0 * innov, x[1] + K1 * innov]
        # P = (I - K*H)P
        # (I - K H) = [[1-K0*1, -K0*0],[-K1*1, 1- K1*0]] = [[1-K0, 0],[-K1, 1]]
        IKH = [[1.0 - K0, 0.0], [-K1, 1.0]]
        P = mat_mul(IKH, P)

        x_f[k] = [x[0], x[1]]
        P_f[k] = [[P[0][0], P[0][1]], [P[1][0], P[1][1]]]

    # RTS SMOOTHER
    x_s = [[x_f[i][0], x_f[i][1]] for i in range(n)]
    P_s = [
        [[P_f[i][0][0], P_f[i][0][1]], [P_f[i][1][0], P_f[i][1][1]]] for i in range(n)
    ]

    for k in range(n - 2, -1, -1):
        dt = t_s[k + 1] - t_s[k]
        F = [[1.0, dt], [0.0, 1.0]]
        FT = mat_transpose(F)
        Pp_inv = mat_inv_2x2(P_p[k + 1])
        # Ck = P_f[k] * F^T * P_p[k+1]^{-1}
        C_left = mat_mul(P_f[k], FT)
        Ck = mat_mul(C_left, Pp_inv)
        # x_s[k] = x_f[k] + Ck * (x_s[k+1] - x_p[k+1])
        dx = [x_s[k + 1][0] - x_p[k + 1][0], x_s[k + 1][1] - x_p[k + 1][1]]
        corr = mat_vec(Ck, dx)
        x_s[k] = [x_f[k][0] + corr[0], x_f[k][1] + corr[1]]
        # P_s[k] not needed for velocity output, so we skip computing it for speed

    theta_s = [x_s[i][0] for i in range(n)]
    omega_s = [x_s[i][1] for i in range(n)]
    return theta_s, omega_s


def _kalman_velocity_over_series(
    time_s_list, angle_deg_list, *, gap_threshold_s=0.1
) -> list:
    """
    Splits the series at big gaps or invalid points and runs the RTS smoother per segment.
    Returns a list of angular velocities (deg/s) aligned with inputs (None where undefined).
    """
    n = len(angle_deg_list)
    out = [None] * n
    # Build valid indices where both time and angle exist
    valid = [
        (time_s_list[i] is not None) and (angle_deg_list[i] is not None)
        for i in range(n)
    ]
    i = 0
    while i < n:
        # skip invalids
        while i < n and not valid[i]:
            i += 1
        if i >= n:
            break
        j = i + 1
        # extend segment until gap or invalid
        while j < n and valid[j]:
            if (time_s_list[j] - time_s_list[j - 1]) > gap_threshold_s:
                break
            j += 1
        # segment [i, j)
        t_seg = [time_s_list[k] for k in range(i, j)]
        y_seg = [angle_deg_list[k] for k in range(i, j)]
        theta_s, omega_s = _kalman_rts_velocity_segment(t_seg, y_seg)
        for k, w in enumerate(omega_s):
            out[i + k] = w
        i = j
    return out


# Friendly alias for UART_logic.stop_logger()
def run(session_id: Optional[str] = None, **_ignored) -> Dict[str, int]:
    """
    Alias to decode_into_decoded_telemetry with **only** session_id honored.
    Other kwargs are ignored intentionally.
    """
    return decode_into_decoded_telemetry(session_id=session_id)


def main() -> int:
    """CLI: python -m gui.components.data.decode_raw_data [session_id]"""
    import sys

    sess = sys.argv[1] if len(sys.argv) > 1 else None
    stats = decode_into_decoded_telemetry(session_id=sess)
    print(stats)
    return 0


if __name__ == "__main__":
    main()
