/**
 * # cli/context — the one capability resolver (Kestrel CLI v1 §2)
 *
 * `resolveOutputCtx` is the single place that decides HOW output renders. It is called **exactly
 * once** in {@link ./index.ts main} before dispatch; the frozen {@link OutputCtx} it returns is
 * threaded to every command handler. Nothing downstream re-probes `isTTY` — the golden rule is
 * enforced structurally: this resolver returns only rendering knobs, so the same inputs produce
 * the same data and the same exit code across all three modes.
 *
 * Pure + deterministic: it reads its `flags`/`env`/`streams` arguments, never `process` directly,
 * so it is trivially unit-testable (see `tests/cli/context.test.ts`). No heavy imports.
 */

import { CliError, EXIT } from "./errors.ts";

/** The three rendering skins. `human` = colored/aligned TTY; `text` = plain TSV/lines (agent
 * default, token-cheap); `json` = deterministic machine JSON. Data is identical across all three. */
export type RenderMode = "human" | "text" | "json";

/** The frozen rendering context every command reads instead of probing the terminal. */
export interface OutputCtx {
  readonly mode: RenderMode;
  readonly color: boolean;
  /** Columns; `Infinity` when non-TTY/machine (never wrap/truncate machine output). */
  readonly width: number;
  /** May prompt (both stdin+stdout TTY, not CI/agent). v1 never prompts; the flag guards future. */
  readonly interactive: boolean;
  /** May animate/spinner (stdout TTY, not CI/agent). v1 has no progress UI. */
  readonly stream: boolean;
}

interface Env {
  readonly [k: string]: string | undefined;
}

interface Streams {
  readonly stdinTTY: boolean;
  readonly stdoutTTY: boolean;
  readonly columns?: number;
}

/** The global rendering flags parsed by {@link ./args.ts} and stripped before command parsing. */
export interface GlobalFlags {
  readonly json?: boolean;
  readonly format?: string;
  readonly agent?: boolean;
  readonly color?: string;
  readonly noColor?: boolean;
  readonly api?: string; // selects the RemoteBackend; ignored by resolveOutputCtx (presentation-only)
}

function usageErr(message: string): CliError {
  return new CliError({ code: "USAGE", exit: EXIT.USAGE, message });
}

/**
 * Resolve the frozen render context. Precedence for **mode**: explicit `--json`/`--format` >
 * `--agent`/env > auto-detect (piped/CI → text, TTY → human). Color, width, interactivity, and
 * streaming are derived independently. See Kestrel CLI v1 §2 for the full precedence table.
 */
export function resolveOutputCtx(flags: GlobalFlags, env: Env, s: Streams): OutputCtx {
  const isCI = !!(env.CI || env.GITHUB_ACTIONS || env.BUILDKITE || env.GITLAB_CI);
  // A marker counts when NON-EMPTY (any value, including "0") — the SAME value semantics as
  // `resolveCaller`'s agent-env table (src/cli/caller.ts `detectAgentEnv`), so the two resolvers can
  // never disagree about whether `KESTREL_AGENT`/`AGENT` is set. Non-empty (not `=== "1"`) is the
  // fail-closed reading: a weird value (`AGENT=true`) resolves toward machine output, never toward
  // an interactive surface (ADR-0035 §b: under any ambiguity, non-interactive).
  const isSet = (v: string | undefined): boolean => v !== undefined && v !== "";
  const agent = flags.agent === true || isSet(env.KESTREL_AGENT) || isSet(env.AGENT);

  // --- mode ---
  let mode: RenderMode;
  if (flags.json === true) mode = "json";
  else if (flags.format !== undefined) {
    if (flags.format !== "json" && flags.format !== "text" && flags.format !== "human") {
      throw usageErr(`--format must be json|text|human, got ${JSON.stringify(flags.format)}`);
    }
    mode = flags.format === "human" ? "human" : (flags.format as RenderMode);
  } else if (agent) {
    mode = "text"; // agent alias defaults to text (token-cheap); pass --json to force json
  } else if (!s.stdoutTTY || isCI) {
    mode = "text"; // piped/redirected/CI → machine
  } else {
    mode = "human";
  }

  // --- interactivity / streaming (independent of the mode's format) ---
  const interactive = mode === "human" && s.stdinTTY && s.stdoutTTY && !isCI && !agent;
  const stream = mode === "human" && s.stdoutTTY && !isCI && !agent;

  // --- color ---
  const noColorFlag = flags.noColor === true;
  let color: boolean;
  if (mode !== "human") color = false; // machine output is never colored
  else if (noColorFlag) color = false; // --no-color is a NO_COLOR-equivalent request
  else if ("NO_COLOR" in env) color = false; // no-color.org: presence wins
  else if (flags.color === "always") color = true;
  else if (flags.color === "never") color = false;
  else if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "0") color = true;
  else if (env.FORCE_COLOR === "0") color = false;
  else if (env.KESTREL_COLOR === "always") color = true;
  else if (env.KESTREL_COLOR === "never") color = false;
  else color = s.stdoutTTY && !isCI;

  if (flags.color !== undefined && flags.color !== "always" && flags.color !== "never" && flags.color !== "auto") {
    throw usageErr(`--color must be always|never|auto, got ${JSON.stringify(flags.color)}`);
  }

  // --- width ---
  const width =
    mode === "human"
      ? (s.columns ?? (env.COLUMNS !== undefined ? Number(env.COLUMNS) : 0)) || 80
      : Infinity; // never wrap/truncate machine output

  return Object.freeze({ mode, color, width, interactive, stream });
}

/** Resolve the live context from `process` — the one place that touches the real terminal. */
export function resolveFromProcess(flags: GlobalFlags): OutputCtx {
  const stdout = process.stdout as NodeJS.WriteStream & { columns?: number };
  return resolveOutputCtx(flags, process.env as Env, {
    stdinTTY: Boolean(process.stdin.isTTY),
    stdoutTTY: Boolean(process.stdout.isTTY),
    ...(typeof stdout.columns === "number" ? { columns: stdout.columns } : {}),
  });
}
