# -*- coding: utf-8 -*-
"""编排层调试脚本。

验证 FlowRunner / TaskExecutor / CursorManager / CLI 参数解析的核心逻辑，
不需要真实 API 或数据库连接——纯逻辑验证。

用法:
    cd apps/etl/connectors/feiqiu
    python -m scripts.debug.debug_orchestration
"""
from __future__ import annotations

import argparse
import json
import logging
import sys
import time
import warnings
from dataclasses import asdict, dataclass, field
from datetime import datetime, timedelta
from pathlib import Path
from zoneinfo import ZoneInfo

# ── 确保项目根目录在 sys.path ──
_FEIQIU_ROOT = Path(__file__).resolve().parents[2]
if str(_FEIQIU_ROOT) not in sys.path:
    sys.path.insert(0, str(_FEIQIU_ROOT))

from orchestration.flow_runner import FlowRunner
from orchestration.task_registry import default_registry, TaskRegistry
from orchestration.task_executor import TaskExecutor
from cli.main import resolve_data_source, parse_args as cli_parse_args


@dataclass
class DebugResult:
    """单个编排层调试步骤的结果"""
    layer: str = "ORCHESTRATION"
    task_code: str = ""
    status: str = ""          # PASS / FAIL / WARN / ERROR
    message: str = ""
    details: dict = field(default_factory=dict)
    duration_sec: float = 0.0
    error_detail: str | None = None
    fix_applied: str | None = None


# ── 工具函数 ──────────────────────────────────────────────────

def _setup_logging() -> logging.Logger:
    logger = logging.getLogger("debug_orchestration")
    logger.setLevel(logging.INFO)
    if not logger.handlers:
        handler = logging.StreamHandler(sys.stdout)
        handler.setFormatter(logging.Formatter(
            "%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S"
        ))
        logger.addHandler(handler)
    return logger


def _sanitize_for_json(obj):
    """递归处理不可序列化的值。"""
    if isinstance(obj, dict):
        return {k: _sanitize_for_json(v) for k, v in obj.items()}
    if isinstance(obj, (list, tuple)):
        return [_sanitize_for_json(v) for v in obj]
    if isinstance(obj, datetime):
        return obj.isoformat()
    if isinstance(obj, type):
        return obj.__name__
    return obj


# ══════════════════════════════════════════════════════════════
# 第1组：FlowRunner Flow 解析逻辑
# ══════════════════════════════════════════════════════════════

def check_all_flows_defined() -> DebugResult:
    """验证 FLOW_LAYERS 包含所有 7 种 Flow 定义。"""
    result = DebugResult(task_code="FLOW_DEFINITIONS")
    t0 = time.monotonic()

    expected_flows = {
        "api_ods", "api_ods_dwd", "api_full",
        "ods_dwd", "dwd_dws", "dwd_dws_index", "dwd_index",
    }
    actual_flows = set(FlowRunner.FLOW_LAYERS.keys())

    missing = expected_flows - actual_flows
    extra = actual_flows - expected_flows

    result.details = {
        "expected": sorted(expected_flows),
        "actual": sorted(actual_flows),
        "missing": sorted(missing),
        "extra": sorted(extra),
    }

    if missing:
        result.status = "FAIL"
        result.message = f"缺少 Flow 定义: {sorted(missing)}"
    elif extra:
        result.status = "WARN"
        result.message = f"存在额外 Flow 定义: {sorted(extra)}"
    else:
        result.status = "PASS"
        result.message = f"全部 {len(expected_flows)} 种 Flow 定义完整"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


def check_flow_layer_mapping() -> DebugResult:
    """验证每种 Flow 的层列表与预期一致。"""
    result = DebugResult(task_code="FLOW_LAYER_MAPPING")
    t0 = time.monotonic()

    expected_mapping = {
        "api_ods": ["ODS"],
        "api_ods_dwd": ["ODS", "DWD"],
        "api_full": ["ODS", "DWD", "DWS", "INDEX"],
        "ods_dwd": ["DWD"],
        "dwd_dws": ["DWS"],
        "dwd_dws_index": ["DWS", "INDEX"],
        "dwd_index": ["INDEX"],
    }

    mismatches = []
    for flow_name, expected_layers in expected_mapping.items():
        actual_layers = FlowRunner.FLOW_LAYERS.get(flow_name)
        if actual_layers != expected_layers:
            mismatches.append({
                "flow": flow_name,
                "expected": expected_layers,
                "actual": actual_layers,
            })

    result.details = {
        "total_flows": len(expected_mapping),
        "mismatches": mismatches,
    }

    if mismatches:
        result.status = "FAIL"
        result.message = f"{len(mismatches)} 种 Flow 层映射不一致"
    else:
        result.status = "PASS"
        result.message = "所有 Flow 层映射正确"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


def check_invalid_flow_rejection() -> DebugResult:
    """验证 FlowRunner.run() 对无效 Flow 名称抛出 ValueError。"""
    result = DebugResult(task_code="INVALID_FLOW_REJECTION")
    t0 = time.monotonic()

    invalid_names = ["nonexistent", "API_ODS", "full", "", "api_full_extra"]
    errors_raised = []
    errors_missed = []

    # FlowRunner.__init__ 需要 config.get()，构造一个最小 stub
    _stub_config = type("StubConfig", (), {"get": lambda self, key, default=None: default})()

    for name in invalid_names:
        try:
            runner = FlowRunner(
                config=_stub_config, task_executor=None, task_registry=None,
                db_conn=None, api_client=None, logger=logging.getLogger("test"),
            )
            runner.run(flow=name)
            errors_missed.append(name)
        except ValueError as exc:
            errors_raised.append({"name": name, "error": str(exc)})
        except Exception:
            # run() 可能在 Flow 名称检查之后因其他 None 依赖抛出异常
            # 说明 Flow 名称检查通过了——这不应该发生
            errors_missed.append(name)

    result.details = {
        "tested": invalid_names,
        "correctly_rejected": [e["name"] for e in errors_raised],
        "missed": errors_missed,
    }

    if errors_missed:
        result.status = "FAIL"
        result.message = f"以下无效 Flow 未被拒绝: {errors_missed}"
    else:
        result.status = "PASS"
        result.message = f"全部 {len(invalid_names)} 个无效 Flow 名称被正确拒绝"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


def check_flow_task_resolution() -> DebugResult:
    """验证 FlowRunner._resolve_tasks() 对每种 Flow 解析出的任务列表非空且合理。"""
    result = DebugResult(task_code="FLOW_TASK_RESOLUTION")
    t0 = time.monotonic()

    # 构造最小 config（_resolve_tasks 需要 config.get）
    from config.settings import AppConfig
    try:
        config = AppConfig.load()
    except Exception:
        # 无 .env 时用空配置
        config = type("FakeConfig", (), {
            "get": lambda self, key, default=None: default,
            "config": {},
        })()

    logger = logging.getLogger("test_resolve")
    runner = FlowRunner(
        config=config, task_executor=None, task_registry=default_registry,
        db_conn=None, api_client=None, logger=logger,
    )

    flow_tasks = {}
    issues = []
    for flow_name, layers in FlowRunner.FLOW_LAYERS.items():
        tasks = runner._resolve_tasks(layers)
        flow_tasks[flow_name] = tasks
        if not tasks:
            issues.append(f"{flow_name}: 解析出空任务列表")

        # 验证 ODS 层应包含 ODS 任务
        if "ODS" in layers:
            ods_tasks = [t for t in tasks if t.startswith("ODS_")]
            if not ods_tasks:
                issues.append(f"{flow_name}: ODS 层无 ODS_ 前缀任务")

        # 验证 DWD 层应包含 DWD_LOAD_FROM_ODS
        if "DWD" in layers:
            if "DWD_LOAD_FROM_ODS" not in tasks:
                issues.append(f"{flow_name}: DWD 层缺少 DWD_LOAD_FROM_ODS")

        # 验证 DWS 层应包含 DWS 任务
        if "DWS" in layers:
            dws_tasks = [t for t in tasks if t.startswith("DWS_")]
            if not dws_tasks:
                issues.append(f"{flow_name}: DWS 层无 DWS_ 前缀任务")

        # 验证 INDEX 层应包含指数任务
        if "INDEX" in layers:
            index_tasks = [t for t in tasks if t in (
                "DWS_WINBACK_INDEX", "DWS_NEWCONV_INDEX",
                "DWS_ML_MANUAL_IMPORT", "DWS_RELATION_INDEX",
            )]
            if not index_tasks:
                issues.append(f"{flow_name}: INDEX 层无指数任务")

    result.details = {
        "flow_tasks": {k: v for k, v in flow_tasks.items()},
        "issues": issues,
    }

    if issues:
        result.status = "WARN"
        result.message = f"{len(issues)} 个问题: {issues[0]}"
    else:
        result.status = "PASS"
        result.message = f"所有 {len(flow_tasks)} 种 Flow 任务解析正确"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


# ══════════════════════════════════════════════════════════════
# 第2组：TaskExecutor 任务分发逻辑
# ══════════════════════════════════════════════════════════════

def check_task_registry_layers() -> DebugResult:
    """验证 TaskRegistry 各层任务数量与预期一致。"""
    result = DebugResult(task_code="TASK_REGISTRY_LAYERS")
    t0 = time.monotonic()

    layer_counts = {}
    expected_counts = {
        "ODS": 23,    # 23 个 ODS 任务
        "DWD": 2,     # DWD_LOAD_FROM_ODS + DWD_QUALITY_CHECK
        "DWS": 15,    # 15 个 DWS 任务
        "INDEX": 4,   # 4 个指数任务
    }

    issues = []
    for layer, expected in expected_counts.items():
        tasks = default_registry.get_tasks_by_layer(layer)
        actual = len(tasks)
        layer_counts[layer] = {"expected": expected, "actual": actual, "tasks": sorted(tasks)}
        if actual != expected:
            issues.append(f"{layer}: 预期 {expected} 个，实际 {actual} 个")

    # 总任务数
    all_codes = default_registry.get_all_task_codes()
    layer_counts["TOTAL"] = {"actual": len(all_codes)}

    result.details = layer_counts

    if issues:
        result.status = "WARN"
        result.message = f"任务数量偏差: {'; '.join(issues)}"
    else:
        result.status = "PASS"
        result.message = f"各层任务数量正确 (ODS={expected_counts['ODS']}, DWD={expected_counts['DWD']}, DWS={expected_counts['DWS']}, INDEX={expected_counts['INDEX']})"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


def check_utility_task_identification() -> DebugResult:
    """验证 TaskRegistry.is_utility_task() 正确识别工具类任务。

    工具类任务的特征：requires_db_config=False 且 task_type="utility"。
    TaskExecutor 对工具类任务跳过游标管理和运行记录。
    """
    result = DebugResult(task_code="UTILITY_TASK_IDENTIFICATION")
    t0 = time.monotonic()

    # 已知的工具类任务（requires_db_config=False）
    known_utility_codes = [
        "MANUAL_INGEST", "INIT_ODS_SCHEMA", "INIT_DWD_SCHEMA",
        "INIT_DWS_SCHEMA", "ODS_JSON_ARCHIVE", "CHECK_CUTOFF",
    ]
    # 已知的非工具类任务（requires_db_config=True）
    known_etl_codes = [
        "ODS_MEMBER", "ODS_ORDER", "ODS_PAYMENT",
        "DWD_LOAD_FROM_ODS",
        "DWS_ASSISTANT_DAILY", "DWS_FINANCE_DAILY",
    ]

    issues = []
    utility_results = {}
    etl_results = {}

    for code in known_utility_codes:
        is_util = default_registry.is_utility_task(code)
        utility_results[code] = is_util
        if not is_util:
            issues.append(f"{code}: 应为工具类任务但 is_utility_task=False")

    for code in known_etl_codes:
        is_util = default_registry.is_utility_task(code)
        etl_results[code] = is_util
        if is_util:
            issues.append(f"{code}: 不应为工具类任务但 is_utility_task=True")

    # INDEX 层任务也是 requires_db_config=False，但它们不是"工具类"——
    # 它们有 layer="INDEX"，is_utility_task 基于 requires_db_config 判断
    index_codes = default_registry.get_tasks_by_layer("INDEX")
    index_utility_status = {}
    for code in index_codes:
        is_util = default_registry.is_utility_task(code)
        index_utility_status[code] = is_util

    result.details = {
        "utility_tasks": utility_results,
        "etl_tasks": etl_results,
        "index_tasks_utility_status": index_utility_status,
        "issues": issues,
    }

    if issues:
        result.status = "FAIL"
        result.message = f"{len(issues)} 个工具类任务识别错误"
    else:
        result.status = "PASS"
        result.message = f"工具类任务识别正确 ({len(known_utility_codes)} 个工具类, {len(known_etl_codes)} 个 ETL 类)"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


def check_task_dispatch_paths() -> DebugResult:
    """验证 TaskExecutor 的任务分发路径逻辑。

    检查 _is_ods_task / is_utility_task 对各类任务的判断是否正确，
    确保 ODS/DWD/DWS/工具类任务走不同的执行路径。
    """
    result = DebugResult(task_code="TASK_DISPATCH_PATHS")
    t0 = time.monotonic()

    dispatch_map = {}
    issues = []

    all_codes = default_registry.get_all_task_codes()
    for code in sorted(all_codes):
        meta = default_registry.get_metadata(code)
        is_utility = default_registry.is_utility_task(code)
        is_ods = TaskExecutor._is_ods_task(code)

        # 判断预期路径
        if is_utility:
            expected_path = "utility"
        elif is_ods:
            expected_path = "ods"
        else:
            expected_path = "standard"

        # 验证一致性
        layer = meta.layer if meta else None
        if layer == "ODS" and not is_ods and not is_utility:
            issues.append(f"{code}: ODS 层任务但 _is_ods_task=False")
        if layer not in ("ODS", None) and is_ods:
            issues.append(f"{code}: 非 ODS 层任务但 _is_ods_task=True")

        dispatch_map[code] = {
            "layer": layer,
            "is_utility": is_utility,
            "is_ods": is_ods,
            "dispatch_path": expected_path,
        }

    # 统计各路径的任务数
    path_counts = {}
    for info in dispatch_map.values():
        p = info["dispatch_path"]
        path_counts[p] = path_counts.get(p, 0) + 1

    result.details = {
        "path_counts": path_counts,
        "issues": issues,
        "sample_dispatch": {k: v for k, v in list(dispatch_map.items())[:10]},
    }

    if issues:
        result.status = "FAIL"
        result.message = f"{len(issues)} 个分发路径不一致"
    else:
        result.status = "PASS"
        result.message = f"任务分发路径正确 (utility={path_counts.get('utility', 0)}, ods={path_counts.get('ods', 0)}, standard={path_counts.get('standard', 0)})"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


# ══════════════════════════════════════════════════════════════
# 第3组：CursorManager 游标推进逻辑
# ══════════════════════════════════════════════════════════════

def check_cursor_manager_interface() -> DebugResult:
    """验证 CursorManager 的接口签名和方法存在性。

    CursorManager 的核心方法：get_or_create / advance，
    这里验证接口签名正确，不需要真实数据库连接。
    """
    from orchestration.cursor_manager import CursorManager
    import inspect

    result = DebugResult(task_code="CURSOR_MANAGER_INTERFACE")
    t0 = time.monotonic()

    issues = []
    method_sigs = {}

    # 检查 get_or_create 方法
    if not hasattr(CursorManager, "get_or_create"):
        issues.append("缺少 get_or_create 方法")
    else:
        sig = inspect.signature(CursorManager.get_or_create)
        params = list(sig.parameters.keys())
        method_sigs["get_or_create"] = params
        # 预期参数: self, task_id, store_id
        if "task_id" not in params or "store_id" not in params:
            issues.append("get_or_create 缺少 task_id 或 store_id 参数")

    # 检查 advance 方法
    if not hasattr(CursorManager, "advance"):
        issues.append("缺少 advance 方法")
    else:
        sig = inspect.signature(CursorManager.advance)
        params = list(sig.parameters.keys())
        method_sigs["advance"] = params
        # 预期参数: self, task_id, store_id, window_start, window_end, run_id
        required = {"task_id", "store_id", "window_start", "window_end", "run_id"}
        missing = required - set(params)
        if missing:
            issues.append(f"advance 缺少参数: {missing}")

    result.details = {
        "method_signatures": method_sigs,
        "issues": issues,
    }

    if issues:
        result.status = "FAIL"
        result.message = f"CursorManager 接口问题: {'; '.join(issues)}"
    else:
        result.status = "PASS"
        result.message = "CursorManager 接口签名正确 (get_or_create, advance)"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


def check_cursor_advance_sql_logic() -> DebugResult:
    """验证 CursorManager.advance() 的 SQL 逻辑。

    通过检查源码确认：
    - advance 使用 UPDATE 语句更新 meta.etl_cursor
    - 支持 last_id 参数（可选）
    - 使用 GREATEST 保护 last_id 不回退
    - 调用 commit() 持久化
    """
    from orchestration.cursor_manager import CursorManager
    import inspect

    result = DebugResult(task_code="CURSOR_ADVANCE_SQL")
    t0 = time.monotonic()

    issues = []
    checks = []

    source = inspect.getsource(CursorManager.advance)

    # 检查 UPDATE 语句
    if "UPDATE meta.etl_cursor" in source:
        checks.append("✓ 使用 UPDATE meta.etl_cursor")
    else:
        issues.append("advance 未使用 UPDATE meta.etl_cursor")

    # 检查 GREATEST 保护 last_id
    if "GREATEST" in source:
        checks.append("✓ 使用 GREATEST 保护 last_id 不回退")
    else:
        checks.append("ℹ 未使用 GREATEST（可能 last_id 直接覆盖）")

    # 检查 commit
    if "commit" in source:
        checks.append("✓ 调用 commit() 持久化")
    else:
        issues.append("advance 未调用 commit()")

    # 检查 last_id 可选分支
    if "last_id" in source and "None" in source:
        checks.append("✓ last_id 参数可选（有 None 分支）")
    else:
        checks.append("ℹ last_id 处理方式需人工确认")

    # 检查 updated_at
    if "updated_at" in source:
        checks.append("✓ 更新 updated_at 时间戳")

    result.details = {
        "checks": checks,
        "issues": issues,
    }

    if issues:
        result.status = "WARN"
        result.message = f"游标推进逻辑有 {len(issues)} 个问题"
    else:
        result.status = "PASS"
        result.message = "游标推进 SQL 逻辑正确"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


def check_cursor_skip_for_utility() -> DebugResult:
    """验证工具类任务确实跳过游标管理。

    通过检查 TaskExecutor.run_single_task 源码确认：
    工具类任务走 _run_utility_task 路径，不调用 cursor_mgr。
    """
    import inspect

    result = DebugResult(task_code="CURSOR_SKIP_UTILITY")
    t0 = time.monotonic()

    issues = []
    checks = []

    source = inspect.getsource(TaskExecutor.run_single_task)

    # 检查工具类任务的早期返回
    if "is_utility_task" in source and "_run_utility_task" in source:
        checks.append("✓ run_single_task 检查 is_utility_task 并分发到 _run_utility_task")
    else:
        issues.append("run_single_task 未检查 is_utility_task")

    # 检查 _run_utility_task 不调用 cursor_mgr（排除注释和 docstring）
    utility_source = inspect.getsource(TaskExecutor._run_utility_task)
    # 过滤掉注释行和 docstring，只检查实际代码中是否调用 self.cursor_mgr
    code_lines = []
    in_docstring = False
    for line in utility_source.splitlines():
        stripped = line.strip()
        if stripped.startswith('"""') or stripped.startswith("'''"):
            if in_docstring:
                in_docstring = False
                continue
            # 单行 docstring
            if stripped.count('"""') >= 2 or stripped.count("'''") >= 2:
                continue
            in_docstring = True
            continue
        if in_docstring:
            continue
        if stripped.startswith("#"):
            continue
        code_lines.append(stripped)
    code_only = "\n".join(code_lines)

    if "cursor_mgr" not in code_only:
        checks.append("✓ _run_utility_task 不调用 cursor_mgr")
    else:
        issues.append("_run_utility_task 中出现了 cursor_mgr 调用")

    # 检查 _run_utility_task 不调用 run_tracker.create_run
    if "create_run" not in code_only:
        checks.append("✓ _run_utility_task 不调用 run_tracker.create_run")
    else:
        issues.append("_run_utility_task 中调用了 run_tracker.create_run")

    result.details = {
        "checks": checks,
        "issues": issues,
    }

    if issues:
        result.status = "FAIL"
        result.message = f"工具类任务游标跳过逻辑有问题: {'; '.join(issues)}"
    else:
        result.status = "PASS"
        result.message = "工具类任务正确跳过游标管理和运行记录"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


# ══════════════════════════════════════════════════════════════
# 第4组：CLI 参数解析（传统模式 vs Flow 模式）
# ══════════════════════════════════════════════════════════════

def check_cli_data_source_resolution() -> DebugResult:
    """验证 resolve_data_source 的优先级逻辑。

    优先级：--data-source > --pipeline-flow（弃用别名）> 默认值 hybrid
    """
    result = DebugResult(task_code="CLI_DATA_SOURCE")
    t0 = time.monotonic()

    issues = []
    test_cases = []

    # 辅助：构造 argparse.Namespace 模拟 CLI 参数
    def make_args(data_source=None, pipeline_flow=None):
        return argparse.Namespace(data_source=data_source, pipeline_flow=pipeline_flow)

    # Case 1: 默认值 → hybrid
    args = make_args()
    ds = resolve_data_source(args)
    case = {"case": "默认值", "expected": "hybrid", "actual": ds}
    test_cases.append(case)
    if ds != "hybrid":
        issues.append(f"默认值应为 hybrid，实际为 {ds}")

    # Case 2: --data-source online → online
    args = make_args(data_source="online")
    ds = resolve_data_source(args)
    case = {"case": "--data-source online", "expected": "online", "actual": ds}
    test_cases.append(case)
    if ds != "online":
        issues.append(f"--data-source online 应为 online，实际为 {ds}")

    # Case 3: --data-source offline → offline
    args = make_args(data_source="offline")
    ds = resolve_data_source(args)
    case = {"case": "--data-source offline", "expected": "offline", "actual": ds}
    test_cases.append(case)
    if ds != "offline":
        issues.append(f"--data-source offline 应为 offline，实际为 {ds}")

    # Case 4: --pipeline-flow FULL → hybrid（弃用映射）
    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter("always")
        args = make_args(pipeline_flow="FULL")
        ds = resolve_data_source(args)
        case = {"case": "--pipeline-flow FULL", "expected": "hybrid", "actual": ds,
                "deprecation_warning": len(w) > 0}
        test_cases.append(case)
        if ds != "hybrid":
            issues.append(f"--pipeline-flow FULL 应映射为 hybrid，实际为 {ds}")
        if not w:
            issues.append("--pipeline-flow FULL 未发出 DeprecationWarning")

    # Case 5: --pipeline-flow FETCH_ONLY → online
    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter("always")
        args = make_args(pipeline_flow="FETCH_ONLY")
        ds = resolve_data_source(args)
        case = {"case": "--pipeline-flow FETCH_ONLY", "expected": "online", "actual": ds}
        test_cases.append(case)
        if ds != "online":
            issues.append(f"--pipeline-flow FETCH_ONLY 应映射为 online，实际为 {ds}")

    # Case 6: --pipeline-flow INGEST_ONLY → offline
    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter("always")
        args = make_args(pipeline_flow="INGEST_ONLY")
        ds = resolve_data_source(args)
        case = {"case": "--pipeline-flow INGEST_ONLY", "expected": "offline", "actual": ds}
        test_cases.append(case)
        if ds != "offline":
            issues.append(f"--pipeline-flow INGEST_ONLY 应映射为 offline，实际为 {ds}")

    # Case 7: --data-source 优先于 --pipeline-flow
    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter("always")
        args = make_args(data_source="online", pipeline_flow="INGEST_ONLY")
        ds = resolve_data_source(args)
        case = {"case": "--data-source online + --pipeline-flow INGEST_ONLY",
                "expected": "online", "actual": ds}
        test_cases.append(case)
        if ds != "online":
            issues.append(f"--data-source 应优先于 --pipeline-flow，实际为 {ds}")

    result.details = {
        "test_cases": test_cases,
        "issues": issues,
    }

    if issues:
        result.status = "FAIL"
        result.message = f"{len(issues)} 个 data_source 解析错误"
    else:
        result.status = "PASS"
        result.message = f"全部 {len(test_cases)} 个 data_source 解析用例通过"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


def check_cli_mode_detection() -> DebugResult:
    """验证 CLI 的 Flow 模式 vs 传统模式判断逻辑。

    通过检查 main() 源码确认：
    - 有 --flow 参数 → Flow 模式（使用 FlowRunner）
    - 无 --flow 参数 → 传统模式（使用 TaskExecutor.run_tasks）
    """
    import inspect
    from cli.main import main as cli_main

    result = DebugResult(task_code="CLI_MODE_DETECTION")
    t0 = time.monotonic()

    issues = []
    checks = []

    source = inspect.getsource(cli_main)

    # 检查 Flow 模式分支
    if "args.flow" in source and "FlowRunner" in source:
        checks.append("✓ 有 --flow 参数时使用 FlowRunner（Flow 模式）")
    else:
        issues.append("未找到 Flow 模式分支（args.flow + FlowRunner）")

    # 检查传统模式分支
    if "run_tasks" in source:
        checks.append("✓ 无 --flow 参数时使用 run_tasks（传统模式）")
    else:
        issues.append("未找到传统模式分支（run_tasks）")

    # 检查 resolve_data_source 调用
    if "resolve_data_source" in source:
        checks.append("✓ 调用 resolve_data_source 解析数据源模式")
    else:
        issues.append("未调用 resolve_data_source")

    # 检查回溯窗口默认逻辑
    if "lookback_hours" in source:
        checks.append("✓ 支持 --lookback-hours 回溯窗口")
    else:
        issues.append("未找到 lookback_hours 回溯窗口逻辑")

    # 检查 window_override 设置
    if "window_override" in source:
        checks.append("✓ 设置 window_override 确保任务使用指定窗口")
    else:
        checks.append("ℹ 未找到 window_override 设置")

    result.details = {
        "checks": checks,
        "issues": issues,
    }

    if issues:
        result.status = "FAIL"
        result.message = f"CLI 模式检测有 {len(issues)} 个问题"
    else:
        result.status = "PASS"
        result.message = "CLI Flow/传统模式检测逻辑正确"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


def check_cli_flow_choices() -> DebugResult:
    """验证 CLI --flow 参数的可选值与 FLOW_LAYERS 一致。"""
    result = DebugResult(task_code="CLI_FLOW_CHOICES")
    t0 = time.monotonic()

    issues = []

    # 从 cli/main.py 获取 FLOW_CHOICES
    try:
        from cli.main import FLOW_CHOICES
        cli_choices = set(FLOW_CHOICES)
    except ImportError:
        # 回退：从 parse_args 的 parser 中提取
        cli_choices = set(FlowRunner.FLOW_LAYERS.keys())
        issues.append("无法导入 FLOW_CHOICES，使用 FLOW_LAYERS 键集合")

    flow_keys = set(FlowRunner.FLOW_LAYERS.keys())

    missing_in_cli = flow_keys - cli_choices
    extra_in_cli = cli_choices - flow_keys

    result.details = {
        "flow_layers_keys": sorted(flow_keys),
        "cli_choices": sorted(cli_choices),
        "missing_in_cli": sorted(missing_in_cli),
        "extra_in_cli": sorted(extra_in_cli),
    }

    if missing_in_cli:
        issues.append(f"FLOW_LAYERS 中有但 CLI 未提供: {sorted(missing_in_cli)}")
    if extra_in_cli:
        issues.append(f"CLI 中有但 FLOW_LAYERS 未定义: {sorted(extra_in_cli)}")

    if issues:
        result.status = "WARN"
        result.message = "; ".join(issues)
    else:
        result.status = "PASS"
        result.message = f"CLI --flow 可选值与 FLOW_LAYERS 完全一致 ({len(flow_keys)} 种)"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


# ══════════════════════════════════════════════════════════════
# 第5组：FlowRunner 处理模式验证
# ══════════════════════════════════════════════════════════════

def check_processing_modes() -> DebugResult:
    """验证 FlowRunner.run() 对四种处理模式的分支逻辑。

    - increment_only: 仅执行增量 ETL
    - verify_only: 跳过增量 ETL，直接执行校验
    - increment_verify: 先增量 ETL，再执行校验
    """
    import inspect

    result = DebugResult(task_code="PROCESSING_MODES")
    t0 = time.monotonic()

    issues = []
    checks = []

    source = inspect.getsource(FlowRunner.run)

    # 检查 verify_only 模式
    if "verify_only" in source:
        checks.append("✓ 支持 verify_only 模式")
        if "_run_verification" in source:
            checks.append("✓ verify_only 调用 _run_verification")
    else:
        issues.append("未找到 verify_only 模式分支")

    # 检查 increment_verify 模式
    if "increment_verify" in source:
        checks.append("✓ 支持 increment_verify 模式")
    else:
        issues.append("未找到 increment_verify 模式分支")

    # 检查 fetch_before_verify 参数
    if "fetch_before_verify" in source:
        checks.append("✓ 支持 fetch_before_verify 参数（校验前先获取 API 数据）")
    else:
        checks.append("ℹ 未找到 fetch_before_verify 参数")

    # 检查 _run_verification 方法存在
    if hasattr(FlowRunner, "_run_verification"):
        checks.append("✓ _run_verification 方法存在")
    else:
        issues.append("缺少 _run_verification 方法")

    result.details = {
        "checks": checks,
        "issues": issues,
    }

    if issues:
        result.status = "FAIL"
        result.message = f"处理模式验证有 {len(issues)} 个问题"
    else:
        result.status = "PASS"
        result.message = "四种处理模式（increment_only/verify_only/increment_verify/full_window）逻辑正确"

    result.duration_sec = round(time.monotonic() - t0, 4)
    return result


# ══════════════════════════════════════════════════════════════
# 主流程
# ══════════════════════════════════════════════════════════════

# 所有检查项，按组排列
ALL_CHECKS = [
    # 第1组：FlowRunner Flow 解析
    ("1.1 Flow 定义完整性", check_all_flows_defined),
    ("1.2 Flow 层映射正确性", check_flow_layer_mapping),
    ("1.3 无效 Flow 拒绝", check_invalid_flow_rejection),
    ("1.4 Flow 任务解析", check_flow_task_resolution),
    # 第2组：TaskExecutor 任务分发
    ("2.1 TaskRegistry 各层任务数量", check_task_registry_layers),
    ("2.2 工具类任务识别", check_utility_task_identification),
    ("2.3 任务分发路径", check_task_dispatch_paths),
    # 第3组：CursorManager 游标推进
    ("3.1 CursorManager 接口签名", check_cursor_manager_interface),
    ("3.2 游标推进 SQL 逻辑", check_cursor_advance_sql_logic),
    ("3.3 工具类任务跳过游标", check_cursor_skip_for_utility),
    # 第4组：CLI 参数解析
    ("4.1 data_source 解析优先级", check_cli_data_source_resolution),
    ("4.2 CLI Flow/传统模式检测", check_cli_mode_detection),
    ("4.3 CLI --flow 可选值一致性", check_cli_flow_choices),
    # 第5组：处理模式
    ("5.1 处理模式分支逻辑", check_processing_modes),
]


def run_orchestration_debug() -> list[DebugResult]:
    """执行编排层全量调试。"""
    logger = _setup_logging()
    logger.info("=" * 60)
    logger.info("编排层调试开始")
    logger.info("=" * 60)
    logger.info("")

    results: list[DebugResult] = []
    current_group = ""

    for idx, (name, check_fn) in enumerate(ALL_CHECKS, start=1):
        # 打印组标题
        group = name.split(".")[0]
        if group != current_group:
            current_group = group
            group_titles = {
                "1": "FlowRunner Flow 解析逻辑",
                "2": "TaskExecutor 任务分发逻辑",
                "3": "CursorManager 游标推进逻辑",
                "4": "CLI 参数解析",
                "5": "处理模式验证",
            }
            logger.info("━" * 60)
            logger.info("▶ 第%s组: %s", group, group_titles.get(group, ""))
            logger.info("━" * 60)

        logger.info("[%d/%d] %s", idx, len(ALL_CHECKS), name)

        try:
            r = check_fn()
        except Exception as exc:
            import traceback
            r = DebugResult(
                task_code=name,
                status="ERROR",
                message=f"检查异常: {exc}",
                error_detail=traceback.format_exc(),
            )
            logger.error("  ✗ 检查异常: %s", exc)

        icon = {"PASS": "✓", "WARN": "⚠", "ERROR": "✗", "FAIL": "✗"}.get(r.status, "?")
        logger.info("  %s %s: %s (%.3fs)", icon, r.status, r.message, r.duration_sec)
        results.append(r)

    # 汇总
    _print_summary(results, logger)

    # 输出 JSON
    output_dir = _FEIQIU_ROOT / "scripts" / "debug" / "output"
    output_dir.mkdir(parents=True, exist_ok=True)
    tz = ZoneInfo("Asia/Shanghai")
    ts = datetime.now(tz).strftime("%Y%m%d_%H%M%S")
    output_file = output_dir / f"debug_orchestration_{ts}.json"
    _save_results(results, output_file)
    logger.info("结果已保存: %s", output_file)

    return results


def _print_summary(results: list[DebugResult], logger: logging.Logger):
    """打印调试汇总。"""
    logger.info("")
    logger.info("=" * 60)
    logger.info("编排层调试汇总")
    logger.info("=" * 60)

    pass_count = sum(1 for r in results if r.status == "PASS")
    warn_count = sum(1 for r in results if r.status == "WARN")
    fail_count = sum(1 for r in results if r.status == "FAIL")
    error_count = sum(1 for r in results if r.status == "ERROR")
    total_duration = sum(r.duration_sec for r in results)

    logger.info("总计: %d 项检查", len(results))
    logger.info("  ✓ PASS:  %d", pass_count)
    logger.info("  ⚠ WARN:  %d", warn_count)
    logger.info("  ✗ FAIL:  %d", fail_count)
    logger.info("  ✗ ERROR: %d", error_count)
    logger.info("  总耗时: %.3f 秒", total_duration)
    logger.info("")

    non_pass = [r for r in results if r.status != "PASS"]
    if non_pass:
        logger.info("需关注的检查项:")
        for r in non_pass:
            logger.info("  [%s] %s: %s", r.status, r.task_code, r.message)
    else:
        logger.info("所有检查项均通过 ✓")


def _save_results(results: list[DebugResult], path: Path):
    """将结果序列化为 JSON。"""
    data = [_sanitize_for_json(asdict(r)) for r in results]
    path.write_text(
        json.dumps(data, ensure_ascii=False, indent=2, default=str),
        encoding="utf-8",
    )


# ── CLI 入口 ──────────────────────────────────────────────────

def main():
    results = run_orchestration_debug()
    has_fail = any(r.status in ("FAIL", "ERROR") for r in results)
    sys.exit(1 if has_fail else 0)


if __name__ == "__main__":
    main()
