"""Strategy registry -- browse, pull, and run your Horizon Cloud strategies locally.

Usage:
    import horizon as hz

    # Browse your deployed strategies
    hz.registry.list()
    hz.registry.search("market making")

    # Get details + version history
    hz.registry.info("strategy-id")
    hz.registry.versions("strategy-id")

    # Download to ~/.horizon/strategies/
    hz.registry.pull("strategy-id")
    hz.registry.pull("strategy-id", version=3)

    # Run locally (pulls + executes)
    hz.registry.run("strategy-id", exchange=hz.Polymarket())
"""

from __future__ import annotations

import importlib.util
import json
import logging
import os
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

logger = logging.getLogger("horizon.registry")

_STRATEGIES_DIR = Path.home() / ".horizon" / "strategies"


@dataclass
class StrategyInfo:
    """Summary of a cloud strategy."""

    id: str
    name: str
    description: str = ""
    status: str = ""
    class_name: str = ""
    created_at: str = ""
    updated_at: str = ""

    def __repr__(self) -> str:
        tag = f" [{self.status}]" if self.status else ""
        return f"Strategy({self.name!r}{tag}, id={self.id[:8]}...)"


@dataclass
class VersionInfo:
    """A single strategy version."""

    id: str
    version_number: int
    description: str = ""
    created_at: str = ""

    def __repr__(self) -> str:
        return f"v{self.version_number} ({self.created_at[:10]})"


@dataclass
class PulledStrategy:
    """A strategy that has been downloaded locally."""

    id: str
    name: str
    version: int | None
    path: Path
    code_path: Path
    manifest_path: Path

    def __repr__(self) -> str:
        ver = f" v{self.version}" if self.version else ""
        return f"PulledStrategy({self.name!r}{ver}, path={self.path})"


class Registry:
    """Browse, pull, and run strategies from Horizon Cloud.

    Uses your ``hz_sdk_`` key for authentication (from ``HORIZON_API_KEY``
    env var or passed explicitly).
    """

    def __init__(self, api_key: str | None = None, base_url: str | None = None):
        self._api_key = api_key
        self._base_url = base_url
        self._cloud = None

    def _get_cloud(self):
        """Lazy-init the HorizonCloud client."""
        if self._cloud is None:
            from horizon.cloud import HorizonCloud
            self._cloud = HorizonCloud(api_key=self._api_key, base_url=self._base_url)
        return self._cloud

    # ── browse ────────────────────────────────────────────────────────

    def list(self) -> list[StrategyInfo]:
        """List all your strategies on Horizon Cloud.

        Returns:
            List of StrategyInfo objects, most recent first.
        """
        cloud = self._get_cloud()
        strategies = cloud.list_strategies()
        return [_to_strategy_info(s) for s in strategies]

    def search(self, query: str) -> list[StrategyInfo]:
        """Search your strategies by name or description.

        Args:
            query: Search term (case-insensitive, matches name or description).

        Returns:
            Matching strategies, ranked by relevance.
        """
        all_strats = self.list()
        q = query.lower()
        scored: list[tuple[int, StrategyInfo]] = []
        for s in all_strats:
            score = 0
            if q in s.name.lower():
                score += 10
            if q in s.description.lower():
                score += 5
            if q in (s.class_name or "").lower():
                score += 3
            if score > 0:
                scored.append((score, s))
        scored.sort(key=lambda x: x[0], reverse=True)
        return [s for _, s in scored]

    def info(self, strategy_id: str) -> dict:
        """Get detailed info about a strategy.

        Returns the full strategy record including config, params, risk_config,
        and estimated metrics.
        """
        cloud = self._get_cloud()
        return cloud.get_strategy(strategy_id)

    def versions(self, strategy_id: str) -> list[VersionInfo]:
        """List all versions of a strategy (newest first).

        Returns:
            List of VersionInfo objects.
        """
        cloud = self._get_cloud()
        resp = cloud._get(f"/strategies/{strategy_id}/versions")
        # The versions endpoint may return data directly or wrapped
        data = resp.get("data", resp) if isinstance(resp, dict) else resp
        if isinstance(data, list):
            return [
                VersionInfo(
                    id=v.get("id", ""),
                    version_number=v.get("version_number", 0),
                    description=v.get("description", ""),
                    created_at=v.get("created_at", ""),
                )
                for v in data
            ]
        return []

    # ── pull ──────────────────────────────────────────────────────────

    def pull(
        self,
        strategy_id: str,
        *,
        version: int | None = None,
        directory: str | Path | None = None,
    ) -> PulledStrategy:
        """Download a strategy from Horizon Cloud to local disk.

        The strategy is saved to ``~/.horizon/strategies/<name>-<short_id>/``
        with the code file and a manifest containing metadata.

        Args:
            strategy_id: UUID of the strategy.
            version: Specific version number to pull (default: latest/current).
            directory: Override download directory (default: ~/.horizon/strategies/).

        Returns:
            PulledStrategy with paths to the downloaded files.
        """
        cloud = self._get_cloud()
        strat = cloud.get_strategy(strategy_id)
        if not strat:
            from horizon.cloud import HorizonCloudError
            raise HorizonCloudError(f"Strategy {strategy_id} not found")

        name = strat.get("name", "strategy")
        code = strat.get("generated_code") or strat.get("code", "")
        config = strat.get("config_yaml", "")
        params = strat.get("params", {})
        risk_config = strat.get("risk_config")

        # If specific version requested, fetch that version's code
        pulled_version = version
        if version is not None:
            ver_list = self.versions(strategy_id)
            target = next((v for v in ver_list if v.version_number == version), None)
            if target is None:
                from horizon.cloud import HorizonCloudError
                raise HorizonCloudError(
                    f"Version {version} not found. Available: "
                    + ", ".join(str(v.version_number) for v in ver_list)
                )
            # Fetch full version data
            ver_data = cloud._get(f"/strategies/{strategy_id}/versions/{target.id}")
            ver_detail = ver_data.get("data", ver_data) if isinstance(ver_data, dict) else ver_data
            if isinstance(ver_detail, dict):
                code = ver_detail.get("generated_code", code)
                config = ver_detail.get("config_yaml", config)
                params = ver_detail.get("params", params)
                risk_config = ver_detail.get("risk_config", risk_config)

        # Create local directory
        safe_name = re.sub(r"[^\w\-]", "_", name.lower().strip())
        short_id = strategy_id[:8]
        dir_name = f"{safe_name}-{short_id}"
        base_dir = Path(directory) if directory else _STRATEGIES_DIR
        strat_dir = base_dir / dir_name
        strat_dir.mkdir(parents=True, exist_ok=True)

        # Write strategy code
        code_path = strat_dir / "strategy.py"
        code_path.write_text(code, encoding="utf-8")

        # Write manifest
        manifest = {
            "id": strategy_id,
            "name": name,
            "description": strat.get("description", ""),
            "class_name": strat.get("class_name", ""),
            "status": strat.get("status", ""),
            "version": pulled_version,
            "config_yaml": config,
            "params": params,
            "risk_config": risk_config,
            "pulled_at": _now_iso(),
        }
        manifest_path = strat_dir / "manifest.json"
        manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")

        ver_str = f" v{pulled_version}" if pulled_version else ""
        logger.info("Pulled %s%s to %s", name, ver_str, strat_dir)

        return PulledStrategy(
            id=strategy_id,
            name=name,
            version=pulled_version,
            path=strat_dir,
            code_path=code_path,
            manifest_path=manifest_path,
        )

    # ── run ───────────────────────────────────────────────────────────

    def run(
        self,
        strategy_id: str,
        *,
        version: int | None = None,
        exchange: Any = None,
        mode: str = "paper",
        **kwargs: Any,
    ) -> None:
        """Pull a strategy from the cloud and run it locally.

        Downloads the strategy code, dynamically loads it, and calls ``hz.run()``
        with the strategy's pipeline.

        Args:
            strategy_id: UUID of the strategy to run.
            version: Specific version number (default: latest).
            exchange: Exchange config (e.g., ``hz.Polymarket()``, ``hz.Coinbase()``).
            mode: ``"paper"`` or ``"live"``.
            **kwargs: Additional arguments passed to ``hz.run()``.
        """
        pulled = self.pull(strategy_id, version=version)

        # Load the manifest for params/risk config
        manifest = json.loads(pulled.manifest_path.read_text(encoding="utf-8"))

        # Dynamically import the strategy module
        spec = importlib.util.spec_from_file_location(
            f"horizon_strategy_{pulled.id[:8]}",
            pulled.code_path,
        )
        if spec is None or spec.loader is None:
            from horizon.cloud import HorizonCloudError
            raise HorizonCloudError(f"Failed to load strategy from {pulled.code_path}")

        module = importlib.util.module_from_spec(spec)
        sys.modules[spec.name] = module
        spec.loader.exec_module(module)

        logger.info(
            "Loaded strategy %s from %s, executing...",
            pulled.name,
            pulled.code_path,
        )

        # The strategy module should call hz.run() when executed.
        # If it defines a main() or run() function, call that instead.
        if hasattr(module, "main"):
            module.main()
        elif hasattr(module, "run") and callable(module.run):
            module.run()
        # Otherwise the module executed hz.run() on import (common pattern)

    # ── local strategies ──────────────────────────────────────────────

    def local(self) -> list[PulledStrategy]:
        """List strategies that have been pulled locally.

        Returns:
            List of PulledStrategy objects for each local strategy directory.
        """
        if not _STRATEGIES_DIR.exists():
            return []
        results = []
        for d in sorted(_STRATEGIES_DIR.iterdir()):
            if not d.is_dir():
                continue
            manifest_path = d / "manifest.json"
            code_path = d / "strategy.py"
            if not code_path.exists():
                continue
            manifest = {}
            if manifest_path.exists():
                try:
                    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
                except Exception:
                    pass
            results.append(PulledStrategy(
                id=manifest.get("id", d.name),
                name=manifest.get("name", d.name),
                version=manifest.get("version"),
                path=d,
                code_path=code_path,
                manifest_path=manifest_path,
            ))
        return results

    def remove(self, strategy_id: str) -> bool:
        """Remove a locally pulled strategy.

        Args:
            strategy_id: UUID or directory name of the strategy.

        Returns:
            True if removed, False if not found.
        """
        import shutil
        for pulled in self.local():
            if pulled.id == strategy_id or pulled.path.name == strategy_id:
                shutil.rmtree(pulled.path)
                logger.info("Removed local strategy: %s", pulled.path)
                return True
        return False

    def __repr__(self) -> str:
        local_count = len(self.local()) if _STRATEGIES_DIR.exists() else 0
        return f"Registry(local={local_count})"


# ── module-level singleton ────────────────────────────────────────────

registry = Registry()
"""Module-level registry instance. Use ``hz.registry.list()``, etc."""


# ── helpers ───────────────────────────────────────────────────────────

def _to_strategy_info(raw: dict) -> StrategyInfo:
    return StrategyInfo(
        id=raw.get("id", ""),
        name=raw.get("name", ""),
        description=raw.get("description", ""),
        status=raw.get("status", ""),
        class_name=raw.get("class_name", ""),
        created_at=raw.get("created_at", ""),
        updated_at=raw.get("updated_at", ""),
    )


def _now_iso() -> str:
    from datetime import datetime, timezone
    return datetime.now(timezone.utc).isoformat()
