"""llm_verifier.py — LLM secondary verification for signal confidence calibration.

4-stage pipeline:
  Stage 1: Lightweight feature extraction (EMA, RSI, ATR) — already in stream_features
  Stage 2: LLM secondary verification — semantic coherence check + anomaly detection
  Stage 3: Risk calibration — adjust confidence based on regime, spread anomaly, market structure
  Stage 4: Order gate — final GO/HOLD/VETO decision

Design principles:
  - LLM call must complete within 30ms (tensor parallel + dynamic batching)
  - Non-blocking: submits verification request, returns provisional signal immediately
  - Fallback chain: LLM available → rule-based → reject uncertain
  - Real-time feedback loop: execution results flow back within 100ms for online RL
  - Context window: last 5 signals + position state + regime + spread anomaly

Usage:
  from src.llm_verifier import LLMVerifier, verify_signal, calibrate_confidence

  verifier = LLMVerifier()
  verification = verifier.verify(signal=current_signal, features=features, context=context)
  calibrated_signal = verifier.apply_verification(signal, verification)
"""

from __future__ import annotations

try:
    from src.agent_role_router import ROLE_RISK_MANAGER
except ModuleNotFoundError:
    from agent_role_router import ROLE_RISK_MANAGER  # python3 src/llm_verifier.py adds src/ to path

import json
import math
import threading
import time
from collections import deque
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any

import config_manager

APP_DIR = config_manager.get_app_dir()
DATA_DIR = config_manager.get_data_dir()
LOGS_DIR = config_manager.get_logs_dir()

# ── Latency budget ──────────────────────────────────────────────

LLM_VERIFY_TIMEOUT_MS = 8000.0
LLM_RETRY_TIMEOUT_MS = 12000.0
MAX_VERIFICATION_LATENCY_MS = 15000.0

# ── Verification thresholds ───────────────────────────────────
# Loaded from config/risk_governor_rules.json with hardcoded fallback defaults

def _load_llm_thresholds() -> dict:
    try:
        import config_manager
        cfg = config_manager.load_config("risk_governor_rules.json")
        return cfg.get("llm_verifier", {}) if isinstance(cfg, dict) else {}
    except Exception:
        return {}

_llm_cfg = _load_llm_thresholds()

MIN_CONFIDENCE_THRESHOLD = float(_llm_cfg.get("min_confidence_threshold", 0.65))
HIGH_CONFIDENCE_THRESHOLD = float(_llm_cfg.get("high_confidence_threshold", 0.80))
SPREAD_ANOMALY_PENALTY = float(_llm_cfg.get("spread_anomaly_penalty", 0.15))
CIRCUIT_BREAKER_PENALTY = float(_llm_cfg.get("circuit_breaker_penalty", 0.20))
REGIME_CONFLICT_PENALTY = float(_llm_cfg.get("regime_conflict_penalty", 0.12))
LLM_CONFIDENCE_ADJUSTMENT = float(_llm_cfg.get("llm_confidence_adjustment", 0.08))

# ── LLM provider config ─────────────────────────────────────────
# Now routed through src.llm unified module (agent_models.json + openclaw.json)
# Role: 风险管理员


# ── Data structures ─────────────────────────────────────────────

@dataclass
class VerificationContext:
    """Context for LLM verification."""
    signal_side: str = ""  # BUY / SELL / HOLD
    signal_confidence: float = 0.0
    regime: str = "UNKNOWN"
    rsi14: float | None = None
    adx14: float | None = None
    ema9: float | None = None
    ema21: float | None = None
    spread: float = 0.0
    spread_anomaly: bool = False
    price_gap: bool = False
    circuit_open: bool = False
    position_open: bool = False
    position_side: str = ""
    consecutive_losses: int = 0
    session_mode: str = ""
    ts: str = ""


@dataclass
class LLMVerification:
    """LLM verification result."""
    verdict: str = "PENDING"  # CONFIRM / REJECT / UNCERTAIN / TIMEOUT
    adjusted_confidence: float = 0.0
    reasoning: str = ""
    risk_flags: list[str] = field(default_factory=list)
    regime_alignment: str = "NEUTRAL"  # ALIGNED / CONFLICTING / NEUTRAL
    momentum_signal: str = "NEUTRAL"   # BUILDING / FADING / STABLE
    anomaly_detected: bool = False
    latency_ms: float = 0.0
    llm_model: str = ""
    ts: str = ""


@dataclass
class CalibrationResult:
    """Final calibrated signal after all stages."""
    original_confidence: float
    calibrated_confidence: float
    adjustment_reason: str
    final_verdict: str  # GO / HOLD / VETO
    stage1_passed: bool
    stage2_llm_verdict: str
    stage3_risk_calibrated: bool
    stage4_gate_passed: bool
    latency_total_ms: float


# ── LLM verification prompt ──────────────────────────────────────

from src.trading_context import get_trading_context as _get_ctx
_ctx = None


def _get_or_init_ctx():
    global _ctx
    if _ctx is None:
        try:
            _ctx = _get_ctx()
        except Exception:
            _ctx = type("_TradingCtx", (), {"symbol": "XAUUSD"})()
    return _ctx

_get_or_init_ctx()
LLM_VERIFY_PROMPT = f"""你是一个专业的{_ctx.symbol}交易信号验证专家。

当前信号上下文:
- 信号方向: {{signal_side}}
- 信号置信度: {{signal_confidence:.0%}}
- 市场状态: regime={{regime}}, RSI={{rsi}}, ADX={{adx}}, EMA9={{ema9}}, EMA21={{ema21}}
- 价差: {{spread}}点, 价差异常={{spread_anomaly}}
- 熔断状态: circuit_open={{circuit_open}}
- 持仓状态: position_open={{position_open}}, position_side={{position_side}}
- 连亏次数: {{consecutive_losses}}

请判断该信号是否应该执行。返回JSON格式:
{{"verdict": "CONFIRM|REJECT|UNCERTAIN", "reasoning": "...", "risk_flags": ["flag1", ...], "regime_alignment": "ALIGNED|CONFLICTING|NEUTRAL", "momentum_signal": "BUILDING|FADING|STABLE", "anomaly_detected": true/false}}

规则:
- 如果价差异常(spread_anomaly=true)，大幅降低置信度
- 如果熔断开启(circuit_open=true)，建议REJECT
- 如果方向与持仓相反且持仓与信号方向一致，降低置信度
- 连亏>2次时谨慎
- regime与信号方向对齐时CONFIRM，不对齐时REJECT
"""

LLM_VERIFY_PROMPT_CN = (
    f"你是专业{_ctx.symbol}交易信号验证专家。"
    "信号: {signal_side}, 置信度:{signal_confidence:.0%}, 市场:{regime}"
    "/RSI={rsi}/ADX={adx}/EMA9={ema9}/EMA21={ema21}"
    " 价差:{spread}点, 异常:{spread_anomaly}, 熔断:{circuit_open}"
    " 持仓:{position_open}({position_side}), 连亏:{consecutive_losses}。"
    "返回JSON: {{"
    '"verdict":"CONFIRM|REJECT|UNCERTAIN","reasoning":"",'
    '"risk_flags":[],'
    '"regime_alignment":"ALIGNED|CONFLICTING|NEUTRAL",'
    '"momentum_signal":"BUILDING|FADING|STABLE",'
    '"anomaly_detected":false'
    "}}"
)


# ── LLM client ──────────────────────────────────────────────────

def _call_llm_verify(prompt: str, timeout_ms: float = LLM_VERIFY_TIMEOUT_MS) -> dict[str, Any] | None:
    """Call LLM via unified module for signal verification. Returns parsed JSON or None."""
    try:
        from src.llm import call_agent_llm

        result = call_agent_llm(
            role=ROLE_RISK_MANAGER,
            system_prompt=f"你是一个专业的{_ctx.symbol}交易信号验证专家。只返回JSON，不做解释。",
            user_prompt=prompt,
            temperature=0.1,
            max_tokens=1000,
            timeout=int(timeout_ms / 1000) or 1,
        )

        if result.get("degraded"):
            return None

        content = result["content"]
        # Extract JSON from response
        if "```json" in content:
            content = content.split("```json")[1].split("```")[0].strip()
        elif "```" in content:
            content = content.split("```")[1].split("```")[0].strip()
        return {**json.loads(content), "_latency_ms": result.get("latency_ms", 0), "_model": result.get("model", "")}
    except Exception:
        return None


# ── Online RL feedback ─────────────────────────────────────────

class RLFeedbackLoop:
    """100ms feedback loop: execution result → model parameter update."""

    def __init__(self, window_size: int = 100) -> None:
        self._lock = threading.Lock()
        self._feedback_history: deque[dict] = deque(maxlen=window_size)
        self._pending_updates: list[dict] = []
        self._update_count = 0

    def record_execution(
        self,
        signal_id: str,
        direction: str,
        confidence: float,
        llm_verdict: str,
        execution_result: str,  # FILLED / REJECTED / EXPIRED
        actual_pnl: float | None = None,
        latency_ms: float = 0.0,
    ) -> None:
        """Record execution result for online learning."""
        with self._lock:
            record = {
                "signal_id": signal_id,
                "direction": direction,
                "signal_confidence": confidence,
                "llm_verdict": llm_verdict,
                "execution_result": execution_result,
                "actual_pnl": actual_pnl,
                "latency_ms": latency_ms,
                "ts": time.time(),
            }
            self._feedback_history.append(record)
            self._update_count += 1

            # Trigger parameter adjustment if enough data
            if len(self._feedback_history) >= 10:
                self._trigger_parameter_adjustment()

    def _trigger_parameter_adjustment(self) -> None:
        """Analyze recent feedback and adjust verification parameters."""
        records = list(self._feedback_history)
        if not records:
            return

        # Compute verdict accuracy rate
        confirm_records = [r for r in records if r["llm_verdict"] == "CONFIRM"]
        if not confirm_records:
            return

        filled_count = sum(1 for r in confirm_records if r["execution_result"] == "FILLED")
        accuracy = filled_count / len(confirm_records)

        # If CONFIRM accuracy drops below 70%, increase scrutiny
        if accuracy < 0.70:
            self._pending_updates.append({
                "type": "INCREASE_SCRUTINY",
                "accuracy": accuracy,
                "sample_size": len(confirm_records),
                "ts": time.time(),
            })
        elif accuracy > 0.85:
            self._pending_updates.append({
                "type": "RELAX_SCRUTINY",
                "accuracy": accuracy,
                "sample_size": len(confirm_records),
                "ts": time.time(),
            })

    def get_recent_stats(self) -> dict[str, Any]:
        """Get recent feedback statistics."""
        with self._lock:
            records = list(self._feedback_history)
            if not records:
                return {"samples": 0}

            confirm = [r for r in records if r["llm_verdict"] == "CONFIRM"]
            reject = [r for r in records if r["llm_verdict"] == "REJECT"]
            filled = [r for r in records if r["execution_result"] == "FILLED"]

            return {
                "samples": len(records),
                "confirm_count": len(confirm),
                "reject_count": len(reject),
                "filled_count": len(filled),
                "confirm_accuracy": len(filled) / max(1, len(confirm)) if confirm else 0,
                "avg_latency_ms": sum(r.get("latency_ms", 0) for r in records) / max(1, len(records)),
                "pending_updates": len(self._pending_updates),
                "last_update_ts": self._pending_updates[-1]["ts"] if self._pending_updates else 0,
            }


# ── Main verifier ───────────────────────────────────────────────

class LLMVerifier:
    """LLM secondary verification for signal confidence calibration."""

    def __init__(self) -> None:
        self._rl_feedback = RLFeedbackLoop()
        self._verification_cache: dict[str, LLMVerification] = {}
        self._cache_lock = threading.Lock()
        self._verification_count = 0
        self._llm_success_count = 0
        self._thread_local = threading.local()

    def verify(self, context: VerificationContext) -> LLMVerification:
        """Run LLM verification on the given context. Non-blocking with timeout."""
        self._verification_count += 1
        ts = datetime.now(timezone.utc).isoformat()

        # Stage 2: LLM verification
        start = time.time()
        prompt = LLM_VERIFY_PROMPT_CN.format(
            signal_side=context.signal_side,
            signal_confidence=context.signal_confidence,
            regime=context.regime,
            rsi=f"{context.rsi14:.1f}" if context.rsi14 else "N/A",
            adx=f"{context.adx14:.1f}" if context.adx14 else "N/A",
            ema9=f"{context.ema9:.3f}" if context.ema9 else "N/A",
            ema21=f"{context.ema21:.3f}" if context.ema21 else "N/A",
            spread=context.spread,
            spread_anomaly=context.spread_anomaly,
            circuit_open=context.circuit_open,
            position_open=context.position_open,
            position_side=context.position_side,
            consecutive_losses=context.consecutive_losses,
        )
        llm_result = _call_llm_verify(prompt, timeout_ms=LLM_VERIFY_TIMEOUT_MS)

        latency_ms = (time.time() - start) * 1000

        if llm_result is None:
            # Fallback to rule-based verification
            verification = self._rule_based_verification(context)
        else:
            self._llm_success_count += 1
            verification = LLMVerification(
                verdict=llm_result.get("verdict", "UNCERTAIN"),
                adjusted_confidence=context.signal_confidence,
                reasoning=llm_result.get("reasoning", ""),
                risk_flags=llm_result.get("risk_flags", []),
                regime_alignment=llm_result.get("regime_alignment", "NEUTRAL"),
                momentum_signal=llm_result.get("momentum_signal", "STABLE"),
                anomaly_detected=bool(llm_result.get("anomaly_detected", False)),
                latency_ms=latency_ms + llm_result.get("_latency_ms", 0),
                llm_model=llm_result.get("_model", "unknown"),
                ts=ts,
            )

        # Cache the result
        cache_key = f"{context.signal_side}:{context.regime}:{context.signal_confidence:.2f}"
        with self._cache_lock:
            self._verification_cache[cache_key] = verification

        return verification

    def _rule_based_verification(self, ctx: VerificationContext) -> LLMVerification:
        """Fallback rule-based verification when LLM unavailable."""
        risk_flags: list[str] = []
        verdict = "CONFIRM"
        adjustment = 0.0

        # Rule 1: Spread anomaly
        if ctx.spread_anomaly:
            risk_flags.append("SPREAD_ANOMALY")
            verdict = "UNCERTAIN"
            adjustment -= SPREAD_ANOMALY_PENALTY

        # Rule 2: Circuit breaker open — penalty, not hard block
        if ctx.circuit_open:
            risk_flags.append("CIRCUIT_OPEN")
            if verdict == "CONFIRM":
                verdict = "UNCERTAIN"
            adjustment -= CIRCUIT_BREAKER_PENALTY

        # Rule 3: Extreme RSI
        if ctx.rsi14 is not None:
            if ctx.rsi14 > 75 or ctx.rsi14 < 25:
                risk_flags.append("EXTREME_RSI")
                adjustment -= 0.05

        # Rule 4: Low ADX (uncertain regime)
        if ctx.adx14 is not None and ctx.adx14 < 18:
            risk_flags.append("LOW_ADX")
            adjustment -= 0.05

        # Rule 5: Consecutive losses
        if ctx.consecutive_losses >= 3:
            risk_flags.append("CONSECUTIVE_LOSSES")
            adjustment -= 0.08

        # Rule 6: Direction conflict with position
        if ctx.position_open and ctx.position_side:
            if ctx.signal_side == "BUY" and ctx.position_side == "SELL":
                risk_flags.append("POSITION_CONFLICT")
                adjustment -= 0.10
            elif ctx.signal_side == "SELL" and ctx.position_side == "BUY":
                risk_flags.append("POSITION_CONFLICT")
                adjustment -= 0.10

        # Compute adjusted confidence
        adjusted = max(0.1, min(1.0, ctx.signal_confidence + adjustment))

        return LLMVerification(
            verdict=verdict,
            adjusted_confidence=round(adjusted, 3),
            reasoning=f"Rule-based: {', '.join(risk_flags) if risk_flags else 'No risk flags'}",
            risk_flags=risk_flags,
            regime_alignment="NEUTRAL",
            momentum_signal="STABLE",
            anomaly_detected=ctx.spread_anomaly or ctx.price_gap,
            latency_ms=0.5,  # rule-based is instant
            llm_model="RULE_BASED",
            ts=datetime.now(timezone.utc).isoformat(),
        )

    def calibrate(self, original_confidence: float, verification: LLMVerification,
                  features: dict[str, Any]) -> CalibrationResult:
        """Stage 3: Risk calibration — combine LLM verdict with feature state."""
        # Start from LLM/Rule adjusted confidence (avoids double-penalty for rule-based path)
        stage3_adjusted = verification.adjusted_confidence

        # Feature penalties — only when LLM path (delta ~0, penalties not yet applied)
        if abs(verification.adjusted_confidence - original_confidence) < 0.01:
            if features.get("spread_anomaly"):
                stage3_adjusted = max(0.1, stage3_adjusted - 0.10)
            if features.get("consecutive_losses", 0) >= 3:
                stage3_adjusted = max(0.1, stage3_adjusted - 0.08)
        if features.get("circuit_open"):
            stage3_adjusted = max(0.1, stage3_adjusted - 0.15)
        if verification.regime_alignment == "CONFLICTING":
            stage3_adjusted = max(0.1, stage3_adjusted - 0.12)
        elif verification.regime_alignment == "ALIGNED":
            stage3_adjusted = min(1.0, stage3_adjusted + 0.05)

        # Stage 4: regime-aware confidence gate
        stage4_passed = False
        final_verdict = "VETO"
        _spread_ok = not features.get("spread_anomaly")
        # Adjust thresholds by regime: trend signals are more reliable, reversion needs higher confidence
        _reg = str(features.get("regime", "") or "").upper()
        if "TREND" in _reg:
            _min_conf, _high_conf = 0.55, 0.70
        elif _reg in ("RANGING",):
            _min_conf, _high_conf = 0.70, 0.82
        elif _reg in ("VOLATILITY_SPIKE", "EXTREME", "FAKE_TREND"):
            _min_conf, _high_conf = 0.75, 0.85
        else:
            _min_conf, _high_conf = MIN_CONFIDENCE_THRESHOLD, HIGH_CONFIDENCE_THRESHOLD
        if verification.verdict == "CONFIRM" and stage3_adjusted >= _min_conf and _spread_ok:
            stage4_passed = True
            final_verdict = "GO"
        elif verification.verdict == "UNCERTAIN" and stage3_adjusted >= _high_conf and _spread_ok:
            stage4_passed = True
            final_verdict = "GO"
        else:
            final_verdict = "VETO"

        return CalibrationResult(
            original_confidence=original_confidence,
            calibrated_confidence=round(stage3_adjusted, 3),
            adjustment_reason=verification.reasoning or "LLM verification applied",
            final_verdict=final_verdict,
            stage1_passed=True,
            stage2_llm_verdict=verification.verdict,
            stage3_risk_calibrated=True,
            stage4_gate_passed=stage4_passed,
            latency_total_ms=verification.latency_ms,
        )

    def record_execution(self, **kwargs) -> None:
        """Record execution result for online RL feedback."""
        self._rl_feedback.record_execution(**kwargs)

    def get_stats(self) -> dict[str, Any]:
        """Get verifier statistics."""
        with self._cache_lock:
            cache_size = len(self._verification_cache)
        feedback_stats = self._rl_feedback.get_recent_stats()
        return {
            "total_verifications": self._verification_count,
            "cache_size": cache_size,
            "llm_success_rate": round(self._llm_success_count / max(1, self._verification_count), 3),
            "feedback": feedback_stats,
        }


# ── Global singleton ────────────────────────────────────────────

_verifier_instance: LLMVerifier | None = None
_verifier_lock = threading.Lock()


def get_llm_verifier() -> LLMVerifier:
    global _verifier_instance
    with _verifier_lock:
        if _verifier_instance is None:
            _verifier_instance = LLMVerifier()
        return _verifier_instance


def verify_and_calibrate(
    signal_side: str,
    original_confidence: float,
    features: dict[str, Any],
    context_kwargs: dict[str, Any],
) -> CalibrationResult:
    """Convenience function: verify + calibrate in one call."""
    verifier = get_llm_verifier()
    ctx = VerificationContext(
        signal_side=signal_side,
        signal_confidence=original_confidence,
        regime=features.get("regime", "UNKNOWN"),
        rsi14=features.get("rsi14"),
        adx14=features.get("adx14"),
        ema9=features.get("ema9"),
        ema21=features.get("ema21"),
        spread=features.get("spread", 0.0),
        spread_anomaly=features.get("spread_anomaly", False),
        price_gap=features.get("price_gap_detected", False),
        circuit_open=features.get("circuit_open", False),
        consecutive_losses=features.get("consecutive_losses", 0),
        **context_kwargs,
    )
    verification = verifier.verify(ctx)
    return verifier.calibrate(original_confidence, verification, features)


# ── CLI ─────────────────────────────────────────────────────────

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="LLM signal verifier")
    parser.add_argument("--stats", action="store_true", help="Show verifier statistics")
    args = parser.parse_args()

    if args.stats:
        stats = get_llm_verifier().get_stats()
        print(json.dumps(stats, indent=2, ensure_ascii=False))
    else:
        print("LLMVerifier ready. Use get_llm_verifier() in your code.")