import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../../shared/src/rotating-jsonl";

type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
type JsonRecord = Record<string, JsonValue>;
type SandboxMode = "full-isolation" | "half-isolation" | "bridge";
type AdapterName = "codex" | "opencode" | "future-agent";
type AgentStatus = "idle" | "starting" | "attached" | "prompting" | "steering" | "interrupted" | "resumed" | "forked" | "finished" | "failed";
type TerminalStatus = "completed" | "interrupted" | "failed" | "unknown";

interface RuntimeConfig {
  host: string;
  port: number;
  dataDir: string;
  logFile: string;
  stateFile: string;
  adapterSummaryFile: string;
  envFilePath: string;
  configTomlPath: string;
  authJsonPath: string;
  sandboxMode: SandboxMode;
  tokenProvider: string;
  tokenProviderSwitchAllowed: boolean;
  configWritable: boolean;
  authWritable: boolean;
  envWritable: boolean;
  adapterNames: AdapterName[];
  defaultAdapter: AdapterName;
  maxPromptChars: number;
  maxTraceItems: number;
  deployServiceId: string;
}

interface AdapterState {
  name: AdapterName;
  status: AgentStatus;
  terminalStatus: TerminalStatus;
  startedAt: string | null;
  updatedAt: string | null;
  threadId: string | null;
  turnId: string | null;
  promptCount: number;
  steerCount: number;
  interruptCount: number;
  resumeCount: number;
  forkCount: number;
  lastPrompt: string | null;
  lastSteer: string | null;
  lastArtifactSummary: JsonRecord | null;
  notes: string[];
}

interface SandboxSnapshot {
  serviceId: string;
  serviceName: string;
  mode: SandboxMode;
  tokenProvider: string;
  tokenProviderSwitchAllowed: boolean;
  configWritable: boolean;
  authWritable: boolean;
  envWritable: boolean;
  configTomlText: string;
  authJsonText: string;
  envVars: Record<string, string>;
  adapters: Record<string, AdapterState>;
  createdAt: string;
  updatedAt: string;
}

const startedAt = new Date().toISOString();
const recentLogs: JsonRecord[] = [];

function envString(name: string, fallback: string): string {
  const value = process.env[name];
  return value === undefined || value.length === 0 ? fallback : value;
}

function envNumber(name: string, fallback: number): number {
  const raw = process.env[name];
  if (raw === undefined || raw.trim().length === 0) return fallback;
  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
  return Math.floor(parsed);
}

function envBoolean(name: string, fallback: boolean): boolean {
  const raw = process.env[name];
  if (raw === undefined || raw.trim().length === 0) return fallback;
  return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
}

function parseSandboxMode(value: string): SandboxMode {
  const normalized = value.trim().toLowerCase();
  if (normalized === "full-isolation" || normalized === "full" || normalized === "isolated") return "full-isolation";
  if (normalized === "half-isolation" || normalized === "half" || normalized === "semi-isolated") return "half-isolation";
  if (normalized === "bridge" || normalized === "bridged") return "bridge";
  throw new Error(`unsupported CODE_AGENT_SANDBOX_MODE: ${value}`);
}

function parseAdapterNames(value: string): AdapterName[] {
  const names = value.split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
  const allowed: AdapterName[] = [];
  for (const name of names) {
    if (name === "codex" || name === "opencode" || name === "future-agent") {
      allowed.push(name);
      continue;
    }
    throw new Error(`unsupported adapter name: ${name}`);
  }
  return Array.from(new Set(allowed));
}

function buildConfig(): RuntimeConfig {
  const port = envNumber("PORT", 4260);
  const adapterNames = parseAdapterNames(envString("CODE_AGENT_SANDBOX_ADAPTERS", "codex,opencode,future-agent"));
  const defaultAdapter = envString("CODE_AGENT_SANDBOX_DEFAULT_ADAPTER", "codex").trim().toLowerCase() as AdapterName;
  if (!adapterNames.includes(defaultAdapter)) {
    throw new Error(`CODE_AGENT_SANDBOX_DEFAULT_ADAPTER must be one of: ${adapterNames.join(", ")}`);
  }
  return {
    host: envString("HOST", "0.0.0.0"),
    port,
    dataDir: envString("CODE_AGENT_SANDBOX_DATA_DIR", "/var/lib/unidesk/code-agent-sandbox"),
    logFile: envString("LOG_FILE", "/var/log/unidesk/code-agent-sandbox.jsonl"),
    stateFile: envString("CODE_AGENT_SANDBOX_STATE_FILE", "/var/lib/unidesk/code-agent-sandbox/state.json"),
    adapterSummaryFile: envString("CODE_AGENT_SANDBOX_ADAPTER_SUMMARY_FILE", "/var/lib/unidesk/code-agent-sandbox/adapters.json"),
    envFilePath: envString("CODE_AGENT_SANDBOX_ENV_FILE_PATH", "/var/lib/unidesk/code-agent-sandbox/env.json"),
    configTomlPath: envString("CODE_AGENT_SANDBOX_CONFIG_TOML_PATH", "/var/lib/unidesk/code-agent-sandbox/config.toml"),
    authJsonPath: envString("CODE_AGENT_SANDBOX_AUTH_JSON_PATH", "/var/lib/unidesk/code-agent-sandbox/auth.json"),
    sandboxMode: parseSandboxMode(envString("CODE_AGENT_SANDBOX_MODE", "half-isolation")),
    tokenProvider: envString("CODE_AGENT_SANDBOX_TOKEN_PROVIDER", "codex"),
    tokenProviderSwitchAllowed: envBoolean("CODE_AGENT_SANDBOX_TOKEN_PROVIDER_SWITCH_ALLOWED", true),
    configWritable: envBoolean("CODE_AGENT_SANDBOX_CONFIG_WRITABLE", true),
    authWritable: envBoolean("CODE_AGENT_SANDBOX_AUTH_WRITABLE", true),
    envWritable: envBoolean("CODE_AGENT_SANDBOX_ENV_WRITABLE", true),
    adapterNames,
    defaultAdapter,
    maxPromptChars: envNumber("CODE_AGENT_SANDBOX_MAX_PROMPT_CHARS", 12_000),
    maxTraceItems: envNumber("CODE_AGENT_SANDBOX_MAX_TRACE_ITEMS", 200),
    deployServiceId: envString("UNIDESK_DEPLOY_SERVICE_ID", "code-agent-sandbox"),
  };
}

const config = buildConfig();
let logWriter: ReturnType<typeof createHourlyJsonlWriter> | null = null;

function getLogWriter(): ReturnType<typeof createHourlyJsonlWriter> {
  if (logWriter === null) {
    mkdirSync(config.dataDir, { recursive: true });
    logWriter = createHourlyJsonlWriter({
      baseLogFile: config.logFile,
      service: "code-agent-sandbox",
      maxBytes: logRetentionBytesForService("code-agent-sandbox"),
    });
    logWriter.prune();
  }
  return logWriter;
}

function log(event: string, detail: JsonRecord = {}): void {
  const record: JsonRecord = { at: new Date().toISOString(), service: "code-agent-sandbox", event, ...detail };
  recentLogs.push(record);
  while (recentLogs.length > 500) recentLogs.shift();
  try {
    getLogWriter().appendJson(record, new Date(String(record.at)));
  } catch {
    // Logging must not break the sandbox control plane.
  }
  console.log(JSON.stringify(record));
}

function jsonResponse(body: unknown, status = 200, headers: Record<string, string> = {}): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { "content-type": "application/json; charset=utf-8", ...headers },
  });
}

function errorToJson(error: unknown): JsonRecord {
  if (error instanceof Error) return { name: error.name, message: error.message, stack: error.stack ?? "" };
  return { message: String(error) };
}

function errorResponse(error: unknown, status = 500): Response {
  log("request_failed", { status, error: errorToJson(error) });
  return jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, status);
}

function clampText(value: string, maxChars: number): string {
  const compact = value.replace(/\s+/gu, " ").trim();
  return compact.length <= maxChars ? compact : `${compact.slice(0, Math.max(0, maxChars - 12))}…<truncated>`;
}

function nowIso(): string {
  return new Date().toISOString();
}

function readTextOrEmpty(path: string): string {
  if (!existsSync(path)) return "";
  try {
    return readFileSync(path, "utf8");
  } catch {
    return "";
  }
}

function readJsonObjectOrEmpty(path: string): Record<string, string> {
  const raw = readTextOrEmpty(path).trim();
  if (raw.length === 0) return {};
  try {
    const parsed = JSON.parse(raw) as unknown;
    if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
    return Object.fromEntries(Object.entries(parsed).flatMap(([key, value]) => typeof value === "string" ? [[key, value]] : []));
  } catch {
    return {};
  }
}

function loadState(): SandboxSnapshot {
  try {
    if (!existsSync(config.stateFile)) throw new Error("state file missing");
    const parsed = JSON.parse(readFileSync(config.stateFile, "utf8")) as unknown;
    if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("state file is not an object");
    const record = parsed as Record<string, unknown>;
    const adapters = typeof record.adapters === "object" && record.adapters !== null && !Array.isArray(record.adapters)
      ? record.adapters as Record<string, unknown>
      : {};
    const fresh = freshSnapshot();
    return {
      serviceId: String(record.serviceId ?? fresh.serviceId),
      serviceName: String(record.serviceName ?? fresh.serviceName),
      mode: parseSandboxMode(String(record.mode ?? fresh.mode)),
      tokenProvider: String(record.tokenProvider ?? fresh.tokenProvider),
      tokenProviderSwitchAllowed: typeof record.tokenProviderSwitchAllowed === "boolean"
        ? record.tokenProviderSwitchAllowed
        : fresh.tokenProviderSwitchAllowed,
      configWritable: typeof record.configWritable === "boolean" ? record.configWritable : fresh.configWritable,
      authWritable: typeof record.authWritable === "boolean" ? record.authWritable : fresh.authWritable,
      envWritable: typeof record.envWritable === "boolean" ? record.envWritable : fresh.envWritable,
      configTomlText: typeof record.configTomlText === "string" ? record.configTomlText : readTextOrEmpty(config.configTomlPath),
      authJsonText: typeof record.authJsonText === "string" ? record.authJsonText : readTextOrEmpty(config.authJsonPath),
      envVars: normalizeEnvVars(record.envVars, readJsonObjectOrEmpty(config.envFilePath)),
      adapters: Object.fromEntries(config.adapterNames.map((name) => [name, normalizeAdapterState(name, adapters[name] ?? fresh.adapters[name])])),
      createdAt: String(record.createdAt ?? fresh.createdAt),
      updatedAt: String(record.updatedAt ?? fresh.updatedAt),
    };
  } catch {
    return freshSnapshot();
  }
}

function normalizeAdapterState(name: string, value: unknown): AdapterState {
  const record = typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
  return {
    name: name as AdapterName,
    status: (record.status as AgentStatus) ?? "idle",
    terminalStatus: (record.terminalStatus as TerminalStatus) ?? "unknown",
    startedAt: typeof record.startedAt === "string" ? record.startedAt : null,
    updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : null,
    threadId: typeof record.threadId === "string" ? record.threadId : null,
    turnId: typeof record.turnId === "string" ? record.turnId : null,
    promptCount: Number(record.promptCount ?? 0) || 0,
    steerCount: Number(record.steerCount ?? 0) || 0,
    interruptCount: Number(record.interruptCount ?? 0) || 0,
    resumeCount: Number(record.resumeCount ?? 0) || 0,
    forkCount: Number(record.forkCount ?? 0) || 0,
    lastPrompt: typeof record.lastPrompt === "string" ? record.lastPrompt : null,
    lastSteer: typeof record.lastSteer === "string" ? record.lastSteer : null,
    lastArtifactSummary: typeof record.lastArtifactSummary === "object" && record.lastArtifactSummary !== null && !Array.isArray(record.lastArtifactSummary)
      ? record.lastArtifactSummary as JsonRecord
      : null,
    notes: Array.isArray(record.notes) ? record.notes.filter((item): item is string => typeof item === "string") : [],
  };
}

function freshSnapshot(): SandboxSnapshot {
  const adapters = Object.fromEntries(config.adapterNames.map((name) => [name, freshAdapterState(name)]));
  return {
    serviceId: config.deployServiceId || "code-agent-sandbox",
    serviceName: "Code Agent Sandbox",
    mode: config.sandboxMode,
    tokenProvider: config.tokenProvider,
    tokenProviderSwitchAllowed: config.tokenProviderSwitchAllowed,
    configWritable: config.configWritable,
    authWritable: config.authWritable,
    envWritable: config.envWritable,
    configTomlText: "",
    authJsonText: "{}",
    envVars: {},
    adapters,
    createdAt: startedAt,
    updatedAt: startedAt,
  };
}

function freshAdapterState(name: AdapterName): AdapterState {
  return {
    name,
    status: "idle",
    terminalStatus: "unknown",
    startedAt: null,
    updatedAt: null,
    threadId: null,
    turnId: null,
    promptCount: 0,
    steerCount: 0,
    interruptCount: 0,
    resumeCount: 0,
    forkCount: 0,
    lastPrompt: null,
    lastSteer: null,
    lastArtifactSummary: null,
    notes: [],
  };
}

let state = freshSnapshot();

function persistState(): void {
  state.updatedAt = nowIso();
  mkdirSync(dirname(config.stateFile), { recursive: true });
  mkdirSync(dirname(config.adapterSummaryFile), { recursive: true });
  writeFileSync(config.stateFile, `${JSON.stringify(state, null, 2)}\n`, "utf8");
  writeFileSync(config.adapterSummaryFile, `${JSON.stringify({ updatedAt: state.updatedAt, adapters: state.adapters }, null, 2)}\n`, "utf8");
  if (state.mode !== "bridge" && state.configWritable) {
    mkdirSync(dirname(config.configTomlPath), { recursive: true });
    writeFileSync(config.configTomlPath, state.configTomlText, "utf8");
  }
  if (state.mode !== "bridge" && state.authWritable) {
    mkdirSync(dirname(config.authJsonPath), { recursive: true });
    writeFileSync(config.authJsonPath, `${state.authJsonText}\n`, "utf8");
  }
  if (state.mode !== "bridge" && state.envWritable) {
    mkdirSync(dirname(config.envFilePath), { recursive: true });
    writeFileSync(config.envFilePath, `${JSON.stringify(state.envVars, null, 2)}\n`, "utf8");
  }
}

function selectAdapter(name: string | null | undefined): AdapterState {
  const chosen = (name ?? config.defaultAdapter) as AdapterName;
  if (!config.adapterNames.includes(chosen)) throw new Error(`adapter not enabled: ${chosen}`);
  const adapter = state.adapters[chosen];
  if (adapter === undefined) throw new Error(`adapter state missing: ${chosen}`);
  return adapter;
}

function adapterSummary(): JsonRecord {
  return {
    ok: true,
    serviceId: state.serviceId,
    mode: state.mode,
    tokenProvider: state.tokenProvider,
    tokenProviderSwitchAllowed: state.tokenProviderSwitchAllowed,
    configWritable: state.configWritable,
    authWritable: state.authWritable,
    envWritable: state.envWritable,
    adapterNames: config.adapterNames,
    defaultAdapter: config.defaultAdapter,
    adapters: Object.values(state.adapters).map((adapter) => ({
      name: adapter.name,
      status: adapter.status,
      terminalStatus: adapter.terminalStatus,
      threadId: adapter.threadId,
      turnId: adapter.turnId,
      promptCount: adapter.promptCount,
      steerCount: adapter.steerCount,
      interruptCount: adapter.interruptCount,
      resumeCount: adapter.resumeCount,
      forkCount: adapter.forkCount,
      lastPromptPreview: adapter.lastPrompt ? clampText(adapter.lastPrompt, 240) : null,
      lastSteerPreview: adapter.lastSteer ? clampText(adapter.lastSteer, 240) : null,
      notes: adapter.notes.slice(-5),
      lastArtifactSummary: adapter.lastArtifactSummary,
    })),
  };
}

function serviceHealth(): JsonRecord {
  return {
    ok: true,
    status: "healthy",
    serviceId: state.serviceId,
    serviceName: state.serviceName,
    mode: state.mode,
    startedAt,
    updatedAt: state.updatedAt,
    tokenProvider: state.tokenProvider,
    tokenProviderSwitchAllowed: state.tokenProviderSwitchAllowed,
    credentialBoundary: {
      fullIsolation: {
        configTomlWritable: true,
        authJsonWritable: true,
        envWritable: true,
        tokenProviderSwitchAllowed: true,
      },
      halfIsolation: {
        configTomlWritable: state.configWritable,
        authJsonWritable: state.authWritable,
        envWritable: state.envWritable,
        tokenProviderSwitchAllowed: state.tokenProviderSwitchAllowed,
      },
      bridge: {
        configTomlWritable: false,
        authJsonWritable: false,
        envWritable: false,
        tokenProviderSwitchAllowed: false,
      },
    },
  };
}

function diagnostics(): JsonRecord {
  return {
    ok: true,
    serviceId: state.serviceId,
    mode: state.mode,
    adapterNames: config.adapterNames,
    defaultAdapter: config.defaultAdapter,
    credentialBoundary: {
      configTomlPath: config.configTomlPath,
      authJsonPath: config.authJsonPath,
      configTomlWritable: state.mode === "bridge" ? false : state.configWritable,
      authJsonWritable: state.mode === "bridge" ? false : state.authWritable,
      envWritable: state.mode === "bridge" ? false : state.envWritable,
      tokenProvider: state.tokenProvider,
      tokenProviderSwitchAllowed: state.mode === "bridge" ? false : state.tokenProviderSwitchAllowed,
    },
    recoveryBoundary: {
      statusRestoreSupported: true,
      queuedTaskRerouteSupported: true,
      interruptedAttemptRetrySupported: true,
      liveMigrationSupported: false,
      resultLossTolerance: "none",
    },
    adapterSummary: adapterSummary().adapters,
    notes: [
      "Code Queue is not a dependency of this sandbox service.",
      "Adapters are contract-first stubs; integration is delegated to follow-up tasks.",
    ],
  };
}

function trace(): JsonRecord {
  return {
    ok: true,
    serviceId: state.serviceId,
    generatedAt: nowIso(),
    limit: config.maxTraceItems,
    terminalStatus: Object.values(state.adapters).reduce<Record<string, TerminalStatus>>((acc, adapter) => {
      acc[adapter.name] = adapter.terminalStatus;
      return acc;
    }, {}),
    adapters: Object.values(state.adapters).slice(0, config.maxTraceItems).map((adapter) => ({
      name: adapter.name,
      status: adapter.status,
      terminalStatus: adapter.terminalStatus,
      startedAt: adapter.startedAt,
      updatedAt: adapter.updatedAt,
      threadId: adapter.threadId,
      turnId: adapter.turnId,
      promptCount: adapter.promptCount,
      steerCount: adapter.steerCount,
      interruptCount: adapter.interruptCount,
      resumeCount: adapter.resumeCount,
      forkCount: adapter.forkCount,
      lastPromptPreview: adapter.lastPrompt ? clampText(adapter.lastPrompt, 120) : null,
      lastSteerPreview: adapter.lastSteer ? clampText(adapter.lastSteer, 120) : null,
      lastArtifactSummary: adapter.lastArtifactSummary,
      notes: adapter.notes.slice(-10),
    })),
  };
}

function recordAdapterMutation(adapter: AdapterState, status: AgentStatus, note: string): void {
  adapter.status = status;
  adapter.updatedAt = nowIso();
  adapter.notes.push(note);
  if (adapter.notes.length > 50) adapter.notes.splice(0, adapter.notes.length - 50);
  persistState();
}

function parseBody(body: unknown): Record<string, unknown> {
  if (typeof body === "object" && body !== null && !Array.isArray(body)) return body as Record<string, unknown>;
  return {};
}

function normalizeEnvVars(value: unknown, fallback: Record<string, string>): Record<string, string> {
  if (typeof value !== "object" || value === null || Array.isArray(value)) return { ...fallback };
  const result: Record<string, string> = {};
  for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
    if (typeof raw === "string") result[key] = raw;
  }
  return result;
}

function adapterAction(action: string, body: Record<string, unknown>): JsonRecord {
  const adapter = selectAdapter(typeof body.adapter === "string" ? body.adapter : null);
  const mode: SandboxMode = state.mode;
  const prompt = typeof body.prompt === "string" ? body.prompt : null;
  const steerPrompt = typeof body.steerPrompt === "string" ? body.steerPrompt : null;
  const artifact = typeof body.artifactSummary === "object" && body.artifactSummary !== null && !Array.isArray(body.artifactSummary)
    ? body.artifactSummary as JsonRecord
    : null;

  switch (action) {
    case "start":
      adapter.startedAt = nowIso();
      adapter.threadId = adapter.threadId ?? `sandbox-${adapter.name}-${Date.now()}`;
      adapter.turnId = null;
      adapter.promptCount += 1;
      adapter.lastPrompt = prompt;
      recordAdapterMutation(adapter, "starting", `start:${adapter.threadId}`);
      return { ok: true, action, adapter: adapter.name, threadId: adapter.threadId, terminalStatus: adapter.terminalStatus };
    case "attach":
      adapter.threadId = adapter.threadId ?? `attached-${adapter.name}-${Date.now()}`;
      adapter.turnId = adapter.turnId ?? `turn-${Date.now()}`;
      recordAdapterMutation(adapter, "attached", `attach:${adapter.threadId}`);
      return { ok: true, action, adapter: adapter.name, threadId: adapter.threadId, turnId: adapter.turnId };
    case "prompt":
      adapter.promptCount += 1;
      adapter.lastPrompt = prompt;
      adapter.turnId = adapter.turnId ?? `turn-${Date.now()}`;
      recordAdapterMutation(adapter, "prompting", `prompt:${clampText(prompt ?? "", 180)}`);
      return { ok: true, action, adapter: adapter.name, turnId: adapter.turnId, promptPreview: clampText(prompt ?? "", config.maxPromptChars) };
    case "steer":
      adapter.steerCount += 1;
      adapter.lastSteer = steerPrompt;
      recordAdapterMutation(adapter, "steering", `steer:${clampText(steerPrompt ?? "", 180)}`);
      return { ok: true, action, adapter: adapter.name, turnId: adapter.turnId, steerPreview: clampText(steerPrompt ?? "", config.maxPromptChars) };
    case "interrupt":
      adapter.interruptCount += 1;
      recordAdapterMutation(adapter, "interrupted", "interrupt");
      adapter.terminalStatus = "interrupted";
      persistState();
      return { ok: true, action, adapter: adapter.name, terminalStatus: adapter.terminalStatus };
    case "resume":
      adapter.resumeCount += 1;
      recordAdapterMutation(adapter, "resumed", "resume");
      adapter.terminalStatus = "unknown";
      persistState();
      return { ok: true, action, adapter: adapter.name, terminalStatus: adapter.terminalStatus };
    case "fork":
      adapter.forkCount += 1;
      recordAdapterMutation(adapter, "forked", "fork");
      return { ok: true, action, adapter: adapter.name, forkCount: adapter.forkCount };
    case "terminal-status":
      return {
        ok: true,
        action,
        adapter: adapter.name,
        terminalStatus: adapter.terminalStatus,
        status: adapter.status,
      };
    case "artifact-summary":
      adapter.lastArtifactSummary = artifact ?? {
        summary: "placeholder",
        source: "code-agent-sandbox",
        generatedAt: nowIso(),
      };
      recordAdapterMutation(adapter, "finished", "artifact-summary");
      return { ok: true, action, adapter: adapter.name, artifactSummary: adapter.lastArtifactSummary };
    case "config-toml":
      if (mode === "bridge") throw new Error("bridge mode is read-only for config.toml");
      if (!state.configWritable) throw new Error("config.toml writes are disabled");
      if (typeof body.text === "string") state.configTomlText = body.text;
      persistState();
      return { ok: true, action, path: config.configTomlPath, writable: state.configWritable, chars: state.configTomlText.length };
    case "auth-json":
      if (mode === "bridge") throw new Error("bridge mode is read-only for auth.json");
      if (!state.authWritable) throw new Error("auth.json writes are disabled");
      if (typeof body.text === "string") state.authJsonText = body.text;
      persistState();
      return { ok: true, action, path: config.authJsonPath, writable: state.authWritable, chars: state.authJsonText.length };
    case "env":
      if (mode === "bridge") throw new Error("bridge mode is read-only for env files");
      if (!state.envWritable) throw new Error("env writes are disabled");
      state.envVars = normalizeEnvVars(body.env, state.envVars);
      persistState();
      return { ok: true, action, path: config.envFilePath, writable: state.envWritable, keys: Object.keys(state.envVars).sort() };
    case "token-provider":
      if (mode === "bridge") throw new Error("bridge mode is read-only for token provider switching");
      if (!state.tokenProviderSwitchAllowed) throw new Error("token provider switching is disabled");
      state.tokenProvider = typeof body.provider === "string" && body.provider.length > 0 ? body.provider : state.tokenProvider;
      persistState();
      return { ok: true, action, tokenProvider: state.tokenProvider };
    case "trace":
      return trace();
    default:
      throw new Error(`unsupported adapter action: ${action}`);
  }
}

async function readJson(req: Request): Promise<Record<string, unknown>> {
  const text = await req.text();
  if (text.trim().length === 0) return {};
  const parsed = JSON.parse(text) as unknown;
  return parseBody(parsed);
}

async function route(req: Request): Promise<Response> {
  const url = new URL(req.url);
  if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/health")) return jsonResponse(serviceHealth());
  if (req.method === "GET" && url.pathname === "/diagnostics") return jsonResponse(diagnostics());
  if (req.method === "GET" && url.pathname === "/trace") return jsonResponse(trace());
  if (req.method === "GET" && url.pathname === "/artifacts") return jsonResponse({ ok: true, serviceId: state.serviceId, summary: adapterSummary() });
  if (req.method === "GET" && url.pathname === "/api/config/toml") return jsonResponse({ ok: true, path: config.configTomlPath, writable: state.mode !== "bridge" && state.configWritable, text: state.configTomlText, chars: state.configTomlText.length });
  if (req.method === "GET" && url.pathname === "/api/auth/json") return jsonResponse({ ok: true, path: config.authJsonPath, writable: state.mode !== "bridge" && state.authWritable, text: state.authJsonText, chars: state.authJsonText.length });
  if (req.method === "GET" && url.pathname === "/api/env") return jsonResponse({ ok: true, path: config.envFilePath, writable: state.mode !== "bridge" && state.envWritable, env: state.envVars, keys: Object.keys(state.envVars).sort() });
  if (req.method === "GET" && url.pathname === "/api/token-provider") return jsonResponse({ ok: true, tokenProvider: state.tokenProvider, switchAllowed: state.mode !== "bridge" && state.tokenProviderSwitchAllowed });
  if (req.method === "GET" && url.pathname === "/logs") {
    return jsonResponse({ ok: true, logs: recentLogs.slice(-200), logFile: config.logFile, currentLogFile: getLogWriter().currentPath(), startedAt });
  }
  if (req.method === "GET" && url.pathname === "/status") return jsonResponse({ ok: true, serviceId: state.serviceId, state, config: { mode: state.mode, adapterNames: config.adapterNames, defaultAdapter: config.defaultAdapter } });
  if (req.method === "POST" && url.pathname.startsWith("/api/adapters/")) {
    const rest = url.pathname.slice("/api/adapters/".length);
    const [adapterName, action] = rest.split("/", 2);
    if (!adapterName || !action) return jsonResponse({ ok: false, error: "adapter name and action are required" }, 400);
    const body = await readJson(req);
    if (typeof body.adapter !== "string") body.adapter = adapterName;
    const result = adapterAction(action, body);
    log("adapter_action", { adapterName, action, bodyPreview: clampText(JSON.stringify(body), 600) });
    return jsonResponse(result);
  }
  if (req.method === "POST" && url.pathname === "/api/state/reload") {
    state = loadState();
    persistState();
    return jsonResponse({ ok: true, state });
  }
  if (req.method === "POST" && url.pathname === "/api/state/seed") {
    const body = await readJson(req);
    const nextMode = typeof body.mode === "string" ? parseSandboxMode(body.mode) : state.mode;
    state.mode = nextMode;
    state.tokenProvider = typeof body.tokenProvider === "string" ? body.tokenProvider : state.tokenProvider;
    state.tokenProviderSwitchAllowed = typeof body.tokenProviderSwitchAllowed === "boolean" ? body.tokenProviderSwitchAllowed : state.tokenProviderSwitchAllowed;
    state.configWritable = typeof body.configWritable === "boolean" ? body.configWritable : state.configWritable;
    state.authWritable = typeof body.authWritable === "boolean" ? body.authWritable : state.authWritable;
    state.envWritable = typeof body.envWritable === "boolean" ? body.envWritable : state.envWritable;
    if (typeof body.configTomlText === "string") state.configTomlText = body.configTomlText;
    if (typeof body.authJsonText === "string") state.authJsonText = body.authJsonText;
    if (typeof body.env === "object" && body.env !== null && !Array.isArray(body.env)) state.envVars = normalizeEnvVars(body.env, state.envVars);
    persistState();
    return jsonResponse({ ok: true, state });
  }
  if (req.method === "GET" && url.pathname === "/api/adapter-summary") return jsonResponse({ ok: true, summary: adapterSummary() });
  return jsonResponse({
    ok: false,
    error: "not found",
    serviceId: state.serviceId,
    available: ["/health", "/diagnostics", "/trace", "/logs", "/status", "/api/config/toml", "/api/auth/json", "/api/env", "/api/token-provider", "/api/adapters/:adapter/:action", "/api/state/reload", "/api/state/seed"],
  }, 404);
}

if (import.meta.main) {
  state = loadState();
  persistState();
  log("service_started", { startedAt, serviceId: state.serviceId, mode: state.mode, adapterNames: config.adapterNames });

  Bun.serve({
    hostname: config.host,
    port: config.port,
    idleTimeout: 120,
    fetch(req) {
      return route(req).catch((error) => errorResponse(error));
    },
  });
}
