from __future__ import annotations

import time
from typing import Any


REGIME_ALIASES = {
    "TREND": "WEAK_TREND",
    "TRENDING": "WEAK_TREND",
    "NORMAL_TREND": "WEAK_TREND",
    "CONSOLIDATION": "RANGING",
    "RANGE": "RANGING",
    "WIDE_RANGE": "RANGING",
    "SIDEWAYS": "RANGING",
    "LOW_VOLATILITY": "RANGING",
    "NEUTRAL": "RANGING",
    "UNCERTAIN": "TRANSITION",
    "DRIFTING": "TRANSITION",
    "SHOCK_REPRICING": "VOLATILITY_SPIKE",
    "BREAKOUT": "STRONG_TREND",
    "EXTREME_TREND": "STRONG_TREND",
}
CANONICAL_TREND_REGIMES = {"STRONG_TREND", "WEAK_TREND", "STEALTH_TREND"}
CANONICAL_RANGE_REGIMES = {"RANGING"}
CANONICAL_DEFENSIVE_REGIMES = {"TRANSITION", "VOLATILITY_SPIKE", "FAKE_TREND", "EXTREME"}
TREND_REGIMES = CANONICAL_TREND_REGIMES | {"TREND", "TRENDING", "BREAKOUT", "EXTREME_TREND", "NORMAL_TREND"}
RANGE_REGIMES = CANONICAL_RANGE_REGIMES | {"RANGE", "WIDE_RANGE", "SIDEWAYS", "LOW_VOLATILITY", "NEUTRAL"}
DEFENSIVE_REGIMES = CANONICAL_DEFENSIVE_REGIMES | {"UNCERTAIN", "DRIFTING", "SHOCK_REPRICING"}
ASIA_SESSION_BUCKETS = {"ASIA", "ASIA_NOISE", "ASIA_DEFENSIVE", "ASIA_TREND"}
OVERLAP_SESSION_BUCKETS = {"OVERLAP", "LONDON_NEW_YORK_OVERLAP", "LONDON_NY_OVERLAP"}

# ── Range loosening state machine (module-level, survives across calls) ──
_loosening_state: dict[str, Any] = {
    "level": 0,
    "direction": "NEUTRAL",
    "strategy_families_seen": [],
    "level3_last_trigger_ts": 0.0,
    "level3_trigger_count_30m": 0,
    "level4_active_until_ts": 0.0,
    "last_update_ts": 0.0,
}


def normalize_regime_label(value: Any) -> str:
    text = str(value or "").strip().upper().replace("-", "_").replace(" ", "_")
    return REGIME_ALIASES.get(text, text or "UNKNOWN")


def is_trend_regime(value: Any) -> bool:
    return normalize_regime_label(value) in CANONICAL_TREND_REGIMES


def is_range_regime(value: Any) -> bool:
    return normalize_regime_label(value) in CANONICAL_RANGE_REGIMES


def is_defensive_regime(value: Any) -> bool:
    return normalize_regime_label(value) in CANONICAL_DEFENSIVE_REGIMES


def regime_family(value: Any) -> str:
    label = normalize_regime_label(value)
    if label in CANONICAL_TREND_REGIMES:
        return "trend"
    if label in CANONICAL_RANGE_REGIMES:
        return "range"
    if label in CANONICAL_DEFENSIVE_REGIMES:
        return "defensive"
    return "unknown"


def resolve_route_family(selection_regime: Any, regime_subtype: Any = "", session_bucket: Any = "NORMAL", atr_ratio: float = 0.0, range_loosening: dict[str, Any] | None = None) -> str:
    regime = normalize_regime_label(selection_regime)
    subtype = normalize_regime_label(regime_subtype)
    session = normalize_session_bucket(session_bucket)

    # ATR thresholds by session to filter out low-quality range/transition markets
    # Thresholds are in ratio (ATR/Price). 0.0004 = 4 pips per 10000 units.
    atr_thresholds = {
        "ASIA": 0.00035,      # Asia is naturally quieter
        "LONDON": 0.00075,    # London needs more volatility to be tradable
        "NEW_YORK": 0.00075,
        "OVERLAP": 0.00095,   # Overlap should be the most volatile
        "NORMAL": 0.0005      # Fallback
    }

    min_atr = atr_thresholds.get(session, 0.0005)

    if is_range_regime(regime):
        if atr_ratio > 0 and atr_ratio < min_atr:
            return "uncertain"
        if range_loosening and range_loosening.get("loosening"):
            return "range_loosening"
        return "range"

    if regime == "FAKE_TREND":
        return "blocked"
    if regime == "TRANSITION":
        return "uncertain"
    if is_defensive_regime(regime):
        return "uncertain"
    if is_trend_regime(regime) or is_trend_regime(subtype):
        return "trend"
    return "uncertain"


def build_regime_audit_payload(payload: dict[str, Any] | None) -> dict[str, Any]:
    source = payload if isinstance(payload, dict) else {}
    selection_regime = normalize_regime_label(source.get("selection_regime", source.get("regime", "UNKNOWN")))
    session_bucket = normalize_session_bucket(source.get("session_bucket", source.get("session_mode", "NORMAL")))
    atr_ratio = _safe_float(source.get("atr_ratio", 0.0), 0.0)
    route_family = str(source.get("route_family", "") or "") or resolve_route_family(
        selection_regime,
        source.get("regime_subtype", ""),
        session_bucket,
        atr_ratio,
    )
    return {
        "regime": selection_regime,
        "selection_regime": selection_regime,
        "regime_source": str(source.get("regime_source", "UNKNOWN") or "UNKNOWN"),
        "regime_confidence": round(_safe_float(source.get("regime_confidence", 0.0), 0.0), 4),
        "regime_4d": normalize_regime_label(source.get("regime_4d", selection_regime)),
        "interpreter_regime": normalize_regime_label(source.get("interpreter_regime", "UNKNOWN")),
        "regime_subtype": normalize_regime_label(source.get("regime_subtype", "UNKNOWN")),
        "regime_family": regime_family(selection_regime),
        "route_family": str(route_family or "uncertain"),
        "session_mode": str(source.get("session_mode", "NORMAL") or "NORMAL").upper(),
        "session_bucket": session_bucket,
        "atr_ratio": round(atr_ratio, 6),
        "regime_flags": list(source.get("regime_flags", [])) if isinstance(source.get("regime_flags", []), list) else [],
        "transition_count": int(source.get("transition_count", 0) or 0),
    }


def normalize_session_bucket(value: Any) -> str:
    text = str(value or "").strip().upper()
    if not text:
        return "NORMAL"
    if "OVERLAP" in text or text in OVERLAP_SESSION_BUCKETS:
        return "OVERLAP"
    if text in ASIA_SESSION_BUCKETS or text.startswith("ASIA_"):
        return "ASIA"
    if text.startswith("LONDON"):
        return "LONDON"
    if text.startswith("NEW_YORK") or text.startswith("NY"):
        return "NEW_YORK"
    return text


def _safe_float(value: Any, default: float = 0.0) -> float:
    try:
        return float(value)
    except Exception:
        return default


def _extract_momentum_snapshot(mt5_snapshot: dict[str, Any] | None) -> dict[str, Any]:
    snap = mt5_snapshot if isinstance(mt5_snapshot, dict) else {}
    plan = snap.get("ai_trade_plan", {}) if isinstance(snap.get("ai_trade_plan", {}), dict) else {}
    market = snap.get("market", {}) if isinstance(snap.get("market", {}), dict) else {}
    result = {
        "atr": 0.0,
        "m3_atr": 0.0,
        "weighted_atr": 0.0,
        "momentum_direction": "NEUTRAL",
        "bar_count": 0,
        "timeframe": str(
            snap.get("timeframe")
            or plan.get("timeframe")
            or market.get("timeframe")
            or ""
        ).upper(),
    }
    bars = snap.get("bars", [])
    if not isinstance(bars, list) or len(bars) < 10:
        return result
    ordered = list(bars)
    try:
        if (ordered[0].get("time", 0) or 0) > (ordered[-1].get("time", 0) or 0):
            ordered.reverse()
    except Exception:
        pass
    closes = []
    try:
        closes = [float((bar or {}).get("close", 0.0) or 0.0) for bar in ordered]
    except Exception:
        return result
    if len(closes) < 10:
        return result
    n = min(len(ordered), 20)
    trs: list[float] = []
    for i in range(len(ordered) - n + 1, len(ordered)):
        if i <= 0:
            continue
        bar = ordered[i] if isinstance(ordered[i], dict) else {}
        prev = ordered[i - 1] if isinstance(ordered[i - 1], dict) else {}
        high = _safe_float(bar.get("high", 0.0))
        low = _safe_float(bar.get("low", 0.0))
        prev_close = _safe_float(prev.get("close", 0.0))
        trs.append(max(high - low, abs(high - prev_close), abs(low - prev_close)))
    atr = sum(trs) / len(trs) if trs else 0.0
    if atr <= 0:
        return result
    momentum: dict[str, float] = {}
    for label, window in {"3": 3, "5": 5, "10": 10}.items():
        if len(closes) > window:
            momentum[label] = (closes[-1] - closes[-window - 1]) / atr
    weighted = (
        momentum.get("3", 0.0) * 0.5
        + momentum.get("5", 0.0) * 0.3
        + momentum.get("10", 0.0) * 0.2
    )
    direction = "NEUTRAL"
    if weighted > 0.1:
        direction = "BULLISH"
    elif weighted < -0.1:
        direction = "BEARISH"
    result.update(
        {
            "atr": round(atr, 6),
            "m3_atr": round(momentum.get("3", 0.0), 4),
            "weighted_atr": round(weighted, 4),
            "momentum_direction": direction,
            "bar_count": len(ordered),
        }
    )
    return result


def _range_loosening_state_machine(
    m1_loosening: dict[str, Any] | None,
    strategy_intents: list[dict[str, Any]] | None,
    session_bucket: str = "NORMAL",
) -> dict[str, Any]:
    """Four-level gradient state machine for range loosening detection.

    Level 0: No M1 loosening, no strategy intents → silent
    Level 1: M1 active alone, or 1+ intents without M1 → observe only
    Level 2: M1 active + >=1 families with direction consensus → activate loosening
    Level 3: M1 active + >=2 families with direction consensus → force reclassify
    Level 4: Level 3 triggered 2x within 30min -> bypass classifier entirely
    """
    global _loosening_state
    now = time.time()
    prev = _loosening_state

    m1_active = m1_loosening and m1_loosening.get("loosening") if m1_loosening else False
    m1_direction = str(m1_loosening.get("direction", "NEUTRAL") or "NEUTRAL") if m1_loosening else "NEUTRAL"

    # Deduplicate strategy intents by family, count direction votes
    intents = strategy_intents if isinstance(strategy_intents, list) else []
    families_seen: list[str] = []
    bull_count = 0
    bear_count = 0
    if intents:
        for intent in intents:
            fam = str(intent.get("strategy_family", "") or "").strip()
            side = str(intent.get("intended_side", "") or "").strip().upper()
            if fam and fam not in families_seen:
                families_seen.append(fam)
            if side == "BUY":
                bull_count += 1
            elif side == "SELL":
                bear_count += 1
    family_count = len(families_seen)

    if bull_count > bear_count:
        unified_direction = "BULLISH"
    elif bear_count > bull_count:
        unified_direction = "BEARISH"
    else:
        unified_direction = m1_direction if m1_active else "NEUTRAL"

    # ── Level 4 expiry check ──
    if prev.get("level") == 4 and now < prev.get("level4_active_until_ts", 0):
        _loosening_state["last_update_ts"] = now
        return _loosening_state
    elif prev.get("level") == 4 and now >= prev.get("level4_active_until_ts", 0):
        _loosening_state = {"level": 0, "direction": "NEUTRAL",
                            "strategy_families_seen": [], "level3_last_trigger_ts": 0.0,
                            "level3_trigger_count_30m": 0, "level4_active_until_ts": 0.0,
                            "last_update_ts": now}
        return _loosening_state

    # ── Level 0: No intents, no M1 ──
    if family_count == 0 and not m1_active:
        _loosening_state = {"level": 0, "direction": "NEUTRAL",
                            "strategy_families_seen": [], "level3_last_trigger_ts": prev.get("level3_last_trigger_ts", 0),
                            "level3_trigger_count_30m": prev.get("level3_trigger_count_30m", 0),
                            "level4_active_until_ts": 0, "last_update_ts": now}
        return _loosening_state

    # ── Level 1: 1 family, no M1 → observe only ──
    if family_count == 1 and not m1_active:
        _loosening_state = {"level": 1, "direction": unified_direction,
                            "strategy_families_seen": families_seen,
                            "level3_last_trigger_ts": prev.get("level3_last_trigger_ts", 0),
                            "level3_trigger_count_30m": prev.get("level3_trigger_count_30m", 0),
                            "level4_active_until_ts": 0, "last_update_ts": now}
        return _loosening_state

    # ── M1 active but direction mismatch with strategy intents → downgrade to Level 1 ──
    if m1_active and family_count > 0 and unified_direction != "NEUTRAL" and m1_direction != "NEUTRAL" and m1_direction != unified_direction:
        _loosening_state = {"level": 1, "direction": "NEUTRAL",
                            "strategy_families_seen": families_seen,
                            "level3_last_trigger_ts": prev.get("level3_last_trigger_ts", 0),
                            "level3_trigger_count_30m": prev.get("level3_trigger_count_30m", 0),
                            "level4_active_until_ts": 0, "last_update_ts": now}
        return _loosening_state

    # ── Level 2: M1 active + >=1 family with direction consensus ──
    if m1_active and family_count >= 1:
        direction_ok = (m1_direction != "NEUTRAL"
                        and (family_count == 0 or unified_direction == "NEUTRAL" or m1_direction == unified_direction))
        if direction_ok:
            is_strong = family_count >= 2
            _loosening_state = {"level": 2, "direction": unified_direction if unified_direction != "NEUTRAL" else m1_direction,
                                "strategy_families_seen": families_seen,
                                "level3_last_trigger_ts": prev.get("level3_last_trigger_ts", 0),
                                "level3_trigger_count_30m": prev.get("level3_trigger_count_30m", 0),
                                "level4_active_until_ts": 0, "last_update_ts": now}
            if is_strong:
                l3_count = prev.get("level3_trigger_count_30m", 0)
                if now - prev.get("level3_last_trigger_ts", 0) > 1800:
                    l3_count = 0
                l3_count += 1
                _loosening_state["level"] = 3
                _loosening_state["level3_last_trigger_ts"] = now
                _loosening_state["level3_trigger_count_30m"] = l3_count
                if l3_count >= 2:
                    _loosening_state["level"] = 4
                    _loosening_state["level4_active_until_ts"] = now + 1800
            return _loosening_state

    # ── Level 2: M1 active alone (no intents) — milder activation ──
    if m1_active and family_count == 0:
        _loosening_state = {"level": 2, "direction": m1_direction,
                            "strategy_families_seen": [],
                            "level3_last_trigger_ts": prev.get("level3_last_trigger_ts", 0),
                            "level3_trigger_count_30m": prev.get("level3_trigger_count_30m", 0),
                            "level4_active_until_ts": 0, "last_update_ts": now}
        return _loosening_state

    # ── Level 1: intents without M1 → observe only ──
    if family_count >= 1 and not m1_active:
        _loosening_state = {"level": 1, "direction": unified_direction,
                            "strategy_families_seen": families_seen,
                            "level3_last_trigger_ts": prev.get("level3_last_trigger_ts", 0),
                            "level3_trigger_count_30m": prev.get("level3_trigger_count_30m", 0),
                            "level4_active_until_ts": 0, "last_update_ts": now}
        return _loosening_state

    _loosening_state["last_update_ts"] = now
    return _loosening_state


def get_loosening_state() -> dict[str, Any]:
    """Return current range loosening state (for blackboard / UI)."""
    return dict(_loosening_state)


def reset_loosening_state() -> None:
    """Reset the loosening state machine (for testing / restart)."""
    global _loosening_state
    _loosening_state = {
        "level": 0, "direction": "NEUTRAL",
        "strategy_families_seen": [], "level3_last_trigger_ts": 0.0,
        "level3_trigger_count_30m": 0, "level4_active_until_ts": 0.0,
        "last_update_ts": 0.0,
    }


def resolve_unified_market_context(
    agent_context: dict[str, Any] | None,
    engine_request: dict[str, Any] | None,
    mt5_snapshot: dict[str, Any] | None = None,
) -> dict[str, Any]:
    agent = agent_context if isinstance(agent_context, dict) else {}
    request = engine_request if isinstance(engine_request, dict) else {}
    interpreter = agent.get("interpreter", {}) if isinstance(agent.get("interpreter", {}), dict) else {}
    macro = agent.get("macro", {}) if isinstance(agent.get("macro", {}), dict) else {}
    session_profile = agent.get("session_profile", {}) if isinstance(agent.get("session_profile", {}), dict) else {}
    regime_4d = agent.get("regime_classification", {}) if isinstance(agent.get("regime_classification", {}), dict) else {}
    plan = request.get("ai_trade_plan", {}) if isinstance(request.get("ai_trade_plan", {}), dict) else {}

    regime_4d_label = normalize_regime_label(regime_4d.get("regime", agent.get("regime", "")))
    regime_4d_conf = _safe_float(regime_4d.get("confidence", agent.get("regime_confidence", 0.0)), 0.0)
    interpreter_regime = normalize_regime_label(
        interpreter.get("market_structure") or interpreter.get("market_regime") or plan.get("regime") or plan.get("mode")
    )
    regime_subtype = normalize_regime_label(interpreter.get("regime_subtype") or plan.get("regime_subtype"))

    session_mode = str(session_profile.get("mode", agent.get("_session_info", {}).get("session_mode", "NORMAL")) or "NORMAL").upper()
    session_bucket = normalize_session_bucket(session_mode)
    if session_bucket == "NORMAL":
        inferred_session_bucket = normalize_session_bucket(regime_subtype)
        if inferred_session_bucket in {"ASIA", "LONDON", "NEW_YORK", "OVERLAP"}:
            session_bucket = inferred_session_bucket

    # Extract ATR ratio for volatility-aware routing
    atr_ratio = _safe_float(regime_4d.get("atr_ratio", 0.0), 0.0)
    if atr_ratio <= 0:
        # Fallback: calculate from snapshot if 4d classification doesn't have it
        momentum_snapshot = _extract_momentum_snapshot(mt5_snapshot)
        atr_val = momentum_snapshot.get("atr", 0.0)
        curr_price = 0.0
        if mt5_snapshot and mt5_snapshot.get("bars"):
            curr_price = float(mt5_snapshot["bars"][-1].get("close", 0.0) or 0.0)
        if curr_price > 0:
            atr_ratio = atr_val / curr_price

    if is_defensive_regime(regime_4d_label) or is_range_regime(regime_4d_label):
        selection_regime = regime_4d_label
        regime_source = "4d"
    elif is_trend_regime(regime_4d_label):
        if regime_4d_conf >= 0.60:
            selection_regime = regime_4d_label
            regime_source = "4d"
        elif session_bucket == "ASIA":
            selection_regime = "TRANSITION"
            regime_source = "session_defensive"
        elif interpreter_regime in {"VOLATILITY_SPIKE", "FAKE_TREND", "EXTREME"}:
            selection_regime = interpreter_regime
            regime_source = "interpreter_defensive"
        else:
            # 4d classifier says trend but confidence < 0.60.
            # Never let a stale interpreter downgrade a trend to RANGING —
            # the 4d classifier is the authoritative source for regime type.
            selection_regime = regime_4d_label
            regime_source = "4d_low_conf"
    else:
        selection_regime = interpreter_regime or regime_4d_label or "TRANSITION"
        regime_source = "interpreter" if interpreter_regime else "fallback"

    # ── Range loosening override ──
    m1_loosening_raw = agent.get("m1_loosening") if isinstance(agent.get("m1_loosening"), dict) else None
    strategy_intents_raw = agent.get("suppressed_strategy_intents") if isinstance(agent.get("suppressed_strategy_intents"), list) else None
    loosening_state = _range_loosening_state_machine(m1_loosening_raw, strategy_intents_raw, session_bucket)
    loosening_level = int(loosening_state.get("level", 0) or 0)

    # Hard defensive veto: VOLATILITY_SPIKE / FAKE_TREND / EXTREME block
    # loosening entirely — gold can reverse violently, short-term momentum
    # must not override these defensive signals at any level.
    _hard_defensive = {"VOLATILITY_SPIKE", "FAKE_TREND", "EXTREME"}
    _loosening_blocked = (selection_regime in _hard_defensive
                          or interpreter_regime in _hard_defensive)

    base_selection_regime = selection_regime
    use_loosening = None
    effective_regime_conf = regime_4d_conf
    if not _loosening_blocked:
        if loosening_level >= 2 and loosening_state.get("direction") != "NEUTRAL":
            use_loosening = {"loosening": True, "direction": loosening_state.get("direction", "NEUTRAL"),
                             "confidence": 0.55 if loosening_level == 2 else 0.65}
        if loosening_level >= 4:
            selection_regime = "WEAK_TREND"
            regime_source = "loosening_l4"
            use_loosening = None
            effective_regime_conf = 0.70
        elif loosening_level >= 2 and use_loosening:
            regime_source = "loosening_l2" if loosening_level == 2 else "loosening_l3"
            effective_regime_conf = 0.52 if loosening_level == 2 else 0.60
            if not is_range_regime(base_selection_regime):
                selection_regime = "WEAK_TREND"

    route_family = resolve_route_family(selection_regime, regime_subtype, session_bucket, atr_ratio, range_loosening=use_loosening)
    if (
        route_family != "blocked"
        and use_loosening
        and loosening_level in {2, 3}
        and is_range_regime(base_selection_regime)
    ):
        route_family = "range_loosening"

    # Level 3: force regime reclassify on next cycle by embedding signal in context
    force_reclassify = loosening_level >= 3

    momentum = _extract_momentum_snapshot(mt5_snapshot)
    effective_macro_direction = str(macro.get("effective_direction", macro.get("direction", "NEUTRAL")) or "NEUTRAL").upper()
    market_context = {
        "regime": selection_regime,
        "selection_regime": selection_regime,
        "regime_source": regime_source,
        "regime_confidence": round(regime_4d_conf, 4),
        "effective_regime_confidence": round(effective_regime_conf, 4),
        "regime_4d": regime_4d_label or "UNKNOWN",
        "interpreter_regime": interpreter_regime or "UNKNOWN",
        "regime_subtype": regime_subtype or "UNKNOWN",
        "regime_family": regime_family(selection_regime),
        "route_family": route_family,
        "session_mode": session_mode,
        "session_bucket": session_bucket,
        "timeframe": str(
            momentum.get("timeframe")
            or request.get("timeframe")
            or request.get("interval")
            or plan.get("timeframe")
            or ""
        ).upper(),
        "atr_ratio": round(atr_ratio, 6),
        "macro_authority": str(macro.get("macro_authority", "DISABLED") or "DISABLED").upper(),
        "effective_macro_direction": effective_macro_direction,
        "binding_direction": str(macro.get("binding_direction", macro.get("direction", effective_macro_direction)) or "NEUTRAL").upper(),
        "execution_advice": str(interpreter.get("execution_advice", "ALLOW_FOLLOW") or "ALLOW_FOLLOW").upper(),
        "counter_trend_risk": str(interpreter.get("counter_trend_risk", "LOW") or "LOW").upper(),
        "structural_side": str(interpreter.get("structural_side", "") or "").upper(),
        "momentum": momentum,
        "regime_flags": list(regime_4d.get("flags", [])) if isinstance(regime_4d.get("flags", []), list) else [],
        "transition_count": int(regime_4d.get("transition_count", agent.get("_transition_count", 0)) or 0),
        "regime_change_count": int(regime_4d.get("change_count", agent.get("_regime_change_count", 0)) or 0),
        "force_reclassify": force_reclassify,
        "loosening_level": loosening_level,
        "loosening_direction": loosening_state.get("direction", "NEUTRAL"),
    }
    return market_context
