import { OpenApi } from "effect/unstable/httpapi"
import { SaeeolHttpApi } from "./api"
import type { OpenApiSpec, OpenApiSchema, OpenApiOperation, OpenApiResponse, OpenApiParameter } from "./public-types"
import {
  InstanceQueryParameters,
  QueryNumberParameters,
  QueryBooleanParameters,
  QueryParameterSchemas,
  PathParameterSchemas,
  LegacyComponentDescriptions,
} from "./public-types"
import { stripOptionalNull, fixSelfReferencingComponents, nullable, stableSchema, rewriteRefs } from "./public-schema"

function matchLegacyOpenApi(input: Record<string, unknown>) {
  const spec = input as OpenApiSpec
  fixSelfReferencingComponents(spec)
  for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) {
    spec.components!.schemas![name] = stripOptionalNull(structuredClone(schema))
  }
  normalizeComponentNames(spec)
  collapseDuplicateComponents(spec)
  applyLegacySchemaOverrides(spec)
  normalizeComponentDescriptions(spec)
  addLegacyErrorSchemas(spec)
  delete spec.components?.schemas?.Unauthorized
  delete spec.components?.schemas?.EffectHttpApiErrorBadRequest
  delete spec.components?.schemas?.EffectHttpApiErrorNotFound
  delete spec.components?.schemas?.effect_HttpApiError_BadRequest
  delete spec.components?.schemas?.effect_HttpApiError_NotFound
  delete spec.components?.securitySchemes
  for (const [path, item] of Object.entries(spec.paths ?? {})) {
    const isInstanceRoute = !path.startsWith("/global/") && !path.startsWith("/auth/")
    for (const method of ["get", "post", "put", "delete", "patch"] as const) {
      const operation = item[method]
      if (!operation) continue
      if (operation.requestBody) {
        delete operation.requestBody.required
        const body = operation.requestBody.content?.["application/json"]
        if (body?.schema) body.schema = stripOptionalNull(structuredClone(body.schema))
        if (path === "/experimental/workspace" && method === "post") {
          const ref = body?.schema?.$ref?.replace("#/components/schemas/", "")
          const properties = ref
            ? spec.components?.schemas?.[ref]?.properties
            : body?.schema?.properties
          if (properties?.branch) properties.branch = { anyOf: [properties.branch, { type: "null" }] }
          if (properties?.extra) properties.extra = { anyOf: [properties.extra, { type: "null" }] }
        }
      }
      for (const response of Object.values(operation.responses ?? {})) {
        for (const content of Object.values(response.content ?? {})) {
          if (content.schema) content.schema = stripOptionalNull(structuredClone(content.schema))
        }
      }
      delete operation.security
      delete operation.responses?.["401"]
      normalizeLegacyErrorResponses(operation)
      normalizeLegacyOperation(operation, path, method)
      if ((path === "/event" || path === "/global/event") && method === "get") {
        operation.responses!["200"] = {
          description: "Event stream",
          content: {
            "text/event-stream": {
              schema: path === "/event"
                ? { $ref: "#/components/schemas/Event" }
                : { $ref: "#/components/schemas/GlobalEvent" },
            },
          },
        }
      }
      if (!isInstanceRoute) continue
      operation.parameters = [
        ...InstanceQueryParameters,
        ...(operation.parameters ?? []).filter(
          (param) => param.in !== "query" || (param.name !== "directory" && param.name !== "workspace"),
        ),
      ]
      for (const param of operation.parameters) normalizeParameter(param, `${method.toUpperCase()} ${path}`)
    }
  }
  return input
}

function addLegacyErrorSchemas(spec: OpenApiSpec) {
  if (!spec.components?.schemas) return
  spec.components.schemas.BadRequestError = {
    type: "object", required: ["data", "errors", "success"],
    properties: { data: {}, errors: { type: "array", items: { type: "object", additionalProperties: {} } }, success: { type: "boolean", enum: [false] } },
  }
  spec.components.schemas.NotFoundError = {
    type: "object", required: ["name", "data"],
    properties: { name: { type: "string", enum: ["NotFoundError"] }, data: { type: "object", required: ["message"], properties: { message: { type: "string" } } } },
  }
}

function collapseDuplicateComponents(spec: OpenApiSpec) {
  const schemas = spec.components?.schemas
  if (!schemas) return
  for (const name of Object.keys(schemas)) {
    const base = name.replace(/\d+$/, "")
    if (base === name || !schemas[base]) continue
    if (stableSchema(schemas[name], schemas) !== stableSchema(schemas[base], schemas)) continue
    rewriteRefs(spec, name, base)
    delete schemas[name]
  }
}

function normalizeComponentNames(spec: OpenApiSpec) {
  const schemas = spec.components?.schemas
  if (!schemas) return
  for (const name of Object.keys(schemas)) {
    const next = componentTypeName(name)
    if (next === name) continue
    if (schemas[next]) {
      if (stableSchema(schemas[name], schemas) === stableSchema(schemas[next], schemas)) {
        rewriteRefs(spec, name, next); delete schemas[name]
      }
      continue
    }
    schemas[next] = schemas[name]; rewriteRefs(spec, name, next); delete schemas[name]
  }
}

function componentTypeName(name: string) {
  if (!name.includes(".")) return name
  return name.split(".").filter((part) => !/^\d+$/.test(part)).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join("")
}

function applyLegacySchemaOverrides(spec: OpenApiSpec) {
  const schemas = spec.components?.schemas
  if (!schemas) return
  if (schemas.AgentConfig) schemas.AgentConfig.additionalProperties = {}
  if (schemas.Command?.properties?.template) schemas.Command.properties.template = { type: "string" }
  if (schemas.Workspace?.properties) {
    schemas.Workspace.properties.branch = nullable(schemas.Workspace.properties.branch)
    schemas.Workspace.properties.directory = nullable(schemas.Workspace.properties.directory)
    schemas.Workspace.properties.extra = nullable(schemas.Workspace.properties.extra)
  }
  if (schemas.GlobalSession?.properties?.project) schemas.GlobalSession.properties.project = nullable(schemas.GlobalSession.properties.project)
  const providerOptions = schemas.ProviderConfig?.properties?.options
  if (providerOptions) providerOptions.additionalProperties = {}
  const model = schemas.ProviderConfig?.properties?.models?.additionalProperties
  const variants = typeof model === "object" ? model.properties?.variants?.additionalProperties : undefined
  if (variants && typeof variants === "object") variants.additionalProperties = {}
  const syncInfo = schemas.SyncEventSessionUpdated?.properties?.data?.properties?.info
  if (syncInfo?.properties) makePropertiesNullable(syncInfo.properties)
}

function normalizeComponentDescriptions(spec: OpenApiSpec) {
  for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) {
    const description = LegacyComponentDescriptions[name as keyof typeof LegacyComponentDescriptions]
    if (description) { schema.description = description; continue }
    delete schema.description
  }
}

function makePropertiesNullable(properties: Record<string, OpenApiSchema>) {
  for (const [key, value] of Object.entries(properties)) {
    if (key === "share" && value.properties?.url) { value.properties.url = nullable(value.properties.url); continue }
    if (key === "time" && value.properties) { makePropertiesNullable(value.properties); continue }
    properties[key] = nullable(value)
  }
}

function normalizeLegacyErrorResponses(operation: OpenApiOperation) {
  if (operation.responses?.["400"] && isBuiltInErrorResponse(operation.responses["400"], "BadRequest"))
    operation.responses["400"] = legacyErrorResponse("Bad request", "BadRequestError")
  if (operation.responses?.["404"] && isBuiltInErrorResponse(operation.responses["404"], "NotFound"))
    operation.responses["404"] = legacyErrorResponse("Not found", "NotFoundError")
}

function normalizeLegacyOperation(operation: OpenApiOperation, path: string, method: string) {
  if (path === "/experimental/console/switch" && method === "post") delete operation.responses?.["400"]
  if (path === "/pty/{ptyID}" && method === "put") delete operation.responses?.["404"]
  if ((path !== "/session/{sessionID}/message" && path !== "/session/{sessionID}/command") || method !== "post") return
  const response = operation.responses?.["200"]?.content?.["application/json"]
  if (!response) return
  response.schema = { type: "object", required: ["info", "parts"], properties: { info: { $ref: "#/components/schemas/AssistantMessage" }, parts: { type: "array", items: { $ref: "#/components/schemas/Part" } } } }
}

function isBuiltInErrorResponse(response: OpenApiResponse, name: "BadRequest" | "NotFound") {
  return response.description === name || response.content?.["application/json"]?.schema?.$ref === `#/components/schemas/EffectHttpApiError${name}`
}

function legacyErrorResponse(description: string, name: "BadRequestError" | "NotFoundError"): OpenApiResponse {
  return { description, content: { "application/json": { schema: { $ref: `#/components/schemas/${name}` } } } }
}

function normalizeParameter(param: OpenApiParameter, route: string) {
  if (!param.schema || typeof param.schema !== "object") return
  if (param.in === "path") { param.schema = pathParameterSchema(route, param.name) ?? stripOptionalNull(param.schema); return }
  if (param.in === "query") {
    const override = QueryParameterSchemas[`${route} ${param.name}` as keyof typeof QueryParameterSchemas]
    if (override) { param.schema = override; return }
    if (QueryNumberParameters.has(param.name)) { param.schema = { type: "number" }; return }
    if (QueryBooleanParameters.has(param.name)) { param.schema = { anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }] }; return }
  }
  param.schema = stripOptionalNull(param.schema)
}

function pathParameterSchema(route: string, name: string) {
  if (name in PathParameterSchemas) return PathParameterSchemas[name as keyof typeof PathParameterSchemas]
  if (name === "id" && route.startsWith("DELETE /experimental/workspace/")) return { type: "string", pattern: "^wrk.*" }
  if (name === "id" && route.startsWith("POST /experimental/workspace/")) return { type: "string", pattern: "^wrk.*" }
  if (name === "requestID" && route.startsWith("POST /permission/")) return { type: "string", pattern: "^per.*" }
  if (name === "requestID" && route.startsWith("POST /question/")) return { type: "string", pattern: "^que.*" }
}

export const PublicApi = SaeeolHttpApi.annotateMerge(
  OpenApi.annotations({
    title: "saeeol",
    version: "1.0.0",
    description: "saeeol api",
    transform: matchLegacyOpenApi,
  }),
)
