"""
entry_v2.py -- [2026-09-28] The user's entry rules v2 (dictated 9/28 evening), a
switchable alternative to setup J's smart_engine (config smart_engine.entry_mode:
"j" = smart_engine as before, "v2" = this). Exits are unchanged (J's).

Checked every poll, in the user's order (all must pass):
  0. timing      >= min_bars finished 1-min bars today (15 -> no buys before ~9:45)
  1. volume      RVOL so far (today's volume / 14-day average volume by this same
                 minute) >= min_rvol; and the last 15 minutes' volume is not a spike
                 (<= max_vol15_x its 14-day normal for those minutes)
  2. resistance  room to the nearest 14-day resistance band >= min_room_pct, or price
                 is above the 14-day high (breakout)
  3. buy vs sell buy share = buy / (buy + sell) over the last 15 minutes >= min_buy_share;
                 and not "falling on net selling" (price down >= 1% in 30 min with
                 net selling)
  4. steady rise 15-bar smoothness (R^2 of closes, rising) >= min_r2 and straightness
                 (net move / path, 15 bars) >= min_eff
  spread         bid/ask spread <= max_spread_pct of the mid (as setup J; no quote -> passes)
  position       price above VWAP but no more than max_vwap_pct above it
  don't buy      5-bar acceleration >= max_acc5 x 1-min ATR (spike), or stretched:
                 move from the open >= stretch_move_x its 14-day normal AND >= 4% over VWAP
  confirm        all of the above on `confirm_checks` polls in a row (~15 s)
Stop: under the nearest 14-day support band (its low - 1c), at least min_stop_pct
below the price and at most max_stop_pct; otherwise min_stop_pct.

The 14-day reference comes from reference.py (data/reference/<date>/<SYM>.json).
Returns smart_engine.SmartEngineDecision so monitor.py / simulate.py handle it
exactly like setup J's decisions (metrics["stage3"]["stop"], metrics["price"]).
"""
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

import volatility
from smart_engine import SmartEngineDecision

ET = ZoneInfo("America/New_York")

DEFAULTS = {
    "min_bars": 15,
    "min_rvol": 1.5,
    "max_vol15_x": 5.0,
    "min_room_pct": 1.0,
    "min_buy_share": 0.55,
    "min_r2": 0.7,
    "min_eff": 0.5,
    "max_vwap_pct": 2.0,
    "max_acc5": 1.5,
    "stretch_move_x": 3.0,
    "stretch_vwap_pct": 4.0,
    "confirm_checks": 3,
    "min_stop_pct": 1.0,
    "max_stop_pct": 3.0,
    "max_spread_pct": 1.0,
}


def _minute_index(ts) -> int:
    t = ts.astimezone(ET)
    return (t.hour - 9) * 60 + t.minute - 30


def _r2_eff(closes):
    n = len(closes)
    xm, ym = (n - 1) / 2, sum(closes) / n
    sxx = sum((k - xm) ** 2 for k in range(n))
    slope = sum((k - xm) * (y - ym) for k, y in enumerate(closes)) / sxx
    sst = sum((y - ym) ** 2 for y in closes)
    r2 = (1 - sum((y - (ym + slope * (k - xm))) ** 2 for k, y in enumerate(closes)) / sst) if sst else 0.0
    steps = [closes[i + 1] - closes[i] for i in range(n - 1)]
    path = sum(abs(s) for s in steps)
    return r2 * (1 if slope > 0 else -1), ((closes[-1] - closes[0]) / path if path else 0.0)


def _atr(bars, period=14):
    a = None
    for p, b in zip(bars, bars[1:]):
        tr = max(b["h"] - b["l"], abs(b["h"] - p["c"]), abs(b["l"] - p["c"]))
        a = tr if a is None else a + (tr - a) / period
    return a


def evaluate(symbol: str, bars: list, ref: dict, flow15: tuple, persistence: dict,
             as_of: datetime = None, cfg: dict = None, quote: dict = None) -> SmartEngineDecision:
    """bars: today's 1-min bars incl. the forming one ({"t","o","h","l","c","v"});
    ref: reference.load(...) for today; flow15: (buy_vol, sell_vol) over the last 15 min."""
    c = {**DEFAULTS, **(cfg or {})}
    now = as_of or datetime.now(timezone.utc)
    d = SmartEngineDecision(symbol=symbol)
    against, for_ = [], []
    price = bars[-1]["c"] if bars else None
    m = {"price": price, "entry_mode": "v2"}
    d.metrics = m
    d.persistence = {}
    done = bars[:-1]
    if price is None or len(done) < c["min_bars"]:
        d.state = "WAIT"
        d.reasons_against = [f"{len(done)} of {c['min_bars']} bars"]
        return d
    if not ref:
        d.state = "WAIT"
        d.reasons_against = ["no 14-day reference file"]
        return d
    mi = min(max(_minute_index(bars[-1]["t"]), 0), 389)
    day_open = bars[0]["o"]
    pv = sum((b["h"] + b["l"] + b["c"]) / 3 * b["v"] for b in bars)
    vol = sum(b["v"] for b in bars)
    vwap = pv / vol if vol else price
    vwap_pct = (price / vwap - 1) * 100
    m.update(vwap=round(vwap, 4), vwap_pct=round(vwap_pct, 3))

    # 1. unusual volume
    rvol = vol / max(ref["cum_vol"][mi], 1)
    last15 = done[-15:]
    norm15 = sum(ref["vol_per_min"][min(max(_minute_index(b["t"]), 0), 389)] for b in last15)
    vol15_x = sum(b["v"] for b in last15) / norm15 if norm15 else None
    m.update(minute=mi, vol_today=vol, ref_cum_vol=ref["cum_vol"][mi], ref_vol15=round(norm15),
             rvol_sofar=round(rvol, 3), vol15_x=round(vol15_x, 3) if vol15_x is not None else None)
    if rvol < c["min_rvol"]:
        against.append(f"1 volume: RVOL so far {rvol:.2f}x < {c['min_rvol']}x")
    if vol15_x is not None and vol15_x > c["max_vol15_x"]:
        against.append(f"1 volume: last-15-min spike {vol15_x:.1f}x > {c['max_vol15_x']}x")

    # 2. price vs 14-day resistance
    sp = volatility.sr_position(ref.get("sr14") or {}, price) if ref.get("sr14") else {}
    room = sp.get("dist_to_resistance_pct")
    above_high = bool(sp.get("above_14d_high"))
    m["sr14"] = sp
    if not above_high and room is not None and room < c["min_room_pct"]:
        against.append(f"2 resistance: only {room:.2f}% to {sp['resistance']['lo']:.2f} "
                       f"({sp['resistance']['touches']} touches)")

    # 3. buy vs sell volume (last 15 minutes)
    bv, sv = flow15
    share = bv / (bv + sv) if bv + sv else None
    ret30 = (price / done[-30]["c"] - 1) * 100 if len(done) >= 30 else None
    m.update(buy_vol15=bv, sell_vol15=sv, buy_share15=round(share, 3) if share is not None else None,
             ret30=round(ret30, 3) if ret30 is not None else None)
    if share is None or share < c["min_buy_share"]:
        against.append(f"3 buy/sell: buy share {share if share is None else round(share * 100)}% < {c['min_buy_share'] * 100:.0f}%")
    if ret30 is not None and ret30 <= -1 and share is not None and share < 0.5:
        against.append("3 buy/sell: falling on net selling")

    # 4. steady rise
    r2, eff = _r2_eff([b["c"] for b in done[-15:]])
    m.update(r2_15=round(r2, 3), eff15=round(eff, 3))
    if r2 < c["min_r2"]:
        against.append(f"4 steady rise: smoothness {r2:.2f} < {c['min_r2']}")
    if eff < c["min_eff"]:
        against.append(f"4 steady rise: straightness {eff:.2f} < {c['min_eff']}")

    # spread (safety guard, as setup J)
    if quote and quote.get("bid") and quote.get("ask") and quote["ask"] >= quote["bid"] > 0:
        mid = (quote["bid"] + quote["ask"]) / 2
        spread_pct = (quote["ask"] - quote["bid"]) / mid * 100
        m.update(bid=quote["bid"], ask=quote["ask"], spread_pct=round(spread_pct, 3))
        if spread_pct > c["max_spread_pct"]:
            against.append(f"spread: {spread_pct:.2f}% > {c['max_spread_pct']}%")

    # position vs VWAP
    if not (0 <= vwap_pct <= c["max_vwap_pct"]):
        against.append(f"VWAP: {vwap_pct:+.2f}% (needs 0 to +{c['max_vwap_pct']}%)")

    # don't-buy warnings
    atr = _atr(done[-30:])
    cs = [b["c"] for b in done[-6:]]
    if atr and len(cs) == 6:
        steps = [cs[i + 1] - cs[i] for i in range(5)]
        acc5 = (sum(steps[2:]) / 3 - sum(steps[:2]) / 2) / atr
        m["acc5"] = round(acc5, 3)
        if acc5 >= c["max_acc5"]:
            against.append(f"warning: acceleration spike {acc5:.2f} ATR")
    move_x = (price / day_open - 1) * 100 / max(ref["abs_move_pct"][mi], 0.05)
    m.update(move_x=round(move_x, 3), day_open=day_open, ref_abs_move_pct=ref["abs_move_pct"][mi])
    if move_x >= c["stretch_move_x"] and vwap_pct >= c["stretch_vwap_pct"]:
        against.append(f"warning: stretched ({move_x:.1f}x normal move, {vwap_pct:+.1f}% over VWAP)")

    # stop: under the nearest 14-day support, 1%..3% below
    stop = price * (1 - c["min_stop_pct"] / 100)
    sup = sp.get("support")
    if sup:
        cand = sup["lo"] - 0.01
        if price * (1 - c["max_stop_pct"] / 100) <= cand <= stop:
            stop = cand
    m["stage3"] = {"stop": round(stop, 4)}

    # confirmation: all rules on N polls in a row
    if against:
        d.state = "REJECT"
        d.reasons_against = against
        return d
    n = (persistence or {}).get("ok", 0) + 1
    d.persistence = {"ok": n}
    for_ = [f"v2: RVOL {rvol:.1f}x, room {'above 14d high' if above_high else f'{room:.1f}%' if room is not None else 'n/a'}, "
            f"buy share {share * 100:.0f}%, smoothness {r2:.2f}, straightness {eff:.2f}, VWAP {vwap_pct:+.1f}%, stop {stop:.2f}"]
    d.reasons_for = for_
    if n < c["confirm_checks"]:
        d.state = "WAIT"
        d.reasons_against = [f"confirming {n}/{c['confirm_checks']}"]
        return d
    d.state = "BUY"
    d.should_enter = True
    return d
