package handler

import (
	"encoding/json"
	"io"
	"net/http"
	"strconv"
	"strings"

	"aijianzhan/platform/internal/agentcap"
	"aijianzhan/platform/internal/apidef"
	"aijianzhan/platform/internal/audit"
	"aijianzhan/platform/internal/authx"
	"aijianzhan/platform/internal/logic/applogic"
	"aijianzhan/platform/internal/meta"
	"aijianzhan/platform/internal/svc"
	"aijianzhan/platform/internal/types"

	"github.com/zeromicro/go-zero/rest"
	"github.com/zeromicro/go-zero/rest/httpx"
	"github.com/zeromicro/go-zero/rest/pathvar"
)

func chain(h http.HandlerFunc, mws ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
	for i := len(mws) - 1; i >= 0; i-- {
		h = mws[i](h)
	}
	return h
}

func RegisterHandlers(server *rest.Server, svcCtx *svc.ServiceContext) {
	authMW := authx.Middleware(svcCtx.JWT, svcCtx.Config.DevAuth)
	rl := svcCtx.Limiter.Middleware
	perm := authx.RequirePermission
	tenant := authx.RequireTenant()
	platformAdmin := authx.RequirePlatformAdmin()
	syncPush := authx.RequireSyncPushAccess()
	appGrant := requireAgentAppGrant(svcCtx)

	// —— 公开 ——
	server.AddRoutes([]rest.Route{
		{Method: http.MethodPost, Path: "/api/v1/auth/token", Handler: rl(tokenHandler(svcCtx))},
		{Method: http.MethodPost, Path: "/api/v1/auth/agent/register", Handler: rl(agentSelfRegisterHandler(svcCtx))},
		{Method: http.MethodPost, Path: "/api/v1/auth/bind-code/redeem", Handler: rl(bindCodeRedeemHandler(svcCtx))},
		{Method: http.MethodPost, Path: "/api/v1/auth/bind/phone-lookup", Handler: rl(bindPhoneLookupHandler(svcCtx))},
		{Method: http.MethodPost, Path: "/api/v1/auth/bind/phone-confirm", Handler: rl(bindPhoneConfirmHandler(svcCtx))},
		{Method: http.MethodGet, Path: "/api/v1/auth/bind/policy", Handler: rl(bindPolicyHandler(svcCtx))},
		{Method: http.MethodPost, Path: "/api/v1/auth/yuheng/ticket-exchange", Handler: rl(yuhengTicketExchangeHandler(svcCtx))},
		{Method: http.MethodPost, Path: "/api/v1/auth/yuheng/restore-by-host", Handler: rl(yuhengRestoreByHostHandler(svcCtx))},
		{Method: http.MethodPost, Path: "/api/v1/auth/register", Handler: rl(registerHandler(svcCtx))},
		{Method: http.MethodPost, Path: "/api/v1/auth/login", Handler: rl(loginHandler(svcCtx))},
		{Method: http.MethodPost, Path: "/api/v1/auth/sms/send", Handler: rl(sendLoginSMSHandler(svcCtx))},
		{Method: http.MethodGet, Path: "/api/v1/meta/apis", Handler: rl(apiCatalogHandler())},
		{Method: http.MethodGet, Path: "/api/v1/meta/openapi.yaml", Handler: rl(openapiHandler())},
		// 授权租约：过期后仍可调用（中间件放行），供远端续费 / 安全狗延期
		{Method: http.MethodGet, Path: "/api/v1/license/status", Handler: rl(licenseStatusHandler(svcCtx))},
		{Method: http.MethodPost, Path: "/api/v1/license/renew", Handler: rl(licenseRenewHandler(svcCtx))},
		{Method: http.MethodPost, Path: "/api/v1/license/extend", Handler: rl(licenseExtendHandler(svcCtx))},
		{Method: http.MethodPut, Path: "/api/v1/license/lease", Handler: rl(licensePutLeaseHandler(svcCtx))},
		{Method: http.MethodPost, Path: "/api/v1/license/import", Handler: rl(licenseImportHandler(svcCtx))},
		// 已发布模块公开展示（看板预览 / 真页面截图，无需登录）
		{Method: http.MethodGet, Path: "/api/v1/public/apps/:slug/blueprint", Handler: rl(publicBlueprintHandler(svcCtx))},
		{Method: http.MethodGet, Path: "/api/v1/public/apps/:slug/:resource", Handler: rl(publicListHandler(svcCtx))},
		// 按用户 ID 加密后的模块路径访问（宿主「访问地址」）
		{Method: http.MethodGet, Path: "/api/v1/public/m/:token/blueprint", Handler: rl(publicModulePathBlueprintHandler(svcCtx))},
	})

	// —— 鉴权：pending 可调用（入驻） ——
	server.AddRoutes([]rest.Route{
		{Method: http.MethodPost, Path: "/api/v1/auth/invites/accept", Handler: chain(inviteAcceptHandler(svcCtx), rl, authMW)},
		{Method: http.MethodPost, Path: "/api/v1/auth/password", Handler: chain(changePasswordHandler(svcCtx), rl, authMW)},
		{Method: http.MethodGet, Path: "/api/v1/auth/me", Handler: chain(meHandler(svcCtx), rl, authMW)},
		{Method: http.MethodPut, Path: "/api/v1/auth/phone", Handler: chain(bindPhoneHandler(svcCtx), rl, authMW)},
		{Method: http.MethodPost, Path: "/api/v1/tenants", Handler: chain(tenantCreateHandler(svcCtx), rl, authMW)},

		// —— 平台超级管理员（仅公司一级：列表/新建）——
		// —— 平台超级管理员（公司一级 + 权限额度）——
		{Method: http.MethodGet, Path: "/api/v1/platform/tenants", Handler: chain(platformListTenantsHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodPost, Path: "/api/v1/platform/tenants", Handler: chain(platformCreateTenantHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodPut, Path: "/api/v1/platform/tenants/:id", Handler: chain(platformUpdateTenantHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodPost, Path: "/api/v1/platform/tenants/:id/enter", Handler: chain(platformEnterTenantHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodPost, Path: "/api/v1/platform/exit", Handler: chain(platformExitTenantHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodPost, Path: "/api/v1/platform/tenants/:id/admin-invite", Handler: chain(platformAdminInviteHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodPost, Path: "/api/v1/platform/tenants/:id/admin-account", Handler: chain(platformAdminAccountHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodGet, Path: "/api/v1/platform/tenants/:id/admins", Handler: chain(platformListAdminsHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodPut, Path: "/api/v1/platform/tenants/:id/admins/:uid", Handler: chain(platformUpdateAdminHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodGet, Path: "/api/v1/platform/perm-modules", Handler: chain(platformPermModulesHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodGet, Path: "/api/v1/platform/tenants/:id/permissions", Handler: chain(platformGetTenantPermsHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodPut, Path: "/api/v1/platform/tenants/:id/permissions", Handler: chain(platformSetTenantPermsHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodGet, Path: "/api/v1/platform/dbsync/lww-overrides", Handler: chain(platformLwwOverridesHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodPost, Path: "/api/v1/platform/dbsync/lww-overrides/:id/rollback", Handler: chain(platformLwwRollbackHandler(svcCtx), rl, authMW, platformAdmin)},
		{Method: http.MethodPost, Path: "/api/v1/platform/apps/ensure-import", Handler: chain(platformEnsureImportHandler(svcCtx), rl, authMW, platformAdmin)},
	})

	// —— 鉴权：需已加入租户 ——
	server.AddRoutes([]rest.Route{
		{Method: http.MethodGet, Path: "/api/v1/apps", Handler: chain(listAppsHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm读取模块))},
		{Method: http.MethodPost, Path: "/api/v1/admin/apps/ensure-import", Handler: chain(adminEnsureImportHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm发布模块))},
		{Method: http.MethodPut, Path: "/api/v1/apps/:slug/draft", Handler: chain(saveDraftHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm写入模块), appGrant)},
		{Method: http.MethodPost, Path: "/api/v1/apps/:slug/publish", Handler: chain(publishHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm发布模块), appGrant)},
		{Method: http.MethodGet, Path: "/api/v1/apps/:slug/blueprint", Handler: chain(getBlueprintHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm读取模块), appGrant)},
		{Method: http.MethodGet, Path: "/api/v1/apps/:slug/agent-capsule", Handler: chain(capsuleHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm读取模块), appGrant)},
		{Method: http.MethodGet, Path: "/api/v1/audit/logs", Handler: chain(auditListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm查看审计))},

		{Method: http.MethodGet, Path: "/api/v1/admin/agents", Handler: chain(agentListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
		{Method: http.MethodPost, Path: "/api/v1/admin/agents", Handler: chain(agentCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
		{Method: http.MethodGet, Path: "/api/v1/admin/agents/:id", Handler: chain(agentGetHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
		{Method: http.MethodPut, Path: "/api/v1/admin/agents/:id", Handler: chain(agentUpdateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
		{Method: http.MethodPost, Path: "/api/v1/admin/agents/:id/rotate-secret", Handler: chain(agentRotateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
		{Method: http.MethodDelete, Path: "/api/v1/admin/agents/:id", Handler: chain(agentDeleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
		// Z12b：智能体自查（无需管理智能体）
		{Method: http.MethodGet, Path: "/api/v1/agents/me", Handler: chain(agentMeHandler(svcCtx), rl, authMW)},

		{Method: http.MethodGet, Path: "/api/v1/admin/bind-codes", Handler: chain(bindCodeListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodPost, Path: "/api/v1/admin/bind-codes", Handler: chain(bindCodeCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodDelete, Path: "/api/v1/admin/bind-codes/:code", Handler: chain(bindCodeRevokeHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},

		{Method: http.MethodGet, Path: "/api/v1/admin/roles", Handler: chain(roleListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
		{Method: http.MethodGet, Path: "/api/v1/admin/entitlements", Handler: chain(companyEntitlementsHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm读取模块))},
		{Method: http.MethodGet, Path: "/api/v1/admin/members", Handler: chain(memberListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
		{Method: http.MethodPost, Path: "/api/v1/admin/members", Handler: chain(memberCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
		{Method: http.MethodPut, Path: "/api/v1/admin/members/:id", Handler: chain(memberUpdateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
		{Method: http.MethodPost, Path: "/api/v1/admin/roles", Handler: chain(roleCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
		{Method: http.MethodGet, Path: "/api/v1/admin/roles/:id", Handler: chain(roleGetHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
		{Method: http.MethodPut, Path: "/api/v1/admin/roles/:id", Handler: chain(roleUpdateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
		{Method: http.MethodDelete, Path: "/api/v1/admin/roles/:id", Handler: chain(roleDeleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},

		{Method: http.MethodGet, Path: "/api/v1/admin/invites", Handler: chain(inviteListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
		{Method: http.MethodPost, Path: "/api/v1/admin/invites", Handler: chain(inviteCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
		{Method: http.MethodDelete, Path: "/api/v1/admin/invites/:id", Handler: chain(inviteRevokeHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},

		{Method: http.MethodGet, Path: "/api/v1/admin/org-units", Handler: chain(orgUnitListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理组织))},
		{Method: http.MethodPost, Path: "/api/v1/admin/org-units", Handler: chain(orgUnitCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理组织))},
		{Method: http.MethodPut, Path: "/api/v1/admin/org-units/:id", Handler: chain(orgUnitUpdateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理组织))},
		{Method: http.MethodDelete, Path: "/api/v1/admin/org-units/:id", Handler: chain(orgUnitDeleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理组织))},

		// 跨库同步中间件：本地 SQLite ↔ 线上 MySQL/Postgres，后台可配线上地址
		{Method: http.MethodGet, Path: "/api/v1/admin/sync/channels", Handler: chain(syncListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels", Handler: chain(syncSaveHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodGet, Path: "/api/v1/admin/sync/channels/:id", Handler: chain(syncGetHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodPut, Path: "/api/v1/admin/sync/channels/:id", Handler: chain(syncSaveHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodDelete, Path: "/api/v1/admin/sync/channels/:id", Handler: chain(syncDeleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodPost, Path: "/api/v1/admin/sync/test", Handler: chain(syncTestHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/prepare", Handler: chain(syncPrepareHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/start", Handler: chain(syncStartHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/stop", Handler: chain(syncStopHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodGet, Path: "/api/v1/admin/sync/conflicts", Handler: chain(syncConflictsHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodPost, Path: "/api/v1/admin/sync/conflicts/:id/resolve", Handler: chain(syncResolveConflictHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/reconcile", Handler: chain(syncReconcileHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/ingest", Handler: chain(syncIngestHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodGet, Path: "/api/v1/admin/sync/channels/:id/inspect", Handler: chain(syncInspectHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/preview", Handler: chain(syncPreviewHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/drop-table", Handler: chain(syncDropTableHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
		{Method: http.MethodGet, Path: "/api/v1/admin/sync/bindings", Handler: chain(syncBindingsListHandler(svcCtx), rl, authMW, tenant, syncPush)},
		{Method: http.MethodPost, Path: "/api/v1/admin/sync/bindings", Handler: chain(syncBindingsEnsureHandler(svcCtx), rl, authMW, tenant, syncPush)},

		// —— 本机 sync agent（形态 B）：白名单 + push/pull；「数据同步」租户级，或登录用户按 Binding 自助 ——
		{Method: http.MethodGet, Path: "/api/v1/agent/sync/channels/:id/whitelist", Handler: chain(agentSyncWhitelistHandler(svcCtx), rl, authMW, tenant, syncPush)},
		{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/push", Handler: chain(agentSyncPushHandler(svcCtx), rl, authMW, tenant, syncPush)},
		{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/push/batch", Handler: chain(agentSyncPushBatchHandler(svcCtx), rl, authMW, tenant, syncPush)},
		{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/pull", Handler: chain(agentSyncPullHandler(svcCtx), rl, authMW, tenant, syncPush)},
		{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/bootstrap", Handler: chain(agentSyncBootstrapHandler(svcCtx), rl, authMW, tenant, syncPush)},
		{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/schema", Handler: chain(agentSyncSchemaDescribeHandler(svcCtx), rl, authMW, tenant, syncPush)},
		{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/schema/ensure", Handler: chain(agentSyncSchemaEnsureHandler(svcCtx), rl, authMW, tenant, syncPush)},
		{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/schema/fingerprint", Handler: chain(agentSyncSchemaFingerprintHandler(svcCtx), rl, authMW, tenant, syncPush)},

		// 存储：POST 创建对象，GET 读取（无 /upload 动词路径；旧路径保留别名防断裂）
		{Method: http.MethodPost, Path: "/api/v1/storage", Handler: chain(uploadHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm上传文件))},
		{Method: http.MethodPost, Path: "/api/v1/storage/upload", Handler: chain(uploadHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm上传文件))},
		{Method: http.MethodGet, Path: "/api/v1/storage/:1/:2/:3", Handler: chain(downloadHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm下载文件))},

		// 动态 CRUD：仅通用行数据传输
		{Method: http.MethodPost, Path: "/api/v1/apps/:slug/:resource/import", Handler: chain(importHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm导入数据), appGrant)},
		{Method: http.MethodGet, Path: "/api/v1/apps/:slug/:resource/export", Handler: chain(exportHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm导出数据), appGrant)},
		{Method: http.MethodGet, Path: "/api/v1/apps/:slug/:resource/aggregate", Handler: chain(aggregateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm查询数据), appGrant)},
		{Method: http.MethodGet, Path: "/api/v1/apps/:slug/:resource", Handler: chain(listHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm查询数据), appGrant)},
		{Method: http.MethodPost, Path: "/api/v1/apps/:slug/:resource", Handler: chain(createHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm新增数据), appGrant)},
		{Method: http.MethodGet, Path: "/api/v1/apps/:slug/:resource/:id", Handler: chain(getHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm查询数据), appGrant)},
		{Method: http.MethodPut, Path: "/api/v1/apps/:slug/:resource/:id", Handler: chain(updateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm更新数据), appGrant)},
		{Method: http.MethodDelete, Path: "/api/v1/apps/:slug/:resource/:id", Handler: chain(deleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm删除数据), appGrant)},
	})
}

func requireAgentAppGrant(svcCtx *svc.ServiceContext) func(http.HandlerFunc) http.HandlerFunc {
	return func(next http.HandlerFunc) http.HandlerFunc {
		return func(w http.ResponseWriter, r *http.Request) {
			if authx.Role(r.Context()) != authx.RoleAgent {
				next(w, r)
				return
			}
			vars := pathvar.Vars(r)
			slug := vars["slug"]
			if slug == "" {
				next(w, r)
				return
			}
			if svcCtx.Agents == nil {
				authx.WriteError(w, http.StatusForbidden, "agent store unavailable")
				return
			}
			ok, err := svcCtx.Agents.HasAppAccess(r.Context(), authx.AgentID(r.Context()), slug)
			if err != nil {
				authx.WriteError(w, http.StatusForbidden, err.Error())
				return
			}
			if ok {
				next(w, r)
				return
			}
			// 新建：目标模块尚不存在时，允许已启用智能体直接 publish/draft（发布成功后自动写入 app_slugs）
			if svcCtx.Meta != nil && (r.Method == http.MethodPost || r.Method == http.MethodPut) {
				existing, gerr := svcCtx.Meta.GetBySlug(r.Context(), authx.TenantID(r.Context()), slug)
				if gerr != nil || existing == nil {
					next(w, r)
					return
				}
			}
			authx.WriteError(w, http.StatusForbidden, "app not granted to agent: "+slug)
		}
	}
}

func agentListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		items, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).List()
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, map[string]any{"items": items})
	}
}

func agentGetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
		acc, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Get(id)
		if err != nil {
			authx.WriteError(w, http.StatusNotFound, err.Error())
			return
		}
		httpx.OkJson(w, acc)
	}
}

func agentCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.AgentCreateReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Create(&req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func agentUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
		var req types.AgentUpdateReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		acc, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Update(id, &req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, acc)
	}
}

func agentRotateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
		resp, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Rotate(id)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func agentDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
		if err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Delete(id); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, map[string]any{"ok": true})
	}
}

func agentMeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		acc, err := applogic.NewAuthLogic(r.Context(), svcCtx).AgentMe()
		if err != nil {
			authx.WriteError(w, http.StatusForbidden, err.Error())
			return
		}
		httpx.OkJson(w, map[string]any{
			"agent_id":      acc.AgentID,
			"tenant_id":     acc.TenantID,
			"name":          acc.Name,
			"client_id":     acc.ClientID,
			"status":        acc.Status,
			"channel_id":    acc.ChannelID,
			"online_db_id":  acc.OnlineDBID,
			"database_name": acc.DatabaseName,
			"sync_bound":    strings.TrimSpace(acc.ChannelID) != "" && strings.TrimSpace(acc.OnlineDBID) != "",
			"app_slugs":     acc.AppSlugs,
			"permissions":   acc.Perms,
		})
	}
}

func bindCodeListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		items, err := applogic.NewAuthLogic(r.Context(), svcCtx).ListBindCodes()
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, map[string]any{"items": items})
	}
}

func bindCodeCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req applogic.BindCodeCreateReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil && err != io.EOF {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		bc, err := applogic.NewAuthLogic(r.Context(), svcCtx).CreateBindCode(req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, bc)
	}
}

func bindCodeRevokeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		code := pathvar.Vars(r)["code"]
		if err := applogic.NewAuthLogic(r.Context(), svcCtx).RevokeBindCode(code); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, map[string]any{"ok": true})
	}
}

func bindCodeRedeemHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req applogic.BindCodeRedeemReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).RedeemBindCode(req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func bindPhoneLookupHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req applogic.PhoneLookupReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).PhoneLookup(req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func bindPolicyHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		httpx.OkJson(w, applogic.NewAuthLogic(r.Context(), svcCtx).BindPolicy())
	}
}

func bindPhoneConfirmHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req applogic.PhoneConfirmReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).PhoneConfirm(req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func yuhengTicketExchangeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req applogic.YuhengTicketExchangeReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).ExchangeYuhengTicket(req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func yuhengRestoreByHostHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req applogic.YuhengRestoreByHostReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).RestoreByYuhengHost(req)
		if err != nil {
			if e := applogic.AsRestoreBindError(err); e != nil {
				body := map[string]any{
					"code":    e.HTTPStatus,
					"message": e.Message,
					"ok":      false,
				}
				if e.NeedBind {
					body["need_bind"] = true
				}
				httpx.WriteJson(w, e.HTTPStatus, body)
				return
			}
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func roleListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		items, err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).List()
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, map[string]any{"items": items})
	}
}

func roleGetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
		role, err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).Get(id)
		if err != nil {
			authx.WriteError(w, http.StatusNotFound, err.Error())
			return
		}
		httpx.OkJson(w, role)
	}
}

func roleCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.RoleCreateReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		role, err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).Create(&req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, role)
	}
}

func roleUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
		var req types.RoleUpdateReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		role, err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).Update(id, &req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, role)
	}
}

func roleDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
		if err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).Delete(id); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, map[string]any{"ok": true})
	}
}

func apiCatalogHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		httpx.OkJson(w, map[string]any{
			"platform": apidef.Catalog,
			"ai":       apidef.AICatalog,
			"verbs":    []string{"GET", "POST", "PUT", "DELETE"},
			"note":     "业务数据只走 apps/{slug}/{resource} CRUD；行业字段由蓝图定义，不在此增删路由。",
		})
	}
}

func tokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.TokenReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).IssueToken(&req)
		if err != nil {
			authx.WriteError(w, http.StatusUnauthorized, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func agentSelfRegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.AgentSelfRegisterReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).SelfRegisterAgent(&req)
		if err != nil {
			msg := err.Error()
			code := http.StatusBadRequest
			if strings.Contains(msg, "invalid register secret") {
				code = http.StatusUnauthorized
			}
			if strings.Contains(msg, "already registered") {
				code = http.StatusConflict
			}
			authx.WriteError(w, code, msg)
			return
		}
		httpx.OkJson(w, resp)
	}
}

func registerHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.RegisterReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).Register(&req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		_ = svcCtx.Audit.Log(r.Context(), resp.TenantID, resp.UserID, "auth.register", req.Username)
		httpx.WriteJson(w, http.StatusCreated, resp)
	}
}

func loginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.LoginReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).Login(&req)
		if err != nil {
			msg := err.Error()
			code := http.StatusUnauthorized
			if strings.Contains(msg, "请填写") || strings.Contains(msg, "格式") || strings.Contains(msg, "未开启") || strings.Contains(msg, "未启用") {
				code = http.StatusBadRequest
			}
			authx.WriteError(w, code, msg)
			return
		}
		label := req.Username
		if label == "" {
			label = req.Phone
		}
		_ = svcCtx.Audit.Log(r.Context(), resp.TenantID, resp.UserID, "auth.login", label)
		httpx.OkJson(w, resp)
	}
}

func sendLoginSMSHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.SMSSendReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		res, err := applogic.NewAuthLogic(r.Context(), svcCtx).SendLoginSMS(req.Phone)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, types.SMSSendResp{
			OK:         true,
			ExpiresIn:  res.ExpiresIn,
			RetryAfter: res.RetryAfter,
			Message:    res.Message,
			DebugCode:  res.DebugCode,
		})
	}
}

func changePasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var body struct {
			OldPassword string `json:"old_password"`
			NewPassword string `json:"new_password"`
		}
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		if err := applogic.NewAuthLogic(r.Context(), svcCtx).ChangePassword(body.OldPassword, body.NewPassword); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		_ = svcCtx.Audit.Log(r.Context(), authx.TenantID(r.Context()), authx.UserID(r.Context()), "auth.change_password", "ok")
		httpx.OkJson(w, map[string]any{"ok": true})
	}
}

func meHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		u, err := applogic.NewAuthLogic(r.Context(), svcCtx).Me()
		if err != nil {
			authx.WriteError(w, http.StatusUnauthorized, err.Error())
			return
		}
		httpx.OkJson(w, map[string]any{
			"user_id":                 u.UserID,
			"username":                u.Username,
			"phone":                   u.Phone,
			"username_login_disabled": u.UsernameLoginDisabled,
			"display_name":            u.DisplayName,
			"role":                    u.Role,
			"tenant_id":               u.TenantID,
			"org_unit_id":             u.OrgUnitID,
			"status":                  u.Status,
			"policy": map[string]any{
				"phone_login_only":                      applogic.NewAuthLogic(r.Context(), svcCtx).PhoneLoginOnlyPolicy(),
				"license_enabled":                       svcCtx.Config.License.Enabled,
				"disable_username_login_if_phone_bound": svcCtx.Config.Auth.DisableUsernameLoginIfPhoneBound,
				"require_phone_bound":                   svcCtx.Config.Auth.RequirePhoneBound,
			},
		})
	}
}

func bindPhoneHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var body struct {
			Phone                string `json:"phone"`
			DisableUsernameLogin *bool  `json:"disable_username_login"`
		}
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		l := applogic.NewAuthLogic(r.Context(), svcCtx)
		u, err := l.BindPhone(body.Phone)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		if body.DisableUsernameLogin != nil {
			u, err = l.SetUsernameLoginDisabled(*body.DisableUsernameLogin)
			if err != nil {
				authx.WriteError(w, http.StatusBadRequest, err.Error())
				return
			}
		}
		_ = svcCtx.Audit.Log(r.Context(), authx.TenantID(r.Context()), authx.UserID(r.Context()),
			"auth.bind_phone", u.Phone)
		httpx.OkJson(w, map[string]any{
			"user_id":                 u.UserID,
			"username":                u.Username,
			"phone":                   u.Phone,
			"username_login_disabled": u.UsernameLoginDisabled,
			"display_name":            u.DisplayName,
		})
	}
}

func inviteAcceptHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.InviteAcceptReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).AcceptInvite(&req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		_ = svcCtx.Audit.Log(r.Context(), resp.TenantID, resp.UserID, "auth.invite.accept", req.Code)
		httpx.OkJson(w, resp)
	}
}

func tenantCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.TenantCreateReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).CreateTenant(&req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		_ = svcCtx.Audit.Log(r.Context(), resp.TenantID, resp.UserID, "tenant.create", req.Name)
		httpx.WriteJson(w, http.StatusCreated, resp)
	}
}

func inviteListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		items, err := applogic.NewInviteAdminLogic(r.Context(), svcCtx).List()
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, map[string]any{"items": items})
	}
}

func inviteCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.InviteCreateReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		inv, err := applogic.NewInviteAdminLogic(r.Context(), svcCtx).Create(&req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.WriteJson(w, http.StatusCreated, inv)
	}
}

func inviteRevokeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
		if err := applogic.NewInviteAdminLogic(r.Context(), svcCtx).Revoke(id); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, map[string]string{"status": "ok"})
	}
}

func orgUnitListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		items, err := applogic.NewOrgUnitLogic(r.Context(), svcCtx).List()
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, map[string]any{"items": items, "max_depth": 5})
	}
}

func orgUnitCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.OrgUnitCreateReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		ou, err := applogic.NewOrgUnitLogic(r.Context(), svcCtx).Create(&req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.WriteJson(w, http.StatusCreated, ou)
	}
}

func orgUnitUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
		var req types.OrgUnitUpdateReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		ou, err := applogic.NewOrgUnitLogic(r.Context(), svcCtx).Update(id, &req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, ou)
	}
}

func orgUnitDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
		if err := applogic.NewOrgUnitLogic(r.Context(), svcCtx).Delete(id); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, map[string]string{"status": "ok"})
	}
}

func auditListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		page, _ := strconv.Atoi(r.URL.Query().Get("page"))
		size, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
		if page <= 0 {
			page = 1
		}
		if size <= 0 {
			size = 50
		}
		items, total, err := svcCtx.Audit.List(r.Context(), authx.TenantID(r.Context()), size, (page-1)*size)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, map[string]any{"items": items, "total": total, "page": page, "page_size": size})
	}
}

func uploadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if err := r.ParseMultipartForm(32 << 20); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		file, hdr, err := r.FormFile("file")
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, "file required")
			return
		}
		defer file.Close()
		ct := hdr.Header.Get("Content-Type")
		if ct == "" {
			ct = "application/octet-stream"
		}
		meta, err := svcCtx.Objects.Put(r.Context(), authx.TenantID(r.Context()), hdr.Filename, ct, file, hdr.Size)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		_ = svcCtx.Audit.Log(r.Context(), authx.TenantID(r.Context()), authx.UserID(r.Context()), "storage.upload", audit.DetailJSON(meta))
		httpx.OkJson(w, types.UploadResp{
			Key: meta.Key, URL: meta.URL, Filename: meta.Filename, ContentType: meta.ContentType, Size: meta.Size,
		})
	}
}

func downloadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// path: /api/v1/storage/t1/20260101/xxx.png → key from URL after /storage/
		idx := strings.Index(r.URL.Path, "/storage/")
		if idx < 0 {
			authx.WriteError(w, http.StatusNotFound, "not found")
			return
		}
		key := r.URL.Path[idx+len("/storage/"):]
		rc, meta, err := svcCtx.Objects.Open(r.Context(), key)
		if err != nil {
			authx.WriteError(w, http.StatusNotFound, err.Error())
			return
		}
		defer rc.Close()
		if meta.ContentType != "" {
			w.Header().Set("Content-Type", meta.ContentType)
		}
		if meta.Filename != "" {
			w.Header().Set("Content-Disposition", "inline; filename="+meta.Filename)
		}
		_, _ = io.Copy(w, rc)
	}
}

func capsuleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		slug := pathvar.Vars(r)["slug"]
		resp, err := applogic.NewCapsuleLogic(r.Context(), svcCtx).Build(slug)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func importHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := pathvar.Vars(r)
		if err := r.ParseMultipartForm(8 << 20); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		file, hdr, err := r.FormFile("file")
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, "file required")
			return
		}
		defer file.Close()
		filename := "import.xlsx"
		if hdr != nil && hdr.Filename != "" {
			filename = hdr.Filename
		}
		resp, err := applogic.NewCrudLogic(r.Context(), svcCtx).ImportRows(vars["slug"], vars["resource"], filename, file)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func exportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := pathvar.Vars(r)
		format := strings.ToLower(r.URL.Query().Get("format"))
		if format == "csv" {
			raw, err := applogic.NewCrudLogic(r.Context(), svcCtx).ExportCSV(vars["slug"], vars["resource"])
			if err != nil {
				authx.WriteError(w, http.StatusBadRequest, err.Error())
				return
			}
			w.Header().Set("Content-Type", "text/csv; charset=utf-8")
			w.Header().Set("Content-Disposition", "attachment; filename="+vars["resource"]+".csv")
			_, _ = w.Write(raw)
			return
		}
		raw, name, err := applogic.NewCrudLogic(r.Context(), svcCtx).ExportExcel(vars["slug"], vars["resource"])
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
		w.Header().Set("Content-Disposition", "attachment; filename="+name)
		_, _ = w.Write(raw)
	}
}

func aggregateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := pathvar.Vars(r)
		q := r.URL.Query()
		resp, err := applogic.NewCrudLogic(r.Context(), svcCtx).Aggregate(
			vars["slug"], vars["resource"], q.Get("group_by"), q.Get("sum"),
		)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func listAppsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		resp, err := applogic.NewPublishLogic(r.Context(), svcCtx).ListApps()
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func saveDraftHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		slug := pathvar.Vars(r)["slug"]
		var req types.DraftReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewPublishLogic(r.Context(), svcCtx).SaveDraft(slug, &req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func publishHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		slug := pathvar.Vars(r)["slug"]
		var req types.PublishReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		resp, err := applogic.NewPublishLogic(r.Context(), svcCtx).Publish(slug, &req)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func getBlueprintHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		slug := pathvar.Vars(r)["slug"]
		bp, err := applogic.NewCrudLogic(r.Context(), svcCtx).GetBlueprint(slug)
		if err != nil {
			authx.WriteError(w, http.StatusNotFound, err.Error())
			return
		}
		httpx.OkJson(w, bp)
	}
}

func publicBlueprintHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		slug := pathvar.Vars(r)["slug"]
		app, err := svcCtx.Meta.FindPublishedBySlug(r.Context(), slug)
		if err != nil {
			authx.WriteError(w, http.StatusNotFound, err.Error())
			return
		}
		if app.Blueprint == nil {
			authx.WriteError(w, http.StatusNotFound, "blueprint missing")
			return
		}
		w.Header().Set("Cache-Control", "no-store")
		w.Header().Set("X-AJZ-App-Slug", app.Slug)
		w.Header().Set("X-AJZ-Blueprint-Revision", app.Blueprint.Revision())
		httpx.OkJson(w, app.Blueprint)
	}
}

func publicModulePathBlueprintHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		token := pathvar.Vars(r)["token"]
		secret := svcCtx.Config.Agent.CapsuleSecret
		if secret == "" {
			secret = svcCtx.JWT.AccessSecret
		}
		claim, err := agentcap.OpenModulePath(secret, token)
		if err != nil {
			authx.WriteError(w, http.StatusNotFound, "invalid access path")
			return
		}
		app, err := svcCtx.Meta.GetBySlug(r.Context(), claim.TenantID, claim.Slug)
		if err != nil || app == nil || app.Blueprint == nil || app.Status != meta.StatusPublished {
			authx.WriteError(w, http.StatusNotFound, "module not found")
			return
		}
		httpx.OkJson(w, app.Blueprint)
	}
}

func publicListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := pathvar.Vars(r)
		slug, resource := vars["slug"], vars["resource"]
		if apidef.IsReservedResource(resource) {
			authx.WriteError(w, http.StatusNotFound, "not found")
			return
		}
		app, err := svcCtx.Meta.FindPublishedBySlug(r.Context(), slug)
		if err != nil {
			authx.WriteError(w, http.StatusNotFound, err.Error())
			return
		}
		q := r.URL.Query()
		page, size := applogic.ParsePage(q.Get("page"), q.Get("page_size"))
		filters := map[string]string{}
		for k, vs := range q {
			if strings.HasPrefix(k, "filter.") && len(vs) > 0 {
				filters[strings.TrimPrefix(k, "filter.")] = vs[0]
			}
		}
		// 以应用所属租户读取数据，无需登录态
		ctx := authx.WithClaims(r.Context(), app.TenantID, 0, authx.Role只读)
		resp, err := applogic.NewCrudLogic(ctx, svcCtx).List(slug, resource, page, size, filters, q.Get("sort"))
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		w.Header().Set("Cache-Control", "no-store")
		w.Header().Set("X-AJZ-App-Slug", app.Slug)
		w.Header().Set("X-AJZ-Resolved-Schema", app.SchemaName)
		if app.Blueprint != nil {
			w.Header().Set("X-AJZ-Blueprint-Revision", app.Blueprint.Revision())
		}
		httpx.OkJson(w, resp)
	}
}

func listHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := pathvar.Vars(r)
		slug, resource := vars["slug"], vars["resource"]
		if apidef.IsReservedResource(resource) {
			authx.WriteError(w, http.StatusNotFound, "not found")
			return
		}
		q := r.URL.Query()
		page, size := applogic.ParsePage(q.Get("page"), q.Get("page_size"))
		filters := map[string]string{}
		for k, vs := range q {
			if strings.HasPrefix(k, "filter.") && len(vs) > 0 {
				filters[strings.TrimPrefix(k, "filter.")] = vs[0]
			}
		}
		resp, err := applogic.NewCrudLogic(r.Context(), svcCtx).List(slug, resource, page, size, filters, q.Get("sort"))
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, resp)
	}
}

func createHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := pathvar.Vars(r)
		if apidef.IsReservedResource(vars["resource"]) {
			authx.WriteError(w, http.StatusBadRequest, "reserved resource name")
			return
		}
		body, err := readJSONObject(r)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		row, err := applogic.NewCrudLogic(r.Context(), svcCtx).Create(vars["slug"], vars["resource"], body)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.WriteJson(w, http.StatusCreated, row)
	}
}

func getHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := pathvar.Vars(r)
		row, err := applogic.NewCrudLogic(r.Context(), svcCtx).Get(vars["slug"], vars["resource"], vars["id"])
		if err != nil {
			authx.WriteError(w, http.StatusNotFound, err.Error())
			return
		}
		httpx.OkJson(w, row)
	}
}

func updateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := pathvar.Vars(r)
		body, err := readJSONObject(r)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		row, err := applogic.NewCrudLogic(r.Context(), svcCtx).Update(vars["slug"], vars["resource"], vars["id"], body)
		if err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		httpx.OkJson(w, row)
	}
}

func deleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := pathvar.Vars(r)
		if err := applogic.NewCrudLogic(r.Context(), svcCtx).Delete(vars["slug"], vars["resource"], vars["id"]); err != nil {
			authx.WriteError(w, http.StatusBadRequest, err.Error())
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}
}

func readJSONObject(r *http.Request) (map[string]any, error) {
	defer r.Body.Close()
	raw, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil {
		return nil, err
	}
	var body map[string]any
	if err := json.Unmarshal(raw, &body); err != nil {
		return nil, err
	}
	return body, nil
}
