import { runHttpServer } from "../application/http-server";
import {
  normalizeServeBindHost,
  serveBindTokenRequiredMessage,
} from "../application/serve-bind-policy";
import {
  applyWatchPolicy,
  envWatchDefaultOn,
} from "../application/watch-policy";
import { DEFAULT_DEBOUNCE_MS } from "../application/watcher";
import { CODEMAP_VERSION } from "../version";

/**
 * Default loopback bind: `127.0.0.1` (refuse `0.0.0.0` unless the user
 * explicitly passes `--host 0.0.0.0`). See [`docs/architecture.md` § HTTP wiring](../../docs/architecture.md#cli-usage).
 */
export const DEFAULT_HOST = "127.0.0.1";
export const DEFAULT_PORT = 7878;

export interface ServeRunOpts {
  host: string;
  port: number;
  /** Bearer token; if undefined the server skips auth. */
  token: string | undefined;
  /** Boot an in-process file watcher so tools always read live data. */
  watch: boolean;
  /** Coalesce burst events into one reindex after `debounceMs` of quiet (only meaningful with `watch: true`). */
  debounceMs: number;
}

/**
 * Parse `argv` after the global bootstrap: `rest[0]` must be `"serve"`.
 * Flags: `--port <n>`, `--host <ip>`, `--token <secret>`. `--root` /
 * `--config` are absorbed by bootstrap.
 */
export function parseServeRest(rest: string[]):
  | { kind: "help" }
  | { kind: "error"; message: string }
  | {
      kind: "run";
      host: string;
      port: number;
      token: string | undefined;
      watch: boolean;
      debounceMs: number;
    } {
  if (rest[0] !== "serve") {
    throw new Error("parseServeRest: expected serve");
  }

  let host: string = DEFAULT_HOST;
  let port: number = DEFAULT_PORT;
  let token: string | undefined;
  // Default-ON watcher (PR adds default-ON; pre-flip default was OFF).
  // `serve` is inherently long-running — stale-index friction is the
  // most-frequent UX issue, so the watcher pays for itself immediately.
  // CODEMAP_WATCH=0 / "false" is the env shortcut to opt out (mirrors
  // --no-watch). CODEMAP_WATCH=1 / "true" is redundant after the default
  // flip but kept for backwards-compat.
  let watch = envWatchDefaultOn(process.env);
  let debounceMs = DEFAULT_DEBOUNCE_MS;

  for (let i = 1; i < rest.length; i++) {
    const a = rest[i]!;
    if (a === "--help" || a === "-h") return { kind: "help" };

    if (a === "--port" || a.startsWith("--port=")) {
      const eq = a.indexOf("=");
      const v = eq !== -1 ? a.slice(eq + 1) : rest[i + 1];
      if (v === undefined || v === "" || v.startsWith("-")) {
        return {
          kind: "error",
          message: 'codemap serve: "--port" requires a number (1-65535).',
        };
      }
      const n = Number(v);
      if (!Number.isInteger(n) || n < 1 || n > 65535) {
        return {
          kind: "error",
          message: `codemap serve: "--port ${v}" is not a valid port (1-65535).`,
        };
      }
      port = n;
      if (eq === -1) i++;
      continue;
    }

    if (a === "--host" || a.startsWith("--host=")) {
      const eq = a.indexOf("=");
      const v = eq !== -1 ? a.slice(eq + 1) : rest[i + 1];
      if (v === undefined || v === "" || v.startsWith("-")) {
        return {
          kind: "error",
          message:
            'codemap serve: "--host" requires an IP or hostname (default 127.0.0.1).',
        };
      }
      host = v;
      if (eq === -1) i++;
      continue;
    }

    if (a === "--token" || a.startsWith("--token=")) {
      const eq = a.indexOf("=");
      const v = eq !== -1 ? a.slice(eq + 1) : rest[i + 1];
      if (v === undefined || v === "" || v.startsWith("-")) {
        return {
          kind: "error",
          message:
            'codemap serve: "--token" requires a non-empty secret (use a long random string).',
        };
      }
      token = v;
      if (eq === -1) i++;
      continue;
    }

    if (a === "--watch") {
      // No-op after default-ON flip; kept so existing scripts/launch
      // commands that pass --watch explicitly still parse cleanly.
      watch = true;
      continue;
    }

    if (a === "--no-watch") {
      watch = false;
      continue;
    }

    if (a === "--debounce" || a.startsWith("--debounce=")) {
      const eq = a.indexOf("=");
      const v = eq !== -1 ? a.slice(eq + 1) : rest[i + 1];
      if (v === undefined || v === "" || v.startsWith("-")) {
        return {
          kind: "error",
          message:
            'codemap serve: "--debounce" requires a non-negative integer (milliseconds).',
        };
      }
      const n = Number(v);
      if (!Number.isInteger(n) || n < 0) {
        return {
          kind: "error",
          message: `codemap serve: "--debounce ${v}" is not a non-negative integer.`,
        };
      }
      debounceMs = n;
      if (eq === -1) i++;
      continue;
    }

    return {
      kind: "error",
      message: `codemap serve: unknown option "${a}". Run \`codemap serve --help\` for usage.`,
    };
  }

  host = normalizeServeBindHost(host);

  const bindMessage = serveBindTokenRequiredMessage(host, token);
  if (bindMessage !== undefined) {
    return { kind: "error", message: bindMessage };
  }

  return { kind: "run", host, port, token, watch, debounceMs };
}

export function printServeCmdHelp(): void {
  console.log(`Usage: codemap serve [--host <ip>] [--port <n>] [--token <secret>]

Spawn an HTTP server exposing the same tool taxonomy as \`codemap mcp\` over
\`POST /tool/{name}\` for non-MCP consumers (CI scripts, simple curl, IDE
plugins that don't speak MCP). Single project root per server (set via
--root / CODEMAP_ROOT).

Default bind: 127.0.0.1:${DEFAULT_PORT} (loopback only — refuse 0.0.0.0 unless
explicitly opted in via --host 0.0.0.0). Any 127.0.0.0/8 address (e.g.
127.0.0.2) is treated as loopback for the --token requirement.

Flags:
  --host <ip>     Bind address (default: ${DEFAULT_HOST}; 127.* is loopback).
  --port <n>      Bind port (default: ${DEFAULT_PORT}).
  --token <secret>
                  Require Authorization: Bearer <secret> on every request.
                  Mandatory when binding a non-loopback address (e.g.
                  --host 0.0.0.0). GET /health is exempt so liveness probes
                  work without leaking the token. Use a long random string.
  --watch         [default ON] Boot an in-process file watcher so every
                  tool reads a live index — eliminates the per-request
                  reindex prelude. Default-ON since 2026-05; explicit
                  flag kept for backwards-compat with existing launch
                  scripts.
  --no-watch      Opt out of the default watcher. Use when you want
                  one-shot HTTP requests without the in-process chokidar
                  loop (CI scripts that fire-and-forget, ephemeral
                  indexes, etc.). Same effect as CODEMAP_WATCH=0 /
                  "false" in the environment.
  --debounce <ms> Coalesce burst events into one reindex after <ms> of
                  quiet (default: ${DEFAULT_DEBOUNCE_MS}). Applies when the
                  watcher is active (default; no-op with --no-watch).
  --help, -h      Show this help.

With --watch, chokidar starts on the first authenticated non-/health
request and stops 5s after the last request completes. That release
grace stops the watcher between stateless requests — not an idle shutdown
of the HTTP listener itself.

Routes (every MCP tool maps to POST /tool/<name>; HTTP returns each
tool's native JSON payload directly — NOT the MCP {content: [...]}
wrapper):
  POST /tool/query
  POST /tool/query_batch
  POST /tool/query_recipe
  POST /tool/audit
  POST /tool/save_baseline
  POST /tool/list_baselines
  POST /tool/drop_baseline
  POST /tool/context
  POST /tool/validate
  POST /tool/show
  POST /tool/snippet
  POST /tool/impact
  POST /tool/affected
  POST /tool/trace
  POST /tool/explore
  POST /tool/node
  POST /tool/apply
  GET  /health                        Liveness probe (auth-exempt).
  GET  /tools                         Tool catalog.
  GET  /resources                     Resource catalog.
  GET  /resources/{encoded-uri}       Mirror of MCP resources:
                                        codemap://recipes
                                        codemap://recipes/{id}
                                        codemap://schema
                                        codemap://skill
                                        codemap://rule
                                        codemap://mcp-instructions
                                        codemap://files/{path}
                                        codemap://symbols/{name}

Errors are JSON: {"error": "<msg>"} with HTTP status 400 (bad input)
/ 401 (auth) / 403 (cross-origin / DNS rebinding) / 404 (unknown
tool, recipe, or baseline) / 500 (engine fault).
Every response carries X-Codemap-Version: <semver>.

Examples:
  codemap serve
  codemap serve --port 9000 --token $(openssl rand -hex 32)
  curl -s -X POST http://127.0.0.1:${DEFAULT_PORT}/tool/query \\
    -H 'Content-Type: application/json' \\
    -d '{"sql":"SELECT name, file_path FROM symbols LIMIT 5"}'

The server runs until SIGINT/SIGTERM (drains in-flight + closes listener).
`);
}

/**
 * Entry-point for `codemap serve`. Bootstraps codemap once, starts the
 * HTTP listener, awaits SIGINT/SIGTERM. Errors propagate as exit code 1
 * via main.
 */
export async function runServeCmd(opts: {
  root: string;
  configFile: string | undefined;
  stateDir?: string | undefined;
  host: string;
  port: number;
  token: string | undefined;
  watch: boolean;
  debounceMs: number;
}): Promise<void> {
  const { watch } = applyWatchPolicy({
    root: opts.root,
    requestedWatch: opts.watch,
    label: "codemap serve",
  });
  await runHttpServer({
    version: CODEMAP_VERSION,
    root: opts.root,
    configFile: opts.configFile,
    stateDir: opts.stateDir,
    host: opts.host,
    port: opts.port,
    token: opts.token,
    watch,
    debounceMs: opts.debounceMs,
  });
}
