#!/usr/bin/env python3
"""
Strategic Analyzer — enhanced market judgment layer.

Four enhancements over the base regime_router:

  1. Trend-stage classification (EARLY / MID / LATE / EXHAUSTION)
  2. Range-quality scoring (QUALITY / NOISE / BREAKOUT_SETUP)
  3. Multi-timeframe macro direction (M5 + H1 + D1 consensus)
  4. GARCH volatility → risk-budget multiplier

All outputs feed into the tactical scheduler and risk governor to replace
coarse STRONG_TREND / WEAK_TREND / RANGING labels with actionable,
fine-grained market intelligence.
"""

from __future__ import annotations

import math
from typing import Any, Dict, List, Optional, Tuple


# ── 1. Trend-stage classifier ─────────────────────────────────────

def classify_trend_stage(
    bars: list[dict],
    adx: float,
    adx_slope: float,
    ema_aligned: bool,
    efficiency: float,
    vol_expanding: bool,
    rsi: float = 50.0,
) -> Dict[str, Any]:
    """Classify the current trend into EARLY / MID / LATE / EXHAUSTION.

    Returns {stage, confidence, description, risk_multiplier, tactics}
    """
    n = len(bars)
    if n < 50:
        return {"stage": "UNCLASSIFIED", "confidence": 0.0,
                "description": "insufficient data", "risk_multiplier": 0.5, "tactics": []}

    closes = [float(b.get("close", 0) or 0) for b in bars]

    # EMA21 distance ratio
    def _ema(series, period):
        k = 2.0 / (period + 1)
        out = [series[0]]
        for v in series[1:]:
            out.append(v * k + out[-1] * (1 - k))
        return out

    e21 = _ema(closes, 21)
    e50 = _ema(closes, 50)
    price = closes[-1]
    ema21_dist = abs(price - e21[-1]) / max(0.01, price)
    ema21_50_gap = abs(e21[-1] - e50[-1]) / max(0.01, price)

    # RSI divergence detection
    rsi_divergence = False
    if len(closes) >= 20:
        recent_high = max(closes[-10:])
        older_high = max(closes[-20:-10])
        if recent_high > older_high and rsi < 65:
            rsi_divergence = True

    # Volume trend
    volumes = [float(b.get("tick_volume", 0) or 0) for b in bars]
    avg_vol_10 = sum(volumes[-10:]) / 10 if len(volumes) >= 10 else 1
    avg_vol_30 = sum(volumes[-30:]) / 30 if len(volumes) >= 30 else 1
    vol_declining = avg_vol_10 < avg_vol_30 * 0.8

    # MACD histogram momentum
    e12 = _ema(closes, 12)
    e26 = _ema(closes, 26)
    macd = [e12[i] - e26[i] for i in range(len(e12))]
    signal = _ema(macd, 9)
    macd_hist = [macd[i] - signal[i] for i in range(len(macd))]
    hist_now = macd_hist[-1]
    hist_5ago = macd_hist[-6] if len(macd_hist) >= 6 else hist_now
    hist_declining = hist_now < hist_5ago

    # Classification
    reasons = []
    tactics = []

    if not ema_aligned and adx < 25:
        return {"stage": "NON_TREND", "confidence": 0.70,
                "description": "no trend structure", "risk_multiplier": 0.6, "tactics": ["favor_range_strategies"]}

    # EARLY: ADX rising from low base, EMA just aligned, price near EMA21
    if adx < 30 and adx_slope > 0.5 and ema_aligned and ema21_dist < 0.005:
        stage, conf = "EARLY_TREND", 0.65
        reasons.append("adx_rising_from_base")
        reasons.append("ema_just_aligned")
        tactics = ["position_small", "wide_stop", "prefer_ema_macd", "allow_pullback_entry"]

    # MID: ADX solid, EMA well-separated, efficiency high
    elif adx >= 28 and ema_aligned and efficiency >= 0.30 and ema21_50_gap >= 0.003:
        stage, conf = "MID_TREND", 0.80
        reasons.append("strong_trend_structure")
        reasons.append("ema_well_separated")
        tactics = ["full_position", "trailing_stop", "prefer_ema_macd", "allow_breakout_entry"]

    # LATE: ADX high but declining, volume dropping, possible divergence
    elif adx >= 30 and (adx_slope < -0.5 or vol_declining or rsi_divergence):
        stage, conf = "LATE_TREND", 0.60
        reasons.append("adx_peaking" if adx_slope < -0.5 else "")
        reasons.append("volume_declining" if vol_declining else "")
        reasons.append("rsi_divergence" if rsi_divergence else "")
        reasons = [r for r in reasons if r]
        tactics = ["reduce_position", "tighten_stop", "prefer_pullback_fade", "avoid_breakout_entry"]
        if hist_declining:
            conf += 0.10
            tactics.append("macd_weakening")

    # EXHAUSTION: clear reversal signals
    elif adx >= 28 and rsi_divergence and vol_declining and not ema_aligned:
        stage, conf = "EXHAUSTION", 0.55
        reasons = ["rsi_divergence", "volume_declining", "ema_broken"]
        tactics = ["close_positions", "no_new_entries", "wait_for_confirmation"]

    # Default: unclassified trend
    else:
        stage, conf = "MID_TREND", 0.50 if adx >= 25 else 0.35
        reasons.append("default_mid" if adx >= 25 else "weak_structure")

    return {
        "stage": stage,
        "confidence": round(conf, 2),
        "description": "; ".join(reasons) if reasons else "classified",
        "risk_multiplier": round({"EARLY_TREND": 0.60, "MID_TREND": 1.0,
                                 "LATE_TREND": 0.70, "EXHAUSTION": 0.30,
                                 "NON_TREND": 0.6, "UNCLASSIFIED": 0.5}.get(stage, 0.7), 2),
        "tactics": tactics,
        "metrics": {
            "ema21_distance": round(ema21_dist, 6),
            "ema21_50_gap": round(ema21_50_gap, 6),
            "rsi_divergence": rsi_divergence,
            "vol_declining": vol_declining,
            "hist_declining": hist_declining,
        },
    }


# ── 2. Range-quality scorer ───────────────────────────────────────

def score_range_quality(
    bars: list[dict],
    adx: float,
    atr_expansion: float,
    bb_width: float,
    bb_position: float,
    efficiency: float,
) -> Dict[str, Any]:
    """Score the quality of a ranging market.

    Returns {quality, score, description, tactics}
    """
    n = len(bars)
    if n < 50:
        return {"quality": "UNKNOWN", "score": 0.0,
                "description": "insufficient data", "tactics": []}

    closes = [float(b.get("close", 0) or 0) for b in bars]
    price = closes[-1]

    # Detect fake breakouts within range
    fake_breakout_count = 0
    highs = [float(b.get("high", 0) or 0) for b in bars]
    lows = [float(b.get("low", 0) or 0) for b in bars]
    for i in range(20, len(bars)):
        window_high = max(highs[i - 20:i])
        window_low = min(lows[i - 20:i])
        if highs[i] > window_high * 1.002 and closes[i] < window_high:
            fake_breakout_count += 1
        elif lows[i] < window_low * 0.998 and closes[i] > window_low:
            fake_breakout_count += 1

    # Score components
    scores = {}

    # ADX: lower is better for range
    if adx < 20:
        scores["adx_low"] = 0.30
    elif adx < 25:
        scores["adx_moderate"] = 0.15
    else:
        scores["adx_high"] = -0.20

    # BB width: good range needs proper bandwidth
    if 0.003 <= bb_width <= 0.015:
        scores["bb_healthy"] = 0.20
    elif bb_width < 0.002:
        scores["bb_too_narrow"] = -0.25
    else:
        scores["bb_wide"] = 0.10

    # BB position: middle is bad, edges are good
    if bb_position <= 0.15 or bb_position >= 0.85:
        scores["bb_at_edge"] = 0.15
    elif 0.40 <= bb_position <= 0.60:
        scores["bb_middle"] = -0.10

    # ATR stability
    if atr_expansion < 1.3:
        scores["atr_stable"] = 0.10
    else:
        scores["atr_unstable"] = -0.15

    # Fake breakout penalty
    if fake_breakout_count >= 3:
        scores["many_fakeouts"] = -0.30
    elif fake_breakout_count >= 1:
        scores["some_fakeouts"] = -0.10

    # Efficiency: low efficiency confirms range
    if efficiency < 0.25:
        scores["low_efficiency"] = 0.15

    total_score = sum(scores.values()) + 0.50  # baseline 0.50
    total_score = max(0.0, min(1.0, total_score))

    if total_score >= 0.70:
        quality = "QUALITY_RANGE"
        tactics = ["full_range_trading", "prefer_rsi_extreme", "prefer_boll_band"]
    elif total_score >= 0.45:
        quality = "NOISY_RANGE"
        tactics = ["half_position", "prefer_key_level", "wider_stops"]
    elif total_score >= 0.25:
        quality = "BREAKOUT_SETUP"
        tactics = ["reduce_range_exposure", "prepare_breakout_mode", "tight_stops"]
    else:
        quality = "AVOID"
        tactics = ["no_range_trading", "wait_for_structure"]

    return {
        "quality": quality,
        "score": round(total_score, 2),
        "description": f"adx={adx:.0f} bbw={bb_width:.4f} fakeouts={fake_breakout_count}",
        "tactics": tactics,
        "breakdown": {k: round(v, 2) for k, v in scores.items()},
    }


# ── 3. Multi-timeframe macro direction ────────────────────────────

def resolve_multi_tf_direction(
    m5_bars: list[dict],
    h1_bars: Optional[list[dict]] = None,
    d1_bars: Optional[list[dict]] = None,
    llm_direction: str = "NEUTRAL",
    llm_confidence: float = 0.0,
) -> Dict[str, Any]:
    """Resolve macro direction from multiple timeframes + LLM input.

    Each timeframe provides a direction signal. Consensus determines confidence.
    """
    def _tf_direction(bars: list[dict]) -> Tuple[str, float]:
        if not bars or len(bars) < 20:
            return "NEUTRAL", 0.0
        closes = [float(b.get("close", 0) or 0) for b in bars]
        def _ema(s, p):
            k = 2.0 / (p + 1); out = [s[0]]
            for v in s[1:]: out.append(v * k + out[-1] * (1 - k))
            return out
        e9 = _ema(closes, 9); e21 = _ema(closes, 21)
        e50 = _ema(closes, 50)
        price = closes[-1]
        # Simple scoring
        score = 0.0
        if e9[-1] > e21[-1]: score += 0.20
        if e21[-1] > e50[-1]: score += 0.15
        if price > e21[-1]: score += 0.15
        # Recent momentum
        if len(closes) >= 10:
            mom = (closes[-1] - closes[-10]) / max(0.01, closes[-10])
            if mom > 0.005: score += 0.15
            elif mom < -0.005: score -= 0.15
        # Normalize
        if score > 0.25: return "LONG", min(0.95, score + 0.30)
        elif score < -0.15: return "SHORT", min(0.95, abs(score) + 0.20)
        return "NEUTRAL", 0.30

    m5_dir, m5_conf = _tf_direction(m5_bars)
    h1_dir, h1_conf = _tf_direction(h1_bars) if h1_bars else ("NEUTRAL", 0.0)
    d1_dir, d1_conf = _tf_direction(d1_bars) if d1_bars else ("NEUTRAL", 0.0)

    # Consensus counting
    directions = [m5_dir, h1_dir, d1_dir]
    longs = directions.count("LONG")
    shorts = directions.count("SHORT")

    if longs >= 2:
        consensus_dir = "LONG"
        consensus_conf = 0.60 + 0.15 * (longs - 2)
    elif shorts >= 2:
        consensus_dir = "SHORT"
        consensus_conf = 0.60 + 0.15 * (shorts - 2)
    elif longs == 1 and shorts == 1:
        consensus_dir = "NEUTRAL"
        consensus_conf = 0.30
    else:
        consensus_dir = "NEUTRAL"
        consensus_conf = 0.20

    # Blend with LLM if available
    final_dir = consensus_dir
    final_conf = consensus_conf
    if llm_direction in ("LONG", "SHORT") and llm_confidence > 0:
        if llm_direction == consensus_dir:
            final_conf = max(consensus_conf, llm_confidence) + 0.10
        elif consensus_dir == "NEUTRAL":
            final_dir = llm_direction
            final_conf = llm_confidence * 0.80
        else:
            final_dir = "NEUTRAL"
            final_conf = 0.25

    final_conf = min(0.95, final_conf)

    return {
        "direction": final_dir,
        "confidence": round(final_conf, 2),
        "consensus_type": {3: "FULL", 2: "MAJORITY", 1: "WEAK", 0: "NONE"}.get(max(longs, shorts), "NONE"),
        "tf_breakdown": {"M5": m5_dir, "H1": h1_dir, "D1": d1_dir},
        "llm_input": llm_direction,
        "risk_implication": "full_risk" if final_conf >= 0.75 else "cautious" if final_conf >= 0.50 else "defensive",
    }


# ── 4. GARCH volatility → risk budget multiplier ──────────────────

def garch_risk_multiplier(
    cond_vol: float = 0.0,
    long_run_vol: float = 0.0,
    alpha: float = 0.0,
    beta: float = 0.0,
    annual_vol: float = 0.0,
) -> Dict[str, Any]:
    """Convert GARCH model output into a risk-budget multiplier.

    GARCH(1,1): sigma_t^2 = omega + alpha * r_{t-1}^2 + beta * sigma_{t-1}^2

    alpha+beta near 1 → persistent volatility → good for trend
    alpha+beta low     → mean-reverting vol → good for range
    cond_vol >> long_run_vol → elevated risk → reduce position
    """
    if cond_vol <= 0 or long_run_vol <= 0:
        return {"multiplier": 1.0, "regime": "unknown",
                "description": "GARCH unavailable", "prefer_strategy_family": None}

    persistence = alpha + beta
    vol_ratio = cond_vol / max(0.0001, long_run_vol)

    # Risk multiplier: shrink when vol is elevated
    if vol_ratio >= 1.5:
        risk_mult = 0.50
        risk_reason = "vol_elevated"
    elif vol_ratio >= 1.2:
        risk_mult = 0.75
        risk_reason = "vol_above_avg"
    elif vol_ratio <= 0.7:
        risk_mult = 1.20
        risk_reason = "vol_below_avg"
    else:
        risk_mult = 1.0
        risk_reason = "vol_normal"

    # Volatility regime preference
    if persistence >= 0.95:
        vol_regime = "PERSISTENT"
        prefer_family = "trend"
        regime_note = "high persistence: trending vol, favor trend strategies"
    elif persistence <= 0.85:
        vol_regime = "MEAN_REVERTING"
        prefer_family = "range"
        regime_note = "low persistence: mean-reverting vol, favor range strategies"
    else:
        vol_regime = "MODERATE"
        prefer_family = None
        regime_note = "moderate persistence: balanced"

    return {
        "multiplier": round(risk_mult, 2),
        "vol_regime": vol_regime,
        "prefer_strategy_family": prefer_family,
        "description": f"{risk_reason}; {regime_note}",
        "metrics": {
            "cond_vol_pct": round(cond_vol * 100, 4),
            "long_run_vol_pct": round(long_run_vol * 100, 4),
            "vol_ratio": round(vol_ratio, 2),
            "persistence": round(persistence, 3),
            "annual_vol_pct": round(annual_vol, 1) if annual_vol > 0 else None,
        },
    }
