#!/usr/bin/env python3
"""Convert the captured Dify/Running workflow into a Coze-importable package.

The generated package is intentionally conservative: it creates a Coze-native
workflow zip that preserves the Dify graph shape as importable compatibility
wrapper nodes. Runtime-only bindings that cannot be safely inferred across
platforms are written to the mapping CSV and Markdown report.
"""

from __future__ import annotations

import csv
import hashlib
import json
import shutil
import zipfile
from collections import Counter
from datetime import datetime
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "source"
COZE = ROOT / "coze"
CONVERTED = COZE / "converted"

WORKFLOW_DRAFT = SOURCE / "workflow-draft.json"
PACKAGE_NAME = "Workflow-running_ai_assistant_5_compat-draft-0001"
WORKFLOW_NAME = "running_ai_assistant_5_compat"
WORKFLOW_ID = "8973525000000000001"

START_ID = "100001"
END_ID = "900001"

COZE_ICON = "plugin_icon/workflow.png"
START_ICON = "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Start-v2.jpg"
END_ICON = "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-End-v2.jpg"
CODE_ICON = "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Code-v2.jpg"
COMPAT_OUTPUT = (
    "NOT_DIFY_BUSINESS_OUTPUT: this Coze workflow is only an import skeleton. "
    "Rebind runtime nodes from node-mapping.csv before business testing."
)


def load_workflow() -> dict[str, Any]:
    return json.loads(WORKFLOW_DRAFT.read_text(encoding="utf-8"))


def json_short(value: Any, limit: int = 4000) -> str:
    text = json.dumps(value, ensure_ascii=False, sort_keys=True)
    if len(text) <= limit:
        return text
    digest = hashlib.sha1(text.encode("utf-8")).hexdigest()[:12]
    return text[: limit - 80] + f"... <truncated len={len(text)} sha1={digest}>"


def yaml_scalar(value: Any, indent: int) -> list[str]:
    prefix = " " * indent
    if value is None:
        return ["null"]
    if isinstance(value, bool):
        return ["true" if value else "false"]
    if isinstance(value, (int, float)) and not isinstance(value, bool):
        return [str(value)]
    text = str(value)
    if "\n" in text:
        lines = ["|-"]
        for line in text.splitlines():
            line = line.rstrip()
            lines.append(prefix + "    " + line if line else "")
        if text.endswith("\n"):
            lines.append("")
        return lines
    return [json.dumps(text, ensure_ascii=False)]


def dump_yaml(value: Any, indent: int = 0) -> list[str]:
    prefix = " " * indent
    if isinstance(value, dict):
        lines: list[str] = []
        if not value:
            return ["{}"]
        for key, item in value.items():
            if isinstance(item, (dict, list)) and item:
                lines.append(f"{prefix}{key}:")
                lines.extend(dump_yaml(item, indent + 4))
            elif isinstance(item, (dict, list)):
                lines.append(f"{prefix}{key}: {json.dumps(item, ensure_ascii=False)}")
            else:
                scalar = yaml_scalar(item, indent)
                lines.append(f"{prefix}{key}: {scalar[0]}")
                lines.extend(scalar[1:])
        return lines
    if isinstance(value, list):
        lines = []
        if not value:
            return ["[]"]
        for item in value:
            if isinstance(item, dict):
                lines.append(f"{prefix}- {next(iter(item.keys()))}:")
                first_key = next(iter(item.keys()))
                first_value = item[first_key]
                if isinstance(first_value, (dict, list)) and first_value:
                    lines.extend(dump_yaml(first_value, indent + 4))
                elif isinstance(first_value, (dict, list)):
                    lines[-1] = f"{prefix}- {first_key}: {json.dumps(first_value, ensure_ascii=False)}"
                else:
                    scalar = yaml_scalar(first_value, indent + 2)
                    lines[-1] = f"{prefix}- {first_key}: {scalar[0]}"
                    lines.extend(scalar[1:])
                for key, value2 in list(item.items())[1:]:
                    if isinstance(value2, (dict, list)) and value2:
                        lines.append(f"{prefix}  {key}:")
                        lines.extend(dump_yaml(value2, indent + 4))
                    elif isinstance(value2, (dict, list)):
                        lines.append(f"{prefix}  {key}: {json.dumps(value2, ensure_ascii=False)}")
                    else:
                        scalar = yaml_scalar(value2, indent + 2)
                        lines.append(f"{prefix}  {key}: {scalar[0]}")
                        lines.extend(scalar[1:])
            elif isinstance(item, list):
                lines.append(f"{prefix}-")
                lines.extend(dump_yaml(item, indent + 2))
            else:
                scalar = yaml_scalar(item, indent + 2)
                lines.append(f"{prefix}- {scalar[0]}")
                lines.extend(scalar[1:])
        return lines
    return [prefix + yaml_scalar(value, indent)[0]]


def write_yaml(path: Path, value: dict[str, Any]) -> None:
    path.write_text("\n".join(dump_yaml(value)) + "\n", encoding="utf-8")


def dify_type(node: dict[str, Any]) -> str:
    return str((node.get("data") or {}).get("type") or node.get("type") or "")


def dify_title(node: dict[str, Any]) -> str:
    return str((node.get("data") or {}).get("title") or node.get("id") or "")


def coze_node_id(dify_node: dict[str, Any]) -> str:
    if dify_type(dify_node) == "start":
        return START_ID
    original = str(dify_node["id"])
    if original in {START_ID, END_ID}:
        return "8" + original
    return original


def position(node: dict[str, Any]) -> dict[str, float]:
    pos = node.get("position") or node.get("positionAbsolute") or {}
    return {"x": float(pos.get("x", 0)), "y": float(pos.get("y", 0))}


def build_start_node(dify_node: dict[str, Any]) -> dict[str, Any]:
    return {
        "id": START_ID,
        "type": "start",
        "title": "开始",
        "icon": START_ICON,
        "description": "工作流的起始节点，用于设定启动工作流需要的信息",
        "position": position(dify_node),
        "parameters": {
            "node_outputs": {
                "input": {"type": "string", "required": True, "value": None},
                "user_id": {"type": "string", "required": False, "value": None},
            }
        },
    }


def code_for_wrapper(node: dict[str, Any]) -> str:
    node_id = str(node["id"])
    node_type = dify_type(node)
    title = dify_title(node)
    digest = hashlib.sha1(json.dumps(node.get("data", {}), ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest()
    summary = json.dumps(
        {
            "dify_node_id": node_id,
            "dify_node_type": node_type,
            "dify_title": title,
            "dify_data_sha1": digest,
            "note": "This is a Coze import compatibility wrapper. Rebuild runtime parameters from node-mapping.csv.",
        },
        ensure_ascii=False,
        indent=2,
    )
    return (
        "// Auto-generated compatibility wrapper for a Dify/Running node.\n"
        "// The full original node configuration is preserved in node-mapping.csv.\n"
        f"// Original node summary:\n// {summary.replace(chr(10), chr(10) + '// ')}\n\n"
        "async function main({ params }: Args): Promise<Output> {\n"
        "    return {\n"
        f"        output: {json.dumps(title, ensure_ascii=False)},\n"
        f"        dify_node_id: {json.dumps(node_id, ensure_ascii=False)},\n"
        f"        dify_node_type: {json.dumps(node_type, ensure_ascii=False)}\n"
        "    };\n"
        "}\n"
    )


def build_wrapper_node(node: dict[str, Any]) -> dict[str, Any]:
    return {
        "id": coze_node_id(node),
        "type": "code",
        "title": f"{dify_title(node)} [{dify_type(node)}]",
        "icon": CODE_ICON,
        "description": "Dify/Running 节点兼容包装：用于通过 Coze 导入校验，运行参数见 node-mapping.csv。",
        "version": "v2",
        "position": position(node),
        "parameters": {
            "code": code_for_wrapper(node),
            "language": 5,
            "node_inputs": [],
            "node_outputs": {
                "output": {"type": "string", "value": None},
                "dify_node_id": {"type": "string", "value": None},
                "dify_node_type": {"type": "string", "value": None},
            },
        },
        "settingOnError": {"processType": 1, "retryTimes": 0, "switch": False, "timeoutMs": 60000},
    }


def build_end_node(nodes: list[dict[str, Any]]) -> dict[str, Any]:
    max_x = max((position(node)["x"] for node in nodes), default=0)
    min_y = min((position(node)["y"] for node in nodes), default=0)
    return {
        "id": END_ID,
        "type": "end",
        "title": "结束",
        "icon": END_ICON,
        "description": "工作流的最终节点，用于返回工作流运行后的结果信息",
        "position": {"x": max_x + 450, "y": min_y},
        "parameters": {
            "node_inputs": [
                {
                    "name": "output",
                    "input": {
                        "type": "string",
                        "value": COMPAT_OUTPUT,
                    },
                }
            ],
            "terminatePlan": "returnVariables",
        },
    }


def build_import_edges(workflow: dict[str, Any], id_map: dict[str, str]) -> list[dict[str, Any]]:
    """Build a Coze-valid linear import skeleton.

    Dify permits branch handles from if-else nodes. During the compatibility
    import we wrap non-start nodes as Coze code nodes, and ordinary Coze code
    nodes cannot safely own multiple branch exits. The original Dify graph is
    therefore preserved in edge-mapping.csv, while the package itself uses a
    one-output chain so Coze can create a draft workflow for manual rebinding.
    """
    ordered_nodes = [id_map[str(node["id"])] for node in workflow["graph"]["nodes"]]
    edges: list[dict[str, Any]] = []
    for source, target in zip(ordered_nodes, ordered_nodes[1:]):
        edges.append({"source_node": source, "target_node": target})
    if ordered_nodes:
        edges.append({"source_node": ordered_nodes[-1], "target_node": END_ID})
    return edges


def build_coze_workflow(workflow: dict[str, Any]) -> tuple[dict[str, Any], dict[str, str]]:
    dify_nodes = workflow["graph"]["nodes"]
    id_map = {str(node["id"]): coze_node_id(node) for node in dify_nodes}
    coze_nodes: list[dict[str, Any]] = []
    for node in dify_nodes:
        if dify_type(node) == "start":
            coze_nodes.append(build_start_node(node))
        else:
            coze_nodes.append(build_wrapper_node(node))
    coze_nodes.append(build_end_node(dify_nodes))
    return (
        {
            "schema_version": "1.0.0",
            "name": WORKFLOW_NAME,
            "id": int(WORKFLOW_ID),
            "description": "跑步AI助手-5.0 的 Dify/Running 到 Coze 格式兼容导入骨架。导入后按映射文件逐节点重建运行参数。",
            "mode": "workflow",
            "icon": COZE_ICON,
            "nodes": coze_nodes,
            "edges": build_import_edges(workflow, id_map),
        },
        id_map,
    )


def build_manifest() -> dict[str, Any]:
    return {
        "type": "Workflow",
        "version": "1.0.0",
        "main": {
            "id": int(WORKFLOW_ID),
            "name": WORKFLOW_NAME,
            "desc": "Dify/Running 到 Coze 兼容导入骨架",
            "icon": COZE_ICON,
            "version": "",
            "flowMode": 0,
            "commitId": "",
        },
        "sub": [],
    }


def runtime_mapping(dify_node_type: str) -> tuple[str, str]:
    mapping = {
        "start": ("start", "可自动转换输入变量；sys.query/sys.user_id 需在 Coze Start 输入中映射为 input/user_id。"),
        "llm": ("llm", "需在 Coze 重新选择模型；Dify prompt_template 可迁移为 systemPrompt/prompt。"),
        "code": ("code", "Python 代码需改写为 Coze Code 节点支持的 JavaScript/TypeScript，或确认当前运行时支持 Python。"),
        "if-else": ("condition", "条件表达式、case_id 和 false 分支需在 Coze Condition 节点逐项重建。"),
        "knowledge-retrieval": ("knowledge", "Dify dataset_ids 不能直接复用；需绑定 Coze 知识库 ID 和检索参数。"),
        "http-request": ("http", "URL、Header、Body 可迁移；env.* 变量和鉴权需在 Coze 工作空间重新配置。"),
        "answer": ("end/output", "Dify 多 Answer 终止点需汇入 Coze End 或 Output 节点。"),
        "assigner": ("variable_merge/code", "Dify conversation variable 写入需改为 Coze 变量/代码节点实现。"),
        "variable-aggregator": ("variable_merge", "聚合变量需在 Coze Variable Merge 中重建。"),
    }
    return mapping.get(dify_node_type, ("code", "未知节点类型，保留为兼容包装节点后人工处理。"))


def write_node_mapping(workflow: dict[str, Any], id_map: dict[str, str], path: Path) -> None:
    rows = []
    for node in workflow["graph"]["nodes"]:
        node_type = dify_type(node)
        target_type, note = runtime_mapping(node_type)
        rows.append(
            {
                "dify_node_id": node["id"],
                "coze_node_id": id_map[str(node["id"])],
                "dify_title": dify_title(node),
                "dify_type": node_type,
                "import_package_type": "start" if node_type == "start" else "code",
                "runtime_target_type": target_type,
                "auto_import_status": "importable_wrapper" if node_type != "start" else "auto_mapped",
                "manual_rebind_note": note,
                "dify_data_json": json_short(node.get("data", {})),
            }
        )
    with path.open("w", encoding="utf-8-sig", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=list(rows[0].keys()))
        writer.writeheader()
        writer.writerows(rows)


def write_edge_mapping(workflow: dict[str, Any], id_map: dict[str, str], path: Path) -> None:
    rows = []
    for edge in workflow["graph"]["edges"]:
        rows.append(
            {
                "dify_edge_id": edge.get("id", ""),
                "dify_source": edge.get("source", ""),
                "dify_target": edge.get("target", ""),
                "coze_source_node": id_map[str(edge.get("source", ""))],
                "coze_target_node": id_map[str(edge.get("target", ""))],
                "dify_source_handle": edge.get("sourceHandle", ""),
                "dify_target_handle": edge.get("targetHandle", ""),
                "coze_import_edge": "source_node -> target_node",
                "manual_rebind_note": "Dify branch handle 已保留在本表；Coze 条件节点重建后需恢复 source_port/分支语义。",
            }
        )
    with path.open("w", encoding="utf-8-sig", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=list(rows[0].keys()))
        writer.writeheader()
        writer.writerows(rows)


def write_report(workflow: dict[str, Any], package_zip: Path, node_csv: Path, edge_csv: Path, workflow_yaml: Path, report: Path) -> None:
    nodes = workflow["graph"]["nodes"]
    edges = workflow["graph"]["edges"]
    type_counter = Counter(dify_type(node) for node in nodes)
    lines = [
        "# Coze / Running 工作流格式兼容报告",
        "",
        f"- 生成时间：{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
        f"- Coze 样本格式来源：`{COZE / 'sample-export' / 'Workflow-dish_recognition_more_simple-draft-5725.zip'}`",
        f"- Running/Dify 原始候选包：`/Users/jack/Downloads/dify-running-ai-assistant-coze-upload-candidate.zip`",
        f"- 兼容输出 Zip：`{package_zip}`",
        f"- Coze DSL：`{workflow_yaml}`",
        f"- 节点映射：`{node_csv}`",
        f"- 连线映射：`{edge_csv}`",
        "",
        "## 根因",
        "",
        "Coze 导入器校验的是 Coze 原生导出的 Workflow Zip，而不是任意 Zip。Coze 包必须包含根目录、`MANIFEST.yml`、`workflow/*.yaml`，并且 YAML 内必须是 `schema_version/name/id/mode/icon/nodes/edges` 结构。原 Running 候选包只有 Dify 的 `dify-export-no-secret.yml`、`workflow-draft.json`、`workflow-normalized.json`，所以格式不兼容。",
        "",
        "## 本次兼容策略",
        "",
        "1. 按 Coze 原生导出样本生成 `MANIFEST.yml` 和 `workflow/running_ai_assistant_5_compat-draft.yaml`。",
        "2. 保留 Dify 工作流的节点数量、节点标题和画布坐标。",
        "3. 除 Start 外，Dify 节点先转换为 Coze `code` 兼容包装节点，避免因为 LLM、知识库、HTTP、条件分支等运行参数 schema 不一致导致导入失败。",
        "4. Coze 导入包采用线性单出口骨架：普通 `code` 包装节点不能承载 Dify 条件节点的多分支出口，否则 Coze 导入阶段会失败。",
        f"5. 每个节点的原始 Dify 参数完整写入 `node-mapping.csv`；原始 {len(edges)} 条分支 handle 和连线语义写入 `edge-mapping.csv`。",
        "6. 导入 Coze 后，按 `runtime_target_type`、`manual_rebind_note` 和 `edge-mapping.csv` 逐节点重建真实运行节点与分支。",
        "",
        "## 数量核对",
        "",
        f"- Dify 节点数：{len(nodes)}",
        f"- Dify 连线数：{len(edges)}",
        f"- Coze 兼容包节点数：{len(nodes) + 1}（额外增加 1 个 Coze End 节点）",
        f"- Coze 兼容包连线数：{len(nodes)}（线性单出口导入骨架；原始 {len(edges)} 条 Dify 连线见 `edge-mapping.csv`）",
        "",
        "## 节点类型映射",
        "",
        "| Dify 类型 | 数量 | Coze 导入包装类型 | Coze 运行目标类型 | 说明 |",
        "| --- | ---: | --- | --- | --- |",
    ]
    for node_type, count in sorted(type_counter.items()):
        target, note = runtime_mapping(node_type)
        import_type = "start" if node_type == "start" else "code"
        lines.append(f"| `{node_type}` | {count} | `{import_type}` | `{target}` | {note} |")
    lines.extend(
        [
            "",
            "## 仍需人工重绑的内容",
            "",
            "- 模型：Dify 的 `deepseek-v4-flash` 需要在 Coze 选择等价模型或替代模型。",
            "- 知识库：Dify `dataset_ids` 不能直接在 Coze 复用，需要绑定 Coze 知识库 ID。",
            "- 环境变量：`env.PROFILE_API_BASE_URL`、`env.PROFILE_API_KEY` 需在 Coze 工作空间重新配置。",
            "- 会话变量：Dify `conversation.*` 写入逻辑需改为 Coze 变量节点或代码节点。",
            "- 分支端口：Dify `sourceHandle` 已写入 `edge-mapping.csv`，导入骨架后需在 Coze 条件节点恢复分支语义。",
            "- 代码节点：Dify Python 代码需改写为 Coze Code 节点的 JavaScript/TypeScript，或确认 Coze 环境支持对应语言。",
            "",
            "## 验证建议",
            "",
            "1. 已上传兼容 Zip，Coze 能创建 `running_ai_assistant_5_compat` 工作流草稿。",
            "2. 已完成兼容包装试运行，页面显示“运行完成 2s / 0 Tokens”，但输出只是兼容包装提示文本。",
            "3. 后续用 `node-mapping.csv` 逐个节点替换包装节点为真实运行节点。",
            "4. 用现有 Excel 中的节点、参数、连线、场景测试逐项回归业务语义。",
            "",
            "## Coze 平台验证结果",
            "",
            "- 工作流地址：https://www.coze.cn/work_flow?workflow_id=7657510397302816794&space_id=7366210605697007616",
            "- 导入结果：PASS，Coze 通知显示“全部导入完成”。",
            "- 兼容包装试运行：PASS_COMPAT_WRAPPER_ONLY，输入 `我想了解5公里如何安排配速训练` 与 `coze_import_test_user_001` 后运行完成。",
            f"- 输出变量：`{COMPAT_OUTPUT}`",
            "- 业务语义测试：PENDING_REBIND，当前未重绑 Coze 原生 LLM、知识库、HTTP、条件、变量和代码节点，不能算跑步助手业务回答通过。",
            "- 证据截图：`evidence/screenshots/coze-import-run-success-20260701.png`",
        ]
    )
    report.write_text("\n".join(lines) + "\n", encoding="utf-8")


def zip_dir(source_dir: Path, target_zip: Path) -> None:
    if target_zip.exists():
        target_zip.unlink()
    with zipfile.ZipFile(target_zip, "w", zipfile.ZIP_DEFLATED) as zf:
        for path in sorted(source_dir.rglob("*")):
            if path.is_file():
                zf.write(path, path.relative_to(source_dir.parent))


def main() -> None:
    workflow = load_workflow()
    output_root = CONVERTED / PACKAGE_NAME
    if output_root.exists():
        shutil.rmtree(output_root)
    workflow_dir = output_root / "workflow"
    workflow_dir.mkdir(parents=True, exist_ok=True)

    coze_workflow, id_map = build_coze_workflow(workflow)
    manifest_path = output_root / "MANIFEST.yml"
    workflow_yaml = workflow_dir / f"{WORKFLOW_NAME}-draft.yaml"
    write_yaml(manifest_path, build_manifest())
    write_yaml(workflow_yaml, coze_workflow)

    package_zip = CONVERTED / f"{PACKAGE_NAME}.zip"
    zip_dir(output_root, package_zip)

    node_csv = CONVERTED / "node-mapping.csv"
    edge_csv = CONVERTED / "edge-mapping.csv"
    report = CONVERTED / "format-compatibility-report.md"
    write_node_mapping(workflow, id_map, node_csv)
    write_edge_mapping(workflow, id_map, edge_csv)
    write_report(workflow, package_zip, node_csv, edge_csv, workflow_yaml, report)

    print(package_zip)
    print(report)


if __name__ == "__main__":
    main()
