from __future__ import annotations

import abc
import enum
from typing import Any

from signalab.account import Account


class Signal(enum.Enum):
    """Trading signal values emitted by indicators and strategies.

    Attributes
    ----------
    BUY : Signal
        Long-side signal.
    SELL : Signal
        Short-side signal.
    HOLD : Signal
        Neutral signal.
    """

    BUY = "buy"
    SELL = "sell"
    HOLD = "hold"

    @staticmethod
    def from_int(value: int) -> Signal:
        """Convert integer representation to a :class:`Signal`.

        Parameters
        ----------
        value : int
            Integer signal value where ``1`` is buy, ``-1`` is sell, and any
            other value is hold.

        Returns
        -------
        Signal
            The converted signal enum value.
        """
        if value == 1:
            return Signal.BUY
        elif value == -1:
            return Signal.SELL
        return Signal.HOLD


class Strategy(abc.ABC):
    """Base class for trading strategies.

    Parameters
    ----------
    name : str
        Human-readable strategy name.
    label : str | None, default=None
        Stable strategy identifier used for account mapping and output columns.
        If ``None``, a snake_case version of ``name`` is used.
    """

    def __init__(self, name: str, label: str | None = None):
        """Initialize base strategy metadata.

        Parameters
        ----------
        name : str
            Human-readable strategy name.
        label : str | None, default=None
            Stable strategy identifier. If ``None``, derived from ``name``.
        """
        self.name = name
        self.label = label or (name.lower().replace(" ", "_"))
        self._account: Account | None = None
        self._all_accounts: set[Account] | None = None
        self._current_market: Any | None = None
        self._signal_columns: list[str] | None = None
        self._signal_names: list[str] | None = None
        self._buy_signal_count_cache: int | None = None
        self._sell_signal_count_cache: int | None = None

    def _set_current_market(self, market: Any) -> None:
        """Set current market snapshot for strategy helper methods."""
        self._current_market = market
        self._buy_signal_count_cache = None
        self._sell_signal_count_cache = None

    def _require_current_market(self) -> Any:
        """Return current market snapshot or raise when unset.

        Returns
        -------
        Any
            Current market row object.

        Raises
        ------
        RuntimeError
            If no current market has been set.
        """
        if self._current_market is None:
            raise RuntimeError(
                "Current market is not set. The engine must call _set_current_market() before strategy helper access."
            )
        return self._current_market

    def _ensure_signal_metadata(self, current_market: Any) -> None:
        """Initialize cached signal column metadata when needed.

        Parameters
        ----------
        current_market : Any
            Current market row with ``_fields`` metadata.
        """
        if self._signal_columns is None:
            self._signal_columns = [
                col for col in current_market._fields if col.endswith("_signal")
            ]
            self._signal_names = [
                col[: -len("_signal")] for col in self._signal_columns
            ]

    def _compute_signal_counts(self) -> tuple[int, int]:
        """Compute buy and sell signal counts for the current market row.

        Returns
        -------
        tuple[int, int]
            Tuple ``(buy_count, sell_count)``.
        """
        if (
            self._buy_signal_count_cache is not None
            and self._sell_signal_count_cache is not None
        ):
            return self._buy_signal_count_cache, self._sell_signal_count_cache

        current_market = self._require_current_market()
        self._ensure_signal_metadata(current_market)
        assert self._signal_columns is not None

        buy_count = 0
        sell_count = 0
        for col in self._signal_columns:
            value = int(getattr(current_market, col))
            if value == 1:
                buy_count += 1
            elif value == -1:
                sell_count += 1

        self._buy_signal_count_cache = buy_count
        self._sell_signal_count_cache = sell_count
        return buy_count, sell_count

    @property
    def account(self) -> Account:
        """Return the strategy account.

        Returns
        -------
        Account
            Account bound to the strategy.

        Raises
        ------
        ValueError
            If the account has not been set yet.
        """
        if self._account is None:
            raise ValueError("Account not set for strategy.")
        return self._account

    def set_account(self, account: Account) -> None:
        """Bind an account instance to the strategy.

        Parameters
        ----------
        account : Account
            Account used by strategy operations.
        """
        self._account = account

    @property
    def all_accounts(self) -> set[Account]:
        """Return all accounts participating in the current backtest.

        Returns
        -------
        set[Account]
            Set of accounts configured in the running engine.

        Raises
        ------
        ValueError
            If accounts have not been set yet.
        """
        if self._all_accounts is None:
            raise ValueError("Accounts not set for strategy.")
        return self._all_accounts

    def _set_all_accounts(self, accounts: set[Account]) -> None:
        """Set all accounts visible to the strategy during a run."""
        self._all_accounts = accounts

    def get_signals(self) -> list[Signal]:
        """Collect all ``*_signal`` fields from the current market row.

        Returns
        -------
        list[Signal]
            Signal values in the same order as detected ``*_signal`` columns.
        """
        current_market = self._require_current_market()
        self._ensure_signal_metadata(current_market)

        assert self._signal_columns is not None and self._signal_names is not None
        return [
            Signal.from_int(getattr(current_market, col))
            for col in self._signal_columns
        ]

    def get_signals_dict(self) -> dict[str, Signal]:
        """Return named signals from the current market row.

        Returns
        -------
        dict[str, Signal]
            Mapping from signal base name (without ``_signal`` suffix) to
            :class:`Signal` value.
        """
        current_market = self._require_current_market()
        self._ensure_signal_metadata(current_market)

        assert self._signal_columns is not None and self._signal_names is not None

        return {
            name: Signal.from_int(getattr(current_market, col))
            for name, col in zip(self._signal_names, self._signal_columns, strict=True)
        }

    def buy_signal_count(self) -> int:
        """Return number of buy signals for the current market row.

        Returns
        -------
        int
            Count of ``*_signal`` columns equal to ``1``.
        """
        buy_count, _ = self._compute_signal_counts()
        return buy_count

    def sell_signal_count(self) -> int:
        """Return number of sell signals for the current market row.

        Returns
        -------
        int
            Count of ``*_signal`` columns equal to ``-1``.
        """
        _, sell_count = self._compute_signal_counts()
        return sell_count

    @abc.abstractmethod
    def perform(self, current_market: Any) -> None:
        """Execute one strategy step for the current market state.

        Parameters
        ----------
        current_market : Any
            Current market row snapshot.
        """
        raise NotImplementedError
