"""
trend_engine.py

[SIMULATION-ONLY 2026-09-05] Combines stream_features.py's multi-horizon
slope readings into a single trend judgment: which of five states is
this symbol in, how strong is that trend (0-100, direction-agnostic),
and is it strengthening or weakening right now. Not consumed anywhere in
the live bot yet -- see config.json's trend_engine._note.

Deliberately reuses intraday_health.py's own scoring PATTERN (a
transparent weighted breakdown of per-signal credit, not a new scheme)
rather than inventing a different style of score for this module --
same "explainable, not a black box" requirement, applied consistently.

DIRECTION vs STATE vs SCORE vs TREND_SLOPE -- four different questions,
kept separate per the project's original design brief's own example
("Trend Score = 87, Trend Direction = UP, Trend Momentum = +12" are
three independent numbers, not one signed scale):

    direction   "UP" / "DOWN" / "NEUTRAL" -- which way, from the
                LONGEST available horizon (slope_5m, falling back to
                slope_3m, then vwap_slope) -- the underlying trend, not
                whatever the noisiest short horizon says.
    state       STRONG_UPTREND / WEAK_UPTREND / NEUTRAL / WEAK_DOWNTREND
                / STRONG_DOWNTREND -- direction + score bucketed into
                the five states.
    score       0-100, how strongly ALL SIX horizons (slope_sub,
                slope_1m, slope_3m, slope_5m, vwap_slope, ema9_slope)
                agree with `direction` -- direction-agnostic magnitude,
                not signed.
    trend_slope  positive = strengthening, negative = weakening.
                Reuses stream_features.py's acceleration.
                momentum_acceleration directly (the cross-horizon
                waterfall comparison) rather than recomputing the same
                thing under a different name -- see that module's
                docstring for what it actually measures.
"""

from dataclasses import dataclass, field

from config_loader import get_config
from indicators import classify_slope

DEFAULT_CONFIG = {
    "flat_slope_threshold_pct": 0.05,
    "strong_trend_score_threshold": 70,
    "weights": {
        "slope_sub": 15,
        "slope_1m": 20,
        "slope_3m": 20,
        "slope_5m": 20,
        "vwap_slope": 15,
        "ema9_slope": 10,
    },
}

STATE_STRONG_UP = "STRONG_UPTREND"
STATE_WEAK_UP = "WEAK_UPTREND"
STATE_NEUTRAL = "NEUTRAL"
STATE_WEAK_DOWN = "WEAK_DOWNTREND"
STATE_STRONG_DOWN = "STRONG_DOWNTREND"


@dataclass
class TrendReading:
    symbol: str
    direction: str = "NEUTRAL"
    state: str = STATE_NEUTRAL
    score: float = 0.0
    trend_slope: float = 0.0
    breakdown: dict = field(default_factory=dict)
    insufficient_data: bool = False
    missing: list = field(default_factory=list)


def _primary_direction(slopes: dict, flat_threshold: float) -> str:
    """The underlying trend direction, from the longest available
    horizon first -- see module docstring. Falls through to shorter
    horizons only when longer ones are genuinely missing (not merely
    flat -- a flat slope_5m legitimately means NEUTRAL, that's not a
    reason to fall back to a shorter, noisier horizon instead)."""
    for key in ("slope_5m", "slope_3m", "vwap_slope", "slope_1m", "slope_sub", "ema9_slope"):
        val = slopes.get(key)
        if val is not None:
            cls = classify_slope(val, flat_threshold)
            return {"positive": "UP", "negative": "DOWN", "flat": "NEUTRAL"}[cls]
    return "NEUTRAL"


def classify_trend(features, cfg: dict = None) -> TrendReading:
    """
    features: a stream_features.StreamFeatures reading (or any object
    exposing the same .slope / .vwap / .ema dict attributes).
    """
    cfg = {**DEFAULT_CONFIG, **(cfg or get_config().get("trend_engine", {}))}
    weights = {**DEFAULT_CONFIG["weights"], **cfg.get("weights", {})}
    flat_threshold = cfg["flat_slope_threshold_pct"]

    if getattr(features, "insufficient_data", False):
        return TrendReading(symbol=features.symbol, insufficient_data=True,
                             missing=["trend -- upstream stream_features reading was insufficient"])

    slopes = {
        "slope_sub": features.slope.get("slope_sub"),
        "slope_1m": features.slope.get("slope_1m"),
        "slope_3m": features.slope.get("slope_3m"),
        "slope_5m": features.slope.get("slope_5m"),
        "vwap_slope": features.vwap.get("slope"),
        "ema9_slope": features.ema.get("ema9_slope"),
    }
    available = {k: v for k, v in slopes.items() if v is not None}
    missing = [k for k, v in slopes.items() if v is None]

    if not available:
        return TrendReading(symbol=features.symbol, insufficient_data=True, missing=missing)

    direction = _primary_direction(slopes, flat_threshold)

    breakdown = {}
    for name, val in available.items():
        cls = classify_slope(val, flat_threshold)
        if direction == "UP":
            credit = 1.0 if cls == "positive" else (0.5 if cls == "flat" else 0.0)
        elif direction == "DOWN":
            credit = 1.0 if cls == "negative" else (0.5 if cls == "flat" else 0.0)
        else:  # direction is NEUTRAL -- confirming flat is full credit, any real slope either way is a mild disagreement
            credit = 1.0 if cls == "flat" else 0.5
        breakdown[name] = round(weights[name] * credit, 2)

    weight_total = sum(weights[k] for k in available)
    score = round(sum(breakdown.values()) / weight_total * 100.0, 2) if weight_total else 0.0

    strong_threshold = cfg["strong_trend_score_threshold"]
    if direction == "UP":
        state = STATE_STRONG_UP if score >= strong_threshold else STATE_WEAK_UP
    elif direction == "DOWN":
        state = STATE_STRONG_DOWN if score >= strong_threshold else STATE_WEAK_DOWN
    else:
        state = STATE_NEUTRAL

    trend_slope = features.acceleration.get("momentum_acceleration", 0.0) or 0.0

    return TrendReading(
        symbol=features.symbol, direction=direction, state=state,
        score=score, trend_slope=round(trend_slope, 4),
        breakdown=breakdown, insufficient_data=False, missing=missing,
    )
