"""Non-repainting swing structure derived from an MLP score series.

The pivot at bar ``p`` becomes usable at ``p + right``.  Signals are emitted
on that confirmation bar, so callers cannot accidentally use future score
values in a backtest.
"""
from __future__ import annotations

from dataclasses import dataclass

import numpy as np


@dataclass(frozen=True)
class ScoreStructure:
    """Confirmed swing relations and the resulting bullish-state gate."""

    higher_low: np.ndarray
    lower_high: np.ndarray
    bullish_state: np.ndarray


def confirmed_score_structure(score: np.ndarray, *, left: int, right: int) -> ScoreStructure:
    """Return confirmed higher-low/lower-high events for ``score``.

    Equal values are deliberately not pivots: treating flat plateaus as
    repeated highs/lows would create arbitrary structural changes.
    """
    if left < 1 or right < 1:
        raise ValueError("left and right must both be at least 1")

    values = np.asarray(score, dtype=float)
    n = len(values)
    higher_low = np.zeros(n, dtype=bool)
    lower_high = np.zeros(n, dtype=bool)
    bullish_state = np.zeros(n, dtype=bool)
    previous_high: float | None = None
    previous_low: float | None = None
    bullish = False

    for confirmed_at in range(left + right, n):
        pivot_at = confirmed_at - right
        pivot = values[pivot_at]
        if not np.isfinite(pivot):
            bullish_state[confirmed_at] = bullish
            continue

        before = values[pivot_at - left:pivot_at]
        after = values[pivot_at + 1:pivot_at + right + 1]
        if np.isfinite(before).all() and np.isfinite(after).all():
            if pivot > np.max(before) and pivot > np.max(after):
                if previous_high is not None and pivot < previous_high:
                    lower_high[confirmed_at] = True
                    bullish = False
                previous_high = pivot
            elif pivot < np.min(before) and pivot < np.min(after):
                if previous_low is not None and pivot > previous_low:
                    higher_low[confirmed_at] = True
                    bullish = True
                previous_low = pivot

        bullish_state[confirmed_at] = bullish

    return ScoreStructure(higher_low=higher_low, lower_high=lower_high, bullish_state=bullish_state)
