"""
fast_prediction_engine.py

[FEATURE 2026-09-11] Replacement prediction stage for the
"fast_prediction" decision engine (config.json's entry.decision_engine),
built after 2026-09-10's live session produced ZERO trades despite HGTY
reaching a 100%-confidence BREAKOUT_RETEST setup at least 8 separate
times across the day. Root cause traced to structure_engine.py's
structure_score needing ~20-40 minutes of real bars before a swing pivot
can even confirm (pivot_confirm_bars' honest causal lag), colliding with
entry_score.py's extension-cliff demanding a full 100 on it anyway.

Per explicit design decision reached over that review: don't recreate
the same trend_score/structure_score/entry_score/risk_score/
prediction_score sprawl. Instead classify a SMALL set of signals --
almost all of them already computed live by stream_features.py, just
not consumed this way yet -- into one descriptive prediction state:
Direction (UP/DOWN/NEUTRAL) + Confidence, plus named classifications
for logging/review (momentum, volume, vwap, ema9, resistance,
extension).

CONFIDENCE IS DESCRIPTIVE ONLY. Per explicit instruction: "Confidence
describes the prediction. It does not control the trade by itself."
This module never decides should_enter -- that is entirely
fast_entry_gate.py's job (hard/soft checklist + a short live-persistence
window). Nothing here is a floor gate.

WHY THIS NEEDED LESS NEW CODE THAN IT LOOKED LIKE: stream_features.py's
slope.slope_5m IS already "price slope over the last 5 one-minute bars"
(see stream_features.py's _horizon_slope_pct), and its
ema_slope_lookback_bars config already defaults to 5 -- the "5-bar
rolling window" this design calls for was already being computed on
every poll cycle, just not read this way. This module is a thin
classifier over already-live data, not a new math layer.
"""

from dataclasses import dataclass, field

from config_loader import get_config
from stream_features import (
    VWAP_STRONG_SUPPORT, VWAP_RECLAIM, VWAP_HOLD, VWAP_EXTENSION,
    VWAP_FAILURE, VWAP_DECLINING, VWAP_FLAT,
)

DIRECTION_UP = "UP"
DIRECTION_DOWN = "DOWN"
DIRECTION_NEUTRAL = "NEUTRAL"

RESISTANCE_SAFE = "SAFE"
RESISTANCE_CAUTION = "CAUTION"
RESISTANCE_TOO_CLOSE = "TOO_CLOSE"

EXTENSION_NORMAL = "NORMAL"
EXTENSION_EXTENDED = "EXTENDED"
EXTENSION_SEVERE = "SEVERELY_EXTENDED"

_VWAP_BULLISH = {VWAP_STRONG_SUPPORT, VWAP_RECLAIM, VWAP_HOLD, VWAP_EXTENSION}
_VWAP_BEARISH = {VWAP_FAILURE, VWAP_DECLINING}

DEFAULT_CONFIG = {
    "min_bars": 5,  # warm-up: the rolling 5-bar window this whole module is built on
    "min_relative_volume": 0.5,
    # [resistance] "distance to resistance as a % of price AND relative to
    # current volatility" -- SAFE if EITHER the flat % or the ATR-relative
    # distance clears its bar (a calm stock leans on the %, a volatile one
    # on the ATR multiple), per explicit instruction.
    "resistance_safe_pct": 0.75,
    "resistance_safe_atr_mult": 0.5,
    "resistance_caution_pct": 0.40,
    # breakout-in-progress override: price has touched/pierced the
    # resistance level intrabar within the last 2 bars AND volume is
    # accelerating at least this much -- waives the distance penalty
    # entirely, per explicit instruction ("the resistance-distance
    # penalty should disappear once the breakout is confirmed").
    "breakout_override_lookback_bars": 2,
    "breakout_override_volume_accel": 1.2,
    # [extension] ATR-normalized distance from EMA9/VWAP, reusing
    # stream_features.py's already-computed price_vs_ema9_atr /
    # vwap_distance_atr rather than inventing new math. EXTENDED is a
    # confidence penalty; SEVERELY_EXTENDED (extra: momentum already
    # weakening/deteriorating) is fast_entry_gate.py's hard reject.
    "extension_atr_extended": 2.0,
    "extension_atr_severe": 3.5,
    "confidence_weights": {"momentum": 0.30, "volume": 0.25, "vwap": 0.25, "ema": 0.20},
}

_MOMENTUM_SCORE = {"Strong": 1.0, "Building": 0.7, "Weakening": 0.2, "Deteriorating": 0.0, "Neutral": 0.5}
_VOLUME_SCORE = {"Expanding": 1.0, "Steady": 0.6, "Declining": 0.1, "Neutral": 0.5}
_VWAP_SCORE = {"Bullish": 1.0, "Neutral": 0.5, "Bearish": 0.0}
_EMA_SCORE = {"Bullish": 1.0, "Reclaiming": 0.6, "Fading": 0.4, "Bearish": 0.0, "Neutral": 0.5}


@dataclass
class FastPredictionReading:
    symbol: str
    direction: str = DIRECTION_NEUTRAL
    confidence: float = 0.0
    momentum: str = "Neutral"
    volume_state: str = "Neutral"
    vwap_state: str = "Neutral"
    ema9_state: str = "Neutral"
    resistance: str = RESISTANCE_SAFE
    resistance_level: float = None
    resistance_source: str = None
    resistance_distance_pct: float = None
    breakout_in_progress: bool = False
    extension: str = EXTENSION_NORMAL
    reasons: list = field(default_factory=list)
    insufficient_data: bool = False


def _momentum_state(slope_5m, momentum_acceleration):
    """slope_5m = price slope over the last 5 one-minute bars (already
    computed by stream_features.py -- see this module's docstring).
    momentum_acceleration = stream_features' cross-horizon waterfall
    (shorter horizons moving further/faster than the 5m one = building)."""
    if slope_5m is None:
        return "Neutral"
    accelerating = momentum_acceleration is not None and momentum_acceleration > 0
    if slope_5m > 0:
        return "Strong" if accelerating else "Weakening"
    return "Building" if accelerating else "Deteriorating"


def _volume_state(volume_acceleration, relative_volume, cfg):
    if volume_acceleration is None or relative_volume is None:
        return "Neutral"
    if relative_volume >= cfg["min_relative_volume"] and volume_acceleration > 1.05:
        return "Expanding"
    if volume_acceleration < 0.95:
        return "Declining"
    return "Steady"


def _vwap_state(classification):
    if classification in _VWAP_BULLISH:
        return "Bullish"
    if classification in _VWAP_BEARISH:
        return "Bearish"
    return "Neutral"


def _ema9_state(price_vs_ema9_pct, ema9_slope):
    """Three-tier EMA relationship per explicit instruction: price-vs-EMA9
    is the immediate state, EMA9's own slope is the momentum tell (price
    can still read above a FLATTENING EMA9 -- that's "Fading", the exact
    "still green but momentum already deteriorating" case this whole
    review was about)."""
    if price_vs_ema9_pct is None:
        return "Neutral"
    rising = ema9_slope is not None and ema9_slope > 0
    if price_vs_ema9_pct > 0:
        return "Bullish" if rising else "Fading"
    return "Reclaiming" if rising else "Bearish"


def _resistance_context(price, bars, premarket_result, cfg):
    """Nearest active resistance ABOVE current price, checked in the
    explicit priority order: session high -> premarket high ->
    previous-day high. previous-day high is optional/fault-tolerant --
    its absence just removes it from consideration, never blocks
    anything (see premarket_scanner.py's prev_day_high_out docstring)."""
    session_high = max((b["h"] for b in bars), default=None) if bars else None
    pm_high = (premarket_result or {}).get("pm_high")
    prev_day_high = (premarket_result or {}).get("prev_day_high")

    for name, level in (("session_high", session_high), ("premarket_high", pm_high),
                        ("previous_day_high", prev_day_high)):
        if level and level > price:
            return name, level, (level - price) / price * 100.0
    return None, None, None


def _breakout_in_progress(bars, resistance_level, volume_acceleration, cfg):
    if resistance_level is None or not bars:
        return False
    lookback = min(cfg["breakout_override_lookback_bars"], len(bars))
    recent_high = max(b["h"] for b in bars[-lookback:])
    touched_or_through = recent_high >= resistance_level * 0.999
    accelerating = volume_acceleration is not None and volume_acceleration >= cfg["breakout_override_volume_accel"]
    return touched_or_through and accelerating


def _resistance_classification(distance_pct, atr_pct, breakout_in_progress, cfg):
    if distance_pct is None:
        return RESISTANCE_SAFE  # nothing overhead -- clear air
    if breakout_in_progress:
        return RESISTANCE_SAFE  # per explicit instruction: don't penalize the breakout itself
    atr_safe_pct = cfg["resistance_safe_atr_mult"] * atr_pct if atr_pct else None
    if distance_pct >= cfg["resistance_safe_pct"] or (atr_safe_pct and distance_pct >= atr_safe_pct):
        return RESISTANCE_SAFE
    if distance_pct >= cfg["resistance_caution_pct"]:
        return RESISTANCE_CAUTION
    return RESISTANCE_TOO_CLOSE


def _extension_classification(price_vs_ema9_atr, vwap_distance_atr, momentum, cfg):
    dists = [abs(x) for x in (price_vs_ema9_atr, vwap_distance_atr) if x is not None]
    if not dists:
        return EXTENSION_NORMAL
    max_dist = max(dists)
    if max_dist >= cfg["extension_atr_severe"] and momentum in ("Weakening", "Deteriorating"):
        return EXTENSION_SEVERE
    if max_dist >= cfg["extension_atr_extended"]:
        return EXTENSION_EXTENDED
    return EXTENSION_NORMAL


# [BUGFIX 2026-09-14] With only four inputs feeding it, a blended-average
# threshold let one actively-contradicting leg get outvoted by the other
# three -- confirmed live: ALLT's Strong momentum + Bullish vwap + Bullish
# ema9 cleared the old 0.55 raw-score threshold for UP even with volume
# reading Declining (rvol 1.18, but decelerating), and it faded within 6
# seconds of fill. Explicit instruction: "we don't have lots of
# indicators so we need to make sure that they are all positive before
# an entry." direction=UP now requires every one of these, not a blend:
#   - momentum in {Strong, Weakening} -- i.e. slope_5m > 0, genuinely
#     rising. Deliberately excludes "Building": that label fires whenever
#     momentum_acceleration > 0 even with slope_5m <= 0 (price still
#     FALLING, just decelerating) -- confirmed as TNON's actual entry
#     reading (slope_5m=-0.157, labeled "Building"), a false start dressed
#     up in a bullish-sounding name.
#   - volume in {Expanding, Steady} -- NOT Declining. [ADJUSTED same day,
#     2026-09-14] Requiring Expanding specifically was retrospectively
#     replayed against every real entry opportunity today (56 symbols,
#     every logged shortlisted evaluation) and collapsed 48 real trades
#     to 1 -- Expanding volume only occurred on 9.3% of ticks, vs.
#     58-91% for the other four legs, so it was the dominant bottleneck,
#     and folding it into a strict-unanimity direction check meant any
#     single non-Expanding tick reset the whole persistence window (the
#     same near-zero-trade starvation shape that got the entire prior
#     engine scrapped on 2026-09-10). Loosened to "not actively
#     Declining" per explicit instruction -- still blocks the ALLT case
#     (Strong momentum + Declining volume) that motivated this whole
#     change, without requiring volume to be accelerating too.
#   - resistance == SAFE -- i.e. genuinely far enough below the nearest
#     overhead level to have room to run, OR a breakout is already
#     confirmed in progress (that's what collapses CAUTION/TOO_CLOSE to
#     SAFE in _resistance_classification() above). CAUTION alone no
#     longer passes -- "enough space to profit, otherwise wait for a
#     confirmed breakout," per explicit instruction.
#
# vwap and ema9 are DELIBERATELY NOT part of this hard gate as of
# 2026-09-14 -- a same-day retrospective replay against every real entry
# opportunity today (56 symbols, every logged shortlisted evaluation)
# showed the full 5-condition unanimity gate (momentum+volume+vwap+ema9+
# resistance) collapsed 48 real trades to 2 (net +$0.85). Dropping vwap
# and ema9 from the requirement (explicit instruction, "let's disable
# vwap and ema9") raised that to 3 simulated trades / net +$5.78 in the
# same replay, still far more selective than the real day's 48 trades /
# -$136.85, without over-constraining on two legs that are still fully
# reflected in the confidence blend below (used for shortlist ranking)
# even though they no longer gate entry outright.
_POSITIVE_MOMENTUM = {"Strong", "Weakening"}
_POSITIVE_VOLUME = {"Expanding", "Steady"}


def _direction_and_confidence(momentum, volume_state, vwap_state, ema9_state, extension, resistance, cfg):
    """direction is now a strict unanimity gate over momentum, volume,
    and resistance only (see comment above -- vwap/ema9 intentionally
    excluded as of 2026-09-14). confidence stays a blended score over
    all four inputs, purely descriptive (shortlist ranking/logging
    only, see module docstring), unaffected by this change."""
    w = cfg["confidence_weights"]
    raw = (_MOMENTUM_SCORE[momentum] * w["momentum"] + _VOLUME_SCORE[volume_state] * w["volume"]
           + _VWAP_SCORE[vwap_state] * w["vwap"] + _EMA_SCORE[ema9_state] * w["ema"])

    if extension == EXTENSION_SEVERE:
        raw *= 0.5
    elif extension == EXTENSION_EXTENDED:
        raw *= 0.85
    if resistance == RESISTANCE_TOO_CLOSE:
        raw *= 0.85
    elif resistance == RESISTANCE_CAUTION:
        raw *= 0.95

    confidence = round(raw * 100.0, 1)

    all_positive = (
        momentum in _POSITIVE_MOMENTUM
        and volume_state in _POSITIVE_VOLUME
        and resistance == RESISTANCE_SAFE
    )
    if all_positive:
        direction = DIRECTION_UP
    elif raw <= 0.35:
        direction = DIRECTION_DOWN
    else:
        direction = DIRECTION_NEUTRAL
    return direction, confidence


def compute_fast_prediction(symbol: str, features, bars: list, premarket_result: dict = None,
                             cfg: dict = None) -> FastPredictionReading:
    """
    features: a stream_features.StreamFeatures reading, computed by the
        caller exactly as prediction_pipeline.py already does today --
        this module doesn't change how features are computed, only how
        they're read.
    bars: the same live 1-min bars features was computed from (needed
        here directly for session_high / breakout-in-progress).
    premarket_result: this symbol's premarket_scanner.py result dict
        (pm_high/pm_low/prev_day_high) -- optional, fail-soft throughout.
    """
    cfg = {**DEFAULT_CONFIG, **(cfg or get_config().get("fast_prediction", {}))}

    if getattr(features, "insufficient_data", False):
        return FastPredictionReading(symbol=symbol, insufficient_data=True,
                                      reasons=["insufficient upstream stream_features data"])
    if len(bars) < cfg["min_bars"]:
        return FastPredictionReading(
            symbol=symbol, insufficient_data=True,
            reasons=[f"warm-up: need {cfg['min_bars']} one-minute bars, have {len(bars)}"])

    slope_5m = features.slope.get("slope_5m")
    momentum_acceleration = features.acceleration.get("momentum_acceleration")
    momentum = _momentum_state(slope_5m, momentum_acceleration)

    volume_acceleration = features.volume.get("volume_acceleration")
    relative_volume = features.volume.get("relative_volume")
    volume_state = _volume_state(volume_acceleration, relative_volume, cfg)

    vwap_classification = features.vwap.get("classification")
    vwap_state = _vwap_state(vwap_classification)

    price_vs_ema9_pct = features.ema.get("price_vs_ema9_pct")
    ema9_slope = features.ema.get("ema9_slope")
    ema9_state = _ema9_state(price_vs_ema9_pct, ema9_slope)

    price = features.price.get("last")
    res_name, res_level, res_distance_pct = _resistance_context(price, bars, premarket_result, cfg)
    breakout = _breakout_in_progress(bars, res_level, volume_acceleration, cfg)
    atr_pct = features.volatility.get("atr_pct")
    resistance_state = _resistance_classification(res_distance_pct, atr_pct, breakout, cfg)

    price_vs_ema9_atr = features.ema.get("price_vs_ema9_atr")
    vwap_distance_atr = features.volatility.get("vwap_distance_atr")
    extension_state = _extension_classification(price_vs_ema9_atr, vwap_distance_atr, momentum, cfg)

    direction, confidence = _direction_and_confidence(
        momentum, volume_state, vwap_state, ema9_state, extension_state, resistance_state, cfg)

    resistance_desc = "clear overhead" if res_level is None else (
        f"{res_name} ${res_level:.2f} ({res_distance_pct:.2f}% away)"
        + (" [breakout in progress -- distance penalty waived]" if breakout else ""))
    reasons = [
        f"momentum={momentum} (slope_5m={slope_5m})",
        f"volume={volume_state} (rvol={relative_volume}, accel={volume_acceleration})",
        f"vwap={vwap_state} ({vwap_classification})",
        f"ema9={ema9_state} (price_vs_ema9={price_vs_ema9_pct}%, ema9_slope={ema9_slope})",
        f"resistance={resistance_state} ({resistance_desc})",
        f"extension={extension_state}",
    ]

    return FastPredictionReading(
        symbol=symbol, direction=direction, confidence=confidence,
        momentum=momentum, volume_state=volume_state, vwap_state=vwap_state, ema9_state=ema9_state,
        resistance=resistance_state, resistance_level=res_level, resistance_source=res_name,
        resistance_distance_pct=res_distance_pct, breakout_in_progress=breakout,
        extension=extension_state, reasons=reasons,
    )
