from pathlib import Path
from typing import Annotated, cast

import requests
from mcp.server.fastmcp import FastMCP
from mcp.types import TextContent
from pydantic import Field

from remotivelabs.cli.mcp.utils.cli import TopologyCLIError, run_topology_cli
from remotivelabs.cli.search.assembler import assemble_parsed_db, count_signals_and_frames
from remotivelabs.cli.search.base import SearchOptions
from remotivelabs.cli.search.models import BrokerChannel, ParsedCanChannel, ParsedDbSearchResponse, ParsedLinChannel
from remotivelabs.cli.search.query import Query
from remotivelabs.cli.search.sqlite.index import SQLiteSearch

# ---------------------------------------------------------------------------
# Type aliases for common parameters
# ---------------------------------------------------------------------------

WorkspacePath = Annotated[str, Field(description="Absolute path to project root directory")]
DatabasePath = Annotated[str, Field(description="Path to the input signal database file (.arxml, .dbc, .ldf, etc.)")]
JsonFormat = Annotated[bool, Field(description="Output in JSON format.")]
YamlFormat = Annotated[bool, Field(description="Output in YAML format, this is default")]
OutputPath = Annotated[str | None, Field(description="Optional output path. If omitted, prints to stdout.")]

TOPOLOGY_JSON_SCHEMA_RESOURCE_URI = "schema://remotive/topology/json-schema"

# Schema version is decoupled from the topology CLI version — update this
# constant manually when a new schema is published to releases.beamylabs.com.
_SCHEMA_URL = "https://releases.beamylabs.com/remotive_topology/topology-0.15.schema.json"

# Module-level caches — populated lazily, live for the server process lifetime.
_schema_cache: dict[str, object] | None = None
_index_cache: dict[tuple[str, float], SQLiteSearch] = {}


def _build_format_flags(json: bool, yaml: bool) -> list[str]:
    flags = []
    if json:
        flags.append("--json")
    if yaml:
        flags.append("--yaml")
    return flags


def _run_show_command(subcommand: str, workspace: str, path: str, json: bool = False, yaml: bool = False) -> TextContent:
    cmd = ["show", subcommand, *_build_format_flags(json, yaml), path]
    return TextContent(type="text", text=run_topology_cli(cmd, workspace))


def _get_or_build_index(workspace: str, path: str) -> SQLiteSearch:
    """Return a cached (or freshly built) SQLiteSearch for the given database file.

    The index is cached by ``(absolute_path, mtime)`` so re-queries on the same
    unmodified file are instant without re-running ``convert``.

    Raises :exc:`~remotivelabs.cli.mcp.utils.cli.TopologyCLIError` on failure.
    """
    abs_path = (Path(workspace).resolve() / path).resolve()
    try:
        mtime = abs_path.stat().st_mtime
    except FileNotFoundError:
        raise TopologyCLIError(f"File not found\nPATH: {abs_path}")

    cache_key = (str(abs_path), mtime)
    if cache_key in _index_cache:
        return _index_cache[cache_key]

    yaml_str = run_topology_cli(["convert", "--file", path], workspace)

    index = SQLiteSearch(":memory:")
    index.connect()
    try:
        index.index_yaml_data(path, str(abs_path), yaml_str)
    except Exception as e:
        index.close()
        raise TopologyCLIError(f"Failed to index database file\nPATH: {abs_path}\nDETAILS: {e}") from e

    # Evict stale entries for the same path (different mtime) to avoid memory growth.
    stale = [k for k in _index_cache if k[0] == str(abs_path)]
    for k in stale:
        _index_cache.pop(k).close()

    _index_cache[cache_key] = index
    return index


# ---------------------------------------------------------------------------
# Resource registration
# ---------------------------------------------------------------------------


def _register_schema_resource(mcp: FastMCP) -> None:
    @mcp.resource(
        uri=TOPOLOGY_JSON_SCHEMA_RESOURCE_URI,
        name="topology_json_schema",
        description="JSON Schema for Remotive Topology. Useful for generating instance and platform files",
        mime_type="application/schema+json",
    )
    def topology_json_schema() -> dict[str, object]:
        """Proxy the JSON Schema through the MCP server (cached for the process lifetime)."""
        global _schema_cache  # noqa: PLW0603
        if _schema_cache is not None:
            return _schema_cache
        try:
            resp = requests.get(_SCHEMA_URL, timeout=10)
            resp.raise_for_status()
            _schema_cache = resp.json()
            return _schema_cache
        except Exception as e:
            raise TopologyCLIError(f"Failed to fetch Schema from {_SCHEMA_URL}: {e}") from e


# ---------------------------------------------------------------------------
# Tool registration
# ---------------------------------------------------------------------------


def _register_show_tools(mcp: FastMCP) -> None:
    @mcp.tool(
        name="show_signals",
        description=(
            "Dump all signals from a signal database file (.arxml, .dbc, .ldf). "
            "Use search_signals instead when you want to find specific signals by name, "
            "description, unit, or any other property."
        ),
    )
    def topology_show_signals(
        workspace: WorkspacePath,
        path: DatabasePath,
        json: JsonFormat = False,
        yaml: YamlFormat = False,
        out_path: OutputPath = None,
    ) -> TextContent:
        cmd = ["show", "signals", *_build_format_flags(json, yaml), path]
        if out_path:
            cmd.append(out_path)
        return TextContent(type="text", text=run_topology_cli(cmd, workspace))

    @mcp.tool(
        name="show_constants",
        description="Show constants from automotive signal database file like .arxml, .dbc, .ldf",
    )
    def topology_show_constants(
        workspace: WorkspacePath,
        path: DatabasePath,
        json: JsonFormat = False,
        yaml: YamlFormat = False,
    ) -> TextContent:
        return _run_show_command("constants", workspace, path, json, yaml)

    @mcp.tool(
        name="show_topology",
        description="Show topology from automotive platform like .arxml or signal database file like .dbc, .ldf",
    )
    def topology_show_topology(
        workspace: WorkspacePath,
        path: DatabasePath,
        json: JsonFormat = False,
        yaml: YamlFormat = False,
        out_path: OutputPath = None,
    ) -> TextContent:
        cmd = ["show", "platform", *_build_format_flags(json, yaml)]
        if out_path:
            cmd += ["--out-path", out_path]
        cmd.append(path)
        return TextContent(type="text", text=run_topology_cli(cmd, workspace))

    @mcp.tool(
        name="show_ethernet",
        description="Show ethernet configuration from a database file (experimental).",
    )
    def topology_show_ethernet(
        workspace: WorkspacePath,
        path: DatabasePath,
        json: JsonFormat = False,
        yaml: YamlFormat = False,
    ) -> TextContent:
        return _run_show_command("ethernet", workspace, path, json, yaml)

    @mcp.tool(
        name="show_services",
        description="Show SOME/IP services from a database file (experimental).",
    )
    def topology_show_services(
        workspace: WorkspacePath,
        path: DatabasePath,
        json: JsonFormat = False,
        yaml: YamlFormat = False,
    ) -> TextContent:
        return _run_show_command("services", workspace, path, json, yaml)


def _register_search_tools(mcp: FastMCP) -> None:
    @mcp.tool(
        name="search_signals",
        description=(
            "Search for signals and frames inside a CAN/LIN signal database file "
            "(.dbc, .arxml, .ldf, .platform.yaml). "
            "Returns a compact hierarchical channel → frame → signal structure with full metadata "
            "(unit, min/max, named values, senders/receivers, CAN IDs, cycle time, etc.). "
            "Prefer this over show_signals when looking for specific signals — it avoids "
            "overwhelming the context with thousands of irrelevant entries. "
            "The index is cached per file so repeated queries are fast."
        ),
    )
    def topology_search_signals(
        workspace: WorkspacePath,
        path: DatabasePath,
        q: Annotated[str, Field(description="Free-text search query (signal name, description, unit, frame name, etc.)")],
        channel: Annotated[str | None, Field(description="Limit results to a specific channel name")] = None,
        limit: Annotated[int, Field(description="Maximum number of results (default 50, max 500)", ge=1, le=500)] = 50,
    ) -> TextContent:
        index = _get_or_build_index(workspace, path)

        filters: dict[str, str] = {}
        if channel:
            filters["channel"] = channel

        opts = SearchOptions(source_filter="yaml", filters=filters)
        rows = index.search(Query.parse(q), limit=limit, opts=opts)

        if not rows:
            suffix = f" in channel '{channel}'" if channel else ""
            return TextContent(type="text", text=f"No signals found matching '{q}'{suffix}.")

        db = assemble_parsed_db(rows, index)
        total = count_signals_and_frames(db)

        typed_channels = cast(
            dict[str, ParsedCanChannel | ParsedLinChannel | BrokerChannel],
            db.channels or {},
        )
        response = ParsedDbSearchResponse(channels=typed_channels, total=total)
        return TextContent(type="text", text=response.model_dump_json(exclude_none=True, indent=2))


def _register_build_tools(mcp: FastMCP) -> None:
    @mcp.tool(
        name="build",
        description="Build an executable environment from topology instance files, always ends with instance.yaml",
    )
    def topology_generate(
        workspace: WorkspacePath,
        path: Annotated[list[str], Field(description="Path to the input instance files")],
        output_dir: Annotated[str, Field(description="Output directory, required")],
    ) -> TextContent:
        cmd = ["generate", *(arg for p in path for arg in ("-f", p)), output_dir]
        return TextContent(type="text", text=run_topology_cli(cmd, workspace))


def register_tools(mcp: FastMCP) -> None:
    """Register all Remotive Topology tools and resources."""
    _register_schema_resource(mcp)
    _register_show_tools(mcp)
    _register_search_tools(mcp)
    _register_build_tools(mcp)
