"""
Strategy registry for managing backtest strategy metadata and catalog persistence.

Follows the same governance pattern as SignalRegistry for consistency.
"""

import json
import logging
from dataclasses import dataclass, asdict
from pathlib import Path

from .config import BacktestConfig

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class StrategyMetadata:
    """
    Metadata for a registered backtest strategy.

    Attributes
    ----------
    name : str
        Unique strategy identifier (e.g., "conservative", "balanced").
    description : str
        Human-readable description of strategy characteristics.
    position_size_mm : float
        Baseline notional position size in millions.
    sizing_mode : str
        Position sizing mode: 'binary' (full position for any non-zero signal)
        or 'proportional' (scaled by signal magnitude).
    stop_loss_pct : float | None
        Stop loss as percentage of initial position value. None to disable.
    take_profit_pct : float | None
        Take profit as percentage of initial position value. None to disable.
    max_holding_days : int | None
        Maximum days to hold a position before forced exit. None for no limit.
    transaction_cost_bps : float
        Round-trip transaction cost in basis points.
    dv01_per_million : float
        DV01 per $1MM notional for risk calculations.
    enabled : bool
        Whether strategy should be included in evaluation.
    """

    name: str
    description: str
    position_size_mm: float
    sizing_mode: str
    stop_loss_pct: float | None
    take_profit_pct: float | None
    max_holding_days: int | None
    transaction_cost_bps: float
    dv01_per_million: float
    enabled: bool = True

    def __post_init__(self) -> None:
        """Validate strategy metadata."""
        if not self.name:
            raise ValueError("Strategy name cannot be empty")
        if self.position_size_mm <= 0:
            raise ValueError(
                f"Strategy '{self.name}': position_size_mm must be positive, "
                f"got {self.position_size_mm}"
            )
        if self.sizing_mode not in {"binary", "proportional"}:
            raise ValueError(
                f"Strategy '{self.name}': sizing_mode must be 'binary' or 'proportional', "
                f"got '{self.sizing_mode}'"
            )
        if self.stop_loss_pct is not None and not (0 < self.stop_loss_pct <= 100):
            raise ValueError(
                f"Strategy '{self.name}': stop_loss_pct must be in (0, 100], "
                f"got {self.stop_loss_pct}"
            )
        if self.take_profit_pct is not None and not (0 < self.take_profit_pct <= 100):
            raise ValueError(
                f"Strategy '{self.name}': take_profit_pct must be in (0, 100], "
                f"got {self.take_profit_pct}"
            )
        if self.max_holding_days is not None and self.max_holding_days <= 0:
            raise ValueError(
                f"Strategy '{self.name}': max_holding_days must be positive, "
                f"got {self.max_holding_days}"
            )
        if self.transaction_cost_bps < 0:
            raise ValueError(
                f"Strategy '{self.name}': transaction_cost_bps must be non-negative, "
                f"got {self.transaction_cost_bps}"
            )
        if self.dv01_per_million <= 0:
            raise ValueError(
                f"Strategy '{self.name}': dv01_per_million must be positive, "
                f"got {self.dv01_per_million}"
            )

    def to_config(
        self,
        position_size_mm_override: float | None = None,
        sizing_mode_override: str | None = None,
        stop_loss_pct_override: float | None = None,
        take_profit_pct_override: float | None = None,
        max_holding_days_override: int | None = None,
    ) -> BacktestConfig:
        """
        Convert strategy metadata to BacktestConfig.

        Supports runtime parameter overrides for rapid experimentation.

        Parameters
        ----------
        position_size_mm_override : float | None, default None
            Override catalog position_size_mm value.
        sizing_mode_override : str | None, default None
            Override catalog sizing_mode value.
        stop_loss_pct_override : float | None, default None
            Override catalog stop_loss_pct value (use False to explicitly disable).
        take_profit_pct_override : float | None, default None
            Override catalog take_profit_pct value (use False to explicitly disable).
        max_holding_days_override : int | None, default None
            Override catalog max_holding_days value (use False to explicitly disable).

        Returns
        -------
        BacktestConfig
            Full backtest configuration with strategy parameters.

        Examples
        --------
        >>> metadata = StrategyMetadata(
        ...     name="aggressive",
        ...     description="High risk tolerance",
        ...     position_size_mm=15.0,
        ...     sizing_mode="binary"
        ... )
        >>> config = metadata.to_config(position_size_mm_override=20.0)
        >>> config.position_size_mm
        20.0
        """
        return BacktestConfig(
            position_size_mm=(
                position_size_mm_override
                if position_size_mm_override is not None
                else self.position_size_mm
            ),
            sizing_mode=(
                sizing_mode_override
                if sizing_mode_override is not None
                else self.sizing_mode
            ),
            stop_loss_pct=(
                stop_loss_pct_override
                if stop_loss_pct_override is not None
                else self.stop_loss_pct
            ),
            take_profit_pct=(
                take_profit_pct_override
                if take_profit_pct_override is not None
                else self.take_profit_pct
            ),
            max_holding_days=(
                max_holding_days_override
                if max_holding_days_override is not None
                else self.max_holding_days
            ),
            transaction_cost_bps=self.transaction_cost_bps,
            dv01_per_million=self.dv01_per_million,
        )


class StrategyRegistry:
    """
    Registry for strategy metadata with JSON catalog persistence.

    Manages strategy definitions, enabling/disabling strategies, and catalog I/O.
    Follows pattern from models.registry.SignalRegistry.

    Parameters
    ----------
    catalog_path : str | Path
        Path to JSON catalog file containing strategy metadata.

    Examples
    --------
    >>> from aponyx.config import STRATEGY_CATALOG_PATH
    >>> registry = StrategyRegistry(STRATEGY_CATALOG_PATH)
    >>> enabled = registry.get_enabled()
    >>> metadata = registry.get_metadata("balanced")
    >>> config = metadata.to_config()
    """

    def __init__(self, catalog_path: str | Path) -> None:
        """
        Initialize registry and load catalog from JSON file.

        Parameters
        ----------
        catalog_path : str | Path
            Path to JSON catalog file.

        Raises
        ------
        FileNotFoundError
            If catalog file does not exist.
        ValueError
            If catalog JSON is invalid or contains duplicate strategy names.
        """
        self._catalog_path = Path(catalog_path)
        self._strategies: dict[str, StrategyMetadata] = {}
        self._load_catalog()

        logger.info(
            "Loaded strategy registry: catalog=%s, strategies=%d, enabled=%d",
            self._catalog_path,
            len(self._strategies),
            len(self.get_enabled()),
        )

    def _load_catalog(self) -> None:
        """Load strategy metadata from JSON catalog file."""
        if not self._catalog_path.exists():
            raise FileNotFoundError(f"Strategy catalog not found: {self._catalog_path}")

        with open(self._catalog_path, "r", encoding="utf-8") as f:
            catalog_data = json.load(f)

        if not isinstance(catalog_data, list):
            raise ValueError("Strategy catalog must be a JSON array")

        for entry in catalog_data:
            try:
                metadata = StrategyMetadata(**entry)
                if metadata.name in self._strategies:
                    raise ValueError(
                        f"Duplicate strategy name in catalog: {metadata.name}"
                    )
                self._strategies[metadata.name] = metadata
            except TypeError as e:
                raise ValueError(
                    f"Invalid strategy metadata in catalog: {entry}. Error: {e}"
                ) from e

        logger.debug("Loaded %d strategies from catalog", len(self._strategies))

        # Fail-fast validation: thresholds already validated in __post_init__
        # No additional validation needed beyond dataclass constraints

    def get_metadata(self, name: str) -> StrategyMetadata:
        """
        Retrieve metadata for a specific strategy.

        Parameters
        ----------
        name : str
            Strategy name.

        Returns
        -------
        StrategyMetadata
            Strategy metadata.

        Raises
        ------
        KeyError
            If strategy name is not registered.
        """
        if name not in self._strategies:
            raise KeyError(
                f"Strategy '{name}' not found in registry. "
                f"Available strategies: {sorted(self._strategies.keys())}"
            )
        return self._strategies[name]

    def get_enabled(self) -> dict[str, StrategyMetadata]:
        """
        Get all enabled strategies.

        Returns
        -------
        dict[str, StrategyMetadata]
            Mapping from strategy name to metadata for enabled strategies only.
        """
        return {name: meta for name, meta in self._strategies.items() if meta.enabled}

    def list_all(self) -> dict[str, StrategyMetadata]:
        """
        Get all registered strategies (enabled and disabled).

        Returns
        -------
        dict[str, StrategyMetadata]
            Mapping from strategy name to metadata for all strategies.
        """
        return self._strategies.copy()

    def strategy_exists(self, name: str) -> bool:
        """
        Check if strategy is registered.

        Parameters
        ----------
        name : str
            Strategy name.

        Returns
        -------
        bool
            True if strategy exists in registry.
        """
        return name in self._strategies

    def save_catalog(self, path: str | Path | None = None) -> None:
        """
        Save strategy metadata to JSON catalog file.

        Parameters
        ----------
        path : str | Path | None
            Output path. If None, overwrites original catalog file.
        """
        output_path = Path(path) if path else self._catalog_path

        catalog_data = [asdict(meta) for meta in self._strategies.values()]

        output_path.parent.mkdir(parents=True, exist_ok=True)
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(catalog_data, f, indent=2)

        logger.info(
            "Saved strategy catalog: path=%s, strategies=%d",
            output_path,
            len(catalog_data),
        )
