/**
 * desktop/src/embedded-server.ts — GLUE: start the reused backend server inside
 * the Electron main process.
 *
 * All pure logic lives in the sibling modules (port / server-config / shell);
 * this file only wires them to electron + the compiled backend under dist/. It
 * resolves a DEFINITE free port BEFORE loadConfig (the backend derives
 * allowedOrigins from the port — config.ts M1), then dynamically imports the
 * compiled ESM server (dist/server.js) and config (dist/config.js) by absolute
 * file URL. The specifiers are computed so esbuild leaves them external (never
 * bundling native node-pty). The Config type import from ../../src/types.js is
 * type-only and erased at build time — no runtime dependency on src/.
 */

import path from 'node:path'
import { pathToFileURL } from 'node:url'
import { app } from 'electron'
import type { Config } from '../../src/types.js'
import type { DesktopPrefs, EmbeddedServer } from './types.js'
import type { Logger } from './logger.js'
import { pickFreePort } from './port.js'
import { buildServerEnv } from './server-config.js'

/** Default listen port when the user hasn't pinned one (free-port fallback still applies). */
const DEFAULT_PORT = 3000
/** How long to wait for the probe of an already-running base app before giving up. */
const PROBE_TIMEOUT_MS = 1500

export interface EmbeddedServerDeps {
  readonly prefs: DesktopPrefs
  readonly logger: Logger
}

/** The runtime handle the compiled backend's startServer() returns. */
interface ServerHandle {
  close(): Promise<void>
}

/**
 * Probe whether a web-terminal base app is ALREADY serving on `port` (typically
 * the always-on tunnel daemon that owns :3000 + frpc). `GET /config/ui` returns a
 * small JSON object with `allowAutoMode` only from our backend, so it doubles as a
 * cheap "is this one of ours?" fingerprint. Any failure (nothing listening, wrong
 * shape, timeout) → false, and we spawn our own server as before.
 */
async function probeExistingBaseApp(port: number): Promise<boolean> {
  try {
    const res = await fetch(`http://127.0.0.1:${port}/config/ui`, {
      signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
    })
    if (!res.ok) return false
    const body: unknown = await res.json()
    return typeof body === 'object' && body !== null && 'allowAutoMode' in body
  } catch {
    return false
  }
}

/**
 * Start the embedded backend and return a lifecycle handle. Throws (after
 * logging) if the compiled server can't be imported or fails to start — the app
 * is useless without it, so callers must surface the failure, not swallow it.
 */
export async function startEmbeddedServer(deps: EmbeddedServerDeps): Promise<EmbeddedServer> {
  const { prefs, logger } = deps
  const preferred = prefs.port ?? DEFAULT_PORT

  // If a base app is already up on the preferred port (the always-on tunnel
  // daemon), RIDE it instead of spawning a duplicate on another port: one base app
  // then serves the desktop UI, LAN, and the frpc tunnel alike. close() is a no-op
  // because we don't own that process — quitting the window must not kill it.
  if (await probeExistingBaseApp(preferred)) {
    logger.info(`reusing base app already on http://127.0.0.1:${preferred} (tunnel daemon) — not spawning a duplicate`)
    return {
      port: preferred,
      bindHost: '127.0.0.1',
      allowedOrigins: [],
      close: async () => {},
    }
  }

  const port = await pickFreePort(preferred)
  const env = buildServerEnv({ prefs, port, platform: process.platform, env: process.env })

  // dist/ is a sibling of the desktop dir in dev; under resourcesPath when packaged.
  const base = app.isPackaged ? process.resourcesPath : path.join(app.getAppPath(), '..')
  const configUrl = pathToFileURL(path.join(base, 'dist', 'config.js')).href
  const serverUrl = pathToFileURL(path.join(base, 'dist', 'server.js')).href

  try {
    const { loadConfig } = (await import(configUrl)) as {
      loadConfig: (e: Record<string, string | undefined>) => Config
    }
    const { startServer } = (await import(serverUrl)) as {
      startServer: (c: Config) => ServerHandle
    }
    const cfg = loadConfig(env)
    const handle = startServer(cfg)
    logger.info(`embedded server listening on http://${cfg.bindHost}:${cfg.port}`)
    return {
      port,
      bindHost: cfg.bindHost,
      allowedOrigins: cfg.allowedOrigins,
      close: () => handle.close(),
    }
  } catch (err: unknown) {
    logger.error('failed to start embedded server', err)
    throw err
  }
}
