"""Claude Advisor — Runtime intelligent brain for GoldAutoTrade.

Zero external dependencies beyond requests (already bundled).
Communicates via JSON files + blackboard — no code changes to existing modules.
Supports CCSwitch, Anthropic direct, or DeepSeek fallback.

Architecture:
  ① Communication:  reads blackboard_state.json / trading_ledger.ndjson / execution_state.json
                     writes blackboard + strategy_market_state.json + risk_governor_rules.json
  ② Monitoring:    5-min cycle — anomaly detection, root cause hint, early warning
  ③ Scheduling:    15-min cycle — strategy weight adjustment, parameter tuning
  ④ Repair:        on anomaly — generate fix → validate → execute → verify
  ⑤ Audit:         all actions logged to logs/advisor_history.ndjson

Usage:
  from src.claude_advisor import ClaudeAdvisor
  advisor = ClaudeAdvisor()
  advisor.tick()  # called from autonomy_loop every 60s
"""
from __future__ import annotations

import hashlib
import json
import os
import time
import traceback
from pathlib import Path
from typing import Any

import config_manager

# ── Paths ────────────────────────────────────────────────────────

# ROOT now resolved via frozen_path (PyInstaller-safe)
from frozen_path import APP_ROOT as ROOT
LOGS_DIR = config_manager.get_logs_dir()
DATA_DIR = config_manager.get_data_dir()
CONFIG_DIR = config_manager.get_config_dir()
BLACKBOARD_FILE = LOGS_DIR / "blackboard_state.json"
LEDGER_FILE = LOGS_DIR / "trading_ledger.ndjson"
EXECUTION_FILE = DATA_DIR / "execution_state.json"
MARKET_STATE_FILE = CONFIG_DIR / "strategy_market_state.json"
RISK_RULES_FILE = CONFIG_DIR / "risk_governor_rules.json"
ADVISOR_HISTORY = LOGS_DIR / "advisor_history.ndjson"
ADVISOR_STATE = LOGS_DIR / "advisor_state.json"

# ── LLM endpoint resolution ──────────────────────────────────────

def _resolve_llm() -> dict[str, Any]:
    """Auto-detect LLM from provider system: CCSwitch env > Anthropic provider > DeepSeek provider.

    Priority:
      1. CCSWITCH_URL env var (explicit override)
      2. Anthropic provider from llm_providers.json (if API key configured)
      3. DeepSeek provider from llm_providers.json (system default)
      4. DeepSeek env-var fallback (legacy)
    """
    # 1. CCSwitch explicit override
    ccs = os.environ.get("CCSWITCH_URL", "").strip()
    if ccs:
        return {
            "url": f"{ccs}/v1/chat/completions",
            "key": os.environ.get("CCSWITCH_KEY", "not-needed"),
            "model": os.environ.get("CCSWITCH_MODEL", "claude-sonnet-4-6"),
            "format": "openai",
        }

    # 2. Use provider system for all remaining detection
    try:
        from src.llm.config import load_llm_providers, get_api_key
        providers = load_llm_providers()

        # Anthropic provider (preferred)
        ant = providers.get("anthropic", {})
        ant_key = get_api_key(ant)
        if ant_key and ant.get("baseUrl"):
            ant_models = ant.get("models", [])
            ant_model = ant_models[0]["id"] if ant_models else "claude-sonnet-4-20250514"
            return {
                "url": f"{ant['baseUrl'].rstrip('/')}/messages",
                "key": ant_key,
                "model": ant_model,
                "format": "anthropic",
            }

        # DeepSeek provider (fallback)
        ds = providers.get("deepseek", {})
        ds_key = get_api_key(ds)
        if ds_key and ds.get("baseUrl"):
            ds_models = ds.get("models", [])
            ds_model = ds_models[0]["id"] if ds_models else "deepseek-chat"
            return {
                "url": f"{ds['baseUrl'].rstrip('/')}/chat/completions",
                "key": ds_key,
                "model": ds_model,
                "format": "openai",
            }

        # Try any other configured provider
        for pname, pinfo in providers.items():
            pk = get_api_key(pinfo)
            if pk and pinfo.get("baseUrl") and pname not in ("deepseek", "anthropic"):
                pmodels = pinfo.get("models", [])
                pmodel = pmodels[0]["id"] if pmodels else ""
                api_type = pinfo.get("api", "openai-completions")
                if api_type == "anthropic-messages":
                    return {
                        "url": f"{pinfo['baseUrl'].rstrip('/')}/messages",
                        "key": pk, "model": pmodel, "format": "anthropic",
                    }
                else:
                    return {
                        "url": f"{pinfo['baseUrl'].rstrip('/')}/chat/completions",
                        "key": pk, "model": pmodel, "format": "openai",
                    }
    except Exception:
        pass

    # 3. Legacy env-var fallback (DeepSeek only)
    ds_env_key = os.environ.get("DEEPSEEK_API_KEY", "")
    return {
        "url": "https://api.deepseek.com/v1/chat/completions",
        "key": ds_env_key,
        "model": "deepseek-chat",
        "format": "openai",
    }


# ── Role system prompts ──────────────────────────────────────────

SYSTEM_PROMPTS: dict[str, str] = {
    "monitor": """你是 GoldAutoTrade 交易系统的实时监测专家。
分析系统运行数据，识别异常模式，给出根因初步定位。
输出 JSON: {"anomalies": [...], "severity": "NORMAL|WARNING|CRITICAL", "summary": "..."}""",

    "scheduler": """你是 GoldAutoTrade 的策略调度专家。
根据市场状态和策略表现，动态调整策略权重和参数。
输出 JSON: {"weight_changes": {...}, "param_changes": {...}, "reason": "..."}""",

    "repair": """你是 GoldAutoTrade 的自动修复专家。
针对检测到的异常，生成可执行的修复方案。
方案必须：1)只修改JSON配置文件 2)有回滚路径 3)不影响正在运行的交易。
输出 JSON: {"fix_type": "config|weight|threshold", "changes": {...}, "rollback": {...}, "validation": "..."}""",
}


# ═══════════════════════════════════════════════════════════════════
# Claude Advisor
# ═══════════════════════════════════════════════════════════════════

class ClaudeAdvisor:
    """Runtime intelligent brain. One instance, embedded in main process."""

    def __init__(self) -> None:
        self._llm = _resolve_llm()
        self._llm_cached_at = time.time()
        self._state = self._load_state()
        self._tick_count = 0
        self._last_monitor_ts = 0.0
        self._last_schedule_ts = 0.0
        self._audit_buffer: list[dict] = []

    # ── State ──────────────────────────────────────────────────

    def _load_state(self) -> dict:
        if ADVISOR_STATE.exists():
            try:
                return json.loads(ADVISOR_STATE.read_text(encoding="utf-8"))
            except Exception:
                pass
        return {"total_ticks": 0, "anomalies_detected": 0, "repairs_done": 0,
                "last_anomaly_ts": 0.0, "consecutive_errors": 0}

    def _save_state(self) -> None:
        self._state["total_ticks"] = self._tick_count
        ADVISOR_STATE.parent.mkdir(parents=True, exist_ok=True)
        ADVISOR_STATE.write_text(json.dumps(self._state, indent=2, ensure_ascii=False),
                                 encoding="utf-8")

    # ── Audit ──────────────────────────────────────────────────

    def _audit(self, action: str, detail: dict, result: str = "success") -> None:
        entry = {"ts": time.time(), "action": action, "detail": detail,
                 "result": result, "iso": time.strftime("%Y-%m-%dT%H:%M:%S+08:00")}
        self._audit_buffer.append(entry)
        ADVISOR_HISTORY.parent.mkdir(parents=True, exist_ok=True)
        with open(ADVISOR_HISTORY, "a", encoding="utf-8") as f:
            f.write(json.dumps(entry, ensure_ascii=False) + "\n")

    # ── LLM Call ───────────────────────────────────────────────

    def _call_llm(self, prompt: str, role: str, max_tokens: int = 1500) -> str:
        """Unified LLM call — auto-routes to CCSwitch/Anthropic/DeepSeek."""
        system = SYSTEM_PROMPTS.get(role, SYSTEM_PROMPTS["monitor"])
        ep = self._llm

        if ep["format"] == "anthropic":
            import requests as _r
            resp = _r.post(
                ep["url"],
                headers={"x-api-key": ep["key"], "anthropic-version": "2023-06-01",
                         "content-type": "application/json"},
                json={"model": ep["model"], "max_tokens": max_tokens,
                      "system": system,
                      "messages": [{"role": "user", "content": prompt}]},
                timeout=120,
            )
            resp.raise_for_status()
            data = resp.json()
            return data["content"][0]["text"]
        else:
            # OpenAI-compatible format (CCSwitch, DeepSeek)
            import requests as _r
            resp = _r.post(
                ep["url"],
                headers={"Authorization": f"Bearer {ep['key']}",
                         "Content-Type": "application/json"},
                json={"model": ep["model"], "max_tokens": max_tokens,
                      "messages": [
                          {"role": "system", "content": system},
                          {"role": "user", "content": prompt},
                      ]},
                timeout=120,
            )
            resp.raise_for_status()
            data = resp.json()
            return data["choices"][0]["message"]["content"]

    # ══════════════════════════════════════════════════════════════
    # ② Monitoring (5-min cycle)
    # ══════════════════════════════════════════════════════════════

    def _collect_metrics(self) -> dict[str, Any]:
        """Collect full-dimensional system metrics."""
        metrics: dict[str, Any] = {"timestamp": time.time(), "sources": {}}

        # Trading ledger — last 50 trades
        if LEDGER_FILE.exists():
            try:
                lines = LEDGER_FILE.read_text(encoding="utf-8").strip().split("\n")
                recent = []
                for line in lines[-50:]:
                    if line.strip():
                        try:
                            recent.append(json.loads(line))
                        except json.JSONDecodeError:
                            pass
                if recent:
                    closes = [r for r in recent if r.get("action") == "CLOSE"]
                    wins = sum(1 for r in closes if float(r.get("pnl", 0)) > 0)
                    metrics["trading"] = {
                        "recent_trades": len(closes),
                        "win_rate": round(wins / max(1, len(closes)), 3),
                        "total_pnl": round(sum(float(r.get("pnl", 0)) for r in closes), 2),
                        "last_trade_ts": float(closes[-1].get("ts", 0)) if closes else 0,
                    }
                    metrics["sources"]["ledger"] = True
            except Exception:
                metrics["sources"]["ledger"] = False

        # Blackboard — Macro Commander state
        if BLACKBOARD_FILE.exists():
            try:
                bb = json.loads(BLACKBOARD_FILE.read_text(encoding="utf-8"))
                mc = bb.get("Macro_Commander", {})
                metrics["macro"] = {
                    "direction": mc.get("bias", "UNKNOWN"),
                    "confidence": mc.get("confidence", 0),
                    "degraded": mc.get("degraded", False),
                    "trigger_reason": mc.get("trigger_reason", ""),
                }
                metrics["sources"]["blackboard"] = True
            except Exception:
                metrics["sources"]["blackboard"] = False

        # Direction calibration — consecutive failures
        cal_file = LOGS_DIR / "direction_calibration.json"
        if cal_file.exists():
            try:
                cal = json.loads(cal_file.read_text(encoding="utf-8"))
                metrics["direction_quality"] = {
                    "consecutive_failures": cal.get("consecutive_failures", 0),
                    "total_failures_since_calibrate": cal.get("total_failures_since_calibrate", 0),
                }
            except Exception:
                pass

        return metrics

    def _detect_anomalies(self, metrics: dict) -> list[dict]:
        """Rule-based + LLM anomaly detection."""
        anomalies: list[dict] = []

        trading = metrics.get("trading", {})
        macro = metrics.get("macro", {})
        dq = metrics.get("direction_quality", {})

        # Rule 1: Consecutive losses
        if trading.get("recent_trades", 0) >= 5 and trading.get("win_rate", 1.0) < 0.35:
            anomalies.append({
                "type": "LOW_WIN_RATE",
                "severity": "WARNING",
                "value": trading["win_rate"],
                "hint": f"Recent win rate {trading['win_rate']:.0%} — possible strategy-regime mismatch",
            })

        # Rule 2: Macro Commander stale
        if macro.get("degraded", False):
            anomalies.append({
                "type": "MACRO_DEGRADED",
                "severity": "WARNING",
                "hint": "Macro Commander in degraded state — LLM direction may be unreliable",
            })

        # Rule 3: Direction calibration triggered
        cf = dq.get("consecutive_failures", 0)
        if cf >= 3:
            anomalies.append({
                "type": "DIRECTION_FAILURE_STREAK",
                "severity": "CRITICAL",
                "value": cf,
                "hint": f"{cf} consecutive direction failures — consider reducing position size",
            })

        # Rule 4: No recent trades
        last_ts = trading.get("last_trade_ts", 0)
        if last_ts > 0 and time.time() - last_ts > 7200 and trading.get("recent_trades", 0) > 0:
            anomalies.append({
                "type": "TRADING_PAUSED",
                "severity": "WARNING",
                "hint": f"No trades in {int((time.time()-last_ts)/3600)}h — check MT5 connection",
            })

        return anomalies

    def monitor_cycle(self) -> dict:
        """5-minute monitoring tick."""
        metrics = self._collect_metrics()
        anomalies = self._detect_anomalies(metrics)

        # Deep analysis via LLM only if anomalies found
        llm_analysis = ""
        if anomalies:
            try:
                prompt = f"系统监测数据:\n{json.dumps(metrics, indent=2, ensure_ascii=False)}\n\n检测到异常:\n{json.dumps(anomalies, indent=2, ensure_ascii=False)}\n\n请给出根因初步定位和行动建议。"
                llm_analysis = self._call_llm(prompt, "monitor", max_tokens=1000)
            except Exception as e:
                llm_analysis = f"LLM analysis failed: {e}"

        # Write to blackboard
        result = {
            "ts": time.time(),
            "metrics_summary": {k: v for k, v in metrics.items() if k not in ("sources",)},
            "anomalies": anomalies,
            "llm_analysis": llm_analysis[:500],
        }
        try:
            if BLACKBOARD_FILE.exists():
                bb = json.loads(BLACKBOARD_FILE.read_text(encoding="utf-8"))
            else:
                bb = {}
            bb["Claude_Advisor"] = result
            BLACKBOARD_FILE.parent.mkdir(parents=True, exist_ok=True)
            BLACKBOARD_FILE.write_text(json.dumps(bb, indent=2, ensure_ascii=False), encoding="utf-8")
        except Exception:
            pass

        self._audit("monitor", {"anomaly_count": len(anomalies), "metrics_keys": list(metrics.get("sources", {}).keys())})
        self._state["anomalies_detected"] += len(anomalies)
        if anomalies:
            self._state["last_anomaly_ts"] = time.time()
        self._last_monitor_ts = time.time()
        return result

    # ══════════════════════════════════════════════════════════════
    # ③ Scheduling (15-min cycle)
    # ══════════════════════════════════════════════════════════════

    def schedule_cycle(self) -> dict:
        """15-minute scheduling tick: adjust strategy weights and parameters."""
        # Read current market state
        try:
            if MARKET_STATE_FILE.exists():
                market = json.loads(MARKET_STATE_FILE.read_text(encoding="utf-8"))
            else:
                market = {}
        except Exception:
            market = {}

        # Read recent performance
        perf = self._collect_metrics().get("trading", {})

        # Build LLM prompt
        prompt_parts = [
            "当前策略配置:",
            json.dumps({
                "router": market.get("decision_policy", {}).get("router", {}),
                "entry_policy": market.get("decision_policy", {}).get("execution", {}).get("entry_policy", "unknown"),
            }, indent=2, ensure_ascii=False),
            f"\n最近交易表现: win_rate={perf.get('win_rate',0):.2%}, total_pnl={perf.get('total_pnl',0)}",
            "\n请判断是否需要调整策略权重。如果需要，给出具体的 weight_changes。"
        ]

        changes = {}
        try:
            response = self._call_llm("\n".join(prompt_parts), "scheduler", max_tokens=800)
            # Try to extract JSON from response
            import re
            m = re.search(r'\{.*\}', response, re.DOTALL)
            if m:
                changes = json.loads(m.group(0))
        except Exception:
            pass

        # Apply weight changes if safe
        weight_changes = changes.get("weight_changes", {})
        if weight_changes and isinstance(weight_changes, dict):
            try:
                router = market.get("decision_policy", {}).get("router", {})
                for regime, weights in weight_changes.items():
                    if regime in router and isinstance(weights, dict):
                        current = router[regime].get("rank_weights", [])
                        new_weights = weights.get("rank_weights", current)
                        # Safety: single weight change <= 0.3
                        if all(abs(new_weights[i] - current[i]) <= 0.3
                               for i in range(min(len(current), len(new_weights)))):
                            router[regime]["rank_weights"] = new_weights

                market.setdefault("decision_policy", {})["router"] = router
                MARKET_STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
                MARKET_STATE_FILE.write_text(json.dumps(market, indent=2, ensure_ascii=False),
                                            encoding="utf-8")
                self._audit("schedule_weight_change", {"changes": weight_changes})
            except Exception:
                pass

        self._last_schedule_ts = time.time()
        return {"weight_changes": weight_changes, "perf_snapshot": perf}

    # ══════════════════════════════════════════════════════════════
    # ④ Repair (on-demand, triggered by critical anomalies)
    # ══════════════════════════════════════════════════════════════

    def repair_cycle(self, anomaly: dict) -> dict:
        """Attempt automated repair for a critical anomaly."""
        if anomaly.get("severity") != "CRITICAL":
            return {"action": "skip", "reason": "not_critical"}

        # Safety gate: only attempt repair if we have a working LLM
        if self._state.get("consecutive_errors", 0) >= 3:
            return {"action": "escalate", "reason": "too_many_consecutive_errors"}

        try:
            prompt = f"检测到严重异常:\n{json.dumps(anomaly, indent=2, ensure_ascii=False)}\n\n请生成修复方案。只能修改JSON配置文件，必须有回滚路径。"
            response = self._call_llm(prompt, "repair", max_tokens=1000)
            import re
            m = re.search(r'\{.*\}', response, re.DOTALL)
            if not m:
                return {"action": "skip", "reason": "no_json_in_response"}
            plan = json.loads(m.group(0))
        except Exception as e:
            self._state["consecutive_errors"] += 1
            return {"action": "error", "reason": str(e)}

        # Validate: only JSON config changes, has rollback
        if not plan.get("rollback"):
            return {"action": "reject", "reason": "no_rollback_path"}

        fix_type = plan.get("fix_type", "")
        changes = plan.get("changes", {})

        # Apply based on type
        applied = False
        if fix_type == "threshold" and isinstance(changes, dict):
            try:
                if RISK_RULES_FILE.exists():
                    rules = json.loads(RISK_RULES_FILE.read_text(encoding="utf-8"))
                else:
                    rules = {}
                # Merge changes into risk rules
                for key, val in changes.items():
                    parts = key.split(".")
                    target = rules
                    for p in parts[:-1]:
                        target = target.setdefault(p, {})
                    target[parts[-1]] = val
                RISK_RULES_FILE.parent.mkdir(parents=True, exist_ok=True)
                backup = RISK_RULES_FILE.with_suffix(".json.bak")
                if RISK_RULES_FILE.exists():
                    backup.write_text(RISK_RULES_FILE.read_text(encoding="utf-8"))
                RISK_RULES_FILE.write_text(json.dumps(rules, indent=2, ensure_ascii=False),
                                          encoding="utf-8")
                applied = True
            except Exception:
                pass

        self._audit("repair", {"anomaly": anomaly["type"], "fix_type": fix_type,
                                "applied": applied, "plan": plan})
        if applied:
            self._state["repairs_done"] += 1
            self._state["consecutive_errors"] = 0

        return {"action": "applied" if applied else "rejected", "plan": plan}

    # ══════════════════════════════════════════════════════════════
    # ⑤ Main tick (called from autonomy_loop)
    # ══════════════════════════════════════════════════════════════

    def tick(self) -> dict:
        """Called every 60s from autonomy_loop.
        Returns summary of actions taken.

        Refreshes LLM config cache every 5 minutes so that provider
        changes made via the GUI take effect without a restart.
        """
        self._tick_count += 1
        now = time.time()
        actions: dict[str, Any] = {"tick": self._tick_count}

        # Refresh LLM config every 5 minutes (provider changes via GUI)
        if now - self._llm_cached_at >= 300:
            self._llm = _resolve_llm()
            self._llm_cached_at = now

        # Monitor: every 5 minutes
        if now - self._last_monitor_ts >= 300:
            try:
                actions["monitor"] = self.monitor_cycle()
            except Exception as e:
                actions["monitor_error"] = str(e)
                self._state["consecutive_errors"] += 1

        # Schedule: every 15 minutes
        if now - self._last_schedule_ts >= 900:
            try:
                actions["schedule"] = self.schedule_cycle()
            except Exception as e:
                actions["schedule_error"] = str(e)

        # Repair: triggered by critical anomalies detected in monitor
        monitor_result = actions.get("monitor", {})
        anomalies = monitor_result.get("anomalies", []) if isinstance(monitor_result, dict) else []
        critical = [a for a in anomalies if a.get("severity") == "CRITICAL"]
        if critical:
            try:
                actions["repair"] = self.repair_cycle(critical[0])
            except Exception as e:
                actions["repair_error"] = str(e)

        self._save_state()
        return actions


# ── Singleton ────────────────────────────────────────────────────

_advisor: ClaudeAdvisor | None = None


def get_advisor() -> ClaudeAdvisor:
    global _advisor
    if _advisor is None:
        _advisor = ClaudeAdvisor()
    return _advisor


def advisor_tick() -> dict:
    """Convenience function for autonomy_loop integration."""
    return get_advisor().tick()
