"""Assembles flat search rows into hierarchical ParsedRemotiveDb structure."""

from __future__ import annotations

import json
from typing import TYPE_CHECKING, Any

from .models import (
    BrokerChannel,
    ParsedAuthProfile,
    ParsedCanChannel,
    ParsedCanFrame,
    ParsedE2eAutosar01,
    ParsedFvProfile,
    ParsedGroup,
    ParsedLinChannel,
    ParsedLinFrame,
    ParsedMultiplexFilter,
    ParsedMultiplexSelector,
    ParsedPduInfo,
    ParsedRemotiveDb,
    ParsedSignal,
)

if TYPE_CHECKING:
    from .sqlite.index import SQLiteSearch


def _parse_json(s: str | None) -> Any:
    if not s:
        return None
    try:
        return json.loads(s)
    except json.JSONDecodeError:
        return None


def _split_list(s: str | None) -> list[str]:
    if s is None:
        return []
    if isinstance(s, list):
        return list(s)
    return [x.strip() for x in str(s).split() if x.strip()]


def _to_bool(v: Any) -> bool | None:
    if v is None:
        return None
    return bool(v)


def _dict_to_pdu_info(d: dict[str, Any] | None) -> ParsedPduInfo | None:
    if not d or not isinstance(d, dict):
        return None
    auth = d.get("auth_profile")
    auth_profile = ParsedAuthProfile.model_construct(**auth) if isinstance(auth, dict) else None
    fv = d.get("fv_profile")
    fv_profile = ParsedFvProfile.model_construct(**fv) if isinstance(fv, dict) else None
    return ParsedPduInfo.model_construct(
        data_id=d.get("data_id"),
        length=d.get("length"),
        fv_id=d.get("fv_id"),
        rx_verify=d.get("rx_verify"),
        key_id=d.get("key_id"),
        build_attempts=d.get("build_attempts"),
        retries=d.get("retries"),
        acceptance_window=d.get("acceptance_window"),
        auth_profile=auth_profile,
        fv_profile=fv_profile,
    )


def _row_to_parsed_signal(row: dict[str, Any]) -> ParsedSignal:
    """Build ParsedSignal from a flat signal row using model_construct."""
    multiplexing: ParsedMultiplexSelector | ParsedMultiplexFilter | None = None
    mult_raw = _parse_json(row.get("multiplex_json"))
    if isinstance(mult_raw, dict):
        mtype = mult_raw.get("type")
        if mtype == "selector":
            multiplexing = ParsedMultiplexSelector.model_construct(type="selector")
        elif mtype == "filter":
            multiplexing = ParsedMultiplexFilter.model_construct(
                type="filter",
                mode=mult_raw.get("mode"),
            )

    named_values = _parse_json(row.get("named_values_json"))
    if isinstance(named_values, dict):
        named_values = {str(k): str(v) for k, v in named_values.items()}
    else:
        named_values = None

    return ParsedSignal.model_construct(
        description=row.get("description") or None,
        start_bit=row.get("start_bit"),
        length=row.get("size"),
        receivers=_split_list(row.get("receivers")),
        min=float(row["min"]) if row.get("min") is not None else None,
        max=float(row["max"]) if row.get("max") is not None else None,
        offset=float(row["offset"]) if row.get("offset") is not None else None,
        factor=float(row["factor"]) if row.get("factor") is not None else None,
        unit=row.get("unit"),
        named_values=named_values,
        start_value=float(row["start_value"]) if row.get("start_value") is not None else None,
        multiplexing=multiplexing,
        is_big_endian=_to_bool(row.get("is_big_endian")),
        is_signed=_to_bool(row.get("is_signed")),
        is_ieee_floating_type=_to_bool(row.get("is_ieee_floating_type")),
        is_raw=_to_bool(row.get("is_raw")),
    )


def _row_to_frame_data(row: dict[str, Any]) -> dict[str, Any]:
    """Extract frame-level fields from a row for ParsedCanFrame or ParsedLinFrame."""
    e2e_raw = _parse_json(row.get("e2e_json"))
    secoc_raw = _parse_json(row.get("secoc_json"))
    groups_raw = _parse_json(row.get("groups_json"))

    e2e = None
    if isinstance(e2e_raw, dict):
        e2e = ParsedE2eAutosar01.model_construct(
            type=e2e_raw.get("type", "autosar_01a"),
            data_id=e2e_raw.get("data_id"),
            signal_crc=e2e_raw.get("signal_crc"),
            signal_counter=e2e_raw.get("signal_counter"),
        )

    secoc = _dict_to_pdu_info(secoc_raw) if isinstance(secoc_raw, dict) else None

    groups = None
    if isinstance(groups_raw, list) and groups_raw:
        gl: list[ParsedGroup] = []
        for g in groups_raw:
            if not isinstance(g, dict):
                continue
            ge2e = g.get("e2e")
            e2e_g = ParsedE2eAutosar01.model_construct(**ge2e) if isinstance(ge2e, dict) else None
            secoc_g = _dict_to_pdu_info(g.get("secoc"))
            gl.append(
                ParsedGroup.model_construct(
                    start_bit=g.get("start_bit"),
                    length=g.get("length"),
                    e2e=e2e_g,
                    secoc=secoc_g,
                )
            )
        groups = gl if gl else None

    frame_id = row.get("frame_id")
    if frame_id is not None:
        try:
            frame_id = int(frame_id)
        except (ValueError, TypeError):
            pass

    return {
        "id": frame_id,
        "description": row.get("description") or None,
        "payload_size": row.get("payload_size"),
        "opt_empty_payload": row.get("opt_empty_payload"),
        "senders": _split_list(row.get("senders")),
        "e2e": e2e,
        "secoc": secoc,
        "groups": groups,
    }


def _fetch_missing_parent_frames(
    rows: list[dict[str, Any]],
    frame_keys: set[tuple[str, str, str]],
    signal_parent_keys: set[tuple[str, str, str]],
    index: SQLiteSearch,
) -> None:
    """Fetch missing parent frames and append to rows."""
    missing = signal_parent_keys - frame_keys
    for fp, ch, fn in missing:
        extra = index.fetch_frame_row(fp, ch, fn)
        if extra:
            rows.append(extra)
            frame_keys.add((fp, ch, fn))


def _group_rows_by_file_channel(
    rows: list[dict[str, Any]],
) -> dict[tuple[str, str], dict[str, dict[str, Any]]]:
    """Group rows by (file_path, channel) then by frame_name."""
    by_file_channel: dict[tuple[str, str], dict[str, dict[str, Any]]] = {}
    for row in rows:
        fp = row.get("file_path") or ""
        ch = row.get("channel") or ""
        key = (fp, ch)
        if key not in by_file_channel:
            by_file_channel[key] = {}
        if row.get("type") == "frame":
            fn = row.get("frame_name", "")
            if fn and fn not in by_file_channel[key]:
                by_file_channel[key][fn] = {"frame": row, "signals": {}}
        else:
            fn = row.get("frame_name") or ""
            if fn:
                if fn not in by_file_channel[key]:
                    by_file_channel[key][fn] = {"frame": None, "signals": {}}
                by_file_channel[key][fn]["signals"][row.get("signal_name", "")] = row
    return by_file_channel


def _build_parsed_frames(
    frames_data: dict[str, dict[str, Any]],
    channel_type: str,
) -> dict[str, ParsedCanFrame | ParsedLinFrame]:
    """Build parsed frames dict from frames_data."""
    parsed_frames: dict[str, ParsedCanFrame | ParsedLinFrame] = {}
    for frame_name, fd in frames_data.items():
        frame_row = fd["frame"]
        signal_rows = fd["signals"]
        if not frame_row and not signal_rows:
            continue
        row = frame_row or next(iter(signal_rows.values()))
        base = _row_to_frame_data(row)
        signals_dict: dict[str, ParsedSignal] = {name: _row_to_parsed_signal(sr) for name, sr in signal_rows.items()}
        base["signals"] = signals_dict if signals_dict else None

        if channel_type == "lin":
            parsed_frames[frame_name] = ParsedLinFrame.model_construct(**base)
        else:
            base["cycle_time"] = float(row["cycle_time"]) if row.get("cycle_time") is not None else None
            base["multiplex_selector"] = row.get("multiplex_selector")
            base["is_can_fd"] = _to_bool(row.get("is_can_fd"))
            base["is_extended"] = _to_bool(row.get("is_extended"))
            base["can_fd_flags"] = row.get("can_fd_flags")
            parsed_frames[frame_name] = ParsedCanFrame.model_construct(**base)
    return parsed_frames


def _build_channel(
    frames_data: dict[str, dict[str, Any]],
) -> ParsedCanChannel | ParsedLinChannel | None:
    """Build ParsedCanChannel or ParsedLinChannel from frames_data."""
    sample_row = None
    for fd in frames_data.values():
        if fd["frame"]:
            sample_row = fd["frame"]
            break
        if fd["signals"]:
            sample_row = next(iter(fd["signals"].values()))
            break
    if not sample_row:
        return None

    channel_type = sample_row.get("channel_type") or "can"
    cluster = sample_row.get("channel_cluster")
    physical_name = sample_row.get("channel_physical_name")
    parsed_frames = _build_parsed_frames(frames_data, channel_type)

    if channel_type == "lin":
        return ParsedLinChannel.model_construct(
            type="lin",
            lin_cluster=cluster,
            lin_physical_channel_name=physical_name,
            frames=parsed_frames or None,
        )
    return ParsedCanChannel.model_construct(
        type="can",
        can_cluster=cluster,
        can_physical_channel_name=physical_name,
        frames=parsed_frames or None,
    )


def assemble_parsed_db(
    rows: list[dict[str, Any]],
    index: SQLiteSearch,
) -> ParsedRemotiveDb:
    """Build ParsedRemotiveDb from flat search rows.

    When a signal matches, its parent frame is always included.
    Groups rows by file_path, channel, frame; fetches missing frame rows.
    """
    frame_keys: set[tuple[str, str, str]] = set()
    signal_parent_keys: set[tuple[str, str, str]] = set()

    for row in rows:
        fp = row.get("file_path")
        ch = row.get("channel")
        if not fp or not ch:
            continue
        if row.get("type") == "frame":
            frame_keys.add((fp, ch, row.get("frame_name", "")))
        else:
            fn = row.get("frame_name")
            if fn:
                signal_parent_keys.add((fp, ch, fn))

    _fetch_missing_parent_frames(rows, frame_keys, signal_parent_keys, index)
    by_file_channel = _group_rows_by_file_channel(rows)

    channels: dict[str, ParsedCanChannel | ParsedLinChannel] = {}
    for (fp, ch), frames_data in by_file_channel.items():
        if not ch:
            continue
        channel = _build_channel(frames_data)
        if channel is not None:
            channels[ch] = channel

    return ParsedRemotiveDb.model_construct(schema=None, channels=channels or None)


def count_signals_and_frames(db: ParsedRemotiveDb) -> int:
    """Count total signals + frames in the hierarchy."""
    total = 0
    for ch in (db.channels or {}).values():
        for fr in (ch.frames or {}).values():
            total += 1
            total += len(fr.signals or {})
    return total


def _group_rows_by_namespace(
    rows: list[dict[str, Any]],
) -> dict[str, dict[str, dict[str, Any]]]:
    """Group broker rows by namespace then by frame_name."""
    by_namespace: dict[str, dict[str, dict[str, Any]]] = {}
    for row in rows:
        ns = row.get("namespace") or ""
        if ns not in by_namespace:
            by_namespace[ns] = {}
        if row.get("type") == "frame":
            fn = row.get("frame_name", "")
            if fn and fn not in by_namespace[ns]:
                by_namespace[ns][fn] = {"frame": row, "signals": {}}
        else:
            fn = row.get("frame_name") or ""
            if fn:
                if fn not in by_namespace[ns]:
                    by_namespace[ns][fn] = {"frame": None, "signals": {}}
                by_namespace[ns][fn]["signals"][row.get("signal_name", "")] = row
    return by_namespace


def _build_broker_frame(fd: dict[str, Any]) -> ParsedCanFrame | None:
    """Build a ParsedCanFrame from a broker frame+signals dict."""
    frame_row = fd["frame"]
    signal_rows = fd["signals"]
    if not frame_row and not signal_rows:
        return None
    row = frame_row or next(iter(signal_rows.values()))
    base = _row_to_frame_data(row)
    signals_dict: dict[str, ParsedSignal] = {name: _row_to_parsed_signal(sr) for name, sr in signal_rows.items()}
    base["signals"] = signals_dict if signals_dict else None
    base["cycle_time"] = float(row["cycle_time"]) if row.get("cycle_time") is not None else None
    base["multiplex_selector"] = row.get("multiplex_selector")
    base["is_can_fd"] = _to_bool(row.get("is_can_fd"))
    base["is_extended"] = _to_bool(row.get("is_extended"))
    base["can_fd_flags"] = row.get("can_fd_flags")
    return ParsedCanFrame.model_construct(**base)


def _fetch_missing_broker_parent_frames(
    rows: list[dict[str, Any]],
    index: SQLiteSearch,
) -> None:
    """Append any parent frame rows that are missing for broker signal rows in *rows*."""
    frame_keys: set[tuple[str, str]] = set()
    signal_parent_keys: set[tuple[str, str]] = set()

    for row in rows:
        ns = row.get("namespace")
        if not ns:
            continue
        if row.get("type") == "frame":
            frame_keys.add((ns, row.get("frame_name", "")))
        else:
            fn = row.get("frame_name")
            if fn:
                signal_parent_keys.add((ns, fn))

    for ns, fn in signal_parent_keys - frame_keys:
        extra = index.fetch_broker_frame_row(ns, fn)
        if extra:
            rows.append(extra)


def assemble_broker_channels(
    rows: list[dict[str, Any]],
    index: SQLiteSearch,
) -> dict[str, BrokerChannel]:
    """Build a dict of BrokerChannel from flat broker (gRPC) search rows.

    Channel keys are namespace names. Missing parent frames for matched signals
    are fetched from the index so every signal is always shown under its frame.
    """
    _fetch_missing_broker_parent_frames(rows, index)

    by_namespace = _group_rows_by_namespace(rows)

    channels: dict[str, BrokerChannel] = {}
    for ns, frames_data in by_namespace.items():
        if not ns:
            continue
        parsed_frames: dict[str, ParsedCanFrame] = {}
        for frame_name, fd in frames_data.items():
            frame = _build_broker_frame(fd)
            if frame is not None:
                parsed_frames[frame_name] = frame
        channels[ns] = BrokerChannel.model_construct(
            type="broker",
            frames=parsed_frames or None,
        )
    return channels


def count_broker_signals_and_frames(channels: dict[str, BrokerChannel]) -> int:
    """Count total signals + frames across all broker channels."""
    total = 0
    for ch in channels.values():
        for fr in (ch.frames or {}).values():
            total += 1
            total += len(fr.signals or {})
    return total
