/**
 * # cli/commands/secrets — `kestrel secrets set|list|unset|path` (kestrel-jh9w.2)
 *
 * The operator-facing face of the `~/.kestrel/.env` store landed in jh9w.1
 * ({@link ../credentials.ts}). Four verbs, one rule: **a secret VALUE never appears in argv
 * and never appears in output.**
 *
 * - `set KEY` reads the value from an interactive prompt (echo suppressed) or from stdin with
 *   `--stdin`. A value passed POSITIONALLY (`secrets set KEY sk-live-…`) is REFUSED with a
 *   named code (`SECRET_VALUE_IN_ARGV`) before anything is written — argv lands in the shell's
 *   history file, in `ps` output, and in any process-supervisor log, so accepting it would leak
 *   the secret to three places the store exists to avoid. Refusing is the fail-closed choice:
 *   the alternative (accept-and-warn) still leaks.
 * - `list` prints NAMES ONLY, sorted — it is built on {@link secretNames}, which never returns
 *   a value, so no listing code path even HOLDS a value to accidentally render.
 * - `unset KEY` removes one key (absent ⇒ exit 3 NOT_FOUND, never a silent success).
 * - `path` prints the store location so an operator can inspect/rotate it by hand.
 *
 * Non-interactive + no `--stdin` is also a refusal (`SECRET_NO_INPUT`), not a hang and not a
 * silent empty value. Dependency-light (node built-ins only), so it stays on the LIGHT command
 * path with the rest of the credential surface.
 */

import { readFileSync } from "node:fs";
import { createInterface } from "node:readline";

import type { OutputCtx } from "../context.ts";
import { parseArgs } from "../args.ts";
import { CliError, EXIT } from "../errors.ts";
import { defaultSecretsPath, saveSecret, deleteSecret, secretNames, checkOperatorSecretKey } from "../credentials.ts";

/** Injected seams so the verb is hermetically testable (no real home, no real tty/stdin). */
export interface SecretsDeps {
  /** Where the store lives; default `$KESTREL_HOME/.kestrel/.env` (else `~`). */
  readonly secretsPath?: string;
  /** Read the whole of stdin (for `--stdin`); default reads fd 0. */
  readonly readStdin?: () => string;
  /** Prompt for a value with echo suppressed; default a muted readline over the tty. */
  readonly promptSecret?: (label: string) => Promise<string>;
  /** Environment (for KESTREL_HOME); default `process.env`. */
  readonly env?: Record<string, string | undefined>;
}

function usage(message: string, hint?: string): CliError {
  return new CliError({ code: "USAGE", exit: EXIT.USAGE, message, ...(hint !== undefined ? { hint } : {}) });
}

/** Read all of stdin as text (fd 0). Synchronous + dependency-free; `--stdin` is a one-shot pipe. */
function readStdinSync(): string {
  try {
    return readFileSync(0, "utf8");
  } catch {
    // A closed / unreadable stdin is NOT an empty secret — fail closed rather than store "".
    throw new CliError({
      code: "SECRET_NO_INPUT",
      exit: EXIT.USAGE,
      message: "--stdin was given but stdin could not be read",
      hint: "pipe the value: `printf %s \"$VALUE\" | kestrel secrets set KEY --stdin`",
    });
  }
}

/**
 * Prompt for a secret on the tty with echo SUPPRESSED. The prompt itself goes to stderr so
 * stdout stays a clean payload channel, and nothing typed is echoed or ever re-printed.
 */
function promptSecretTty(label: string): Promise<string> {
  return new Promise((resolve) => {
    const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
    // Suppress echo: readline re-renders the line on every keystroke through `_writeToOutput`;
    // replacing it with a prompt-only write keeps the typed characters off the screen.
    (rl as unknown as { _writeToOutput: (s: string) => void })._writeToOutput = (s: string) => {
      if (s.includes(label)) process.stderr.write(label);
    };
    rl.question(label, (answer) => {
      rl.close();
      process.stderr.write("\n");
      resolve(answer);
    });
  });
}

/** A value read from stdin carries the pipe's trailing newline; strip exactly one. */
function stripOneTrailingNewline(s: string): string {
  if (s.endsWith("\r\n")) return s.slice(0, -2);
  if (s.endsWith("\n")) return s.slice(0, -1);
  return s;
}

/**
 * `kestrel secrets <verb>` — the operator secret store's CLI face. Dispatches the four verbs;
 * an unknown/absent verb is a loud USAGE error (never a default verb).
 */
export async function secretsCommand(
  argv: readonly string[],
  ctx: OutputCtx,
  deps: SecretsDeps = {},
): Promise<number> {
  const parsed = parseArgs(argv, new Set(["stdin"]), new Set());
  const env = deps.env ?? (process.env as Record<string, string | undefined>);
  const path = deps.secretsPath ?? defaultSecretsPath(env);
  const [verb, ...rest] = parsed.positionals;

  switch (verb) {
    case "set":
      return await setSecret(rest, parsed.bools.has("stdin"), ctx, path, deps);
    case "list":
      return listSecrets(rest, parsed.bools.has("stdin"), ctx, path);
    case "unset":
      return unsetSecret(rest, ctx, path);
    case "path":
      return printPath(rest, ctx, path);
    case undefined:
      throw usage("missing secrets verb", "one of: set | list | unset | path");
    default:
      throw usage(`unknown secrets verb ${JSON.stringify(verb)}`, "one of: set | list | unset | path");
  }
}

async function setSecret(
  rest: readonly string[],
  wantStdin: boolean,
  ctx: OutputCtx,
  path: string,
  deps: SecretsDeps,
): Promise<number> {
  const key = rest[0];
  if (key === undefined) throw usage("missing KEY", "usage: kestrel secrets set KEY [--stdin]");
  // ── The operator-surface key policy (kestrel-jh9w.5.1): the paper-only live-broker refusal
  // (OSS-ADR-0054 §5) then the canonical-uppercase format check. This is the SAME `checkOperatorSecretKey`
  // the local MCP `kestrel.secrets.set` tool applies — the guard is single-sourced (credentials.ts), never
  // wired to only one face. A violation is refused fail-closed (nothing is read from stdin, nothing is
  // written) with its stable named code so an agent matches on the code, not on prose.
  const keyViolation = checkOperatorSecretKey(key);
  if (keyViolation !== undefined) {
    throw new CliError({ code: keyViolation.code, exit: EXIT.USAGE, message: keyViolation.message, hint: keyViolation.hint });
  }
  // ── The argv refusal. A value in argv has ALREADY leaked (shell history / `ps` / supervisor
  // logs) by the time we see it, so we refuse loudly and write NOTHING, rather than accepting it
  // and normalizing the habit. Named code so an agent can match on it, not on prose.
  if (rest.length > 1) {
    throw new CliError({
      code: "SECRET_VALUE_IN_ARGV",
      exit: EXIT.USAGE,
      message: `refusing to read a secret value from argv for ${JSON.stringify(key)} — argv leaks into shell history, \`ps\`, and process logs`,
      hint: `pipe it instead: printf %s "$VALUE" | kestrel secrets set ${key} --stdin   (or run \`kestrel secrets set ${key}\` on a tty to be prompted)`,
    });
  }

  let value: string;
  if (wantStdin) {
    value = stripOneTrailingNewline((deps.readStdin ?? readStdinSync)());
  } else if (ctx.interactive) {
    value = (await (deps.promptSecret ?? promptSecretTty)(`value for ${key}: `)).trim();
  } else {
    // Fail closed: no tty to prompt on and no --stdin ⇒ there is no non-leaking way to receive
    // the value. Never hang waiting on a pipe that may never come, never store an empty string.
    throw new CliError({
      code: "SECRET_NO_INPUT",
      exit: EXIT.USAGE,
      message: `no way to read the value for ${JSON.stringify(key)} — this session is not interactive and --stdin was not given`,
      hint: `printf %s "$VALUE" | kestrel secrets set ${key} --stdin`,
    });
  }
  if (value.length === 0) {
    throw new CliError({
      code: "SECRET_EMPTY",
      exit: EXIT.USAGE,
      message: `refusing to store an EMPTY value for ${JSON.stringify(key)}`,
      hint: `to remove it instead: kestrel secrets unset ${key}`,
    });
  }

  saveSecret(key, value, path);

  // Every output path below is REDACTED — the confirmation names the key and the file, never the
  // value (a terminal scrollback / CI log is exactly where a secret must not land).
  if (ctx.mode === "json") {
    process.stdout.write(JSON.stringify({ schema: "kestrel.secrets.set/v1", key, path, stored: true }) + "\n");
    return 0;
  }
  if (ctx.mode === "text") {
    process.stdout.write(`set\t${key}\t${path}\n`);
    return 0;
  }
  process.stdout.write(`stored ${key} in ${path} (value redacted, file 0600)\n`);
  return 0;
}

function listSecrets(rest: readonly string[], wantStdin: boolean, ctx: OutputCtx, path: string): number {
  if (rest.length > 0) throw usage(`unexpected argument ${JSON.stringify(rest[0])}`, "usage: kestrel secrets list");
  if (wantStdin) throw usage("--stdin is only valid for `kestrel secrets set`");
  // NAMES only — `secretNames` never returns a value, so this path cannot render one.
  const names = secretNames(path);
  if (ctx.mode === "json") {
    process.stdout.write(JSON.stringify({ schema: "kestrel.secrets.list/v1", path, names }) + "\n");
    return 0;
  }
  if (ctx.mode === "text") {
    process.stdout.write(names.map((n) => `secret\t${n}\n`).join(""));
    return 0;
  }
  if (names.length === 0) {
    process.stdout.write(`no secrets stored in ${path}\n`);
    return 0;
  }
  process.stdout.write(`${names.length} secret(s) in ${path} (names only, values never printed)\n`);
  process.stdout.write(names.map((n) => `  ${n}\n`).join(""));
  return 0;
}

function unsetSecret(rest: readonly string[], ctx: OutputCtx, path: string): number {
  const key = rest[0];
  if (key === undefined) throw usage("missing KEY", "usage: kestrel secrets unset KEY");
  if (rest.length > 1) throw usage(`unexpected argument ${JSON.stringify(rest[1])}`, "usage: kestrel secrets unset KEY");
  const removed = deleteSecret(key, path);
  if (!removed) {
    // Absent ⇒ a typed NOT_FOUND, never a silent success: an operator rotating a key must be able
    // to tell "removed" from "was never there under that name".
    throw new CliError({
      code: "NOT_FOUND",
      exit: EXIT.NOT_FOUND,
      message: `no secret named ${JSON.stringify(key)} in ${path}`,
      hint: "run `kestrel secrets list` for the stored names",
    });
  }
  if (ctx.mode === "json") {
    process.stdout.write(JSON.stringify({ schema: "kestrel.secrets.unset/v1", key, path, removed: true }) + "\n");
    return 0;
  }
  if (ctx.mode === "text") {
    process.stdout.write(`unset\t${key}\t${path}\n`);
    return 0;
  }
  process.stdout.write(`removed ${key} from ${path}\n`);
  return 0;
}

function printPath(rest: readonly string[], ctx: OutputCtx, path: string): number {
  if (rest.length > 0) throw usage(`unexpected argument ${JSON.stringify(rest[0])}`, "usage: kestrel secrets path");
  if (ctx.mode === "json") {
    process.stdout.write(JSON.stringify({ schema: "kestrel.secrets.path/v1", path }) + "\n");
    return 0;
  }
  process.stdout.write(`${path}\n`);
  return 0;
}
