/**
 * # session/harness/harness-client — the ONE {@link LlmClient} factory (routes a {@link HarnessProvider} to its lane)
 *
 * The harness has two KINDS of serving lane and they cannot share a constructor:
 *
 *   - **SDK lanes** (`bedrock` / `anthropic` / `google` / `gateway` / `local`) — HTTP, built by
 *     `aiSdkLlmClient` in `./ai-sdk-client.ts` (the ONE module that imports the provider packages).
 *   - **The SUBPROCESS lane** (`codex-cli`) — the ZERO-CASH ChatGPT subscription, built by `codexCliClient`
 *     in `./codex-cli-client.ts`. It shells out to `codex exec` because no OpenAI-compatible HTTP endpoint
 *     accepts a ChatGPT-subscription OAuth token.
 *
 * Callers should not care. This module is the single seam they call — give it a provider + model id, get an
 * `LlmClient`. It exists so that adding the subscription lane did not require every runner
 * (`run-grid`, `eval-watcher`, the fan fleets) to learn a second construction path and risk silently falling
 * back to a CASH provider when the FREE one was available.
 *
 * ## The `subagent` lane is NOT here — and that is not an omission
 * A Claude Code subagent is ONE-SHOT: it is spawned, it does work, it returns text. It cannot be driven
 * turn-by-turn inside the harness's multi-turn loop, so there is no `subagent` {@link LlmClient} to build and
 * no `subagent` member of {@link HarnessProvider}. Claude-via-subagent is an **AUTHORING** lane — it produces
 * a plan DOC which is frozen into a fixture (`scripts/bench/author-cells.ts`). MULTI-TURN WATCHING still
 * requires a real API endpoint (bedrock / gateway / local). That is the shape of the thing, not a gap.
 */

import { aiSdkLlmClient, type AiSdkClientOptions, type HarnessProvider } from "./ai-sdk-client.ts";
import { codexCliClient, type CodexCliClientOptions } from "./codex-cli-client.ts";
import type { LlmClient } from "../agent.ts";

/** Options for {@link harnessLlmClient} — the SDK options, plus the subprocess lane's extras (ignored off
 * the `codex-cli` lane, so every existing call site can pass exactly what it passes today). */
export interface HarnessClientOptions extends AiSdkClientOptions {
  /** `codex-cli` ONLY: reasoning effort for the turn (default `low` — see {@link codexCliClient}). */
  readonly reasoningEffort?: CodexCliClientOptions["reasoningEffort"];
  /** `codex-cli` ONLY: per-call wall-clock ceiling (a subprocess turn is slow; default 180s). */
  readonly codexTimeoutMs?: number;
  /** `codex-cli` ONLY test seam: a stand-in for the real `codex exec` subprocess. */
  readonly codexExec?: CodexCliClientOptions["exec"];
}

/** Does this lane serve at ZERO MARGINAL CASH? `local` is free hardware; `codex-cli` is a subscription
 * already paid for. Mirrors `scripts/bench/pricing.ts`'s `FREE_CASH_LANES` at the provider level, so a runner
 * can log the cost posture of the lane it just built without importing the bench pricing module. */
export function isFreeProvider(provider: HarnessProvider): boolean {
  return provider === "local" || provider === "codex-cli";
}

/** Whether a provider's OWN credential is available, and — when absent — the human name of what is missing.
 * Every lane authenticates from a DIFFERENT source, so "is the Bedrock key present?" is the wrong question
 * for any lane but Bedrock: a codex-cli entrant (ChatGPT OAuth in `$CODEX_HOME/auth.json`) or a local
 * entrant (no credential) must run whether or not an AWS key exists. A runner asks THIS, per model, so it
 * skips a lane only when ITS OWN credential is missing — and can say which one. */
export interface CredentialStatus {
  readonly ready: boolean;
  /** The env var / file a present credential would come from — for logging either way. */
  readonly credential: string;
  /** Set only when `ready === false`: the human-facing reason the lane can't serve. */
  readonly missing?: string;
}

/**
 * Resolve whether `provider`'s own credential is present in the environment.
 *
 *   - `bedrock`   → `AWS_BEARER_TOKEN_BEDROCK`
 *   - `gateway`   → `AI_GATEWAY_API_KEY`
 *   - `google`    → `GOOGLE_API_KEY` / `GEMINI_API_KEY` / `GOOGLE_GENERATIVE_AI_API_KEY` (first present)
 *   - `anthropic` → `ANTHROPIC_API_KEY`
 *   - `codex-cli` → the ChatGPT-subscription OAuth in `$CODEX_HOME/auth.json` (NOT an env key)
 *   - `local`     → no credential (a local server authenticates itself)
 */
export function providerCredentialStatus(provider: HarnessProvider, env: NodeJS.ProcessEnv = process.env): CredentialStatus {
  const present = (name: string): boolean => typeof env[name] === "string" && env[name]!.trim().length > 0;
  switch (provider) {
    case "bedrock":
      return present("AWS_BEARER_TOKEN_BEDROCK")
        ? { ready: true, credential: "AWS_BEARER_TOKEN_BEDROCK" }
        : { ready: false, credential: "AWS_BEARER_TOKEN_BEDROCK", missing: "AWS_BEARER_TOKEN_BEDROCK absent (Bedrock credits lane)" };
    case "gateway":
      return present("AI_GATEWAY_API_KEY")
        ? { ready: true, credential: "AI_GATEWAY_API_KEY" }
        : { ready: false, credential: "AI_GATEWAY_API_KEY", missing: "AI_GATEWAY_API_KEY absent (Vercel AI Gateway CASH lane)" };
    case "google": {
      const names = ["GOOGLE_API_KEY", "GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"];
      return names.some(present)
        ? { ready: true, credential: names.find(present)! }
        : { ready: false, credential: names.join(" / "), missing: `${names.join(" / ")} all absent (Vertex/Gemini lane)` };
    }
    case "anthropic":
      return present("ANTHROPIC_API_KEY")
        ? { ready: true, credential: "ANTHROPIC_API_KEY" }
        : { ready: false, credential: "ANTHROPIC_API_KEY", missing: "ANTHROPIC_API_KEY absent (direct Anthropic CASH lane)" };
    case "codex-cli": {
      // The subscription auth lives in a FILE, not an env var. Presence is checked by the client at
      // construction (it resolves the binary and the CLI reads $CODEX_HOME/auth.json); here we only report
      // that this lane needs no ENV credential, so a runner never skips it for a missing AWS/gateway key.
      const home = env.CODEX_HOME ?? `${env.HOME ?? "~"}/.codex`;
      return { ready: true, credential: `${home}/auth.json (ChatGPT OAuth)` };
    }
    case "local":
      return { ready: true, credential: env.LOCAL_LLM_BASE_URL ?? "LOCAL_LLM_BASE_URL (default localhost)" };
    case "openai":
      return present("OPENAI_API_KEY")
        ? { ready: true, credential: "OPENAI_API_KEY" }
        : { ready: false, credential: "OPENAI_API_KEY", missing: "OPENAI_API_KEY absent (direct OpenAI CASH lane)" };
    case "azure":
      // Azure needs BOTH the endpoint and the key; report the first missing one so a runner can act on it.
      return present("AZURE_OPENAI_ENDPOINT") && present("AZURE_OPENAI_API_KEY")
        ? { ready: true, credential: "AZURE_OPENAI_API_KEY" }
        : {
            ready: false,
            credential: "AZURE_OPENAI_ENDPOINT + AZURE_OPENAI_API_KEY",
            missing: `${present("AZURE_OPENAI_ENDPOINT") ? "" : "AZURE_OPENAI_ENDPOINT "}${present("AZURE_OPENAI_API_KEY") ? "" : "AZURE_OPENAI_API_KEY "}absent (Azure OpenAI CREDITS lane)`.trim(),
          };
    case "fireworks":
      return present("FIREWORKS_API_KEY")
        ? { ready: true, credential: "FIREWORKS_API_KEY" }
        : { ready: false, credential: "FIREWORKS_API_KEY", missing: "FIREWORKS_API_KEY absent (Fireworks hosted-open CASH lane)" };
    case "vertex-maas": {
      // Auth is an ADC BEARER (gcloud application-default), not an env key — like codex-cli, the
      // credential lives outside the env, and a dead ADC fails loudly at call time (AdcAuthError names
      // the reauth command). What IS env-gated is the GCP PROJECT id: the endpoint URL embeds it, so
      // without one the lane cannot even construct. Report that as the missing piece.
      const projectVars = ["GOOGLE_VERTEX_PROJECT", "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT"];
      const projectVar = projectVars.find(present);
      return projectVar !== undefined
        ? { ready: true, credential: `gcloud ADC bearer + ${projectVar} (Vertex MaaS CREDITS lane)` }
        : {
            ready: false,
            credential: `gcloud ADC bearer + ${projectVars.join(" / ")}`,
            missing: `${projectVars.join(" / ")} all absent (Vertex Model Garden MaaS CREDITS lane — the endpoint URL embeds the project id)`,
          };
    }
  }
}

/**
 * Build the {@link LlmClient} for `provider` — the SINGLE construction seam every runner should use.
 *
 * Routes `codex-cli` to the subscription subprocess lane and everything else to the AI SDK. Never falls back
 * between lanes: a `codex-cli` request that cannot be served FAILS (the CLI is missing / the model id is
 * empty) rather than quietly becoming a CASH gateway call — the cost regression this lane exists to prevent.
 */
export function harnessLlmClient(opts: HarnessClientOptions): LlmClient {
  if (opts.provider === "codex-cli") {
    return codexCliClient({
      modelId: opts.modelId,
      ...(opts.reasoningEffort !== undefined ? { reasoningEffort: opts.reasoningEffort } : {}),
      ...(opts.codexTimeoutMs !== undefined ? { timeoutMs: opts.codexTimeoutMs } : {}),
      ...(opts.secrets !== undefined ? { secrets: opts.secrets } : {}),
      ...(opts.captureWire !== undefined ? { captureWire: opts.captureWire } : {}),
      ...(opts.codexExec !== undefined ? { exec: opts.codexExec } : {}),
    });
  }
  return aiSdkLlmClient(opts);
}
