import pandas as pd
from pathlib import Path
import sys

BASE_PATH = Path(__file__).resolve().parents[2]
SRC_PATH = BASE_PATH / "src"

if str(SRC_PATH) not in sys.path:
    sys.path.append(str(SRC_PATH))

from features.spot_lag_v3 import get_model_ready_spot_lag_v3
from ingestion.price_loader_15m import load_eurusd_15m


def build_hourly_signals(
    threshold: float = 2.0,
    allowed_hours: set | None = None,
):
    df = get_model_ready_spot_lag_v3().copy()
    df["hour"] = pd.to_datetime(df["datetime"]).dt.hour

    if allowed_hours is not None:
        df = df[df["hour"].isin(allowed_hours)].copy()

    df = df.sort_values("datetime").reset_index(drop=True)

    signals = []
    i = 0
    while i < len(df):
        row = df.iloc[i]

        signal = 0
        if row["lag_zscore_24h_v3"] >= threshold:
            signal = 1
        elif row["lag_zscore_24h_v3"] <= -threshold:
            signal = -1

        if signal != 0:
            signals.append(
                {
                    "signal_time": row["datetime"],
                    "signal": signal,
                    "signal_price": row["close"],
                    "lag_zscore": row["lag_zscore_24h_v3"],
                }
            )
            # one signal at a time on the hourly signal layer
            i += 48
        else:
            i += 1

    return pd.DataFrame(signals)


def simulate_trade_from_entry(
    m15: pd.DataFrame,
    entry_idx: int,
    signal: int,
    stop: float,
    hold_bars: int,
    spread_cost: float,
):
    entry_row = m15.iloc[entry_idx]
    entry_price = entry_row["close"]

    exit_idx = min(entry_idx + hold_bars, len(m15) - 1)
    path = m15.iloc[entry_idx: exit_idx + 1].copy()

    future_close = path.iloc[-1]["close"]

    if signal == 1:
        adverse = (path["low"].min() - entry_price) / entry_price
        stop_hit = adverse < -stop
        ret = future_close / entry_price - 1
        if stop_hit:
            ret = -stop
    else:
        adverse = (path["high"].max() - entry_price) / entry_price
        stop_hit = adverse > stop
        ret = -(future_close / entry_price - 1)
        if stop_hit:
            ret = -stop

    ret -= spread_cost

    return {
        "entry_time": entry_row["datetime"],
        "entry_price": entry_price,
        "return": ret,
    }


def find_pullback_entry(
    m15: pd.DataFrame,
    signal_time: pd.Timestamp,
    signal: int,
    signal_price: float,
    fib_level: float | str,
    lookahead_bars: int = 24,  # 6 hours on 15m
):
    """
    fib_level:
        "immediate" or numeric in [0.236, 0.382, 0.5, 0.618, 0.786]
    """
    future = m15[m15["datetime"] >= signal_time].copy().reset_index(drop=True)
    if future.empty:
        return None

    future = future.iloc[:lookahead_bars].copy()
    if future.empty:
        return None

    # Immediate entry = first available 15m close at/after signal time
    if fib_level == "immediate":
        return 0

    # Build signal-to-extreme move inside the waiting window
    if signal == 1:
        extreme_price = future["high"].max()
        move = extreme_price - signal_price
        if move <= 0:
            return None

        target_price = extreme_price - fib_level * move

        # find first bar whose low touches the pullback level
        hits = future.index[future["low"] <= target_price].tolist()
        if not hits:
            return None

        hit_idx = hits[0]

        # simple continuation confirmation:
        # after the pullback hits, require a 15m close above prior 15m high
        for j in range(hit_idx + 1, len(future)):
            if future.iloc[j]["close"] > future.iloc[j - 1]["high"]:
                return j

        return None

    else:
        extreme_price = future["low"].min()
        move = signal_price - extreme_price
        if move <= 0:
            return None

        target_price = extreme_price + fib_level * move

        # find first bar whose high touches the pullback level
        hits = future.index[future["high"] >= target_price].tolist()
        if not hits:
            return None

        hit_idx = hits[0]

        # simple continuation confirmation:
        # after pullback hits, require a 15m close below prior 15m low
        for j in range(hit_idx + 1, len(future)):
            if future.iloc[j]["close"] < future.iloc[j - 1]["low"]:
                return j

        return None


def run_backtest_for_pullback(
    fib_level,
    threshold: float = 2.0,
    stop: float = 0.0025,
    spread_cost: float = 0.0001,
    hold_hours: int = 48,
):
    # London + NY only, as that was your best session test
    allowed_hours = set(range(7, 17))

    hourly_signals = build_hourly_signals(
        threshold=threshold,
        allowed_hours=allowed_hours,
    )

    m15 = load_eurusd_15m().copy()
    m15 = m15.sort_values("datetime").reset_index(drop=True)

    hold_bars = hold_hours * 4  # 15m bars

    trades = []
    last_exit_time = None

    for _, sig in hourly_signals.iterrows():
        signal_time = sig["signal_time"]

        # single active position only
        if last_exit_time is not None and signal_time < last_exit_time:
            continue

        entry_offset = find_pullback_entry(
            m15=m15,
            signal_time=signal_time,
            signal=sig["signal"],
            signal_price=sig["signal_price"],
            fib_level=fib_level,
            lookahead_bars=24,
        )

        if entry_offset is None:
            continue

        future = m15[m15["datetime"] >= signal_time].copy().reset_index(drop=True)
        if future.empty or entry_offset >= len(future):
            continue

        entry_time = future.iloc[entry_offset]["datetime"]
        entry_idx_list = m15.index[m15["datetime"] == entry_time].tolist()
        if not entry_idx_list:
            continue

        entry_idx = entry_idx_list[0]

        trade = simulate_trade_from_entry(
            m15=m15,
            entry_idx=entry_idx,
            signal=sig["signal"],
            stop=stop,
            hold_bars=hold_bars,
            spread_cost=spread_cost,
        )

        trade["signal_time"] = signal_time
        trade["signal"] = sig["signal"]
        trade["fib_level"] = fib_level

        trades.append(trade)

        last_exit_time = entry_time + pd.Timedelta(hours=hold_hours)

    trades = pd.DataFrame(trades)

    if trades.empty:
        return {
            "entry_type": fib_level,
            "trades": 0,
            "avg_return": None,
            "win_rate": None,
            "final_equity": None,
            "max_drawdown": None,
        }

    trades["equity_curve"] = (1 + trades["return"]).cumprod()
    trades["running_peak"] = trades["equity_curve"].cummax()
    trades["drawdown"] = trades["equity_curve"] / trades["running_peak"] - 1

    return {
        "entry_type": fib_level,
        "trades": len(trades),
        "avg_return": trades["return"].mean(),
        "win_rate": (trades["return"] > 0).mean(),
        "final_equity": trades["equity_curve"].iloc[-1],
        "max_drawdown": trades["drawdown"].min(),
    }


def run_test():
    fib_levels = ["immediate", 0.236, 0.382, 0.5, 0.618, 0.786]
    results = []

    for level in fib_levels:
        res = run_backtest_for_pullback(level)
        results.append(res)

    result_df = pd.DataFrame(results)

    print("\n=== PULLBACK LADDER TEST V1 ===\n")
    print(result_df.to_string(index=False))

    print("\nBest by final equity:")
    print(result_df.sort_values("final_equity", ascending=False).head(1).to_string(index=False))

    print("\nBest by lowest drawdown:")
    print(result_df.sort_values("max_drawdown", ascending=False).head(1).to_string(index=False))


if __name__ == "__main__":
    run_test()