from __future__ import annotations

import copy
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import config_manager
from strategy_sdk.types import StrategyManifest


@dataclass(frozen=True)
class BuiltinStrategySpec:
    strategy_id: str
    name: str
    entrypoint: str
    kind: str = "signal"
    description: str = ""
    aliases: tuple[str, ...] = ()
    capabilities: tuple[str, ...] = ()
    version: str = "legacy-2.1.0"
    base_strategy_id: str = ""
    default_mode: str = "builtin"
    trust_level: str = "builtin"
    params: dict[str, Any] = field(default_factory=dict)


class StrategyRegistry:
    def __init__(self):
        self._specs: dict[str, BuiltinStrategySpec] = {}
        self._aliases: dict[str, str] = {}
        self._register_builtin_defaults()

    def _register_builtin_defaults(self) -> None:
        self.register_builtin(
            BuiltinStrategySpec(
                strategy_id="ema_macd",
                name="EMA MACD",
                entrypoint="signal_generators.ema_macd_signal",
                description="EMA + MACD built-in signal generator",
                aliases=("ema", "default"),
                capabilities=("intraday", "long_short", "trend_following"),
            )
        )
        self.register_builtin(
            BuiltinStrategySpec(
                strategy_id="wyckoff",
                name="Wyckoff",
                entrypoint="signal_generators.wyckoff_signal",
                description="Wyckoff volume structure strategy",
                aliases=("wyck",),
                capabilities=("range", "volume"),
            )
        )
        self.register_builtin(
            BuiltinStrategySpec(
                strategy_id="composite",
                name="Composite",
                entrypoint="signal_generators.composite_signal",
                kind="composer",
                description="Built-in weighted composite strategy",
                aliases=("multi", "hybrid"),
                capabilities=("composition", "voting"),
            )
        )

    def register_builtin(self, spec: BuiltinStrategySpec) -> None:
        self._specs[spec.strategy_id] = spec
        self._aliases[spec.strategy_id] = spec.strategy_id
        for alias in spec.aliases:
            self._aliases[alias] = spec.strategy_id

    def _strategy_market_state_file(self) -> Path:
        return config_manager.get_config_dir() / "strategy_market_state.json"

    def _capabilities_from_template(
        self,
        template: dict[str, Any],
        base_spec: BuiltinStrategySpec,
    ) -> tuple[str, ...]:
        style = str(template.get("strategy_style", "") or "").strip().lower()
        applicable_markets = {
            str(item).strip().upper()
            for item in template.get("applicable_markets", [])
            if str(item).strip()
        } if isinstance(template.get("applicable_markets", []), list) else set()
        if style == "mean_reversion" or "RANGING" in applicable_markets:
            return ("intraday", "long_short", "mean_reversion")
        if style == "breakout":
            return ("intraday", "long_short", "breakout")
        if style == "momentum":
            return ("intraday", "long_short", "momentum")
        if style == "trend":
            return ("intraday", "long_short", "trend_following")
        return base_spec.capabilities

    def _load_market_state_assets(self) -> dict[str, BuiltinStrategySpec]:
        strategies: dict[str, Any] | Any = {}
        try:
            from src.unified_strategy_state import load_unified_state

            payload = load_unified_state(use_cache=False)
            strategies = payload.get("strategies", {}) if isinstance(payload, dict) else {}
        except Exception:
            strategies = {}
        if not isinstance(strategies, dict) or not strategies:
            state_file = self._strategy_market_state_file()
            if not state_file.exists():
                return {}
            try:
                payload = json.loads(state_file.read_text(encoding="utf-8"))
            except Exception:
                return {}
            strategies = payload.get("strategies", {}) if isinstance(payload, dict) else {}
        if not isinstance(strategies, dict):
            return {}
        assets: dict[str, BuiltinStrategySpec] = {}
        for raw_strategy_id, row in strategies.items():
            if not isinstance(row, dict):
                continue
            strategy_id = str(raw_strategy_id or "").strip()
            if not strategy_id or strategy_id in self._specs:
                continue
            artifact_type = str(row.get("artifact_type", "") or "").strip().lower()
            base_strategy_id = str(
                row.get("base_engine")
                or ("composite" if bool(row.get("is_composite", False)) else "ema_macd")
                or "ema_macd"
            ).strip()
            base_strategy_id = self._aliases.get(base_strategy_id, base_strategy_id)
            base_spec = self._specs.get(base_strategy_id, self._specs["ema_macd"])
            template = (
                copy.deepcopy(row.get("template", {}))
                if isinstance(row.get("template", {}), dict)
                else {}
            )
            optimized = row.get("optimized", {}) if isinstance(row.get("optimized", {}), dict) else {}
            optimized_params = (
                optimized.get("params", {})
                if isinstance(optimized.get("params", {}), dict)
                else {}
            )
            params = copy.deepcopy(template)
            params.update(copy.deepcopy(optimized_params))
            if bool(row.get("is_composite", False)):
                components = [
                    str(item).strip()
                    for item in row.get("components", [])
                    if str(item).strip()
                ] if isinstance(row.get("components", []), list) else []
                template_components = (
                    template.get("components", [])
                    if isinstance(template.get("components", []), list)
                    else []
                )
                if not components:
                    components = [
                        str(item).strip()
                        for item in template_components
                        if str(item).strip()
                    ]
                weights = (
                    copy.deepcopy(template.get("weights", {}))
                    if isinstance(template.get("weights", {}), dict)
                    else {}
                )
                if components:
                    params["composite_strategies"] = components
                if weights:
                    params["composite_weights"] = weights
            description = str(
                row.get("source_document")
                or row.get("description")
                or row.get("display_name")
                or row.get("name")
                or strategy_id
            )
            entrypoint = str(row.get("entrypoint", "") or "").strip()
            if not entrypoint:
                if artifact_type == "plugin":
                    entrypoint = f"strategies.{strategy_id}"
                else:
                    entrypoint = _resolve_entrypoint_for_asset(strategy_id, base_spec)
            assets[strategy_id] = BuiltinStrategySpec(
                strategy_id=strategy_id,
                name=str(row.get("display_name") or row.get("name") or strategy_id),
                entrypoint=entrypoint,
                kind=(
                    "signal"
                    if artifact_type == "plugin"
                    else "composer" if bool(row.get("is_composite", False)) else base_spec.kind
                ),
                description=description,
                capabilities=self._capabilities_from_template(template, base_spec),
                version=str(row.get("current_version") or "1.0.0"),
                base_strategy_id=base_strategy_id,
                default_mode=(
                    str(row.get("default_mode", "plugin"))
                    if artifact_type == "plugin"
                    else "asset"
                ),
                trust_level=str(
                    row.get("trust_level", "sandbox" if artifact_type == "plugin" else "workspace_asset")
                ),
                params=params,
            )
        return assets

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

    def _load_dynamic_manifests(self) -> dict[str, BuiltinStrategySpec]:
        """Scan strategies/*.manifest.json for AI-generated / plugin strategies."""
        sdir = self._strategies_dir()
        if not sdir.is_dir():
            return {}
        assets: dict[str, BuiltinStrategySpec] = {}
        for mf in sorted(sdir.glob("*.manifest.json")):
            try:
                data = json.loads(mf.read_text(encoding="utf-8"))
            except Exception:
                continue
            sid = str(data.get("strategy_id", "") or mf.stem.split(".manifest")[0]).strip()
            if not sid or sid in self._specs or sid in self._aliases:
                continue
            entrypoint = str(data.get("entrypoint", f"strategies.{sid}") or f"strategies.{sid}").strip()
            assets[sid] = BuiltinStrategySpec(
                strategy_id=sid,
                name=str(data.get("name", sid)),
                entrypoint=entrypoint,
                kind=str(data.get("kind", "signal")),
                description=str(data.get("description", "")),
                capabilities=tuple(data.get("capabilities", []) if isinstance(data.get("capabilities", []), list) else []),
                version=str(data.get("version", "1.0.0")),
                default_mode=str(data.get("default_mode", "plugin")),
                trust_level=str(data.get("trust_level", "sandbox")),
                params=data.get("params_schema", {}) if isinstance(data.get("params_schema", {}), dict) else {},
            )
        return assets

    def _all_specs(self) -> dict[str, BuiltinStrategySpec]:
        """Unified view: builtin + market_state assets + dynamic manifests."""
        merged: dict[str, BuiltinStrategySpec] = dict(self._specs)
        merged.update(self._load_market_state_assets())
        merged.update(self._load_dynamic_manifests())
        return merged

    def resolve(self, strategy_id: str | None) -> str:
        key = str(strategy_id or "ema_macd").strip()
        if key in self._aliases:
            return self._aliases[key]
        all_specs = self._all_specs()
        return key if key in all_specs else key

    def get_spec(self, strategy_id: str | None) -> BuiltinStrategySpec:
        resolved = self.resolve(strategy_id)
        all_specs = self._all_specs()
        if resolved not in all_specs:
            raise KeyError(f"Unknown strategy_id: {strategy_id}")
        return all_specs[resolved]

    def list_manifests(self) -> list[StrategyManifest]:
        manifests: list[StrategyManifest] = []
        for spec in self._all_specs().values():
            manifests.append(
                StrategyManifest(
                    strategy_id=spec.strategy_id,
                    name=spec.name,
                    version=spec.version,
                    kind=spec.kind,
                    entrypoint=spec.entrypoint,
                    description=spec.description,
                    capabilities=list(spec.capabilities),
                    supported_symbols=["XAUUSD"],
                    supported_timeframes=["M1", "M5", "M15", "H1"],
                    default_mode=spec.default_mode,
                    trust_level=spec.trust_level,
                )
            )
        return manifests

    def instantiate_legacy(self, strategy_id: str | None, params: dict[str, Any] | None = None):
        spec = self.get_spec(strategy_id)
        merged_params = copy.deepcopy(spec.params) if isinstance(spec.params, dict) else {}
        if isinstance(params, dict):
            merged_params.update(copy.deepcopy(params))

        # Plugin type (AI-generated / strategies/ directory): direct import
        if spec.kind == "signal" and spec.entrypoint and str(spec.entrypoint).startswith("strategies."):
            import importlib
            mod = importlib.import_module(spec.entrypoint)
            # Prefer create_plugin() factory; fall back to StrategyPlugin subclass
            factory = getattr(mod, "create_plugin", None)
            if callable(factory):
                return factory(merged_params or None)
            for attr_name in dir(mod):
                obj = getattr(mod, attr_name)
                if (
                    isinstance(obj, type)
                    and hasattr(obj, "__bases__")
                    and any(b.__name__ == "StrategyPlugin" for b in obj.__mro__)
                ):
                    return obj(merged_params or None)
            raise ImportError(f"No StrategyPlugin subclass or create_plugin() found in {spec.entrypoint}")

        # Legacy type: through signal_generators.create_generator()
        from signal_generators import create_generator
        from strategy_sdk.legacy_adapter import LegacySignalStrategyAdapter
        # If strategy_id has its own signal generator, use it directly
        # instead of falling back to the base engine (e.g., rsi_extreme → rsi_extreme, not ema_macd)
        create_strategy_id = spec.strategy_id if spec.strategy_id in _SIGNAL_GENERATOR_ENTRYPOINTS else (spec.base_strategy_id or spec.strategy_id)
        legacy = create_generator(create_strategy_id, merged_params or None)
        return LegacySignalStrategyAdapter(spec, legacy)


_DEFAULT_REGISTRY: StrategyRegistry | None = None


# ── Signal generator entrypoint map (module level) ──────────────────

_SIGNAL_GENERATOR_ENTRYPOINTS = {
    "ema_macd":      "signal_generators.ema_macd_signal",
    "wyckoff":       "signal_generators.wyckoff_signal",
    "composite":     "signal_generators.composite_signal",
    "rsi_extreme":   "signal_generators.rsi_extreme_signal",
    "boll_band":     "signal_generators.boll_band_signal",
    "adx_breakout":  "signal_generators.adx_breakout_signal",
    "pullback_fade": "signal_generators.pullback_fade_signal",
    "grid_osc":      "signal_generators.grid_osc_signal",
    "key_level":     "signal_generators.key_level_signal",
}


def _resolve_entrypoint_for_asset(strategy_id: str, base_spec) -> str:
    """If the strategy_id matches a known signal generator, use its entrypoint."""
    import importlib
    ep = _SIGNAL_GENERATOR_ENTRYPOINTS.get(strategy_id, "")
    if ep:
        try:
            mod_name, _ = ep.rsplit(".", 1)
            importlib.import_module(mod_name)
            return ep
        except ImportError:
            pass
    return base_spec.entrypoint


def get_strategy_registry() -> StrategyRegistry:
    global _DEFAULT_REGISTRY
    if _DEFAULT_REGISTRY is None:
        _DEFAULT_REGISTRY = StrategyRegistry()
    return _DEFAULT_REGISTRY
