"""Dynamic strategy discovery via manifest files.

Place a `*.manifest.json` file in the `strategies/` directory to register a
new strategy plugin without touching the registry source code.
"""

from __future__ import annotations

import importlib
import json
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any

import config_manager


@dataclass(frozen=True)
class DynamicStrategySpec:
    strategy_id: str
    name: str
    entrypoint: str
    kind: str = "signal"
    description: str = ""
    author: str = ""
    version: str = "1.0.0"
    tags: tuple[str, ...] = ()
    capabilities: tuple[str, ...] = ()
    supported_symbols: tuple[str, ...] = ("XAUUSD",)
    supported_timeframes: tuple[str, ...] = ("M5", "M15", "H1")
    params_schema: dict[str, Any] = None  # type: ignore[assignment]
    default_mode: str = "plugin"
    trust_level: str = "sandbox"
    manifest_path: str = ""

    def __post_init__(self):
        if self.params_schema is None:
            object.__setattr__(self, "params_schema", {})


class DynamicStrategyRegistry:
    """Scans `strategies/` for *.manifest.json files and loads plugins.

    Each manifest file describes a strategy (its ID, entrypoint module,
    parameters, capabilities).  The registry validates that the entrypoint
    module is importable and contains a `create_plugin()` factory or a
    class implementing `StrategyPlugin`.

    Usage::

        registry = DynamicStrategyRegistry()
        registry.rescan()                 # pick up new manifests
        plugin = registry.instantiate("my_custom")
    """

    def __init__(self):
        self._specs: dict[str, DynamicStrategySpec] = {}
        self._cache: dict[str, Any] = {}
        self._rescan()

    # -- paths ----------------------------------------------------------

    def _strategies_dir(self) -> Path:
        return config_manager.get_project_dir() / "strategies"

    # -- scanning -------------------------------------------------------

    def _rescan(self) -> None:
        strategies_dir = self._strategies_dir()
        if not strategies_dir.is_dir():
            return
        for entry in sorted(strategies_dir.iterdir()):
            if not entry.name.endswith(".manifest.json"):
                continue
            try:
                spec = self._parse_manifest(entry)
            except Exception:
                continue
            self._specs[spec.strategy_id] = spec

    def rescan(self) -> int:
        """Re-scan the strategies directory.  Returns count of loaded specs."""
        before = len(self._specs)
        self._rescan()
        self._cache.clear()
        return len(self._specs) - before

    def _parse_manifest(self, path: Path) -> DynamicStrategySpec:
        raw = json.loads(path.read_text(encoding="utf-8"))
        return DynamicStrategySpec(
            strategy_id=str(raw["strategy_id"]).strip(),
            name=str(raw.get("name", raw["strategy_id"])).strip(),
            entrypoint=str(raw["entrypoint"]).strip(),
            kind=str(raw.get("kind", "signal")).strip(),
            description=str(raw.get("description", "")),
            author=str(raw.get("author", "")),
            version=str(raw.get("version", "1.0.0")),
            tags=tuple(str(t).strip() for t in raw.get("tags", [])),
            capabilities=tuple(
                str(c).strip() for c in raw.get("capabilities", [])
            ),
            supported_symbols=tuple(
                str(s).strip() for s in raw.get("supported_symbols", ["XAUUSD"])
            ),
            supported_timeframes=tuple(
                str(t).strip() for t in raw.get("supported_timeframes", ["M5", "M15", "H1"])
            ),
            params_schema=raw.get("params_schema", {}),
            default_mode=str(raw.get("default_mode", "plugin")),
            trust_level=str(raw.get("trust_level", "sandbox")),
            manifest_path=str(path),
        )

    # -- query ----------------------------------------------------------

    def get_spec(self, strategy_id: str) -> DynamicStrategySpec:
        if strategy_id not in self._specs:
            raise KeyError(f"Unknown dynamic strategy: {strategy_id}")
        return self._specs[strategy_id]

    def list_ids(self) -> list[str]:
        return sorted(self._specs.keys())

    def list_manifests(self) -> list[dict[str, Any]]:
        result: list[dict[str, Any]] = []
        for spec in self._specs.values():
            result.append(
                {
                    "strategy_id": spec.strategy_id,
                    "name": spec.name,
                    "version": spec.version,
                    "kind": spec.kind,
                    "entrypoint": spec.entrypoint,
                    "description": spec.description,
                    "author": spec.author,
                    "tags": list(spec.tags),
                    "capabilities": list(spec.capabilities),
                    "supported_symbols": list(spec.supported_symbols),
                    "supported_timeframes": list(spec.supported_timeframes),
                    "default_mode": spec.default_mode,
                    "trust_level": spec.trust_level,
                }
            )
        return result

    # -- instantiation --------------------------------------------------

    def instantiate(
        self,
        strategy_id: str,
        params: dict[str, Any] | None = None,
    ):
        """Import and instantiate a strategy plugin from its manifest.

        The entrypoint module must expose either:
          - ``create_plugin(params) -> StrategyPlugin`` factory function, or
          - a class subclassing ``StrategyPlugin`` with the same name as the
            module (PascalCase).

        Returns a ``StrategyPlugin`` instance.
        """
        from strategy_sdk.interfaces import StrategyPlugin

        spec = self.get_spec(strategy_id)
        module = importlib.import_module(spec.entrypoint)

        factory = getattr(module, "create_plugin", None)
        if callable(factory):
            plugin = factory(params or {})
            if not isinstance(plugin, StrategyPlugin):
                raise TypeError(
                    f"create_plugin() must return a StrategyPlugin, got {type(plugin)}"
                )
            self._cache[strategy_id] = plugin
            return plugin

        module_name = spec.entrypoint.rsplit(".", 1)[-1]
        class_name = _to_pascal_case(module_name)
        cls = getattr(module, class_name, None)
        if cls is None:
            for attr_name in dir(module):
                obj = getattr(module, attr_name)
                if (
                    isinstance(obj, type)
                    and issubclass(obj, StrategyPlugin)
                    and obj is not StrategyPlugin
                ):
                    cls = obj
                    break

        if cls is None:
            raise ImportError(
                f"No create_plugin() or StrategyPlugin subclass found in "
                f"{spec.entrypoint}"
            )

        plugin = cls(params or {})
        if not isinstance(plugin, StrategyPlugin):
            raise TypeError(f"{cls.__name__} must be a StrategyPlugin subclass")
        self._cache[strategy_id] = plugin
        return plugin

    def invalidate(self, strategy_id: str) -> None:
        self._cache.pop(strategy_id, None)


def _to_pascal_case(snake: str) -> str:
    return "".join(part.capitalize() for part in snake.split("_"))


_DEFAULT_DYNAMIC_REGISTRY: DynamicStrategyRegistry | None = None


def get_dynamic_strategy_registry() -> DynamicStrategyRegistry:
    global _DEFAULT_DYNAMIC_REGISTRY
    if _DEFAULT_DYNAMIC_REGISTRY is None:
        _DEFAULT_DYNAMIC_REGISTRY = DynamicStrategyRegistry()
    return _DEFAULT_DYNAMIC_REGISTRY
