"""Portfolio risk filters for use inside ``generate_signal()``.

Each filter takes a weights dict and returns a (possibly modified) weights
dict. They can be chained::

    weights = max_weight(weights, limit=0.15)
    weights = max_positions(weights, limit=5)
    weights = normalize(weights, target=1.0)

All thresholds are explicit parameters — no hard-coded magic numbers.
"""

from __future__ import annotations

from typing import Any, Dict, Optional, Tuple

from cpz.indicators._helpers import EPSILON


def max_positions(
    weights: Dict[str, float],
    limit: int = 5,
    *,
    rank_by: str = "abs_weight",
) -> Dict[str, float]:
    """Keep only the top *limit* positions.

    Args:
        weights: Signal weights ``{symbol: weight}``.
        limit: Maximum number of positions to keep.
        rank_by: How to rank — ``"abs_weight"`` (default, largest absolute
                 weight first) or ``"weight"`` (largest signed weight first).
    """
    if len(weights) <= limit:
        return weights
    if rank_by == "abs_weight":
        ranked = sorted(weights.items(), key=lambda x: abs(x[1]), reverse=True)
    elif rank_by == "weight":
        ranked = sorted(weights.items(), key=lambda x: x[1], reverse=True)
    else:
        raise ValueError(f"Unknown rank_by '{rank_by}'. Use: abs_weight, weight")
    return dict(ranked[:limit])


def max_weight(
    weights: Dict[str, float],
    limit: float = 0.15,
) -> Dict[str, float]:
    """Cap each position at +/- *limit*.

    Args:
        weights: Signal weights.
        limit: Maximum absolute weight per position.
    """
    return {
        k: max(min(v, limit), -limit)
        for k, v in weights.items()
        if abs(v) > EPSILON
    }


def max_exposure(
    weights: Dict[str, float],
    *,
    gross: float = 1.5,
    net: float = 0.3,
) -> Dict[str, float]:
    """Scale weights to respect gross and net exposure limits.

    Args:
        weights: Signal weights.
        gross: Maximum gross exposure (sum of absolute weights).
        net: Maximum absolute net exposure (|sum of weights|).
    """
    if not weights:
        return weights

    current_gross = sum(abs(v) for v in weights.values())
    current_net = abs(sum(weights.values()))

    scale = 1.0
    if current_gross > gross and current_gross > EPSILON:
        scale = min(scale, gross / current_gross)
    if current_net > net and current_net > EPSILON:
        scale = min(scale, net / current_net)

    if abs(scale - 1.0) > EPSILON:
        return {k: v * scale for k, v in weights.items()}
    return weights


def normalize(
    weights: Dict[str, float],
    target: float = 1.0,
    *,
    method: str = "abs_sum",
) -> Dict[str, float]:
    """Scale weights so they sum to *target*.

    Args:
        weights: Signal weights.
        target: Target sum.
        method: ``"abs_sum"`` — scale so sum(|w|) = target (default).
                ``"sum"`` — scale so sum(w) = target (preserves direction).
    """
    if not weights:
        return weights

    if method == "abs_sum":
        total = sum(abs(v) for v in weights.values())
    elif method == "sum":
        total = sum(weights.values())
    else:
        raise ValueError(f"Unknown method '{method}'. Use: abs_sum, sum")

    if abs(total) < EPSILON:
        return weights
    scale = target / total
    return {k: v * scale for k, v in weights.items()}


def long_only(weights: Dict[str, float]) -> Dict[str, float]:
    """Remove all short (negative) weights."""
    return {k: v for k, v in weights.items() if v > EPSILON}


def min_weight(
    weights: Dict[str, float],
    threshold: float = 0.01,
) -> Dict[str, float]:
    """Remove positions with absolute weight below *threshold*.

    Args:
        weights: Signal weights.
        threshold: Minimum absolute weight to keep.
    """
    return {k: v for k, v in weights.items() if abs(v) >= threshold}


def sector_limit(
    weights: Dict[str, float],
    sectors: Dict[str, str],
    limit: float = 0.30,
) -> Dict[str, float]:
    """Cap total exposure per sector.

    Args:
        weights: Signal weights ``{symbol: weight}``.
        sectors: Sector mapping ``{symbol: sector_name}``.
        limit: Maximum absolute weight per sector.
    """
    sector_totals: Dict[str, float] = {}
    for sym, w in weights.items():
        sec = sectors.get(sym, "unknown")
        sector_totals[sec] = sector_totals.get(sec, 0.0) + abs(w)

    result = dict(weights)
    for sec, total in sector_totals.items():
        if total > limit and total > EPSILON:
            scale = limit / total
            for sym in result:
                if sectors.get(sym, "unknown") == sec:
                    result[sym] *= scale
    return result


def regime_filter(
    vix_bars: Any,
    *,
    threshold: float = 30.0,
    col_close: str = "close",
) -> bool:
    """Returns ``True`` when the VIX is below *threshold* (risk-on environment).

    Pass the VIX DataFrame as ``data.get("^VIX")`` or similar.

    Args:
        vix_bars: VIX price data (DataFrame, Series, array, or ``None``).
        threshold: VIX level above which the regime is risk-off.
        col_close: Column name for close price.
    """
    if vix_bars is None:
        return True
    from cpz.indicators._helpers import ensure_array
    arr = ensure_array(vix_bars, col_close)
    if len(arr) == 0:
        return True
    return float(arr[-1]) < threshold


def trend_filter(
    benchmark_bars: Any,
    *,
    period: int = 200,
    col_close: str = "close",
) -> bool:
    """Returns ``True`` when the benchmark is above its SMA (uptrend).

    Args:
        benchmark_bars: Benchmark price data (e.g. SPY).
        period: SMA period for trend determination.
        col_close: Column name for close price.
    """
    if benchmark_bars is None:
        return True
    from cpz.indicators import sma
    from cpz.indicators._helpers import ensure_array

    close = ensure_array(benchmark_bars, col_close)
    if len(close) == 0:
        return True
    sma_val = sma(benchmark_bars, period, col_close=col_close)
    return float(close[-1]) > sma_val


def max_drawdown_filter(
    weights: Dict[str, float],
    state: Dict[str, Any],
    *,
    threshold: float = 0.10,
    peak_key: str = "_peak_equity",
    equity_key: str = "_current_equity",
) -> Dict[str, float]:
    """Flatten to cash if drawdown exceeds *threshold*.

    Reads equity peak and current value from *state* dict. Key names
    are configurable.

    Args:
        weights: Signal weights.
        state: Strategy state dict (must contain equity tracking keys).
        threshold: Maximum drawdown fraction before flattening (e.g. 0.10 = 10 %).
        peak_key: State key for peak equity value.
        equity_key: State key for current equity value.
    """
    peak = state.get(peak_key, 1.0)
    current = state.get(equity_key, 1.0)
    if peak > EPSILON:
        dd = (peak - current) / peak
        if dd > threshold:
            return {}
    return weights
