"""Optional MCP server exposing the public ferro-ta API."""

from __future__ import annotations

import dataclasses
import enum
import importlib
import inspect
import json
import re
from collections.abc import Callable, Mapping
from datetime import date, datetime, time
from functools import lru_cache
from itertools import count
from typing import Any, cast, get_args, get_origin, get_type_hints

import numpy as np

import ferro_ta
from ferro_ta.tools import compute_indicator, run_backtest
from ferro_ta.tools.api_info import methods as api_methods

__all__ = ["create_server", "run_server", "handle_list_tools", "handle_call_tool"]

_MCP_INSTALL_HINT = (
    "The ferro-ta MCP server requires the optional 'mcp' dependency. "
    'Install it with `pip install "ferro-ta[mcp]"` or `uv sync --extra mcp`.'
)

_REFERENCE_HELP = (
    "Use {'instance_id': '<id>'} for stored objects or "
    "{'callable': '<public ferro-ta name>'} for public callables."
)

_JSON_ANY_TYPE: list[str] = [
    "array",
    "boolean",
    "integer",
    "null",
    "number",
    "object",
    "string",
]

_NO_DEFAULT = object()
_INSTANCE_STORE: dict[str, Any] = {}
_INSTANCE_META: dict[str, dict[str, Any]] = {}
_INSTANCE_COUNTER = count(1)


@dataclasses.dataclass(frozen=True)
class _ToolSpec:
    """Resolved metadata and dispatcher for a single MCP tool."""

    name: str
    description: str
    input_schema: dict[str, Any]
    wrapper_signature: inspect.Signature
    invoke: Callable[[dict[str, Any]], Any]


def _import_object(module_name: str, name: str) -> Any:
    """Import *name* from *module_name*."""
    module = importlib.import_module(module_name)
    return getattr(module, name)


@lru_cache(maxsize=1)
def _discover_public_callables() -> tuple[list[dict[str, str]], dict[str, Any]]:
    """Return canonical public callable metadata and lookup targets."""
    entries = api_methods()
    top_level = sorted(
        [item for item in entries if item["category"] == "top_level"],
        key=lambda item: item["name"],
    )
    top_level_names = {item["name"] for item in top_level}
    extras = sorted(
        [
            item
            for item in entries
            if item["category"] != "top_level" and item["name"] not in top_level_names
        ],
        key=lambda item: item["name"],
    )

    public_entries: list[dict[str, str]] = []
    targets: dict[str, Any] = {}
    for item in [*top_level, *extras]:
        name = item["name"]
        if name in targets:
            continue
        targets[name] = _import_object(item["module"], name)
        public_entries.append(item)

    return public_entries, targets


def _slugify(value: str) -> str:
    """Convert *value* into a short identifier fragment."""
    slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
    return slug or "object"


def _instance_ref(value: Any, *, source_tool: str) -> dict[str, Any]:
    """Store *value* and return a serialisable reference payload."""
    identifier = f"{_slugify(type(value).__name__)}-{next(_INSTANCE_COUNTER):04d}"
    _INSTANCE_STORE[identifier] = value
    payload = {
        "instance_id": identifier,
        "type": f"{type(value).__module__}.{type(value).__name__}",
        "repr": repr(value),
        "callable": callable(value),
        "source_tool": source_tool,
    }
    snapshot = _object_snapshot(value)
    if snapshot is not None:
        payload["snapshot"] = snapshot
    _INSTANCE_META[identifier] = payload
    return payload


def _get_instance(identifier: str) -> Any:
    """Return the stored instance for *identifier* or raise a clear error."""
    try:
        return _INSTANCE_STORE[identifier]
    except KeyError as exc:
        raise KeyError(f"Unknown instance_id: {identifier!r}") from exc


def _is_instance_ref(value: Any) -> bool:
    """Return whether *value* is a stored-object reference payload."""
    return isinstance(value, dict) and set(value) == {"instance_id"}


def _is_callable_ref(value: Any) -> bool:
    """Return whether *value* is a public-callable reference payload."""
    return isinstance(value, dict) and set(value) == {"callable"}


def _public_method_summaries(value: Any) -> list[dict[str, str]]:
    """Return public callable methods for *value*."""
    result: list[dict[str, str]] = []
    for method_name, member in inspect.getmembers(value):
        if method_name.startswith("_") or not callable(member):
            continue
        try:
            signature = str(inspect.signature(member))
        except (TypeError, ValueError):
            signature = "()"
        result.append({"name": method_name, "signature": signature})
    return result


def _object_snapshot(value: Any) -> Any:
    """Return a serialisable snapshot for common non-primitive objects."""
    if isinstance(value, enum.Enum):
        return value.value

    if dataclasses.is_dataclass(value) and not isinstance(value, type):
        return _normalise_json(dataclasses.asdict(value), store_objects=False)

    dynamic_value = cast(Any, value)

    if hasattr(dynamic_value, "to_dict") and callable(dynamic_value.to_dict):
        try:
            return _normalise_json(dynamic_value.to_dict(), store_objects=False)
        except TypeError:
            if dynamic_value.__class__.__module__.startswith("pandas"):
                return _normalise_json(
                    dynamic_value.to_dict(orient="list"), store_objects=False
                )
            if dynamic_value.__class__.__module__.startswith("polars"):
                return _normalise_json(
                    dynamic_value.to_dict(as_series=False), store_objects=False
                )
        except Exception:
            return None

    if hasattr(dynamic_value, "__dict__"):
        fields = {
            key: val
            for key, val in vars(dynamic_value).items()
            if not key.startswith("_") and not callable(val)
        }
        if fields:
            return _normalise_json(fields, store_objects=False)

    slots = getattr(type(dynamic_value), "__slots__", ())
    if slots:
        fields = {}
        for slot in slots:
            if slot.startswith("_") or not hasattr(dynamic_value, slot):
                continue
            slot_value = getattr(dynamic_value, slot)
            if callable(slot_value):
                continue
            fields[slot] = slot_value
        if fields:
            return _normalise_json(fields, store_objects=False)

    return None


def _normalise_json(value: Any, *, store_objects: bool = True) -> Any:
    """Convert Python and numpy-rich values into JSON-safe values."""
    if value is None or isinstance(value, (str, bool)):
        return value

    if isinstance(value, (int, np.integer)):
        return int(value)

    if isinstance(value, (float, np.floating)):
        return None if np.isnan(value) else float(value)

    if isinstance(value, (date, datetime, time)):
        return value.isoformat()

    if isinstance(value, enum.Enum):
        return _normalise_json(value.value, store_objects=store_objects)

    if isinstance(value, np.ndarray):
        return [
            _normalise_json(item, store_objects=store_objects)
            for item in value.tolist()
        ]

    if isinstance(value, np.generic):
        return _normalise_json(value.item(), store_objects=store_objects)

    if isinstance(value, Mapping):
        return {
            str(key): _normalise_json(item, store_objects=store_objects)
            for key, item in value.items()
        }

    if isinstance(value, (list, tuple, set, frozenset)):
        return [_normalise_json(item, store_objects=store_objects) for item in value]

    if hasattr(value, "tolist") and callable(value.tolist):
        try:
            return _normalise_json(value.tolist(), store_objects=store_objects)
        except Exception:
            pass

    snapshot = _object_snapshot(value)
    if snapshot is not None:
        return snapshot

    if store_objects:
        return _instance_ref(value, source_tool="return_value")

    return repr(value)


def _json_result(
    payload: Any,
    *,
    structured_key: str | None = None,
) -> dict[str, Any]:
    """Wrap *payload* in the helper response shape."""
    structured: dict[str, Any] | None = None
    if isinstance(payload, dict):
        structured = payload
    elif structured_key is not None:
        structured = {structured_key: payload}

    result: dict[str, Any] = {
        "content": [{"type": "text", "text": json.dumps(payload)}],
    }
    if structured is not None:
        result["structuredContent"] = structured
    return result


def _text_result(
    text: str,
    *,
    structured: dict[str, Any] | None = None,
    is_error: bool = False,
) -> dict[str, Any]:
    """Wrap *text* in the helper response shape."""
    result: dict[str, Any] = {
        "content": [{"type": "text", "text": text}],
    }
    if structured is not None:
        result["structuredContent"] = structured
    if is_error:
        result["isError"] = True
    return result


def _response_from_payload(payload: Any) -> dict[str, Any]:
    """Create a helper response from a normalised payload."""
    if isinstance(payload, str):
        return _text_result(payload, structured={"value": payload})
    return _json_result(payload)


def _load_mcp_sdk() -> tuple[type[Any], type[Any], type[Any]]:
    """Import the optional MCP SDK lazily."""
    try:
        fastmcp = importlib.import_module("mcp.server.fastmcp")
        types = importlib.import_module("mcp.types")
    except ImportError as exc:  # pragma: no cover - exercised via tests
        raise RuntimeError(_MCP_INSTALL_HINT) from exc

    return fastmcp.FastMCP, types.CallToolResult, types.TextContent


def _to_call_tool_result(result: dict[str, Any]) -> Any:
    """Convert helper-style results into an MCP SDK CallToolResult."""
    _, call_tool_result_type, text_content_type = _load_mcp_sdk()
    content = [
        text_content_type(type="text", text=item["text"])
        for item in result.get("content", [])
        if item.get("type") == "text"
    ]
    return call_tool_result_type(
        content=content,
        structuredContent=result.get("structuredContent"),
        isError=result.get("isError", False),
    )


def _safe_default(value: Any) -> Any:
    """Return a JSON-safe schema default or a sentinel when unavailable."""
    if value is inspect._empty:
        return _NO_DEFAULT
    if isinstance(value, enum.Enum):
        return value.value
    if isinstance(value, (str, bool, int, float)) or value is None:
        return value
    if isinstance(value, tuple) and all(
        isinstance(item, (str, bool, int, float)) or item is None for item in value
    ):
        return list(value)
    if isinstance(value, list) and all(
        isinstance(item, (str, bool, int, float)) or item is None for item in value
    ):
        return value
    return _NO_DEFAULT


def _wrapper_default(value: Any) -> Any:
    """Return a lightweight default for generated wrapper signatures."""
    default = _safe_default(value)
    if default is _NO_DEFAULT:
        return None
    return default


def _annotation_label(annotation: Any) -> str:
    """Return a readable label for *annotation*."""
    if annotation is inspect._empty:
        return "Any"
    if isinstance(annotation, str):
        return annotation
    origin = get_origin(annotation)
    if origin is not None:
        return str(annotation).replace("typing.", "")
    return getattr(annotation, "__name__", repr(annotation))


def _annotation_options(annotation: Any) -> list[Any]:
    """Flatten simple union annotations into a list of options."""
    if annotation is inspect._empty:
        return [Any]

    origin = get_origin(annotation)
    if origin in (None,):
        return [annotation]

    if origin in (list, tuple, dict):
        return [annotation]

    if origin in (Callable,):
        return [annotation]

    args = [arg for arg in get_args(annotation) if arg is not type(None)]
    return args or [annotation]


def _is_enum_annotation(annotation: Any) -> type[enum.Enum] | None:
    """Return the enum class inside *annotation*, if any."""
    for option in _annotation_options(annotation):
        if inspect.isclass(option) and issubclass(option, enum.Enum):
            return option
    return None


def _annotation_includes_callable(annotation: Any) -> bool:
    """Return whether *annotation* includes a callable type."""
    label = _annotation_label(annotation)
    if "Callable" in label:
        return True
    for option in _annotation_options(annotation):
        origin = get_origin(option)
        if origin in (Callable,):
            return True
    return False


def _annotation_includes_custom_class(annotation: Any) -> bool:
    """Return whether *annotation* includes a non-builtin class."""
    for option in _annotation_options(annotation):
        if not inspect.isclass(option):
            continue
        if issubclass(option, enum.Enum):
            return True
        if option.__module__ == "builtins":
            continue
        if option in (date, datetime, time):
            continue
        return True
    return False


def _schema_and_py_type(
    annotation: Any, *, param_name: str
) -> tuple[dict[str, Any], Any]:
    """Map Python annotations to JSON Schema and wrapper annotations."""
    enum_type = _is_enum_annotation(annotation)
    if enum_type is not None:
        raw_values = [member.value for member in enum_type]
        if all(isinstance(item, str) for item in raw_values):
            schema = {"type": "string", "enum": list(raw_values)}
            return schema, str
        if all(isinstance(item, int) for item in raw_values):
            schema = {"type": "integer", "enum": list(raw_values)}
            return schema, int

    label = _annotation_label(annotation)
    lower = label.lower()

    if "bool" in lower:
        return {"type": "boolean"}, bool
    if "int" in lower and "point" not in lower:
        return {"type": "integer"}, int
    if (
        "float" in lower
        or "number" in lower
        or "scalar" in lower
        or "ndarray" in lower
        or "arraylike" in lower
    ):
        if "scalarorarray" in lower:
            return {
                "type": _JSON_ANY_TYPE,
                "description": f"Parameter `{param_name}`. {_REFERENCE_HELP}",
            }, Any
        if "ndarray" in lower or "arraylike" in lower:
            return {"type": "array", "items": {}}, list[Any]
        return {"type": "number"}, float
    if (
        "list" in lower
        or "tuple" in lower
        or "sequence" in lower
        or "iterable" in lower
    ):
        return {"type": "array", "items": {}}, list[Any]
    if "dict" in lower or "mapping" in lower:
        return {"type": "object"}, dict[str, Any]
    if "str" in lower or "date" in lower or "datetime" in lower or "time" in lower:
        return {"type": "string"}, str
    if _annotation_includes_callable(annotation) or _annotation_includes_custom_class(
        annotation
    ):
        return {
            "type": _JSON_ANY_TYPE,
            "description": f"Parameter `{param_name}`. {_REFERENCE_HELP}",
        }, Any
    return {"type": _JSON_ANY_TYPE}, Any


def _build_signature_and_schema(
    signature: inspect.Signature,
    *,
    type_hints: dict[str, Any],
) -> tuple[inspect.Signature, dict[str, Any]]:
    """Build a wrapper signature and JSON schema for *signature*."""
    wrapper_parameters: list[inspect.Parameter] = []
    properties: dict[str, Any] = {}
    required: list[str] = []

    for parameter in signature.parameters.values():
        if parameter.name in {"self", "cls"}:
            continue

        if parameter.kind == inspect.Parameter.VAR_POSITIONAL:
            properties["args"] = {
                "type": "array",
                "items": {},
                "description": "Extra positional arguments.",
            }
            wrapper_parameters.append(
                inspect.Parameter(
                    "args",
                    kind=inspect.Parameter.KEYWORD_ONLY,
                    annotation=list[Any],
                    default=None,
                )
            )
            continue

        if parameter.kind == inspect.Parameter.VAR_KEYWORD:
            properties["kwargs"] = {
                "type": "object",
                "description": "Extra keyword arguments.",
            }
            wrapper_parameters.append(
                inspect.Parameter(
                    "kwargs",
                    kind=inspect.Parameter.KEYWORD_ONLY,
                    annotation=dict[str, Any],
                    default=None,
                )
            )
            continue

        annotation = type_hints.get(parameter.name, parameter.annotation)
        schema, py_type = _schema_and_py_type(annotation, param_name=parameter.name)
        default = _safe_default(parameter.default)
        if default is not _NO_DEFAULT:
            schema["default"] = default
        else:
            required.append(parameter.name)

        properties[parameter.name] = schema
        wrapper_parameters.append(
            inspect.Parameter(
                parameter.name,
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=py_type,
                default=(
                    inspect._empty
                    if parameter.default is inspect._empty
                    else _wrapper_default(parameter.default)
                ),
            )
        )

    wrapper_signature = inspect.Signature(parameters=wrapper_parameters)
    input_schema: dict[str, Any] = {
        "type": "object",
        "properties": properties,
        "required": required,
        "additionalProperties": False,
    }
    return wrapper_signature, input_schema


def _type_hints_for_target(target: Any) -> dict[str, Any]:
    """Return best-effort type hints for *target*."""
    hinted = target.__init__ if inspect.isclass(target) else target
    try:
        return get_type_hints(hinted, include_extras=True)
    except Exception:
        return {}


def _resolve_public_callable(name: str) -> Any:
    """Resolve a public ferro-ta callable by its exposed MCP name."""
    _, targets = _discover_public_callables()
    if name in targets:
        return targets[name]
    aliases = {
        "sma": ferro_ta.SMA,
        "ema": ferro_ta.EMA,
        "rsi": ferro_ta.RSI,
        "macd": ferro_ta.MACD,
        "backtest": run_backtest,
    }
    try:
        return aliases[name]
    except KeyError as exc:
        raise KeyError(f"Unknown callable reference: {name!r}") from exc


def _coerce_enum(value: Any, enum_type: type[enum.Enum]) -> enum.Enum:
    """Convert *value* into *enum_type*."""
    if isinstance(value, enum_type):
        return value
    if isinstance(value, str):
        if value in enum_type.__members__:
            return enum_type[value]
        for member in enum_type:
            if member.value == value:
                return member
    return enum_type(value)


def _decode_value(value: Any, annotation: Any = Any) -> Any:
    """Resolve stored-object and callable references inside *value*."""
    if _is_instance_ref(value):
        return _get_instance(str(value["instance_id"]))

    if _is_callable_ref(value):
        return _resolve_public_callable(str(value["callable"]))

    enum_type = _is_enum_annotation(annotation)
    if enum_type is not None:
        try:
            return _coerce_enum(value, enum_type)
        except Exception:
            pass

    if _annotation_includes_callable(annotation) and isinstance(value, str):
        try:
            return _resolve_public_callable(value)
        except KeyError:
            pass

    if isinstance(value, list):
        return [_decode_value(item, Any) for item in value]

    if isinstance(value, tuple):
        return tuple(_decode_value(item, Any) for item in value)

    if isinstance(value, dict):
        return {str(key): _decode_value(item, Any) for key, item in value.items()}

    return value


def _invoke_target(
    target: Any,
    *,
    signature: inspect.Signature,
    type_hints: dict[str, Any],
    arguments: dict[str, Any],
) -> Any:
    """Call *target* with decoded arguments."""
    positional_args: list[Any] = []
    keyword_args: dict[str, Any] = {}
    has_varargs = any(
        parameter.kind == inspect.Parameter.VAR_POSITIONAL
        for parameter in signature.parameters.values()
    )
    before_varargs = True

    for parameter in signature.parameters.values():
        if parameter.name in {"self", "cls"}:
            continue

        annotation = type_hints.get(parameter.name, parameter.annotation)

        if parameter.kind == inspect.Parameter.VAR_POSITIONAL:
            before_varargs = False
            extra_args = arguments.get("args", []) or []
            positional_args.extend(_decode_value(item, Any) for item in extra_args)
            continue

        if parameter.kind == inspect.Parameter.VAR_KEYWORD:
            extra_kwargs = arguments.get("kwargs", {}) or {}
            if not isinstance(extra_kwargs, dict):
                raise TypeError("kwargs must be a JSON object")
            keyword_args.update(
                {
                    str(key): _decode_value(item, Any)
                    for key, item in extra_kwargs.items()
                }
            )
            continue

        expects_positional = (
            before_varargs
            and has_varargs
            and parameter.kind
            in (
                inspect.Parameter.POSITIONAL_ONLY,
                inspect.Parameter.POSITIONAL_OR_KEYWORD,
            )
        )

        if parameter.name in arguments:
            decoded = _decode_value(arguments[parameter.name], annotation)
        elif parameter.default is not inspect._empty:
            if expects_positional:
                decoded = parameter.default
            else:
                continue
        else:
            raise KeyError(f"Missing required argument: {parameter.name}")

        if expects_positional or parameter.kind == inspect.Parameter.POSITIONAL_ONLY:
            positional_args.append(decoded)
        else:
            keyword_args[parameter.name] = decoded

    return target(*positional_args, **keyword_args)


def _describe_instance_payload(identifier: str) -> dict[str, Any]:
    """Return a detailed description for a stored instance."""
    value = _get_instance(identifier)
    payload = dict(_INSTANCE_META.get(identifier, {}))
    payload.update(
        {
            "instance_id": identifier,
            "module": type(value).__module__,
            "class_name": type(value).__name__,
            "methods": _public_method_summaries(value),
        }
    )
    return payload


def _list_instances_payload() -> list[dict[str, Any]]:
    """Return current stored-object metadata."""
    return [
        _describe_instance_payload(identifier) for identifier in sorted(_INSTANCE_STORE)
    ]


def _build_public_tool_spec(item: dict[str, str], target: Any) -> _ToolSpec:
    """Create a generated tool spec for a public ferro-ta callable."""
    is_enum = inspect.isclass(target) and issubclass(target, enum.Enum)
    type_hints = _type_hints_for_target(target)

    if is_enum:
        signature = inspect.Signature(
            [
                inspect.Parameter(
                    "value",
                    kind=inspect.Parameter.KEYWORD_ONLY,
                    annotation=str,
                    default=inspect._empty,
                )
            ]
        )
        wrapper_signature, input_schema = _build_signature_and_schema(
            signature,
            type_hints={"value": str},
        )
        description = (
            item["doc"]
            or f"Construct a {item['name']} enum member. Returns a stored instance reference."
        )

        def invoke_enum(
            arguments: dict[str, Any], *, enum_type: type[enum.Enum] = target
        ) -> Any:
            if "value" not in arguments:
                raise KeyError("Missing required argument: value")
            member = _coerce_enum(arguments["value"], enum_type)
            return _instance_ref(member, source_tool=item["name"])

        return _ToolSpec(
            name=item["name"],
            description=description,
            input_schema=input_schema,
            wrapper_signature=wrapper_signature,
            invoke=invoke_enum,
        )

    signature = inspect.signature(target)
    wrapper_signature, input_schema = _build_signature_and_schema(
        signature,
        type_hints=type_hints,
    )
    is_class = inspect.isclass(target)
    description = item["doc"] or f"Call {item['name']}."
    if is_class:
        description = (
            item["doc"]
            or f"Construct a {item['name']} instance. Returns a stored instance reference."
        )

    def invoke_target(
        arguments: dict[str, Any],
        *,
        raw_target: Any = target,
        raw_signature: inspect.Signature = signature,
        raw_type_hints: dict[str, Any] = type_hints,
        returns_instance: bool = is_class,
        source_tool: str = item["name"],
    ) -> Any:
        result = _invoke_target(
            raw_target,
            signature=raw_signature,
            type_hints=raw_type_hints,
            arguments=arguments,
        )
        if returns_instance:
            return _instance_ref(result, source_tool=source_tool)
        return _normalise_json(result)

    return _ToolSpec(
        name=item["name"],
        description=description,
        input_schema=input_schema,
        wrapper_signature=wrapper_signature,
        invoke=invoke_target,
    )


def _legacy_series_tool(
    name: str,
    *,
    indicator_name: str,
    description: str,
) -> _ToolSpec:
    """Return a legacy lowercase indicator alias tool."""
    wrapper_signature = inspect.Signature(
        [
            inspect.Parameter(
                "close",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=list[float],
                default=inspect._empty,
            ),
            inspect.Parameter(
                "timeperiod",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=int,
                default=14,
            ),
        ]
    )
    input_schema = {
        "type": "object",
        "properties": {
            "close": {
                "type": "array",
                "items": {"type": "number"},
                "description": "Close price series.",
            },
            "timeperiod": {
                "type": "integer",
                "description": "Look-back period.",
                "default": 14,
            },
        },
        "required": ["close"],
        "additionalProperties": False,
    }

    def invoke(arguments: dict[str, Any]) -> Any:
        close = np.asarray(arguments["close"], dtype=np.float64)
        timeperiod = int(arguments.get("timeperiod", 14))
        return _normalise_json(
            compute_indicator(indicator_name, close, timeperiod=timeperiod)
        )

    return _ToolSpec(
        name=name,
        description=description,
        input_schema=input_schema,
        wrapper_signature=wrapper_signature,
        invoke=invoke,
    )


def _legacy_macd_tool() -> _ToolSpec:
    """Return the legacy lowercase MACD alias tool."""
    wrapper_signature = inspect.Signature(
        [
            inspect.Parameter(
                "close",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=list[float],
                default=inspect._empty,
            ),
            inspect.Parameter(
                "fastperiod",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=int,
                default=12,
            ),
            inspect.Parameter(
                "slowperiod",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=int,
                default=26,
            ),
            inspect.Parameter(
                "signalperiod",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=int,
                default=9,
            ),
        ]
    )
    input_schema = {
        "type": "object",
        "properties": {
            "close": {
                "type": "array",
                "items": {"type": "number"},
                "description": "Close price series.",
            },
            "fastperiod": {
                "type": "integer",
                "description": "Fast EMA period.",
                "default": 12,
            },
            "slowperiod": {
                "type": "integer",
                "description": "Slow EMA period.",
                "default": 26,
            },
            "signalperiod": {
                "type": "integer",
                "description": "Signal EMA period.",
                "default": 9,
            },
        },
        "required": ["close"],
        "additionalProperties": False,
    }

    def invoke(arguments: dict[str, Any]) -> Any:
        close = np.asarray(arguments["close"], dtype=np.float64)
        result = compute_indicator(
            "MACD",
            close,
            fastperiod=int(arguments.get("fastperiod", 12)),
            slowperiod=int(arguments.get("slowperiod", 26)),
            signalperiod=int(arguments.get("signalperiod", 9)),
        )
        return _normalise_json(result)

    return _ToolSpec(
        name="macd",
        description=(
            "Compute MACD (Moving Average Convergence/Divergence). "
            "Returns the line, signal, and histogram."
        ),
        input_schema=input_schema,
        wrapper_signature=wrapper_signature,
        invoke=invoke,
    )


def _legacy_backtest_tool() -> _ToolSpec:
    """Return the legacy lowercase backtest alias tool."""
    wrapper_signature = inspect.Signature(
        [
            inspect.Parameter(
                "close",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=list[float],
                default=inspect._empty,
            ),
            inspect.Parameter(
                "strategy",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=str,
                default="rsi_30_70",
            ),
            inspect.Parameter(
                "commission_per_trade",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=float,
                default=0.0,
            ),
            inspect.Parameter(
                "slippage_bps",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=float,
                default=0.0,
            ),
        ]
    )
    input_schema = {
        "type": "object",
        "properties": {
            "close": {
                "type": "array",
                "items": {"type": "number"},
                "description": "Close price series.",
            },
            "strategy": {
                "type": "string",
                "description": (
                    "Strategy name: 'rsi_30_70', 'sma_crossover', or 'macd_crossover'."
                ),
                "default": "rsi_30_70",
            },
            "commission_per_trade": {
                "type": "number",
                "description": "Fixed commission per trade.",
                "default": 0.0,
            },
            "slippage_bps": {
                "type": "number",
                "description": "Slippage in basis points.",
                "default": 0.0,
            },
        },
        "required": ["close"],
        "additionalProperties": False,
    }

    def invoke(arguments: dict[str, Any]) -> Any:
        close = np.asarray(arguments["close"], dtype=np.float64)
        result = run_backtest(
            str(arguments.get("strategy", "rsi_30_70")),
            close,
            commission_per_trade=float(arguments.get("commission_per_trade", 0.0)),
            slippage_bps=float(arguments.get("slippage_bps", 0.0)),
        )
        return _normalise_json(result)

    return _ToolSpec(
        name="backtest",
        description=(
            "Run a vectorized backtest on close prices using a named strategy. "
            "Returns final equity, trade count, and the equity curve."
        ),
        input_schema=input_schema,
        wrapper_signature=wrapper_signature,
        invoke=invoke,
    )


def _instance_management_specs() -> list[_ToolSpec]:
    """Return generic stored-object management tools."""
    list_signature = inspect.Signature([])
    simple_schema = {
        "type": "object",
        "properties": {},
        "required": [],
        "additionalProperties": False,
    }

    describe_signature = inspect.Signature(
        [
            inspect.Parameter(
                "instance_id",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=str,
                default=inspect._empty,
            )
        ]
    )
    describe_schema = {
        "type": "object",
        "properties": {
            "instance_id": {
                "type": "string",
                "description": "Stored object identifier.",
            }
        },
        "required": ["instance_id"],
        "additionalProperties": False,
    }

    call_signature = inspect.Signature(
        [
            inspect.Parameter(
                "instance_id",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=str,
                default=inspect._empty,
            ),
            inspect.Parameter(
                "method",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=str,
                default=inspect._empty,
            ),
            inspect.Parameter(
                "args",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=list[Any],
                default=None,
            ),
            inspect.Parameter(
                "kwargs",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=dict[str, Any],
                default=None,
            ),
        ]
    )
    call_schema = {
        "type": "object",
        "properties": {
            "instance_id": {
                "type": "string",
                "description": "Stored object identifier.",
            },
            "method": {
                "type": "string",
                "description": "Public method name.",
            },
            "args": {
                "type": "array",
                "items": {},
                "description": "Optional positional arguments.",
            },
            "kwargs": {
                "type": "object",
                "description": f"Optional keyword arguments. {_REFERENCE_HELP}",
            },
        },
        "required": ["instance_id", "method"],
        "additionalProperties": False,
    }

    callable_signature = inspect.Signature(
        [
            inspect.Parameter(
                "instance_id",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=str,
                default=inspect._empty,
            ),
            inspect.Parameter(
                "args",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=list[Any],
                default=None,
            ),
            inspect.Parameter(
                "kwargs",
                kind=inspect.Parameter.KEYWORD_ONLY,
                annotation=dict[str, Any],
                default=None,
            ),
        ]
    )
    callable_schema = {
        "type": "object",
        "properties": {
            "instance_id": {
                "type": "string",
                "description": "Stored callable identifier.",
            },
            "args": {
                "type": "array",
                "items": {},
                "description": "Optional positional arguments.",
            },
            "kwargs": {
                "type": "object",
                "description": f"Optional keyword arguments. {_REFERENCE_HELP}",
            },
        },
        "required": ["instance_id"],
        "additionalProperties": False,
    }

    def list_instances_tool(arguments: dict[str, Any]) -> Any:
        del arguments
        return _list_instances_payload()

    def describe_instance_tool(arguments: dict[str, Any]) -> Any:
        return _describe_instance_payload(str(arguments["instance_id"]))

    def call_instance_method_tool(arguments: dict[str, Any]) -> Any:
        value = _get_instance(str(arguments["instance_id"]))
        method_name = str(arguments["method"])
        if method_name.startswith("_"):
            raise ValueError("Only public methods can be called")
        method = getattr(value, method_name)
        if not callable(method):
            raise TypeError(
                f"{method_name!r} is not callable on {arguments['instance_id']!r}"
            )
        args = [_decode_value(item, Any) for item in (arguments.get("args") or [])]
        kwargs = {
            str(key): _decode_value(item, Any)
            for key, item in (arguments.get("kwargs") or {}).items()
        }
        return _normalise_json(method(*args, **kwargs))

    def call_stored_callable_tool(arguments: dict[str, Any]) -> Any:
        value = _get_instance(str(arguments["instance_id"]))
        if not callable(value):
            raise TypeError(f"{arguments['instance_id']!r} is not callable")
        args = [_decode_value(item, Any) for item in (arguments.get("args") or [])]
        kwargs = {
            str(key): _decode_value(item, Any)
            for key, item in (arguments.get("kwargs") or {}).items()
        }
        return _normalise_json(value(*args, **kwargs))

    def delete_instance_tool(arguments: dict[str, Any]) -> Any:
        identifier = str(arguments["instance_id"])
        payload = _describe_instance_payload(identifier)
        _INSTANCE_STORE.pop(identifier)
        _INSTANCE_META.pop(identifier, None)
        return {"deleted": True, **payload}

    return [
        _ToolSpec(
            name="list_instances",
            description="List stored MCP object references created during this session.",
            input_schema=simple_schema,
            wrapper_signature=list_signature,
            invoke=list_instances_tool,
        ),
        _ToolSpec(
            name="describe_instance",
            description="Describe a stored MCP object reference and list its public methods.",
            input_schema=describe_schema,
            wrapper_signature=describe_signature,
            invoke=describe_instance_tool,
        ),
        _ToolSpec(
            name="call_instance_method",
            description="Call a public method on a stored MCP object reference.",
            input_schema=call_schema,
            wrapper_signature=call_signature,
            invoke=call_instance_method_tool,
        ),
        _ToolSpec(
            name="call_stored_callable",
            description="Invoke a stored callable object reference.",
            input_schema=callable_schema,
            wrapper_signature=callable_signature,
            invoke=call_stored_callable_tool,
        ),
        _ToolSpec(
            name="delete_instance",
            description="Delete a stored MCP object reference.",
            input_schema=describe_schema,
            wrapper_signature=describe_signature,
            invoke=delete_instance_tool,
        ),
    ]


@lru_cache(maxsize=1)
def _tool_catalog() -> dict[str, _ToolSpec]:
    """Return the full MCP tool catalog."""
    catalog: dict[str, _ToolSpec] = {}

    legacy_specs = [
        _legacy_series_tool(
            "sma",
            indicator_name="SMA",
            description="Compute the Simple Moving Average (SMA) of a price series.",
        ),
        _legacy_series_tool(
            "ema",
            indicator_name="EMA",
            description="Compute the Exponential Moving Average (EMA) of a price series.",
        ),
        _legacy_series_tool(
            "rsi",
            indicator_name="RSI",
            description="Compute the Relative Strength Index (RSI) of a price series.",
        ),
        _legacy_macd_tool(),
        _legacy_backtest_tool(),
    ]
    for spec in legacy_specs:
        catalog[spec.name] = spec

    public_entries, targets = _discover_public_callables()
    for item in public_entries:
        spec = _build_public_tool_spec(item, targets[item["name"]])
        catalog[spec.name] = spec

    for spec in _instance_management_specs():
        catalog[spec.name] = spec

    return catalog


def _make_fastmcp_wrapper(spec: _ToolSpec) -> Callable[..., Any]:
    """Create a FastMCP-friendly wrapper for *spec*."""

    def wrapper(**kwargs: Any) -> Any:
        return _to_call_tool_result(handle_call_tool(spec.name, dict(kwargs)))

    wrapper.__name__ = f"tool_{_slugify(spec.name).replace('-', '_')}"
    wrapper.__doc__ = spec.description
    setattr(wrapper, "__signature__", spec.wrapper_signature)
    wrapper.__annotations__ = {
        parameter.name: (
            Any if parameter.annotation is inspect._empty else parameter.annotation
        )
        for parameter in spec.wrapper_signature.parameters.values()
    }
    wrapper.__annotations__["return"] = Any
    return wrapper


def handle_list_tools() -> dict[str, Any]:
    """Return the MCP ListTools response."""
    return {
        "tools": [
            {
                "name": spec.name,
                "description": spec.description,
                "inputSchema": spec.input_schema,
            }
            for spec in _tool_catalog().values()
        ]
    }


def handle_call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
    """Dispatch an MCP CallTool request and return helper-style content."""
    try:
        spec = _tool_catalog().get(name)
        if spec is None:
            return _text_result(
                f"Unknown tool: {name!r}",
                structured={"error": f"Unknown tool: {name!r}"},
                is_error=True,
            )

        payload = spec.invoke(arguments)
        return _response_from_payload(payload)

    except Exception as exc:
        return _text_result(
            f"Error: {exc}",
            structured={"error": str(exc)},
            is_error=True,
        )


@lru_cache(maxsize=1)
def create_server() -> Any:
    """Create the FastMCP server lazily so MCP stays optional."""
    fast_mcp_type, _, _ = _load_mcp_sdk()
    app = fast_mcp_type(
        "ferro-ta",
        instructions=(
            "Expose ferro-ta's public API over MCP. "
            "Use exact public ferro-ta names such as SMA, RSI, compute_indicator, "
            "TickAggregator, or AlertManager, or the legacy aliases sma, ema, rsi, "
            "macd, and backtest. Use list_instances, describe_instance, "
            "call_instance_method, call_stored_callable, and delete_instance for "
            "stateful objects and stored callables."
        ),
    )

    for spec in _tool_catalog().values():
        app.add_tool(
            _make_fastmcp_wrapper(spec), name=spec.name, description=spec.description
        )

    return app


def run_server() -> None:  # pragma: no cover
    """Run the stdio MCP server."""
    try:
        create_server().run(transport="stdio")
    except RuntimeError as exc:
        raise SystemExit(str(exc)) from exc
