"""
simulate_regime_strategies.py

[SIMULATION-ONLY 2026-09-04] Bar-by-bar backtest that replays every symbol
in a day's snapshot (data/snapshots/<date>/) through entry_engine's REAL
evaluate_entry() logic twice per symbol -- once with the base `entry`
config (BASELINE) and once with that symbol's regime_strategies override
applied via regime_strategies.evaluate_entry_for_regime() (REGIME) -- and
compares what each rule set would have done, using a simplified ATR
trailing-stop exit shared by both runs so any P/L difference is isolated
to the entry-rule change, not the exit mechanic.

WHAT THIS IS NOT: a faithful reproduction of monitor.py's live loop. Real
data gaps and simplifications, all deliberate and documented at point of
use below:

  - Regime is read once from data/snapshots/<date>/regimes.json (the
    already-computed first-90-min classification), not re-evaluated
    bar-by-bar. Good enough for testing "which rule set would apply,"
    not a claim that regime is static all day.
  - pm_high (breakout resistance) is the max high of that symbol's
    Yahoo pre-market bars (04:00-09:29), since that's real premarket
    PRICE data we have -- unlike premarket VOLUME, which Yahoo reports
    as a flat 0 for every symbol (verified before writing this script).
  - opening_baseline_volume / intraday_health's avg_vol_baseline can't
    use real premarket volume for the same reason, so both are proxied
    from the symbol's OWN early regular-session pace (see
    _volume_baselines() below) rather than a true independent baseline.
    entry.require_opening_volume_expansion is forced OFF for both rule
    sets in this backtest (see FORCED_OVERRIDES below) -- verified
    against real data that an early-bars-average baseline is
    structurally biased: most of these names post their heaviest volume
    right at the open and decay afterward, so cumulative-vs-early-pace
    keeps falling below 1.0 as the session goes on regardless of whether
    anything unusual is happening, which isn't the real check's intent
    and would otherwise make the regime overrides look far harsher than
    they actually are. intraday_health's avg_vol_baseline (recent-window
    vs today's-own-average, not vs only the first 3 bars) doesn't have
    this bias and is left as-is.
  - No bid/ask quotes exist in this data, so evaluate_entry()'s spread
    check is skipped entirely (quote=None) for every call, both runs.
  - Exit uses a single ATR-at-entry trailing stop with no fade-
    confirmation grace layer (see config.json's stop.fade_confirmation)
    -- this is a conservative/pessimistic simplification: the live bot
    can forgive one stop touch, this backtest cannot, so its stop-outs
    are if anything a bit trigger-happier than reality.
  - Position sizing is a fixed $1000 notional per trade for every
    symbol, not risk_manager.py's real account-risk-percent sizing --
    needed to make aggregate dollar totals comparable across 145
    symbols without simulating account equity/exposure state.
  - Each symbol is simulated independently with no cross-symbol
    trading.max_positions cap and no shared account risk limits.
  - Scans only the first CLASSIFICATION_WINDOW_MINUTES minutes of the
    regular session for entry opportunities (see the constant below) --
    2026-09-04's real entries all landed within the first 61 minutes,
    and confirmation attempts logged through the rest of that day never
    once confirmed after 10:31, so this bound costs nothing observed
    while keeping runtime tractable.

Usage:
    python3 simulate_regime_strategies.py [YYYY-MM-DD]
"""

import csv
import json
import logging
import sys
from pathlib import Path
from statistics import mean

from indicators import atr
from intraday_health import compute_health
from entry_engine import evaluate_entry
from regime_strategies import get_regime_overrides
from regime_detector import bars_from_price_rows

# entry_engine.evaluate_entry() / intraday_health.compute_health() both log
# through the shared "sip_bot" logger tree (logger_setup.py), which writes
# to logs/sip_bot_<today's real date>.log -- the LIVE bot's production log
# file. This backtest calls those functions tens of thousands of times per
# run; left unsuppressed it silently floods today's real log with
# simulated CONFIRMATION/HEALTH lines indistinguishable from live activity
# (this happened once during development -- see git history / session
# notes). Suppress the whole tree before either module can emit anything.
logging.getLogger("sip_bot").setLevel(logging.CRITICAL + 1)

BASE_DIR = Path(__file__).resolve().parent
SCAN_WINDOW_MINUTES = 150          # first 2.5h of regular session -- see module docstring
NOTIONAL_PER_TRADE = 1000.0        # fixed $ per simulated trade, all symbols
EARLY_BASELINE_BARS = 3            # bars used to set each symbol's own "normal pace" baseline

# ATR-trail params -- mirrors config.json's stop.* (atr method), no fade-confirmation layer
STOP_ATR_PERIOD = 14
STOP_ATR_MULT_INITIAL = 1.2
STOP_ATR_MULT_TRAILING = 0.4
STOP_MIN_PCT = 1.0
STOP_MIN_CENTS = 0.03
STOP_MAX_PCT = 3.0

# Applied on top of EVERY run (baseline and regime alike) -- see module
# docstring's opening_baseline_volume note for why this check can't be
# faithfully simulated from this data and would otherwise bias results.
FORCED_OVERRIDES = {"require_opening_volume_expansion": False}


def load_snapshot(date: str):
    snap_dir = BASE_DIR / "data" / "snapshots" / date
    if not snap_dir.exists():
        raise FileNotFoundError(f"No snapshot at {snap_dir} -- run build_snapshot.py first.")
    regimes_path = snap_dir / "regimes.json"
    if not regimes_path.exists():
        raise FileNotFoundError(f"No {regimes_path} -- run simulate_regimes.py first.")
    regimes = json.loads(regimes_path.read_text())["symbols"]
    trades = {}
    trades_path = snap_dir / "trades.jsonl"
    if trades_path.exists():
        for line in trades_path.read_text().splitlines():
            line = line.strip()
            if line:
                t = json.loads(line)
                trades[t["symbol"]] = t
    return snap_dir, regimes, trades


def split_premarket_regular(bars: list):
    pm = [b for b in bars if b["t"].time().strftime("%H:%M") < "09:30"]
    regular = [b for b in bars if "09:30" <= b["t"].time().strftime("%H:%M") <= "16:00"]
    return pm, regular


def _stop_distance(entry_price: float, atr_value: float, mult: float) -> float:
    dist = mult * atr_value
    dist = max(dist, STOP_MIN_CENTS, entry_price * STOP_MIN_PCT / 100.0)
    dist = min(dist, entry_price * STOP_MAX_PCT / 100.0)
    return dist


def simulate_exit(entry_idx: int, entry_price: float, bars: list):
    """
    Walks forward from entry_idx+1 applying a ratchet-only ATR trailing
    stop. Returns (exit_idx, exit_price, exit_reason).
    """
    entry_bars = bars[:entry_idx + 1]
    a = atr(entry_bars, period=min(STOP_ATR_PERIOD, max(2, len(entry_bars) - 1)))
    initial_dist = _stop_distance(entry_price, a, STOP_ATR_MULT_INITIAL)
    trail_dist = _stop_distance(entry_price, a, STOP_ATR_MULT_TRAILING)

    current_stop = entry_price - initial_dist
    highest = entry_price

    for i in range(entry_idx + 1, len(bars)):
        b = bars[i]
        highest = max(highest, b["h"])
        candidate_stop = highest - trail_dist
        current_stop = max(current_stop, candidate_stop)
        if b["l"] <= current_stop:
            return i, current_stop, "TRAILING_STOP"

    return len(bars) - 1, bars[-1]["c"], "END_OF_DAY"


def _volume_baselines(regular_bars: list, i: int):
    """
    Proxy baselines built from the symbol's OWN early-session pace, in
    place of real premarket volume (unavailable -- see module docstring).
    Returns (opening_baseline_volume, health_avg_vol_baseline) as of bar i.
    """
    early = regular_bars[:EARLY_BASELINE_BARS]
    early_avg = mean(b["v"] for b in early) if early else (regular_bars[i]["v"] or 1.0)
    early_avg = early_avg or 1.0
    bars_elapsed = i + 1
    opening_baseline_volume = early_avg * bars_elapsed

    lookback = min(8, bars_elapsed)  # matches intraday_health's default slope_lookback_bars
    session_so_far = regular_bars[:i + 1]
    session_avg_per_bar = mean(b["v"] for b in session_so_far) or 1.0
    health_avg_vol_baseline = session_avg_per_bar * lookback

    return opening_baseline_volume, health_avg_vol_baseline


def run_one_ruleset(symbol: str, regular_bars: list, pm_high: float, cfg_override: dict):
    """
    Scans regular_bars up to SCAN_WINDOW_MINUTES for the first confirmed
    entry under this rule set (cfg_override=None for baseline, or a
    regime overrides dict). Returns a result dict, or None if never
    confirmed in the window.
    """
    premarket_result = {"pm_high": pm_high}
    limit = min(len(regular_bars), SCAN_WINDOW_MINUTES)
    effective_override = {**FORCED_OVERRIDES, **(cfg_override or {})}

    for i in range(EARLY_BASELINE_BARS, limit):
        live_bars = regular_bars[:i + 1]
        opening_baseline_volume, health_baseline = _volume_baselines(regular_bars, i)
        health_reading = compute_health(symbol, live_bars, health_baseline)

        decision = evaluate_entry(
            symbol, premarket_result, live_bars, None, opening_baseline_volume,
            health_reading=health_reading, is_reentry=False,
            cfg_override=effective_override,
        )
        if decision.should_enter:
            entry_price = live_bars[-1]["c"]
            exit_idx, exit_price, exit_reason = simulate_exit(i, entry_price, regular_bars)
            shares = NOTIONAL_PER_TRADE / entry_price
            pl = (exit_price - entry_price) * shares
            return {
                "entered": True,
                "entry_time": live_bars[-1]["t"].strftime("%H:%M:%S"),
                "entry_price": round(entry_price, 4),
                "exit_time": regular_bars[exit_idx]["t"].strftime("%H:%M:%S"),
                "exit_price": round(exit_price, 4),
                "exit_reason": exit_reason,
                "confirmation_score": decision.confirmation_score,
                "pl": round(pl, 2),
            }
    return {"entered": False}


def run(date: str):
    snap_dir, regimes, real_trades = load_snapshot(date)
    prices_dir = snap_dir / "prices"
    symbols = sorted(p.stem for p in prices_dir.glob("*.csv"))

    rows_out = []
    for sym in symbols:
        with open(prices_dir / f"{sym}.csv") as f:
            rows = list(csv.DictReader(f))
        bars = bars_from_price_rows(rows)
        pm_bars, regular_bars = split_premarket_regular(bars)
        if len(regular_bars) < EARLY_BASELINE_BARS + 1:
            continue
        pm_high = max((b["h"] for b in pm_bars), default=regular_bars[0]["h"])

        regime_info = regimes.get(sym, {})
        regime = regime_info.get("regime")
        overrides = get_regime_overrides(regime, force=True) if regime else {}

        baseline = run_one_ruleset(sym, regular_bars, pm_high, cfg_override=None)
        regime_result = (run_one_ruleset(sym, regular_bars, pm_high, cfg_override=overrides)
                          if overrides else baseline)

        rows_out.append({
            "symbol": sym,
            "regime": regime,
            "has_regime_rules": bool(overrides),
            "baseline": baseline,
            "regime_result": regime_result,
            "real_trade": real_trades.get(sym),
        })

    out_path = snap_dir / "regime_strategy_simulation.json"
    out_path.write_text(json.dumps(rows_out, indent=2))

    # ---- console summary ----
    def agg(results, key):
        entered = [r for r in results if r[key] and r[key]["entered"]]
        total_pl = sum(r[key]["pl"] for r in entered)
        wins = sum(1 for r in entered if r[key]["pl"] > 0)
        return entered, total_pl, wins

    print(f"Snapshot: {date}  ({len(rows_out)} symbols simulated, ${NOTIONAL_PER_TRADE:.0f} notional/trade)\n")

    base_entered, base_pl, base_wins = agg(rows_out, "baseline")
    print(f"BASELINE rules (no regime overrides) applied to all {len(rows_out)} symbols:")
    print(f"  trades taken: {len(base_entered)}   wins: {base_wins}   "
          f"win rate: {base_wins/len(base_entered)*100:.1f}%   total P/L: ${base_pl:.2f}"
          if base_entered else "  trades taken: 0")

    regime_entered, regime_pl, regime_wins = agg(rows_out, "regime_result")
    print(f"\nWITH regime-specific overrides where defined ({sum(1 for r in rows_out if r['has_regime_rules'])} "
          f"symbols affected):")
    print(f"  trades taken: {len(regime_entered)}   wins: {regime_wins}   "
          f"win rate: {regime_wins/len(regime_entered)*100:.1f}%   total P/L: ${regime_pl:.2f}"
          if regime_entered else "  trades taken: 0")

    print(f"\nDelta vs baseline: {len(regime_entered) - len(base_entered):+d} trades, "
          f"${regime_pl - base_pl:+.2f} P/L")

    # per-regime breakdown, only for regimes that actually carry overrides
    print("\nPer-regime detail (only regimes with independent rules defined):")
    for regime_name in sorted({r["regime"] for r in rows_out if r["has_regime_rules"]}):
        subset = [r for r in rows_out if r["regime"] == regime_name]
        b_entered, b_pl, b_wins = agg(subset, "baseline")
        r_entered, r_pl, r_wins = agg(subset, "regime_result")
        print(f"  {regime_name}  ({len(subset)} symbols)")
        print(f"    baseline : {len(b_entered):3} trades, ${b_pl:8.2f} P/L, {b_wins} win(s)")
        print(f"    regime   : {len(r_entered):3} trades, ${r_pl:8.2f} P/L, {r_wins} win(s)")

    # cross-check against today's real trades
    print("\nReal 2026-09-04 trades vs this backtest's baseline replay (sanity check):")
    for r in rows_out:
        if not r["real_trade"]:
            continue
        rt = r["real_trade"]
        b = r["baseline"]
        sim_status = f"entered {b['entry_time']} @ ${b['entry_price']}" if b["entered"] else "NOT entered by baseline replay"
        print(f"  {r['symbol']:6} real: entry {rt['entry_time'][11:19]} @ ${rt['entry_price']:.2f} "
              f"P/L ${rt['current_pl']:+.2f}   |   backtest baseline: {sim_status}")

    print(f"\nWrote {out_path.relative_to(BASE_DIR)}")


if __name__ == "__main__":
    date = sys.argv[1] if len(sys.argv) > 1 else "2026-09-04"
    run(date)
