"""Strategy contract + registry. One Protocol, one registry — strategies plug in without the rest of
the system knowing their internals (DRY: the research/backtest layers depend only on this contract)."""
from __future__ import annotations

from typing import Protocol, runtime_checkable

import numpy as np

from fxhnt.domain.models import PriceSeries


@runtime_checkable
class Strategy(Protocol):
    kind: str
    rationale: str  # the structural reason the edge should exist (the gauntlet requires one)

    def positions(self, prices: PriceSeries, params: dict[str, float]) -> np.ndarray:
        """Target position in [-1, 1] per day, set from info up to and including day t and applied to
        day t+1's return (callers/implementations must avoid lookahead)."""
        ...


_REGISTRY: dict[str, Strategy] = {}


def register(strategy: Strategy) -> Strategy:
    _REGISTRY[strategy.kind] = strategy
    return strategy


def get_strategy(kind: str) -> Strategy:
    if kind not in _REGISTRY:
        raise KeyError(f"unknown strategy {kind!r}; available: {available()}")
    return _REGISTRY[kind]


def available() -> list[str]:
    return sorted(_REGISTRY)


def rolling_mean(x: np.ndarray, window: int) -> np.ndarray:
    """Causal rolling mean (uses only x[..t]); shorter window at the start, no lookahead."""
    cs = np.concatenate([[0.0], np.cumsum(x)])
    out = np.empty(len(x))
    for t in range(len(x)):
        lo = max(0, t + 1 - window)
        out[t] = (cs[t + 1] - cs[lo]) / (t + 1 - lo)
    return out


def rolling_std(x: np.ndarray, window: int) -> np.ndarray:
    """Causal rolling standard deviation (uses only x[..t]); 0 until ≥2 points."""
    out = np.zeros(len(x))
    for t in range(len(x)):
        lo = max(0, t + 1 - window)
        seg = x[lo:t + 1]
        out[t] = float(seg.std()) if len(seg) > 1 else 0.0
    return out


def rolling_max(x: np.ndarray, window: int) -> np.ndarray:
    """Causal rolling maximum (uses only x[..t]); shorter window at the start, no lookahead."""
    out = np.empty(len(x))
    for t in range(len(x)):
        lo = max(0, t + 1 - window)
        out[t] = float(x[lo:t + 1].max())
    return out


def rolling_min(x: np.ndarray, window: int) -> np.ndarray:
    """Causal rolling minimum (uses only x[..t]); shorter window at the start, no lookahead."""
    out = np.empty(len(x))
    for t in range(len(x)):
        lo = max(0, t + 1 - window)
        out[t] = float(x[lo:t + 1].min())
    return out
