// auth-mode.mjs — Detect and validate Claude auth mode for cron-safe runs.
// Zero dependencies. The detector and validator are pure-testable: both
// `runner` (claude CLI invoker) and `claudeJsonPath` (~/.claude.json) are
// dependency-injected so tests stub them rather than spawning real subprocess
// or touching the user's home directory.
//
// Subscription browser-OAuth tokens expire ~15h and are architecturally
// incompatible with cron. This lib surfaces that case as a hard fail so the
// installer/cron-runner can refuse to proceed.

import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';

/**
 * Default subprocess runner — invokes a command and returns its exit code.
 * Returns 0 on success, the actual exit code on failure, 127 on spawn error.
 */
function defaultRunner(cmd, args) {
  try {
    execFileSync(cmd, args, { stdio: 'ignore' });
    return 0;
  } catch (err) {
    if (typeof err.status === 'number') return err.status;
    return 127;
  }
}

/**
 * Safely read and parse a Claude config JSON file. Returns null on any error.
 * @param {string} path
 * @returns {object|null}
 */
export function readClaudeJson(path) {
  try {
    const text = readFileSync(path, 'utf8');
    const obj = JSON.parse(text);
    return obj && typeof obj === 'object' ? obj : null;
  } catch {
    return null;
  }
}

/**
 * Detect the active Claude authentication mode.
 *
 * Resolution order:
 *  1. ANTHROPIC_API_KEY env-var → 'api-key'
 *  2. CLAUDE_CODE_OAUTH_TOKEN env-var → 'long-oauth'
 *  3. ~/.claude.json onboarded + `claude auth status` exits 0 → 'subscription-browser-only'
 *  4. otherwise → 'unauthenticated'
 *
 * @param {object} [opts]
 * @param {(cmd: string, args: string[]) => number} [opts.runner]
 * @param {string} [opts.claudeJsonPath]
 * @param {object} [opts.env] — defaults to process.env
 * @returns {'api-key'|'long-oauth'|'subscription-browser-only'|'unauthenticated'}
 */
export function detectAuthMode(opts = {}) {
  const env = opts.env ?? process.env;
  const runner = opts.runner ?? defaultRunner;
  const claudeJsonPath = opts.claudeJsonPath ?? join(homedir(), '.claude.json');

  if (env.ANTHROPIC_API_KEY && env.ANTHROPIC_API_KEY.trim() !== '') {
    return 'api-key';
  }
  if (env.CLAUDE_CODE_OAUTH_TOKEN && env.CLAUDE_CODE_OAUTH_TOKEN.trim() !== '') {
    return 'long-oauth';
  }

  const claudeJson = readClaudeJson(claudeJsonPath);
  if (!claudeJson || claudeJson.hasCompletedOnboarding !== true) {
    return 'unauthenticated';
  }

  const exitCode = runner('claude', ['auth', 'status']);
  return exitCode === 0 ? 'subscription-browser-only' : 'unauthenticated';
}

/**
 * Throw a clear error if the detected mode is incompatible with cron.
 * Subscription-browser-only OAuth dies after ~15h; unauthenticated has no
 * credential. Both must be rejected before headless cron runs.
 *
 * @param {string} mode
 * @throws {Error} with code 'EAUTHCRON' if mode is not safe for cron
 */
export function validateAuthForCron(mode) {
  if (mode === 'api-key' || mode === 'long-oauth') return;
  const e = new Error(
    `Auth mode "${mode}" is not safe for cron. ` +
      'Run `claude setup-token` to generate a long-lived OAuth, ' +
      'or set ANTHROPIC_API_KEY in the cron environment.'
  );
  e.code = 'EAUTHCRON';
  e.detectedMode = mode;
  throw e;
}
