#!/usr/bin/env python3
"""
pptagent67/launch_paas.py — 通过 AgentPaaS API 或 Claude Code CLI 启动 PPT 生成
================================================================================

两种运行模式：
  1. AgentPaaS API 模式: 启动 PaaS 服务，注册所有 agent，通过 REST API 执行
  2. Claude Code CLI 桥接模式: 利用 Claude Max Plan 的 claude CLI 作为 LLM 后端

用法:
  # 模式 1: PaaS 全流程
  python launch_paas.py all --port 8000

  # 模式 2: 使用 Claude Code CLI (Max Plan)
  python launch_paas.py claude-code
  python launch_paas.py claude-code --topic "AI 在教育中的应用" --template business
  python launch_paas.py claude-code --workspace ./workspace/claude_run_xxx  # 断点续跑
"""
from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
import time
import urllib.request
import urllib.error
from pathlib import Path
from typing import Dict, Optional

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
AGENT_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(PROJECT_ROOT))

PAAS_URL = os.environ.get("AGENTPAAS_URL", "http://127.0.0.1:8000")
API_KEY = os.environ.get("AGENTPAAS_API_KEY", "")

AGENT_CONFIGS = [
    ("ppt-67-orchestrator",         "PPT 主编排器",   AGENT_DIR / "orchestrator.yml"),
    ("ppt-67-knowledge-extractor",  "知识提取师",     AGENT_DIR / "agents" / "knowledge-extractor.yml"),
    ("ppt-67-outline-generator",    "大纲生成师",     AGENT_DIR / "agents" / "outline-generator.yml"),
    ("ppt-67-content-refiner",      "内容精修师",     AGENT_DIR / "agents" / "content-refiner.yml"),
    ("ppt-67-renderer",             "PPT 渲染师",     AGENT_DIR / "agents" / "ppt-renderer.yml"),
]

# 迭代控制参数
MAX_ROUNDS = 3
QUALITY_THRESHOLD = 0.70
MAX_TURNS_PER_PHASE = 25
PHASE_TIMEOUT = 1800
API_RETRY_MAX = 3
API_RETRY_BACKOFF = 30
PHASE_COOLDOWN = 10


# ═══════════════════════════════════════════════════════════
# 1. PaaS API Helpers
# ═══════════════════════════════════════════════════════════

def api_call(method: str, path: str, data: dict = None) -> dict:
    url = f"{PAAS_URL}/api/v1{path}"
    body = json.dumps(data).encode("utf-8") if data else None
    req = urllib.request.Request(url, data=body, method=method)
    req.add_header("Content-Type", "application/json")
    req.add_header("Authorization", f"Bearer {API_KEY}")
    try:
        with urllib.request.urlopen(req, timeout=600) as resp:
            return json.loads(resp.read())
    except urllib.error.HTTPError as e:
        body = e.read().decode()
        print(f"  API Error {e.code}: {body[:500]}")
        return {"error": body}
    except urllib.error.URLError as e:
        print(f"  Connection error: {e}")
        return {"error": str(e)}


def wait_for_server(timeout: int = 30):
    for i in range(timeout):
        try:
            req = urllib.request.Request(f"{PAAS_URL}/health")
            with urllib.request.urlopen(req, timeout=2):
                return True
        except Exception:
            time.sleep(1)
    return False


# ═══════════════════════════════════════════════════════════
# 2. 命令: serve / register / run / all
# ═══════════════════════════════════════════════════════════

def cmd_serve(args):
    print("Starting AgentPaaS server...")
    proc = subprocess.Popen(
        [sys.executable, "-m", "agentpaas", "serve", "--port", str(args.port), "--dev"],
        cwd=str(PROJECT_ROOT),
        stdout=subprocess.PIPE, stderr=subprocess.PIPE,
    )
    print(f"  PID: {proc.pid}, URL: http://127.0.0.1:{args.port}")
    if not wait_for_server(30):
        print("  ERROR: Server failed to start")
        proc.kill()
        return None
    print("  ✓ Server ready")
    return proc


def cmd_register(args):
    import yaml
    print(f"\nRegistering {len(AGENT_CONFIGS)} agents...")
    agent_ids = {}
    for agent_id, name, yml_path in AGENT_CONFIGS:
        with open(yml_path, "r", encoding="utf-8") as f:
            config = yaml.safe_load(f)
        result = api_call("POST", "/agents", {
            "name": name,
            "description": config.get("description", ""),
            "config": config,
            "tags": ["pptagent67", config.get("type", "react")],
        })
        if "agent_id" in result:
            agent_ids[agent_id] = result["agent_id"]
            print(f"  ✓ {name:20s} → {result['agent_id']}")
        else:
            print(f"  ✗ {name:20s} → {result.get('error', 'Unknown')}")

    mapping_path = AGENT_DIR / ".paas_agent_ids.json"
    mapping_path.write_text(json.dumps(agent_ids, indent=2), encoding="utf-8")
    return agent_ids


def cmd_run(args):
    mapping_path = AGENT_DIR / ".paas_agent_ids.json"
    if not mapping_path.exists():
        print("ERROR: Run 'register' first.")
        return
    agent_ids = json.loads(mapping_path.read_text())
    orchestrator_id = agent_ids.get("ppt-67-orchestrator")
    if not orchestrator_id:
        print("ERROR: Orchestrator not registered.")
        return

    topic = args.topic if hasattr(args, "topic") and args.topic else "[默认主题]"
    result = api_call("POST", f"/agents/{orchestrator_id}/run", {
        "input": topic,
        "parameters": {"template": getattr(args, "template", "academic")},
        "context": {"sub_agent_ids": agent_ids},
    })
    if "error" not in result:
        print(f"  ✓ Run completed: {result.get('run_id')}")
    else:
        print(f"  ✗ Failed: {result['error'][:500]}")


def cmd_all(args):
    global API_KEY
    proc = cmd_serve(args)
    if not proc:
        return
    try:
        key_result = subprocess.run(
            [sys.executable, "-m", "agentpaas", "create-tenant", "--name", "ppt-lab"],
            capture_output=True, text=True, cwd=str(PROJECT_ROOT),
        )
        for line in key_result.stdout.splitlines():
            if line.startswith("API Key:"):
                API_KEY = line.split(":", 1)[1].strip()
                os.environ["AGENTPAAS_API_KEY"] = API_KEY
        cmd_register(args)
        cmd_run(args)
    finally:
        proc.terminate()
        proc.wait(timeout=5)


# ═══════════════════════════════════════════════════════════
# 3. 命令: claude-code — 使用 Claude Code CLI
# ═══════════════════════════════════════════════════════════

def run_phase(phase_dir: Path, agent_name: str, desc: str,
              prompt_content: str, round_num: int) -> str:
    """执行单个阶段，带自动重试"""
    phase_dir.mkdir(parents=True, exist_ok=True)
    (phase_dir / "artifacts").mkdir(exist_ok=True)
    (phase_dir / "prompt.md").write_text(prompt_content, encoding="utf-8")

    print(f"\n{'━'*60}")
    print(f"  Phase: {phase_dir.name} — {desc}")
    print(f"  Agent: {agent_name} | Round: {round_num}")
    print(f"{'━'*60}")

    cmd = [
        "claude", "-p",
        "--model", "claude-opus-4-6",
        "--max-turns", str(MAX_TURNS_PER_PHASE),
        "--allowedTools", "Edit,Write,Read,Bash,Glob,Grep,WebSearch,WebFetch",
    ]

    for attempt in range(1, API_RETRY_MAX + 1):
        t0 = time.time()
        try:
            result = subprocess.run(
                cmd, input=prompt_content,
                capture_output=True, text=True,
                timeout=PHASE_TIMEOUT, cwd=str(phase_dir),
            )
            elapsed = time.time() - t0
            output = result.stdout or ""
            stderr = result.stderr or ""

            is_api_error = any(
                p in output or p in stderr
                for p in ["Request timed out", "overloaded_error", "529", "rate_limit"]
            )
            if is_api_error and attempt < API_RETRY_MAX:
                wait = API_RETRY_BACKOFF * (2 ** (attempt - 1))
                print(f"  ⚠ API error after {elapsed:.0f}s. Retrying in {wait}s...")
                time.sleep(wait)
                continue

            if result.returncode != 0 and stderr:
                output = f"[STDERR] {stderr[:500]}\n{output}"

            (phase_dir / "claude_output.md").write_text(output, encoding="utf-8")
            print(f"  ✓ Done in {elapsed:.0f}s | {len(output)} chars")
            return output

        except subprocess.TimeoutExpired:
            msg = f"[TIMEOUT] {phase_dir.name} timed out after {PHASE_TIMEOUT}s"
            print(f"  ✗ {msg}")
            (phase_dir / "claude_output.md").write_text(msg, encoding="utf-8")
            return msg
        except FileNotFoundError:
            print("  ✗ 'claude' CLI not found.")
            return "[ERROR] claude CLI not found"
        except Exception as e:
            if attempt < API_RETRY_MAX:
                time.sleep(API_RETRY_BACKOFF * attempt)
            else:
                msg = f"[ERROR] {e}"
                (phase_dir / "claude_output.md").write_text(msg, encoding="utf-8")
                return msg

    return "[FAILED] All retries exhausted"


def build_prompt(workspace: Path, phase_dir: Path, desc: str,
                 system_prompt: str, topic: str, round_num: int,
                 template: str, prev_outputs: list,
                 revision_context: str = "") -> str:
    input_parts = [
        f"workspace: {workspace}",
        f"phase_dir: {phase_dir}",
        f"round: {round_num}",
        f"template: {template}",
        f"\n## PPT 主题:\n{topic[:8000]}",
    ]
    if revision_context:
        input_parts.append(f"\n## 修改建议 (Round {round_num - 1}):\n{revision_context[:4000]}")
    for prev in prev_outputs[-2:]:
        input_parts.append(f"\n## Previous phase output:\n{prev[:3000]}")

    return f"""# {desc}

## System Instructions
{system_prompt[:8000]}

## Input Context
{chr(10).join(input_parts)}

## Important
- 将所有产出文件写入 {phase_dir}/
- 先写 work_plan.md，再执行，最后写 report.json 和 report.md
- report.json 必须包含 _meta 字段（含 phase, round, status）
- 这是 Round {round_num}{'，请根据修改建议重点改进' if round_num > 1 else ''}
"""


def cmd_claude_code(args):
    """通过 Claude Code CLI 执行 PPT 生成流程"""
    import yaml
    from datetime import datetime

    global MAX_ROUNDS, MAX_TURNS_PER_PHASE, PHASE_TIMEOUT, PHASE_COOLDOWN
    if hasattr(args, "max_rounds") and args.max_rounds:
        MAX_ROUNDS = args.max_rounds
    if hasattr(args, "max_turns") and args.max_turns:
        MAX_TURNS_PER_PHASE = args.max_turns

    topic = args.topic if hasattr(args, "topic") and args.topic else ""
    template = args.template if hasattr(args, "template") and args.template else "academic"

    if not topic:
        topic_path = AGENT_DIR / "TOPIC.md"
        if topic_path.exists():
            topic = topic_path.read_text(encoding="utf-8")
        else:
            print("ERROR: No topic. Use --topic or create TOPIC.md")
            return

    print(f"""
╔══════════════════════════════════════════════════════════════╗
║  PPTAgent-67 via Claude Code CLI                             ║
║  Template: {template:<10s} | Threshold: {QUALITY_THRESHOLD:.0%} | Max rounds: {MAX_ROUNDS}  ║
╚══════════════════════════════════════════════════════════════╝
""")

    # 工作区
    if hasattr(args, "workspace") and args.workspace:
        workspace = Path(args.workspace)
    else:
        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        workspace = AGENT_DIR / "workspace" / f"claude_run_{ts}"
        workspace.mkdir(parents=True, exist_ok=True)
    print(f"  Workspace: {workspace}")
    print(f"  Topic: {topic[:80]}...")

    MAIN_PHASES = [
        ("01_knowledge_extraction", "knowledge-extractor", "从知识库提取信息"),
        ("02_outline_generation",   "outline-generator",   "生成 PPT 大纲"),
        ("03_content_refinement",   "content-refiner",     "精修内容 & 质量评估"),
        ("04_ppt_rendering",        "ppt-renderer",        "渲染 .pptx 文件"),
    ]

    for round_num in range(1, MAX_ROUNDS + 1):
        print(f"\n{'═'*60}")
        print(f"  ROUND {round_num}/{MAX_ROUNDS}")
        print(f"{'═'*60}")

        prev_outputs = []
        revision_context = ""

        for phase_idx, (phase_name, agent_name, desc) in enumerate(MAIN_PHASES):
            phase_dir = workspace / f"round_{round_num}" / phase_name

            # 跳过已完成
            if (phase_dir / "report.json").exists():
                print(f"\n  ⏭ Skipping {phase_name} (exists)")
                existing = (phase_dir / "claude_output.md").read_text(encoding="utf-8") \
                    if (phase_dir / "claude_output.md").exists() else ""
                prev_outputs.append(existing[:5000])
                continue

            agent_yml = AGENT_DIR / "agents" / f"{agent_name}.yml"
            with open(agent_yml, "r", encoding="utf-8") as f:
                config = yaml.safe_load(f)

            prompt = build_prompt(
                workspace, phase_dir, desc,
                config.get("systemPrompt", ""), topic,
                round_num, template, prev_outputs,
                revision_context=revision_context,
            )
            output = run_phase(phase_dir, agent_name, desc, prompt, round_num)
            prev_outputs.append(output[:5000])

            # 精修阶段后检查质量
            if phase_name == "03_content_refinement":
                report_path = phase_dir / "report.json"
                quality = 0.0
                if report_path.exists():
                    try:
                        data = json.loads(report_path.read_text(encoding="utf-8"))
                        quality = data.get("quality_score", 0)
                    except Exception:
                        pass

                print(f"\n  Quality: {quality:.2f} (threshold: {QUALITY_THRESHOLD})")
                if quality < QUALITY_THRESHOLD and round_num < MAX_ROUNDS:
                    print(f"  ⚠ Below threshold. Will iterate.")
                    revision_context = output[:4000]
                    break  # 跳过渲染，进入下一轮

            if phase_idx < len(MAIN_PHASES) - 1:
                time.sleep(PHASE_COOLDOWN)

        # 检查是否渲染完成
        pptx_dir = workspace / f"round_{round_num}" / "04_ppt_rendering"
        if (pptx_dir / "report.json").exists():
            print(f"\n  ✅ PPT rendered! Round {round_num} complete.")
            break

    print(f"\n{'═'*60}")
    print(f"  Pipeline complete. Workspace: {workspace}")
    print(f"{'═'*60}\n")


# ═══════════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════════

def main():
    parser = argparse.ArgumentParser(description="PPTAgent-67 PaaS Launcher")
    sub = parser.add_subparsers(dest="command")

    serve_p = sub.add_parser("serve")
    serve_p.add_argument("--port", type=int, default=8000)

    sub.add_parser("register")

    run_p = sub.add_parser("run")
    run_p.add_argument("--topic", type=str, default="")
    run_p.add_argument("--template", type=str, default="academic")

    all_p = sub.add_parser("all")
    all_p.add_argument("--port", type=int, default=8000)
    all_p.add_argument("--topic", type=str, default="")
    all_p.add_argument("--template", type=str, default="academic")

    cc_p = sub.add_parser("claude-code")
    cc_p.add_argument("--topic", type=str, default="")
    cc_p.add_argument("--template", type=str, default="academic",
                       choices=["academic", "business", "minimal", "tech"])
    cc_p.add_argument("--workspace", type=str, default=None)
    cc_p.add_argument("--max-rounds", type=int, default=None)
    cc_p.add_argument("--max-turns", type=int, default=None)

    args = parser.parse_args()
    commands = {
        "serve": cmd_serve,
        "register": cmd_register,
        "run": cmd_run,
        "all": cmd_all,
        "claude-code": cmd_claude_code,
    }

    if args.command in commands:
        commands[args.command](args)
    else:
        parser.print_help()
        print("\nExamples:")
        print('  python launch_paas.py claude-code --topic "AI 在医疗中的应用" --template business')
        print("  python launch_paas.py all --port 8000")


if __name__ == "__main__":
    main()
