"""Code evolution orchestrator — coordinates strategy discovery, sandbox
validation, and parameter evolution.

Phase 1: plugs the missing bridge between legacy signal generators and the
StrategyPlugin interface, enables file-based strategy discovery, and runs
sandbox backtests on new strategies before allowing promotion.
"""

from __future__ import annotations

import json
import time as _time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import config_manager


def run_code_evolution_cycle(
    project_root: Path | None = None,
    tenant_id: str = "default",
    source: str = "manual",
    evolution_report: dict[str, Any] | None = None,
    evolution_verdict: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Run one complete evolution cycle.

    Steps:
      1. Rescan dynamic strategy manifests.
      2. For each dynamic strategy at trust_level=sandbox, run sandbox
         backtest to decide whether it can promote to paper.
      3. Run parameter evolution (EV001-EV004 from strategy_evolution).
      4. Persist the combined report.

    Returns a report dict suitable for logging and LLM analysis.
    """
    report: dict[str, Any] = {
        "cycle_ts": datetime.now(timezone.utc).isoformat(),
        "phase": 1,
        "tenant_id": tenant_id,
        "source": source,
        "evolution_verdict": evolution_verdict or {},
        "dynamic": _evolve_dynamic_strategies(),
        "parameter": _evolve_parameters(),
    }
    _persist_report(report)
    return report


# -- dynamic strategy sandbox validation --------------------------------

def _evolve_dynamic_strategies() -> dict[str, Any]:
    """Rescan dynamic strategies and validate sandbox-tier ones."""
    result: dict[str, Any] = {"scanned": 0, "sandboxed": 0, "promotions": []}

    try:
        from strategy_sdk.dynamic_registry import get_dynamic_strategy_registry
        registry = get_dynamic_strategy_registry()
        new_count = registry.rescan()
        result["scanned"] = new_count

        for sid in registry.list_ids():
            spec = registry.get_spec(sid)
            if spec.trust_level != "sandbox":
                continue
            result["sandboxed"] += 1
            promotion = _validate_and_promote(registry, spec)
            if promotion:
                result["promotions"].append(promotion)
    except Exception as exc:
        result["error"] = str(exc)

    return result


def _validate_and_promote(registry, spec) -> dict[str, Any] | None:
    """Run sandbox backtest on a sandbox-tier strategy.

    If the strategy fails sandbox gates, attempt automated reflection + regeneration
    (up to 5 iterations). Only returns KEEP_IN_SANDBOX if all iterations fail.
    """
    from strategy_sdk.sandbox import SandboxParams, StrategySandbox

    strategy_id = spec.strategy_id

    try:
        plugin = registry.instantiate(strategy_id)
    except Exception as exc:
        return {"strategy_id": strategy_id, "error": f"instantiate: {exc}"}

    bars = _load_sandbox_bars()
    if len(bars) < SandboxParams().min_bars:
        return {
            "strategy_id": strategy_id,
            "error": f"insufficient bars ({len(bars)})",
        }

    # First attempt
    sandbox = StrategySandbox(plugin, bars, SandboxParams())
    report = sandbox.run()
    passes = _check_sandbox_gates(report)
    reflection_rounds = 0
    reflection_log: list[dict[str, Any]] = []

    # ── Sandbox reflection loop ──
    if not passes:
        reflection_result = _reflect_and_regenerate(
            strategy_id=strategy_id,
            registry=registry,
            spec=spec,
            report=report,
            bars=bars,
            max_iterations=5,
        )
        if reflection_result.get("regenerated"):
            # Re-run sandbox with improved code
            try:
                plugin_v2 = registry.instantiate(strategy_id)
                sandbox_v2 = StrategySandbox(plugin_v2, bars, SandboxParams())
                report_v2 = sandbox_v2.run()
                passes_v2 = _check_sandbox_gates(report_v2)
                reflection_rounds = reflection_result.get("rounds", 0)
                reflection_log = reflection_result.get("log", [])
                if passes_v2:
                    report = report_v2
                    passes = True
                else:
                    # Use the better of the two reports
                    if report_v2.get("score", 0) > report.get("score", 0):
                        report = report_v2
            except Exception:
                pass

    return {
        "strategy_id": strategy_id,
        "name": spec.name,
        "version": spec.version,
        "passes": passes,
        "metrics": {
            k: report.get(k)
            for k in (
                "total_trades", "wins", "losses", "total_pnl",
                "win_rate", "max_drawdown_pct", "sharpe_ratio",
                "profit_factor", "score",
            )
            if k in report
        },
        "recommendation": (
            "PROMOTE_TO_PAPER" if passes else "KEEP_IN_SANDBOX"
        ),
        "reflection_rounds": reflection_rounds,
        "reflection_log": reflection_log,
    }


def _reflect_and_regenerate(
    strategy_id: str,
    registry,
    spec,
    report: dict[str, Any],
    bars: list[dict[str, Any]],
    max_iterations: int = 5,
) -> dict[str, Any]:
    """When sandbox gates fail, call LLM to analyze root cause and regenerate.

    Returns:
        {"regenerated": bool, "rounds": int, "log": [...]}
    """
    from src.strategy_reflector import (
        normalize_evaluation,
        classify_failure_mode,
        build_reflect_sandbox_prompt,
    )

    metrics = {
        k: report.get(k)
        for k in ("total_trades", "wins", "losses", "total_pnl",
                   "win_rate", "max_drawdown_pct", "sharpe_ratio",
                   "profit_factor", "score", "guards_tripped")
    }
    normalized = normalize_evaluation("sandbox", {}, raw_metrics=metrics)
    failure_mode = classify_failure_mode(normalized)
    log: list[dict[str, Any]] = []

    # Read current strategy code
    try:
        strategy_py_path = config_manager.get_project_dir() / "strategies" / f"{strategy_id}.py"
        current_code = strategy_py_path.read_text(encoding="utf-8") if strategy_py_path.exists() else ""
    except Exception:
        current_code = ""

    if not current_code:
        return {"regenerated": False, "rounds": 0, "log": [{"error": "no source code found"}]}

    regenerated = False
    for iteration in range(1, max_iterations + 1):
        log.append({
            "iteration": iteration,
            "failure_mode": failure_mode,
            "metrics_snapshot": metrics,
        })

        try:
            from src.llm import call_agent_llm
            user_prompt = build_reflect_sandbox_prompt(
                strategy_id, current_code, normalized, metrics,
            )
            result = call_agent_llm(
                role="AI 策略开发师",
                system_prompt=(
                    "你是黄金自动交易系统的策略反思专家，负责分析沙盒回测失败原因并输出改进方案。"
                    "只返回一个 JSON 对象，不要解释。"
                    "必须包含 root_cause, failure_mode, improvements（数组）, improved_signal_body（Python代码字符串）, confidence。"
                ),
                user_prompt=user_prompt,
                temperature=0.3,
                max_tokens=3500,
                timeout=120,
                response_format={"type": "json_object"},
            )
        except Exception as e:
            log[-1]["error"] = f"LLM call failed: {e}"
            break

        if result.get("degraded", False):
            log[-1]["error"] = "LLM degraded"
            break

        import json as _json
        content = str(result.get("content", ""))
        # Extract JSON object from potential markdown wrapping
        try:
            parsed = _json.loads(content)
        except _json.JSONDecodeError:
            # Try extracting from markdown code block
            if "```json" in content:
                content = content.split("```json")[1].split("```")[0]
            elif "```" in content:
                parts = content.split("```")
                if len(parts) >= 2:
                    content = parts[1]
            try:
                parsed = _json.loads(content.strip())
            except _json.JSONDecodeError:
                log[-1]["error"] = "invalid JSON response"
                break

        new_signal_body = str(parsed.get("improved_signal_body", "") or "")
        if len(new_signal_body) < 50:
            log[-1]["error"] = "improved_signal_body too short"
            break

        # Write improved code back
        try:
            # Build complete strategy file
            new_code = _stamp_signal_body_into_plugin(
                strategy_id, spec.name, new_signal_body, spec,
            )
            strategy_py_path.write_text(new_code, encoding="utf-8")
            current_code = new_code
            regenerated = True
            log[-1]["regenerated"] = True
            log[-1]["root_cause"] = str(parsed.get("root_cause", ""))
            log[-1]["improvements"] = parsed.get("improvements", [])
            log[-1]["confidence"] = parsed.get("confidence", 0.5)
        except Exception as e:
            log[-1]["error"] = f"write failed: {e}"
            break

    return {"regenerated": regenerated, "rounds": len(log), "log": log}


def _stamp_signal_body_into_plugin(
    strategy_id: str, display_name: str,
    signal_body: str, spec,
) -> str:
    """Stamp an improved signal body into a StrategyPlugin template."""
    import textwrap
    return f'''"""
{display_name}
Auto-improved by Sandbox Reflection Loop.
Strategy ID: {strategy_id}
"""
from strategy_sdk.interfaces import StrategyPlugin
from strategy_sdk.types import (
    MarketSnapshot,
    PortfolioStateView,
    StrategyInitContext,
    StrategyIntent,
    StrategyManifest,
    StrategyRiskOpinion,
)


class {_to_class_name(strategy_id)}(StrategyPlugin):
    """{display_name}"""

    def __init__(self, params=None):
        self._params = params or {{}}
        self._ctx = None

    def manifest(self):
        return StrategyManifest(
            strategy_id="{strategy_id}",
            name="{display_name}",
            version="1.0.0",
            kind="signal",
            entrypoint="strategies.{strategy_id}",
            description="{display_name}",
            author="SandboxReflector",
            tags=["ai-improved"],
            capabilities=["intraday"],
            supported_symbols=["XAUUSD"],
            supported_timeframes=["M5", "M15", "H1"],
            params_schema={{}},
            default_mode="plugin",
            trust_level="sandbox",
        )

    def initialize(self, ctx: StrategyInitContext) -> None:
        self._ctx = ctx
        if ctx.config:
            self._params.update(ctx.config)

    def on_market_snapshot(self, snapshot: MarketSnapshot) -> StrategyIntent:
{textwrap.indent(signal_body, "        ")}

    def on_risk_check(
        self,
        intent: StrategyIntent,
        portfolio: PortfolioStateView,
    ) -> StrategyRiskOpinion:
        if intent.action == "HOLD":
            return StrategyRiskOpinion(allow=True, reasons=["no action"])
        if intent.confidence < 0.5:
            return StrategyRiskOpinion(allow=False, severity="WARN",
                                        reasons=["low confidence"], score=intent.confidence)
        return StrategyRiskOpinion(allow=True, severity="INFO",
                                    reasons=["signal OK"], score=intent.confidence)

    def shutdown(self) -> None:
        pass
'''


def _check_sandbox_gates(report: dict[str, Any]) -> bool:
    """Minimum criteria for a sandbox strategy to qualify for paper trading."""
    return (
        report.get("total_trades", 0) >= 10
        and report.get("win_rate", 0) >= 40.0
        and report.get("profit_factor", 0) >= 1.0
        and report.get("max_drawdown_pct", 100) <= 50.0
        and not report.get("guards_tripped", True)
    )


def _load_sandbox_bars() -> list[dict[str, Any]]:
    """Load historical bars from the offline data cache if available."""
    data_dir = config_manager.get_data_dir() / "offline"
    cache = data_dir / "price_cache.json"
    if cache.exists():
        try:
            return json.loads(cache.read_text(encoding="utf-8"))
        except Exception:
            pass
    return []


# -- parameter evolution (delegate) ------------------------------------

def _evolve_parameters() -> dict[str, Any]:
    """Run parameter-level evolution (EV001-EV004)."""
    try:
        from src.strategy_evolution import generate_evolution_report, apply_suggestions
    except ImportError:
        try:
            from strategy_evolution import generate_evolution_report, apply_suggestions
        except ImportError:
            return {"error": "strategy_evolution module not importable"}

    offline_path = config_manager.get_data_dir() / "offline" / "best_params.json"
    ledger_path = config_manager.get_logs_dir() / "trading_ledger.ndjson"

    offline_stats: dict[str, Any] = {}
    if offline_path.exists():
        try:
            offline_stats = json.loads(offline_path.read_text(encoding="utf-8"))
        except Exception:
            pass
    try:
        from src.strategy_evolution import normalize_offline_stats
        offline_stats = normalize_offline_stats(offline_stats)
    except Exception:
        pass

    trades: list[dict[str, Any]] = []
    if ledger_path.exists():
        try:
            for line in ledger_path.read_text(encoding="utf-8").strip().splitlines():
                if line.strip():
                    trades.append(json.loads(line))
        except Exception:
            pass

    evolution_report = generate_evolution_report(offline_stats, trades)
    suggestions = apply_suggestions(evolution_report)
    return {
        "suggestions_count": len(suggestions),
        "suggestions": suggestions,
    }


# -- persistence -------------------------------------------------------

def _to_class_name(strategy_id: str) -> str:
    return "".join(p.capitalize() for p in strategy_id.replace("-", "_").split("_"))


def _persist_report(report: dict[str, Any]) -> None:
    out_dir = config_manager.get_logs_dir()
    out_dir.mkdir(parents=True, exist_ok=True)
    report_path = out_dir / "code_evolution_report_latest.json"
    report_path.write_text(
        json.dumps(report, indent=2, default=str, ensure_ascii=False),
        encoding="utf-8",
    )
