import z from "zod"
import * as ProviderAuth from "#auth/provider-auth.ts"
import * as Config from "#config/config.ts"
import * as Connector from "#connector/connector.ts"
import * as Mcp from "#mcp/manager.ts"
import * as Plugin from "#plugin/plugin.ts"
import * as ModelsDev from "#provider/modelsdev.ts"
import * as AnyboxHTTP from "#provider/anybox-http.ts"
import * as Provider from "#provider/provider.ts"
import { ApiError } from "#server/error.ts"
import { clearProjectModelListCache } from "#server/usecases/model-list-cache.ts"
import * as PromptPresets from "#session/support/prompt-presets.ts"
import * as PromptUrlInstall from "#session/support/prompt-url-install.ts"
import * as SkillGitInstall from "#skill/git-install.ts"
import * as Skill from "#skill/skill.ts"
import * as SkillManager from "#skill/manage.ts"
import * as ToolRegistry from "#tool/registry.ts"
import * as Log from "#util/log.ts"

const log = Log.create({ service: "settings" })

export const SkillFileQuery = z.object({
  path: z.string().min(1),
})

export const SkillFileBody = z.object({
  path: z.string().min(1),
  content: z.string(),
})

export const CreateSkillBody = z.object({
  name: z.string().min(1),
  parentDirectory: z.string().min(1).nullable().optional(),
})

export const RenameSkillBody = z.object({
  directory: z.string().min(1),
  name: z.string().min(1),
})

export const DeleteSkillQuery = z.object({
  directory: z.string().min(1),
})

export const CreateSkillFolderBody = z.object({
  name: z.string().min(1),
  parentDirectory: z.string().min(1).nullable().optional(),
})

export const RenameSkillFolderBody = z.object({
  directory: z.string().min(1),
  name: z.string().min(1),
})

export const DeleteSkillFolderQuery = z.object({
  directory: z.string().min(1),
})

export const MoveSkillDirectoryBody = z.object({
  directory: z.string().min(1),
  parentDirectory: z.string().min(1).nullable().optional(),
})

export const PreviewSkillGitInstallBody = z.object({
  source: z.string().min(1),
  parentDirectory: z.string().min(1).nullable().optional(),
})

export const InstallSkillGitPreviewBody = z.object({
  previewID: z.string().min(1),
  skillIDs: z.array(z.string().min(1)),
  parentDirectory: z.string().min(1).nullable().optional(),
})

export const InstallSkillLocalFileBody = z.object({
  sourcePath: z.string().min(1),
  parentDirectory: z.string().min(1).nullable().optional(),
})

export const UpdateMcpServerBody = Config.McpServerInput
export const UpdateGlobalProviderBody = Config.Provider
export const UpdateGlobalModelSelectionBody = Config.ModelSelection
export const PluginCatalogQuery = z.object({
  freshness: z.enum(["cached", "fresh"]).optional(),
})
export const InstallPluginBody = Plugin.InstallPluginInput
export const UpdateInstalledPluginBody = Plugin.UpdateInstalledPluginInput
export const SavePluginConnectorApiKeyBody = Plugin.SavePluginConnectorApiKeyInput
export const SaveConnectorApiKeyBody = Connector.SaveConnectorApiKeyInput
export const SaveConnectorConfigBody = Connector.SaveConnectorConfigInput
export const PluginConnectorAuthFlowBody = z.object({}).optional()
export const ConnectorAuthFlowBody = z.object({}).optional()

export const ProviderAuthFlowBody = z.object({
  method: z.string().min(1),
  baseURL: z.string().nullable().optional(),
})

export const ProviderAuthApiKeyBody = z.object({
  apiKey: z.string().nullable().optional(),
})

export const ProviderConnectionTestBody = z.object({
  method: z.string().optional(),
  credentialMode: z.enum(["active", "manual", "environment"]).optional(),
  apiKey: z.string().nullable().optional(),
  baseURL: z.string().nullable().optional(),
})

export const PromptPresetCreateBody = z.object({
  label: z.string().optional(),
  content: z.string().optional(),
  description: z.string().optional(),
})

export const PromptPresetBody = z.object({
  label: z.string().optional(),
  content: z.string(),
  description: z.string().optional(),
})

export const PromptPresetSelectionBody = z.object({
  systemPromptPresetID: z.string().min(1),
  planModePromptPresetID: z.string().min(1),
  sideChatPromptPresetID: z.string().min(1),
})

export const PreviewPromptUrlInstallBody = z.object({
  source: z.string().min(1),
})

export const InstallPromptUrlPreviewBody = z.object({
  previewID: z.string().min(1),
  promptIDs: z.array(z.string().min(1)),
})

export const UpdateBuiltinToolSelectionBody = z
  .object({
    tools: z.record(z.string(), z.boolean()),
  })
  .strict()

export const UpdateToolPermissionModeBody = z
  .object({
    mode: Config.PermissionMode,
  })
  .strict()

function toToolInputSchema(schema: z.ZodType) {
  try {
    return z.toJSONSchema(schema)
  } catch {
    return {}
  }
}

function parseModelReference(value: string) {
  const [providerID, ...rest] = value.split("/")
  const modelID = rest.join("/")
  if (!providerID || !modelID) {
    throw new ApiError(400, "INVALID_MODEL_REFERENCE", `Model '${value}' must use the format provider/model`)
  }

  return {
    providerID,
    modelID,
  }
}

function toSkillApiError(error: unknown) {
  if (error instanceof SkillManager.SkillManagerError) {
    if (error.code === "SKILL_FILE_NOT_FOUND" || error.code === "SKILL_NOT_FOUND") {
      return new ApiError(404, error.code, error.message)
    }

    if (error.code === "SKILL_ALREADY_EXISTS") {
      return new ApiError(409, error.code, error.message)
    }

    return new ApiError(400, error.code, error.message)
  }

  if (error instanceof SkillGitInstall.SkillGitInstallError) {
    if (error.code === "SKILL_GIT_PREVIEW_NOT_FOUND") {
      return new ApiError(404, error.code, error.message)
    }

    if (error.code === "SKILL_ALREADY_EXISTS" || error.code === "SKILL_GIT_SKILL_UNAVAILABLE") {
      return new ApiError(409, error.code, error.message)
    }

    return new ApiError(400, error.code, error.message)
  }

  return error
}

function toPromptPresetApiError(error: unknown) {
  if (error instanceof PromptPresets.PromptPresetStoreError) {
    switch (error.code) {
      case "INVALID_PROMPT_FILE":
      case "INVALID_PROMPT_PATH":
      case "DUPLICATE_PROMPT_PRESET":
        return new ApiError(400, error.code, error.message)
      default:
        return error
    }
  }

  if (!(error instanceof Error)) {
    return error
  }

  if (error.message.startsWith("Unknown prompt preset")) {
    return new ApiError(404, "PROMPT_PRESET_NOT_FOUND", error.message)
  }

  if (error.message.includes("cannot be reset") || error.message.includes("cannot be deleted")) {
    return new ApiError(400, "PROMPT_PRESET_ACTION_NOT_ALLOWED", error.message)
  }

  return error
}

function toPromptUrlInstallApiError(error: unknown) {
  if (error instanceof PromptUrlInstall.PromptUrlInstallError) {
    if (error.code === "PROMPT_URL_PREVIEW_NOT_FOUND") {
      return new ApiError(404, error.code, error.message)
    }

    if (error.code === "PROMPT_URL_FETCH_FAILED") {
      return new ApiError(502, error.code, error.message)
    }

    return new ApiError(400, error.code, error.message)
  }

  return error
}

function toPluginApiError(error: unknown) {
  if (error instanceof Plugin.PluginError) {
    switch (error.code) {
      case "PLUGIN_NOT_FOUND":
      case "INSTALLED_PLUGIN_NOT_FOUND":
      case "PLUGIN_CONNECTOR_NOT_FOUND":
        return new ApiError(404, error.code, error.message)
      case "PLUGIN_ALREADY_INSTALLED":
        return new ApiError(409, error.code, error.message)
      case "PLUGIN_PACKAGE_DOWNLOAD_FAILED":
      case "PLUGIN_REGISTRY_UNAVAILABLE":
        return new ApiError(502, error.code, error.message)
      case "PLUGIN_CONFIG_INVALID":
      case "PLUGIN_RISK_NOT_ALLOWED":
      case "PLUGIN_CONNECTOR_NOT_CONNECTED":
      case "PLUGIN_PACKAGE_UNAVAILABLE":
      case "PLUGIN_PACKAGE_INVALID":
        return new ApiError(400, error.code, error.message)
    }
  }

  return error
}

async function readProviderAuthState(providerID: string) {
  const catalog = await Provider.catalog()
  const item = catalog.find((entry) => entry.id === providerID)
  if (item?.authState) return item.authState
  return ProviderAuth.createDisconnectedProviderAuthState(providerID)
}

function assertProviderConfigDoesNotContainSecrets(input: Config.Provider) {
  if (input.options && "apiKey" in input.options) {
    throw new ApiError(
      400,
      "PROVIDER_API_KEY_NOT_ALLOWED",
      "Provider API keys must be saved through the credential store, not provider config.",
    )
  }
}

export async function listProviderCatalog() {
  return Provider.catalog()
}

export async function refreshProviderCatalog() {
  try {
    await ModelsDev.refresh()
  } catch (error) {
    throw new ApiError(
      502,
      "PROVIDER_CATALOG_REFRESH_FAILED",
      error instanceof Error ? error.message : String(error),
    )
  }

  clearProjectModelListCache()
  return Provider.catalog()
}

export async function listProviders() {
  return {
    items: await Provider.listPublicProviders(),
    selection: await Provider.getSelection(),
  }
}

export async function listModels() {
  return {
    items: await Provider.listModels(),
    selection: await Provider.getSelection(),
  }
}

export async function updateProvider(
  providerID: string,
  input: z.infer<typeof UpdateGlobalProviderBody>,
) {
  assertProviderConfigDoesNotContainSecrets(input)

  try {
    await Provider.validateProviderConfig(providerID, input, Config.GLOBAL_CONFIG_ID)
  } catch (error) {
    throw new ApiError(
      400,
      "PROVIDER_VALIDATION_FAILED",
      error instanceof Error ? error.message : String(error),
    )
  }

  const providerConfig = await Config.setProvider(Config.GLOBAL_CONFIG_ID, providerID, input)
  clearProjectModelListCache()
  const provider = await Provider.getPublicProvider(providerID)
  if (!provider) {
    throw new ApiError(404, "PROVIDER_NOT_FOUND", `Provider '${providerID}' not found in the catalog`)
  }

  return {
    provider,
    selection: {
      model: providerConfig.model,
      small_model: providerConfig.small_model,
      image_model: providerConfig.image_model,
      image_generation: providerConfig.image_generation,
    },
  }
}

export async function removeProvider(providerID: string) {
  const providerConfig = await Config.removeProvider(Config.GLOBAL_CONFIG_ID, providerID)
  clearProjectModelListCache()

  return {
    providerID,
    selection: {
      model: providerConfig.model,
      small_model: providerConfig.small_model,
      image_model: providerConfig.image_model,
      image_generation: providerConfig.image_generation,
    },
  }
}

export async function updateModelSelection(input: z.infer<typeof UpdateGlobalModelSelectionBody>) {
  if (input.model) {
    const ref = parseModelReference(input.model)
    await Provider.getModel(ref.providerID, ref.modelID)
  }

  if (input.small_model) {
    const ref = parseModelReference(input.small_model)
    await Provider.getModel(ref.providerID, ref.modelID)
  }

  if (input.image_model) {
    const ref = parseModelReference(input.image_model)
    const model = await Provider.getModel(ref.providerID, ref.modelID)
    if (!model.capabilities.output.image) {
      throw new ApiError(
        400,
        "MODEL_NOT_IMAGE_CAPABLE",
        `Model '${input.image_model}' does not support image output`,
      )
    }
  }

  const selection = await Config.setModelSelection(Config.GLOBAL_CONFIG_ID, input)

  return {
    model: selection.model,
    small_model: selection.small_model,
    image_model: selection.image_model,
    image_generation: selection.image_generation,
  }
}

export async function getProviderAuth(providerID: string) {
  return readProviderAuthState(providerID)
}

function classifyProviderConnectionError(error: unknown) {
  const message = error instanceof Error ? error.message : String(error)
  if (error instanceof AnyboxHTTP.AnyboxHTTPError) {
    const status = error.code === "http_error" ? "auth_error" : "network_error"
    return {
      status: status as "auth_error" | "network_error" | "config_error",
      message,
      errorCode: error.code,
      diagnostics: error.diagnostics,
    }
  }

  const normalized = message.toLowerCase()

  if (
    normalized.includes("rejected the api key") ||
    normalized.includes("401") ||
    normalized.includes("403") ||
    normalized.includes("unauthorized") ||
    normalized.includes("forbidden")
  ) {
    return {
      status: "auth_error" as const,
      message,
      errorCode: undefined,
      diagnostics: undefined,
    }
  }

  if (
    normalized.includes("could not reach") ||
    normalized.includes("network") ||
    normalized.includes("timeout") ||
    normalized.includes("fetch") ||
    normalized.includes("enotfound") ||
    normalized.includes("econnrefused")
  ) {
    return {
      status: "network_error" as const,
      message,
      errorCode: undefined,
      diagnostics: undefined,
    }
  }

  return {
    status: "config_error" as const,
    message,
    errorCode: undefined,
    diagnostics: undefined,
  }
}

function toConnectorApiError(error: unknown) {
  if (error instanceof Connector.ConnectorError) {
    switch (error.code) {
      case "CONNECTOR_NOT_FOUND":
        return new ApiError(404, error.code, error.message)
      case "CONNECTOR_RUNTIME_MISSING":
      case "CONNECTOR_UNAVAILABLE":
      case "CONNECTOR_CREDENTIAL_UNSUPPORTED":
      case "CONNECTOR_NOT_CONNECTED":
      case "CONNECTOR_CONFIG_REQUIRED":
      case "CONNECTOR_CONFIG_UNSUPPORTED":
      case "CONNECTOR_REGISTRY_INVALID":
        return new ApiError(400, error.code, error.message)
    }
  }

  return error
}

function getProviderConnectionCapability(
  provider: Provider.ProviderCatalogItem,
  method: string | undefined,
) {
  if (!method) return undefined
  return provider.authCapabilities.find((capability) => capability.method === method)
}

function hasProviderConnectionCandidate(
  provider: Provider.ProviderCatalogItem,
  input: {
    method?: string
    credentialMode?: "active" | "manual" | "environment"
    apiKey?: string
  },
) {
  const capability = getProviderConnectionCapability(provider, input.method)
  const credentials = provider.authState.credentials

  if (input.method && !capability) return false

  if (capability?.kind === "api_key") {
    if (input.apiKey) return true

    if (input.credentialMode === "environment") {
      return credentials.some(
        (credential) =>
          credential.method === capability.method &&
          credential.kind === "api_key" &&
          credential.source === "environment" &&
          credential.configured,
      )
    }

    if (input.credentialMode === "manual") {
      return credentials.some(
        (credential) =>
          credential.method === capability.method &&
          credential.kind === "api_key" &&
          credential.source === "credential_store" &&
          credential.configured,
      )
    }

    return credentials.some(
      (credential) =>
        credential.method === capability.method &&
        credential.kind === "api_key" &&
        credential.configured,
    )
  }

  if (capability) {
    return credentials.some(
      (credential) =>
        credential.method === capability.method &&
        credential.configured,
    )
  }

  return provider.authState.status === "connected" || provider.apiKeyConfigured
}

export async function testProviderConnection(
  providerID: string,
  input: z.infer<typeof ProviderConnectionTestBody>,
) {
  const catalog = await Provider.catalog()
  const provider = catalog.find((entry) => entry.id === providerID)

  if (!provider) {
    throw new ApiError(404, "PROVIDER_NOT_FOUND", `Provider '${providerID}' not found in the catalog`)
  }

  const method = input.method?.trim() || provider.authState.activeMethod
  const credentialMode = input.credentialMode ?? "active"
  const apiKey = input.apiKey?.trim()
  const baseURL = input.baseURL?.trim()
  const capability = getProviderConnectionCapability(provider, method)

  if (method && !capability) {
    return {
      providerID,
      ok: false,
      status: "unsupported" as const,
      checkedAt: Date.now(),
      message: `Provider '${provider.name}' does not support connection method '${method}'.`,
    }
  }

  if (!hasProviderConnectionCandidate(provider, { method, credentialMode, apiKey })) {
    return {
      providerID,
      ok: false,
      status: "not_connected" as const,
      checkedAt: Date.now(),
      message: "未找到可用连接。请先配置环境变量、登录账号或保存 API key。",
    }
  }

  try {
    const optionsPayload: {
      apiKey?: string
      baseURL?: string
    } = {}

    if (apiKey) optionsPayload.apiKey = apiKey
    if (baseURL) optionsPayload.baseURL = baseURL

    await Provider.validateProviderConfig(
      providerID,
      {
        name: provider.name,
        env: provider.env,
        options: Object.keys(optionsPayload).length > 0 ? optionsPayload : undefined,
      },
      Config.GLOBAL_CONFIG_ID,
      {
        auth: {
          method,
          credentialMode,
          transientApiKey: apiKey,
        },
        requireCredential: true,
      },
    )

    return {
      providerID,
      ok: true,
      status: "working" as const,
      checkedAt: Date.now(),
      message: "连接测试成功。",
    }
  } catch (error) {
    const classified = classifyProviderConnectionError(error)
    return {
      providerID,
      ok: false,
      status: classified.status,
      checkedAt: Date.now(),
      message: classified.message,
      errorCode: classified.errorCode,
      diagnostics: classified.diagnostics,
    }
  }
}

export async function startProviderAuthFlow(input: {
  providerID: string
  method: string
  serverBaseURL: string
  baseURL?: string | null
}) {
  try {
    const catalog = await Provider.catalog()
    const provider = catalog.find((entry) => entry.id === input.providerID)
    return await ProviderAuth.startProviderAuthFlow({
      ...input,
      providerBaseURL: input.baseURL?.trim() || provider?.baseURL,
    })
  } catch (error) {
    if (error instanceof AnyboxHTTP.AnyboxHTTPError) {
      log.warn("provider-auth-flow-network-failed", {
        providerID: input.providerID,
        method: input.method,
        errorCode: error.code,
        diagnostics: error.diagnostics,
        message: error.message,
      })
    }
    throw new ApiError(400, "PROVIDER_AUTH_FLOW_FAILED", error instanceof Error ? error.message : String(error))
  }
}

export async function getProviderAuthFlow(providerID: string, flowID: string) {
  const flow = await ProviderAuth.getProviderFlow(providerID, flowID)
  if (!flow) {
    throw new ApiError(404, "PROVIDER_AUTH_FLOW_NOT_FOUND", `Auth flow '${flowID}' was not found`)
  }

  if (flow.status === "connected") {
    clearProjectModelListCache()
  }

  return flow
}

export async function cancelProviderAuthFlow(providerID: string, flowID: string) {
  const flow = await ProviderAuth.cancelProviderAuthFlow(providerID, flowID)
  if (!flow) {
    throw new ApiError(404, "PROVIDER_AUTH_FLOW_NOT_FOUND", `Auth flow '${flowID}' was not found`)
  }

  return flow
}

export async function completeProviderAuthCallback(providerID: string, url: URL) {
  const result = await ProviderAuth.completeProviderBrowserCallback({
    providerID,
    url,
  })
  if (result.ok) {
    clearProjectModelListCache()
  }

  return {
    html: ProviderAuth.renderProviderAuthCallbackPage({
      ok: result.ok,
      title: result.title,
      message: result.message,
    }),
    status: result.status,
  }
}

export async function saveProviderApiKey(
  providerID: string,
  input: z.infer<typeof ProviderAuthApiKeyBody>,
) {
  await ProviderAuth.saveProviderApiKey(providerID, input.apiKey)
  clearProjectModelListCache()
  return readProviderAuthState(providerID)
}

export async function deleteProviderSession(providerID: string) {
  await ProviderAuth.deleteProviderSession(providerID)
  clearProjectModelListCache()
  return readProviderAuthState(providerID)
}

export async function listMcpServers() {
  return Config.listMcpServers(Config.GLOBAL_CONFIG_ID)
}

export async function updateMcpServer(serverID: string, input: z.infer<typeof UpdateMcpServerBody>) {
  return Config.setMcpServer(Config.GLOBAL_CONFIG_ID, serverID, input)
}

export async function getMcpServerDiagnostic(serverID: string) {
  const server = await Config.getMcpServer(Config.GLOBAL_CONFIG_ID, serverID)
  if (!server) {
    throw new ApiError(404, "MCP_SERVER_NOT_FOUND", `MCP server '${serverID}' is not configured globally`)
  }

  return Mcp.diagnoseServer(server)
}

export async function removeMcpServer(serverID: string) {
  return {
    serverID,
    removed: Boolean(await Config.removeMcpServer(Config.GLOBAL_CONFIG_ID, serverID)),
  }
}

export async function listPluginCatalog(input: z.infer<typeof PluginCatalogQuery> = {}) {
  return input.freshness === "cached" ? Plugin.listCachedCatalog() : await Plugin.listCatalog()
}

export function listInstalledPlugins() {
  return Plugin.listInstalled()
}

export async function installPlugin(pluginID: string, input: z.infer<typeof InstallPluginBody>) {
  try {
    return await Plugin.install(pluginID, input)
  } catch (error) {
    throw toPluginApiError(error)
  }
}

export async function updateInstalledPlugin(pluginID: string, input: z.infer<typeof UpdateInstalledPluginBody>) {
  try {
    return await Plugin.update(pluginID, input)
  } catch (error) {
    throw toPluginApiError(error)
  }
}

export async function removeInstalledPlugin(pluginID: string) {
  try {
    return await Plugin.remove(pluginID)
  } catch (error) {
    throw toPluginApiError(error)
  }
}

export async function getInstalledPluginDiagnostic(pluginID: string) {
  try {
    return await Plugin.diagnose(pluginID)
  } catch (error) {
    throw toPluginApiError(error)
  }
}

export function listConnectorCatalog() {
  try {
    return Connector.listDefinitions()
  } catch (error) {
    throw toConnectorApiError(error)
  }
}

export async function listConnectors() {
  try {
    return await Connector.listStatuses()
  } catch (error) {
    throw toConnectorApiError(error)
  }
}

export async function getConnector(connectorID: string) {
  try {
    return await Connector.getStatus(connectorID)
  } catch (error) {
    throw toConnectorApiError(error)
  }
}

export async function saveConnectorApiKey(
  connectorID: string,
  input: z.infer<typeof SaveConnectorApiKeyBody>,
) {
  try {
    return await Connector.saveConnectorApiKey(connectorID, input)
  } catch (error) {
    throw toConnectorApiError(error)
  }
}

export async function deleteConnectorApiKey(connectorID: string) {
  try {
    return await Connector.removeConnectorApiKey(connectorID)
  } catch (error) {
    throw toConnectorApiError(error)
  }
}

export async function saveConnectorConfig(
  connectorID: string,
  input: z.infer<typeof SaveConnectorConfigBody>,
) {
  try {
    return await Connector.saveConnectorConfig(connectorID, input)
  } catch (error) {
    throw toConnectorApiError(error)
  }
}

export async function deleteConnectorConfig(connectorID: string) {
  try {
    return await Connector.removeConnectorConfig(connectorID)
  } catch (error) {
    throw toConnectorApiError(error)
  }
}

export async function startConnectorAuthFlow(connectorID: string, input: { serverBaseURL: string }) {
  try {
    return await Connector.startConnectorOAuthFlow(connectorID, input)
  } catch (error) {
    throw toConnectorApiError(error)
  }
}

export async function getConnectorAuthFlow(connectorID: string, flowID: string) {
  try {
    return await Connector.getConnectorOAuthFlow(connectorID, flowID)
  } catch (error) {
    throw toConnectorApiError(error)
  }
}

export async function cancelConnectorAuthFlow(connectorID: string, flowID: string) {
  try {
    return await Connector.cancelConnectorOAuthFlow(connectorID, flowID)
  } catch (error) {
    throw toConnectorApiError(error)
  }
}

export async function deleteConnectorAuthSession(connectorID: string) {
  try {
    return await Connector.deleteConnectorOAuthSession(connectorID)
  } catch (error) {
    throw toConnectorApiError(error)
  }
}

export async function getConnectorDiagnostic(connectorID: string) {
  try {
    return await Connector.diagnoseConnector(connectorID)
  } catch (error) {
    throw toConnectorApiError(error)
  }
}

export async function listInstalledPluginConnectors(pluginID: string) {
  try {
    return await Plugin.listConnectorStatuses(pluginID)
  } catch (error) {
    throw toPluginApiError(error)
  }
}

export async function saveInstalledPluginConnectorApiKey(
  pluginID: string,
  appID: string,
  input: z.infer<typeof SavePluginConnectorApiKeyBody>,
) {
  try {
    return await Plugin.saveConnectorApiKey(pluginID, appID, input)
  } catch (error) {
    throw toPluginApiError(error)
  }
}

export async function deleteInstalledPluginConnectorApiKey(pluginID: string, appID: string) {
  try {
    return await Plugin.removeConnectorApiKey(pluginID, appID)
  } catch (error) {
    throw toPluginApiError(error)
  }
}

export async function startInstalledPluginConnectorAuthFlow(
  pluginID: string,
  appID: string,
  input: { serverBaseURL: string },
) {
  try {
    return await Plugin.startConnectorOAuthFlow(pluginID, appID, input)
  } catch (error) {
    throw toPluginApiError(error)
  }
}

export async function getInstalledPluginConnectorAuthFlow(pluginID: string, appID: string, flowID: string) {
  try {
    return await Plugin.getConnectorOAuthFlow(pluginID, appID, flowID)
  } catch (error) {
    throw toPluginApiError(error)
  }
}

export async function cancelInstalledPluginConnectorAuthFlow(pluginID: string, appID: string, flowID: string) {
  try {
    return await Plugin.cancelConnectorOAuthFlow(pluginID, appID, flowID)
  } catch (error) {
    throw toPluginApiError(error)
  }
}

export async function deleteInstalledPluginConnectorAuthSession(pluginID: string, appID: string) {
  try {
    return await Plugin.deleteConnectorOAuthSession(pluginID, appID)
  } catch (error) {
    throw toPluginApiError(error)
  }
}

export async function getInstalledPluginConnectorDiagnostic(pluginID: string, appID: string) {
  try {
    return await Plugin.diagnoseConnector(pluginID, appID)
  } catch (error) {
    throw toPluginApiError(error)
  }
}

export async function listBuiltinTools() {
  const [items, selection] = await Promise.all([
    ToolRegistry.builtinTools(),
    Config.getToolSelection(Config.GLOBAL_CONFIG_ID),
  ])

  return {
    items: await Promise.all(
      items.map(async (item) => {
        const runtime = await item.init()
        const explicitStates = [item.id, ...(item.aliases ?? [])]
          .map((name) => selection.tools[name])
          .filter((value): value is boolean => typeof value === "boolean")

        return {
          id: item.id,
          title: runtime.title ?? item.title ?? item.id,
          description: runtime.description,
          inputSchema: toToolInputSchema(runtime.parameters),
          aliases: item.aliases ?? [],
          capabilities: item.capabilities ?? {},
          enabled: !explicitStates.includes(false),
        }
      }),
    ),
    selection,
  }
}

export async function updateBuiltinToolSelection(input: z.infer<typeof UpdateBuiltinToolSelectionBody>) {
  const items = await ToolRegistry.builtinTools()
  const knownToolIDs = new Set(items.map((item) => item.id))
  const tools: Record<string, boolean> = {}

  for (const [toolID, enabled] of Object.entries(input.tools)) {
    const normalizedToolID = toolID.trim()
    if (!normalizedToolID) continue
    if (!knownToolIDs.has(normalizedToolID)) {
      throw new ApiError(400, "UNKNOWN_BUILTIN_TOOL", `Unknown built-in tool id '${normalizedToolID}'.`)
    }
    tools[normalizedToolID] = enabled
  }

  return Config.setToolSelection(Config.GLOBAL_CONFIG_ID, tools)
}

export async function getToolPermissionMode() {
  return Config.getPermissionMode(Config.GLOBAL_CONFIG_ID)
}

export async function updateToolPermissionMode(input: z.infer<typeof UpdateToolPermissionModeBody>) {
  return Config.setPermissionMode(Config.GLOBAL_CONFIG_ID, input.mode)
}

export async function listPromptPresets() {
  return PromptPresets.listPromptPresetSummaries(Config.GLOBAL_CONFIG_ID)
}

export async function getPromptPresetSelection() {
  return PromptPresets.getPromptPresetSelection(Config.GLOBAL_CONFIG_ID)
}

export async function updatePromptPresetSelection(input: z.infer<typeof PromptPresetSelectionBody>) {
  try {
    return await PromptPresets.updatePromptPresetSelection(input, Config.GLOBAL_CONFIG_ID)
  } catch (error) {
    throw toPromptPresetApiError(error)
  }
}

export async function createPromptPreset(input: z.infer<typeof PromptPresetCreateBody>) {
  try {
    return await PromptPresets.createPromptPreset(input, Config.GLOBAL_CONFIG_ID)
  } catch (error) {
    throw toPromptPresetApiError(error)
  }
}

export async function readPromptPreset(presetID: string) {
  try {
    return await PromptPresets.readPromptPresetDocument(presetID, Config.GLOBAL_CONFIG_ID)
  } catch (error) {
    throw toPromptPresetApiError(error)
  }
}

export async function updatePromptPreset(presetID: string, input: z.infer<typeof PromptPresetBody>) {
  try {
    return await PromptPresets.updatePromptPreset(presetID, input, Config.GLOBAL_CONFIG_ID)
  } catch (error) {
    throw toPromptPresetApiError(error)
  }
}

export async function resetPromptPreset(presetID: string) {
  try {
    return await PromptPresets.resetPromptPreset(presetID, Config.GLOBAL_CONFIG_ID)
  } catch (error) {
    throw toPromptPresetApiError(error)
  }
}

export async function deletePromptPreset(presetID: string) {
  try {
    return await PromptPresets.deletePromptPreset(presetID, Config.GLOBAL_CONFIG_ID)
  } catch (error) {
    throw toPromptPresetApiError(error)
  }
}

export async function previewPromptUrlInstall(input: z.infer<typeof PreviewPromptUrlInstallBody>) {
  try {
    return await PromptUrlInstall.previewPromptUrlInstall(input.source)
  } catch (error) {
    throw toPromptUrlInstallApiError(error)
  }
}

export async function installPromptUrlPreview(input: z.infer<typeof InstallPromptUrlPreviewBody>) {
  try {
    return await PromptUrlInstall.installPromptsFromUrlPreview(input, Config.GLOBAL_CONFIG_ID)
  } catch (error) {
    throw toPromptUrlInstallApiError(toPromptPresetApiError(error))
  }
}

export async function listSkills() {
  return Skill.listGlobal()
}

export async function getSkillTree() {
  return SkillManager.getGlobalSkillTree()
}

export async function readSkillFile(input: z.infer<typeof SkillFileQuery>) {
  try {
    return await SkillManager.readGlobalSkillFile(input.path)
  } catch (error) {
    throw toSkillApiError(error)
  }
}

export async function writeSkillFile(input: z.infer<typeof SkillFileBody>) {
  try {
    return await SkillManager.writeGlobalSkillFile(input)
  } catch (error) {
    throw toSkillApiError(error)
  }
}

export async function createSkill(input: z.infer<typeof CreateSkillBody>) {
  try {
    return await SkillManager.createGlobalSkill(input)
  } catch (error) {
    throw toSkillApiError(error)
  }
}

export async function renameSkill(input: z.infer<typeof RenameSkillBody>) {
  try {
    return await SkillManager.renameGlobalSkill(input)
  } catch (error) {
    throw toSkillApiError(error)
  }
}

export async function deleteSkill(input: z.infer<typeof DeleteSkillQuery>) {
  try {
    await SkillManager.deleteGlobalSkill(input.directory)
    return {
      directory: input.directory,
      removed: true,
    }
  } catch (error) {
    throw toSkillApiError(error)
  }
}

export async function createSkillFolder(input: z.infer<typeof CreateSkillFolderBody>) {
  try {
    return await SkillManager.createGlobalSkillFolder(input)
  } catch (error) {
    throw toSkillApiError(error)
  }
}

export async function renameSkillFolder(input: z.infer<typeof RenameSkillFolderBody>) {
  try {
    return await SkillManager.renameGlobalSkillFolder(input)
  } catch (error) {
    throw toSkillApiError(error)
  }
}

export async function deleteSkillFolder(input: z.infer<typeof DeleteSkillFolderQuery>) {
  try {
    await SkillManager.deleteGlobalSkillFolder(input.directory)
    return {
      directory: input.directory,
      removed: true,
    }
  } catch (error) {
    throw toSkillApiError(error)
  }
}

export async function moveSkillDirectory(input: z.infer<typeof MoveSkillDirectoryBody>) {
  try {
    return await SkillManager.moveGlobalSkillDirectory(input)
  } catch (error) {
    throw toSkillApiError(error)
  }
}

export async function previewSkillGitInstall(input: z.infer<typeof PreviewSkillGitInstallBody>) {
  try {
    return await SkillGitInstall.previewGlobalSkillGitInstall(input.source, input.parentDirectory)
  } catch (error) {
    throw toSkillApiError(error)
  }
}

export async function installSkillGitPreview(input: z.infer<typeof InstallSkillGitPreviewBody>) {
  try {
    return await SkillGitInstall.installGlobalSkillsFromGitPreview(input)
  } catch (error) {
    throw toSkillApiError(error)
  }
}

export async function installSkillLocalFile(input: z.infer<typeof InstallSkillLocalFileBody>) {
  try {
    return await SkillGitInstall.installGlobalSkillFromLocalPath(input.sourcePath, input.parentDirectory)
  } catch (error) {
    throw toSkillApiError(error)
  }
}
