import { existsSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { readConfig } from '../core/config.ts'
import { logger } from '../core/logger.ts'
import { VERSION } from '../core/version.ts'
import { api } from './api/index.ts'
import { handleMcpRelay } from './mcp/relay.ts'
import { handleWireRequest } from './proxy/index.ts'
import { configureCostSpike } from './risk/cost-spike.ts'
import { initRoutesTable, onRoutesReload } from './routes/load.ts'
import { initRoutingTables } from './routing/load.ts'
import { primeObservedModels, seedFromRoutes } from './routing/seed.ts'
import { getDb } from './store/db.ts'
import { consumePendingRestore } from './update/backup.ts'
import { initWireWatcher } from './wire/watcher.ts'

const HERE = dirname(fileURLToPath(import.meta.url))
// In dev: src/daemon/ → ../../ui-dist
// In prod: dist/bin/  → ../../ui-dist
const UI_DIST = join(HERE, '..', '..', 'ui-dist')

const STARTED_AT = Date.now()

export async function startServer(opts: { port: number }): Promise<void> {
  const config = await readConfig()
  configureCostSpike(config.costSpikeLimits) // tune risk thresholds to the user's budget
  await initRoutesTable()
  await initRoutingTables() // load model registry + routing policy + secrets
  await primeObservedModels()
  void seedFromRoutes() // one seeded provider + harvested models per wire route
  onRoutesReload(() => void seedFromRoutes()) // keep them in sync
  await consumePendingRestore() // restore DB if a rollback queued one (before the DB is opened)
  await getDb() // open SQLite + run schema bootstrap
  await initWireWatcher() // watch wired files for drift

  const app = new Hono()

  app.get('/health', (c) =>
    c.json({
      status: 'ok',
      uptime: (Date.now() - STARTED_AT) / 1000,
      version: VERSION,
    }),
  )

  // REST API for the UI / CLI to query captured traces.
  app.route('/api', api)

  // Real reverse proxy with decoder dispatch.
  // Single path segment: the agent type. Body.model carries per-instance
  // dispatch (wrap-style: `thomas-openclaw-main`; OAuth: real model id
  // like `claude-opus-4-7`). Proxy looks up `findRoute(agent, modelId)`.
  //
  // CORS: agents that fetch through Thomas from a browser/Electron
  // renderer (Claude Desktop's model-discovery panel is the headline
  // case) trigger preflight on non-simple headers like `anthropic-version`
  // and `x-api-key`. Without a CORS response the renderer aborts with
  // net::ERR_FAILED before the GET is sent. Open the wire path to any
  // origin — localhost:9877 is already a user-controlled trust boundary
  // (anything that can reach it can already issue normal proxy calls).
  // Echo the origin so the response works for renderers that send
  // `credentials: include` (Allow-Origin: * is rejected in that mode).
  app.use(
    '/wire/*',
    cors({
      // `origin: '*'` + `credentials: true` → hono echoes the request's
      // Origin in Access-Control-Allow-Origin (since `*` literal is
      // invalid with credentials). Echo is what we want — credentials
      // are needed when the renderer sends `fetch(..., {credentials:'include'})`.
      origin: '*',
      allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
      // Leave allowHeaders/exposeHeaders empty: hono echoes the
      // preflight's Access-Control-Request-Headers verbatim. A literal
      // `*` would also fail under credentials.
      maxAge: 86400,
      credentials: true,
    }),
  )
  app.all('/wire/:agent/*', handleWireRequest)

  // MCP relay for http/sse MCP servers — forwards to the real upstream and
  // captures JSON-RPC frames. Distinct prefix from /wire/:agent/* (model
  // calls). Same open-CORS rationale (Claude Desktop is an Electron renderer).
  app.use(
    '/wire-mcp/*',
    cors({
      origin: '*',
      allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
      maxAge: 86400,
      credentials: true,
    }),
  )
  app.all('/wire-mcp/:agent/:server/*', handleMcpRelay)
  app.all('/wire-mcp/:agent/:server', handleMcpRelay)

  // Static UI bundle (built from internal/ui via `npm run ui:build`).
  // Registered last so it acts as the fallthrough.
  if (existsSync(UI_DIST)) {
    app.use('/*', serveStatic({ root: UI_DIST }))
  } else {
    logger.warn(`ui-dist not found at ${UI_DIST}; serving stub. Run \`npm run ui:build\` to fix.`)
    app.get('/', (c) => c.html(STUB_HTML))
  }

  await new Promise<void>((resolve) => {
    serve({ fetch: app.fetch, port: opts.port }, () => {
      resolve()
    })
  })
}

const STUB_HTML = `<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Thomas</title>
  <style>
    body { font: 14px/1.6 -apple-system, BlinkMacSystemFont, sans-serif;
           max-width: 640px; margin: 4em auto; padding: 0 1em; color: #222 }
    h1 { font-size: 1.4em; margin-bottom: 0 }
    .slug { color: #888; margin-top: 0.2em }
    code { background: #f4f4f4; padding: 1px 4px; border-radius: 3px }
  </style>
</head>
<body>
  <h1>Thomas</h1>
  <p class="slug">Get Thomas on the wire.</p>
  <p>UI bundle missing. Build it with <code>npm run ui:build</code>.</p>
  <p>Or use the CLI: <code>thomas list</code>, <code>thomas show &lt;run-id&gt;</code>.</p>
</body>
</html>`
