#!/usr/bin/env python3
"""询盘采集统一入口（v3.0 三档模式 + 真实工作流）

⚠️ 真实接口约定（已 grep 源码验证）：
  - scrape_inquiries.py [--max N] [--depth quick] [--stdout-json]  ⚠️ 不是 --limit/--output
  - normalize_aw_inquiries.py <input> --output <out>     位置参数 + --output
  - score_inquiry_v3.py <input> --json                   位置参数 + stdout JSON
  - build_platform_writeback.py <score> --config <cfg> --output <out>
  - poll_new_inquiries.py [--dry-run] [--test-push]      自扫 outputs/score_*.json
  - notify_dispatcher.py --payload <file>                单条推送（poll 生成的 payload）

⚠️ 输出文件命名约定（被 poll_new_inquiries.py 依赖）：
  - 评分文件必须命名 score_<timestamp>.json（不是 scored_*.json）
  - score_30.json 是基线文件，poll 会跳过
  - 含 "compare" 的文件 poll 也会跳过

【三档模式】
  --mode trial-sample      首次试跑（默认）：采 5-10 条 → normalize → score → 报告
  --mode new-only          常驻模式（cron 用）：增量采 → normalize → score → poll → notify
  --mode full-backfill     全量回溯：采全部历史 + 断点续传 + 批量评分（不推送）

【全链路】
  采集 → normalize → score (v3) → [writeback payload] → poll (diff) → notify
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import subprocess
import datetime
from pathlib import Path

# === 自动推断 WORKSPACE_ROOT（跨平台）===
WORKSPACE_ROOT = Path(__file__).resolve().parent.parent
SCRIPTS_DIR = WORKSPACE_ROOT / "scripts"
ENGINE_DIR = WORKSPACE_ROOT / "scripts_engine"
OUTPUTS_DIR = WORKSPACE_ROOT / "outputs"
TEMPLATES_DIR = WORKSPACE_ROOT / "templates"
CONFIG_DIR = WORKSPACE_ROOT / "config"
STATE_DIR = WORKSPACE_ROOT / "state"
RUN_LOG_DIR = WORKSPACE_ROOT / "run_logs"

for d in (OUTPUTS_DIR, CONFIG_DIR, STATE_DIR, RUN_LOG_DIR):
    d.mkdir(exist_ok=True)


def _ts() -> str:
    return datetime.datetime.now().strftime("%Y%m%d_%H%M%S")


def _log(stage: str, payload: dict) -> Path:
    """阶段日志落盘"""
    log_file = RUN_LOG_DIR / f"run_{_ts()}_{stage}.json"
    payload = {"_stage": stage, "_timestamp": datetime.datetime.now().isoformat(), **payload}
    log_file.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
    return log_file


def _run(cmd: list, log_stdout_path: Path = None, capture: bool = False) -> tuple[int, str, str]:
    """执行命令，可选捕获 stdout 落盘（用于 score_inquiry_v3.py 这种 stdout-json 脚本）"""
    print(f"   📡 $ {' '.join(str(c) for c in cmd)}")
    if capture:
        proc = subprocess.run(cmd, cwd=str(WORKSPACE_ROOT), capture_output=True, text=True)
        if log_stdout_path and proc.stdout:
            log_stdout_path.write_text(proc.stdout, encoding="utf-8")
        if proc.stderr.strip():
            print(f"   stderr: {proc.stderr.strip()[:200]}")
        return proc.returncode, proc.stdout, proc.stderr
    else:
        rc = subprocess.call(cmd, cwd=str(WORKSPACE_ROOT))
        return rc, "", ""


# ============================================================================
# 全链路钩子
# ============================================================================

def _pipeline_normalize(raw_file: Path) -> Path | None:
    """raw → normalized.json"""
    out_file = OUTPUTS_DIR / f"normalized_{_ts()}.json"
    cmd = [sys.executable, str(SCRIPTS_DIR / "normalize_aw_inquiries.py"),
           str(raw_file), "--output", str(out_file)]
    rc, _, _ = _run(cmd)
    if rc != 0 or not out_file.exists():
        _log("normalize_failed", {"raw_file": str(raw_file), "rc": rc})
        return None
    print(f"   ✅ {out_file.name}")
    return out_file


def _pipeline_score(normalized_file: Path) -> Path | None:
    """normalized → score_<ts>.json（命名遵循 poll_new_inquiries 约定）"""
    out_file = OUTPUTS_DIR / f"score_{_ts()}.json"
    cmd = [sys.executable, str(SCRIPTS_DIR / "score_inquiry_v3.py"),
           str(normalized_file), "--json"]
    rc, stdout, _ = _run(cmd, log_stdout_path=out_file, capture=True)
    if rc != 0 or not out_file.exists():
        _log("score_failed", {"normalized_file": str(normalized_file), "rc": rc})
        return None
    # 解析 summary 给用户看
    try:
        data = json.loads(stdout)
        s = data.get("summary", {})
        gc = s.get("grade_counts", {})
        print(f"   ✅ {out_file.name} | 总 {s.get('total')} 条 | "
              f"S={gc.get('S',0)} A={gc.get('A',0)} B={gc.get('B',0)} "
              f"C={gc.get('C',0)} 无效={gc.get('无效',0)}")
    except Exception:
        print(f"   ✅ {out_file.name}")
    return out_file


def _pipeline_writeback(score_file: Path) -> Path | None:
    """score → writeback payload（飞书/钉钉 command_plan）"""
    cfg = TEMPLATES_DIR / "platform_writeback_config.sample.json"
    if not cfg.exists():
        print(f"   ⚠️  跳过 writeback：缺少配置 {cfg.name}")
        return None
    out_file = OUTPUTS_DIR / f"writeback_{_ts()}.json"
    cmd = [sys.executable, str(SCRIPTS_DIR / "build_platform_writeback.py"),
           str(score_file), "--config", str(cfg), "--output", str(out_file)]
    rc, _, _ = _run(cmd)
    if rc != 0 or not out_file.exists():
        _log("writeback_failed", {"score_file": str(score_file), "rc": rc})
        return None
    print(f"   ✅ {out_file.name}")
    return out_file


def _pipeline_poll_notify(dry_run: bool = False) -> None:
    """扫所有 score_*.json → 找新询盘 → 写 push_payloads/ → 调 notify_dispatcher"""
    args = [sys.executable, str(SCRIPTS_DIR / "poll_new_inquiries.py")]
    if dry_run:
        args.append("--dry-run")
    rc, _, _ = _run(args)
    _log("poll_done", {"dry_run": dry_run, "rc": rc})
    if dry_run or rc != 0:
        return

    # poll 已经把新增的 payload 写到 outputs/push_payloads/
    # notify_dispatcher 每条 payload 推一次
    payload_dir = OUTPUTS_DIR / "push_payloads"
    if not payload_dir.exists():
        print(f"   ℹ️  无 push_payloads/ 目录，跳过推送")
        return
    # 只推本轮新生成的（按 mtime 过滤过去 60 秒）
    now = datetime.datetime.now().timestamp()
    new_payloads = [p for p in payload_dir.glob("*.json") if now - p.stat().st_mtime < 60]
    print(f"\n🔔 待推送 payload：{len(new_payloads)} 条")
    for payload in new_payloads:
        cmd = [sys.executable, str(SCRIPTS_DIR / "notify_dispatcher.py"),
               "--payload", str(payload)]
        _run(cmd)
    _log("notify_done", {"pushed": len(new_payloads)})


def _run_full_pipeline(raw_file: Path, enable_notify: bool = True) -> None:
    """完整链路：raw → normalize → score → writeback → [poll → notify]"""
    if not raw_file or not raw_file.exists():
        print("⚠️  原始采集文件不存在，跳过后续链路")
        return

    print(f"\n{'─'*60}")
    print(f"🔗 全链路：{raw_file.name}")
    print(f"{'─'*60}")

    normalized = _pipeline_normalize(raw_file)
    if not normalized:
        print("❌ normalize 失败，链路中止")
        return

    score = _pipeline_score(normalized)
    if not score:
        print("❌ score 失败，链路中止")
        return

    # writeback 是可选的（需要 templates/platform_writeback_config.sample.json）
    _pipeline_writeback(score)

    if enable_notify:
        print(f"\n{'─'*60}")
        print(f"🔔 增量推送（poll → notify）")
        print(f"{'─'*60}")
        _pipeline_poll_notify(dry_run=False)
    else:
        print(f"\nℹ️  本模式跳过推送（仅评分）")


# ============================================================================
# 三档模式
# ============================================================================

def run_trial_sample(sample_size: int = 10, enable_pipeline: bool = True) -> int:
    """🟢 首次试跑：采 5-10 条 + 评分 + 不推送"""
    print(f"\n{'='*60}\n🟢 trial-sample | 样本量 {sample_size}\n{'='*60}")

    out_file = OUTPUTS_DIR / f"trial_sample_{_ts()}.json"
    engine_script = ENGINE_DIR / "scrape_inquiries.py"

    if not engine_script.exists():
        print(f"❌ 找不到引擎脚本：{engine_script}")
        print(f"   请检查解压是否完整，scripts_engine/ 目录必须存在")
        return 1

    # 讲师版引擎真实参数（grep 源码确认 2026-05-26）：
    #   --max N           最多抓 N 条（替代 --limit）
    #   --depth quick     不翻详情，试跑用最快档
    #   --stdout-json     JSON 走 stdout，不写默认 inquiries.json
    # 我们捕获 stdout 写到时间戳文件，避免污染项目根目录
    cmd = [sys.executable, str(engine_script),
           "--max", str(sample_size),
           "--depth", "quick",
           "--stdout-json"]
    print(f"📡 采集中（最多 {sample_size} 条，quick 模式）...")
    rc, stdout, stderr = _run(cmd, log_stdout_path=out_file, capture=True)

    if rc != 0 or not out_file.exists() or out_file.stat().st_size < 10:
        print(f"\n⚠️  采集失败（rc={rc}）")
        if stderr.strip():
            print(f"   stderr 末尾: {stderr.strip()[-400:]}")
        print(f"   常见原因：1) 未装 playwright（pip install playwright && playwright install chromium）")
        print(f"            2) 国际站登录态失效（先跑 --clear-login-state 重新登录）")
        print(f"            3) 引擎 CLI 已变（查 scripts_engine/ENGINE_SKILL_REF.md）")
        print(f"   不联网测试链路：python3 scripts/run_scrape.py --mode dry-run-pipeline")
        return rc or 1

    print(f"✅ 采集完成：{out_file.name} ({out_file.stat().st_size // 1024} KB)")
    if enable_pipeline:
        _run_full_pipeline(out_file, enable_notify=False)
        print(f"\n📌 下一步：检查 outputs/score_*.json 评分合理性，OK 后开 cron 跑 new-only")
    return 0


def run_new_only(enable_pipeline: bool = True, enable_notify: bool = True) -> int:
    """🔵 常驻模式：增量采集 → 评分 → poll → notify（cron 用）"""
    print(f"\n{'='*60}\n🔵 new-only | cron 增量\n{'='*60}")

    out_file = OUTPUTS_DIR / f"new_only_{_ts()}.json"
    engine_script = ENGINE_DIR / "scrape_inquiries.py"

    if not engine_script.exists():
        print(f"❌ 找不到引擎脚本：{engine_script}")
        return 1

    # 讲师版引擎真实参数：--mode new 是默认（今日新建询盘），
    # 增量去重由下游 poll_new_inquiries.py 通过 seen_inquiry_ids.json 完成，
    # 不依赖引擎层做时间窗 diff
    cmd = [sys.executable, str(engine_script),
           "--mode", "new",
           "--stdout-json"]
    rc, stdout, stderr = _run(cmd, log_stdout_path=out_file, capture=True)

    if rc != 0 or not out_file.exists() or out_file.stat().st_size < 10:
        print(f"⚠️  本轮采集失败或无新询盘（rc={rc}）")
        if stderr.strip() and rc != 0:
            print(f"   stderr 末尾: {stderr.strip()[-300:]}")
        # 即使没新数据，也跑一次 poll（处理之前积压的 score 文件）
        if enable_pipeline and enable_notify:
            _pipeline_poll_notify(dry_run=False)
        return 0

    print(f"✅ 采集：{out_file.name} ({out_file.stat().st_size // 1024} KB)")
    if enable_pipeline:
        _run_full_pipeline(out_file, enable_notify=enable_notify)
    return 0


def run_full_backfill(days: int = 90, batch_days: int = 7,
                       force: bool = False, enable_pipeline: bool = True) -> int:
    """🔴 全量回溯：断点续传 + 批量评分（不推送）"""
    print(f"\n{'='*60}\n🔴 full-backfill | 过去 {days} 天 / 分片 {batch_days} 天\n{'='*60}")
    print("⚠️  数小时～半天，消耗大量后台 API 配额")
    print("⚠️  默认不推送（避免历史数据轰炸）\n")

    if not force:
        ans = input("输入 YES（全部大写）确认执行：").strip()
        if ans != "YES":
            print("已取消。")
            return 0

    manager_script = ENGINE_DIR / "accio_scrape_manager.py"
    cmd = [sys.executable, str(manager_script),
           "--total-days", str(days),
           "--batch-days", str(batch_days)]
    rc, _, _ = _run(cmd)

    if enable_pipeline:
        raw_dir = OUTPUTS_DIR / "inquiries" / "raw"
        if raw_dir.exists():
            shards = sorted(raw_dir.glob("**/*.json"))
            print(f"\n🔗 批量评分 {len(shards)} 个分片（不推送）")
            for shard in shards:
                _run_full_pipeline(shard, enable_notify=False)
        else:
            print(f"\nℹ️  未找到 {raw_dir}，跳过批量评分")
    return rc


def run_dry_run_pipeline() -> int:
    """🧪 链路自检：用 outputs/pilot_inquiries_30.json 跑完整链路（不联网）"""
    print(f"\n{'='*60}\n🧪 dry-run-pipeline | 用真实样本测链路\n{'='*60}")

    sample = OUTPUTS_DIR / "pilot_inquiries_30.json"
    if not sample.exists():
        print(f"❌ 找不到样本文件：{sample}")
        return 1

    _run_full_pipeline(sample, enable_notify=False)
    print(f"\n✅ 链路自检完成。所有阶段产物在 outputs/")
    print(f"📋 阶段日志在 run_logs/")
    print(f"\n如果以上 4 个阶段（normalize→score→writeback→poll）都成功，")
    print(f"说明 v3 工作链路已贯通，可以放心开 trial-sample / new-only 模式")
    return 0


# ============================================================================
# 主入口
# ============================================================================

def main():
    p = argparse.ArgumentParser(
        description="询盘采集统一入口（v3.0 三档模式 + 真实工作流）",
        formatter_class=argparse.RawTextHelpFormatter,
    )
    p.add_argument(
        "--mode",
        choices=["trial-sample", "new-only", "full-backfill", "dry-run-pipeline"],
        default="trial-sample",
        help="""采集模式：
  trial-sample        首次试跑（默认）：采 5-10 条 + 评分，不推送
  new-only            常驻模式（cron 用）：增量采集 + 评分 + 自动推送 S/A
  full-backfill       全量回溯：断点续传，批量评分（不推送）
  dry-run-pipeline    链路自检：用真实样本走完整链路（不联网，验证脚本接口）""",
    )
    p.add_argument("--sample-size", type=int, default=10, help="trial-sample：5-20")
    p.add_argument("--backfill-days", type=int, default=90)
    p.add_argument("--batch-days", type=int, default=7)
    p.add_argument("--force", action="store_true", help="full-backfill：跳过二次确认")
    p.add_argument("--no-pipeline", action="store_true", help="只采集，不走 normalize/score/notify")
    p.add_argument("--no-notify", action="store_true", help="仅 new-only：跑评分但不推送")

    args = p.parse_args()
    enable_pipeline = not args.no_pipeline

    if args.mode == "trial-sample":
        n = max(5, min(args.sample_size, 20))
        sys.exit(run_trial_sample(n, enable_pipeline))
    elif args.mode == "new-only":
        sys.exit(run_new_only(enable_pipeline, not args.no_notify))
    elif args.mode == "full-backfill":
        sys.exit(run_full_backfill(args.backfill_days, args.batch_days, args.force, enable_pipeline))
    elif args.mode == "dry-run-pipeline":
        sys.exit(run_dry_run_pipeline())


if __name__ == "__main__":
    main()
