from __future__ import annotations

import json

from mcp.server.fastmcp import FastMCP

from .backend_client import BackendClient
from .config import DEFAULT_PROFILE
from .response_trim import BUILDER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
from .session_store import SessionStore
from .tools.ai_builder_tools import AiBuilderTools
from .tools.auth_tools import AuthTools
from .tools.feedback_tools import FeedbackTools
from .tools.file_tools import FileTools
from .tools.workspace_tools import WorkspaceTools


def _config_failure(message: str, *, fix_hint: str) -> dict:
    return {
        "status": "failed",
        "error_code": "CONFIG_ERROR",
        "recoverable": True,
        "message": message,
        "details": {"fix_hint": fix_hint},
        "verification": {},
    }


def build_builder_server() -> FastMCP:
    server = FastMCP(
        "Qingflow App Builder MCP",
        instructions=(
            "Use this server for AI-native Qingflow builder workflows. "
            "`feedback_submit` is always available as a cross-cutting helper when the current capability is unsupported, awkward, or still cannot satisfy the user's need after reasonable use; it does not require Qingflow login or workspace selection, and it should be called only after explicit user confirmation. "
            "Follow the resource path resolve -> summary read -> apply -> publish_verify. "
            "Use builder_tool_contract when you need a machine-readable contract, aliases, allowed enums, or a minimal valid example for a public builder tool. "
            "Use solution_install when the user explicitly wants to install a packaged solution/template by solution_key, optionally copying bundled demo data. "
            "Use package_list to find visible app packages by keyword and package_get to read package detail before editing; if creating or updating an app package may be appropriate, use package_apply with explicit user intent; otherwise use app_resolve to locate app resources, "
            "Use workspace_icon_catalog_get before creating app packages, apps, or portals when supported icon/color candidates are needed; new workspace resources require explicit non-template icon + color, and the CLI validates choices without inferring business defaults. "
            "app_get as the default app map read, then app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for focused configuration reads, "
            "member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
            "then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_associated_resources_apply/app_charts_apply/portal_apply/portal_delete to execute normalized patches; app_custom_buttons_apply is maintenance-only for explicit standalone button work outside normal view design. These apply/delete tools perform basic validation, normalization, writes, and readback verification where applicable. Schema/layout/views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, portal delete separates DELETE execution from readback verification, portal pc layout is a 24-column grid and mobile is a 6-column grid so omit position or use layout_preset when unsure, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
            "Builder apply/write outputs include schema_version, operation, summary, and resources[]; use resources[].id/key/name/ids/parent as the stable UI and agent display entry, and keep legacy fields such as field_diff/views_diff/chart_results only for compatibility or troubleshooting. "
            "For existing object parameter replacement, prefer patch_views, patch_buttons, patch_resources, and patch_charts with set/unset; the tool reads current config and full-saves internally, while upsert_* is for creation or full target configuration and should not be used as an incomplete partial update. "
            "For builder delete/remove apply results, separate delete execution from readback verification: after DELETE is sent, resources expose delete_executed, readback_status, and safe_to_retry_delete=false. If readback_status is unavailable or still_exists, do not blindly repeat the delete; confirm later with app_get/view_get/chart_get or the relevant apply readback. Views/buttons use single-item readback; associated resources use one app-level resource-pool readback because there is no confirmed single-item GET. "
            "For builder write/apply results, separate write execution from final readback verification: status=partial_success with write_executed=true and safe_to_retry=false means the write was sent or succeeded, while final readback, publish verification, or metadata confirmation is pending or permission-restricted. Report it as written but unverified; do not repeat the same write blindly. "
            "For app_schema_apply, configure data title and data cover directly in field JSON with as_data_title=true and as_data_cover=true; data title is required and exactly one field may be marked, while data cover is optional and must be a top-level attachment field. For multi-app creation, pass apps[]/--apps-file on app_schema_apply; each item may have client_key, and relation fields may use target_app_ref to point at another same-call client_key. "
            "For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows is a matrix of field names compiled to backend queryCondition queIds. New views default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless associated_resources is explicitly patched. View writes use ViewManagementAuth (beingViewManageStatus), falling back to DataManageAuth when advanced app permissions are not enabled. "
            "For ordinary business buttons, declare them on the target view with app_views_apply.views[].action_buttons; app_views_apply writes the view first, then creates/binds the button. Use app_custom_buttons_apply only when the user explicitly asks to maintain existing standalone buttons outside view design, such as custom style/icon changes, cross-view reuse, deleting button bodies, bulk binding reorder, or exact qRobot/wings config. Button body writes require EditAppAuth; view_configs placement writes require ViewManagementAuth, matching the backend viewConfig route. For addData buttons, prefer action_buttons.target_app_key + field_mappings/default_values on the view path; do not ask agents to write raw que_relation unless maintaining a legacy config. field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id means current record id (-17), 编号/record_number means visible record number (0). To fill a target relation with the current record, map {'source_field': '数据ID', 'target_field': '目标引用字段'}; default_values is only for static constants. View button bindings merge by default; standalone view_configs are only for explicit maintenance. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
            "For BI reports, keep report-body development separate from Qingflow in-app display: use app_charts_apply to create, update, remove, or reorder app-source QingBI chart bodies/configs with dataSourceType=qingflow; chart dimension/metric/filter/query fields must come from app_get_fields.chart_fields, not record schema or form-only fields; dataset BI reports are not created or edited by app_charts_apply yet and should be created in QingBI first, then attached with app_associated_resources_apply using report_source=dataset. "
            "For associated views/reports, use app_associated_resources_apply. Use match_mappings for filtering associated resources: dynamic current-record conditions use source_field, static conditions use value. match_mappings also supports 数据ID(-17) and 编号(0). Do not ask agents to write raw match_rules unless preserving a legacy backend config. "
            "For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id, and view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key because the tool resolves those to the internal id. Before creating an associated resource, read app_get.associated_resources and reuse an existing matching target_app_key + view_key/chart_key through patch_resources; client_key only works inside one apply call and is not persisted. Do not ask agents to pass backend raw sourceType: views infer the internal Qingflow view source, reports default to BI app reports, and dataset reports use report_source=dataset. "
            "For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
            "Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
            "For workflow edits, use app_flow_get_schema/app_flow_get when raw WorkflowSpec shape is needed, then app_flow_apply with spec for full replacement or patch_nodes for targeted node edits. Declare node assignees and editable fields explicitly. "
            "If builder writes are blocked by the current user's own edit lock, use app_release_edit_lock_if_mine with the lock owner details from the failed result. "
            "Do not handcraft internal solution payloads or rely on build_id/stage/repair. "
            "If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, first summarize the gap, ask whether to submit feedback, and call feedback_submit only after explicit user confirmation."
        ),
    )
    sessions = SessionStore()
    backend = BackendClient()
    auth = wrap_trimmed_methods(AuthTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
    workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
    files = wrap_trimmed_methods(FileTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
    ai_builder = wrap_trimmed_methods(AiBuilderTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
    feedback = FeedbackTools(backend, mcp_side="App Builder MCP")

    @server.tool()
    def auth_use_credential(
        profile: str = DEFAULT_PROFILE,
        base_url: str | None = None,
        qf_version: str | None = None,
        credential: str = "",
        persist: bool = False,
    ) -> dict:
        return auth.auth_use_credential(
            profile=profile,
            base_url=base_url,
            qf_version=qf_version,
            credential=credential,
            persist=persist,
        )

    @server.tool()
    def auth_whoami(profile: str = DEFAULT_PROFILE) -> dict:
        return auth.auth_whoami(profile=profile)

    @server.tool()
    def auth_logout(profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict:
        return auth.auth_logout(profile=profile, forget_persisted=forget_persisted)

    @server.tool()
    def workspace_list(
        profile: str = DEFAULT_PROFILE,
        page_num: int = 1,
        page_size: int = 20,
        include_external: bool = False,
    ) -> dict:
        return workspace.workspace_list(
            profile=profile,
            page_num=page_num,
            page_size=page_size,
            include_external=include_external,
        )

    @server.tool()
    def workspace_get(
        profile: str = DEFAULT_PROFILE,
        ws_id: int | None = None,
    ) -> dict:
        return workspace.workspace_get(
            profile=profile,
            ws_id=ws_id,
        )

    @server.tool()
    def file_upload_local(
        profile: str = DEFAULT_PROFILE,
        upload_kind: str = "attachment",
        file_path: str = "",
        upload_mark: str | None = None,
        content_type: str | None = None,
        bucket_type: str | None = None,
        path_id: int | None = None,
        file_related_url: str | None = None,
    ) -> dict:
        return files.file_upload_local(
            profile=profile,
            upload_kind=upload_kind,
            file_path=file_path,
            upload_mark=upload_mark,
            content_type=content_type,
            bucket_type=bucket_type,
            path_id=path_id,
            file_related_url=file_related_url,
        )

    @server.tool()
    def feedback_submit(
        category: str = "",
        title: str = "",
        description: str = "",
        expected_behavior: str | None = None,
        actual_behavior: str | None = None,
        impact_scope: str | None = None,
        tool_name: str | None = None,
        app_key: str | None = None,
        record_id: str | int | None = None,
        workflow_node_id: str | int | None = None,
        note: str | None = None,
    ) -> dict:
        try:
            return trim_public_response(
                "builder:feedback_submit",
                feedback.feedback_submit(
                    category=category,
                    title=title,
                    description=description,
                    expected_behavior=expected_behavior,
                    actual_behavior=actual_behavior,
                    impact_scope=impact_scope,
                    tool_name=tool_name,
                    app_key=app_key,
                    record_id=record_id,
                    workflow_node_id=workflow_node_id,
                    note=note,
                ),
            )
        except RuntimeError as exc:
            try:
                payload = json.loads(str(exc))
            except json.JSONDecodeError:
                raise
            if isinstance(payload, dict):
                raise RuntimeError(json.dumps(trim_error_response(payload), ensure_ascii=False)) from None
            raise

    @server.tool()
    def builder_tool_contract(tool_name: str = "") -> dict:
        return ai_builder.builder_tool_contract(tool_name=tool_name)

    @server.tool()
    def workspace_icon_catalog_get(profile: str = DEFAULT_PROFILE) -> dict:
        return ai_builder.workspace_icon_catalog_get(profile=profile)

    @server.tool()
    def package_list(profile: str = DEFAULT_PROFILE, trial_status: str = "all", query: str = "") -> dict:
        return ai_builder.package_list(profile=profile, trial_status=trial_status, query=query)

    @server.tool()
    def package_get(profile: str = DEFAULT_PROFILE, package_id: int = 0) -> dict:
        return ai_builder.package_get(profile=profile, package_id=package_id)

    @server.tool()
    def package_apply(
        profile: str = DEFAULT_PROFILE,
        package_id: int | None = None,
        package_name: str | None = None,
        icon: str | None = None,
        color: str | None = None,
        visibility: dict | None = None,
        items: list[dict] | None = None,
        allow_detach: bool = False,
    ) -> dict:
        return ai_builder.package_apply(
            profile=profile,
            package_id=package_id,
            package_name=package_name,
            icon=icon,
            color=color,
            visibility=visibility,
            items=items,
            allow_detach=allow_detach,
        )

    @server.tool()
    def solution_install(
        profile: str = DEFAULT_PROFILE,
        solution_key: str = "",
        being_copy_data: bool = True,
        solution_source: str = "solutionDetail",
    ) -> dict:
        return ai_builder.solution_install(
            profile=profile,
            solution_key=solution_key,
            being_copy_data=being_copy_data,
            solution_source=solution_source,
        )

    @server.tool()
    def member_search(
        profile: str = DEFAULT_PROFILE,
        query: str = "",
        page_num: int = 1,
        page_size: int = 20,
        contain_disable: bool = False,
    ) -> dict:
        return ai_builder.member_search(
            profile=profile,
            query=query,
            page_num=page_num,
            page_size=page_size,
            contain_disable=contain_disable,
        )

    @server.tool()
    def role_search(
        profile: str = DEFAULT_PROFILE,
        keyword: str = "",
        page_num: int = 1,
        page_size: int = 20,
    ) -> dict:
        return ai_builder.role_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)

    @server.tool()
    def role_create(
        profile: str = DEFAULT_PROFILE,
        role_name: str = "",
        member_uids: list[int] | None = None,
        member_emails: list[str] | None = None,
        member_names: list[str] | None = None,
        role_icon: str = "ex-user-outlined",
    ) -> dict:
        return ai_builder.role_create(
            profile=profile,
            role_name=role_name,
            member_uids=member_uids or [],
            member_emails=member_emails or [],
            member_names=member_names or [],
            role_icon=role_icon,
        )

    @server.tool()
    def app_release_edit_lock_if_mine(
        profile: str = DEFAULT_PROFILE,
        app_key: str = "",
        lock_owner_email: str = "",
        lock_owner_name: str = "",
    ) -> dict:
        return ai_builder.app_release_edit_lock_if_mine(
            profile=profile,
            app_key=app_key,
            lock_owner_email=lock_owner_email,
            lock_owner_name=lock_owner_name,
        )

    @server.tool()
    def app_resolve(
        profile: str = DEFAULT_PROFILE,
        app_key: str = "",
        app_name: str = "",
        package_id: int | None = None,
    ) -> dict:
        has_app_key = bool((app_key or "").strip())
        has_app_name = bool((app_name or "").strip())
        has_package_id = package_id is not None
        if has_app_key and (has_app_name or has_package_id):
            return _config_failure(
                "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):
            return _config_failure(
                "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 ai_builder.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_id=package_id)

    @server.tool()
    def button_style_catalog_get(profile: str = DEFAULT_PROFILE) -> dict:
        return ai_builder.button_style_catalog_get(profile=profile)

    @server.tool()
    def app_custom_buttons_apply(
        profile: str = DEFAULT_PROFILE,
        app_key: str = "",
        upsert_buttons: list[dict] | None = None,
        patch_buttons: list[dict] | None = None,
        remove_buttons: list[dict] | None = None,
        view_configs: list[dict] | None = None,
        apps: list[dict] | None = None,
    ) -> dict:
        return ai_builder.app_custom_buttons_apply(
            profile=profile,
            app_key=app_key,
            upsert_buttons=upsert_buttons or [],
            patch_buttons=patch_buttons or [],
            remove_buttons=remove_buttons or [],
            view_configs=view_configs or [],
            apps=apps,
        )

    @server.tool()
    def app_associated_resources_apply(
        profile: str = DEFAULT_PROFILE,
        app_key: str = "",
        upsert_resources: list[dict] | None = None,
        patch_resources: list[dict] | None = None,
        remove_associated_item_ids: list[int] | None = None,
        reorder_associated_item_ids: list[int] | None = None,
        view_configs: list[dict] | None = None,
        apps: list[dict] | None = None,
    ) -> dict:
        return ai_builder.app_associated_resources_apply(
            profile=profile,
            app_key=app_key,
            upsert_resources=upsert_resources or [],
            patch_resources=patch_resources or [],
            remove_associated_item_ids=remove_associated_item_ids or [],
            reorder_associated_item_ids=reorder_associated_item_ids or [],
            view_configs=view_configs or [],
            apps=apps,
        )

    @server.tool()
    def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
        return ai_builder.app_get(profile=profile, app_key=app_key, app_keys=app_keys)

    @server.tool()
    def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
        return ai_builder.app_get_fields(profile=profile, app_key=app_key, app_keys=app_keys)

    @server.tool()
    def app_repair_code_blocks(
        profile: str = DEFAULT_PROFILE,
        app_key: str = "",
        field: str | None = None,
        apply: bool = False,
    ) -> dict:
        return ai_builder.app_repair_code_blocks(profile=profile, app_key=app_key, field=field, apply=apply)

    @server.tool()
    def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
        return ai_builder.app_get_layout(profile=profile, app_key=app_key, app_keys=app_keys)

    @server.tool()
    def app_get_views(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
        return ai_builder.app_get_views(profile=profile, app_key=app_key, app_keys=app_keys)

    @server.tool()
    def app_get_flow(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
        return ai_builder.app_get_flow(profile=profile, app_key=app_key, app_keys=app_keys)

    @server.tool()
    def app_get_charts(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
        return ai_builder.app_get_charts(profile=profile, app_key=app_key, app_keys=app_keys)

    @server.tool()
    def app_get_buttons(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
        return ai_builder.app_get_buttons(profile=profile, app_key=app_key, app_keys=app_keys)

    @server.tool()
    def app_get_associated_resources(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
        return ai_builder.app_get_associated_resources(profile=profile, app_key=app_key, app_keys=app_keys)

    @server.tool()
    def portal_list(profile: str = DEFAULT_PROFILE) -> dict:
        return ai_builder.portal_list(profile=profile)

    @server.tool()
    def portal_get(
        profile: str = DEFAULT_PROFILE,
        dash_key: str = "",
        being_draft: bool = True,
    ) -> dict:
        return ai_builder.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)

    @server.tool()
    def view_get(profile: str = DEFAULT_PROFILE, view_key: str = "") -> dict:
        return ai_builder.view_get(profile=profile, view_key=view_key)

    @server.tool()
    def chart_get(profile: str = DEFAULT_PROFILE, chart_id: str = "") -> dict:
        return ai_builder.chart_get(profile=profile, chart_id=chart_id)

    @server.tool()
    def app_schema_apply(
        profile: str = DEFAULT_PROFILE,
        app_key: str = "",
        package_id: int | None = None,
        app_name: str = "",
        app_title: str = "",
        icon: str = "",
        color: str = "",
        visibility: dict | None = None,
        publish: bool = True,
        form: list[dict] | None = None,
        add_fields: list[dict] | None = None,
        update_fields: list[dict] | None = None,
        remove_fields: list[dict] | None = None,
        apps: list[dict] | None = None,
    ) -> dict:
        if apps:
            if app_key or app_name or app_title or form or add_fields or update_fields or remove_fields:
                return _config_failure(
                    "app_schema_apply multi-app mode accepts package_id plus apps only.",
                    fix_hint="Use apps[].form for batch mode, or use the single-app arguments without apps.",
                )
            if package_id is None:
                return _config_failure(
                    "app_schema_apply multi-app mode requires package_id.",
                    fix_hint="Pass `package_id` and `apps[].app_name` for new apps, or `apps[].app_key` for existing apps.",
                )
            return ai_builder.app_schema_apply(
                profile=profile,
                package_id=package_id,
                visibility=visibility,
                publish=publish,
                form=None,
                add_fields=[],
                update_fields=[],
                remove_fields=[],
                apps=apps,
            )
        has_app_key = bool((app_key or "").strip())
        has_app_name = bool((app_name or "").strip())
        has_app_title = bool((app_title or "").strip())
        has_package_id = package_id is not None
        create_by_shape = has_package_id and (has_app_name or has_app_title)
        if has_app_key:
            if has_package_id:
                return _config_failure(
                    "app_schema_apply edit mode accepts app_key and optional app_name rename only.",
                    fix_hint="For existing apps, use `app_key` and optionally `app_name`. For create mode, use `package_id + app_name`.",
                )
        elif not create_by_shape:
            return _config_failure(
                "app_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 ai_builder.app_schema_apply(
            profile=profile,
            app_key=app_key,
            package_id=package_id,
            app_name=app_name,
            app_title=app_title,
            icon=icon,
            color=color,
            visibility=visibility,
            publish=publish,
            form=form or [],
            add_fields=add_fields or [],
            update_fields=update_fields or [],
            remove_fields=remove_fields or [],
            apps=None,
        )

    @server.tool()
    def app_layout_apply(
        profile: str = DEFAULT_PROFILE,
        app_key: str = "",
        mode: str = "merge",
        publish: bool = True,
        sections: list[dict] | None = None,
        apps: list[dict] | None = None,
    ) -> dict:
        return ai_builder.app_layout_apply(profile=profile, app_key=app_key, mode=mode, publish=publish, sections=sections or [], apps=apps)

    @server.tool()
    def app_flow_get(
        profile: str = DEFAULT_PROFILE,
        app_key: str = "",
        version_id: str = "",
    ) -> dict:
        return ai_builder.app_flow_get(profile=profile, app_key=app_key, version_id=version_id or None)

    @server.tool()
    def app_flow_get_schema(profile: str = DEFAULT_PROFILE, schema_version: str = "") -> dict:
        return ai_builder.app_flow_get_schema(profile=profile, schema_version=schema_version or None)

    @server.tool()
    def app_flow_apply(
        profile: str = DEFAULT_PROFILE,
        app_key: str = "",
        publish: bool = True,
        spec: dict | None = None,
        idempotency_key: str = "",
        schema_version: str = "",
        patch_nodes: list[dict] | None = None,
    ) -> dict:
        return ai_builder.app_flow_apply(
            profile=profile,
            app_key=app_key,
            publish=publish,
            spec=spec,
            idempotency_key=idempotency_key or None,
            schema_version=schema_version or None,
            patch_nodes=patch_nodes,
        )

    @server.tool()
    def app_views_apply(
        profile: str = DEFAULT_PROFILE,
        app_key: str = "",
        publish: bool = True,
        upsert_views: list[dict] | None = None,
        patch_views: list[dict] | None = None,
        remove_views: list[str] | None = None,
        apps: list[dict] | None = None,
        views: list[dict] | None = None,
    ) -> dict:
        return ai_builder.app_views_apply(
            profile=profile,
            app_key=app_key,
            publish=publish,
            upsert_views=upsert_views or [],
            patch_views=patch_views or [],
            remove_views=remove_views or [],
            apps=apps,
            views=views,
        )

    @server.tool()
    def app_charts_apply(
        profile: str = DEFAULT_PROFILE,
        app_key: str = "",
        upsert_charts: list[dict] | None = None,
        patch_charts: list[dict] | None = None,
        remove_chart_ids: list[str] | None = None,
        reorder_chart_ids: list[str] | None = None,
        apps: list[dict] | None = None,
    ) -> dict:
        return ai_builder.app_charts_apply(
            profile=profile,
            app_key=app_key,
            upsert_charts=upsert_charts or [],
            patch_charts=patch_charts or [],
            remove_chart_ids=remove_chart_ids or [],
            reorder_chart_ids=reorder_chart_ids or [],
            apps=apps,
        )

    @server.tool()
    def portal_apply(
        profile: str = DEFAULT_PROFILE,
        dash_key: str = "",
        dash_name: str = "",
        name: str = "",
        package_id: int | None = None,
        publish: bool = True,
        sections: list[dict] | None = None,
        pages: list[dict] | None = None,
        layout_preset: str = "",
        visibility: dict | None = None,
        auth: dict | None = None,
        icon: str | None = None,
        color: str | None = None,
        hide_copyright: bool | None = None,
        dash_global_config: dict | None = None,
        config: dict | None = None,
        payload: dict | None = None,
        patch_sections: list[dict] | None = None,
    ) -> dict:
        payload = payload if isinstance(payload, dict) else {}
        has_dash_key = bool((dash_key or "").strip())
        effective_dash_name = (dash_name or name or str(payload.get("dash_name") or payload.get("dashName") or payload.get("name") or "")).strip()
        has_dash_name = bool(effective_dash_name)
        effective_package_id = package_id if package_id is not None else payload.get("package_id") or payload.get("packageId") or payload.get("package_tag_id")
        has_package_id = effective_package_id is not None
        if has_dash_key and has_package_id:
            return _config_failure(
                "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):
            return _config_failure(
                "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`.",
            )
        return ai_builder.portal_apply(
            profile=profile,
            dash_key=dash_key,
            dash_name=dash_name,
            name=name,
            package_id=package_id,
            publish=publish,
            sections=sections or [],
            pages=pages or [],
            layout_preset=layout_preset,
            visibility=visibility,
            auth=auth,
            icon=icon,
            color=color,
            hide_copyright=hide_copyright,
            dash_global_config=dash_global_config,
            config=config or {},
            payload=payload,
            patch_sections=patch_sections,
        )

    @server.tool()
    def portal_delete(
        profile: str = DEFAULT_PROFILE,
        dash_key: str = "",
    ) -> dict:
        return ai_builder.portal_delete(
            profile=profile,
            dash_key=dash_key,
        )

    @server.tool()
    def app_publish_verify(
        profile: str = DEFAULT_PROFILE,
        app_key: str = "",
        app_keys: list[str] | None = None,
        expected_package_id: int | None = None,
    ) -> dict:
        return ai_builder.app_publish_verify(
            profile=profile,
            app_key=app_key,
            app_keys=app_keys,
            expected_package_id=expected_package_id,
        )

    return server


def main() -> None:
    build_builder_server().run()


if __name__ == "__main__":
    main()
