#!/usr/bin/env python3
"""Build a Coze package that follows the original Dify workflow structure.

This is intentionally not a replay workflow. It preserves the captured Dify
graph shape, node titles, coordinates, and original branch handles so the Coze
canvas can be used as the real migration target.
"""

from __future__ import annotations

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

from convert_running_to_coze_package import (
    CODE_ICON,
    COZE_ICON,
    END_ICON,
    START_ICON,
    dump_yaml,
    json_short,
    position,
)


ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "source"
CONVERTED = ROOT / "coze" / "converted"
WORKFLOW_DRAFT = SOURCE / "workflow-draft.json"

PACKAGE_NAME = "Workflow-running_ai_assistant_5_original_structure_v1-draft-0001"
WORKFLOW_NAME = "running_ai_assistant_5_original_structure_v1"
WORKFLOW_ID = "8973525000000000092"
START_ID = "100001"
ANSWER_MUX_ID = "899999"
END_ID = "900001"

LLM_ICON = "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg"
KNOWLEDGE_ICON = "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-KnowledgeQuery-v2.jpg"


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


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
    return str(dify_node["id"])


def selector_to_value(selector: list[str] | tuple[str, ...] | None) -> dict[str, str] | str:
    if not selector:
        return ""
    ref_node, *path_parts = [str(part) for part in selector]
    if ref_node == "sys" and path_parts == ["query"]:
        return {"ref_node": START_ID, "path": "input"}
    if ref_node == "sys" and path_parts == ["user_id"]:
        return {"ref_node": START_ID, "path": "user_id"}
    if ref_node == "conversation":
        return {"ref_node": START_ID, "path": "conversation_" + "_".join(path_parts)}
    return {"ref_node": ref_node, "path": ".".join(path_parts)}


def scalar_type(dify_type_name: str | None) -> str:
    mapping = {
        "string": "string",
        "number": "float",
        "integer": "integer",
        "boolean": "boolean",
        "object": "object",
        "array": "list",
        "array[string]": "list",
    }
    return mapping.get(str(dify_type_name or "").lower(), "string")


def node_outputs_from_dify(outputs: dict[str, Any] | None, *, defaults: dict[str, str] | None = None) -> dict[str, Any]:
    if not outputs:
        outputs = defaults or {"output": "string"}
    result: dict[str, Any] = {}
    for name, spec in outputs.items():
        if isinstance(spec, dict):
            out_type = scalar_type(spec.get("type"))
        else:
            out_type = scalar_type(str(spec))
        if out_type == "list":
            result[name] = {"type": "list", "items": {"type": "string", "value": None}, "value": None}
        else:
            result[name] = {"type": out_type, "value": None}
    return result


def build_start_node(node: dict[str, Any]) -> dict[str, Any]:
    data = node.get("data") or {}
    outputs: dict[str, Any] = {
        "input": {"type": "string", "required": True, "value": None},
        "user_id": {"type": "string", "required": False, "value": None},
    }
    for variable in data.get("variables") or []:
        name = variable.get("variable") or variable.get("label")
        if name:
            outputs[str(name)] = {
                "type": scalar_type(variable.get("type")),
                "required": bool(variable.get("required", False)),
                "value": None,
            }
    for conversation_name in ["last_route", "last_intent", "last_stage", "missing_info", "last_user_query", "pending_task"]:
        outputs[f"conversation_{conversation_name}"] = {"type": "string", "required": False, "value": None}
    return {
        "id": START_ID,
        "type": "start",
        "title": "开始",
        "icon": START_ICON,
        "description": "原 Dify 开始节点：用户输入",
        "position": position(node),
        "parameters": {"node_outputs": outputs},
    }


def prompt_parts(node: dict[str, Any]) -> tuple[str, str]:
    system_chunks: list[str] = []
    user_chunks: list[str] = []
    for item in (node.get("data") or {}).get("prompt_template") or []:
        text = str(item.get("text") or "")
        if item.get("role") == "system":
            system_chunks.append(text)
        else:
            user_chunks.append(text)
    return "\n\n".join(system_chunks), "\n\n".join(user_chunks)


def build_llm_node(node: dict[str, Any]) -> dict[str, Any]:
    data = node.get("data") or {}
    model = data.get("model") or {}
    completion = model.get("completion_params") or {}
    system_prompt, prompt = prompt_parts(node)
    temperature = completion.get("temperature", 0)
    return {
        "id": coze_node_id(node),
        "type": "llm",
        "title": dify_title(node),
        "icon": LLM_ICON,
        "description": "从 Dify LLM 节点自动迁移 prompt；模型需在 Coze 工作台复核绑定。",
        "version": "3",
        "position": position(node),
        "parameters": {
            "fcParam": {"llmNodeUID": "", "pluginFCParam": {}, "spaceID": "7366210605697007616", "workflowVersion": ""},
            "fcParamVar": {"knowledgeFCParam": {}},
            "llmParam": [
                {"name": "apiMode", "input": {"type": "integer", "value": "0"}},
                {"name": "temperature", "input": {"type": "float", "value": str(temperature)}},
                {"name": "topP", "input": {"type": "float", "value": "1"}},
                {"name": "frequencyPenalty", "input": {"type": "float", "value": "0"}},
                {"name": "maxTokens", "input": {"type": "integer", "value": str(completion.get("max_tokens", 4096))}},
                {"name": "modelName", "input": {"type": "string", "value": str(model.get("name") or "deepseek-v4-flash")}},
                {"name": "modelType", "input": {"type": "integer", "value": "0"}},
                {"name": "prompt", "input": {"type": "string", "value": prompt or "{{input}}"}},
                {"name": "systemPrompt", "input": {"type": "string", "value": system_prompt}},
                {"name": "enableChatHistory", "input": {"type": "boolean", "value": False}},
                {"name": "chatHistoryRound", "input": {"type": "integer", "value": "3"}},
            ],
            "node_inputs": [{"name": "input", "input": {"value": {"path": "input", "ref_node": START_ID}}}],
            "node_outputs": {
                "text": {"type": "string", "value": None},
                "output": {"type": "string", "value": None},
                "reasoning_content": {"type": "string", "value": None},
            },
            "settingOnError": {"processType": 1, "retryTimes": 0, "switch": False, "timeoutMs": 600000},
        },
    }


def code_return_for_outputs(outputs: dict[str, Any]) -> str:
    values = []
    for name, spec in outputs.items():
        out_type = spec.get("type")
        if out_type in {"integer", "float", "number"}:
            default = "0"
        elif out_type == "boolean":
            default = "false"
        elif out_type == "object":
            default = "{}"
        elif out_type == "list":
            default = "[]"
        else:
            default = json.dumps(f"PENDING_REBIND:{name}", ensure_ascii=False)
        values.append(f"        {json.dumps(name, ensure_ascii=False)}: {default}")
    return "{\n" + ",\n".join(values) + "\n    }"


def build_pending_code(node: dict[str, Any], target_type: str, note: str, outputs: dict[str, Any] | None = None) -> dict[str, Any]:
    data = node.get("data") or {}
    output_schema = node_outputs_from_dify(outputs or data.get("outputs"), defaults={"output": "string"})
    digest = hashlib.sha1(json.dumps(data, ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest()[:12]
    summary = {
        "dify_node_id": str(node["id"]),
        "dify_node_type": dify_type(node),
        "target_type": target_type,
        "dify_data_sha1": digest,
        "note": note,
    }
    code = (
        "// Original-structure migration placeholder.\n"
        "// This node preserves the original Dify node position, title, outputs, and edges.\n"
        "// Rebind it to the Coze native runtime type shown below before production use.\n"
        f"const MIGRATION_NOTE = {json.dumps(summary, ensure_ascii=False, indent=2)};\n\n"
        "async function main({ params }: Args): Promise<Output> {\n"
        f"    return {code_return_for_outputs(output_schema)};\n"
        "}\n"
    )
    return {
        "id": coze_node_id(node),
        "type": "code",
        "title": f"{dify_title(node)}",
        "icon": CODE_ICON,
        "description": f"原 Dify `{dify_type(node)}` 节点；目标 Coze 类型：{target_type}。{note}",
        "version": "v2",
        "position": position(node),
        "parameters": {
            "code": code,
            "language": 5,
            "node_inputs": [],
            "node_outputs": output_schema,
        },
        "settingOnError": {"processType": 1, "retryTimes": 0, "switch": False, "timeoutMs": 60000},
    }


def build_code_node(node: dict[str, Any]) -> dict[str, Any]:
    return build_pending_code(
        node,
        "code",
        "原 Dify 代码是 Python，需人工或脚本转换为 Coze TypeScript 后再启用。",
    )


def build_knowledge_node(node: dict[str, Any]) -> dict[str, Any]:
    data = node.get("data") or {}
    retrieval = data.get("multiple_retrieval_config") or {}
    dataset_ids = [str(item) for item in data.get("dataset_ids") or []]
    selector = data.get("query_variable_selector") or []
    return {
        "id": coze_node_id(node),
        "type": "knowledge",
        "title": dify_title(node),
        "icon": KNOWLEDGE_ICON,
        "description": "从 Dify knowledge-retrieval 自动迁移；datasetList 需替换为 Coze 知识库 ID。",
        "position": position(node),
        "parameters": {
            "datasetParam": [
                {
                    "name": "datasetList",
                    "input": {"type": "list", "items": {"type": "string", "value": None}, "value": dataset_ids},
                },
                {"name": "topK", "input": {"type": "integer", "value": str(retrieval.get("top_k", 3))}},
                {"name": "useRerank", "input": {"type": "boolean", "value": bool(retrieval.get("reranking_enable", False))}},
                {"name": "useRewrite", "input": {"type": "boolean", "value": False}},
                {"name": "isPersonalOnly", "input": {"type": "boolean", "value": False}},
                {"name": "datasetType", "input": {"type": "integer", "value": "0"}},
                {"name": "VolcanoInfoList", "input": {"type": "list", "items": {"type": "object", "value": None}, "value": []}},
                {"name": "useNl2sql", "input": {"type": "boolean", "value": False}},
                {"name": "minScore", "input": {"type": "float", "value": "0.5"}},
                {"name": "strategy", "input": {"type": "integer", "value": "1"}},
            ],
            "node_inputs": [
                {"name": "Query", "input": {"value": selector_to_value(selector)}},
                {"name": "enableChatHistory", "input": {"type": "boolean", "value": False}},
                {"name": "chatHistoryRound", "input": {"type": "float", "value": 3}},
            ],
            "node_outputs": {
                "result": {"type": "string", "value": None},
                "outputList": {
                    "type": "list",
                    "items": {"type": "object", "properties": {"output": {"type": "string", "value": None}}, "value": None},
                    "value": None,
                },
            },
        },
    }


def build_answer_as_code(node: dict[str, Any]) -> dict[str, Any]:
    data = node.get("data") or {}
    answer = str(data.get("answer") or "")
    code = (
        "// Original Dify answer node preserved as a Coze code node.\n"
        "// Coze requires a single fixed end node (900001), so this node keeps\n"
        "// the Dify answer template and feeds the shared answer multiplexer.\n"
        f"const ANSWER_TEMPLATE = {json.dumps(answer, ensure_ascii=False)};\n\n"
        "async function main({ params }: Args): Promise<Output> {\n"
        "    return { output: ANSWER_TEMPLATE };\n"
        "}\n"
    )
    return {
        "id": coze_node_id(node),
        "type": "code",
        "title": dify_title(node),
        "icon": CODE_ICON,
        "description": "原 Dify answer 节点；Coze 只允许一个固定结束节点，因此先保留为答案输出节点。",
        "version": "v2",
        "position": position(node),
        "parameters": {
            "code": code,
            "language": 5,
            "node_inputs": [],
            "node_outputs": {"output": {"type": "string", "value": None}},
        },
        "settingOnError": {"processType": 1, "retryTimes": 0, "switch": False, "timeoutMs": 60000},
    }


def build_answer_mux_node(answer_nodes: list[dict[str, Any]]) -> dict[str, Any]:
    if not answer_nodes:
        mux_inputs = [{"name": "fallback", "input": {"type": "string", "value": ""}}]
        values = ["params.fallback"]
    else:
        mux_inputs = []
        values = []
        for index, node in enumerate(answer_nodes, 1):
            input_name = f"answer_{index}"
            mux_inputs.append({"name": input_name, "input": {"value": {"path": "output", "ref_node": coze_node_id(node)}}})
            values.append(f"params.{input_name}")
    code = (
        "// Shared terminal bridge for all preserved Dify answer nodes.\n"
        "async function main({ params }: Args): Promise<Output> {\n"
        f"    const candidates = [{', '.join(values)}];\n"
        "    const output = candidates.find((value) => typeof value === 'string' && value.length > 0) || '';\n"
        "    return { output };\n"
        "}\n"
    )
    max_x = max((position(node)["x"] for node in answer_nodes), default=7600)
    avg_y = sum((position(node)["y"] for node in answer_nodes), 0.0) / max(len(answer_nodes), 1)
    return {
        "id": ANSWER_MUX_ID,
        "type": "code",
        "title": "答案汇聚 / Coze 单结束节点适配",
        "icon": CODE_ICON,
        "description": "Coze 仅允许一个固定 end 节点；该节点汇聚原 Dify 多个 answer 分支。",
        "version": "v2",
        "position": {"x": max_x + 360, "y": avg_y},
        "parameters": {
            "code": code,
            "language": 5,
            "node_inputs": mux_inputs,
            "node_outputs": {"output": {"type": "string", "value": None}},
        },
        "settingOnError": {"processType": 1, "retryTimes": 0, "switch": False, "timeoutMs": 60000},
    }


def build_end_node(answer_nodes: list[dict[str, Any]]) -> dict[str, Any]:
    max_x = max((position(node)["x"] for node in answer_nodes), default=8000)
    avg_y = sum((position(node)["y"] for node in answer_nodes), 0.0) / max(len(answer_nodes), 1)
    return {
        "id": END_ID,
        "type": "end",
        "title": "结束",
        "icon": END_ICON,
        "description": "Coze 固定结束节点；最终返回答案汇聚节点输出。",
        "position": {"x": max_x + 720, "y": avg_y},
        "parameters": {
            "node_inputs": [
                {"name": "output", "input": {"type": "string", "value": {"path": "output", "ref_node": ANSWER_MUX_ID}}}
            ],
            "terminatePlan": "returnVariables",
        },
    }


def build_node(node: dict[str, Any]) -> dict[str, Any]:
    node_type = dify_type(node)
    if node_type == "start":
        return build_start_node(node)
    if node_type == "llm":
        return build_llm_node(node)
    if node_type == "knowledge-retrieval":
        return build_knowledge_node(node)
    if node_type == "code":
        return build_code_node(node)
    if node_type == "answer":
        return build_answer_as_code(node)
    if node_type == "if-else":
        return build_pending_code(node, "condition", "条件表达式和分支端口已保留在 edges.source_port，需在 Coze 改为原生条件节点。")
    if node_type == "http-request":
        return build_pending_code(node, "http", "URL、Header、Body、重试和异常分支已保留在映射文件，需在 Coze 改为原生 HTTP 节点。")
    if node_type == "assigner":
        return build_pending_code(node, "variable_assign", "会话变量写入需在 Coze 变量赋值节点中重建。")
    if node_type == "variable-aggregator":
        return build_pending_code(node, "variable_merge", "变量聚合需在 Coze 变量聚合/代码节点中重建。", outputs={"output": "string"})
    return build_pending_code(node, "unknown", "未知 Dify 节点类型，需人工确认。")


def build_edges(workflow: dict[str, Any], id_map: dict[str, str]) -> list[dict[str, Any]]:
    edges: list[dict[str, Any]] = []
    for edge in workflow["graph"]["edges"]:
        item = {
            "source_node": id_map[str(edge["source"])],
            "target_node": id_map[str(edge["target"])],
        }
        source_handle = edge.get("sourceHandle")
        if source_handle and source_handle != "source":
            item["source_port"] = str(source_handle)
        edges.append(item)
    for node in workflow["graph"]["nodes"]:
        if dify_type(node) == "answer":
            edges.append({"source_node": id_map[str(node["id"])], "target_node": ANSWER_MUX_ID})
    edges.append({"source_node": ANSWER_MUX_ID, "target_node": END_ID})
    return edges


def build_workflow(workflow: dict[str, Any]) -> tuple[dict[str, Any], dict[str, str]]:
    nodes = workflow["graph"]["nodes"]
    id_map = {str(node["id"]): coze_node_id(node) for node in nodes}
    answer_nodes = [node for node in nodes if dify_type(node) == "answer"]
    coze_nodes = [build_node(node) for node in nodes]
    coze_nodes.append(build_answer_mux_node(answer_nodes))
    coze_nodes.append(build_end_node(answer_nodes))
    return (
        {
            "schema_version": "1.0.0",
            "name": WORKFLOW_NAME,
            "id": int(WORKFLOW_ID),
            "description": "跑步AI助手-5.0 原 Dify 编排结构迁移包：保留原节点、原坐标和原分支边，非回放测试包。",
            "mode": "workflow",
            "icon": COZE_ICON,
            "nodes": coze_nodes,
            "edges": build_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": "跑步AI助手-5.0 原 Dify 编排结构迁移包 v1",
            "icon": COZE_ICON,
            "version": "",
            "flowMode": 0,
            "commitId": "",
        },
        "sub": [],
    }


def write_original_mapping(workflow: dict[str, Any], id_map: dict[str, str], path: Path) -> None:
    rows = []
    for node in workflow["graph"]["nodes"]:
        data = node.get("data") or {}
        target = {
            "start": "start",
            "llm": "llm",
            "knowledge-retrieval": "knowledge",
            "code": "code_pending_python_to_typescript",
            "answer": "answer_template_code_with_shared_end",
            "if-else": "condition_pending_native_rebind",
            "http-request": "http_pending_native_rebind",
            "assigner": "variable_assign_pending_native_rebind",
            "variable-aggregator": "variable_merge_pending_native_rebind",
        }.get(dify_type(node), "unknown")
        rows.append(
            {
                "dify_node_id": node["id"],
                "coze_node_id": id_map[str(node["id"])],
                "title": dify_title(node),
                "dify_type": dify_type(node),
                "coze_import_type": build_node(node)["type"],
                "runtime_target_type": target,
                "position_json": json.dumps(position(node), ensure_ascii=False),
                "dify_data_json": json_short(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_original_report(workflow: dict[str, Any], package_zip: 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 original-structure migration package",
        "",
        "## 结论",
        "",
        "本包不是 88 条测试回放包，而是按 Dify 原始编排生成的结构迁移包。",
        "",
        f"- 输出 Zip：`{package_zip}`",
        f"- Workflow YAML：`{workflow_yaml}`",
        f"- Dify 原节点数：{len(nodes)}",
        f"- Coze 迁移包节点数：{len(nodes) + 2}",
        f"- Dify 原连线数：{len(edges)}",
        f"- Coze 迁移包连线数：{len(edges) + type_counter.get('answer', 0) + 1}",
        f"- Coze 平台桥接节点：2 个（答案汇聚 `{ANSWER_MUX_ID}` + 固定结束 `{END_ID}`）",
        "",
        "## 节点类型统计",
        "",
        "| Dify 类型 | 数量 | 本包处理方式 |",
        "| --- | ---: | --- |",
    ]
    handling = {
        "start": "Coze start",
        "llm": "Coze llm，迁移 prompt，模型需复核",
        "knowledge-retrieval": "Coze knowledge，dataset 需换绑 Coze 知识库",
        "code": "Coze code 占位，保留输出结构，Python 需转 TypeScript",
        "answer": "保留为答案输出 code 节点，统一汇聚到 Coze 固定 end",
        "if-else": "结构保留，需重绑 Coze condition",
        "http-request": "结构保留，需重绑 Coze HTTP",
        "assigner": "结构保留，需重绑变量赋值",
        "variable-aggregator": "结构保留，需重绑变量聚合",
    }
    for node_type, count in sorted(type_counter.items()):
        lines.append(f"| `{node_type}` | {count} | {handling.get(node_type, '待人工确认')} |")
    lines.extend(
        [
            "",
            "## 与 replay v4 的差异",
            "",
            "- replay v4：22 个 Code 节点，只服务 88 条验收问题回放。",
            "- original-structure v1：保留 58 个 Dify 业务节点和 72 条原始边；另加 2 个 Coze 平台桥接节点以满足固定 end 约束。",
            "",
            "## 仍需处理",
            "",
            "1. Dify Python Code 节点需要转换为 Coze TypeScript。",
            "2. if-else、HTTP、变量赋值和变量聚合节点需要在 Coze 平台改为原生节点。",
            "3. 知识库 ID、模型 ID、环境变量和鉴权不能跨平台直接复用，需要在 Coze 工作台重新绑定。",
        ]
    )
    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_workflow(workflow)
    workflow_yaml = workflow_dir / f"{WORKFLOW_NAME}-draft.yaml"
    write_yaml(output_root / "MANIFEST.yml", build_manifest())
    write_yaml(workflow_yaml, coze_workflow)

    package_zip = CONVERTED / f"{PACKAGE_NAME}.zip"
    zip_dir(output_root, package_zip)
    write_original_mapping(workflow, id_map, CONVERTED / "original-structure-node-mapping.csv")
    write_original_report(workflow, package_zip, workflow_yaml, CONVERTED / "original-structure-migration-report.md")
    print(package_zip)
    print(workflow_yaml)


if __name__ == "__main__":
    main()
