import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { GlobalBus } from "@/bus/global"
import { Log } from "../util/log"
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
import { Hono } from "hono"
import { cors } from "hono/cors"
import { stream, streamSSE } from "hono/streaming"
import { proxy } from "hono/proxy"
import { Session } from "../session"
import z from "zod"
import { Provider } from "../provider/provider"
import { filter, mapValues, sortBy, pipe } from "remeda"
import { NamedError } from "@opencode-ai/util/error"
import { ModelsDev } from "../provider/models"
import { Ripgrep } from "../file/ripgrep"
import { Config } from "../config/config"
import { File } from "../file"
import { LSP } from "../lsp"
import { Format } from "../format"
import { MessageV2 } from "../session/message-v2"
import { TuiRoute } from "./tui"
import { Permission } from "../permission"
import { Instance } from "../project/instance"
import { Vcs } from "../project/vcs"
import { Agent } from "../agent/agent"
import { Auth } from "../auth"
import { Command } from "../command"
import { ProviderAuth } from "../provider/auth"
import { Global } from "../global"
import { ProjectRoute } from "./project"
import { ToolRegistry } from "../tool/registry"
import { zodToJsonSchema } from "zod-to-json-schema"
import { SessionPrompt } from "../session/prompt"
import { SessionCompaction } from "../session/compaction"
import { SessionRevert } from "../session/revert"
import { lazy } from "../util/lazy"
import { Todo } from "../session/todo"
import { InstanceBootstrap } from "../project/bootstrap"
import { MCP } from "../mcp"
import { Storage } from "../storage/storage"
import type { ContentfulStatusCode } from "hono/utils/http-status"
import { TuiEvent } from "@/cli/cmd/tui/event"
import { Snapshot } from "@/snapshot"
import { SessionSummary } from "@/session/summary"
import { SessionStatus } from "@/session/status"
import { upgradeWebSocket, websocket } from "hono/bun"
import { errors } from "./error"
import { Pty } from "@/pty"

// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis.AI_SDK_LOG_WARNINGS = false

export namespace Server {
  const log = Log.create({ service: "server" })

  export const Event = {
    Connected: BusEvent.define("server.connected", z.object({})),
    Disposed: BusEvent.define("global.disposed", z.object({})),
  }

  const app = new Hono()
  export const App = lazy(() =>
    app
      .onError((err, c) => {
        log.error("failed", {
          error: err,
        })
        if (err instanceof NamedError) {
          let status: ContentfulStatusCode
          if (err instanceof Storage.NotFoundError) status = 404
          else if (err instanceof Provider.ModelNotFoundError) status = 400
          else status = 500
          return c.json(err.toObject(), { status })
        }
        const message = err instanceof Error && err.stack ? err.stack : err.toString()
        return c.json(new NamedError.Unknown({ message }).toObject(), {
          status: 500,
        })
      })
      .use(async (c, next) => {
        const skipLogging = c.req.path === "/log"
        if (!skipLogging) {
          log.info("request", {
            method: c.req.method,
            path: c.req.path,
          })
        }
        const timer = log.time("request", {
          method: c.req.method,
          path: c.req.path,
        })
        await next()
        if (!skipLogging) {
          timer.stop()
        }
      })
      .use(cors())
      .get(
        "/global/event",
        describeRoute({
          summary: "Get global events",
          description: "Subscribe to global events from the OpenCode system using server-sent events.",
          operationId: "global.event",
          responses: {
            200: {
              description: "Event stream",
              content: {
                "text/event-stream": {
                  schema: resolver(
                    z
                      .object({
                        directory: z.string(),
                        payload: BusEvent.payloads(),
                      })
                      .meta({
                        ref: "GlobalEvent",
                      }),
                  ),
                },
              },
            },
          },
        }),
        async (c) => {
          log.info("global event connected")
          return streamSSE(c, async (stream) => {
            stream.writeSSE({
              data: JSON.stringify({
                payload: {
                  type: "server.connected",
                  properties: {},
                },
              }),
            })
            async function handler(event: any) {
              await stream.writeSSE({
                data: JSON.stringify(event),
              })
            }
            GlobalBus.on("event", handler)

            // Send heartbeat every 30s to prevent WKWebView timeout (60s default)
            const heartbeat = setInterval(() => {
              stream.writeSSE({
                data: JSON.stringify({
                  payload: {
                    type: "server.heartbeat",
                    properties: {},
                  },
                }),
              })
            }, 30000)

            await new Promise<void>((resolve) => {
              stream.onAbort(() => {
                clearInterval(heartbeat)
                GlobalBus.off("event", handler)
                resolve()
                log.info("global event disconnected")
              })
            })
          })
        },
      )
      .post(
        "/global/dispose",
        describeRoute({
          summary: "Dispose instance",
          description: "Clean up and dispose all OpenCode instances, releasing all resources.",
          operationId: "global.dispose",
          responses: {
            200: {
              description: "Global disposed",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
          },
        }),
        async (c) => {
          await Instance.disposeAll()
          GlobalBus.emit("event", {
            directory: "global",
            payload: {
              type: Event.Disposed.type,
              properties: {},
            },
          })
          return c.json(true)
        },
      )
      .use(async (c, next) => {
        const directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
        return Instance.provide({
          directory,
          init: InstanceBootstrap,
          async fn() {
            return next()
          },
        })
      })
      .get(
        "/doc",
        openAPIRouteHandler(app, {
          documentation: {
            info: {
              title: "opencode",
              version: "0.0.3",
              description: "opencode api",
            },
            openapi: "3.1.1",
          },
        }),
      )
      .use(validator("query", z.object({ directory: z.string().optional() })))

      .route("/project", ProjectRoute)

      .get(
        "/pty",
        describeRoute({
          summary: "List PTY sessions",
          description: "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.",
          operationId: "pty.list",
          responses: {
            200: {
              description: "List of sessions",
              content: {
                "application/json": {
                  schema: resolver(Pty.Info.array()),
                },
              },
            },
          },
        }),
        async (c) => {
          return c.json(Pty.list())
        },
      )
      .post(
        "/pty",
        describeRoute({
          summary: "Create PTY session",
          description: "Create a new pseudo-terminal (PTY) session for running shell commands and processes.",
          operationId: "pty.create",
          responses: {
            200: {
              description: "Created session",
              content: {
                "application/json": {
                  schema: resolver(Pty.Info),
                },
              },
            },
            ...errors(400),
          },
        }),
        validator("json", Pty.CreateInput),
        async (c) => {
          const info = await Pty.create(c.req.valid("json"))
          return c.json(info)
        },
      )
      .get(
        "/pty/:ptyID",
        describeRoute({
          summary: "Get PTY session",
          description: "Retrieve detailed information about a specific pseudo-terminal (PTY) session.",
          operationId: "pty.get",
          responses: {
            200: {
              description: "Session info",
              content: {
                "application/json": {
                  schema: resolver(Pty.Info),
                },
              },
            },
            ...errors(404),
          },
        }),
        validator("param", z.object({ ptyID: z.string() })),
        async (c) => {
          const info = Pty.get(c.req.valid("param").ptyID)
          if (!info) {
            throw new Storage.NotFoundError({ message: "Session not found" })
          }
          return c.json(info)
        },
      )
      .put(
        "/pty/:ptyID",
        describeRoute({
          summary: "Update PTY session",
          description: "Update properties of an existing pseudo-terminal (PTY) session.",
          operationId: "pty.update",
          responses: {
            200: {
              description: "Updated session",
              content: {
                "application/json": {
                  schema: resolver(Pty.Info),
                },
              },
            },
            ...errors(400),
          },
        }),
        validator("param", z.object({ ptyID: z.string() })),
        validator("json", Pty.UpdateInput),
        async (c) => {
          const info = await Pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
          return c.json(info)
        },
      )
      .delete(
        "/pty/:ptyID",
        describeRoute({
          summary: "Remove PTY session",
          description: "Remove and terminate a specific pseudo-terminal (PTY) session.",
          operationId: "pty.remove",
          responses: {
            200: {
              description: "Session removed",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(404),
          },
        }),
        validator("param", z.object({ ptyID: z.string() })),
        async (c) => {
          await Pty.remove(c.req.valid("param").ptyID)
          return c.json(true)
        },
      )
      .get(
        "/pty/:ptyID/connect",
        describeRoute({
          summary: "Connect to PTY session",
          description:
            "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.",
          operationId: "pty.connect",
          responses: {
            200: {
              description: "Connected session",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(404),
          },
        }),
        validator("param", z.object({ ptyID: z.string() })),
        upgradeWebSocket((c) => {
          const id = c.req.param("ptyID")
          let handler: ReturnType<typeof Pty.connect>
          if (!Pty.get(id)) throw new Error("Session not found")
          return {
            onOpen(_event, ws) {
              handler = Pty.connect(id, ws)
            },
            onMessage(event) {
              handler?.onMessage(String(event.data))
            },
            onClose() {
              handler?.onClose()
            },
          }
        }),
      )

      .get(
        "/config",
        describeRoute({
          summary: "Get configuration",
          description: "Retrieve the current OpenCode configuration settings and preferences.",
          operationId: "config.get",
          responses: {
            200: {
              description: "Get config info",
              content: {
                "application/json": {
                  schema: resolver(Config.Info),
                },
              },
            },
          },
        }),
        async (c) => {
          return c.json(await Config.get())
        },
      )

      .patch(
        "/config",
        describeRoute({
          summary: "Update configuration",
          description: "Update OpenCode configuration settings and preferences.",
          operationId: "config.update",
          responses: {
            200: {
              description: "Successfully updated config",
              content: {
                "application/json": {
                  schema: resolver(Config.Info),
                },
              },
            },
            ...errors(400),
          },
        }),
        validator("json", Config.Info),
        async (c) => {
          const config = c.req.valid("json")
          await Config.update(config)
          return c.json(config)
        },
      )
      .get(
        "/experimental/tool/ids",
        describeRoute({
          summary: "List tool IDs",
          description:
            "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
          operationId: "tool.ids",
          responses: {
            200: {
              description: "Tool IDs",
              content: {
                "application/json": {
                  schema: resolver(z.array(z.string()).meta({ ref: "ToolIDs" })),
                },
              },
            },
            ...errors(400),
          },
        }),
        async (c) => {
          return c.json(await ToolRegistry.ids())
        },
      )
      .get(
        "/experimental/tool",
        describeRoute({
          summary: "List tools",
          description:
            "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.",
          operationId: "tool.list",
          responses: {
            200: {
              description: "Tools",
              content: {
                "application/json": {
                  schema: resolver(
                    z
                      .array(
                        z
                          .object({
                            id: z.string(),
                            description: z.string(),
                            parameters: z.any(),
                          })
                          .meta({ ref: "ToolListItem" }),
                      )
                      .meta({ ref: "ToolList" }),
                  ),
                },
              },
            },
            ...errors(400),
          },
        }),
        validator(
          "query",
          z.object({
            provider: z.string(),
            model: z.string(),
          }),
        ),
        async (c) => {
          const { provider } = c.req.valid("query")
          const tools = await ToolRegistry.tools(provider)
          return c.json(
            tools.map((t) => ({
              id: t.id,
              description: t.description,
              // Handle both Zod schemas and plain JSON schemas
              parameters: (t.parameters as any)?._def ? zodToJsonSchema(t.parameters as any) : t.parameters,
            })),
          )
        },
      )
      .post(
        "/instance/dispose",
        describeRoute({
          summary: "Dispose instance",
          description: "Clean up and dispose the current OpenCode instance, releasing all resources.",
          operationId: "instance.dispose",
          responses: {
            200: {
              description: "Instance disposed",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
          },
        }),
        async (c) => {
          await Instance.dispose()
          return c.json(true)
        },
      )
      .get(
        "/path",
        describeRoute({
          summary: "Get paths",
          description: "Retrieve the current working directory and related path information for the OpenCode instance.",
          operationId: "path.get",
          responses: {
            200: {
              description: "Path",
              content: {
                "application/json": {
                  schema: resolver(
                    z
                      .object({
                        home: z.string(),
                        state: z.string(),
                        config: z.string(),
                        worktree: z.string(),
                        directory: z.string(),
                      })
                      .meta({
                        ref: "Path",
                      }),
                  ),
                },
              },
            },
          },
        }),
        async (c) => {
          return c.json({
            home: Global.Path.home,
            state: Global.Path.state,
            config: Global.Path.config,
            worktree: Instance.worktree,
            directory: Instance.directory,
          })
        },
      )
      .get(
        "/vcs",
        describeRoute({
          summary: "Get VCS info",
          description: "Retrieve version control system (VCS) information for the current project, such as git branch.",
          operationId: "vcs.get",
          responses: {
            200: {
              description: "VCS info",
              content: {
                "application/json": {
                  schema: resolver(Vcs.Info),
                },
              },
            },
          },
        }),
        async (c) => {
          const branch = await Vcs.branch()
          return c.json({
            branch,
          })
        },
      )
      .get(
        "/session",
        describeRoute({
          summary: "List sessions",
          description: "Get a list of all OpenCode sessions, sorted by most recently updated.",
          operationId: "session.list",
          responses: {
            200: {
              description: "List of sessions",
              content: {
                "application/json": {
                  schema: resolver(Session.Info.array()),
                },
              },
            },
          },
        }),
        async (c) => {
          const sessions = await Array.fromAsync(Session.list())
          pipe(
            await Array.fromAsync(Session.list()),
            filter((s) => !s.time.archived),
            sortBy((s) => s.time.updated),
          )
          return c.json(sessions)
        },
      )
      .get(
        "/session/status",
        describeRoute({
          summary: "Get session status",
          description: "Retrieve the current status of all sessions, including active, idle, and completed states.",
          operationId: "session.status",
          responses: {
            200: {
              description: "Get session status",
              content: {
                "application/json": {
                  schema: resolver(z.record(z.string(), SessionStatus.Info)),
                },
              },
            },
            ...errors(400),
          },
        }),
        async (c) => {
          const result = SessionStatus.list()
          return c.json(result)
        },
      )
      .get(
        "/session/:sessionID",
        describeRoute({
          summary: "Get session",
          description: "Retrieve detailed information about a specific OpenCode session.",
          tags: ["Session"],
          operationId: "session.get",
          responses: {
            200: {
              description: "Get session",
              content: {
                "application/json": {
                  schema: resolver(Session.Info),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: Session.get.schema,
          }),
        ),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          log.info("SEARCH", { url: c.req.url })
          const session = await Session.get(sessionID)
          return c.json(session)
        },
      )
      .get(
        "/session/:sessionID/children",
        describeRoute({
          summary: "Get session children",
          tags: ["Session"],
          description: "Retrieve all child sessions that were forked from the specified parent session.",
          operationId: "session.children",
          responses: {
            200: {
              description: "List of children",
              content: {
                "application/json": {
                  schema: resolver(Session.Info.array()),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: Session.children.schema,
          }),
        ),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          const session = await Session.children(sessionID)
          return c.json(session)
        },
      )
      .get(
        "/session/:sessionID/todo",
        describeRoute({
          summary: "Get session todos",
          description: "Retrieve the todo list associated with a specific session, showing tasks and action items.",
          operationId: "session.todo",
          responses: {
            200: {
              description: "Todo list",
              content: {
                "application/json": {
                  schema: resolver(Todo.Info.array()),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string().meta({ description: "Session ID" }),
          }),
        ),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          const todos = await Todo.get(sessionID)
          return c.json(todos)
        },
      )
      .post(
        "/session",
        describeRoute({
          summary: "Create session",
          description: "Create a new OpenCode session for interacting with AI assistants and managing conversations.",
          operationId: "session.create",
          responses: {
            ...errors(400),
            200: {
              description: "Successfully created session",
              content: {
                "application/json": {
                  schema: resolver(Session.Info),
                },
              },
            },
          },
        }),
        validator("json", Session.create.schema.optional()),
        async (c) => {
          const body = c.req.valid("json") ?? {}
          const session = await Session.create(body)
          return c.json(session)
        },
      )
      .delete(
        "/session/:sessionID",
        describeRoute({
          summary: "Delete session",
          description: "Delete a session and permanently remove all associated data, including messages and history.",
          operationId: "session.delete",
          responses: {
            200: {
              description: "Successfully deleted session",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: Session.remove.schema,
          }),
        ),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          await Session.remove(sessionID)
          return c.json(true)
        },
      )
      .patch(
        "/session/:sessionID",
        describeRoute({
          summary: "Update session",
          description: "Update properties of an existing session, such as title or other metadata.",
          operationId: "session.update",
          responses: {
            200: {
              description: "Successfully updated session",
              content: {
                "application/json": {
                  schema: resolver(Session.Info),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string(),
          }),
        ),
        validator(
          "json",
          z.object({
            title: z.string().optional(),
            time: z
              .object({
                archived: z.number().optional(),
              })
              .optional(),
          }),
        ),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          const updates = c.req.valid("json")

          const updatedSession = await Session.update(sessionID, (session) => {
            if (updates.title !== undefined) {
              session.title = updates.title
            }
            if (updates.time?.archived !== undefined) session.time.archived = updates.time.archived
          })

          return c.json(updatedSession)
        },
      )
      .post(
        "/session/:sessionID/init",
        describeRoute({
          summary: "Initialize session",
          description:
            "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.",
          operationId: "session.init",
          responses: {
            200: {
              description: "200",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string().meta({ description: "Session ID" }),
          }),
        ),
        validator("json", Session.initialize.schema.omit({ sessionID: true })),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          const body = c.req.valid("json")
          await Session.initialize({ ...body, sessionID })
          return c.json(true)
        },
      )
      .post(
        "/session/:sessionID/fork",
        describeRoute({
          summary: "Fork session",
          description: "Create a new session by forking an existing session at a specific message point.",
          operationId: "session.fork",
          responses: {
            200: {
              description: "200",
              content: {
                "application/json": {
                  schema: resolver(Session.Info),
                },
              },
            },
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: Session.fork.schema.shape.sessionID,
          }),
        ),
        validator("json", Session.fork.schema.omit({ sessionID: true })),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          const body = c.req.valid("json")
          const result = await Session.fork({ ...body, sessionID })
          return c.json(result)
        },
      )
      .post(
        "/session/:sessionID/abort",
        describeRoute({
          summary: "Abort session",
          description: "Abort an active session and stop any ongoing AI processing or command execution.",
          operationId: "session.abort",
          responses: {
            200: {
              description: "Aborted session",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string(),
          }),
        ),
        async (c) => {
          SessionPrompt.cancel(c.req.valid("param").sessionID)
          return c.json(true)
        },
      )
      .post(
        "/session/:sessionID/share",
        describeRoute({
          summary: "Share session",
          description: "Create a shareable link for a session, allowing others to view the conversation.",
          operationId: "session.share",
          responses: {
            200: {
              description: "Successfully shared session",
              content: {
                "application/json": {
                  schema: resolver(Session.Info),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string(),
          }),
        ),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          await Session.share(sessionID)
          const session = await Session.get(sessionID)
          return c.json(session)
        },
      )
      .get(
        "/session/:sessionID/diff",
        describeRoute({
          summary: "Get message diff",
          description: "Get the file changes (diff) that resulted from a specific user message in the session.",
          operationId: "session.diff",
          responses: {
            200: {
              description: "Successfully retrieved diff",
              content: {
                "application/json": {
                  schema: resolver(Snapshot.FileDiff.array()),
                },
              },
            },
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: SessionSummary.diff.schema.shape.sessionID,
          }),
        ),
        validator(
          "query",
          z.object({
            messageID: SessionSummary.diff.schema.shape.messageID,
          }),
        ),
        async (c) => {
          const query = c.req.valid("query")
          const params = c.req.valid("param")
          const result = await SessionSummary.diff({
            sessionID: params.sessionID,
            messageID: query.messageID,
          })
          return c.json(result)
        },
      )
      .delete(
        "/session/:sessionID/share",
        describeRoute({
          summary: "Unshare session",
          description: "Remove the shareable link for a session, making it private again.",
          operationId: "session.unshare",
          responses: {
            200: {
              description: "Successfully unshared session",
              content: {
                "application/json": {
                  schema: resolver(Session.Info),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: Session.unshare.schema,
          }),
        ),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          await Session.unshare(sessionID)
          const session = await Session.get(sessionID)
          return c.json(session)
        },
      )
      .post(
        "/session/:sessionID/summarize",
        describeRoute({
          summary: "Summarize session",
          description: "Generate a concise summary of the session using AI compaction to preserve key information.",
          operationId: "session.summarize",
          responses: {
            200: {
              description: "Summarized session",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string().meta({ description: "Session ID" }),
          }),
        ),
        validator(
          "json",
          z.object({
            providerID: z.string(),
            modelID: z.string(),
            auto: z.boolean().optional().default(false),
          }),
        ),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          const body = c.req.valid("json")
          const msgs = await Session.messages({ sessionID })
          let currentAgent = await Agent.defaultAgent()
          for (let i = msgs.length - 1; i >= 0; i--) {
            const info = msgs[i].info
            if (info.role === "user") {
              currentAgent = info.agent || (await Agent.defaultAgent())
              break
            }
          }
          await SessionCompaction.create({
            sessionID,
            agent: currentAgent,
            model: {
              providerID: body.providerID,
              modelID: body.modelID,
            },
            auto: body.auto,
          })
          await SessionPrompt.loop(sessionID)
          return c.json(true)
        },
      )
      .get(
        "/session/:sessionID/message",
        describeRoute({
          summary: "Get session messages",
          description: "Retrieve all messages in a session, including user prompts and AI responses.",
          operationId: "session.messages",
          responses: {
            200: {
              description: "List of messages",
              content: {
                "application/json": {
                  schema: resolver(MessageV2.WithParts.array()),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string().meta({ description: "Session ID" }),
          }),
        ),
        validator(
          "query",
          z.object({
            limit: z.coerce.number().optional(),
          }),
        ),
        async (c) => {
          const query = c.req.valid("query")
          const messages = await Session.messages({
            sessionID: c.req.valid("param").sessionID,
            limit: query.limit,
          })
          return c.json(messages)
        },
      )
      .get(
        "/session/:sessionID/diff",
        describeRoute({
          summary: "Get session diff",
          description: "Get all file changes (diffs) made during this session.",
          operationId: "session.diff",
          responses: {
            200: {
              description: "List of diffs",
              content: {
                "application/json": {
                  schema: resolver(Snapshot.FileDiff.array()),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string().meta({ description: "Session ID" }),
          }),
        ),
        async (c) => {
          const diff = await Session.diff(c.req.valid("param").sessionID)
          return c.json(diff)
        },
      )
      .get(
        "/session/:sessionID/message/:messageID",
        describeRoute({
          summary: "Get message",
          description: "Retrieve a specific message from a session by its message ID.",
          operationId: "session.message",
          responses: {
            200: {
              description: "Message",
              content: {
                "application/json": {
                  schema: resolver(
                    z.object({
                      info: MessageV2.Info,
                      parts: MessageV2.Part.array(),
                    }),
                  ),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string().meta({ description: "Session ID" }),
            messageID: z.string().meta({ description: "Message ID" }),
          }),
        ),
        async (c) => {
          const params = c.req.valid("param")
          const message = await MessageV2.get({
            sessionID: params.sessionID,
            messageID: params.messageID,
          })
          return c.json(message)
        },
      )
      .delete(
        "/session/:sessionID/message/:messageID/part/:partID",
        describeRoute({
          description: "Delete a part from a message",
          operationId: "part.delete",
          responses: {
            200: {
              description: "Successfully deleted part",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string().meta({ description: "Session ID" }),
            messageID: z.string().meta({ description: "Message ID" }),
            partID: z.string().meta({ description: "Part ID" }),
          }),
        ),
        async (c) => {
          const params = c.req.valid("param")
          await Session.removePart({
            sessionID: params.sessionID,
            messageID: params.messageID,
            partID: params.partID,
          })
          return c.json(true)
        },
      )
      .patch(
        "/session/:sessionID/message/:messageID/part/:partID",
        describeRoute({
          description: "Update a part in a message",
          operationId: "part.update",
          responses: {
            200: {
              description: "Successfully updated part",
              content: {
                "application/json": {
                  schema: resolver(MessageV2.Part),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string().meta({ description: "Session ID" }),
            messageID: z.string().meta({ description: "Message ID" }),
            partID: z.string().meta({ description: "Part ID" }),
          }),
        ),
        validator("json", MessageV2.Part),
        async (c) => {
          const params = c.req.valid("param")
          const body = c.req.valid("json")
          if (body.id !== params.partID || body.messageID !== params.messageID || body.sessionID !== params.sessionID) {
            throw new Error(
              `Part mismatch: body.id='${body.id}' vs partID='${params.partID}', body.messageID='${body.messageID}' vs messageID='${params.messageID}', body.sessionID='${body.sessionID}' vs sessionID='${params.sessionID}'`,
            )
          }
          const part = await Session.updatePart(body)
          return c.json(part)
        },
      )
      .post(
        "/session/:sessionID/message",
        describeRoute({
          summary: "Send message",
          description: "Create and send a new message to a session, streaming the AI response.",
          operationId: "session.prompt",
          responses: {
            200: {
              description: "Created message",
              content: {
                "application/json": {
                  schema: resolver(
                    z.object({
                      info: MessageV2.Assistant,
                      parts: MessageV2.Part.array(),
                    }),
                  ),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string().meta({ description: "Session ID" }),
          }),
        ),
        validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
        async (c) => {
          c.status(200)
          c.header("Content-Type", "application/json")
          return stream(c, async (stream) => {
            const sessionID = c.req.valid("param").sessionID
            const body = c.req.valid("json")
            const msg = await SessionPrompt.prompt({ ...body, sessionID })
            stream.write(JSON.stringify(msg))
          })
        },
      )
      .post(
        "/session/:sessionID/prompt_async",
        describeRoute({
          summary: "Send async message",
          description:
            "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.",
          operationId: "session.prompt_async",
          responses: {
            204: {
              description: "Prompt accepted",
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string().meta({ description: "Session ID" }),
          }),
        ),
        validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
        async (c) => {
          c.status(204)
          c.header("Content-Type", "application/json")
          return stream(c, async () => {
            const sessionID = c.req.valid("param").sessionID
            const body = c.req.valid("json")
            SessionPrompt.prompt({ ...body, sessionID })
          })
        },
      )
      .post(
        "/session/:sessionID/command",
        describeRoute({
          summary: "Send command",
          description: "Send a new command to a session for execution by the AI assistant.",
          operationId: "session.command",
          responses: {
            200: {
              description: "Created message",
              content: {
                "application/json": {
                  schema: resolver(
                    z.object({
                      info: MessageV2.Assistant,
                      parts: MessageV2.Part.array(),
                    }),
                  ),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string().meta({ description: "Session ID" }),
          }),
        ),
        validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          const body = c.req.valid("json")
          const msg = await SessionPrompt.command({ ...body, sessionID })
          return c.json(msg)
        },
      )
      .post(
        "/session/:sessionID/shell",
        describeRoute({
          summary: "Run shell command",
          description: "Execute a shell command within the session context and return the AI's response.",
          operationId: "session.shell",
          responses: {
            200: {
              description: "Created message",
              content: {
                "application/json": {
                  schema: resolver(MessageV2.Assistant),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string().meta({ description: "Session ID" }),
          }),
        ),
        validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          const body = c.req.valid("json")
          const msg = await SessionPrompt.shell({ ...body, sessionID })
          return c.json(msg)
        },
      )
      .post(
        "/session/:sessionID/revert",
        describeRoute({
          summary: "Revert message",
          description: "Revert a specific message in a session, undoing its effects and restoring the previous state.",
          operationId: "session.revert",
          responses: {
            200: {
              description: "Updated session",
              content: {
                "application/json": {
                  schema: resolver(Session.Info),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string(),
          }),
        ),
        validator("json", SessionRevert.RevertInput.omit({ sessionID: true })),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          log.info("revert", c.req.valid("json"))
          const session = await SessionRevert.revert({
            sessionID,
            ...c.req.valid("json"),
          })
          return c.json(session)
        },
      )
      .post(
        "/session/:sessionID/unrevert",
        describeRoute({
          summary: "Restore reverted messages",
          description: "Restore all previously reverted messages in a session.",
          operationId: "session.unrevert",
          responses: {
            200: {
              description: "Updated session",
              content: {
                "application/json": {
                  schema: resolver(Session.Info),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string(),
          }),
        ),
        async (c) => {
          const sessionID = c.req.valid("param").sessionID
          const session = await SessionRevert.unrevert({ sessionID })
          return c.json(session)
        },
      )
      .post(
        "/session/:sessionID/permissions/:permissionID",
        describeRoute({
          summary: "Respond to permission",
          description: "Approve or deny a permission request from the AI assistant.",
          operationId: "permission.respond",
          responses: {
            200: {
              description: "Permission processed successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "param",
          z.object({
            sessionID: z.string(),
            permissionID: z.string(),
          }),
        ),
        validator("json", z.object({ response: Permission.Response })),
        async (c) => {
          const params = c.req.valid("param")
          const sessionID = params.sessionID
          const permissionID = params.permissionID
          Permission.respond({
            sessionID,
            permissionID,
            response: c.req.valid("json").response,
          })
          return c.json(true)
        },
      )
      .get(
        "/command",
        describeRoute({
          summary: "List commands",
          description: "Get a list of all available commands in the OpenCode system.",
          operationId: "command.list",
          responses: {
            200: {
              description: "List of commands",
              content: {
                "application/json": {
                  schema: resolver(Command.Info.array()),
                },
              },
            },
          },
        }),
        async (c) => {
          const commands = await Command.list()
          return c.json(commands)
        },
      )
      .get(
        "/config/providers",
        describeRoute({
          summary: "List config providers",
          description: "Get a list of all configured AI providers and their default models.",
          operationId: "config.providers",
          responses: {
            200: {
              description: "List of providers",
              content: {
                "application/json": {
                  schema: resolver(
                    z.object({
                      providers: Provider.Info.array(),
                      default: z.record(z.string(), z.string()),
                    }),
                  ),
                },
              },
            },
          },
        }),
        async (c) => {
          using _ = log.time("providers")
          const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
          return c.json({
            providers: Object.values(providers),
            default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
          })
        },
      )
      .get(
        "/provider",
        describeRoute({
          summary: "List providers",
          description: "Get a list of all available AI providers, including both available and connected ones.",
          operationId: "provider.list",
          responses: {
            200: {
              description: "List of providers",
              content: {
                "application/json": {
                  schema: resolver(
                    z.object({
                      all: ModelsDev.Provider.array(),
                      default: z.record(z.string(), z.string()),
                      connected: z.array(z.string()),
                    }),
                  ),
                },
              },
            },
          },
        }),
        async (c) => {
          const config = await Config.get()
          const disabled = new Set(config.disabled_providers ?? [])
          const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined

          const allProviders = await ModelsDev.get()
          const filteredProviders: Record<string, (typeof allProviders)[string]> = {}
          for (const [key, value] of Object.entries(allProviders)) {
            if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
              filteredProviders[key] = value
            }
          }

          const connected = await Provider.list()
          const providers = Object.assign(
            mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x)),
            connected,
          )
          return c.json({
            all: Object.values(providers),
            default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
            connected: Object.keys(connected),
          })
        },
      )
      .get(
        "/provider/auth",
        describeRoute({
          summary: "Get provider auth methods",
          description: "Retrieve available authentication methods for all AI providers.",
          operationId: "provider.auth",
          responses: {
            200: {
              description: "Provider auth methods",
              content: {
                "application/json": {
                  schema: resolver(z.record(z.string(), z.array(ProviderAuth.Method))),
                },
              },
            },
          },
        }),
        async (c) => {
          return c.json(await ProviderAuth.methods())
        },
      )
      .post(
        "/provider/:providerID/oauth/authorize",
        describeRoute({
          summary: "OAuth authorize",
          description: "Initiate OAuth authorization for a specific AI provider to get an authorization URL.",
          operationId: "provider.oauth.authorize",
          responses: {
            200: {
              description: "Authorization URL and method",
              content: {
                "application/json": {
                  schema: resolver(ProviderAuth.Authorization.optional()),
                },
              },
            },
            ...errors(400),
          },
        }),
        validator(
          "param",
          z.object({
            providerID: z.string().meta({ description: "Provider ID" }),
          }),
        ),
        validator(
          "json",
          z.object({
            method: z.number().meta({ description: "Auth method index" }),
          }),
        ),
        async (c) => {
          const providerID = c.req.valid("param").providerID
          const { method } = c.req.valid("json")
          const result = await ProviderAuth.authorize({
            providerID,
            method,
          })
          return c.json(result)
        },
      )
      .post(
        "/provider/:providerID/oauth/callback",
        describeRoute({
          summary: "OAuth callback",
          description: "Handle the OAuth callback from a provider after user authorization.",
          operationId: "provider.oauth.callback",
          responses: {
            200: {
              description: "OAuth callback processed successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(400),
          },
        }),
        validator(
          "param",
          z.object({
            providerID: z.string().meta({ description: "Provider ID" }),
          }),
        ),
        validator(
          "json",
          z.object({
            method: z.number().meta({ description: "Auth method index" }),
            code: z.string().optional().meta({ description: "OAuth authorization code" }),
          }),
        ),
        async (c) => {
          const providerID = c.req.valid("param").providerID
          const { method, code } = c.req.valid("json")
          await ProviderAuth.callback({
            providerID,
            method,
            code,
          })
          return c.json(true)
        },
      )
      .get(
        "/find",
        describeRoute({
          summary: "Find text",
          description: "Search for text patterns across files in the project using ripgrep.",
          operationId: "find.text",
          responses: {
            200: {
              description: "Matches",
              content: {
                "application/json": {
                  schema: resolver(Ripgrep.Match.shape.data.array()),
                },
              },
            },
          },
        }),
        validator(
          "query",
          z.object({
            pattern: z.string(),
          }),
        ),
        async (c) => {
          const pattern = c.req.valid("query").pattern
          const result = await Ripgrep.search({
            cwd: Instance.directory,
            pattern,
            limit: 10,
          })
          return c.json(result)
        },
      )
      .get(
        "/find/file",
        describeRoute({
          summary: "Find files",
          description: "Search for files by name or pattern in the project directory.",
          operationId: "find.files",
          responses: {
            200: {
              description: "File paths",
              content: {
                "application/json": {
                  schema: resolver(z.string().array()),
                },
              },
            },
          },
        }),
        validator(
          "query",
          z.object({
            query: z.string(),
            dirs: z.enum(["true", "false"]).optional(),
          }),
        ),
        async (c) => {
          const query = c.req.valid("query").query
          const dirs = c.req.valid("query").dirs
          const results = await File.search({
            query,
            limit: 10,
            dirs: dirs !== "false",
          })
          return c.json(results)
        },
      )
      .get(
        "/find/symbol",
        describeRoute({
          summary: "Find symbols",
          description: "Search for workspace symbols like functions, classes, and variables using LSP.",
          operationId: "find.symbols",
          responses: {
            200: {
              description: "Symbols",
              content: {
                "application/json": {
                  schema: resolver(LSP.Symbol.array()),
                },
              },
            },
          },
        }),
        validator(
          "query",
          z.object({
            query: z.string(),
          }),
        ),
        async (c) => {
          /*
          const query = c.req.valid("query").query
          const result = await LSP.workspaceSymbol(query)
          return c.json(result)
          */
          return c.json([])
        },
      )
      .get(
        "/file",
        describeRoute({
          summary: "List files",
          description: "List files and directories in a specified path.",
          operationId: "file.list",
          responses: {
            200: {
              description: "Files and directories",
              content: {
                "application/json": {
                  schema: resolver(File.Node.array()),
                },
              },
            },
          },
        }),
        validator(
          "query",
          z.object({
            path: z.string(),
          }),
        ),
        async (c) => {
          const path = c.req.valid("query").path
          const content = await File.list(path)
          return c.json(content)
        },
      )
      .get(
        "/file/content",
        describeRoute({
          summary: "Read file",
          description: "Read the content of a specified file.",
          operationId: "file.read",
          responses: {
            200: {
              description: "File content",
              content: {
                "application/json": {
                  schema: resolver(File.Content),
                },
              },
            },
          },
        }),
        validator(
          "query",
          z.object({
            path: z.string(),
          }),
        ),
        async (c) => {
          const path = c.req.valid("query").path
          const content = await File.read(path)
          return c.json(content)
        },
      )
      .get(
        "/file/status",
        describeRoute({
          summary: "Get file status",
          description: "Get the git status of all files in the project.",
          operationId: "file.status",
          responses: {
            200: {
              description: "File status",
              content: {
                "application/json": {
                  schema: resolver(File.Info.array()),
                },
              },
            },
          },
        }),
        async (c) => {
          const content = await File.status()
          return c.json(content)
        },
      )
      .post(
        "/log",
        describeRoute({
          summary: "Write log",
          description: "Write a log entry to the server logs with specified level and metadata.",
          operationId: "app.log",
          responses: {
            200: {
              description: "Log entry written successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(400),
          },
        }),
        validator(
          "json",
          z.object({
            service: z.string().meta({ description: "Service name for the log entry" }),
            level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
            message: z.string().meta({ description: "Log message" }),
            extra: z
              .record(z.string(), z.any())
              .optional()
              .meta({ description: "Additional metadata for the log entry" }),
          }),
        ),
        async (c) => {
          const { service, level, message, extra } = c.req.valid("json")
          const logger = Log.create({ service })

          switch (level) {
            case "debug":
              logger.debug(message, extra)
              break
            case "info":
              logger.info(message, extra)
              break
            case "error":
              logger.error(message, extra)
              break
            case "warn":
              logger.warn(message, extra)
              break
          }

          return c.json(true)
        },
      )
      .get(
        "/agent",
        describeRoute({
          summary: "List agents",
          description: "Get a list of all available AI agents in the OpenCode system.",
          operationId: "app.agents",
          responses: {
            200: {
              description: "List of agents",
              content: {
                "application/json": {
                  schema: resolver(Agent.Info.array()),
                },
              },
            },
          },
        }),
        async (c) => {
          const modes = await Agent.list()
          return c.json(modes)
        },
      )
      .get(
        "/mcp",
        describeRoute({
          summary: "Get MCP status",
          description: "Get the status of all Model Context Protocol (MCP) servers.",
          operationId: "mcp.status",
          responses: {
            200: {
              description: "MCP server status",
              content: {
                "application/json": {
                  schema: resolver(z.record(z.string(), MCP.Status)),
                },
              },
            },
          },
        }),
        async (c) => {
          return c.json(await MCP.status())
        },
      )
      .post(
        "/mcp",
        describeRoute({
          summary: "Add MCP server",
          description: "Dynamically add a new Model Context Protocol (MCP) server to the system.",
          operationId: "mcp.add",
          responses: {
            200: {
              description: "MCP server added successfully",
              content: {
                "application/json": {
                  schema: resolver(z.record(z.string(), MCP.Status)),
                },
              },
            },
            ...errors(400),
          },
        }),
        validator(
          "json",
          z.object({
            name: z.string(),
            config: Config.Mcp,
          }),
        ),
        async (c) => {
          const { name, config } = c.req.valid("json")
          const result = await MCP.add(name, config)
          return c.json(result.status)
        },
      )
      .post(
        "/mcp/:name/auth",
        describeRoute({
          summary: "Start MCP OAuth",
          description: "Start OAuth authentication flow for a Model Context Protocol (MCP) server.",
          operationId: "mcp.auth.start",
          responses: {
            200: {
              description: "OAuth flow started",
              content: {
                "application/json": {
                  schema: resolver(
                    z.object({
                      authorizationUrl: z.string().describe("URL to open in browser for authorization"),
                    }),
                  ),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        async (c) => {
          const name = c.req.param("name")
          const supportsOAuth = await MCP.supportsOAuth(name)
          if (!supportsOAuth) {
            return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
          }
          const result = await MCP.startAuth(name)
          return c.json(result)
        },
      )
      .post(
        "/mcp/:name/auth/callback",
        describeRoute({
          summary: "Complete MCP OAuth",
          description:
            "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.",
          operationId: "mcp.auth.callback",
          responses: {
            200: {
              description: "OAuth authentication completed",
              content: {
                "application/json": {
                  schema: resolver(MCP.Status),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        validator(
          "json",
          z.object({
            code: z.string().describe("Authorization code from OAuth callback"),
          }),
        ),
        async (c) => {
          const name = c.req.param("name")
          const { code } = c.req.valid("json")
          const status = await MCP.finishAuth(name, code)
          return c.json(status)
        },
      )
      .post(
        "/mcp/:name/auth/authenticate",
        describeRoute({
          summary: "Authenticate MCP OAuth",
          description: "Start OAuth flow and wait for callback (opens browser)",
          operationId: "mcp.auth.authenticate",
          responses: {
            200: {
              description: "OAuth authentication completed",
              content: {
                "application/json": {
                  schema: resolver(MCP.Status),
                },
              },
            },
            ...errors(400, 404),
          },
        }),
        async (c) => {
          const name = c.req.param("name")
          const supportsOAuth = await MCP.supportsOAuth(name)
          if (!supportsOAuth) {
            return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
          }
          const status = await MCP.authenticate(name)
          return c.json(status)
        },
      )
      .delete(
        "/mcp/:name/auth",
        describeRoute({
          summary: "Remove MCP OAuth",
          description: "Remove OAuth credentials for an MCP server",
          operationId: "mcp.auth.remove",
          responses: {
            200: {
              description: "OAuth credentials removed",
              content: {
                "application/json": {
                  schema: resolver(z.object({ success: z.literal(true) })),
                },
              },
            },
            ...errors(404),
          },
        }),
        async (c) => {
          const name = c.req.param("name")
          await MCP.removeAuth(name)
          return c.json({ success: true as const })
        },
      )
      .post(
        "/mcp/:name/connect",
        describeRoute({
          description: "Connect an MCP server",
          operationId: "mcp.connect",
          responses: {
            200: {
              description: "MCP server connected successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
          },
        }),
        validator("param", z.object({ name: z.string() })),
        async (c) => {
          const { name } = c.req.valid("param")
          await MCP.connect(name)
          return c.json(true)
        },
      )
      .post(
        "/mcp/:name/disconnect",
        describeRoute({
          description: "Disconnect an MCP server",
          operationId: "mcp.disconnect",
          responses: {
            200: {
              description: "MCP server disconnected successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
          },
        }),
        validator("param", z.object({ name: z.string() })),
        async (c) => {
          const { name } = c.req.valid("param")
          await MCP.disconnect(name)
          return c.json(true)
        },
      )
      .get(
        "/lsp",
        describeRoute({
          summary: "Get LSP status",
          description: "Get LSP server status",
          operationId: "lsp.status",
          responses: {
            200: {
              description: "LSP server status",
              content: {
                "application/json": {
                  schema: resolver(LSP.Status.array()),
                },
              },
            },
          },
        }),
        async (c) => {
          return c.json(await LSP.status())
        },
      )
      .get(
        "/formatter",
        describeRoute({
          summary: "Get formatter status",
          description: "Get formatter status",
          operationId: "formatter.status",
          responses: {
            200: {
              description: "Formatter status",
              content: {
                "application/json": {
                  schema: resolver(Format.Status.array()),
                },
              },
            },
          },
        }),
        async (c) => {
          return c.json(await Format.status())
        },
      )
      .post(
        "/tui/append-prompt",
        describeRoute({
          summary: "Append TUI prompt",
          description: "Append prompt to the TUI",
          operationId: "tui.appendPrompt",
          responses: {
            200: {
              description: "Prompt processed successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(400),
          },
        }),
        validator("json", TuiEvent.PromptAppend.properties),
        async (c) => {
          await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"))
          return c.json(true)
        },
      )
      .post(
        "/tui/open-help",
        describeRoute({
          summary: "Open help dialog",
          description: "Open the help dialog in the TUI to display user assistance information.",
          operationId: "tui.openHelp",
          responses: {
            200: {
              description: "Help dialog opened successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
          },
        }),
        async (c) => {
          // TODO: open dialog
          return c.json(true)
        },
      )
      .post(
        "/tui/open-sessions",
        describeRoute({
          summary: "Open sessions dialog",
          description: "Open the session dialog",
          operationId: "tui.openSessions",
          responses: {
            200: {
              description: "Session dialog opened successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
          },
        }),
        async (c) => {
          await Bus.publish(TuiEvent.CommandExecute, {
            command: "session.list",
          })
          return c.json(true)
        },
      )
      .post(
        "/tui/open-themes",
        describeRoute({
          summary: "Open themes dialog",
          description: "Open the theme dialog",
          operationId: "tui.openThemes",
          responses: {
            200: {
              description: "Theme dialog opened successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
          },
        }),
        async (c) => {
          await Bus.publish(TuiEvent.CommandExecute, {
            command: "session.list",
          })
          return c.json(true)
        },
      )
      .post(
        "/tui/open-models",
        describeRoute({
          summary: "Open models dialog",
          description: "Open the model dialog",
          operationId: "tui.openModels",
          responses: {
            200: {
              description: "Model dialog opened successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
          },
        }),
        async (c) => {
          await Bus.publish(TuiEvent.CommandExecute, {
            command: "model.list",
          })
          return c.json(true)
        },
      )
      .post(
        "/tui/submit-prompt",
        describeRoute({
          summary: "Submit TUI prompt",
          description: "Submit the prompt",
          operationId: "tui.submitPrompt",
          responses: {
            200: {
              description: "Prompt submitted successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
          },
        }),
        async (c) => {
          await Bus.publish(TuiEvent.CommandExecute, {
            command: "prompt.submit",
          })
          return c.json(true)
        },
      )
      .post(
        "/tui/clear-prompt",
        describeRoute({
          summary: "Clear TUI prompt",
          description: "Clear the prompt",
          operationId: "tui.clearPrompt",
          responses: {
            200: {
              description: "Prompt cleared successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
          },
        }),
        async (c) => {
          await Bus.publish(TuiEvent.CommandExecute, {
            command: "prompt.clear",
          })
          return c.json(true)
        },
      )
      .post(
        "/tui/execute-command",
        describeRoute({
          summary: "Execute TUI command",
          description: "Execute a TUI command (e.g. agent_cycle)",
          operationId: "tui.executeCommand",
          responses: {
            200: {
              description: "Command executed successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(400),
          },
        }),
        validator("json", z.object({ command: z.string() })),
        async (c) => {
          const command = c.req.valid("json").command
          await Bus.publish(TuiEvent.CommandExecute, {
            // @ts-expect-error
            command: {
              session_new: "session.new",
              session_share: "session.share",
              session_interrupt: "session.interrupt",
              session_compact: "session.compact",
              messages_page_up: "session.page.up",
              messages_page_down: "session.page.down",
              messages_half_page_up: "session.half.page.up",
              messages_half_page_down: "session.half.page.down",
              messages_first: "session.first",
              messages_last: "session.last",
              agent_cycle: "agent.cycle",
            }[command],
          })
          return c.json(true)
        },
      )
      .post(
        "/tui/show-toast",
        describeRoute({
          summary: "Show TUI toast",
          description: "Show a toast notification in the TUI",
          operationId: "tui.showToast",
          responses: {
            200: {
              description: "Toast notification shown successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
          },
        }),
        validator("json", TuiEvent.ToastShow.properties),
        async (c) => {
          await Bus.publish(TuiEvent.ToastShow, c.req.valid("json"))
          return c.json(true)
        },
      )
      .post(
        "/tui/publish",
        describeRoute({
          summary: "Publish TUI event",
          description: "Publish a TUI event",
          operationId: "tui.publish",
          responses: {
            200: {
              description: "Event published successfully",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(400),
          },
        }),
        validator(
          "json",
          z.union(
            Object.values(TuiEvent).map((def) => {
              return z
                .object({
                  type: z.literal(def.type),
                  properties: def.properties,
                })
                .meta({
                  ref: "Event" + "." + def.type,
                })
            }),
          ),
        ),
        async (c) => {
          const evt = c.req.valid("json")
          await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)!, evt.properties)
          return c.json(true)
        },
      )
      .route("/tui/control", TuiRoute)
      .put(
        "/auth/:providerID",
        describeRoute({
          summary: "Set auth credentials",
          description: "Set authentication credentials",
          operationId: "auth.set",
          responses: {
            200: {
              description: "Successfully set authentication credentials",
              content: {
                "application/json": {
                  schema: resolver(z.boolean()),
                },
              },
            },
            ...errors(400),
          },
        }),
        validator(
          "param",
          z.object({
            providerID: z.string(),
          }),
        ),
        validator("json", Auth.Info),
        async (c) => {
          const providerID = c.req.valid("param").providerID
          const info = c.req.valid("json")
          await Auth.set(providerID, info)
          return c.json(true)
        },
      )
      .get(
        "/event",
        describeRoute({
          summary: "Subscribe to events",
          description: "Get events",
          operationId: "event.subscribe",
          responses: {
            200: {
              description: "Event stream",
              content: {
                "text/event-stream": {
                  schema: resolver(BusEvent.payloads()),
                },
              },
            },
          },
        }),
        async (c) => {
          log.info("event connected")
          return streamSSE(c, async (stream) => {
            stream.writeSSE({
              data: JSON.stringify({
                type: "server.connected",
                properties: {},
              }),
            })
            const unsub = Bus.subscribeAll(async (event) => {
              await stream.writeSSE({
                data: JSON.stringify(event),
              })
              if (event.type === Bus.InstanceDisposed.type) {
                stream.close()
              }
            })

            // Send heartbeat every 30s to prevent WKWebView timeout (60s default)
            const heartbeat = setInterval(() => {
              stream.writeSSE({
                data: JSON.stringify({
                  type: "server.heartbeat",
                  properties: {},
                }),
              })
            }, 30000)

            await new Promise<void>((resolve) => {
              stream.onAbort(() => {
                clearInterval(heartbeat)
                unsub()
                resolve()
                log.info("event disconnected")
              })
            })
          })
        },
      )
      .all("/*", async (c) => {
        return proxy(`https://desktop.opencode.ai${c.req.path}`, {
          ...c.req,
          headers: {
            host: "desktop.opencode.ai",
          },
        })
      }),
  )

  export async function openapi() {
    const result = await generateSpecs(App(), {
      documentation: {
        info: {
          title: "opencode",
          version: "1.0.0",
          description: "opencode api",
        },
        openapi: "3.1.1",
      },
    })
    return result
  }

  export function listen(opts: { port: number; hostname: string }) {
    const args = {
      hostname: opts.hostname,
      idleTimeout: 0,
      fetch: App().fetch,
      websocket: websocket,
    } as const
    if (opts.port === 0) {
      try {
        return Bun.serve({ ...args, port: 4096 })
      } catch {
        // port 4096 not available, fall through to use port 0
      }
    }
    return Bun.serve({ ...args, port: opts.port })
  }
}
