from __future__ import annotations

import argparse
from copy import deepcopy

from ..context import CliContext
from ..json_io import load_json_value
from .common import load_list_arg, load_object_arg, parse_bool_text, raise_config_error, require_list_arg


def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
    parser = subparsers.add_parser("builder", aliases=["build"], help="稳定 builder 命令")
    builder_subparsers = parser.add_subparsers(dest="builder_command", required=True)

    file_parser = builder_subparsers.add_parser("file", help="builder 侧文件上传")
    file_subparsers = file_parser.add_subparsers(dest="builder_file_command", required=True)
    file_upload_local = file_subparsers.add_parser("upload-local", help="上传本地文件并返回附件值")
    file_upload_local.add_argument("--upload-kind", default="attachment")
    file_upload_local.add_argument("--file-path", required=True)
    file_upload_local.add_argument("--upload-mark")
    file_upload_local.add_argument("--content-type")
    file_upload_local.add_argument("--bucket-type")
    file_upload_local.add_argument("--path-id", type=int)
    file_upload_local.add_argument("--file-related-url")
    file_upload_local.set_defaults(handler=_handle_file_upload_local, format_hint="file_upload_local")

    feedback = builder_subparsers.add_parser("feedback", help="builder 侧反馈提交")
    feedback_subparsers = feedback.add_subparsers(dest="builder_feedback_command", required=True)
    feedback_submit = feedback_subparsers.add_parser("submit", help="提交 builder 能力反馈")
    feedback_submit.add_argument("--category", required=True)
    feedback_submit.add_argument("--title", required=True)
    feedback_submit.add_argument("--description", required=True)
    feedback_submit.add_argument("--expected-behavior")
    feedback_submit.add_argument("--actual-behavior")
    feedback_submit.add_argument("--impact-scope")
    feedback_submit.add_argument("--tool-name")
    feedback_submit.add_argument("--app-key")
    feedback_submit.add_argument("--record-id")
    feedback_submit.add_argument("--workflow-node-id")
    feedback_submit.add_argument("--note")
    feedback_submit.set_defaults(handler=_handle_feedback_submit, format_hint="feedback_submit")

    contract = builder_subparsers.add_parser("contract", help="读取 builder tool 合约")
    contract.add_argument("--tool-name", required=True)
    contract.set_defaults(handler=_handle_contract, format_hint="builder_summary")

    icon = builder_subparsers.add_parser("icon", help="工作区图标目录")
    icon_subparsers = icon.add_subparsers(dest="builder_icon_command", required=True)
    icon_catalog = icon_subparsers.add_parser("catalog", help="读取应用、应用包、门户可用图标目录")
    icon_catalog.set_defaults(handler=_handle_icon_catalog, format_hint="builder_summary")

    member = builder_subparsers.add_parser("member", help="成员目录")
    member_subparsers = member.add_subparsers(dest="builder_member_command", required=True)
    member_search = member_subparsers.add_parser("search", help="搜索成员")
    member_search.add_argument("--query", required=True)
    member_search.add_argument("--page-num", type=int, default=1)
    member_search.add_argument("--page-size", type=int, default=20)
    member_search.add_argument("--contain-disable", action=argparse.BooleanOptionalAction, default=False)
    member_search.set_defaults(handler=_handle_member_search, format_hint="builder_summary")

    role = builder_subparsers.add_parser("role", help="角色目录")
    role_subparsers = role.add_subparsers(dest="builder_role_command", required=True)
    role_search = role_subparsers.add_parser("search", help="搜索角色")
    role_search.add_argument("--keyword", required=True)
    role_search.add_argument("--page-num", type=int, default=1)
    role_search.add_argument("--page-size", type=int, default=20)
    role_search.set_defaults(handler=_handle_role_search, format_hint="builder_summary")

    role_create = role_subparsers.add_parser("create", help="创建角色")
    role_create.add_argument("--role-name", required=True)
    role_create.add_argument("--member-uids-file")
    role_create.add_argument("--member-emails-file")
    role_create.add_argument("--member-names-file")
    role_create.add_argument("--role-icon", default="ex-user-outlined")
    role_create.set_defaults(handler=_handle_role_create, format_hint="builder_summary")

    package = builder_subparsers.add_parser("package", help="应用包")
    package_subparsers = package.add_subparsers(dest="builder_package_command", required=True)

    solution = builder_subparsers.add_parser("solution", help="解决方案")
    solution_subparsers = solution.add_subparsers(dest="builder_solution_command", required=True)
    solution_install = solution_subparsers.add_parser("install", help="安装解决方案")
    solution_install.add_argument("--solution-key", required=True)
    solution_install.add_argument("--being-copy-data", action=argparse.BooleanOptionalAction, default=True)
    solution_install.add_argument("--solution-source", default="solutionDetail")
    solution_install.set_defaults(handler=_handle_solution_install, format_hint="builder_summary")

    package_list = package_subparsers.add_parser("list", help="列出应用包")
    package_list.add_argument("--trial-status", default="all")
    package_list.add_argument("--query", default="")
    package_list.set_defaults(handler=_handle_package_list, format_hint="builder_summary")

    package_get = package_subparsers.add_parser("get", help="读取应用包详情")
    package_get.add_argument("--package-id", type=int, required=True)
    package_get.set_defaults(handler=_handle_package_get, format_hint="builder_summary")

    package_apply = package_subparsers.add_parser("apply", help="创建或更新应用包配置")
    package_apply.add_argument("--config-file", required=True)
    package_apply.set_defaults(handler=_handle_package_apply, format_hint="builder_summary", force_json_output=True)

    app = builder_subparsers.add_parser("app", help="应用")
    app_subparsers = app.add_subparsers(dest="builder_app_command", required=True)

    app_resolve = app_subparsers.add_parser("resolve", help="解析应用")
    app_resolve.add_argument("--app-key", default="")
    app_resolve.add_argument("--app-name", default="")
    app_resolve.add_argument("--package-id", type=int)
    app_resolve.set_defaults(handler=_handle_app_resolve, format_hint="builder_summary")

    app_release_lock = app_subparsers.add_parser("release-edit-lock-if-mine", help="释放当前账号持有的编辑锁")
    app_release_lock.add_argument("--app-key", required=True)
    app_release_lock.add_argument("--lock-owner-email", required=True)
    app_release_lock.add_argument("--lock-owner-name", required=True)
    app_release_lock.set_defaults(handler=_handle_app_release_edit_lock_if_mine, format_hint="builder_summary", force_json_output=True)

    app_get = app_subparsers.add_parser("get", help="读取应用配置（字段请使用: builder app get --app-key APP fields）")
    app_get.add_argument(
        "builder_app_get_section",
        nargs="?",
        choices=["summary", "fields", "layout", "views", "flow", "charts", "buttons", "associated-resources"],
        default="summary",
    )
    app_get.add_argument("--app-key", default="")
    app_get.add_argument("--app-keys", default="", help="逗号分隔的多应用 app_key；用于批量读取同一 section")
    app_get.add_argument("--app-keys-file", help="JSON 字符串数组；用于批量读取同一 section")
    app_get.set_defaults(handler=_handle_app_get, format_hint="builder_summary")

    app_repair_code_blocks = app_subparsers.add_parser("repair-code-blocks", help="扫描或修复现有代码块配置")
    app_repair_code_blocks.add_argument("--app-key", required=True)
    app_repair_code_blocks.add_argument("--field")
    app_repair_code_blocks.add_argument("--apply", action="store_true")
    app_repair_code_blocks.set_defaults(handler=_handle_app_repair_code_blocks, format_hint="builder_summary")

    button = builder_subparsers.add_parser("button", help="自定义按钮")
    button_subparsers = button.add_subparsers(dest="builder_button_command", required=True)

    button_catalog = button_subparsers.add_parser("catalog", help="读取按钮样式目录")
    button_catalog.set_defaults(handler=_handle_button_catalog, format_hint="builder_summary")

    button_get = button_subparsers.add_parser("get", help="读取应用自定义按钮配置")
    button_get.add_argument("--app-key", default="")
    button_get.add_argument("--app-keys", default="", help="逗号分隔的多应用 app_key；用于批量读取按钮配置")
    button_get.add_argument("--app-keys-file", help="JSON 字符串数组；用于批量读取按钮配置")
    button_get.set_defaults(handler=_handle_button_get, format_hint="builder_summary")

    button_apply = button_subparsers.add_parser("apply", help="声明式创建、更新或删除自定义按钮")
    button_apply.add_argument("--app-key", default="")
    button_apply.add_argument("--apps-file", help="多应用按钮配置 JSON 数组；每项包含 app_key 和按钮 payload")
    button_apply.add_argument("--upsert-buttons-file")
    button_apply.add_argument("--patch-buttons-file")
    button_apply.add_argument("--remove-buttons-file")
    button_apply.add_argument("--view-configs-file")
    button_apply.set_defaults(handler=_handle_button_apply, format_hint="builder_summary", force_json_output=True)

    associated_resource = builder_subparsers.add_parser("associated-resource", aliases=["associated-resources"], help="关联视图/报表")
    associated_resource_subparsers = associated_resource.add_subparsers(dest="builder_associated_resource_command", required=True)
    associated_resource_get = associated_resource_subparsers.add_parser("get", help="读取应用关联资源配置")
    associated_resource_get.add_argument("--app-key", default="")
    associated_resource_get.add_argument("--app-keys", default="", help="逗号分隔的多应用 app_key；用于批量读取关联资源配置")
    associated_resource_get.add_argument("--app-keys-file", help="JSON 字符串数组；用于批量读取关联资源配置")
    associated_resource_get.set_defaults(handler=_handle_associated_resource_get, format_hint="builder_summary")

    associated_resource_apply = associated_resource_subparsers.add_parser("apply", help="声明式管理应用关联资源池和视图展示配置")
    associated_resource_apply.add_argument("--app-key", default="")
    associated_resource_apply.add_argument("--apps-file", help="多应用关联资源配置 JSON 数组；每项包含 app_key 和资源 payload")
    associated_resource_apply.add_argument("--upsert-resources-file")
    associated_resource_apply.add_argument("--patch-resources-file")
    associated_resource_apply.add_argument("--remove-associated-item-ids-file")
    associated_resource_apply.add_argument("--reorder-associated-item-ids-file")
    associated_resource_apply.add_argument("--view-configs-file")
    associated_resource_apply.set_defaults(handler=_handle_associated_resource_apply, format_hint="builder_summary", force_json_output=True)

    portal = builder_subparsers.add_parser("portal", help="门户")
    portal_subparsers = portal.add_subparsers(dest="builder_portal_command", required=True)
    portal_list = portal_subparsers.add_parser("list", help="列出可配置门户")
    portal_list.set_defaults(handler=_handle_portal_list, format_hint="builder_summary")

    portal_get = portal_subparsers.add_parser("get", help="读取门户详情")
    portal_get.add_argument("--dash-key", required=True)
    portal_get.add_argument("--being-draft", action=argparse.BooleanOptionalAction, default=True)
    portal_get.set_defaults(handler=_handle_portal_get, format_hint="builder_summary")

    portal_apply = portal_subparsers.add_parser("apply", help="更新门户")
    portal_apply.add_argument("--dash-key", default="")
    portal_apply.add_argument("--dash-name", default="")
    portal_apply.add_argument("--package-id", type=int)
    portal_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
    portal_apply.add_argument("--payload-file")
    portal_apply.add_argument("--sections-file")
    portal_apply.add_argument("--layout-preset", default="")
    portal_apply.add_argument("--visibility-file")
    portal_apply.add_argument("--auth-file")
    portal_apply.add_argument("--icon")
    portal_apply.add_argument("--color")
    portal_apply.add_argument("--hide-copyright", action=argparse.BooleanOptionalAction, default=None)
    portal_apply.add_argument("--dash-global-config-file")
    portal_apply.add_argument("--config-file")
    portal_apply.add_argument("--patch-sections-file", help="门户组件局部更新 JSON 数组，每项含 chart_ref/view_ref/order + set/unset")
    portal_apply.set_defaults(handler=_handle_portal_apply, format_hint="builder_summary", force_json_output=True)

    portal_delete = portal_subparsers.add_parser("delete", help="删除门户")
    portal_delete.add_argument("--dash-key", required=True)
    portal_delete.set_defaults(handler=_handle_portal_delete, format_hint="builder_summary", force_json_output=True)

    schema_apply = builder_subparsers.add_parser("schema", help="字段搭建")
    schema_apply_subparsers = schema_apply.add_subparsers(dest="builder_schema_command", required=True)
    schema_apply_apply = schema_apply_subparsers.add_parser("apply", help="执行字段变更")
    schema_apply_apply.add_argument("--app-key", default="")
    schema_apply_apply.add_argument("--package-id", type=int)
    schema_apply_apply.add_argument("--app-name", default="")
    schema_apply_apply.add_argument("--app-title", default="")
    schema_apply_apply.add_argument("--icon")
    schema_apply_apply.add_argument("--color")
    schema_apply_apply.add_argument("--visibility-file")
    schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=None)
    schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 对象；每项可带 client_key/app_name/form，支持 relation target_app_ref")
    schema_apply_apply.add_argument("--form-file", help="单应用表单结构 JSON 数组；form[].rows[][] 同时声明字段和表单分组布局")
    schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组；字段可用 as_data_title/as_data_cover 标记数据标题/封面")
    schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组；set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
    schema_apply_apply.add_argument("--remove-fields-file")
    schema_apply_apply.set_defaults(handler=_handle_schema_apply, format_hint="builder_summary", force_json_output=True)

    layout_apply = builder_subparsers.add_parser("layout", help="布局")
    layout_apply_subparsers = layout_apply.add_subparsers(dest="builder_layout_command", required=True)
    layout_apply_apply = layout_apply_subparsers.add_parser("apply", help="执行布局变更")
    layout_apply_apply.add_argument("--app-key", default="")
    layout_apply_apply.add_argument("--apps-file", help="多应用布局 JSON 数组；每项包含 app_key/mode?/publish?/sections")
    layout_apply_apply.add_argument("--mode", choices=["merge", "replace"], default="merge")
    layout_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
    layout_apply_apply.add_argument("--sections-file")
    layout_apply_apply.set_defaults(handler=_handle_layout_apply, format_hint="builder_summary", force_json_output=True)

    views_apply = builder_subparsers.add_parser("views", help="视图")
    views_apply_subparsers = views_apply.add_subparsers(dest="builder_views_command", required=True)
    views_apply_apply = views_apply_subparsers.add_parser("apply", help="执行视图变更")
    views_apply_apply.add_argument("--app-key", default="")
    views_apply_apply.add_argument("--views-file", help='推荐批量视图 JSON 对象：{"views":[...]}；每项包含 app_key、operation、视图配置')
    views_apply_apply.add_argument("--apps-file", help="多应用视图 JSON 数组；每项包含 app_key 和 upsert/patch/remove payload")
    views_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
    views_apply_apply.add_argument("--upsert-views-file")
    views_apply_apply.add_argument("--patch-views-file")
    views_apply_apply.add_argument("--remove-views-file")
    views_apply_apply.set_defaults(handler=_handle_views_apply, format_hint="builder_summary", force_json_output=True)

    flow_apply = builder_subparsers.add_parser("flow", help="流程")
    flow_apply_subparsers = flow_apply.add_subparsers(dest="builder_flow_command", required=True)
    flow_schema = flow_apply_subparsers.add_parser("schema", help="读取 WorkflowSpec JSON Schema")
    flow_schema.add_argument("--schema-version", default="")
    flow_schema.set_defaults(handler=_handle_flow_schema, format_hint="builder_summary")

    flow_get = flow_apply_subparsers.add_parser("get", help="读取 WorkflowSpec")
    flow_get.add_argument("--app-key", required=True)
    flow_get.add_argument("--version-id", default="")
    flow_get.set_defaults(handler=_handle_flow_get, format_hint="builder_summary")

    flow_apply_apply = flow_apply_subparsers.add_parser("apply", help="执行流程变更")
    flow_apply_apply.add_argument("--app-key", required=True)
    flow_apply_apply.add_argument("--mode", choices=["replace"], default="replace", help=argparse.SUPPRESS)
    flow_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
    flow_apply_apply.add_argument("--nodes-file", help=argparse.SUPPRESS)
    flow_apply_apply.add_argument("--transitions-file", help=argparse.SUPPRESS)
    flow_apply_apply.add_argument("--spec-file")
    flow_apply_apply.add_argument("--patch-nodes-file", help="节点局部更新 JSON 数组，每项含 id + set/unset")
    flow_apply_apply.add_argument("--idempotency-key", default="")
    flow_apply_apply.add_argument("--schema-version", default="")
    flow_apply_apply.set_defaults(handler=_handle_flow_apply, format_hint="builder_summary", force_json_output=True)

    charts_apply = builder_subparsers.add_parser("charts", help="报表")
    charts_apply_subparsers = charts_apply.add_subparsers(dest="builder_charts_command", required=True)
    charts_apply_apply = charts_apply_subparsers.add_parser("apply", help="执行报表变更")
    charts_apply_apply.add_argument("--app-key", default="")
    charts_apply_apply.add_argument("--apps-file", help="多应用报表 JSON 数组；每项包含 app_key 和 upsert/patch/remove/reorder payload")
    charts_apply_apply.add_argument("--upsert-file")
    charts_apply_apply.add_argument("--patch-file")
    charts_apply_apply.add_argument("--remove-chart-ids-file")
    charts_apply_apply.add_argument("--reorder-chart-ids-file")
    charts_apply_apply.set_defaults(handler=_handle_charts_apply, format_hint="builder_summary", force_json_output=True)

    publish_verify = builder_subparsers.add_parser("publish", help="发布校验")
    publish_verify_subparsers = publish_verify.add_subparsers(dest="builder_publish_command", required=True)
    publish_verify_verify = publish_verify_subparsers.add_parser("verify", help="校验应用发布")
    publish_verify_verify.add_argument("--app-key", default="")
    publish_verify_verify.add_argument("--app-keys", default="", help="逗号分隔的多应用 app_key；用于批量发布校验")
    publish_verify_verify.add_argument("--app-keys-file", help="JSON 字符串数组；用于批量发布校验")
    publish_verify_verify.add_argument("--expected-package-id", type=int)
    publish_verify_verify.set_defaults(handler=_handle_publish_verify, format_hint="builder_summary", force_json_output=True)

    view = builder_subparsers.add_parser("view", help="视图详情")
    view_subparsers = view.add_subparsers(dest="builder_view_command", required=True)
    view_get = view_subparsers.add_parser("get", help="读取视图详情")
    view_get.add_argument("--view-key", required=True)
    view_get.set_defaults(handler=_handle_view_get, format_hint="builder_summary")

    chart = builder_subparsers.add_parser("chart", help="报表详情")
    chart_subparsers = chart.add_subparsers(dest="builder_chart_command", required=True)
    chart_get = chart_subparsers.add_parser("get", help="读取报表配置详情")
    chart_get.add_argument("--chart-id", required=True)
    chart_get.set_defaults(handler=_handle_chart_get, format_hint="builder_summary")


def _handle_file_upload_local(args: argparse.Namespace, context: CliContext) -> dict:
    return context.files.file_upload_local(
        profile=args.profile,
        upload_kind=args.upload_kind,
        file_path=args.file_path,
        upload_mark=args.upload_mark,
        content_type=args.content_type,
        bucket_type=args.bucket_type,
        path_id=args.path_id,
        file_related_url=args.file_related_url,
    )


def _handle_feedback_submit(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder_feedback.feedback_submit(
        category=args.category,
        title=args.title,
        description=args.description,
        expected_behavior=args.expected_behavior,
        actual_behavior=args.actual_behavior,
        impact_scope=args.impact_scope,
        tool_name=args.tool_name,
        app_key=args.app_key,
        record_id=args.record_id,
        workflow_node_id=args.workflow_node_id,
        note=args.note,
    )


def _handle_contract(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.builder_tool_contract(tool_name=args.tool_name)


def _handle_icon_catalog(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.workspace_icon_catalog_get(profile=args.profile)


def _handle_member_search(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.member_search(
        profile=args.profile,
        query=args.query,
        page_num=args.page_num,
        page_size=args.page_size,
        contain_disable=bool(args.contain_disable),
    )


def _handle_role_search(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.role_search(
        profile=args.profile,
        keyword=args.keyword,
        page_num=args.page_num,
        page_size=args.page_size,
    )


def _handle_role_create(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.role_create(
        profile=args.profile,
        role_name=args.role_name,
        member_uids=load_list_arg(args.member_uids_file, option_name="--member-uids-file"),
        member_emails=load_list_arg(args.member_emails_file, option_name="--member-emails-file"),
        member_names=load_list_arg(args.member_names_file, option_name="--member-names-file"),
        role_icon=args.role_icon,
    )


def _handle_solution_install(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.solution_install(
        profile=args.profile,
        solution_key=args.solution_key,
        being_copy_data=bool(args.being_copy_data),
        solution_source=args.solution_source,
    )


def _handle_package_get(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.package_get(profile=args.profile, package_id=args.package_id)


def _handle_package_list(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.package_list(profile=args.profile, trial_status=args.trial_status, query=args.query)


def _handle_package_apply(args: argparse.Namespace, context: CliContext) -> dict:
    config = load_object_arg(args.config_file, option_name="--config-file")
    if not isinstance(config, dict):
        raise_config_error("package apply --config-file must contain a JSON object.")
    config.pop("create_if_missing", None)
    return context.builder.package_apply(profile=args.profile, **config)


def _handle_app_resolve(args: argparse.Namespace, context: CliContext) -> dict:
    has_app_key = bool((args.app_key or "").strip())
    has_app_name = bool((args.app_name or "").strip())
    has_package_id = args.package_id is not None
    if has_app_key and (has_app_name or has_package_id):
        raise_config_error(
            "app resolve accepts exactly one selector mode.",
            fix_hint="Use only `--app-key`, or use `--app-name` together with `--package-id`.",
        )
    if not has_app_key and not (has_app_name and has_package_id):
        raise_config_error(
            "app resolve requires either --app-key, or --app-name together with --package-id.",
            fix_hint="For an existing known app, pass `--app-key`. For package-scoped lookup, pass both `--app-name` and `--package-id`.",
        )
    return context.builder.app_resolve(
        profile=args.profile,
        app_key=args.app_key,
        app_name=args.app_name,
        package_id=args.package_id,
    )


def _handle_app_release_edit_lock_if_mine(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.app_release_edit_lock_if_mine(
        profile=args.profile,
        app_key=args.app_key,
        lock_owner_email=args.lock_owner_email,
        lock_owner_name=args.lock_owner_name,
    )


def _handle_button_list(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.app_custom_button_list(profile=args.profile, app_key=args.app_key)


def _handle_button_catalog(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.button_style_catalog_get(profile=args.profile)


def _handle_button_apply(args: argparse.Namespace, context: CliContext) -> dict:
    apps = load_list_arg(args.apps_file, option_name="--apps-file") if getattr(args, "apps_file", None) else None
    if apps is None and not (args.app_key or "").strip():
        raise_config_error("builder button apply requires --app-key unless --apps-file is provided")
    return context.builder.app_custom_buttons_apply(
        profile=args.profile,
        app_key=args.app_key,
        upsert_buttons=load_list_arg(args.upsert_buttons_file, option_name="--upsert-buttons-file"),
        patch_buttons=load_list_arg(args.patch_buttons_file, option_name="--patch-buttons-file"),
        remove_buttons=load_list_arg(args.remove_buttons_file, option_name="--remove-buttons-file"),
        view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
        apps=apps,
    )


def _handle_associated_resource_apply(args: argparse.Namespace, context: CliContext) -> dict:
    apps = load_list_arg(args.apps_file, option_name="--apps-file") if getattr(args, "apps_file", None) else None
    if apps is None and not (args.app_key or "").strip():
        raise_config_error("builder associated-resource apply requires --app-key unless --apps-file is provided")
    return context.builder.app_associated_resources_apply(
        profile=args.profile,
        app_key=args.app_key,
        upsert_resources=load_list_arg(args.upsert_resources_file, option_name="--upsert-resources-file"),
        patch_resources=load_list_arg(args.patch_resources_file, option_name="--patch-resources-file"),
        remove_associated_item_ids=load_list_arg(args.remove_associated_item_ids_file, option_name="--remove-associated-item-ids-file"),
        reorder_associated_item_ids=load_list_arg(args.reorder_associated_item_ids_file, option_name="--reorder-associated-item-ids-file"),
        view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
        apps=apps,
    )


def _handle_button_get(args: argparse.Namespace, context: CliContext) -> dict:
    app_keys = _app_keys_from_args(args)
    if app_keys is not None:
        return context.builder.app_get_buttons(profile=args.profile, app_key="", app_keys=app_keys)
    if not (args.app_key or "").strip():
        raise_config_error("builder button get requires --app-key unless --app-keys or --app-keys-file is provided")
    return context.builder.app_get_buttons(profile=args.profile, app_key=args.app_key)


def _handle_associated_resource_get(args: argparse.Namespace, context: CliContext) -> dict:
    app_keys = _app_keys_from_args(args)
    if app_keys is not None:
        return context.builder.app_get_associated_resources(profile=args.profile, app_key="", app_keys=app_keys)
    if not (args.app_key or "").strip():
        raise_config_error("builder associated-resource get requires --app-key unless --app-keys or --app-keys-file is provided")
    return context.builder.app_get_associated_resources(profile=args.profile, app_key=args.app_key)


def _handle_button_create(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.app_custom_button_create(
        profile=args.profile,
        app_key=args.app_key,
        payload=load_object_arg(args.payload_file, option_name="--payload-file"),
    )


def _handle_button_update(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.app_custom_button_update(
        profile=args.profile,
        app_key=args.app_key,
        button_id=args.button_id,
        payload=load_object_arg(args.payload_file, option_name="--payload-file"),
    )


def _handle_button_delete(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.app_custom_button_delete(profile=args.profile, app_key=args.app_key, button_id=args.button_id)


def _app_keys_from_args(args: argparse.Namespace) -> list[str] | None:
    keys: list[str] = []
    raw = str(getattr(args, "app_keys", "") or "").strip()
    if raw:
        keys.extend(part.strip() for part in raw.split(",") if part.strip())
    keys_file = getattr(args, "app_keys_file", None)
    if keys_file:
        keys.extend(str(item).strip() for item in require_list_arg(keys_file, option_name="--app-keys-file") if str(item).strip())
    return keys or None


def _handle_app_get(args: argparse.Namespace, context: CliContext) -> dict:
    handlers = {
        "summary": context.builder.app_get,
        "fields": context.builder.app_get_fields,
        "layout": context.builder.app_get_layout,
        "views": context.builder.app_get_views,
        "flow": context.builder.app_get_flow,
        "charts": context.builder.app_get_charts,
        "buttons": context.builder.app_get_buttons,
        "associated-resources": context.builder.app_get_associated_resources,
    }
    app_keys = _app_keys_from_args(args)
    if app_keys is not None:
        return handlers[args.builder_app_get_section](profile=args.profile, app_key="", app_keys=app_keys)
    if not (args.app_key or "").strip():
        raise_config_error("builder app get requires --app-key unless --app-keys or --app-keys-file is provided")
    return handlers[args.builder_app_get_section](profile=args.profile, app_key=args.app_key)


def _handle_app_repair_code_blocks(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.app_repair_code_blocks(
        profile=args.profile,
        app_key=args.app_key,
        field=args.field,
        apply=bool(args.apply),
    )


def _handle_portal_list(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.portal_list(profile=args.profile)


def _handle_portal_get(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.portal_get(profile=args.profile, dash_key=args.dash_key, being_draft=bool(args.being_draft))


def _handle_view_get(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.view_get(profile=args.profile, view_key=args.view_key)


def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.chart_get(profile=args.profile, chart_id=args.chart_id)


def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
    apps_payload = _load_apps_file_arg(args.apps_file)
    apps = apps_payload["apps"]
    apps_warnings = apps_payload.get("warnings") or []
    package_id = args.package_id
    if package_id is None and apps_payload.get("package_id") is not None:
        package_id = _coerce_apps_file_package_id(apps_payload["package_id"])
    if args.apps_file:
        file_publish = _coerce_apps_file_bool(
            apps_payload.get("publish"),
            field_name="publish",
            default=True,
        )
        effective_publish = bool(args.publish if args.publish is not None else file_publish)
        if not apps:
            raise_config_error(
                "schema apply multi-app mode requires a non-empty --apps-file.",
                fix_hint="Pass a JSON array, or a JSON object like {\"package_id\":1001,\"apps\":[...]} with at least one app item.",
                error_code="APPS_FILE_EMPTY",
            )
        if args.app_key or args.app_name or args.app_title or args.form_file or args.add_fields_file or args.update_fields_file or args.remove_fields_file:
            raise_config_error(
                "schema apply multi-app mode accepts --package-id plus --apps-file only.",
                fix_hint="Use apps[].form inside --apps-file for batch mode, or remove --apps-file and use --form-file for a single app.",
            )
        if package_id is None:
            raise_config_error(
                "schema apply multi-app mode requires --package-id.",
                fix_hint="Pass `--package-id`, or put `package_id` at the top level of --apps-file. `package_name` alone does not create the package here; run `builder package apply` first.",
            )
        result = context.builder.app_schema_apply(
            profile=args.profile,
            package_id=package_id,
            visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
            publish=effective_publish,
            apps=apps,
            form=None,
            add_fields=[],
            update_fields=[],
            remove_fields=[],
        )
        if apps_warnings and isinstance(result, dict):
            result_warnings = list(result.get("warnings") or [])
            result_warnings.extend(deepcopy(apps_warnings))
            result["warnings"] = result_warnings
        return result
    has_app_key = bool((args.app_key or "").strip())
    has_app_name = bool((args.app_name or "").strip())
    has_app_title = bool((args.app_title or "").strip())
    has_package_id = args.package_id is not None
    if has_app_key:
        if has_package_id:
            raise_config_error(
                "schema apply edit mode accepts --app-key and optional --app-name only.",
                fix_hint="For existing apps, use `--app-key` and optionally `--app-name`. For create mode, use `--package-id --app-name`.",
            )
    else:
        if not has_package_id or not (has_app_name or has_app_title):
            raise_config_error(
                "schema apply create mode requires --package-id and --app-name.",
                fix_hint="Use `--app-key` for existing apps, or pass `--package-id --app-name` to create a new app.",
            )
    return context.builder.app_schema_apply(
        profile=args.profile,
        app_key=args.app_key,
        package_id=args.package_id,
        app_name=args.app_name,
        app_title=args.app_title,
        icon=args.icon,
        color=args.color,
        visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
        publish=True if args.publish is None else bool(args.publish),
        form=load_list_arg(args.form_file, option_name="--form-file") if args.form_file else [],
        add_fields=load_list_arg(args.add_fields_file, option_name="--add-fields-file"),
        update_fields=load_list_arg(args.update_fields_file, option_name="--update-fields-file"),
        remove_fields=load_list_arg(args.remove_fields_file, option_name="--remove-fields-file"),
    )


def _load_apps_file_arg(path: str | None) -> dict[str, object]:
    if not path:
        return {"apps": []}
    expected_shape = {
        "package_id": 1001,
        "apps": [
            {
                "client_key": "employee",
                "app_name": "员工花名册",
                "icon": "business-personalcard",
                "color": "emerald",
                "form": [
                    {
                        "section": "基础信息",
                        "rows": [[{"name": "员工名称", "type": "text", "data_title": True}]],
                    }
                ],
            }
        ],
    }
    expected_shape_details = {
        "expected_shape": expected_shape,
        "expected_shape_json": (
            '{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
            '"icon":"business-personalcard","color":"emerald","form":[{"section":"基础信息",'
            '"rows":[[{"name":"员工名称","type":"text","data_title":true}]]}]}]}'
        ),
    }
    payload = load_json_value(path, option_name="--apps-file")
    if isinstance(payload, list):
        if len(payload) == 1 and isinstance(payload[0], dict) and "apps" in payload[0]:
            wrapper = payload[0]
            apps = wrapper.get("apps")
            if not isinstance(apps, list):
                raise_config_error(
                    "--apps-file wrapper object requires an apps array.",
                    fix_hint='Use {"package_id":1001,"apps":[...]} with apps[].form, or pass a raw apps array with form sections.',
                    error_code="APPS_FILE_SHAPE_INVALID",
                    details=expected_shape_details,
                )
            result: dict[str, object] = {
                "apps": apps,
                "warnings": [
                    {
                        "code": "APPS_FILE_WRAPPER_ARRAY_UNWRAPPED",
                        "message": "--apps-file was a singleton wrapper array; normalized it to {package_id, apps}.",
                    }
                ],
            }
            if wrapper.get("package_id") is not None:
                result["package_id"] = wrapper.get("package_id")
            if wrapper.get("publish") is not None:
                result["publish"] = wrapper.get("publish")
            return result
        if any(isinstance(item, dict) and "apps" in item for item in payload):
            raise_config_error(
                "--apps-file root array items must be app items, not package wrapper objects.",
                fix_hint='Use one object {"package_id":1001,"apps":[...]} with apps[].form, or a raw app array with form sections. Do not wrap multiple {package_id, apps} objects in an array.',
                error_code="APPS_FILE_SHAPE_INVALID",
                details=expected_shape_details,
            )
        return {"apps": payload}
    if isinstance(payload, dict):
        apps = payload.get("apps")
        if not isinstance(apps, list):
            raise_config_error(
                "--apps-file JSON object requires an apps array.",
                fix_hint="Use {\"package_id\":1001,\"apps\":[...]} or pass a raw JSON array.",
                error_code="APPS_FILE_SHAPE_INVALID",
                details=expected_shape_details,
            )
        result: dict[str, object] = {"apps": apps}
        if payload.get("package_id") is not None:
            result["package_id"] = payload.get("package_id")
        if payload.get("publish") is not None:
            result["publish"] = payload.get("publish")
        return result
    raise_config_error(
        "--apps-file must be a JSON array or an object containing apps.",
        fix_hint="Use [{...}] or {\"package_id\":1001,\"apps\":[...]}",
        error_code="APPS_FILE_SHAPE_INVALID",
        details=expected_shape_details,
    )


def _coerce_apps_file_package_id(value: object) -> int:
    package_id: int
    if isinstance(value, bool):
        _raise_apps_file_package_id_invalid(value)
    if isinstance(value, int):
        package_id = value
    elif isinstance(value, str):
        stripped = value.strip()
        try:
            package_id = int(stripped)
        except ValueError:
            _raise_apps_file_package_id_invalid(value)
    else:
        _raise_apps_file_package_id_invalid(value)
    if package_id <= 0:
        _raise_apps_file_package_id_invalid(value)
    return package_id


def _raise_apps_file_package_id_invalid(value: object) -> None:
    raise_config_error(
        "--apps-file package_id must be a positive integer.",
        fix_hint='Use a numeric package_id, for example {"package_id":1001,"apps":[...]}.',
        error_code="APPS_FILE_PACKAGE_ID_INVALID",
        details={
            "field": "package_id",
            "value": value,
            "expected": "positive integer or numeric string",
        },
    )


def _coerce_apps_file_bool(value: object, *, field_name: str, default: bool) -> bool:
    if value is None:
        return default
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        try:
            return parse_bool_text(value)
        except argparse.ArgumentTypeError:
            pass
    raise_config_error(
        f"--apps-file {field_name} must be a boolean.",
        fix_hint=f'Use "{field_name}": true or "{field_name}": false in --apps-file.',
        error_code="APPS_FILE_BOOLEAN_INVALID",
        details={
            "field": field_name,
            "value": value,
            "expected": "boolean true/false or string true/false/1/0/yes/no",
        },
    )


def _load_views_file_arg(path: str | None) -> list[object] | None:
    if not path:
        return None
    payload = load_json_value(path, option_name="--views-file")
    if isinstance(payload, dict):
        views = payload.get("views")
        if not isinstance(views, list):
            raise_config_error(
                "--views-file JSON object must contain a views array.",
                fix_hint='Use {"views": [{"operation": "upsert", "app_key": "...", "name": "...", "type": "table", "columns": [...]}]}.',
                error_code="VIEWS_FILE_SHAPE_INVALID",
                details={"expected": '{"views": [...]}', "actual_type": type(views).__name__},
            )
        return views
    if isinstance(payload, list):
        return payload
    raise_config_error(
        "--views-file must be a JSON object with views[] or a JSON array.",
        fix_hint='Prefer {"views": [...]} for new automation. A bare array is accepted only for compatibility.',
        error_code="VIEWS_FILE_SHAPE_INVALID",
        details={"actual_type": type(payload).__name__},
    )


def _handle_layout_apply(args: argparse.Namespace, context: CliContext) -> dict:
    apps = load_list_arg(args.apps_file, option_name="--apps-file") if getattr(args, "apps_file", None) else None
    if apps is None and not (args.app_key or "").strip():
        raise_config_error("builder layout apply requires --app-key unless --apps-file is provided")
    if apps is None and not getattr(args, "sections_file", None):
        raise_config_error(
            "--sections-file is required",
            fix_hint="Pass --sections-file for single-app layout apply, or use --apps-file for batch layout apply.",
            error_code="ARGUMENT_ERROR",
            details={
                "prog": "qingflow builder layout apply",
                "usage": "qingflow builder layout apply --app-key APP_KEY --sections-file SECTIONS.json",
            },
        )
    return context.builder.app_layout_apply(
        profile=args.profile,
        app_key=args.app_key,
        mode=args.mode,
        publish=bool(args.publish),
        sections=[] if apps is not None else require_list_arg(args.sections_file, option_name="--sections-file"),
        apps=apps,
    )


def _handle_views_apply(args: argparse.Namespace, context: CliContext) -> dict:
    views = _load_views_file_arg(args.views_file) if getattr(args, "views_file", None) else None
    apps = load_list_arg(args.apps_file, option_name="--apps-file") if getattr(args, "apps_file", None) else None
    if views is not None:
        if apps is not None or (args.app_key or "").strip() or getattr(args, "upsert_views_file", None) or getattr(args, "patch_views_file", None) or getattr(args, "remove_views_file", None):
            raise_config_error(
                "builder views apply --views-file cannot be mixed with --app-key, --apps-file, --upsert-views-file, --patch-views-file, or --remove-views-file.",
                fix_hint="Use one shape: preferred --views-file for flat batch jobs, or legacy --app-key/--apps-file for grouped payloads.",
            )
        return context.builder.app_views_apply(
            profile=args.profile,
            app_key="",
            publish=bool(args.publish),
            upsert_views=[],
            patch_views=[],
            remove_views=[],
            views=views,
        )
    if apps is None and not (args.app_key or "").strip():
        raise_config_error("builder views apply requires --views-file, --apps-file, or --app-key")
    return context.builder.app_views_apply(
        profile=args.profile,
        app_key=args.app_key,
        publish=bool(args.publish),
        upsert_views=load_list_arg(args.upsert_views_file, option_name="--upsert-views-file"),
        patch_views=load_list_arg(args.patch_views_file, option_name="--patch-views-file"),
        remove_views=load_list_arg(args.remove_views_file, option_name="--remove-views-file"),
        apps=apps,
    )


def _handle_flow_apply(args: argparse.Namespace, context: CliContext) -> dict:
    patch_nodes = load_list_arg(getattr(args, "patch_nodes_file", None), option_name="--patch-nodes-file")
    spec = load_object_arg(getattr(args, "spec_file", None), option_name="--spec-file")
    has_legacy_nodes = bool(getattr(args, "nodes_file", None) or getattr(args, "transitions_file", None))
    if has_legacy_nodes:
        raise_config_error(
            "builder flow apply no longer exposes legacy --nodes-file/--transitions-file input.",
            fix_hint="Use --spec-file for a complete WorkflowSpec, or --patch-nodes-file for targeted edits after builder flow get.",
            error_code="FLOW_LEGACY_INPUT_DISABLED",
            details={"allowed_input_modes": ["--spec-file", "--patch-nodes-file"]},
        )
    selected_modes = sum(1 for selected in (bool(patch_nodes), bool(spec), has_legacy_nodes) if selected)
    if selected_modes != 1:
        raise_config_error(
            "builder flow apply requires exactly one input mode: --spec-file or --patch-nodes-file.",
            fix_hint="Use --spec-file for a complete WorkflowSpec, or --patch-nodes-file for targeted edits after builder flow get.",
        )
    if patch_nodes:
        return context.builder.app_flow_apply(
            profile=args.profile,
            app_key=args.app_key,
            publish=bool(args.publish),
            patch_nodes=patch_nodes,
            idempotency_key=args.idempotency_key,
            schema_version=args.schema_version,
        )
    if spec:
        return context.builder.app_flow_apply(
            profile=args.profile,
            app_key=args.app_key,
            publish=bool(args.publish),
            spec=spec,
            idempotency_key=args.idempotency_key,
            schema_version=args.schema_version,
        )
    raise_config_error(
        "builder flow apply requires --spec-file or --patch-nodes-file.",
        fix_hint="Use --spec-file for a complete WorkflowSpec, or --patch-nodes-file for targeted edits after builder flow get.",
    )


def _handle_flow_schema(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.app_flow_get_schema(profile=args.profile, schema_version=args.schema_version)


def _handle_flow_get(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.app_flow_get(profile=args.profile, app_key=args.app_key, version_id=args.version_id)


def _handle_charts_apply(args: argparse.Namespace, context: CliContext) -> dict:
    apps = load_list_arg(args.apps_file, option_name="--apps-file") if getattr(args, "apps_file", None) else None
    if apps is None and not (args.app_key or "").strip():
        raise_config_error("builder charts apply requires --app-key unless --apps-file is provided")
    return context.builder.app_charts_apply(
        profile=args.profile,
        app_key=args.app_key,
        upsert_charts=load_list_arg(args.upsert_file, option_name="--upsert-file"),
        patch_charts=load_list_arg(args.patch_file, option_name="--patch-file"),
        remove_chart_ids=load_list_arg(args.remove_chart_ids_file, option_name="--remove-chart-ids-file"),
        reorder_chart_ids=load_list_arg(args.reorder_chart_ids_file, option_name="--reorder-chart-ids-file"),
        apps=apps,
    )


def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
    payload = load_object_arg(args.payload_file, option_name="--payload-file") if getattr(args, "payload_file", None) else None
    payload_obj = payload if isinstance(payload, dict) else {}
    effective_dash_name = (args.dash_name or str(payload_obj.get("dash_name") or payload_obj.get("dashName") or payload_obj.get("name") or "")).strip()
    effective_package_id = args.package_id if args.package_id is not None else payload_obj.get("package_id") or payload_obj.get("packageId") or payload_obj.get("package_tag_id")
    has_dash_key = bool((args.dash_key or "").strip())
    has_dash_name = bool(effective_dash_name)
    has_package_id = effective_package_id is not None
    if has_dash_key and has_package_id:
        raise_config_error(
            "portal apply accepts exactly one selector mode.",
            fix_hint="Use `--dash-key` to update an existing portal, or use `--package-id --dash-name` to create a new portal.",
        )
    if not has_dash_key and not (has_package_id and has_dash_name):
        raise_config_error(
            "portal apply requires either --dash-key, or --package-id together with --dash-name.",
            fix_hint="Use `--dash-key` for an existing portal. For create mode, pass `--package-id --dash-name`.",
        )
    sections = [] if not args.sections_file else require_list_arg(args.sections_file, option_name="--sections-file")
    patch_sections = (
        load_list_arg(args.patch_sections_file, option_name="--patch-sections-file")
        if args.patch_sections_file
        else None
    )
    return context.builder.portal_apply(
        profile=args.profile,
        dash_key=args.dash_key,
        dash_name=args.dash_name,
        package_id=args.package_id,
        publish=bool(args.publish),
        sections=sections,
        layout_preset=args.layout_preset,
        visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
        auth=load_object_arg(args.auth_file, option_name="--auth-file"),
        icon=args.icon,
        color=args.color,
        hide_copyright=args.hide_copyright,
        dash_global_config=load_object_arg(args.dash_global_config_file, option_name="--dash-global-config-file"),
        config=load_object_arg(args.config_file, option_name="--config-file"),
        payload=payload,
        patch_sections=patch_sections,
    )


def _handle_portal_delete(args: argparse.Namespace, context: CliContext) -> dict:
    return context.builder.portal_delete(
        profile=args.profile,
        dash_key=args.dash_key,
    )


def _handle_publish_verify(args: argparse.Namespace, context: CliContext) -> dict:
    app_keys = _app_keys_from_args(args)
    if app_keys is not None:
        return context.builder.app_publish_verify(
            profile=args.profile,
            app_key="",
            app_keys=app_keys,
            expected_package_id=args.expected_package_id,
        )
    if not (args.app_key or "").strip():
        raise_config_error("builder publish verify requires --app-key unless --app-keys or --app-keys-file is provided")
    return context.builder.app_publish_verify(
        profile=args.profile,
        app_key=args.app_key,
        expected_package_id=args.expected_package_id,
    )
