/**
 * Masked password prompt — reads from stdin without echoing characters.
 *
 * Uses raw mode on stdin so we can intercept each keypress and suppress echo.
 * Handles:
 *   - printable characters appended to the buffer
 *   - backspace (0x7f / 0x08) removes last character
 *   - Enter (0x0d / 0x0a) completes the read
 *   - Ctrl+C (0x03) throws an error to abort
 *
 * Falls back to a plain readline prompt when stdin is not a TTY (e.g. tests /
 * piped input) so that callers can still provide a value programmatically.
 */

import { createInterface } from 'node:readline';

/**
 * Display `label` and read a single masked password from stdin.
 *
 * @param label - The prompt text, e.g. "Admin password: "
 * @returns The password string entered by the user.
 */
export async function promptMasked(label: string): Promise<string> {
  // Non-TTY: fall back to plain readline (value will echo, but that's the
  // caller's concern — headless callers should supply env vars instead).
  if (!process.stdin.isTTY) {
    return promptPlain(label);
  }

  process.stdout.write(label);

  return new Promise<string>((resolve, reject) => {
    const chunks: string[] = [];

    const onData = (chunk: Buffer): void => {
      for (let i = 0; i < chunk.length; i++) {
        const byte = chunk[i] as number;

        if (byte === 0x03) {
          // Ctrl+C — restore normal mode and abort
          cleanUp();
          process.stdout.write('\n');
          reject(new Error('Aborted by user (Ctrl+C)'));
          return;
        }

        if (byte === 0x0d || byte === 0x0a) {
          // Enter — done
          cleanUp();
          process.stdout.write('\n');
          resolve(chunks.join(''));
          return;
        }

        if (byte === 0x7f || byte === 0x08) {
          // Backspace / DEL
          if (chunks.length > 0) {
            chunks.pop();
            // Erase the last '*' on screen
            process.stdout.write('\b \b');
          }
          continue;
        }

        // Printable character
        if (byte >= 0x20 && byte <= 0x7e) {
          chunks.push(String.fromCharCode(byte));
          process.stdout.write('*');
        }
      }
    };

    function cleanUp(): void {
      process.stdin.setRawMode(false);
      process.stdin.pause();
      process.stdin.removeListener('data', onData);
    }

    process.stdin.setRawMode(true);
    process.stdin.resume();
    process.stdin.on('data', onData);
  });
}

/**
 * Prompt for a password twice, re-prompting until both entries match.
 * Applies the provided `validate` function once the two entries agree.
 *
 * @param label        - Prompt text for the first entry.
 * @param confirmLabel - Prompt text for the confirmation entry.
 * @param validate     - Optional validator; return an error string on failure.
 * @returns The confirmed password.
 */
export async function promptMaskedConfirmed(
  label: string,
  confirmLabel: string,
  validate?: (value: string) => string | undefined,
): Promise<string> {
  for (;;) {
    const first = await promptMasked(label);
    const second = await promptMasked(confirmLabel);

    if (first !== second) {
      console.log('Passwords do not match — please try again.\n');
      continue;
    }

    if (validate) {
      const error = validate(first);
      if (error) {
        console.log(`${error} — please try again.\n`);
        continue;
      }
    }

    return first;
  }
}

// ── Internal helpers ──────────────────────────────────────────────────────────

function promptPlain(label: string): Promise<string> {
  return new Promise((resolve) => {
    const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
    rl.question(label, (answer) => {
      rl.close();
      resolve(answer);
    });
  });
}
