"""Parser for remotive-signal-database YAML files.

Converts the channel → frame → signal hierarchy into flat signal dicts
compatible with the existing search index schema.
"""

from __future__ import annotations

import json
from dataclasses import dataclass
from typing import Any


def _to_json(obj: Any) -> str | None:
    """Serialize to JSON string, or None if empty/None."""
    if obj is None:
        return None
    if isinstance(obj, (dict, list)) and not obj:
        return None
    return json.dumps(obj)


@dataclass
class _FrameContext:
    """Collected channel- and frame-level fields shared by both the frame record and its child signal records.

    Created once per frame during YAML parsing and passed to both _frame_dict()
    and _signal_dict() so that signals inherit parent context without re-extracting it.
    """

    channel_name: str
    channel_type: str | None
    frame_name: str
    frame_id: str | None
    frame_description: str
    cycle_time: float | None
    senders: list[str]
    file_path: str
    workspace_path: str
    # Channel-level
    channel_cluster: str | None
    channel_physical_name: str | None
    # Frame-level (CAN/LIN)
    payload_size: int | None
    opt_empty_payload: str | None
    is_can_fd: bool | None
    is_extended: bool | None
    can_fd_flags: int | None
    multiplex_selector: str | None
    e2e: dict[str, Any] | None
    secoc: dict[str, Any] | None
    groups: list[dict[str, Any]] | None


def parse_yaml_signals(file_path: str, workspace_path: str, data: dict[str, Any]) -> list[dict[str, Any]]:
    """Parse a loaded remotive-signal-database YAML document into signal dicts.

    Args:
        file_path: Workspace-relative path stored in the index and used for filtering.
        workspace_path: Absolute filesystem path of the YAML file on disk.
        data: Parsed YAML document (top-level dict).

    Returns:
        List of signal dicts ready for insertion into the search index.
    """
    results: list[dict[str, Any]] = []
    channels: dict[str, Any] = data.get("channels") or {}

    for channel_name, channel_data in channels.items():
        if not isinstance(channel_data, dict):
            continue

        channel_type: str | None = channel_data.get("type")
        channel_cluster = channel_data.get("can_cluster") or channel_data.get("lin_cluster")
        channel_physical_name = channel_data.get("can_physical_channel_name") or channel_data.get("lin_physical_channel_name")
        frames: dict[str, Any] = channel_data.get("frames") or {}

        for frame_name, frame_data in frames.items():
            if not isinstance(frame_data, dict):
                continue

            frame_id = str(frame_data["id"]) if frame_data.get("id") is not None else None
            frame_description: str = frame_data.get("description") or ""
            frame_senders: list[str] = frame_data.get("senders") or []
            frame_cycle_time: float | None = frame_data.get("cycle_time")
            payload_size = frame_data.get("payload_size")
            opt_empty_payload = frame_data.get("opt_empty_payload")
            is_can_fd = frame_data.get("is_can_fd")
            is_extended = frame_data.get("is_extended")
            can_fd_flags = frame_data.get("can_fd_flags")
            multiplex_selector = frame_data.get("multiplex_selector")
            e2e = frame_data.get("e2e")
            secoc = frame_data.get("secoc")
            groups = frame_data.get("groups")

            ctx = _FrameContext(
                channel_name=channel_name,
                channel_type=channel_type,
                frame_name=frame_name,
                frame_id=frame_id,
                frame_description=frame_description,
                cycle_time=frame_cycle_time,
                senders=frame_senders,
                file_path=file_path,
                workspace_path=workspace_path,
                channel_cluster=channel_cluster,
                channel_physical_name=channel_physical_name,
                payload_size=payload_size,
                opt_empty_payload=opt_empty_payload,
                is_can_fd=is_can_fd,
                is_extended=is_extended,
                can_fd_flags=can_fd_flags,
                multiplex_selector=multiplex_selector,
                e2e=e2e,
                secoc=secoc,
                groups=groups,
            )
            results.append(_frame_dict(ctx))

            signals: dict[str, Any] = frame_data.get("signals") or {}
            for signal_name, signal_data in signals.items():
                if not isinstance(signal_data, dict):
                    continue
                results.append(_signal_dict(ctx, signal_name, signal_data))

    return results


def _frame_dict(ctx: _FrameContext) -> dict[str, Any]:
    return {
        "type": "frame",
        "channel": ctx.channel_name,
        "channel_type": ctx.channel_type,
        "namespace": None,
        "frame_name": ctx.frame_name,
        "frame_id": ctx.frame_id,
        "description": ctx.frame_description,
        "senders": ctx.senders,
        "receivers": [],
        "unit": None,
        "named_values": None,
        "named_values_json": None,
        "min": None,
        "max": None,
        "factor": None,
        "offset": None,
        "cycle_time": ctx.cycle_time,
        "start_value": None,
        "size": None,
        "is_raw": None,
        "source": "yaml",
        "file_path": ctx.file_path,
        "workspace_path": ctx.workspace_path,
        "start_bit": None,
        "is_big_endian": None,
        "is_signed": None,
        "is_ieee_floating_type": None,
        "multiplex_json": None,
        "payload_size": ctx.payload_size,
        "opt_empty_payload": ctx.opt_empty_payload,
        "is_can_fd": ctx.is_can_fd,
        "is_extended": ctx.is_extended,
        "can_fd_flags": ctx.can_fd_flags,
        "multiplex_selector": ctx.multiplex_selector,
        "e2e_json": _to_json(ctx.e2e),
        "secoc_json": _to_json(ctx.secoc),
        "groups_json": _to_json(ctx.groups),
        "channel_cluster": ctx.channel_cluster,
        "channel_physical_name": ctx.channel_physical_name,
    }


def _signal_dict(
    ctx: _FrameContext,
    signal_name: str,
    signal_data: dict[str, Any],
) -> dict[str, Any]:
    named_values_raw = signal_data.get("named_values")
    if isinstance(named_values_raw, dict) and named_values_raw:
        named_values: dict[str, str] | None = {str(k): str(v) for k, v in named_values_raw.items()}
        named_values_json: str | None = _to_json(named_values_raw)
    else:
        named_values = None
        named_values_json = None

    receivers_raw = signal_data.get("receivers") or []

    is_raw_val = signal_data.get("is_raw")
    is_raw: bool | None = bool(is_raw_val) if is_raw_val is not None else None

    multiplexing = signal_data.get("multiplex")
    multiplex_json = _to_json(multiplexing) if multiplexing else None

    return {
        "type": "signal",
        "signal_name": signal_name,
        "channel": ctx.channel_name,
        "channel_type": ctx.channel_type,
        "namespace": None,
        "frame_name": ctx.frame_name,
        "frame_id": ctx.frame_id,
        "description": signal_data.get("description") or "",
        "senders": ctx.senders,
        "receivers": receivers_raw if isinstance(receivers_raw, list) else [receivers_raw],
        "unit": signal_data.get("unit"),
        "named_values": named_values,
        "named_values_json": named_values_json,
        "min": signal_data.get("min"),
        "max": signal_data.get("max"),
        "factor": signal_data.get("factor"),
        "offset": signal_data.get("offset"),
        "cycle_time": signal_data.get("cycle_time", ctx.cycle_time),
        "start_value": signal_data.get("start_value"),
        "size": signal_data.get("length"),
        "is_raw": is_raw,
        "source": "yaml",
        "file_path": ctx.file_path,
        "workspace_path": ctx.workspace_path,
        "start_bit": signal_data.get("start_bit"),
        "is_big_endian": signal_data.get("is_big_endian"),
        "is_signed": signal_data.get("is_signed"),
        "is_ieee_floating_type": signal_data.get("is_ieee_floating_type"),
        "multiplex_json": multiplex_json,
        "payload_size": None,
        "opt_empty_payload": None,
        "is_can_fd": None,
        "is_extended": None,
        "can_fd_flags": None,
        "multiplex_selector": None,
        "e2e_json": None,
        "secoc_json": None,
        "groups_json": None,
        "channel_cluster": ctx.channel_cluster,
        "channel_physical_name": ctx.channel_physical_name,
    }
