from __future__ import annotations

import inspect
import itertools
from collections.abc import Iterable, Iterator
from dataclasses import dataclass
from typing import Any

from signalab.account import Account
from signalab.indicator.base import Indicator
from signalab.strategy.base import Strategy


def _as_values(value: Any) -> tuple[Any, ...]:
    """Normalize a grid value into a tuple of candidates.

    Parameters
    ----------
    value : Any
        Scalar or iterable candidate specification.

    Returns
    -------
    tuple[Any, ...]
        Normalized candidates.
    """
    if isinstance(value, (str, bytes)):
        return (value,)
    if isinstance(value, Iterable):
        values = tuple(value)
        if not values:
            raise ValueError("Parameter grid values cannot be empty.")
        return values
    return (value,)


def _constructor_param_names(cls: type[Any]) -> tuple[set[str], bool]:
    """Return constructor argument names and kwargs support flag.

    Parameters
    ----------
    cls : type[Any]
        Target class.

    Returns
    -------
    tuple[set[str], bool]
        Known parameter names and whether ``**kwargs`` is accepted.
    """
    signature = inspect.signature(cls.__init__)
    names: set[str] = set()
    accepts_kwargs = False
    for name, param in signature.parameters.items():
        if name == "self":
            continue
        if param.kind == inspect.Parameter.VAR_KEYWORD:
            accepts_kwargs = True
            continue
        names.add(name)
    return names, accepts_kwargs


def _validate_param_space(
    cls: type[Any],
    param_space: dict[str, tuple[Any, ...]],
    *,
    context: str,
) -> None:
    """Validate parameter names for a constructor-backed grid.

    Parameters
    ----------
    cls : type[Any]
        Target class.
    param_space : dict[str, tuple[Any, ...]]
        Parameter grid to validate.
    context : str
        Error context label.
    """
    known_params, accepts_kwargs = _constructor_param_names(cls)
    unknown = set(param_space).difference(known_params)
    if unknown and not accepts_kwargs:
        unknown_list = ", ".join(sorted(unknown))
        raise ValueError(
            f"Unknown {context} parameter(s) for {cls.__name__}: {unknown_list}"
        )


def _expand_space(param_space: dict[str, tuple[Any, ...]]) -> list[dict[str, Any]]:
    """Expand a parameter space into cartesian product dictionaries.

    Parameters
    ----------
    param_space : dict[str, tuple[Any, ...]]
        Parameter candidates.

    Returns
    -------
    list[dict[str, Any]]
        Expanded configuration dictionaries.
    """
    if not param_space:
        return [{}]
    keys = list(param_space)
    values = [param_space[key] for key in keys]
    return [dict(zip(keys, combo, strict=True)) for combo in itertools.product(*values)]


@dataclass(frozen=True)
class _StrategySpace:
    """Internal strategy grid specification."""

    cls: type[Strategy]
    param_space: dict[str, tuple[Any, ...]]


@dataclass(frozen=True)
class _IndicatorSpace:
    """Internal indicator grid specification."""

    cls: type[Indicator]
    param_space: dict[str, tuple[Any, ...]]


@dataclass(frozen=True)
class IndicatorConfig:
    """Resolved indicator configuration for one experiment run."""

    cls: type[Indicator]
    params: dict[str, Any]

    def make(self) -> Indicator:
        """Instantiate indicator from stored class and params.

        Returns
        -------
        Indicator
            Indicator instance.
        """
        return self.cls(**self.params)


@dataclass(frozen=True)
class ExperimentConfig:
    """Resolved experiment configuration for one run."""

    strategy_cls: type[Strategy]
    strategy_params: dict[str, Any]
    indicators: tuple[IndicatorConfig, ...]
    account_params: dict[str, Any]
    individual_accounts: bool

    def make_strategy(self) -> Strategy:
        """Instantiate strategy from stored class and params.

        Returns
        -------
        Strategy
            Strategy instance.
        """
        return self.strategy_cls(**self.strategy_params)

    def make_indicators(self) -> list[Indicator]:
        """Instantiate all configured indicators.

        Returns
        -------
        list[Indicator]
            Indicator instances.
        """
        return [indicator.make() for indicator in self.indicators]

    def make_account(self) -> Account:
        """Instantiate account from configured account parameters.

        Returns
        -------
        Account
            Account instance.
        """
        return Account(**self.account_params)


class ParamGrid:
    """Built parameter grid for experiment iteration."""

    def __init__(self, configs: list[ExperimentConfig]):
        """Initialize built parameter grid.

        Parameters
        ----------
        configs : list[ExperimentConfig]
            Expanded run configurations.
        """
        self._configs = configs

    def __len__(self) -> int:
        """Return number of configurations in the grid.

        Returns
        -------
        int
            Configuration count.
        """
        return len(self._configs)

    def __iter__(self) -> Iterator[ExperimentConfig]:
        """Iterate over expanded configurations.

        Returns
        -------
        Iterator[ExperimentConfig]
            Configuration iterator.
        """
        return iter(self._configs)

    def to_list(self) -> list[ExperimentConfig]:
        """Return configurations as a list.

        Returns
        -------
        list[ExperimentConfig]
            Configuration list.
        """
        return list(self._configs)


class ParamGridBuilder:
    """Builder for strategy/indicator/account experiment configurations."""

    def __init__(self) -> None:
        """Initialize empty parameter-grid builder."""
        self._strategies: list[_StrategySpace] = []
        self._indicators: list[_IndicatorSpace] = []
        self._account_space: dict[str, tuple[Any, ...]] = {}
        self._individual_accounts: tuple[bool, ...] = (False,)

        self._indicator_selector: str = "all"
        self._selector_include_empty = False
        self._selector_k: int | None = None

    def strategy(self, cls: type[Strategy], **params: Any) -> ParamGridBuilder:
        """Add strategy parameter space.

        Parameters
        ----------
        cls : type[Strategy]
            Strategy class.
        **params : Any
            Strategy parameter candidates.

        Returns
        -------
        ParamGridBuilder
            Builder instance.
        """
        if not issubclass(cls, Strategy):
            raise ValueError(f"Strategy class must inherit Strategy, got: {cls}")
        space = {key: _as_values(value) for key, value in params.items()}
        _validate_param_space(cls, space, context="strategy")
        self._strategies.append(_StrategySpace(cls=cls, param_space=space))
        return self

    def indicator(self, cls: type[Indicator], **params: Any) -> ParamGridBuilder:
        """Add indicator parameter space.

        Parameters
        ----------
        cls : type[Indicator]
            Indicator class.
        **params : Any
            Indicator parameter candidates.

        Returns
        -------
        ParamGridBuilder
            Builder instance.
        """
        if not issubclass(cls, Indicator):
            raise ValueError(f"Indicator class must inherit Indicator, got: {cls}")
        space = {key: _as_values(value) for key, value in params.items()}
        _validate_param_space(cls, space, context="indicator")
        self._indicators.append(_IndicatorSpace(cls=cls, param_space=space))
        return self

    def account(self, **params: Any) -> ParamGridBuilder:
        """Set account parameter space.

        Parameters
        ----------
        **params : Any
            Account parameter candidates.

        Returns
        -------
        ParamGridBuilder
            Builder instance.
        """
        space = {key: _as_values(value) for key, value in params.items()}
        _validate_param_space(Account, space, context="account")
        self._account_space.update(space)
        return self

    def individual_accounts(self, values: bool | Iterable[bool]) -> ParamGridBuilder:
        """Set individual-account mode candidates.

        Parameters
        ----------
        values : bool | Iterable[bool]
            Candidate values for ``individual_accounts``.

        Returns
        -------
        ParamGridBuilder
            Builder instance.
        """
        if isinstance(values, bool):
            self._individual_accounts = (values,)
            return self
        normalized = tuple(values)
        if not normalized:
            raise ValueError("individual_accounts values cannot be empty")
        self._individual_accounts = normalized
        return self

    def indicator_selection_all(self) -> ParamGridBuilder:
        """Use all configured indicators in every run.

        Returns
        -------
        ParamGridBuilder
            Builder instance.
        """
        self._indicator_selector = "all"
        return self

    def indicator_selection_all_subsets(
        self, include_empty: bool = False
    ) -> ParamGridBuilder:
        """Use all indicator subsets.

        Parameters
        ----------
        include_empty : bool, default=False
            Whether to include empty indicator subset.

        Returns
        -------
        ParamGridBuilder
            Builder instance.
        """
        self._indicator_selector = "all_subsets"
        self._selector_include_empty = include_empty
        return self

    def indicator_selection_k_of_n(self, k: int) -> ParamGridBuilder:
        """Use indicator subsets with exactly ``k`` indicators.

        Parameters
        ----------
        k : int
            Number of indicators per subset.

        Returns
        -------
        ParamGridBuilder
            Builder instance.
        """
        if k < 0:
            raise ValueError("k must be non-negative")
        self._indicator_selector = "k_of_n"
        self._selector_k = k
        return self

    def _subsets_all(
        self, indicators: tuple[_IndicatorSpace, ...]
    ) -> list[tuple[_IndicatorSpace, ...]]:
        """Return single subset containing all indicators."""
        return [indicators]

    def _subsets_all_subsets(
        self, indicators: tuple[_IndicatorSpace, ...]
    ) -> list[tuple[_IndicatorSpace, ...]]:
        """Return all indicator subsets according to current options."""
        n = len(indicators)
        start_k = 0 if self._selector_include_empty else 1
        return [
            combo
            for k in range(start_k, n + 1)
            for combo in itertools.combinations(indicators, k)
        ]

    def _subsets_k_of_n(
        self, indicators: tuple[_IndicatorSpace, ...]
    ) -> list[tuple[_IndicatorSpace, ...]]:
        """Return indicator subsets containing exactly ``k`` entries."""
        assert self._selector_k is not None
        return list(itertools.combinations(indicators, self._selector_k))

    def _indicator_subsets(self) -> list[tuple[_IndicatorSpace, ...]]:
        """Build indicator subsets from configured selector.

        Returns
        -------
        list[tuple[_IndicatorSpace, ...]]
            Indicator subsets.
        """
        indicators = tuple(self._indicators)
        if not indicators:
            return [()]

        if self._indicator_selector == "all":
            return self._subsets_all(indicators)

        if self._indicator_selector == "all_subsets":
            return self._subsets_all_subsets(indicators)

        if self._indicator_selector == "k_of_n":
            return self._subsets_k_of_n(indicators)

        raise RuntimeError(
            f"Unknown indicator selection mode: {self._indicator_selector}"
        )

    def build(self) -> ParamGrid:
        """Build full cartesian product parameter grid.

        Returns
        -------
        ParamGrid
            Expanded parameter grid.
        """
        if not self._strategies:
            raise ValueError("At least one strategy must be configured.")

        strategy_configs: list[tuple[type[Strategy], dict[str, Any]]] = []
        for strategy in self._strategies:
            for params in _expand_space(strategy.param_space):
                strategy_configs.append((strategy.cls, params))

        indicator_subsets = self._indicator_subsets()
        account_configs = _expand_space(self._account_space)
        configs: list[ExperimentConfig] = []

        for strategy_cls, strategy_params in strategy_configs:
            for subset in indicator_subsets:
                indicator_param_product = [
                    _expand_space(indicator.param_space) for indicator in subset
                ]
                combos = itertools.product(*indicator_param_product) if subset else [()]
                for indicator_params_combo in combos:
                    indicators = tuple(
                        IndicatorConfig(cls=ind_space.cls, params=params)
                        for ind_space, params in zip(
                            subset, indicator_params_combo, strict=True
                        )
                    )
                    for account_params in account_configs:
                        for individual in self._individual_accounts:
                            configs.append(
                                ExperimentConfig(
                                    strategy_cls=strategy_cls,
                                    strategy_params=dict(strategy_params),
                                    indicators=indicators,
                                    account_params=dict(account_params),
                                    individual_accounts=individual,
                                )
                            )

        return ParamGrid(configs)
