/**
 * Entry point — load config, create auth manager, start proxy server.
 * @see .omo/plans/zcode-proxy.md Task 7
 */
import { loadConfig } from "./config/loader.js";
import { createStoredAuthManagerWithAccounts } from "./auth/runtime.js";
import { importFromZCodeConfig } from "./auth/desktop.js";
import { saveLoginCredential } from "./auth/login-store.js";
import { startServer, type ProxyServer } from "./server/server.js";
import { startControlListener, LogBuffer, type ControlState } from "./android/control.js";
import { loadCredential, clearCredential, getStorePath } from "./auth/store.js";
import { ZaiOAuthClient, BigmodelOAuthClient, LOGIN_TIMEOUT_MS, parsePastedCallbackUrl, type OAuthResult } from "./auth/oauth.js";
import { KeyResolver } from "./auth/resolver.js";
import type { Credential } from "./auth/types.js";
import type { ProviderId } from "./provider/types.js";
import type { ProxyConfig } from "./config/types.js";
import { updateConfigYaml, updateAccountPolicyYaml, ensureConfigFile } from "./config/edit.js";
import { openBrowser } from "./runtime/open-browser.js";
import { pasteLoginInstructions, readPastedLine, boldIfTTY } from "./runtime/paste-login.js";
import { buildServerOptions } from "./server/server-options.js";
import { readFileSync, existsSync, writeFileSync } from "node:fs";
import { randomUUID } from "node:crypto";
import { ensureNodeFetchNoTimeouts } from "./runtime/node-fetch-compat.js";
import { installGuestErrorBoundary } from "./runtime/guest-error.js";
import { MemoryGuard, shutdownCaptchaRuntime, startEventLoopMonitor } from "./runtime/health-monitor.js";
import { installWatchdog, respawnHookCommand, spawnRespawnHook } from "./runtime/watchdog.js";
import {
  addAccount,
  getAccountStorePath,
  loadAccountStore,
  removeAccount,
  updateAccountStore,
  migrateAccountStore,
  duplicateCredentialGroups,
  sameIdentityGroups,
  rememberAccount,
} from "./auth/account-store.js";
import { createAccountRotator } from "./auth/account-rotator.js";
import { accountIdentity } from "./auth/account-identity.js";
import { buildHealthReport, parseLiveQuota, parseLiveStatus, renderHealthText, type OfflineAccount } from "./auth/account-health.js";

export const VERSION = "4.6.4";

if (require.main === module) main();

export interface ServeArgs {
  configPath?: string;
  debug: boolean;
}

/**
 * Parse `serve` subcommand arguments. The token `debug` toggles debug mode;
 * any other token is treated as the config path. Order-independent:
 *   []                → { debug: false }
 *   ["debug"]         → { debug: true }
 *   ["my.yaml"]       → { configPath: "my.yaml", debug: false }
 *   ["debug","x.yaml"] → { configPath: "x.yaml", debug: true }
 *   ["x.yaml","debug"] → { configPath: "x.yaml", debug: true }
 */
export function parseServeArgs(args: string[]): ServeArgs {
  const debug = args.includes("debug");
  const configPath = args.find((a) => a !== "debug");
  return { configPath, debug };
}

export function main(): void {
  // Fire-and-forget is race-safe: the dynamic import resolves in a microtask,
  // before the listener's event-loop callback can admit a request.
  void ensureNodeFetchNoTimeouts();
  try {
    runCli();
  } catch (err) {
    process.stderr.write(`zcode-proxy: uncaught error: ${(err as Error).stack ?? String(err)}\n`);
    process.exit(1);
  }
}

function runCli(): void {
  const args = process.argv.slice(2);

  // `--cli` opts out of the default TUI and restores the classic CLI dispatch
  // (bare `--cli` = the old no-arg default: serve).
  if (args[0] === "--cli") {
    dispatchCli(args.slice(1));
    return;
  }
  // Default surface is the TUI. Bare invocation, the retired `tui` token
  // (kept as a silent alias), and tui-style args (`debug`, `*.yaml`) all land
  // here — the former `tui <args>` subcommand simply dropped its prefix.
  if (
    args.length === 0 ||
    args[0] === "tui" ||
    args[0] === "debug" ||
    args[0].endsWith(".yaml") ||
    args[0].endsWith(".yml")
  ) {
    launchTui(parseServeArgs(args[0] === "tui" ? args.slice(1) : args));
    return;
  }
  dispatchCli(args);
}

/** Classic CLI dispatch — subcommand routing where bare = serve. */
function dispatchCli(args: string[]): void {
  const cmd = args[0] ?? "serve";

  if (cmd === "auth") {
    authCommand(args.slice(1));
  } else if (cmd === "claim") {
    void claimCommand(args.slice(1));
  } else if (cmd === "android") {
    // Explicit catch: an async startup failure (e.g. control port already
    // bound by an orphaned process) must exit non-zero deterministically, not
    // surface as an unhandled rejection.
    runAndroid().catch((err: unknown) => {
      process.stderr.write(`zcode-proxy: android entry failed: ${(err as Error).stack ?? String(err)}\n`);
      process.exit(1);
    });
  } else if (cmd === "tui") {
    // Kept for muscle memory under `--cli`: the default dispatch already
    // routes `tui` to the TUI, but `--cli tui` should not regress to an error.
    launchTui(parseServeArgs(args.slice(1)));
  } else if (cmd === "serve" || cmd.endsWith(".yaml") || cmd.endsWith(".yml")) {
    const serveArgs = cmd === "serve"
      ? parseServeArgs(args.slice(1))
      : parseServeArgs(args);
    serve(serveArgs.configPath, serveArgs.debug);
  } else if (cmd === "version" || cmd === "--version" || cmd === "-v") {
    console.log(`zcode-proxy ${VERSION}`);
  } else if (cmd === "help" || cmd === "--help" || cmd === "-h") {
    printHelp();
  } else {
    console.error(`Unknown command: ${cmd}\n`);
    printHelp();
    process.exit(1);
  }
}

function launchTui(args: ServeArgs): void {
  // Dynamic import: the TUI module imports helpers back from this file, so a
  // static edge would create a load-time cycle (same pattern as claimCommand).
  import("./tui/app.js")
    .then((m) => m.runTui(args))
    .catch((err: unknown) => {
      process.stderr.write(`zcode-proxy: tui failed: ${(err as Error).stack ?? String(err)}\n`);
      process.exit(1);
    });
}

function printHelp(): void {
  console.log(`zcode-proxy ${VERSION}

Usage:
  zcode-proxy                       Interactive terminal UI (default):
                                    login, start/stop, live logs
  zcode-proxy [debug] [config.yaml] Same, with debug diagnostics / custom config
  zcode-proxy serve [config.yaml]   Start the proxy server (classic CLI mode)
  zcode-proxy serve debug [config.yaml]
                                    Start with verbose per-request diagnostics
  zcode-proxy --cli                 Classic CLI mode (bare --cli = serve)
  zcode-proxy android               Android entry: proxy + localhost control listener
  zcode-proxy auth login <provider> Login via OAuth (provider: zai | bigmodel)
  zcode-proxy auth login <provider> --account ID [--replace]
  zcode-proxy auth login <provider> --import
                                    Import the current Desktop login (legacy config only if no shared store)
  zcode-proxy auth logout           Clear stored credentials
  zcode-proxy auth status           Show current authentication state
  zcode-proxy auth accounts [--json]
                                    List configured accounts (redacted, offline)
  zcode-proxy auth accounts remove ID [--yes]
                                    Remove one configured account
  zcode-proxy auth accounts pause|resume ID
                                    Pause/resume an account in the policy (live on next request)
  zcode-proxy auth accounts explain --model MODEL --operation OP
                                    Explain policy/provider/model selection (read-only)
  zcode-proxy auth accounts doctor [--json]
                                    Read-only encrypted-pool/config diagnostics
  zcode-proxy auth accounts migrate
                                    Explicitly migrate legacy encrypted pool data
  zcode-proxy auth accounts quota
                                    Live pool-wide quota overview (requires running proxy)
  zcode-proxy auth accounts health [--json]
                                    One verdict per account: state + quota (requires running proxy).
                                    One run = up to 2 upstream billing requests per unique
                                    account (15 s cached); no inference, captcha or token refresh
  zcode-proxy auth accounts --live
                                    Authenticated live runtime status (redacted)
  zcode-proxy claim [list|now]      List / claim weekend-plan trial packages
  zcode-proxy version               Show version
  zcode-proxy help                  Show this help

Examples:
  zcode-proxy                       Terminal UI: login, start/stop, live logs
  zcode-proxy debug                 Terminal UI with per-request diagnostics
  zcode-proxy serve debug           CLI: start with extra debug logging
  zcode-proxy auth login bigmodel   OAuth login for Bigmodel
  zcode-proxy auth login bigmodel --import
                                    Import existing key from ZCode config
  zcode-proxy auth status           Check if logged in
  zcode-proxy auth accounts --json  List account ids and redacted state
`);
}

async function serve(configPath: string | undefined, debug: boolean): Promise<void> {
  if (process.env.ZCODE_PROXY_ALLOW_UNSANDBOXED_CAPTCHA === "1") installGuestErrorBoundary();
  const path = configPath ?? process.env.ZCODE_PROXY_CONFIG ?? "config.yaml";
  if (ensureConfigFile(path)) {
    ensureDeviceMidInConfig(path);
    console.log(`Created ${path} from bundled template.`);
    console.log(`Run: zcode-proxy auth login <zai|bigmodel>\n`);
  }
  const config = loadConfig(path);

  let auth = await createStoredAuthManagerWithAccounts(config.plan, {
    ...(config.auth.accounts ?? { enabled: false }),
    provider: config.provider,
  });
  let cred: Credential | null = null;
  if (!auth.isAccountPoolEnabled()) {
    cred = await loadCredential();
    if (!cred) {
      console.error("Not logged in. Run: zcode-proxy auth login " + config.provider);
      process.exit(1);
    }
    auth.setOAuthCredential(cred);
  } else {
    try { await auth.getCredential(); }
    catch (err) {
      console.error(`No usable configured account. Run: zcode-proxy auth login ${config.provider} --account ID`);
      if (debug) console.error((err as Error).message);
      process.exit(1);
    }
  }

  if (debug) printDebugBanner(config, path, cred);

  const server = await startServer(buildServerOptions(config, auth, debug));
  const url = `http://${server.hostname}:${server.port}`;
  console.log(`zcode-proxy listening on ${url}`);
  startEventLoopMonitor();
  installWatchdog();
  const memoryGuard = new MemoryGuard({
    canRestart: () => respawnHookCommand(process.env, process.pid, "memory") !== null,
    restart: async () => {
      // Exit only once the manager is actually running: a proxy that stops
      // without it stays down for good.
      if (!(await spawnRespawnHook("memory"))) return false;
      // The manager waits for this pid to exit before starting the
      // replacement; give in-flight responses a bounded chance to finish.
      shutdownCaptchaRuntime();
      const forceExit = setTimeout(() => process.exit(0), 10_000);
      forceExit.unref();
      // Long streams may keep close() busy past the 10 s cap: give it 6 s,
      // then the bounded (3 s) metadata write, then exit — all inside the cap.
      void Promise.race([server.close(), new Promise<void>((resolve) => setTimeout(resolve, 6_000))])
        .then(() => flushAccountMetadata(auth))
        .then(() => process.exit(0));
      return true;
    },
  });
  memoryGuard.start();
  if (config.plan === "start-plan") {
    // Pre-solve the captcha token pool in the background so first requests
    // don't pay the full solve latency (in-process happy-dom backend).
    import("./proxy/captcha.js")
      .then((m) => m.startCaptchaPool(config.identity.appVersion))
      .catch((err) => console.error(`[captcha] pool warmup failed: ${(err as Error).message}`));
  }
  if (config.claim.enabled && config.claim.auto) {
    import("./claim/runtime.js")
      .then((m) => {
        m.startAutoClaim(config, auth);
        console.log(`  claim: auto ON (poll ${Math.round(config.claim.pollIntervalMs / 1000)}s)`);
      })
      .catch((err) => console.error(`[claim] scheduler failed to start: ${(err as Error).message}`));
  }
  console.log(`  provider: ${config.provider}`);
  console.log(`  plan: ${config.plan}`);
  console.log(`  models: ${config.models.length} available`);
  if (config.responses.enabled) console.log(`  /v1/responses: ON`);
  if (config.async.enabled) {
    console.log(config.plan === "coding-plan" ? `  /async/v1/*: ON` : `  /async/v1/*: OFF (requires plan "coding-plan")`);
  }
  if (debug) console.log(`  debug: ON`);

  // Unsaved account metadata (a store write that lost a lock race) gets one
  // last, bounded write before the process exits.
  let stopping = false;
  const stop = (): void => {
    if (stopping) return;
    stopping = true;
    shutdownCaptchaRuntime();
    void flushAccountMetadata(auth).finally(() => server.stop(true));
  };
  process.on("SIGINT", () => {
    console.log("\nShutting down...");
    stop();
  });
  process.on("SIGTERM", stop);
}

/** Bounded final write of unsaved account metadata (referenced timer: shutdown waits for it). */
export async function flushAccountMetadata(auth: { flushPersistence?: () => Promise<void>; getPersistenceStatus?: () => { unsaved?: boolean } }, deadlineMs = 3_000): Promise<void> {
  if (typeof auth.flushPersistence !== "function") return;
  let timer: ReturnType<typeof setTimeout> | undefined;
  await Promise.race([
    auth.flushPersistence().catch(() => {}),
    new Promise<void>((resolve) => { timer = setTimeout(resolve, deadlineMs); }),
  ]);
  if (timer) clearTimeout(timer);
  if (auth.getPersistenceStatus?.().unsaved) console.error("[accounts] shutdown: account metadata could not be written; it will be rebuilt from the next requests");
}

/**
 * Desktop-Linux identity defaults for the Android entry (anti-pattern #34).
 * Without these, the Node process on Android reports its true host values:
 * `X-Platform: linux-arm64` and `X-Os-Version: 6.1.xx-android14-…` — a kernel
 * string no real ZCode desktop emits. `identity.ts` reads these env vars per
 * request, so setting them once here retargets every upstream call. Values are
 * deliberately CONSTANT (Ubuntu 24.04 x64 profile — the largest desktop-Linux
 * population): kernel strings are shared by millions of real machines, and
 * stability is required by anti-pattern #13 (never randomize fingerprints).
 * Explicit env values (adb shell setprop / NodeRunner) still win — each is set
 * with `??`, not unconditionally.
 */
export function applyAndroidIdentityDefaults(): void {
  process.env.ZCODE_IDENTITY_PLATFORM = process.env.ZCODE_IDENTITY_PLATFORM ?? "linux";
  process.env.ZCODE_IDENTITY_ARCH = process.env.ZCODE_IDENTITY_ARCH ?? "x64";
  process.env.ZCODE_IDENTITY_RELEASE = process.env.ZCODE_IDENTITY_RELEASE ?? "6.8.0-49-generic";
}

/**
 * Android entry — starts the proxy plus a localhost control listener.
 * Caller (Kotlin shell) must set env: ZCODE_CONTROL_PORT (control listener),
 * ZCODE_OAUTH_CALLBACK_PORT (fixed OAuth callback port for WebView redirect).
 */
async function runAndroid(): Promise<void> {
  applyAndroidIdentityDefaults();
  const path = process.env.ZCODE_PROXY_CONFIG ?? "config.yaml";
  ensureConfigFile(path);
  const config = loadConfig(path);

  const logBuffer = new LogBuffer();
  const origLog = console.log;
  const origErr = console.error;
  const origWarn = console.warn;
  console.log = (...args: unknown[]) => { logBuffer.push(args.join(" ")); origLog(...args); };
  console.error = (...args: unknown[]) => { logBuffer.push("[error] " + args.join(" ")); origErr(...args); };
  console.warn = (...args: unknown[]) => { logBuffer.push("[warn] " + args.join(" ")); origWarn(...args); };

  let auth = await createStoredAuthManagerWithAccounts(config.plan, {
    ...(config.auth.accounts ?? { enabled: false }),
    provider: config.provider,
  });
  let claimScheduler: { stop(): void } | null = null;

  const serverRef: { current: ProxyServer | null } = { current: null };

  async function startProxy(): Promise<{ ok: true; port: number } | { ok: false; error: string }> {
    if (serverRef.current) return { ok: false, error: "already_running" };
    if (!auth.isAccountPoolEnabled()) {
      const cred = await loadCredential().catch(() => null);
      if (!cred) return { ok: false, error: "not_logged_in" };
      auth.setOAuthCredential(cred);
    } else {
      try { await auth.getCredential(); }
      catch { return { ok: false, error: "not_logged_in" }; }
    }
    try {
      const s = await startServer(buildServerOptions(config, auth, false));
      serverRef.current = s;
      console.log(`zcode-proxy listening on http://${s.hostname}:${s.port}`);
      return { ok: true, port: s.port };
    } catch (err) {
      return { ok: false, error: (err as Error).message };
    }
  }

  async function stopProxy(): Promise<{ ok: true } | { ok: false; error: string }> {
    const s = serverRef.current;
    if (!s) return { ok: false, error: "not_running" };
    try {
      s.stop(false);
      serverRef.current = null;
      console.log("zcode-proxy stopped");
      return { ok: true };
    } catch (err) {
      return { ok: false, error: (err as Error).message };
    }
  }

  async function setConfig(changes: {
    provider?: ProviderId;
    plan?: "coding-plan" | "start-plan";
  }): Promise<{ ok: true; provider: ProviderId; plan: "coding-plan" | "start-plan" } | { ok: false; error: string }> {
    if (serverRef.current) return { ok: false, error: "stop_proxy_first" };
    const nextProvider = changes.provider ?? config.provider;
    const nextPlan = changes.plan ?? config.plan;
    // Rebuild the selection filter atomically while the proxy is stopped. A
    // subsequent start must never reuse credentials selected for the old
    // provider/plan.
    let nextAuth: typeof auth;
    try {
      nextAuth = await createStoredAuthManagerWithAccounts(nextPlan, {
      ...(config.auth.accounts ?? { enabled: false }),
        provider: nextProvider,
      });
    } catch (err) {
      return { ok: false, error: `account_pool_unavailable: ${(err as Error).message}` };
    }
    claimScheduler?.stop();
    claimScheduler = null;
    config.provider = nextProvider;
    config.plan = nextPlan;
    auth = nextAuth;
    updateConfigYaml(path, { provider: config.provider, plan: config.plan });
    controlState.provider = config.provider;
    controlState.plan = config.plan;
    if (config.claim.enabled && config.claim.auto) {
      import("./claim/runtime.js").then((m) => {
        claimScheduler = m.startAutoClaim(config, auth);
      }).catch((err) => console.error(`[claim] scheduler restart failed: ${(err as Error).message}`));
    }
    console.log(`config updated: provider=${config.provider} plan=${config.plan}`);
    return { ok: true, provider: config.provider, plan: config.plan };
  }

  console.log("control listener ready; proxy stopped — use startProxy command to start");

  if (config.claim.enabled && config.claim.auto) {
    import("./claim/runtime.js")
      .then((m) => {
        claimScheduler = m.startAutoClaim(config, auth);
        console.log(`[claim] auto ON (poll ${Math.round(config.claim.pollIntervalMs / 1000)}s; waits for login)`);
      })
      .catch((err) => console.error(`[claim] scheduler failed to start: ${(err as Error).message}`));
  }

  const controlPort = Number(process.env.ZCODE_CONTROL_PORT ?? 0) || 0;
  const controlState: ControlState = {
    provider: config.provider,
    plan: config.plan,
    proxyPort: serverRef.current?.port ?? 0,
  };
  const controlListener = await startControlListener({
    capability: process.env.ZCODE_CONTROL_CAPABILITY,
    port: controlPort,
    state: controlState,
    logBuffer,
    onAuthStatus: async () => {
      if (!auth.isAccountPoolEnabled()) return (await loadCredential()) != null;
      await auth.refreshAccountPool();
      return auth.listAccounts().some(account => account.state !== "invalid" && account.state !== "expired");
    },
    onStartProxy: startProxy,
    onStopProxy: stopProxy,
    onSetConfig: setConfig,
    onShutdown: async () => {
      serverRef.current?.stop(true);
    },
  });

  console.log(`control listener: 127.0.0.1:${controlPort}`);
  console.log(`provider: ${config.provider}`);
  console.log(`plan: ${config.plan}`);

  process.on("SIGINT", () => {
    void controlListener.close().then(() => serverRef.current?.stop(true));
  });
  process.on("SIGTERM", () => {
    void controlListener.close().then(() => serverRef.current?.stop(true));
  });
}

function printDebugBanner(config: ProxyConfig, path: string, cred: Credential | null): void {
  const credShape = cred
    ? `<redacted> (${cred.apiKey.length} chars)`
    : "(none)";
  const active = config.providers[config.provider];
  console.log("=== zcode-proxy DEBUG MODE ===");
  console.log(`  config file: ${path}`);
  console.log(`  server: ${config.server.host}:${config.server.port}`);
  console.log(`  proxy api key: ${config.auth.proxyApiKey ? "required" : "open (no client auth)"}`);
  console.log(`  provider: ${config.provider}`);
  console.log(`  plan: ${config.plan}`);
  console.log(`  identity: appVersion=${config.identity.appVersion} sourceTitle=${config.identity.sourceTitle} referer=${config.identity.refererOrigin}`);
  console.log(`  client identity: mode=${config.clientIdentity.mode} ttl=${config.clientIdentity.ttlSeconds}s max=${config.clientIdentity.maxSessions}`);
  console.log(`  anthropic base: ${active.anthropicBase}`);
  console.log(`  openai base:    ${active.openaiBase}`);
  console.log(`  credential: ${credShape}`);
  console.log(`  models (${config.models.length}): ${config.models.join(", ")}`);
  console.log(`  log level: ${config.logging.level}`);
  console.log("===============================");
}

function authCommand(args: string[]): void {
  const sub = args[0];

  if (sub === "login") {
    void authLogin(args.slice(1));
  } else if (sub === "logout") {
    authLogout();
  } else if (sub === "status") {
    void authStatus();
  } else if (sub === "accounts") {
    void authAccounts(args.slice(1));
  } else {
    console.error("Usage: zcode-proxy auth <login|logout|status|accounts>");
    process.exit(1);
  }
}

async function claimCommand(args: string[]): Promise<void> {
  const sub = args[0] ?? "now";
  if (sub !== "list" && sub !== "now") {
    console.error("Usage: zcode-proxy claim [list|now]");
    process.exit(1);
  }
  const path = process.env.ZCODE_PROXY_CONFIG ?? "config.yaml";
  if (!existsSync(path)) {
    console.error(`Config file not found: ${path} (run serve once or create it).`);
    process.exit(1);
  }
  // The billing gateway requires a stable X-Device-Mid — self-heal configs
  // created before the deviceMid feature (idempotent: reuses existing value).
  ensureDeviceMidInConfig(path);
  const config = loadConfig(path);
  try {
    const { runClaimCli } = await import("./claim/runtime.js");
    await runClaimCli(config, sub);
  } catch (err) {
    console.error(`claim failed: ${(err as Error).message}`);
    process.exit(1);
  }
}

async function authLogin(args: string[]): Promise<void> {
  const provider = args[0] as ProviderId | undefined;
  const importMode = args.includes("--import");
  const accountIndex = args.indexOf("--account");
  const accountId = accountIndex >= 0 ? args[accountIndex + 1] : undefined;
  const replaceAccount = args.includes("--replace");
  // Headless paste login: --paste flag or ZCODE_OAUTH_PASTE=1 (docker-friendly).
  const pasteMode =
    args.includes("--paste") || /^(1|true|yes)$/i.test(process.env.ZCODE_OAUTH_PASTE ?? "");

  if (!provider || (provider !== "zai" && provider !== "bigmodel")) {
    console.error("Usage: zcode-proxy auth login <zai|bigmodel> [--import] [--paste] [--account ID] [--replace]");
    process.exit(1);
  }
  if (accountIndex >= 0 && (!accountId || accountId.startsWith("--"))) {
    console.error("--account requires an account ID.");
    process.exit(1);
  }
  if (replaceAccount && !accountId) {
    console.error("--replace requires --account ID.");
    process.exitCode = 2;
    return;
  }
  if (pasteMode && provider !== "bigmodel") {
    console.error("--paste applies to the bigmodel auth-code flow only.");
    console.error("zai login is server-mediated (no localhost callback) and already works headless.");
    process.exit(1);
  }

  ensureConfigWithDeviceMid();

  const mode = importMode ? "(import)" : pasteMode ? "(OAuth, paste)" : "(OAuth)";
  console.log(`Logging in: ${provider} ${mode}\n`);

  let cred: Credential;

  if (importMode) {
    const configuredPlan = loadConfig(process.env.ZCODE_PROXY_CONFIG ?? "config.yaml").plan;
    cred = importFromZCodeConfig(provider, configuredPlan);
  } else {
    const { accessToken, userId, jwt } = await runOAuth(provider, pasteMode);
    console.log("\nResolving API key...");
    const resolver = new KeyResolver();
    cred = await resolver.resolveCodingPlanCredential(accessToken, provider, userId);
    if (jwt) cred.jwt = jwt;
  }

  if (accountId) {
    let accountOptions: { path?: string };
    try {
      accountOptions = accountStoreOptions();
      const configuredPlan = (() => {
        try {
          const cfgPath = process.env.ZCODE_PROXY_CONFIG ?? "config.yaml";
          return existsSync(cfgPath) ? loadConfig(cfgPath).plan : undefined;
        } catch { return undefined; }
      })();
      await addAccount({
        id: accountId,
        credential: cred,
        createdAt: Date.now(),
        plan: configuredPlan ?? (process.env.ZCODE_PROXY_PLAN === "start-plan" ? "start-plan" : "coding-plan"),
      }, { ...accountOptions, replace: replaceAccount });
    } catch (err) {
      console.error(`Account login failed: ${safeAccountError(err)}`);
      process.exitCode = 1;
      return;
    }
    console.log(`\nLogged in as ${provider} (account ${accountId}).`);
    console.log(`  Stored: ${getAccountStorePath(accountOptions.path)}`);
    // An explicit id is honored even when it aliases a stored login; say so,
    // because quota belongs to the upstream user, so the alias adds no capacity
    // (the rotator itself still treats differing tokens as separate entries).
    const identity = accountIdentity(cred);
    if (identity) {
      try {
        const stored = await loadAccountStore(accountOptions);
        const plan = stored.find((account) => account.id === accountId)?.plan ?? "";
        const alias = stored.find((account) => account.id !== accountId
          && account.credential.provider === cred.provider && (account.plan ?? "") === plan
          && accountIdentity(account.credential) === identity);
        if (alias) {
          console.log(`Note: this login is also stored as ${alias.id}; both entries share one upstream quota, so the extra entry adds no capacity.`);
          console.log(`Remove the extra entry with: zcode-kit accounts remove ${alias.id} --yes`);
        }
      } catch { /* the login itself succeeded; the hint is best effort */ }
    }
    return;
  }

  let savedAccountId: string | undefined;
  try { savedAccountId = await saveLoginCredential(cred); }
  catch (err) {
    console.error(`Login could not be saved: ${safeAccountError(err)}`);
    process.exitCode = 1;
    return;
  }
  console.log(`\nLogged in as ${provider}${savedAccountId ? ` (account ${savedAccountId})` : ""}.`);
  console.log("  Credential: stored (redacted)");
  console.log(`  Stored:  ${savedAccountId ? getAccountStorePath(accountStoreOptions().path) : getStorePath()}`);
  if (!savedAccountId) console.log("  Account Rotator is disabled; this replaces the single-account login. Enable with: zcode-kit accounts enable");
}

/**
 * Resolve an optional configured account-store path without starting the
 * proxy or creating a config. Environment remains the authoritative override;
 * the YAML path is only consulted when it is already present and valid.
 */
function accountStoreOptions(): { path?: string } {
  if (process.env.ZCODE_PROXY_ACCOUNTS_PATH) return {};
  const path = process.env.ZCODE_PROXY_CONFIG ?? "config.yaml";
  if (!existsSync(path)) return {};
  try {
    const configured = loadConfig(path).auth.accounts?.path;
    if (!configured) return {};
    // Leave tilde shorthand intact; account-store.ts owns its normalization
    // so the special value "~" still resolves to the default accounts.json
    // file rather than the home directory itself.
    return { path: configured };
  } catch {
    // A present but malformed config must never make a mutating account
    // command silently target the default store instead of the configured
    // pool. Callers surface this as a bounded, secret-free error.
    throw new Error("proxy config is unavailable; set ZCODE_PROXY_ACCOUNTS_PATH explicitly");
  }
}

function safeAccountError(err: unknown): string {
  // AccountStoreError messages are intentionally short, but never echo an
  // arbitrary provider/error string in a CLI surface that promises redaction.
  const code = typeof err === "object" && err && "code" in err ? String((err as { code?: unknown }).code) : "";
  if (["invalid", "locked", "corrupt", "conflict"].includes(code)) return (err as Error).message;
  return "account store operation failed";
}

async function authAccounts(args: string[]): Promise<void> {
  const sub = args[0];

  if (sub === "import-current") {
    try {
      const config = loadConfig(process.env.ZCODE_PROXY_CONFIG ?? "config.yaml");
      const options = { ...accountStoreOptions(), plan: config.plan };
      // Validate the pool before importing anything; corrupt stores stay untouched.
      await loadAccountStore(options);
      const legacy = await loadCredential();
      if (!legacy && existsSync(getStorePath())) throw new Error("credential store unavailable");
      if (legacy) await rememberAccount(legacy, options);
      let desktop: Credential | undefined;
      try { desktop = importFromZCodeConfig(config.provider, config.plan); } catch { /* no current Desktop login */ }
      if (desktop) await rememberAccount(desktop, options);
      console.log(JSON.stringify({ accountCount: (await loadAccountStore(options)).length }));
    } catch (err) {
      console.error(`Account import failed: ${safeAccountError(err)}`);
      process.exitCode = 1;
    }
    return;
  }

  if (sub === "migrate") {
    try {
      const result = await migrateAccountStore(accountStoreOptions());
      console.log(JSON.stringify({ schemaVersion: 1, source: "offline", migrated: result.migrationPerformed, revision: result.revision, accountCount: result.accounts.length }, null, 2));
    } catch (err) {
      console.error(`Account store migration failed: ${safeAccountError(err)}`);
      process.exitCode = 1;
    }
    return;
  }

  if (sub === "pause" || sub === "resume") {
    const id = args[1];
    if (!id || id.startsWith("--")) {
      console.error(`Usage: zcode-proxy auth accounts ${sub} ID`);
      process.exitCode = 2;
      return;
    }
    try {
      const options = accountStoreOptions();
      const result = await updateAccountStore((accounts) => {
        if (!accounts.some((account) => account.id === id)) throw new Error("account not found");
        return accounts.map((account) => account.id === id ? { ...account, paused: sub === "pause" } : account);
      }, options);
      console.log(`${sub === "pause" ? "Paused" : "Resumed"} account: ${id}`);
      console.log("Policy applies to new requests; an in-flight request keeps its original account context.");
    } catch (err) {
      console.error(`Account policy update failed: ${safeAccountError(err)}`);
      process.exitCode = 1;
    }
    return;
  }

  if (sub === "explain") {
    await explainAccounts(args.slice(1));
    return;
  }

  if (sub === "doctor") {
    await doctorAccounts(args.slice(1));
    return;
  }

  if (sub === "quota") {
    await liveAccountsRoute("/accounts/quota", args.includes("--json"));
    return;
  }

  if (sub === "health") {
    await healthAccounts(args.slice(1));
    return;
  }

  if (args.includes("--live")) {
    await liveAccountsRoute("/accounts/status", args.includes("--json"));
    return;
  }

  if (sub === "remove") {
    const id = args[1];
    if (!id || id.startsWith("--")) {
      console.error("Usage: zcode-proxy auth accounts remove ID [--yes]");
      process.exitCode = 2;
      return;
    }
    if (!args.includes("--yes")) {
      console.error(`This removes account \"${id}\" from the encrypted account pool.`);
      console.error("Re-run with --yes to confirm. The account's Desktop login is not touched.");
      process.exitCode = 2;
      return;
    }
    try {
      const options = accountStoreOptions();
      const removed = await removeAccount(id, options);
      if (!removed) {
        console.error(`Account not found: ${id}`);
        process.exitCode = 1;
        return;
      }
      console.log(`Removed account: ${id}`);
    } catch (err) {
      console.error(`Account removal failed: ${safeAccountError(err)}`);
      process.exitCode = 1;
    }
    return;
  }

  if (sub && sub.startsWith("-")) {
    // `accounts --json` is the common spelling; all flags are handled below.
  } else if (sub !== undefined) {
    console.error("Usage: zcode-proxy auth accounts [--json] | auth accounts remove ID [--yes] | auth accounts health [--json]");
    console.error("  health: up to 2 upstream billing requests per unique account (15 s cached); no inference, captcha or token refresh");
    process.exitCode = 2;
    return;
  }

  try {
    const options = accountStoreOptions();
    // The rotator's bounded view includes local cooldown state (exhausted)
    // while still omitting every credential field. Decrypting the pool here is
    // offline only; no upstream/quota request is made.
    const records = (createAccountRotator(await loadAccountStore(options)).list())
      .map(({ lastFailureReason: _redacted, ...record }) => record);
    if (args.includes("--json")) {
      console.log(JSON.stringify({ schemaVersion: 1, source: "offline-config", asOf: new Date().toISOString(), accounts: records }, null, 2));
      return;
    }
    if (records.length === 0) {
      console.log("No accounts configured.");
      console.log("Add one with: zcode-proxy auth login <zai|bigmodel> --account ID");
      return;
    }
    console.log("Configured accounts:");
    for (const account of records) {
      const label = account.label ? ` (${account.label.replace(/[\r\n\t]+/g, " ").slice(0, 120)})` : "";
      const plan = account.plan ? ` plan=${account.plan}` : "";
      console.log(`  ${account.id}${label}  provider=${account.provider}${plan}  state=${account.state}  credential=${account.credentialPreview}`);
    }
    console.log(`  store: ${getAccountStorePath(options.path)}`);
  } catch (err) {
    const message = `Account listing failed: ${safeAccountError(err)}`;
    if (args.includes("--json")) console.log(JSON.stringify({ schemaVersion: 1, source: "offline-config", asOf: new Date().toISOString(), accounts: [], error: { code: "account_store_unavailable", message } }, null, 2));
    else console.error(message);
    process.exitCode = 1;
  }
}

function flagValue(args: string[], name: string): string | undefined {
  const index = args.indexOf(name);
  if (index < 0) return undefined;
  const value = args[index + 1];
  return value && !value.startsWith("--") ? value : undefined;
}

/** Explain account policy using the same static eligibility dimensions as the request path. */
async function explainAccounts(args: string[]): Promise<void> {
  const model = flagValue(args, "--model");
  const operation = flagValue(args, "--operation") ?? "inference";
  if (!model || !["inference", "billing", "quota", "async"].includes(operation)) {
    console.error("Usage: zcode-proxy auth accounts explain --model MODEL --operation inference|billing|quota|async [--json]");
    process.exitCode = 2;
    return;
  }
  try {
    const path = process.env.ZCODE_PROXY_CONFIG ?? "config.yaml";
    const config = loadConfig(path);
    const records = await loadAccountStore(accountStoreOptions());
    const policy = config.auth.accounts;
    const modelAllowed = config.models.includes(model);
    const candidates = records.map((record) => {
      const reasons: string[] = [];
      if (record.credential.provider !== config.provider) reasons.push("provider_mismatch");
      if (record.plan && record.plan !== config.plan) reasons.push("plan_mismatch");
      if (policy?.allowedIds && !policy.allowedIds.includes(record.id)) reasons.push("not_allowlisted");
      if (record.paused === true || policy?.pausedIds?.includes(record.id)) reasons.push("paused");
      if (policy?.allowPaid === false && (!record.plan || (record.plan !== "coding-plan" && record.plan !== "start-plan"))) reasons.push("cost_unknown_or_paid");
      if (!modelAllowed) reasons.push("model_not_configured");
      if (operation === "async" && config.plan !== "coding-plan") reasons.push("async_requires_coding_plan");
      if (operation === "billing" || operation === "quota") {
        if (!record.credential.jwt) reasons.push("billing_capability_unknown");
      }
      return { id: record.id, provider: record.credential.provider, plan: record.plan ?? null, decision: reasons.length ? "excluded" : "eligible", reasons };
    });
    const output = { schemaVersion: 1, source: "offline-config", asOf: new Date().toISOString(), model, operation, provider: config.provider, plan: config.plan, candidates };
    console.log(JSON.stringify(output, null, 2));
  } catch (err) {
    const output = { schemaVersion: 1, source: "offline-config", error: { code: "accounts_explain_failed", message: safeAccountError(err) } };
    console.log(JSON.stringify(output, null, 2));
    process.exitCode = 1;
  }
}

/** Read-only encrypted pool/config diagnostics with a stable redacted JSON contract. */
async function doctorAccounts(args: string[]): Promise<void> {
  const json = args.includes("--json");
  const checks: Array<{ code: string; status: "ok" | "error" | "warning"; detail?: string }> = [];
  try {
    const path = process.env.ZCODE_PROXY_CONFIG ?? "config.yaml";
    const config = loadConfig(path);
    checks.push({ code: "config_valid", status: "ok" });
    const accounts = await loadAccountStore(accountStoreOptions());
    checks.push({ code: "store_readable", status: "ok", detail: `${accounts.length} account(s)` });
    const duplicateGroups = duplicateCredentialGroups(accounts);
    if (duplicateGroups.length) checks.push({ code: "duplicate_effective_credentials", status: "warning", detail: `${duplicateGroups.length} alias group(s) are counted once` });
    for (const group of sameIdentityGroups(accounts)) {
      checks.push({ code: "same_identity_accounts", status: "warning", detail: `ids ${group.join(",")} share one login; keep the newest and remove the others` });
    }
    const invalidProvider = accounts.filter((account) => account.credential.provider !== config.provider).length;
    if (invalidProvider) checks.push({ code: "provider_mismatch", status: "warning", detail: `${invalidProvider} account(s)` });
    if (!config.auth.accounts?.enabled) checks.push({ code: "pool_disabled", status: "warning", detail: "legacy single-account mode is active" });
  } catch (err) {
    checks.push({ code: "doctor_unavailable", status: "error", detail: safeAccountError(err) });
  }
  // Runtime metadata (cooldowns, last use) the running proxy could not write
  // yet: a warning, since the proxy keeps retrying in the background and the
  // next change writes again; lost only if the proxy stops before it lands.
  try {
    const { ok, body } = await fetchLiveAccounts("/accounts/status", 3_000);
    const persistence = ok && body && typeof body === "object" ? (body as { persistence?: { unsaved?: unknown; attempts?: unknown; nextRetryAt?: unknown } }).persistence : undefined;
    if (persistence?.unsaved === true) {
      const next = typeof persistence.nextRetryAt === "number" ? `; next retry ${new Date(persistence.nextRetryAt).toISOString()}` : "; retries paused until the next change";
      checks.push({ code: "runtime_changes_unsaved", status: "warning", detail: `account metadata not written to the store yet (${Number(persistence.attempts) || 0} failed write(s)${next})` });
    } else if (persistence) {
      checks.push({ code: "runtime_changes_saved", status: "ok" });
    }
  } catch {
    // proxy not running: nothing unsaved in memory
  }
  const output = { schemaVersion: 1, source: "offline", asOf: new Date().toISOString(), checks };
  if (json) console.log(JSON.stringify(output, null, 2));
  else for (const check of checks) console.log(`${check.status.toUpperCase()} ${check.code}${check.detail ? `: ${check.detail}` : ""}`);
  if (checks.some((check) => check.status === "error")) process.exitCode = 1;
}

/** Authenticated GET against the local proxy; throws when it cannot be reached. */
async function fetchLiveAccounts(path: string, timeoutMs: number): Promise<{ ok: boolean; body: unknown }> {
  const config = loadConfig(process.env.ZCODE_PROXY_CONFIG ?? "config.yaml");
  const key = config.auth.proxyApiKey?.trim();
  if (!key) throw new Error("proxy API key is not configured");
  const host = config.server.host === "::1" ? "[::1]" : config.server.host;
  const response = await fetch(`http://${host}:${config.server.port}${path}`, {
    headers: { authorization: `Bearer ${key}` },
    signal: AbortSignal.timeout(timeoutMs),
  });
  const body = await response.json().catch(() => ({ schemaVersion: 1, error: { code: "invalid_response" } }));
  return { ok: response.ok, body };
}

async function liveAccountsRoute(route: string, json: boolean): Promise<void> {
  try {
    const { ok, body } = await fetchLiveAccounts(route, 5_000);
    console.log(JSON.stringify(body, null, 2));
    if (!ok) process.exitCode = 1;
  } catch (err) {
    console.log(JSON.stringify({ schemaVersion: 1, source: "live-runtime", error: { code: "live_status_unavailable", message: "proxy is not reachable" } }, null, 2));
    process.exitCode = 1;
  }
}

/**
 * Combined per-account verdict from the offline store, `/accounts/status` and
 * `/accounts/quota`. Exit 0 only when at least one account can serve now.
 */
async function healthAccounts(args: string[]): Promise<void> {
  const json = args.includes("--json");
  let offline: OfflineAccount[] = [];
  try {
    const stored = await loadAccountStore(accountStoreOptions());
    const listed = new Map(createAccountRotator(stored).list().map((record) => [record.id, record]));
    // Mirror the quota route: the first stored entry of a login is the one
    // that gets queried; later aliases are reported against it.
    const duplicateOf = new Map<string, string>();
    for (const group of [...duplicateCredentialGroups(stored), ...sameIdentityGroups(stored)]) {
      for (const id of group.slice(1)) if (!duplicateOf.has(id)) duplicateOf.set(id, group[0]);
    }
    offline = stored.map((account) => ({
      id: account.id,
      provider: account.credential.provider,
      plan: account.plan ?? null,
      paused: account.paused === true,
      state: listed.get(account.id)?.state ?? "unknown",
      lastUsedAt: account.lastUsedAt ?? null,
      exhaustedUntil: account.exhaustedUntil ?? null,
      ...(duplicateOf.has(account.id) ? { duplicateOf: duplicateOf.get(account.id) } : {}),
    }));
  } catch (err) {
    console.error(`Account store unavailable: ${safeAccountError(err)}`);
  }
  const live = async (path: string, timeoutMs: number): Promise<unknown> => {
    try {
      const { ok, body } = await fetchLiveAccounts(path, timeoutMs);
      return ok ? body : null;
    } catch { return null; }
  };
  // Quota fans out to upstream billing (bounded, cached 15 s), hence the longer budget.
  const [statusBody, quotaBody] = await Promise.all([
    live("/accounts/status", 5_000),
    live("/accounts/quota", 20_000),
  ]);
  const status = parseLiveStatus(statusBody);
  const quota = parseLiveQuota(quotaBody);
  const report = buildHealthReport(offline, status, quota, Date.now());
  console.log(json ? JSON.stringify(report, null, 2) : renderHealthText(report));
  if (report.summary.usable === 0) process.exitCode = 1;
}

/**
 * Ensure config.yaml exists and carries a stable `identity.deviceMid`.
 * Creates the file from the bundled template when missing (desktop flow;
 * Android's mid comes from NodeRunner env injection instead and is never
 * written here). Returns the mid (existing or freshly generated).
 */
function ensureConfigWithDeviceMid(): string {
  const path = process.env.ZCODE_PROXY_CONFIG ?? "config.yaml";
  if (ensureConfigFile(path)) {
    console.log(`Created ${path} from bundled template.`);
  }
  return ensureDeviceMidInConfig(path);
}

/**
 * Generate-or-reuse `identity.deviceMid` in a YAML config via targeted line
 * edit (comments preserved): fills an empty `deviceMid:` value, inserts one
 * under a block-style `identity:` key, or appends a new `identity:` block when
 * the key is absent entirely. Idempotent — an existing non-empty value is
 * returned untouched. The regexes are function-local on purpose: `main()` runs
 * synchronously at module top (before later top-level statements initialize),
 * so any module-level const this function touches would still be undefined on
 * the boot-time `serve` path.
 */
export function ensureDeviceMidInConfig(path: string): string {
  const deviceMidLine = /^(\s*)deviceMid:\s*(.*)$/m;
  const identityBlockLine = /^identity:\s*$/m;
  const raw = readFileSync(path, "utf-8");

  const existing = deviceMidLine.exec(raw);
  if (existing) {
    const value = existing[2].trim().replace(/^"|"$/g, "");
    if (value.length > 0) return value;
  }

  const mid = randomUUID();
  let updated: string;
  if (existing) {
    updated = raw.replace(deviceMidLine, `${existing[1]}deviceMid: "${mid}"`);
  } else if (identityBlockLine.test(raw)) {
    updated = raw.replace(identityBlockLine, `identity:\n  deviceMid: "${mid}"`);
  } else {
    const block = `identity:\n  deviceMid: "${mid}"\n`;
    updated = raw.endsWith("\n") || raw.length === 0 ? raw + block : raw + "\n" + block;
  }
  writeFileSync(path, updated, "utf-8");
  console.log(`Device identity generated: ${mid.slice(0, 8)}… (stored in ${path})`);
  return mid;
}

function authLogout(): void {
  if (!existsSync(getStorePath())) {
    console.log("Not logged in.");
    return;
  }
  clearCredential();
  console.log("Logged out. Credentials removed.");
}

async function authStatus(): Promise<void> {
  // An enabled pool is authoritative. Do not report the legacy credentials
  // file as logged out when the configured accounts are healthy (the pool may
  // intentionally have no compatibility credentials.json at all).
  try {
    const cfgPath = process.env.ZCODE_PROXY_CONFIG ?? "config.yaml";
    if (existsSync(cfgPath)) {
      const config = loadConfig(cfgPath);
      if (config.auth.accounts?.enabled) {
        const auth = await createStoredAuthManagerWithAccounts(config.plan, {
          ...config.auth.accounts,
          provider: config.provider,
        });
        const records = auth.listAccounts();
        const usable = records.filter((record) => record.state === "ready" || record.state === "active");
        console.log(`Account pool: ${usable.length > 0 ? "logged in" : "not logged in"}`);
        console.log(`  Accounts: ${records.length} configured, ${usable.length} usable`);
        console.log(`  Store:    ${getAccountStorePath(accountStoreOptions().path)}`);
        return;
      }
    }
  } catch (err) {
    console.error(`Account pool status unavailable: ${safeAccountError(err)}`);
    process.exitCode = 1;
    return;
  }

  const cred = await loadCredential();
  if (!cred) {
    console.log("Not logged in.");
    console.log("Run: zcode-proxy auth login <zai|bigmodel>");
    return;
  }
  console.log(`Logged in: ${cred.provider}`);
  console.log("  Credential: configured (redacted)");
  console.log(`  Store:   ${getStorePath()}`);
}

async function runOAuth(provider: ProviderId, pasteMode: boolean): Promise<OAuthResult> {
  if (provider === "bigmodel") {
    const oauth = new BigmodelOAuthClient();
    if (pasteMode) return runPasteLogin(oauth);
    const result = await oauth.authorize((url) => {
      console.log("Open this URL to authorize:\n");
      console.log(`  ${url}\n`);
      console.log("Waiting for authorization... (expires in 300s)\n");
      console.log(
        "Headless/Docker? The callback page will NOT load here — Ctrl-C and " +
        "re-run with `auth login bigmodel --paste` to paste the redirected URL instead.\n",
      );
      openBrowser(url);
    });
    return result;
  }

  const oauth = new ZaiOAuthClient();
  const result = await oauth.authorize((url) => {
    console.log("Open this URL to authorize:\n");
    console.log(`  ${url}\n`);
    console.log("Waiting for authorization... (expires in 300s)\n");
    openBrowser(url);
  });
  return result;
}

/**
 * Headless bigmodel login (`auth login bigmodel --paste`): the localhost
 * callback server is still bound — it defines the redirect port and the
 * browser can never reach it from inside a container anyway — but instead of
 * waiting on it, the user pastes the redirected URL back. The exact
 * `started.callbackUrl` string is used BOTH as the authorize `redirect` param
 * and as the exchange `redirect_uri` (the token endpoint requires them to
 * match), so the pair stays consistent by construction.
 */
async function runPasteLogin(oauth: BigmodelOAuthClient): Promise<OAuthResult> {
  const started = await oauth.start();
  try {
    console.log(pasteLoginInstructions(started.authorizeUrl, started.callbackUrl, LOGIN_TIMEOUT_MS));
    openBrowser(started.authorizeUrl);
    process.stdout.write("\n" + boldIfTTY("Paste the FULL redirected URL here, then press Enter:") + "\n> ");
    const pasted = await readPastedLine(LOGIN_TIMEOUT_MS);
    const code = parsePastedCallbackUrl(pasted, started.state);
    console.log("\nExchanging authorization code...");
    const tokens = await oauth.exchangeCode(code, started.callbackUrl, started.state);
    return { accessToken: tokens.accessToken, provider: "bigmodel", userId: tokens.userId, jwt: tokens.jwt };
  } finally {
    await oauth.close();
  }
}
