"""signal_arbitrator.py — Multi-strategy signal conflict resolution.

Resolves conflicts when multiple strategies produce signals on the same bar.
Implements the three-level coordination model:

  Level 1 — Regime gate:      Blocks strategies that don't match current market regime
  Level 2 — Direction merge:   Same-direction signals stack; opposite signals arbitrate
  Level 3 — Position cap:      Enforces max_positions and total exposure limits

Usage::

    from src.signal_arbitrator import arbitrate

    signals = [
        {"strategy_id": "ema_macd", "side": "BUY", "confidence": 0.78, "timeframe": "H1"},
        {"strategy_id": "rsi_extreme", "side": "SELL", "confidence": 0.65, "timeframe": "M5"},
    ]
    accepted, rejected, meta = arbitrate(signals, regime="WEAK_TREND")
"""
from __future__ import annotations
from typing import Any
from src.strategy_registry import (
    CANONICAL_STRATEGY_REGISTRY,
    LLM_TO_REGIME,
    SESSION_NOISE_BLOCK,
    STRATEGY_TYPE_PRIORITY,
    TIMEFRAME_PRIORITY,
    canonical_strategy_id,
    get_strategy_runtime_contract,
    is_strategy_regime_compatible,
    strategy_type_priority_for_route,
    strategy_style,
)

STRATEGY_TYPES: dict[str, str] = {
    strategy_id: str(spec.get("style", "hybrid") or "hybrid")
    for strategy_id, spec in CANONICAL_STRATEGY_REGISTRY.items()
}
REGIME_ALLOWLIST: dict[str, list[str]] = {
    strategy_id: list(spec.get("applicable_regimes", []))
    for strategy_id, spec in CANONICAL_STRATEGY_REGISTRY.items()
}


# ── Arbitration functions ─────────────────────────────────────────

def _strategy_type(signal: dict) -> str:
    return strategy_style(signal.get("strategy_id", ""))


def _strategy_type_priority(signal: dict, route_family: str = "", regime: str = "") -> float:
    strategy_id = signal.get("strategy_id", "")
    base_priority = 1
    if str(route_family or "").strip():
        base_priority = strategy_type_priority_for_route(strategy_id, route_family)
    else:
        base_priority = STRATEGY_TYPE_PRIORITY.get(_strategy_type(signal), 1)
    
    # New optimization: Dynamic performance-based weighting
    performance_multiplier = 1.0
    try:
        from src.learning.trading_brain import get_brain
        performance_multiplier = get_brain().get_strategy_performance_multiplier(strategy_id, regime)
    except Exception:
        pass
        
    return base_priority * performance_multiplier


def _timeframe_priority(signal: dict) -> int:
    tf = str(signal.get("timeframe", "M5") or "M5").upper()
    return TIMEFRAME_PRIORITY.get(tf, 1)


def _signal_sort_key(signal: dict, route_family: str = "", regime: str = "") -> tuple[int, float, int]:
    """Sort signals by: timeframe priority → confidence → strategy type priority.
    Higher = wins arbitration.
    """
    return (_timeframe_priority(signal),
            float(signal.get("confidence", 0.5) or 0.5),
            _strategy_type_priority(signal, route_family, regime))


def _normalize_signal(signal: dict) -> dict:
    """Ensure signal has all required fields for arbitration."""
    return {
        "strategy_id": canonical_strategy_id(signal.get("strategy_id", "unknown")),
        "side": str(signal.get("side", "NONE") or "NONE").upper(),
        "confidence": float(signal.get("confidence", 0.5) or 0.5),
        "timeframe": str(signal.get("timeframe", "M5") or "M5").upper(),
        "volume_plan": signal.get("volume_plan", signal.get("volume_lots", 0.01)),
        "entry": signal.get("entry"),
        "sl": signal.get("sl"),
        "tp": signal.get("tp"),
        "session_mode": str(signal.get("session_mode", "") or "").upper(),
        "regime_confidence": float(signal.get("regime_confidence", 1.0) or 1.0),
        "strategy_instance_id": str(signal.get("_instance_id") or signal.get("instance_id") or signal.get("strategy_id", "") or ""),
        "_counter_direction_allowed": bool(signal.get("_counter_direction_allowed", False)),
        "_original": signal.get("_original", signal),
    }



def _regime_gate(signals: list[dict], regime: str, route_family: str = "") -> tuple[list[dict], list[dict]]:
    """Filter signals using the unified runtime contract registry."""
    passed = []
    blocked = []
    
    # New optimization: Market environment adaptation (Volatility check)
    try:
        from src.market_analyzer import detect_extreme_market
        # We need bars/closes to call this. If not available, we skip.
        # This is a conceptual addition as arbitrate() doesn't receive bars.
    except ImportError:
        pass

    for s in signals:
        compatible, reason = is_strategy_regime_compatible(
            s.get("strategy_id"),
            regime,
            regime_confidence=float(s.get("regime_confidence", 1.0) or 1.0),
            session_mode=str(s.get("session_mode", "") or "").upper(),
            route_family=route_family,
        )
        # Secondary safety catch: trend strategies should never trade in RANGING.
        if compatible and regime == "RANGING" and _strategy_type(s) == "trend":
            compatible = False
            reason = "range_trend_strategy_hard_block"
        if compatible:
            passed.append(s)
        else:
            blocked.append({**s, "_block_reason": f"regime_gate:{regime}:{reason or 'strategy_not_allowed'}"})
    return passed, blocked


def _conflict_circuit_breaker(signals: list[dict]) -> bool:
    """If there's high consensus conflict (e.g. 3 BUY vs 3 SELL), trigger a circuit breaker.
    
    Returns True if circuit breaker is triggered.
    """
    buys = len([s for s in signals if s.get("side") == "BUY"])
    sells = len([s for s in signals if s.get("side") == "SELL"])

    if min(buys, sells) >= 3:
        return True
    if len(signals) >= 5 and buys >= 2 and sells >= 2:
        return True
    return False


def _resolve_opposites(signals: list[dict], route_family: str = "", regime: str = "") -> tuple[list[dict], list[dict]]:
    """Resolve conflicting signals (opposite directions).

    Strategy: sort by priority key, then walk from highest to lowest.
    Each signal is accepted if its direction doesn't conflict with any
    higher-priority accepted signal. Conflicts are rejected.

    Counter-direction exemption: mean-reversion strategies with
    _counter_direction_allowed=True can coexist with trend signals.
    """
    if len(signals) <= 1:
        return list(signals), []

    sorted_signals = sorted(
        signals,
        key=lambda signal: _signal_sort_key(signal, route_family, regime),
        reverse=True,
    )
    accepted: list[dict] = []
    rejected: list[dict] = []

    for sig in sorted_signals:
        conflicts = False
        for acc in accepted:
            if sig["side"] != acc["side"] and sig["side"] in ("BUY", "SELL") and acc["side"] in ("BUY", "SELL"):
                # Counter-direction exemption: mean-reversion can oppose trend
                sig_type = _strategy_type(sig)
                acc_type = _strategy_type(acc)
                sig_exempt = sig.get("_counter_direction_allowed", False)
                acc_exempt = acc.get("_counter_direction_allowed", False)

                if sig_type == "mean_reversion" and sig_exempt:
                    continue  # exempt
                if acc_type == "mean_reversion" and acc_exempt:
                    continue  # exempt

                conflicts = True
                break

        if conflicts:
            rejected.append({**sig, "_block_reason": "direction_conflict"})
        else:
            accepted.append(sig)

    return accepted, rejected


# ── Category flood detection ─────────────────────────────────────────

def _detect_same_category_flood(signals: list[dict]) -> list[dict]:
    """同类别信号超过2个时，只保留置信度最高的2个，其余抑制。

    防止单一 regime 下同类策略（如4个趋势策略）同时发出信号，
    造成过度集中和方向同质化风险。
    """
    from collections import defaultdict
    by_category: dict[str, list[dict]] = defaultdict(list)
    for sig in signals:
        cat = _strategy_type(sig)
        by_category[cat].append(sig)

    result: list[dict] = []
    for cat, sigs in by_category.items():
        if len(sigs) > 2:
            sigs.sort(key=lambda s: float(s.get("confidence", 0.0) or 0.0), reverse=True)
            for s in sigs[2:]:
                s["_suppressed"] = True
                s["_suppressed_reason"] = f"CATEGORY_FLOOD:{cat}"
            result.extend(sigs[:2])
        else:
            result.extend(sigs)
    return result


# ── Counter-style conflict resolution ─────────────────────────────────

def _resolve_counter_style_conflict(signals: list[dict], route_family: str = "", regime: str = "") -> list[dict]:
    """趋势 vs 回归冲突：低置信度趋势让步给高置信度回归。

    场景：WEAK_TREND 下趋势策略看多（conf=0.55），
          但回归策略看空（conf=0.72）。
          趋势置信度不够时，回归信号优先。
    """
    trend_sigs = [s for s in signals if _strategy_type(s) == "trend" and not s.get("_suppressed")]
    meanrev_sigs = [s for s in signals if _strategy_type(s) == "mean_reversion" and not s.get("_suppressed")]

    if not trend_sigs or not meanrev_sigs:
        return signals
    if str(route_family or "").strip().lower() == "range":
        return signals
    if regime in ("RANGING", "TRANSITION"):
        # In ranging/transition regimes mean-reversion is the preferred style.
        # Do not let trend signals suppress mean-reversion signals.
        return signals

    regime_confidence = float(signals[0].get("regime_confidence", 1.0) or 1.0)

    # In weak-trend regimes with low confidence, only strong trend consensus
    # (conf >= 0.70) may suppress opposite mean-reversion signals.
    if regime == "WEAK_TREND" and regime_confidence < 0.65:
        weak_trend = all(float(t.get("confidence", 0.0) or 0.0) < 0.70 for t in trend_sigs)
    else:
        weak_trend = all(float(t.get("confidence", 0.0) or 0.0) < 0.75 for t in trend_sigs)
    if weak_trend:
        # 回归信号不受压制（已是正常流程）
        pass
    else:
        # 趋势很强 → 压制相反的回归信号。但仅当趋势信号方向一致时有效。
        _trend_sides = {str(t.get("side","")).upper() for t in trend_sigs if str(t.get("side","")).upper() in ("BUY","SELL")}
        _trend_consensus = len(_trend_sides) == 1
        if _trend_consensus:
            _dominant = next(iter(_trend_sides))
            for m in meanrev_sigs:
                if m.get("side") != _dominant and m.get("side") in ("BUY", "SELL"):
                    m["_suppressed"] = True
                    m["_suppressed_reason"] = "MEAN_REVERSION_OVERRIDE_BY_STRONG_TREND"

    return signals


def _apply_position_cap(accepted: list[dict], max_positions: int = 3, regime: str = "") -> tuple[list[dict], list[dict]]:
    """Limit total accepted signals to max_positions, keeping highest priority."""
    if len(accepted) <= max_positions:
        return accepted, []

    sorted_signals = sorted(accepted, key=lambda s: _signal_sort_key(s, regime=regime), reverse=True)
    return sorted_signals[:max_positions], [
        {**s, "_block_reason": f"position_cap:{max_positions}"}
        for s in sorted_signals[max_positions:]
    ]


def _enforce_total_exposure(accepted: list[dict], max_total_volume: float = 0.06) -> list[dict]:
    """Scale down volumes if total exceeds max_total_volume."""
    total_vol = sum(float(s.get("volume_plan", 0.01) or 0.01) for s in accepted)
    if total_vol <= max_total_volume:
        return accepted

    scale = max_total_volume / total_vol
    for s in accepted:
        original = float(s.get("volume_plan", 0.01) or 0.01)
        s["volume_plan"] = round(original * scale, 2)
        s["_volume_scaled"] = True
        s["_volume_original"] = original
    return accepted


# ── Main entry point ──────────────────────────────────────────────

def arbitrate(
    signals: list[dict],
    regime: str = "UNKNOWN",
    *,
    route_family: str = "",
    max_positions: int = 3,
    max_total_volume: float = 0.06,
    garbage_allowed_types: list[str] | None = None,
    garbage_size_multiplier: float = 1.0,
) -> dict[str, Any]:
    """Resolve multi-strategy signal conflicts.

    Args:
        signals: List of signal dicts. Each must have at minimum:
                 {strategy_id, side, confidence}. Optional: timeframe, volume_plan.
        regime: Current market regime label from regime_router.
        max_positions: Hard cap on number of simultaneous accepted signals.
        max_total_volume: Hard cap on total position size.

    Returns:
        dict with:
          accepted   — list of signals to execute
          rejected   — list of signals that were blocked (with _block_reason)
          meta       — arbitration metadata for logging
    """
    if not signals:
        return {
            "accepted": [],
            "rejected": [],
            "meta": {"regime": regime, "route_family": route_family, "stages": []},
        }

    normalized = [_normalize_signal(s) for s in signals]
    meta_stages: list[dict] = []

    # Stage 1: Regime gate
    passed, regime_blocked = _regime_gate(normalized, regime, route_family)
    meta_stages.append({"stage": "regime_gate", "passed": len(passed), "blocked": len(regime_blocked)})

    # Stage 1.25: Garbage type gate — when garbage detector constrains allowed strategy types
    # None = not active; [] = nothing allowed (DEAD_ZONE)
    if garbage_allowed_types is not None:
        garbage_passed = []
        garbage_blocked = []
        for s in passed:
            stype = strategy_style(s.get("strategy_id"))
            if stype in garbage_allowed_types:
                garbage_passed.append(s)
            else:
                garbage_blocked.append({
                    **s,
                    "_block_reason": f"garbage_type_gate:{stype}_not_in_{sorted(garbage_allowed_types)}"
                })
        meta_stages.append({
            "stage": "garbage_type_gate",
            "passed": len(garbage_passed),
            "blocked": len(garbage_blocked),
            "allowed_types": sorted(garbage_allowed_types),
        })
        passed = garbage_passed
        regime_blocked = regime_blocked + garbage_blocked

    # Stage 1.5: Conflict Circuit Breaker
    if _conflict_circuit_breaker(passed):
        meta_stages.append({"stage": "conflict_circuit_breaker", "triggered": True})
        return {
            "accepted": [],
            "rejected": normalized,
            "meta": {
                "regime": regime,
                "route_family": route_family,
                "stages": meta_stages,
                "input_count": len(signals),
                "output_count": 0,
                "reason": "CONFLICT_CIRCUIT_BREAKER_TRIGGERED"
            },
        }

    # Stage 2: Direction conflict resolution
    accepted, dir_rejected = _resolve_opposites(passed, route_family, regime)
    meta_stages.append({"stage": "direction_arbitration", "accepted": len(accepted), "rejected": len(dir_rejected)})
    rejected = regime_blocked + dir_rejected

    # Stage 2.5: Category flood detection — max 2 per strategy type
    before_flood = len(accepted)
    accepted = _detect_same_category_flood(accepted)
    flood_suppressed = before_flood - len([s for s in accepted if not s.get("_suppressed")])
    meta_stages.append({"stage": "category_flood", "before": before_flood, "suppressed": flood_suppressed})

    # Stage 2.6: Counter-style conflict — weak trend vs strong mean-reversion
    accepted = _resolve_counter_style_conflict(accepted, route_family, regime)
    suppressed = [s for s in accepted if s.get("_suppressed")]
    if suppressed:
        rejected.extend(
            {
                **s,
                "_block_reason": str(
                    s.get("_suppressed_reason") or "suppressed_by_signal_arbitrator"
                ),
            }
            for s in suppressed
        )
    accepted = [s for s in accepted if not s.get("_suppressed")]
    meta_stages.append({"stage": "counter_style", "remaining": len(accepted), "suppressed": len(suppressed)})

    # Stage 2.7: Size scaling (regime conviction + garbage detector)
    # garbage_size_multiplier combines CHOP_ZONE reduction and regime confidence.
    # Scale all accepted volumes proportionally.
    if garbage_size_multiplier != 1.0:
        _sz_before = sum(float(s.get("volume_plan", 0.01) or 0.01) for s in accepted)
        for s in accepted:
            _orig = float(s.get("volume_plan", 0.01) or 0.01)
            _new = round(_orig * garbage_size_multiplier, 2)
            if _new != _orig:
                s["_size_original"] = _orig
                s["_size_scaled"] = True
            s["volume_plan"] = _new
        _sz_after = sum(float(s.get("volume_plan", 0.01) or 0.01) for s in accepted)
    else:
        _sz_before = _sz_after = 0.0
    meta_stages.append({"stage": "size_scaling", "multiplier": garbage_size_multiplier, "before": round(_sz_before, 4), "after": round(_sz_after, 4)})

    # Stage 3: Position cap
    accepted, cap_rejected = _apply_position_cap(accepted, max_positions, regime)
    rejected.extend(cap_rejected)
    meta_stages.append({"stage": "position_cap", "accepted": len(accepted), "capped": len(cap_rejected)})

    # Stage 4: Total exposure enforcement
    accepted = _enforce_total_exposure(accepted, max_total_volume)
    meta_stages.append({"stage": "exposure_limit", "final": len(accepted)})

    return {
        "accepted": accepted,
        "rejected": rejected,
        "meta": {
            "regime": regime,
            "route_family": route_family,
            "stages": meta_stages,
            "input_count": len(signals),
            "output_count": len(accepted),
        },
    }
