#!/usr/bin/env node

/**
 * report.ts
 *
 * Export a diagnostic report for GitHub Issues.
 * Collects system info, doctor output, logs, manifests, and registry info.
 */

import fs from 'fs';
import os from 'os';
import path from 'path';
import { execFileSync, spawnSync } from 'child_process';
import { COMMAND_NAME } from './constant';
import {
  BrowserType,
  detectInstalledBrowsers,
  getBrowserConfig,
  parseBrowserType,
} from './browser-config';
import { getLogDir } from './utils';
import { collectDoctorReport, DoctorReport } from './doctor';

const REPORT_SCHEMA_VERSION = 1;
const DEFAULT_LOG_LINES = 200;
const DEFAULT_TAIL_BYTES = 256 * 1024;
const MAX_LOG_FILES = 6;
const MAX_FULL_LOG_BYTES = 1024 * 1024;

type IncludeLogsMode = 'none' | 'tail' | 'full';

export interface ReportOptions {
  json?: boolean;
  output?: string;
  copy?: boolean;
  redact?: boolean;
  includeLogs?: string;
  logLines?: number;
  browser?: string;
}

interface VersionResult {
  version?: string;
  error?: string;
}

interface ManifestSnapshot {
  browser: string;
  scope: 'user' | 'system';
  path: string;
  exists: boolean;
  json?: unknown;
  raw?: string;
  error?: string;
}

interface LogFileSnapshot {
  name: string;
  path: string;
  mtime?: string;
  size?: number;
  note?: string;
  content?: string;
  truncated?: boolean;
  error?: string;
}

interface WrapperLogsSnapshot {
  dir: string;
  mode: IncludeLogsMode;
  files: LogFileSnapshot[];
  error?: string;
}

interface WindowsRegistryEntrySnapshot {
  browser: string;
  scope: 'user' | 'system';
  key: string;
  expectedManifestPath: string;
  value?: string;
  raw?: string;
  error?: string;
}

interface WindowsRegistrySnapshot {
  entries: WindowsRegistryEntrySnapshot[];
}

export interface DiagnosticReport {
  schemaVersion: number;
  timestamp: string;
  tool: {
    name: string;
    version: string;
  };
  environment: {
    platform: NodeJS.Platform;
    arch: string;
    node: {
      version: string;
      execPath: string;
    };
    os: {
      type: string;
      release: string;
      version?: string;
    };
    cwd: string;
    env: Record<string, string | null>;
  };
  packageManager: {
    npm: VersionResult;
    pnpm: VersionResult;
  };
  doctor?: DoctorReport;
  doctorError?: string;
  manifests: ManifestSnapshot[];
  wrapperLogs: WrapperLogsSnapshot;
  windowsRegistry?: WindowsRegistrySnapshot;
  redaction: {
    enabled: boolean;
  };
}

function stringifyError(err: unknown): string {
  if (err instanceof Error) return err.message;
  return String(err);
}

function readPackageJson(): Record<string, unknown> {
  try {
    return require('../../package.json') as Record<string, unknown>;
  } catch {
    return {};
  }
}

function getToolVersion(): { name: string; version: string } {
  const pkg = readPackageJson();
  const name = typeof pkg.name === 'string' ? pkg.name : COMMAND_NAME;
  const version = typeof pkg.version === 'string' ? pkg.version : 'unknown';
  return { name, version };
}

function safeOsVersion(): string | undefined {
  try {
    return os.version();
  } catch {
    return undefined;
  }
}

function safeExecVersion(command: string): VersionResult {
  try {
    const out = execFileSync(command, ['-v'], {
      encoding: 'utf8',
      stdio: ['ignore', 'pipe', 'pipe'],
      timeout: 2500,
      windowsHide: true,
    });
    return { version: out.trim() };
  } catch (e) {
    return { error: stringifyError(e) };
  }
}

function parseIncludeLogsMode(raw: unknown): IncludeLogsMode {
  const v = typeof raw === 'string' ? raw.toLowerCase() : '';
  if (v === 'none' || v === 'tail' || v === 'full') return v;
  return 'tail';
}

function parsePositiveInt(raw: unknown, fallback: number): number {
  if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) return Math.floor(raw);
  if (typeof raw === 'string') {
    const parsed = Number.parseInt(raw, 10);
    if (Number.isFinite(parsed) && parsed > 0) return parsed;
  }
  return fallback;
}

function resolveBrowsers(browserArg: string | undefined): BrowserType[] {
  if (!browserArg) {
    const detected = detectInstalledBrowsers();
    return detected.length > 0 ? detected : [BrowserType.CHROME, BrowserType.CHROMIUM];
  }

  const normalized = browserArg.toLowerCase();
  if (normalized === 'all') return [BrowserType.CHROME, BrowserType.CHROMIUM];
  if (normalized === 'detect' || normalized === 'auto') {
    const detected = detectInstalledBrowsers();
    return detected.length > 0 ? detected : [BrowserType.CHROME, BrowserType.CHROMIUM];
  }

  const parsed = parseBrowserType(normalized);
  if (!parsed) {
    throw new Error(`Invalid browser: ${browserArg}. Use 'chrome', 'chromium', or 'all'`);
  }
  return [parsed];
}

function readJsonSnapshot(filePath: string): {
  exists: boolean;
  json?: unknown;
  raw?: string;
  error?: string;
} {
  try {
    if (!fs.existsSync(filePath)) return { exists: false };
    const raw = fs.readFileSync(filePath, 'utf8');
    try {
      const json = JSON.parse(raw) as unknown;
      return { exists: true, json };
    } catch (e) {
      return { exists: true, raw, error: `Failed to parse JSON: ${stringifyError(e)}` };
    }
  } catch (e) {
    return { exists: fs.existsSync(filePath), error: stringifyError(e) };
  }
}

function collectManifests(browsers: BrowserType[]): ManifestSnapshot[] {
  const results: ManifestSnapshot[] = [];
  for (const browser of browsers) {
    const config = getBrowserConfig(browser);
    for (const scope of ['user', 'system'] as const) {
      const manifestPaths = Array.from(
        new Set(
          scope === 'user'
            ? (config.userManifestPaths?.length ? config.userManifestPaths : [config.userManifestPath])
            : (config.systemManifestPaths?.length
                ? config.systemManifestPaths
                : [config.systemManifestPath]),
        ),
      );
      for (const manifestPath of manifestPaths) {
        const snap = readJsonSnapshot(manifestPath);
        results.push({
          browser,
          scope,
          path: manifestPath,
          exists: snap.exists,
          json: snap.json,
          raw: snap.raw,
          error: snap.error,
        });
      }
    }
  }
  return results;
}

function readFileTail(
  filePath: string,
  maxBytes: number,
  maxLines: number,
): { content: string; truncated: boolean } {
  const stat = fs.statSync(filePath);
  const size = stat.size;
  const bytesToRead = Math.min(size, maxBytes);
  const start = Math.max(0, size - bytesToRead);

  const fd = fs.openSync(filePath, 'r');
  try {
    const buf = Buffer.alloc(bytesToRead);
    fs.readSync(fd, buf, 0, bytesToRead, start);
    const text = buf.toString('utf8');
    const lines = text.split(/\r?\n/);
    const tail = lines.slice(Math.max(0, lines.length - maxLines));
    return { content: tail.join('\n'), truncated: size > maxBytes || lines.length > maxLines };
  } finally {
    fs.closeSync(fd);
  }
}

function readFileLastBytes(
  filePath: string,
  maxBytes: number,
): { content: string; truncated: boolean } {
  const stat = fs.statSync(filePath);
  const size = stat.size;
  if (size <= maxBytes) {
    const content = fs.readFileSync(filePath, 'utf8');
    return { content, truncated: false };
  }

  const bytesToRead = maxBytes;
  const start = Math.max(0, size - bytesToRead);

  const fd = fs.openSync(filePath, 'r');
  try {
    const buf = Buffer.alloc(bytesToRead);
    fs.readSync(fd, buf, 0, bytesToRead, start);
    const content = buf.toString('utf8');
    return { content, truncated: true };
  } finally {
    fs.closeSync(fd);
  }
}

function collectWrapperLogs(
  logDir: string,
  mode: IncludeLogsMode,
  logLines: number,
): WrapperLogsSnapshot {
  if (!fs.existsSync(logDir)) {
    return { dir: logDir, mode, files: [], error: 'Log directory does not exist' };
  }

  const prefixes = ['native_host_wrapper_', 'native_host_stderr_'];
  let entries: fs.Dirent[] = [];
  try {
    entries = fs.readdirSync(logDir, { withFileTypes: true });
  } catch (e) {
    return { dir: logDir, mode, files: [], error: stringifyError(e) };
  }

  const candidates = entries
    .filter((ent) => ent.isFile())
    .map((ent) => ent.name)
    .filter((name) => name.endsWith('.log') && prefixes.some((p) => name.startsWith(p)));

  const filesWithStat: Array<{ name: string; fullPath: string; mtimeMs: number; size: number }> =
    [];
  for (const name of candidates) {
    const fullPath = path.join(logDir, name);
    try {
      const stat = fs.statSync(fullPath);
      filesWithStat.push({ name, fullPath, mtimeMs: stat.mtimeMs, size: stat.size });
    } catch {
      // ignore
    }
  }

  filesWithStat.sort((a, b) => b.mtimeMs - a.mtimeMs);

  const selected = filesWithStat.slice(0, MAX_LOG_FILES);
  const snapshots: LogFileSnapshot[] = [];

  for (const file of selected) {
    const snap: LogFileSnapshot = {
      name: file.name,
      path: file.fullPath,
      mtime: new Date(file.mtimeMs).toISOString(),
      size: file.size,
    };

    if (mode !== 'none') {
      try {
        if (mode === 'tail') {
          const read = readFileTail(file.fullPath, DEFAULT_TAIL_BYTES, logLines);
          snap.content = read.content;
          snap.truncated = read.truncated;
          snap.note = `Tail: last ${logLines} lines (from last ${DEFAULT_TAIL_BYTES} bytes)`;
        } else {
          const read = readFileLastBytes(file.fullPath, MAX_FULL_LOG_BYTES);
          snap.content = read.content;
          snap.truncated = read.truncated;
          snap.note = read.truncated
            ? `Truncated: showing last ${MAX_FULL_LOG_BYTES} bytes`
            : 'Full file';
        }
      } catch (e) {
        snap.error = stringifyError(e);
      }
    } else {
      snap.note = 'Content omitted';
    }

    snapshots.push(snap);
  }

  return { dir: logDir, mode, files: snapshots };
}

function queryWindowsRegistryDefaultValue(registryKey: string): {
  value?: string;
  raw?: string;
  error?: string;
} {
  try {
    const output = execFileSync('reg', ['query', registryKey, '/ve'], {
      encoding: 'utf8',
      stdio: ['ignore', 'pipe', 'pipe'],
      timeout: 2500,
      windowsHide: true,
    });
    const lines = output
      .split(/\r?\n/)
      .map((l) => l.trim())
      .filter(Boolean);
    for (const line of lines) {
      const match = line.match(/REG_SZ\s+(.*)$/i);
      if (match?.[1]) return { value: match[1].trim(), raw: output };
    }
    return { raw: output, error: 'No REG_SZ default value found' };
  } catch (e) {
    return { error: stringifyError(e) };
  }
}

function collectWindowsRegistry(browsers: BrowserType[]): WindowsRegistrySnapshot {
  const entries: WindowsRegistryEntrySnapshot[] = [];

  for (const browser of browsers) {
    const config = getBrowserConfig(browser);
    const keySpecs = [
      config.registryKey
        ? { key: config.registryKey, scope: 'user' as const, expected: config.userManifestPath }
        : null,
      config.systemRegistryKey
        ? {
            key: config.systemRegistryKey,
            scope: 'system' as const,
            expected: config.systemManifestPath,
          }
        : null,
    ].filter(Boolean) as Array<{ key: string; scope: 'user' | 'system'; expected: string }>;

    for (const spec of keySpecs) {
      const res = queryWindowsRegistryDefaultValue(spec.key);
      entries.push({
        browser,
        scope: spec.scope,
        key: spec.key,
        expectedManifestPath: spec.expected,
        value: res.value,
        raw: res.raw,
        error: res.error,
      });
    }
  }

  return { entries };
}

// ============================================================================
// Redaction
// ============================================================================

function escapeRegExp(input: string): string {
  return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function buildLiteralReplacements(): Array<[RegExp, string]> {
  const replacements: Array<[RegExp, string]> = [];
  const ignoreCase = process.platform === 'win32';

  const addLiteral = (literal: string | undefined, replacement: string): void => {
    if (!literal) return;
    const variants = new Set<string>();
    variants.add(literal);
    variants.add(literal.replace(/\\/g, '/'));
    variants.add(literal.replace(/\//g, '\\'));

    for (const v of variants) {
      if (!v) continue;
      replacements.push([new RegExp(escapeRegExp(v), ignoreCase ? 'gi' : 'g'), replacement]);
    }
  };

  addLiteral(os.homedir(), '<HOME>');
  addLiteral(process.env.USERPROFILE, '<USERPROFILE>');
  addLiteral(process.env.HOME, '<HOME>');

  try {
    const username = os.userInfo().username;
    if (username) {
      replacements.push([
        new RegExp(`\\b${escapeRegExp(username)}\\b`, ignoreCase ? 'gi' : 'g'),
        '<USER>',
      ]);
    }
  } catch {
    // ignore
  }

  return replacements;
}

function createRedactor(enabled: boolean): (input: string) => string {
  if (!enabled) return (s) => s;

  const literalReplacements = buildLiteralReplacements();
  const patternReplacements: Array<[RegExp, string]> = [
    // Sensitive key=value patterns (supports JSON-style "key": "value" and env-style KEY=value)
    [
      /(\b[A-Z0-9_]*(?:TOKEN|PASSWORD|SECRET|API_KEY|ACCESS_KEY|PRIVATE_KEY)\b)(\s*["']?\s*[:=]\s*["']?)([^\s"']+)/gi,
      '$1$2<REDACTED>',
    ],
    // HTTP Authorization headers
    [/(Authorization:\s*Bearer\s+)[^\s]+/gi, '$1<REDACTED>'],
    [/(Authorization:\s*Basic\s+)[^\s]+/gi, '$1<REDACTED>'],
    // JSON-style Authorization fields ("Authorization": "Bearer ...")
    [
      /(\bAuthorization\b)(\s*["']?\s*[:=]\s*["']?)(Bearer\s+|Basic\s+)?[^\s"']+/gi,
      '$1$2$3<REDACTED>',
    ],
    // Cookies
    [/(Cookie:\s*)[^\r\n]+/gi, '$1<REDACTED>'],
    [/(Set-Cookie:\s*)[^\r\n]+/gi, '$1<REDACTED>'],
    // JSON-style Cookie fields ("Cookie": "...")
    [/(\b(?:Cookie|Set-Cookie)\b)(\s*["']?\s*[:=]\s*["']?)[^\r\n"']+/gi, '$1$2<REDACTED>'],
    // Common API header patterns (supports JSON-style)
    [
      /(\b(?:x-api-key|api-key|x-auth-token|x-access-token)\b)(\s*["']?\s*[:=]\s*["']?)([^\s"']+)/gi,
      '$1$2<REDACTED>',
    ],
    // Email addresses
    [/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '<EMAIL>'],
    // User paths (Windows and macOS/Linux)
    [/[A-Z]:\\Users\\[^\\]+/gi, '<USERPROFILE>'],
    [/\/Users\/[^/]+/g, '/Users/<USER>'],
  ];

  return (input: string): string => {
    let out = input;
    for (const [re, replacement] of literalReplacements) {
      out = out.replace(re, replacement);
    }
    for (const [re, replacement] of patternReplacements) {
      out = out.replace(re, replacement);
    }
    return out;
  };
}

function redactDeep(value: unknown, redact: (s: string) => string): unknown {
  if (typeof value === 'string') return redact(value);
  if (Array.isArray(value)) return value.map((v) => redactDeep(v, redact));
  if (value && typeof value === 'object') {
    const obj = value as Record<string, unknown>;
    const out: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(obj)) {
      out[k] = redactDeep(v, redact);
    }
    return out;
  }
  return value;
}

// ============================================================================
// Output Rendering
// ============================================================================

function renderMarkdown(report: DiagnosticReport): string {
  const lines: string[] = [];

  lines.push(`# ${report.tool.name} Diagnostic Report`);
  lines.push('');
  lines.push(`**Generated:** ${report.timestamp}`);
  lines.push(`**Redaction:** ${report.redaction.enabled ? 'enabled (default)' : 'disabled'}`);
  lines.push('');

  lines.push('## Environment');
  lines.push('');
  lines.push(`- **Platform:** ${report.environment.platform} (${report.environment.arch})`);
  lines.push(
    `- **OS:** ${report.environment.os.type} ${report.environment.os.release}${
      report.environment.os.version ? ` (${report.environment.os.version})` : ''
    }`,
  );
  lines.push(`- **Node:** ${report.environment.node.version}`);
  lines.push(`- **Node execPath:** \`${report.environment.node.execPath}\``);
  lines.push(`- **CWD:** \`${report.environment.cwd}\``);
  lines.push('');

  lines.push('## Package Managers');
  lines.push('');
  lines.push(
    `- **npm:** ${
      report.packageManager.npm.version ?? `ERROR: ${report.packageManager.npm.error ?? 'unknown'}`
    }`,
  );
  lines.push(
    `- **pnpm:** ${
      report.packageManager.pnpm.version ??
      `ERROR: ${report.packageManager.pnpm.error ?? 'unknown'}`
    }`,
  );
  lines.push('');

  lines.push('## Relevant Environment Variables');
  lines.push('');
  for (const [k, v] of Object.entries(report.environment.env)) {
    lines.push(`- \`${k}\`: ${v ?? '<unset>'}`);
  }
  lines.push('');

  lines.push('## Doctor Output');
  lines.push('');
  if (report.doctor) {
    lines.push('<details>');
    lines.push('<summary>Click to expand doctor JSON</summary>');
    lines.push('');
    lines.push('```json');
    lines.push(JSON.stringify(report.doctor, null, 2));
    lines.push('```');
    lines.push('</details>');
  } else {
    lines.push(`**Doctor failed:** ${report.doctorError ?? 'unknown error'}`);
  }
  lines.push('');

  lines.push('## Wrapper Logs');
  lines.push('');
  lines.push(`**Log directory:** \`${report.wrapperLogs.dir}\``);
  lines.push(`**Mode:** ${report.wrapperLogs.mode}`);
  if (report.wrapperLogs.error) {
    lines.push(`**Error:** ${report.wrapperLogs.error}`);
  }
  lines.push('');
  if (report.wrapperLogs.files.length === 0) {
    lines.push('No wrapper logs found.');
  } else {
    for (const f of report.wrapperLogs.files) {
      lines.push(`### ${f.name}`);
      lines.push('');
      lines.push(`- **Path:** \`${f.path}\``);
      if (f.mtime) lines.push(`- **Modified:** ${f.mtime}`);
      if (typeof f.size === 'number') lines.push(`- **Size:** ${f.size} bytes`);
      if (f.note) lines.push(`- **Note:** ${f.note}`);
      if (f.error) {
        lines.push(`- **Error:** ${f.error}`);
        lines.push('');
        continue;
      }
      if (typeof f.content === 'string') {
        if (f.truncated) lines.push('*(Truncated)*');
        lines.push('');
        lines.push('<details>');
        lines.push('<summary>Click to expand log content</summary>');
        lines.push('');
        lines.push('```text');
        lines.push(f.content);
        lines.push('```');
        lines.push('</details>');
      } else {
        lines.push('*(Content omitted)*');
      }
      lines.push('');
    }
  }
  lines.push('');

  lines.push('## Manifests');
  lines.push('');
  for (const m of report.manifests) {
    lines.push(`### ${m.browser} (${m.scope})`);
    lines.push('');
    lines.push(`- **Path:** \`${m.path}\``);
    if (!m.exists) {
      lines.push('- **Status:** not found');
      lines.push('');
      continue;
    }
    if (m.error) {
      lines.push(`- **Status:** error (${m.error})`);
    }
    if (m.json !== undefined) {
      lines.push('');
      lines.push('```json');
      lines.push(JSON.stringify(m.json, null, 2));
      lines.push('```');
    } else if (typeof m.raw === 'string') {
      lines.push('');
      lines.push('```text');
      lines.push(m.raw);
      lines.push('```');
    }
    lines.push('');
  }

  if (report.windowsRegistry) {
    lines.push('## Windows Registry');
    lines.push('');
    for (const entry of report.windowsRegistry.entries) {
      lines.push(`### ${entry.browser} (${entry.scope})`);
      lines.push('');
      lines.push(`- **Key:** \`${entry.key}\``);
      lines.push(`- **Expected manifest:** \`${entry.expectedManifestPath}\``);
      if (entry.error) {
        lines.push(`- **Error:** ${entry.error}`);
        lines.push('');
        continue;
      }
      if (entry.value) lines.push(`- **Default value:** \`${entry.value}\``);
      if (entry.raw) {
        lines.push('');
        lines.push('```text');
        lines.push(entry.raw);
        lines.push('```');
      }
      lines.push('');
    }
  }

  lines.push('---');
  lines.push('');
  lines.push(
    '> If you are opening a GitHub Issue, paste everything above. ' +
      `You can disable redaction with: \`${report.tool.name} report --no-redact\``,
  );

  return lines.join('\n');
}

function writeOutput(
  outputPath: string | undefined,
  content: string,
): { ok: true; destination: string } | { ok: false; error: string } {
  if (!outputPath || outputPath === '-' || outputPath.toLowerCase() === 'stdout') {
    process.stdout.write(content);
    return { ok: true, destination: 'stdout' };
  }

  try {
    const resolved = path.resolve(outputPath);
    fs.writeFileSync(resolved, content, 'utf8');
    return { ok: true, destination: resolved };
  } catch (e) {
    return { ok: false, error: stringifyError(e) };
  }
}

function tryCopyToClipboard(text: string): { ok: boolean; method?: string; error?: string } {
  const spawn = (cmd: string, args: string[]): { ok: boolean; error?: string } => {
    const res = spawnSync(cmd, args, {
      input: text,
      encoding: 'utf8',
      timeout: 3000,
      windowsHide: true,
    });
    if (res.error) return { ok: false, error: stringifyError(res.error) };
    if (res.status !== 0) return { ok: false, error: `Exit code ${res.status ?? 'unknown'}` };
    return { ok: true };
  };

  if (process.platform === 'darwin') {
    const r = spawn('pbcopy', []);
    return r.ok ? { ok: true, method: 'pbcopy' } : { ok: false, method: 'pbcopy', error: r.error };
  }
  if (process.platform === 'win32') {
    const r = spawn('clip', []);
    return r.ok ? { ok: true, method: 'clip' } : { ok: false, method: 'clip', error: r.error };
  }

  // Linux: try wl-copy, xclip, xsel
  for (const cmd of [
    { cmd: 'wl-copy', args: [] as string[] },
    { cmd: 'xclip', args: ['-selection', 'clipboard'] as string[] },
    { cmd: 'xsel', args: ['--clipboard', '--input'] as string[] },
  ]) {
    const r = spawn(cmd.cmd, cmd.args);
    if (r.ok) return { ok: true, method: cmd.cmd };
  }

  return { ok: false, error: 'No clipboard command available (tried wl-copy, xclip, xsel)' };
}

// ============================================================================
// Main Report Function
// ============================================================================

export async function runReport(options: ReportOptions): Promise<number> {
  try {
    const includeLogs = parseIncludeLogsMode(options.includeLogs);
    const logLines = parsePositiveInt(options.logLines, DEFAULT_LOG_LINES);
    const redactionEnabled = options.redact !== false;

    const tool = getToolVersion();
    const browsers = resolveBrowsers(options.browser);

    // Collect doctor report
    let doctor: DoctorReport | undefined;
    let doctorError: string | undefined;
    try {
      doctor = await collectDoctorReport({
        json: true,
        fix: false,
        browser: options.browser,
      });
    } catch (e) {
      doctorError = stringifyError(e);
    }

    // Build the report
    const report: DiagnosticReport = {
      schemaVersion: REPORT_SCHEMA_VERSION,
      timestamp: new Date().toISOString(),
      tool,
      environment: {
        platform: process.platform,
        arch: process.arch,
        node: { version: process.version, execPath: process.execPath },
        os: { type: os.type(), release: os.release(), version: safeOsVersion() },
        cwd: process.cwd(),
        env: {
          WEBPAGE_MCP_NODE_PATH: process.env.WEBPAGE_MCP_NODE_PATH ?? null,
          VOLTA_HOME: process.env.VOLTA_HOME ?? null,
          ASDF_DATA_DIR: process.env.ASDF_DATA_DIR ?? null,
          FNM_DIR: process.env.FNM_DIR ?? null,
          NVM_DIR: process.env.NVM_DIR ?? null,
          // nvm-windows uses different environment variables
          NVM_HOME: process.env.NVM_HOME ?? null,
          NVM_SYMLINK: process.env.NVM_SYMLINK ?? null,
          npm_config_user_agent: process.env.npm_config_user_agent ?? null,
        },
      },
      packageManager: {
        npm: safeExecVersion('npm'),
        pnpm: safeExecVersion('pnpm'),
      },
      doctor,
      doctorError,
      manifests: collectManifests(browsers),
      wrapperLogs: collectWrapperLogs(getLogDir(), includeLogs, logLines),
      windowsRegistry: process.platform === 'win32' ? collectWindowsRegistry(browsers) : undefined,
      redaction: { enabled: redactionEnabled },
    };

    // Apply redaction
    const redact = createRedactor(redactionEnabled);
    const finalReport = redactionEnabled
      ? (redactDeep(report, redact) as DiagnosticReport)
      : report;

    // Render output
    const output = options.json
      ? JSON.stringify(finalReport, null, 2) + '\n'
      : renderMarkdown(finalReport) + '\n';

    // Write output
    const write = writeOutput(options.output, output);
    if (!write.ok) {
      process.stderr.write(`Failed to write report: ${write.error}\n`);
      process.stdout.write(output);
    } else if (write.destination !== 'stdout') {
      process.stderr.write(`Report written to: ${write.destination}\n`);
    }

    // Copy to clipboard if requested
    if (options.copy) {
      const copied = tryCopyToClipboard(output);
      if (copied.ok) {
        process.stderr.write(`Copied to clipboard (${copied.method})\n`);
      } else {
        process.stderr.write(`Failed to copy to clipboard: ${copied.error ?? 'unknown error'}\n`);
      }
    }

    return 0;
  } catch (e) {
    process.stderr.write(`Report failed: ${stringifyError(e)}\n`);
    return 1;
  }
}
