/**
 * Wiki Command
 *
 * Generates repository documentation from the knowledge graph.
 * Usage: gitnexus wiki [path] [options]
 */

import path from 'path';
import readline from 'readline';
import { execSync, execFileSync } from 'child_process';
import cliProgress from 'cli-progress';
import { getGitRoot, isGitRepo } from '../storage/git.js';
import {
  getStoragePaths,
  loadMeta,
  loadCLIConfig,
  saveCLIConfig,
} from '../storage/repo-manager.js';
import { WikiGenerator, type WikiOptions } from '../core/wiki/generator.js';
import { resolveLLMConfig, type LLMProvider } from '../core/wiki/llm-client.js';
import { detectCursorCLI } from '../core/wiki/cursor-client.js';
import { detectLocalCLI } from '../core/wiki/local-cli-client.js';
import { logger } from '../core/logger.js';

export interface WikiCommandOptions {
  force?: boolean;
  model?: string;
  baseUrl?: string;
  apiKey?: string;
  apiVersion?: string;
  reasoningModel?: boolean;
  concurrency?: string;
  gist?: boolean;
  provider?: LLMProvider;
  verbose?: boolean;
  review?: boolean;
  timeout?: string;
  retries?: string;
  lang?: string;
}

function parsePositiveIntegerOption(
  value: string | undefined,
  flag: string,
  multiplier = 1,
): number | undefined {
  if (value === undefined) return undefined;
  const trimmed = value.trim();
  if (!/^[1-9]\d*$/.test(trimmed)) {
    throw new Error(`${flag} must be a positive integer`);
  }
  const parsed = parseInt(trimmed, 10);
  if (parsed > Math.floor(Number.MAX_SAFE_INTEGER / multiplier)) {
    throw new Error(`${flag} is too large`);
  }
  return parsed;
}

function isLocalProvider(
  provider: LLMProvider | undefined,
): provider is 'cursor' | 'claude' | 'codex' | 'opencode' {
  return (
    provider === 'cursor' ||
    provider === 'claude' ||
    provider === 'codex' ||
    provider === 'opencode'
  );
}

function localModelConfigKey(provider: 'cursor' | 'claude' | 'codex' | 'opencode') {
  if (provider === 'cursor') return 'cursorModel';
  if (provider === 'claude') return 'claudeModel';
  if (provider === 'codex') return 'codexModel';
  if (provider === 'opencode') return 'opencodeModel';
  throw new Error(`Unsupported local provider: ${provider satisfies never}`);
}

/**
 * Prompt the user for input via stdin.
 */
function prompt(question: string, hide = false): Promise<string> {
  return new Promise((resolve) => {
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
    });

    if (hide && process.stdin.isTTY) {
      // Mask input for API keys
      process.stdout.write(question);
      let input = '';
      process.stdin.setRawMode(true);
      process.stdin.resume();
      process.stdin.setEncoding('utf-8');

      const onData = (char: string) => {
        if (char === '\n' || char === '\r' || char === '\u0004') {
          process.stdin.setRawMode(false);
          process.stdin.removeListener('data', onData);
          process.stdout.write('\n');
          rl.close();
          resolve(input);
        } else if (char === '\u0003') {
          // Ctrl+C
          process.stdin.setRawMode(false);
          rl.close();
          process.exit(1);
        } else if (char === '\u007F' || char === '\b') {
          // Backspace
          if (input.length > 0) {
            input = input.slice(0, -1);
            process.stdout.write('\b \b');
          }
        } else {
          input += char;
          process.stdout.write('*');
        }
      };
      process.stdin.on('data', onData);
    } else {
      rl.question(question, (answer) => {
        rl.close();
        resolve(answer.trim());
      });
    }
  });
}

export const wikiCommand = async (inputPath?: string, options?: WikiCommandOptions) => {
  // Snapshot GITNEXUS_VERBOSE at entry — wikiCommand mutates it (the impl
  // below) so cursor-client (process.env-driven) sees the right value during
  // this run. Restored in finally so back-to-back wiki calls in long-running
  // hosts don't leak verbose state from one invocation to the next. Pairs
  // with the same snapshot/restore pattern in `analyzeCommand`.
  const originalVerbose = process.env.GITNEXUS_VERBOSE;
  try {
    await wikiCommandImpl(inputPath, options);
  } finally {
    if (originalVerbose === undefined) {
      delete process.env.GITNEXUS_VERBOSE;
    } else {
      process.env.GITNEXUS_VERBOSE = originalVerbose;
    }
  }
};

const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions): Promise<void> => {
  // Set verbose mode globally for cursor-client to pick up
  if (options?.verbose) {
    process.env.GITNEXUS_VERBOSE = '1';
  }

  console.log('\n  GitNexus Wiki Generator\n');

  // ── Resolve repo path ───────────────────────────────────────────────
  let repoPath: string;
  if (inputPath) {
    repoPath = path.resolve(inputPath);
  } else {
    const gitRoot = getGitRoot(process.cwd());
    if (!gitRoot) {
      console.log('  Error: Not inside a git repository\n');
      process.exitCode = 1;
      return;
    }
    repoPath = gitRoot;
  }

  if (!isGitRepo(repoPath)) {
    console.log('  Error: Not a git repository\n');
    process.exitCode = 1;
    return;
  }

  // ── Check for existing index ────────────────────────────────────────
  const { storagePath, lbugPath } = getStoragePaths(repoPath);
  const meta = await loadMeta(storagePath);

  if (!meta) {
    console.log('  Error: No GitNexus index found.');
    console.log('  Run `gitnexus analyze` first to index this repository.\n');
    process.exitCode = 1;
    return;
  }

  let timeoutSeconds: number | undefined;
  let retries: number | undefined;
  try {
    timeoutSeconds = parsePositiveIntegerOption(options?.timeout, '--timeout', 1000);
    retries = parsePositiveIntegerOption(options?.retries, '--retries');
  } catch (error) {
    console.log(`  Error: ${(error as Error).message}\n`);
    process.exitCode = 1;
    return;
  }

  // ── Resolve LLM config (with interactive fallback) ─────────────────
  // Save any CLI overrides immediately
  if (
    options?.apiKey ||
    options?.model ||
    options?.baseUrl ||
    options?.provider ||
    options?.apiVersion ||
    options?.reasoningModel !== undefined
  ) {
    const existing = await loadCLIConfig();
    const updates: Partial<typeof existing> = {};
    if (options.apiKey) updates.apiKey = options.apiKey;
    if (options.baseUrl) updates.baseUrl = options.baseUrl;
    if (options.provider) updates.provider = options.provider;
    if (options.apiVersion) updates.apiVersion = options.apiVersion;
    if (options.reasoningModel !== undefined) updates.isReasoningModel = options.reasoningModel;
    // Save model to appropriate field based on provider.
    if (options.model) {
      const targetProvider = options.provider ?? existing.provider;
      if (isLocalProvider(targetProvider)) {
        updates[localModelConfigKey(targetProvider)] = options.model;
      } else {
        updates.model = options.model;
      }
    }
    await saveCLIConfig({ ...existing, ...updates });
    console.log('  Config saved to ~/.gitnexus/config.json\n');
  }

  const savedConfig = await loadCLIConfig();
  const hasSavedConfig = !!(
    isLocalProvider(savedConfig.provider) ||
    (savedConfig.apiKey && savedConfig.baseUrl)
  );
  const hasCLIOverrides = !!(
    options?.apiKey ||
    options?.model ||
    options?.baseUrl ||
    options?.provider ||
    options?.apiVersion ||
    options?.reasoningModel !== undefined
  );

  let llmConfig = await resolveLLMConfig({
    model: options?.model,
    baseUrl: options?.baseUrl,
    apiKey: options?.apiKey,
    provider: options?.provider,
    apiVersion: options?.apiVersion,
    isReasoningModel: options?.reasoningModel,
  });

  // Run interactive setup if no saved config and no CLI flags provided
  // (even if env vars exist — let user explicitly choose their provider)
  if (!hasSavedConfig && !hasCLIOverrides) {
    if (!process.stdin.isTTY) {
      // Non-interactive mode — need either API key or Cursor CLI
      if (!llmConfig.apiKey && !isLocalProvider(llmConfig.provider)) {
        console.log('  Error: No LLM API key found.');
        console.log('  Set OPENAI_API_KEY or GITNEXUS_API_KEY environment variable,');
        console.log('  or pass --api-key <key>, or use --provider cursor|claude|codex|opencode.\n');
        process.exitCode = 1;
        return;
      }
      // Non-interactive with env var or cursor — just use it
    } else {
      console.log("  No LLM configured. Let's set it up.\n");
      console.log(
        '  Supports OpenAI, OpenRouter, Azure, any OpenAI-compatible API, Cursor CLI, Claude CLI, Codex CLI, or OpenCode CLI.\n',
      );

      // Check if local agent CLIs are available.
      const hasCursor = detectCursorCLI();
      const hasClaude = detectLocalCLI('claude');
      const hasCodex = detectLocalCLI('codex');
      const hasOpenCode = detectLocalCLI('opencode');
      const localChoices: Array<{
        choice: string;
        provider: 'cursor' | 'claude' | 'codex' | 'opencode';
      }> = [];

      // Provider selection
      console.log('  [1] OpenAI (api.openai.com)');
      console.log('  [2] OpenRouter (openrouter.ai)');
      console.log('  [3] Azure OpenAI');
      console.log('  [4] Custom endpoint');
      let nextChoice = 5;
      if (hasCursor) {
        const choice = String(nextChoice++);
        localChoices.push({
          choice,
          provider: 'cursor',
        });
        console.log(`  [${choice}] Cursor CLI (local, uses your Cursor subscription)`);
      }
      if (hasClaude) {
        const choice = String(nextChoice++);
        localChoices.push({
          choice,
          provider: 'claude',
        });
        console.log(`  [${choice}] Claude CLI (local, uses your Claude Code login)`);
      }
      if (hasCodex) {
        const choice = String(nextChoice++);
        localChoices.push({
          choice,
          provider: 'codex',
        });
        console.log(`  [${choice}] Codex CLI (local, uses your Codex login)`);
      }
      if (hasOpenCode) {
        const choice = String(nextChoice++);
        localChoices.push({
          choice,
          provider: 'opencode',
        });
        console.log(`  [${choice}] OpenCode CLI (local, uses your OpenCode login/config)`);
      }
      console.log('');

      const maxChoice = String(nextChoice - 1);
      const choice = await prompt(`  Select provider (1/${maxChoice}): `);

      let baseUrl: string;
      let defaultModel: string;
      let provider: LLMProvider = 'openai';
      let key = '';

      const selectedLocal = localChoices.find((item) => item.choice === choice);
      if (selectedLocal) {
        // Local CLI selected - model defaults to the CLI's configured default.
        provider = selectedLocal.provider;
        baseUrl = '';

        const modelInput = await prompt('  Model (leave empty for CLI default): ');
        const model = modelInput || '';

        const localConfig = { ...savedConfig, provider };
        if (model) (localConfig as Record<string, unknown>)[localModelConfigKey(provider)] = model;
        await saveCLIConfig(localConfig);
        console.log('  Config saved to ~/.gitnexus/config.json\n');

        llmConfig = { ...llmConfig, provider, model, apiKey: '', baseUrl: '' };
      } else if (choice === '3') {
        // Azure OpenAI guided setup — minimal prompts
        console.log('\n  Azure OpenAI setup.\n');

        const endpoint = (
          await prompt('  Endpoint URL (e.g. https://my-resource.openai.azure.com): ')
        )
          .trim()
          .replace(/\/+$/, '');
        if (!endpoint) {
          console.log('\n  No endpoint provided. Aborting.\n');
          process.exitCode = 1;
          return;
        }

        const deploymentName = (await prompt('  Deployment name: ')).trim();
        if (!deploymentName) {
          console.log('\n  No deployment name provided. Aborting.\n');
          process.exitCode = 1;
          return;
        }

        // API key — use env var if available
        const envKey = process.env.GITNEXUS_API_KEY || process.env.OPENAI_API_KEY || '';
        let azureKey: string;
        if (envKey) {
          const masked = envKey.slice(0, 6) + '...' + envKey.slice(-4);
          const useEnv = await prompt(`  Use existing env key (${masked})? (Y/n): `);
          if (!useEnv || useEnv.toLowerCase() === 'y' || useEnv.toLowerCase() === 'yes') {
            azureKey = envKey;
          } else {
            azureKey = await prompt('  API key: ', true);
          }
        } else {
          azureKey = await prompt('  API key: ', true);
        }

        if (!azureKey) {
          console.log('\n  No key provided. Aborting.\n');
          process.exitCode = 1;
          return;
        }

        // Always use v1 API format — no need for api-version
        const azureBaseUrl = `${endpoint}/openai/v1`;

        await saveCLIConfig({
          ...savedConfig,
          apiKey: azureKey,
          baseUrl: azureBaseUrl,
          model: deploymentName,
          provider: 'azure',
        });
        console.log('  Config saved to ~/.gitnexus/config.json\n');

        llmConfig = {
          ...llmConfig,
          apiKey: azureKey,
          baseUrl: azureBaseUrl,
          model: deploymentName,
          provider: 'azure',
        };
      } else {
        // OpenAI-compatible provider (OpenAI, OpenRouter, Custom)
        if (choice === '2') {
          baseUrl = 'https://openrouter.ai/api/v1';
          defaultModel = 'minimax/minimax-m2.5';
          provider = 'openrouter';
        } else if (choice === '4') {
          baseUrl = await prompt('  Base URL (e.g. http://localhost:11434/v1): ');
          if (!baseUrl) {
            console.log('\n  No URL provided. Aborting.\n');
            process.exitCode = 1;
            return;
          }
          defaultModel = 'gpt-4o-mini';
          provider = 'custom';
        } else {
          baseUrl = 'https://api.openai.com/v1';
          defaultModel = 'gpt-4o-mini';
          provider = 'openai';
        }

        // Model
        const modelInput = await prompt(`  Model (default: ${defaultModel}): `);
        const model = modelInput || defaultModel;

        // API key — pre-fill hint if env var exists
        const envKey = process.env.GITNEXUS_API_KEY || process.env.OPENAI_API_KEY || '';
        if (envKey) {
          const masked = envKey.slice(0, 6) + '...' + envKey.slice(-4);
          const useEnv = await prompt(`  Use existing env key (${masked})? (Y/n): `);
          if (!useEnv || useEnv.toLowerCase() === 'y' || useEnv.toLowerCase() === 'yes') {
            key = envKey;
          } else {
            key = await prompt('  API key: ', true);
          }
        } else {
          key = await prompt('  API key: ', true);
        }

        if (!key) {
          console.log('\n  No key provided. Aborting.\n');
          process.exitCode = 1;
          return;
        }

        // Save
        await saveCLIConfig({ apiKey: key, baseUrl, model, provider });
        console.log('  Config saved to ~/.gitnexus/config.json\n');

        llmConfig = { ...llmConfig, apiKey: key, baseUrl, model, provider };
      }
    }
  }

  // ── Apply per-run overrides not saved to config ────────────────────
  if (timeoutSeconds !== undefined) {
    llmConfig.requestTimeoutMs = timeoutSeconds * 1000;
  }
  if (retries !== undefined) {
    llmConfig.maxAttempts = retries;
  }

  // ── Setup progress bar with elapsed timer ──────────────────────────
  const bar = new cliProgress.SingleBar(
    {
      format: '  {bar} {percentage}% | {phase}',
      barCompleteChar: '\u2588',
      barIncompleteChar: '\u2591',
      hideCursor: true,
      barGlue: '',
      autopadding: true,
      clearOnComplete: false,
      stopOnComplete: false,
    },
    cliProgress.Presets.shades_grey,
  );

  bar.start(100, 0, { phase: 'Initializing...' });

  const t0 = Date.now();
  let lastPhase = '';
  let phaseStart = t0;

  // Tick elapsed time every second while stuck on the same phase
  const elapsedTimer = setInterval(() => {
    if (lastPhase) {
      const elapsed = Math.round((Date.now() - phaseStart) / 1000);
      if (elapsed >= 3) {
        bar.update({ phase: `${lastPhase} (${elapsed}s)` });
      }
    }
  }, 1000);

  // ── Run generator ───────────────────────────────────────────────────
  const wikiOptions: WikiOptions = {
    force: options?.force,
    concurrency: options?.concurrency ? parseInt(options.concurrency, 10) : undefined,
    reviewOnly: options?.review,
    lang: options?.lang,
  };

  const generator = new WikiGenerator(
    repoPath,
    storagePath,
    lbugPath,
    llmConfig,
    wikiOptions,
    (phase, percent, detail) => {
      const label = detail || phase;
      if (label !== lastPhase) {
        lastPhase = label;
        phaseStart = Date.now();
      }
      bar.update(percent, { phase: label });
    },
  );

  try {
    const result = await generator.run();

    clearInterval(elapsedTimer);
    bar.stop();

    const elapsed = ((Date.now() - t0) / 1000).toFixed(1);

    const wikiDir = path.join(storagePath, 'wiki');
    const viewerPath = path.join(wikiDir, 'index.html');
    const treeFile = path.join(wikiDir, 'module_tree.json');

    // Review mode: show module tree and ask for confirmation
    if (options?.review && result.moduleTree) {
      console.log(`\n  Module structure ready for review (${elapsed}s)\n`);
      console.log('  Modules to generate:\n');

      const printTree = (nodes: typeof result.moduleTree, indent = 0) => {
        for (const node of nodes) {
          const prefix = '  '.repeat(indent + 2);
          const fileCount = node.files?.length || 0;
          const childCount = node.children?.length || 0;
          const suffix =
            fileCount > 0
              ? ` (${fileCount} files)`
              : childCount > 0
                ? ` (${childCount} children)`
                : '';
          console.log(`${prefix}- ${node.name}${suffix}`);
          if (node.children && node.children.length > 0) {
            printTree(node.children, indent + 1);
          }
        }
      };
      printTree(result.moduleTree);

      console.log(`\n  Tree saved to: ${treeFile}`);
      console.log('  You can edit this file to remove/rename modules.\n');

      // Ask for confirmation (auto-continue in non-interactive environments)
      if (!process.stdin.isTTY) {
        console.log('  Non-interactive mode — auto-continuing with generation.\n');
      }
      const answer = process.stdin.isTTY
        ? await prompt('  Continue with generation? (Y/n/edit): ')
        : 'y';
      const choice = answer.trim().toLowerCase();

      if (choice === 'n' || choice === 'no') {
        console.log('\n  Generation cancelled. Run `gitnexus wiki` later to generate.\n');
        return;
      }

      if (choice === 'edit' || choice === 'e') {
        // Open editor for the user
        const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
        console.log(`\n  Opening ${treeFile} in ${editor}...`);
        console.log('  Save and close the editor when done.\n');

        try {
          execFileSync(editor, [treeFile], { stdio: 'inherit', windowsHide: true });
        } catch {
          console.log(`  Could not open editor. Please edit manually:\n  ${treeFile}\n`);
          console.log('  Then run `gitnexus wiki` to continue.\n');
          return;
        }
      }

      // Continue with generation using the (possibly edited) tree
      console.log('\n  Continuing with wiki generation...\n');
      bar.start(100, 30, { phase: 'Generating pages...' });

      // Re-run generator without reviewOnly flag
      const continueOptions: WikiOptions = {
        ...wikiOptions,
        reviewOnly: false,
      };

      const continueGenerator = new WikiGenerator(
        repoPath,
        storagePath,
        lbugPath,
        llmConfig,
        continueOptions,
        (phase, percent, detail) => {
          const label = detail || phase;
          if (label !== lastPhase) {
            lastPhase = label;
            phaseStart = Date.now();
          }
          bar.update(percent, { phase: label });
        },
      );

      const continueResult = await continueGenerator.run();

      bar.update(100, { phase: 'Done' });
      bar.stop();

      const totalElapsed = ((Date.now() - t0) / 1000).toFixed(1);
      console.log(`\n  Wiki generated successfully (${totalElapsed}s)\n`);
      console.log(`  Mode: ${continueResult.mode}`);
      console.log(`  Pages: ${continueResult.pagesGenerated}`);
      console.log(`  Output: ${wikiDir}`);
      console.log(`  Viewer: ${viewerPath}`);

      if (continueResult.failedModules && continueResult.failedModules.length > 0) {
        console.log(`\n  Failed modules (${continueResult.failedModules.length}):`);
        for (const mod of continueResult.failedModules) {
          console.log(`    - ${mod}`);
        }
      }

      console.log('');
      await maybePublishGist(viewerPath, options?.gist);
      return;
    }

    bar.update(100, { phase: 'Done' });

    if (result.mode === 'up-to-date' && !options?.force) {
      console.log('\n  Wiki is already up to date.');
      console.log(`  Viewer: ${viewerPath}\n`);
      await maybePublishGist(viewerPath, options?.gist);
      return;
    }

    console.log(`\n  Wiki generated successfully (${elapsed}s)\n`);
    console.log(`  Mode: ${result.mode}`);
    console.log(`  Pages: ${result.pagesGenerated}`);
    console.log(`  Output: ${wikiDir}`);
    console.log(`  Viewer: ${viewerPath}`);

    if (result.failedModules && result.failedModules.length > 0) {
      console.log(`\n  Failed modules (${result.failedModules.length}):`);
      for (const mod of result.failedModules) {
        console.log(`    - ${mod}`);
      }
      console.log('  Re-run to retry failed modules (pages will be regenerated).');
    }

    console.log('');

    await maybePublishGist(viewerPath, options?.gist);
  } catch (err: any) {
    clearInterval(elapsedTimer);
    bar.stop();

    if (err.message?.includes('No source files')) {
      console.log(`\n  ${err.message}\n`);
    } else if (err.message?.includes('LLM request timed out after')) {
      console.log(`\n  Timeout: ${err.message}\n`);
    } else if (err.message?.includes('content filter')) {
      // Content filter block — actionable message
      console.log(`\n  Content Filter: ${err.message}\n`);
      console.log(
        '  To resolve: rephrase your prompt or adjust the content filter policy for your deployment.\n',
      );
    } else if (err.message?.includes('API key') || err.message?.includes('API error')) {
      console.log(`\n  LLM Error: ${err.message}\n`);

      // Offer to reconfigure on auth-related failures
      const isAuthError =
        err.message?.includes('401') ||
        err.message?.includes('403') ||
        err.message?.includes('502') ||
        err.message?.includes('authenticate') ||
        err.message?.includes('Unauthorized');
      if (isAuthError && process.stdin.isTTY) {
        const answer = await new Promise<string>((resolve) => {
          const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
          rl.question('  Reconfigure LLM settings? (Y/n): ', (ans) => {
            rl.close();
            resolve(ans.trim().toLowerCase());
          });
        });
        if (!answer || answer === 'y' || answer === 'yes') {
          // Clear saved config so next run triggers interactive setup
          await saveCLIConfig({});
          console.log('  Config cleared. Run `gitnexus wiki` again to reconfigure.\n');
        }
      }
    } else {
      console.log(`\n  Error: ${err.message}\n`);
      if (process.env.GITNEXUS_VERBOSE) {
        logger.error({ err }, 'wiki command failed');
      }
    }
    process.exitCode = 1;
  }
};

// ─── Gist Publishing ───────────────────────────────────────────────────

function hasGhCLI(): boolean {
  try {
    execSync('gh --version', { stdio: 'ignore', windowsHide: true });
    return true;
  } catch {
    return false;
  }
}

/**
 * Strict Gist URL predicate. Rejects:
 *   - any URL that does not parse (URL constructor throws)
 *   - schemes other than https (drops `http:`, `file:`, `gist:`-style spoofs)
 *   - hostnames that are not exactly `gist.github.com` (drops substring spoofs
 *     like `https://evil.com/?u=gist.github.com` and userinfo-prefixed shapes
 *     like `https://[email protected]/...` — note that URL.hostname
 *     strips userinfo, so the equality check rejects the userinfo-prefixed
 *     spoof if the actual host differs from gist.github.com)
 *   - any URL containing userinfo (`username[:password]@`), which the URL
 *     parser exposes via `.username` / `.password`. Defense-in-depth: even
 *     when hostname matches, a credential-bearing URL is suspect and not
 *     produced by `gh gist create`.
 *
 * Closes the substring-bypass class CodeQL `js/incomplete-url-substring-
 * sanitization` flags.
 */
function isGistUrl(line: string): boolean {
  const trimmed = line.trim();
  try {
    const u = new URL(trimmed);
    return (
      u.protocol === 'https:' &&
      u.hostname === 'gist.github.com' &&
      u.username === '' &&
      u.password === ''
    );
  } catch {
    return false;
  }
}

function publishGist(htmlPath: string): { url: string; rawUrl: string } | null {
  try {
    const output = execFileSync(
      'gh',
      ['gist', 'create', htmlPath, '--desc', 'Repository Wiki — generated by GitNexus', '--public'],
      { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true },
    ).trim();

    // `gh gist create` prints the gist URL as a line in the output. Find the
    // first parseable Gist URL — if no line is a valid Gist URL, fail closed
    // (do NOT fall back to lines[last]: a non-Gist last line would propagate
    // through the regex below and produce a malformed `rawUrl`).
    const gistUrl = output.split('\n').find(isGistUrl);
    if (!gistUrl) return null;

    // Build a raw viewer URL via gist.githack.com.
    // gist URL format: https://gist.github.com/{user}/{id}
    const match = gistUrl.match(/gist\.github\.com\/([^/]+)\/([a-f0-9]+)/);
    let rawUrl = gistUrl;
    if (match) {
      rawUrl = `https://gistcdn.githack.com/${match[1]}/${match[2]}/raw/index.html`;
    }

    return { url: gistUrl.trim(), rawUrl };
  } catch {
    return null;
  }
}

async function maybePublishGist(htmlPath: string, gistFlag?: boolean): Promise<void> {
  if (gistFlag === false) return;

  // Check that the HTML file exists
  try {
    const fs = await import('fs/promises');
    await fs.access(htmlPath);
  } catch {
    return;
  }

  if (!hasGhCLI()) {
    if (gistFlag) {
      console.log('  GitHub CLI (gh) is not installed. Cannot publish gist.');
      console.log('  Install it: https://cli.github.com\n');
    }
    return;
  }

  let shouldPublish = !!gistFlag;

  if (!shouldPublish && process.stdin.isTTY) {
    const answer = await new Promise<string>((resolve) => {
      const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
      rl.question('  Publish wiki as a GitHub Gist for easy viewing? (Y/n): ', (ans) => {
        rl.close();
        resolve(ans.trim().toLowerCase());
      });
    });
    shouldPublish = !answer || answer === 'y' || answer === 'yes';
  }

  if (!shouldPublish) return;

  console.log('\n  Publishing to GitHub Gist...');
  const result = publishGist(htmlPath);

  if (result) {
    console.log(`  Gist:   ${result.url}`);
    console.log(`  Viewer: ${result.rawUrl}\n`);
  } else {
    console.log('  Failed to publish gist. Make sure `gh auth login` is configured.\n');
  }
}
