"""Pydantic response models for the search API."""

from __future__ import annotations

from typing import Union

from pydantic import BaseModel, Field


class SearchBaseModel(BaseModel):
    """Base for Pydantic models used by the search API (shared extension point)."""


# ---------------------------------------------------------------------------
# ParsedRemotiveDb hierarchy (matches TypeScript types from remotive-topology)
# ---------------------------------------------------------------------------


class ParsedMultiplexSelector(SearchBaseModel):
    type: str = "selector"


class ParsedMultiplexFilter(SearchBaseModel):
    type: str = "filter"
    mode: int | None = None


class ParsedFvProfile(SearchBaseModel):
    use_timestamp: bool | None = None
    tx_length: int | None = None
    length: int | None = None


class ParsedAuthProfile(SearchBaseModel):
    auth_algo: str | None = None  # e.g. 'aes_128_cbc'
    tx_length: int | None = None


class ParsedE2eAutosar01(SearchBaseModel):
    type: str = "autosar_01a"
    data_id: int | None = None
    signal_crc: str | None = None
    signal_counter: str | None = None


class ParsedPduInfo(SearchBaseModel):
    data_id: int | None = None
    length: int | None = None
    fv_id: int | None = None
    rx_verify: bool | None = None
    key_id: int | None = None
    build_attempts: int | None = None
    retries: int | None = None
    acceptance_window: int | None = None
    auth_profile: ParsedAuthProfile | None = None
    fv_profile: ParsedFvProfile | None = None


class ParsedGroup(SearchBaseModel):
    start_bit: int | None = None
    length: int | None = None
    e2e: ParsedE2eAutosar01 | None = None
    secoc: ParsedPduInfo | None = None


class ParsedSignal(SearchBaseModel):
    """A single signal within a frame."""

    description: str | None = Field(default=None, description="Human-readable description of the signal.")
    start_bit: int | None = Field(default=None, description="Start bit position within the frame payload.")
    length: int | None = Field(default=None, description="Bit length of the signal.")
    receivers: list[str] | None = Field(default=None, description="ECU nodes that receive this signal.")
    min: float | None = Field(default=None, description="Minimum physical value after scaling.")
    max: float | None = Field(default=None, description="Maximum physical value after scaling.")
    offset: float | None = Field(default=None, description="Offset applied in the linear value conversion.")
    factor: float | None = Field(default=None, description="Factor applied in the linear value conversion.")
    unit: str | None = Field(default=None, description="Physical unit of the signal (e.g. 'rpm', 'degC').")
    named_values: dict[str, str] | None = Field(
        default=None,
        description='Map of raw integer value → symbolic name (e.g. {"0": "Off", "1": "On"}).',
    )
    start_value: float | None = Field(default=None, description="Default/initial value of the signal.")
    multiplexing: Union[ParsedMultiplexSelector, ParsedMultiplexFilter, None] = Field(
        default=None, description="Multiplexing descriptor; null when the signal is not multiplexed."
    )
    is_big_endian: bool | None = Field(default=None, description="True when bytes are big-endian (Motorola) encoded.")
    is_signed: bool | None = Field(default=None, description="True when the raw value is a signed integer.")
    is_ieee_floating_type: bool | None = Field(default=None, description="True when the signal uses IEEE 754 float encoding.")
    is_raw: bool | None = Field(default=None, description="True when no scaling is applied (raw bit pattern only).")


class ParsedLinFrame(SearchBaseModel):
    """A LIN bus frame containing signals."""

    id: int | None = Field(default=None, description="LIN frame identifier.")
    description: str | None = Field(default=None, description="Human-readable description of the frame.")
    payload_size: int | None = Field(default=None, description="Frame payload size in bytes.")
    opt_empty_payload: str | None = None
    senders: list[str] | None = Field(default=None, description="ECU nodes that transmit this frame.")
    signals: dict[str, ParsedSignal] | None = Field(default=None, description="Map of signal name → signal definition.")


class ParsedCanFrame(SearchBaseModel):
    """A CAN bus frame containing signals."""

    id: int | None = Field(default=None, description="CAN frame identifier (decimal).")
    description: str | None = Field(default=None, description="Human-readable description of the frame.")
    payload_size: int | None = Field(default=None, description="Frame payload size in bytes.")
    cycle_time: float | None = Field(default=None, description="Cyclic transmission time in milliseconds.")
    opt_empty_payload: str | None = None
    multiplex_selector: str | None = Field(default=None, description="Name of the multiplex selector signal, if any.")
    is_can_fd: bool | None = Field(default=None, description="True for CAN FD frames.")
    is_extended: bool | None = Field(default=None, description="True for 29-bit extended CAN identifiers.")
    can_fd_flags: int | None = None
    e2e: ParsedE2eAutosar01 | None = None
    secoc: ParsedPduInfo | None = None
    groups: list[ParsedGroup] | None = None
    senders: list[str] | None = Field(default=None, description="ECU nodes that transmit this frame.")
    signals: dict[str, ParsedSignal] | None = Field(default=None, description="Map of signal name → signal definition.")


class ParsedLinChannel(SearchBaseModel):
    """A LIN bus channel (cluster)."""

    type: str = Field(default="lin", description="Channel type discriminator; always 'lin'.")
    lin_cluster: str | None = Field(default=None, description="LIN cluster name.")
    lin_physical_channel_name: str | None = Field(default=None, description="Physical channel name within the cluster.")
    frames: dict[str, ParsedLinFrame] | None = Field(default=None, description="Map of frame name → frame definition.")


class ParsedCanChannel(SearchBaseModel):
    """A CAN bus channel (cluster)."""

    type: str = Field(default="can", description="Channel type discriminator; always 'can'.")
    can_cluster: str | None = Field(default=None, description="CAN cluster name.")
    can_physical_channel_name: str | None = Field(default=None, description="Physical channel name within the cluster.")
    frames: dict[str, ParsedCanFrame] | None = Field(default=None, description="Map of frame name → frame definition.")


class BrokerChannel(SearchBaseModel):
    """A live broker channel (gRPC topology source)."""

    type: str = Field(default="broker", description="Channel type discriminator; always 'broker'.")
    frames: dict[str, ParsedCanFrame] | None = Field(default=None, description="Map of frame name → frame definition.")


class ParsedRemotiveDb(SearchBaseModel):
    schema_version: str | None = Field(default=None, alias="schema")
    channels: dict[str, ParsedCanChannel | ParsedLinChannel] | None = None


class ParsedDbSearchResponse(SearchBaseModel):
    """Hierarchical search response keyed by channel/namespace name.

    Returned when `mode=search` (default) and `slim=false` (default).
    Each channel contains frames; each matching signal's parent frame is always
    included. Channel type is `"broker"` for topology results and `"can"` /
    `"lin"` for file results.
    """

    channels: dict[str, ParsedCanChannel | ParsedLinChannel | BrokerChannel] | None = Field(
        default=None,
        description=(
            "Map of channel/namespace name → channel object. "
            "Channel type is 'broker' for topology results, 'can' or 'lin' for file results."
        ),
    )
    total: int = Field(description="Total number of matching frames + signals.")

    model_config = {
        "json_schema_extra": {
            "examples": [
                {
                    "channels": {
                        "PowertrainCAN": {
                            "type": "broker",
                            "frames": {
                                "EngineData": {
                                    "id": None,
                                    "description": "Engine sensor data frame",
                                    "payload_size": None,
                                    "cycle_time": 50.0,
                                    "senders": ["ECU"],
                                    "signals": {
                                        "EngineRPM": {
                                            "description": "Engine rotations per minute",
                                            "unit": "rpm",
                                            "min": 0.0,
                                            "max": 8000.0,
                                            "factor": 1.0,
                                        }
                                    },
                                }
                            },
                        }
                    },
                    "total": 1,
                }
            ]
        }
    }


class SlimSearchResponse(SearchBaseModel):
    """Slim search response returning signal IDs only (no frame metadata).

    Returned when `mode=search` and `slim=true`. Each ID has the form
    `"{namespace}:{signal_name}"` for topology results and
    `"{channel}:{signal_name}"` for file results.
    """

    ids: list[str] = Field(
        description=("List of matching signal IDs in the form 'namespace:signal_name' (topology) or 'channel:signal_name' (files).")
    )
    total: int = Field(description="Total number of matching signals.")

    model_config = {
        "json_schema_extra": {
            "examples": [{"ids": ["PowertrainCAN:EngineData.EngineRPM", "PowertrainCAN:EngineData.EngineTemp"], "total": 2}]
        }
    }


class SuggestionItem(SearchBaseModel):
    """A single autocomplete suggestion."""

    field: str = Field(
        description=(
            "The field the suggestion came from (e.g. 'signal_name', 'frame_name', 'frame_id', "
            "'named_values', 'file_path', 'description', 'unit', 'namespace', 'channel')."
        )
    )
    suggestion: str = Field(description="The suggested completion value.")


class SuggestResponse(SearchBaseModel):
    """Suggest response returning autocomplete candidates.

    Returned when `mode=suggest`. Results are deduped, capped at 100, and
    exclude values already present in the `selected` parameter.
    """

    suggestions: list[SuggestionItem] = Field(description="List of autocomplete suggestions.")
    total: int = Field(description="Total number of suggestion candidates before capping.")

    model_config = {
        "json_schema_extra": {
            "examples": [
                {
                    "suggestions": [
                        {"field": "signal_name", "suggestion": "EngineRPM"},
                        {"field": "description", "suggestion": "Engine"},
                    ],
                    "total": 2,
                }
            ]
        }
    }


class IndexResponse(SearchBaseModel):
    """Response returned after a successful (re-)index operation."""

    indexed: int = Field(description="Number of frames + signals written to the index.")

    model_config = {"json_schema_extra": {"examples": [{"indexed": 1423}]}}


class StatusResponse(SearchBaseModel):
    """Index status response.

    Returned when `mode=status`. Reports how many entries are in the index and
    when they were last updated.
    """

    total: int = Field(description="Total number of frames + signals currently in the index.")
    last_indexed: str | None = Field(
        default=None,
        description="ISO 8601 timestamp of the most recent index operation, or null if the index is empty.",
    )

    model_config = {"json_schema_extra": {"examples": [{"total": 1423, "last_indexed": "2026-03-09T10:22:01.123456+00:00"}]}}
