"""
Leak-safe fractal feature reference.

This module reproduces the corrected `afml.features.fractals` pipeline from
Part 11 with `leak_safe=True`, expressed as an explicit bar-indexed contract so
that an event-driven MQL5 port can be verified against it line for line.

Contract (bar t, n = half-window):

    center      c = t - n
    fractal_high[t]  = 1 if high[c] == max(high[t-2n : t])
    fractal_low[t]   = 1 if low[c]  == min(low[t-2n : t])
    high_strength[t] = high[c] / mean(high[t-2n : t]) - 1   (0 when no fractal)
    low_strength[t]  = 1 - low[c] / mean(low[t-2n : t])     (0 when no fractal)
    valid_*[t]       = fractal_*[t] and strength[t] > threshold[t]
    breakout_up[t]   = close[t] > high[c] and fractal_low[t] == 1
    breakout_down[t] = close[t] < low[c]  and fractal_high[t] == 1

Everything published at bar t depends only on bars <= t.

References
----------
Williams, B. (1998). Trading Chaos. Wiley.
Lopez de Prado, M. (2018). Advances in Financial Machine Learning. Wiley. Ch. 7.
"""

from __future__ import annotations

from typing import Dict

import numpy as np
import pandas as pd

FRAC_EMPTY = -1e38


def fractal_features(
    high: pd.Series,
    low: pd.Series,
    close: pd.Series,
    volatility: pd.Series,
    n: int = 2,
    lookback_period: int = 20,
    ma_period: int = 20,
    threshold: float = 0.001,
    dynamic_threshold: bool = False,
) -> pd.DataFrame:
    """Compute the leak-safe fractal feature matrix.

    Parameters
    ----------
    high, low, close : pd.Series
        Bar OHLC components sharing one index.
    volatility : pd.Series
        Per-bar volatility, e.g. ATR expressed as a fraction of price.
    n : int
        Half-window. A fractal centered at bar c is confirmed at bar c + n.
    lookback_period : int
        Number of confirmed fractal *events* retained for support/resistance.
    ma_period : int
        Window for the trend moving average and breakout density.
    threshold : float
        Strength threshold when `dynamic_threshold` is False; multiplier on the
        center bar's volatility when it is True.
    dynamic_threshold : bool
        Scale the validity threshold by volatility at the center bar.

    Returns
    -------
    pd.DataFrame
        Eighteen columns, all causal at bar t.
    """
    if not (len(high) == len(low) == len(close) == len(volatility)):
        raise ValueError("All input series must share one length")
    if n < 1:
        raise ValueError("n must be >= 1")

    h = np.asarray(high, dtype=np.float64)
    lo = np.asarray(low, dtype=np.float64)
    c = np.asarray(close, dtype=np.float64)
    v = np.asarray(volatility, dtype=np.float64)
    T = len(h)
    w = 2 * n + 1

    cols = [
        "fractal_high",
        "fractal_low",
        "fractal_high_strength",
        "fractal_low_strength",
        "valid_fractal_high",
        "valid_fractal_low",
        "fractal_breakout_up",
        "fractal_breakout_down",
        "resistance_level",
        "support_level",
        "distance_to_resistance",
        "distance_to_support",
        "fractal_trend_strength",
        "fractal_trend_direction",
        "fractal_ma_ratio",
        "fractal_buy_signal",
        "fractal_sell_signal",
        "signal_strength",
    ]
    out = {k: np.full(T, FRAC_EMPTY, dtype=np.float64) for k in cols}

    res_events: list[float] = []
    sup_events: list[float] = []
    resistance = FRAC_EMPTY
    support = FRAC_EMPTY

    bo_up = np.zeros(T, dtype=np.float64)
    bo_dn = np.zeros(T, dtype=np.float64)

    for t in range(T):
        if t < 2 * n:
            continue

        s = t - 2 * n
        cen = t - n
        win_h = h[s : t + 1]
        win_l = lo[s : t + 1]

        is_fh = 1.0 if h[cen] == win_h.max() else 0.0
        is_fl = 1.0 if lo[cen] == win_l.min() else 0.0

        fhs = h[cen] / (win_h.sum() / w) - 1.0 if is_fh else 0.0
        fls = 1.0 - lo[cen] / (win_l.sum() / w) if is_fl else 0.0

        thr = threshold * v[cen] if dynamic_threshold else threshold
        vfh = 1.0 if (is_fh and fhs > thr) else 0.0
        vfl = 1.0 if (is_fl and fls > thr) else 0.0

        if vfh:
            res_events.append(h[cen])
            if len(res_events) > lookback_period:
                res_events.pop(0)
            resistance = max(res_events)
        if vfl:
            sup_events.append(lo[cen])
            if len(sup_events) > lookback_period:
                sup_events.pop(0)
            support = min(sup_events)

        bo_up[t] = 1.0 if (c[t] > h[cen] and is_fl == 1.0) else 0.0
        bo_dn[t] = 1.0 if (c[t] < lo[cen] and is_fh == 1.0) else 0.0

        mid = 0.5 * (h[t] + lo[t])
        d_res = (resistance - mid) / mid if resistance != FRAC_EMPTY else FRAC_EMPTY
        d_sup = (mid - support) / mid if support != FRAC_EMPTY else FRAC_EMPTY

        out["fractal_high"][t] = is_fh
        out["fractal_low"][t] = is_fl
        out["fractal_high_strength"][t] = fhs
        out["fractal_low_strength"][t] = fls
        out["valid_fractal_high"][t] = vfh
        out["valid_fractal_low"][t] = vfl
        out["fractal_breakout_up"][t] = bo_up[t]
        out["fractal_breakout_down"][t] = bo_dn[t]
        out["resistance_level"][t] = resistance
        out["support_level"][t] = support
        out["distance_to_resistance"][t] = d_res
        out["distance_to_support"][t] = d_sup

        # --- trend block: needs ma_period bars of breakout history and close
        if t >= max(2 * n + ma_period - 1, ma_period - 1):
            dens = bo_up[t - ma_period + 1 : t + 1].sum() + bo_dn[t - ma_period + 1 : t + 1].sum()
            bal = bo_up[t - ma_period + 1 : t + 1].sum() - bo_dn[t - ma_period + 1 : t + 1].sum()
            ma = c[t - ma_period + 1 : t + 1].sum() / ma_period
            direction = float(np.sign(bal))

            out["fractal_trend_strength"][t] = dens / ma_period
            out["fractal_trend_direction"][t] = direction
            out["fractal_ma_ratio"][t] = c[t] / ma - 1.0

            buy = 1.0 if (bo_up[t] == 1.0 and direction >= 0.0) else 0.0
            sell = 1.0 if (bo_dn[t] == 1.0 and direction <= 0.0) else 0.0
            strength = 0.0
            if buy:
                strength = fls / (v[t] + 1e-8)
            elif sell:
                strength = fhs / (v[t] + 1e-8)
            strength = min(max(strength, 0.0), 1.0)

            out["fractal_buy_signal"][t] = buy
            out["fractal_sell_signal"][t] = sell
            out["signal_strength"][t] = strength

    return pd.DataFrame(out, index=high.index)


def leaky_fractal_high(high: pd.Series, n: int = 2) -> pd.Series:
    """The uncorrected centered detector, retained for the leak comparison."""
    return (high == high.rolling(2 * n + 1, center=True).max()).astype(int)


def synthetic_ohlc(
    n_bars: int = 1200,
    seed: int = 7,
    start: float = 1.1000,
    vol: float = 0.0012,
) -> Dict[str, pd.Series]:
    """Generate a realistic OHLC series with volatility clustering.

    A GARCH(1,1)-like variance recursion drives log returns; each bar's high
    and low are drawn as absolute normal excursions around the open-close range
    so that local extrema (and therefore fractals) occur at plausible rates.
    """
    rng = np.random.default_rng(seed)
    omega, alpha, beta = vol**2 * 0.05, 0.08, 0.87
    sig2 = np.full(n_bars, vol**2)
    r = np.zeros(n_bars)
    for i in range(1, n_bars):
        sig2[i] = omega + alpha * r[i - 1] ** 2 + beta * sig2[i - 1]
        r[i] = rng.normal(0.0, np.sqrt(sig2[i]))

    close = start * np.exp(np.cumsum(r))
    open_ = np.concatenate([[start], close[:-1]])
    span = np.sqrt(sig2) * np.abs(rng.normal(0.0, 1.0, n_bars))
    high = np.maximum(open_, close) + span * close
    low = np.minimum(open_, close) - span * close

    idx = pd.date_range("2023-01-02", periods=n_bars, freq="h")
    tr = np.maximum(high - low, np.abs(high - open_))
    atr = pd.Series(tr, index=idx).rolling(14, min_periods=1).mean()

    return {
        "open": pd.Series(open_, index=idx),
        "high": pd.Series(high, index=idx),
        "low": pd.Series(low, index=idx),
        "close": pd.Series(close, index=idx),
        "volatility": atr / pd.Series(close, index=idx),
    }
