"""
scorer.py

Transparent scoring engine. Every score comes with a `breakdown` dict
showing exactly how many points each factor contributed, so a stock's
rank is always explainable — never a black box, per project requirement.

Entry point:
    score_premarket_candidate(symbol, bars, pm_high, pm_low, avg_vol_baseline)
        -> used by premarket_scanner.py for the single full-universe scan
           at schedule.premarket_scan_time (see monitor.py's module
           docstring -- the 09:00-09:25 development-monitoring window and
           09:25 final-scoring review that used to call this repeatedly
           per symbol were eliminated 2026-09-01)

All weights and thresholds come from config.json under
"premarket_scoring" — nothing here is hard-coded strategy tuning.
"""

from config_loader import get_config
from indicators import (
    vwap, vwap_slope, relative_volume, volume_acceleration, price_momentum,
    is_higher_highs_higher_lows, consolidation_tightness, distance_from_high_pct,
    extension_from_vwap_pct, spread_pct, atr, ema, normalized_slope_pct,
    pullback_then_continuation,
)
# [BUGFIX 2026-09-15] Reuse fast_prediction_engine's own resistance/
# extension/momentum classifiers instead of scorer.py's independent,
# differently-thresholded versions of the same ideas -- see this
# module's scoring below for the real-world case this fixes (the
# scanner used to give FULL credit to a stock sitting right at its
# premarket high, while the entry gate treats that as TOO_CLOSE and
# rejects it outright unless a breakout is actively confirmed). Calling
# fast_prediction_engine's actual functions (not reimplementing them)
# guarantees "ranks well here" and "passes the entry gate" stay the
# same question by construction, with zero drift risk going forward.
# Deliberately does NOT import or touch anything in fast_entry_gate.py
# or fast_prediction_engine.py's own live decision path -- this is a
# one-directional dependency (scorer reads their pure classifiers),
# so the live entry engine is completely unaffected by this change.
from fast_prediction_engine import (
    DEFAULT_CONFIG as _FPE_DEFAULT_CONFIG,
    _resistance_classification, _breakout_in_progress, _extension_classification,
    _momentum_state, RESISTANCE_SAFE, RESISTANCE_CAUTION,
    EXTENSION_NORMAL, EXTENSION_EXTENDED, EXTENSION_SEVERE,
)


def _clamp(x, lo=0.0, hi=100.0):
    return max(lo, min(hi, x))


def score_premarket_candidate(symbol: str, bars: list, pm_high: float, pm_low: float,
                               avg_vol_baseline: float, bid: float = None, ask: float = None,
                               daily_atr_pct: float = None) -> dict:
    """
    bars: list of 1-min premarket bars, oldest first, each
          {"t","o","h","l","c","v"}
    pm_high / pm_low: premarket high/low so far
    avg_vol_baseline: 20-day average volume (or similar) used for RVOL
    bid/ask: latest quote, optional (affects spread/liquidity subscore)
    daily_atr_pct: [FEATURE 2026-09-15] this symbol's own trailing-30-day
        average daily true range, as a % of price -- from premarket_
        scanner.py's compute_volatility_baseline(). None (the default,
        for any caller that doesn't have it yet) means "unknown," which
        NEVER rejects a candidate -- only a confirmed-low reading does.
        This is a structural liquidity/character check (is this even
        capable of the kind of move a day trade needs), completely
        separate from today's intraday extension/resistance scoring
        above -- see config.json's universe.min_daily_atr_pct for the
        real-world BDC/SPAC cases (WHF, APXT) that motivated it.

    Returns: {"symbol", "total_score", "breakdown": {...}, "flags": {...}}
    """
    cfg = get_config()["premarket_scoring"]
    w = cfg["weights"]

    if not bars:
        return {"symbol": symbol, "total_score": 0.0, "breakdown": {}, "flags": {"no_data": True}}

    closes = [b["c"] for b in bars]
    volumes = [b["v"] for b in bars]
    current_price = closes[-1]
    total_volume = sum(volumes)

    v_wap = vwap(bars)
    v_slope = vwap_slope(bars, lookback=min(cfg["vwap_lookback_bars"], max(1, len(bars) - 1)))
    rvol = relative_volume(total_volume, avg_vol_baseline)
    vol_accel = volume_acceleration(volumes)
    momentum = price_momentum(closes[-cfg["momentum_lookback_snapshots"]:] if len(closes) >= cfg["momentum_lookback_snapshots"] else closes)
    hh_hl = is_higher_highs_higher_lows(bars, lookback=min(4, len(bars)))
    tightness = consolidation_tightness(bars)
    dist_from_high = distance_from_high_pct(current_price, pm_high)
    extension = extension_from_vwap_pct(current_price, v_wap)
    spr = spread_pct(bid, ask) if bid and ask else 0.5  # neutral assumption if no quote

    # [BUGFIX 2026-09-15] Premarket-bar analogs of the inputs
    # fast_prediction_engine's classifiers need, computed with the same
    # indicators.py math stream_features.py itself uses (this module's
    # own docstring: "Shared by scorer.py ... and stream_features.py ...
    # so both stages use identical math"). atr_pct/ema9 feed the
    # ATR-normalized extension check below; slope_5m_proxy/momentum_
    # accel_proxy are a premarket-bar stand-in for stream_features'
    # live slope_5m/momentum_acceleration (same "5-bar window,
    # recent-half-vs-earlier-half" shape, just computed from 1-min
    # premarket bars instead of live sub-minute ticks) -- close enough
    # to classify Strong/Building/Weakening/Deteriorating consistently
    # with how the entry gate will read the same stock once it's live.
    atr_val = atr(bars, period=min(14, max(2, len(bars) - 1)))
    atr_pct_val = (atr_val / current_price * 100.0) if current_price and atr_val else None
    ema9_val = ema(closes, 9)
    window = closes[-6:] if len(closes) >= 6 else closes
    slope_5m_proxy = normalized_slope_pct(window)
    if len(closes) >= 6:
        momentum_accel_proxy = normalized_slope_pct(closes[-3:]) - normalized_slope_pct(closes[-6:-3])
    else:
        momentum_accel_proxy = 0.0
    momentum_state_proxy = _momentum_state(slope_5m_proxy, momentum_accel_proxy)
    fpe_cfg = {**_FPE_DEFAULT_CONFIG, **get_config().get("fast_prediction", {})}

    breakdown = {}

    # --- Price quality: is price actually inside the $5-$15 tradable band with room? ---
    price_min = get_config()["universe"]["price_min"]
    price_max = get_config()["universe"]["price_max"]
    if price_min <= current_price <= price_max:
        # Favor the middle of the band slightly (avoids edge-of-range illiquid names)
        mid = (price_min + price_max) / 2
        band_half = (price_max - price_min) / 2
        closeness = 1 - abs(current_price - mid) / band_half
        breakdown["price_quality"] = w["price_quality"] * max(0.3, closeness)
    else:
        breakdown["price_quality"] = 0.0

    # --- Volume quality: absolute premarket volume vs minimum threshold ---
    min_vol = cfg["min_premarket_volume"]
    if total_volume >= min_vol:
        vol_ratio = min(total_volume / (min_vol * 5), 1.0)  # saturate at 5x minimum
        breakdown["volume_quality"] = w["volume_quality"] * vol_ratio
    else:
        breakdown["volume_quality"] = w["volume_quality"] * (total_volume / min_vol) * 0.3

    # --- RVOL ---
    min_rvol = cfg["min_rvol"]
    if rvol >= min_rvol:
        rvol_ratio = min(rvol / (min_rvol * 3), 1.0)
        breakdown["rvol"] = w["rvol"] * rvol_ratio
    else:
        breakdown["rvol"] = w["rvol"] * (rvol / min_rvol) * 0.3 if min_rvol > 0 else 0.0

    # --- Volume acceleration: rewards developing volume, not one spike ---
    accel_min = cfg["acceleration_min_ratio"]
    if vol_accel >= accel_min:
        accel_ratio = min((vol_accel - 1.0) / 1.0, 1.0)
        breakdown["volume_acceleration"] = w["volume_acceleration"] * _clamp(accel_ratio, 0, 1)
    else:
        breakdown["volume_acceleration"] = w["volume_acceleration"] * _clamp(vol_accel / accel_min, 0, 1) * 0.4

    # --- Price momentum ---
    breakdown["price_momentum"] = w["price_momentum"] * _clamp(momentum / 8.0, 0, 1) if momentum > 0 else 0.0

    # --- Price structure: a clean stairstep (hh_hl) earns full credit;
    # a healthy pullback-then-continuation ("coil back") earns partial
    # credit instead of being punished as hard as a stock with no
    # structure at all -- this is exactly the setup fast_prediction_
    # engine's own VWAP_RECLAIM / EMA9 "Reclaiming" states are built to
    # reward once the session opens, so the scanner shouldn't be
    # filtering it out before that ever gets a chance to matter.
    # [BUGFIX 2026-09-15] pullback_then_continuation() already existed
    # in indicators.py for exactly this pattern but was never wired
    # into scoring here.
    coiled_recovery = pullback_then_continuation(
        bars, max_retrace_pct=cfg.get("coil_max_retrace_pct", 50.0))
    if hh_hl:
        breakdown["price_structure"] = w["price_structure"]
    elif coiled_recovery:
        breakdown["price_structure"] = w["price_structure"] * 0.7
    else:
        breakdown["price_structure"] = w["price_structure"] * 0.2

    # --- VWAP structure: price above VWAP is healthy ---
    breakdown["vwap_structure"] = w["vwap_structure"] if current_price > v_wap else w["vwap_structure"] * 0.15

    # --- VWAP slope: rising VWAP is healthy ---
    breakdown["vwap_slope"] = w["vwap_slope"] if v_slope > 0 else w["vwap_slope"] * 0.1

    # --- Consolidation quality: tight range = healthier base ---
    # Lower tightness % is better, scale so <=3% is full credit, >=15% is zero.
    consolidation_score = _clamp((15 - tightness) / (15 - 3), 0, 1)
    breakdown["consolidation_quality"] = w["consolidation_quality"] * consolidation_score

    # --- Room below resistance (was "distance from PM high") ---
    # [BUGFIX 2026-09-15] This used to give FULL credit to a stock
    # sitting right at (0% away from) its premarket high -- treating
    # "at the ceiling" as strength. fast_entry_gate.py requires the
    # OPPOSITE: real room below the nearest resistance level before it
    # will even call direction UP (RESISTANCE_TOO_CLOSE is a hard
    # REJECT unless a breakout is actively confirmed). Real case: WHF/
    # VEEA/TNON all got ranked into the shortlist under the old logic
    # and then burned their slot getting rejected for exactly this.
    # Now reuses fast_prediction_engine's own resistance classifier
    # (same pct/ATR-relative thresholds, same breakout-in-progress
    # exception) against pm_high -- the only resistance reference
    # available before the session has a real session_high of its own.
    if pm_high and pm_high > current_price:
        dist_to_resistance = (pm_high - current_price) / pm_high * 100.0
        breakout_in_progress = _breakout_in_progress(bars, pm_high, vol_accel, fpe_cfg)
    else:
        dist_to_resistance = None  # already at/through the premarket high -- clear air
        breakout_in_progress = False
    resistance_state = _resistance_classification(
        dist_to_resistance, atr_pct_val, breakout_in_progress, fpe_cfg)
    if resistance_state == RESISTANCE_SAFE:
        resistance_score = 1.0
    elif resistance_state == RESISTANCE_CAUTION:
        resistance_score = 0.5
    else:
        resistance_score = 0.1
    breakdown["distance_from_pm_high"] = w["distance_from_pm_high"] * resistance_score

    # --- Spread / liquidity ---
    max_spread = cfg["max_spread_pct"]
    spread_score = _clamp((max_spread - spr) / max_spread, 0, 1)
    breakdown["spread_liquidity"] = w["spread_liquidity"] * spread_score

    # --- Momentum consistency: penalize choppy back-and-forth closes ---
    if len(closes) >= 3:
        diffs = [closes[i] - closes[i - 1] for i in range(1, len(closes))]
        same_direction = sum(1 for d in diffs if d > 0)
        consistency = same_direction / len(diffs)
        breakdown["momentum_consistency"] = w["momentum_consistency"] * consistency
    else:
        breakdown["momentum_consistency"] = w["momentum_consistency"] * 0.5

    # --- Extension risk ---
    # [BUGFIX 2026-09-15] Was a flat 25%-from-VWAP cutoff (extension_
    # risk_pct_threshold), unrelated to the ATR-normalized distance
    # fast_prediction_engine actually checks (extension_atr_extended/
    # extension_atr_severe, distance from EMA9 or VWAP in ATR units).
    # A stock could clear this flat threshold while already reading
    # EXTENDED/SEVERELY_EXTENDED live, or the reverse for a low-ATR
    # name. Now reuses the entry gate's own classifier so "not extended"
    # here means the same thing it will mean a few minutes later.
    price_vs_ema9_atr = ((current_price - ema9_val) / atr_val) if atr_val else None
    vwap_distance_atr = ((current_price - v_wap) / atr_val) if atr_val else None
    extension_state = _extension_classification(
        price_vs_ema9_atr, vwap_distance_atr, momentum_state_proxy, fpe_cfg)
    if extension_state == EXTENSION_SEVERE:
        breakdown["extension_risk_penalty"] = -w["extension_risk_penalty"]
    elif extension_state == EXTENSION_EXTENDED:
        breakdown["extension_risk_penalty"] = -w["extension_risk_penalty"] * 0.4
    else:
        breakdown["extension_risk_penalty"] = 0.0

    total = sum(breakdown.values())
    max_possible = sum(v for k, v in w.items() if k != "extension_risk_penalty")
    normalized = _clamp(total / max_possible * 100.0, 0, 100)

    # [FEATURE 2026-09-15] "unknown" (daily_atr_pct is None) always
    # passes -- only a CONFIRMED-low reading rejects, same fail-open
    # contract as prev_day_high's absence never blocking a symbol.
    min_daily_atr_pct = get_config()["universe"]["min_daily_atr_pct"]
    meets_min_volatility = daily_atr_pct is None or daily_atr_pct >= min_daily_atr_pct

    flags = {
        "meets_min_volume": total_volume >= min_vol,
        "meets_min_rvol": rvol >= min_rvol,
        "meets_min_volatility": meets_min_volatility,
        "spread_ok": spr <= max_spread,
        "extended": extension_state != EXTENSION_NORMAL,
        "hh_hl_structure": hh_hl,
        "coiled_recovery": coiled_recovery,
        "resistance_state": resistance_state,
        "price_above_vwap": current_price > v_wap,
        "vwap_rising": v_slope > 0,
    }

    return {
        "symbol": symbol,
        "total_score": round(normalized, 2),
        "raw_score": round(total, 2),
        "breakdown": {k: round(v, 2) for k, v in breakdown.items()},
        "metrics": {
            "price": current_price,
            "vwap": round(v_wap, 4),
            "vwap_slope": round(v_slope, 4),
            "rvol": round(rvol, 2),
            "volume_acceleration": round(vol_accel, 2),
            "premarket_volume": total_volume,
            "momentum_pct": round(momentum, 2),
            "consolidation_tightness_pct": round(tightness, 2),
            "distance_from_pm_high_pct": round(dist_from_high, 2),
            "extension_from_vwap_pct": round(extension, 2),
            "spread_pct": round(spr, 2),
            "daily_atr_pct": round(daily_atr_pct, 2) if daily_atr_pct is not None else None,
        },
        "flags": flags,
    }
