"""No-lookahead indicator surface (Strategy Lab, Phase 2).

An :class:`Indicators` object is built once per market and fed each replay event **in delivery
order**, before the strategy observes that same event. Every accessor is therefore a pure function
of *events seen so far* plus *static pre-close metadata* (the scheduled ``end_date``) — nothing
else. This is the hard boundary that keeps a Strategy-Lab strategy honest:

  * It NEVER stores or exposes ``winning_token_id`` or ``resolved_ts``. A ``ResolutionEvent`` handed
    to :meth:`observe` is deliberately dropped — the winner is unknowable to a strategy until the
    engine settles, and this object gives no way to peek at it.
  * ``time_to_close`` is measured against the *scheduled* ``end_date`` (a pre-close marker), never
    the settlement instant.

There is no method that returns a future event, the resolution, or the label. Absence is the
guarantee: a strategy that tries ``indicators.winning_token_id`` gets an ``AttributeError``.
"""

from __future__ import annotations

from datetime import datetime, timedelta
from decimal import Decimal

from ante_strategy_sdk.types import Market, TokenId, TradeEvent

# Bump on any change to the indicator surface (method set / semantics) so a cached strategy result
# is invalidated when the observable inputs change.
INDICATOR_API_VERSION = 1


class Indicators:
    """Per-market, monotonically-updated view of trade history + static pre-close metadata."""

    __slots__ = ("_token_ids", "_end_date", "_prices")

    def __init__(self, market: Market, end_date: datetime | None) -> None:
        self._token_ids: tuple[TokenId, ...] = tuple(market.token_ids)
        # Static scheduled close. NEVER the resolution instant — see module docstring.
        self._end_date: datetime | None = end_date
        # Per-token list of trade prices, in delivery order. Trades only; no resolution/label state.
        self._prices: dict[TokenId, list[Decimal]] = {t: [] for t in self._token_ids}

    def observe(self, event: object) -> None:
        """Record one delivered event. Only ``TradeEvent`` prices are retained; every other event
        type — crucially ``ResolutionEvent`` — is ignored, so the label can never enter this view.
        """
        if isinstance(event, TradeEvent):
            bucket = self._prices.get(event.token_id)
            if bucket is not None:
                bucket.append(event.price)

    # --- pure accessors over events seen so far ---------------------------

    def trades_seen(self, token_id: TokenId) -> int:
        return len(self._prices.get(token_id, ()))

    def last_price(self, token_id: TokenId) -> Decimal | None:
        bucket = self._prices.get(token_id)
        return bucket[-1] if bucket else None

    def rolling_mean(self, token_id: TokenId, n: int) -> Decimal | None:
        """Mean of the last ``n`` trade prices seen for ``token_id`` (fewer if <n available).
        ``None`` when no trades have been seen yet.
        """
        window = self._window(token_id, n)
        if not window:
            return None
        return sum(window, Decimal(0)) / Decimal(len(window))

    def rolling_vol(self, token_id: TokenId, n: int) -> Decimal | None:
        """Population standard deviation of the last ``n`` trade prices seen for ``token_id``.
        ``None`` with no trades; ``Decimal(0)`` with a single sample (no dispersion yet).
        """
        window = self._window(token_id, n)
        if not window:
            return None
        k = len(window)
        if k == 1:
            return Decimal(0)
        mean = sum(window, Decimal(0)) / Decimal(k)
        variance = sum(((p - mean) ** 2 for p in window), Decimal(0)) / Decimal(k)
        return variance.sqrt()

    # --- static pre-close metadata ----------------------------------------

    def time_to_close(self, now_ts: datetime) -> timedelta | None:
        """``end_date - now_ts`` using the STATIC scheduled close. ``None`` if ``end_date`` unknown.
        Positive before the scheduled close, negative after. Never derived from ``resolved_ts``.
        """
        if self._end_date is None:
            return None
        return self._end_date - now_ts

    # --- internals --------------------------------------------------------

    def _window(self, token_id: TokenId, n: int) -> list[Decimal]:
        if n <= 0:
            raise ValueError(f"window size must be > 0: {n}")
        bucket = self._prices.get(token_id, [])
        return bucket[-n:]
