"""Unified strategy lifecycle manager.

Full promotion chain::

    sandbox ──(review)──→ marketplace ──(deploy)──→ paper ──→ canary ──→ full
       ↑                      │                        │          │         │
    AI 生成              策略集市备用              paper 实盘    canary    full
    (不可执行)           (可被发现,不可执行)       (轻量执行)  (部分执行) (全量)

Rollback paths exist at every stage for safety.

Usage::

    from src.strategy_lifecycle import get_stage, transition, list_strategies_by_stage

    stage = get_stage("my_strategy")
    result = transition("my_strategy", "marketplace", "passed_review", {"reviewer": "human"})
"""

from __future__ import annotations

import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List

# ── unified promotion chain ──────────────────────────────────────

LIFECYCLE_STAGES = ["sandbox", "marketplace", "paper", "canary", "full"]

VALID_TRANSITIONS: Dict[str, List[str]] = {
    "sandbox":     ["marketplace"],                           # review → marketplace
    "marketplace": ["paper", "sandbox"],                      # deploy → paper, or reject → sandbox
    "paper":       ["canary", "marketplace"],                 # promote → canary, or demote
    "canary":      ["full", "paper"],                         # promote → full, or demote
    "full":        ["canary"],                                # demote only
}

# ── Stages that allow live signal execution ──
EXECUTABLE_STAGES = {"paper", "canary", "full"}

# ── Stages that are visible/discoverable but not executable ──
STANDBY_STAGES = {"sandbox", "marketplace"}


# ── state path resolution ────────────────────────────────────────

def _lifecycle_path(root: Path, strategy_id: str) -> Path:
    return root / "strategies" / strategy_id / "lifecycle.json"


# ── public API ───────────────────────────────────────────────────

def get_stage(strategy_id: str, root: Path | None = None) -> str:
    """Return the current lifecycle stage for a strategy.

    Priority:
      1. lifecycle.json (authoritative, set by transition())
      2. manifest.json trust_level (migration path for pre-lifecycle strategies)
      3. "sandbox" (default for strategies with no lifecycle record)
    """
    if root is None:
        root = Path(".")
    path = _lifecycle_path(root, strategy_id)
    if path.exists():
        try:
            data = json.loads(path.read_text(encoding="utf-8"))
            stage = data.get("stage", "")
            if stage in LIFECYCLE_STAGES:
                return stage
        except (json.JSONDecodeError, OSError):
            pass
    # Migration: read trust_level from manifest.json
    manifest_path = root / "strategies" / f"{strategy_id}.manifest.json"
    if manifest_path.exists():
        try:
            manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
            tl = str(manifest.get("trust_level", "") or "").lower()
            # Map old trust_level values to lifecycle stages
            if tl in ("builtin", "proven", "live_full", "live_reduced", "full"):
                return "full"
            if tl in ("paper", "canary"):
                return tl
            if tl == "marketplace":
                return "marketplace"
            if tl == "sandbox":
                return "sandbox"
            # "shadow" → sandbox (migration)
            if tl == "shadow":
                return "sandbox"
        except (json.JSONDecodeError, OSError):
            pass
    return "sandbox"


def transition(
    strategy_id: str,
    to_stage: str,
    reason: str,
    metrics: Dict[str, Any] | None = None,
    root: Path | None = None,
) -> Dict[str, Any]:
    """Promote or demote a strategy, with validation.

    Returns ``{"ok": True, ...}`` on success or ``{"ok": False, "error": ...}``
    when the transition is not allowed.
    """
    if root is None:
        root = Path(".")
    current = get_stage(strategy_id, root)

    allowed = VALID_TRANSITIONS.get(current, [])
    if to_stage not in allowed:
        return {
            "ok": False,
            "error": f"Invalid transition: {current} → {to_stage} (allowed: {allowed})",
        }

    path = _lifecycle_path(root, strategy_id)
    path.parent.mkdir(parents=True, exist_ok=True)

    entry = {
        "strategy_id": strategy_id,
        "stage": to_stage,
        "previous_stage": current,
        "transitioned_at": datetime.now(timezone.utc).isoformat(),
        "reason": reason,
        "metrics_at_transition": metrics or {},
    }

    # load existing, append to history
    if path.exists():
        try:
            data = json.loads(path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            data = {}
        history: list = data.get("history", [])
        history.append(entry)
        data["stage"] = to_stage
        data["history"] = history[-50:]  # keep last 50 entries
    else:
        data = {
            "strategy_id": strategy_id,
            "stage": to_stage,
            "created_at": datetime.now(timezone.utc).isoformat(),
            "history": [entry],
        }

    path.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")

    # Sync lifecycle_stage to unified strategy state
    try:
        from src.unified_strategy_state import load_unified_state, save_unified_state, invalidate_cache
        invalidate_cache()
        unified = load_unified_state()
        strategies = unified.get("strategies", {})
        if isinstance(strategies, dict) and strategy_id in strategies:
            strategies[strategy_id]["lifecycle_stage"] = to_stage
            save_unified_state(unified)
    except Exception:
        pass  # non-blocking: lifecycle.json remains the audit trail

    return {"ok": True, "from": current, "to": to_stage, "reason": reason}


def list_strategies_by_stage(root: Path | None = None) -> Dict[str, List[str]]:
    """Group all tracked strategies by lifecycle stage."""
    if root is None:
        root = Path(".")
    result: Dict[str, List[str]] = {s: [] for s in LIFECYCLE_STAGES}
    strategies_dir = root / "strategies"
    if not strategies_dir.exists():
        return result
    for d in sorted(strategies_dir.iterdir()):
        if d.is_dir():
            stage = get_stage(d.name, root)
            result.setdefault(stage, []).append(d.name)
    return result


# ── Deployment helpers ───────────────────────────────────────────

def _sync_manifest_trust_level(strategy_id: str, trust_level: str, root: Path) -> bool:
    """Update the manifest.json trust_level to match lifecycle stage."""
    manifest_path = root / "strategies" / f"{strategy_id}.manifest.json"
    if not manifest_path.exists():
        return False
    try:
        manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
        manifest["trust_level"] = trust_level
        manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")
        return True
    except Exception:
        return False


def approve_to_marketplace(
    strategy_id: str,
    reviewer: str = "human",
    notes: str = "",
    root: Path | None = None,
) -> Dict[str, Any]:
    """Approve a sandbox strategy → marketplace (standby pool, not yet executable)."""
    if root is None:
        root = Path(".")
    result = transition(strategy_id, "marketplace",
                        f"approved_by:{reviewer}" + (f" | {notes}" if notes else ""),
                        {"reviewer": reviewer, "notes": notes},
                        root=root)
    if result.get("ok"):
        _sync_manifest_trust_level(strategy_id, "marketplace", root)
    return result


def deploy_to_paper(
    strategy_id: str,
    reason: str = "",
    root: Path | None = None,
) -> Dict[str, Any]:
    """Deploy a marketplace strategy → paper (live executable)."""
    if root is None:
        root = Path(".")
    result = transition(strategy_id, "paper", reason, root=root)
    if result.get("ok"):
        _sync_manifest_trust_level(strategy_id, "paper", root)
    return result


def reject_to_sandbox(
    strategy_id: str,
    reason: str = "",
    root: Path | None = None,
) -> Dict[str, Any]:
    """Reject a strategy back to sandbox for rework."""
    if root is None:
        root = Path(".")
    result = transition(strategy_id, "sandbox", reason, root=root)
    if result.get("ok"):
        _sync_manifest_trust_level(strategy_id, "sandbox", root)
    return result


def list_marketplace_strategies(root: Path | None = None) -> list[Dict[str, Any]]:
    """List all strategies in the marketplace (standby, ready to deploy)."""
    if root is None:
        root = Path(".")
    strategies_dir = root / "strategies"
    if not strategies_dir.exists():
        return []
    result: list[Dict[str, Any]] = []
    for mf in sorted(strategies_dir.glob("*.manifest.json")):
        try:
            data = json.loads(mf.read_text(encoding="utf-8"))
        except Exception:
            continue
        sid = data.get("strategy_id", mf.stem.split(".manifest")[0])
        stage = get_stage(sid, root)
        data["_lifecycle_stage"] = stage
        data["_manifest_path"] = str(mf)
        result.append(data)
    return result
