/**
 * OutputManager for dual-mode output (text + JSON).
 *
 * See: research-modern-typescript-cli-patterns.md#4-dual-output-mode-text--json
 */

import pc from 'picocolors';
import type { Command } from 'commander';
import { marked } from 'marked';
import { markedTerminal } from 'marked-terminal';
import { spawn } from 'node:child_process';

import type { CommandContext, ColorOption } from './context.js';
import { shouldColorize } from './context.js';
import { PAGINATION_LINE_THRESHOLD } from '../../lib/settings.js';
import { parseMarkdown } from '../../utils/markdown-utils.js';
import type { OperationLogger } from '../../lib/types.js';

/**
 * Standard icons for CLI output. Use these constants instead of hardcoded characters.
 *
 * Message icons (prefix messages with these):
 * - SUCCESS_ICON: ✓ for completed operations
 * - ERROR_ICON: ✗ for failures
 * - WARN_ICON: ⚠ for warnings
 * - NOTICE_ICON: • for notices/bullets
 *
 * Status icons (show issue status):
 * - OPEN_ICON: ○ for open issues
 * - IN_PROGRESS_ICON: ◐ for in-progress issues
 * - BLOCKED_ICON: ● for blocked issues
 * - CLOSED_ICON: ✓ for closed issues (same as SUCCESS_ICON)
 */

/**
 * Format a section heading - ALL CAPS for consistent CLI output.
 * Per arch-cli-interface-design-system.md, section headings should be
 * ALL CAPS, bold, followed by blank line before content.
 *
 * @param text - The heading text to format
 * @returns Uppercase heading string
 */
export function formatHeading(text: string): string {
  return text.toUpperCase();
}

export const ICONS = {
  // Message icons
  SUCCESS: '✓', // U+2713
  ERROR: '✗', // U+2717
  WARN: '⚠', // U+26A0
  NOTICE: '•', // U+2022

  // Status icons
  OPEN: '○', // U+25CB
  IN_PROGRESS: '◐', // U+25D0
  BLOCKED: '●', // U+25CF
  CLOSED: '✓', // U+2713 (same as SUCCESS)
  DEFERRED: '○', // U+25CB (same as OPEN)
} as const;

/**
 * Pre-parse argv to determine color setting before Commander parses options.
 * This is needed because help output happens before full option parsing.
 */
export function getColorOptionFromArgv(): ColorOption {
  const colorArg = process.argv.find((arg) => arg.startsWith('--color='));
  if (colorArg) {
    const value = colorArg.split('=')[1];
    if (value === 'always' || value === 'never' || value === 'auto') {
      return value;
    }
  }
  // Check for --color followed by value
  const colorIdx = process.argv.indexOf('--color');
  if (colorIdx !== -1 && process.argv[colorIdx + 1]) {
    const value = process.argv[colorIdx + 1];
    if (value === 'always' || value === 'never' || value === 'auto') {
      return value;
    }
  }
  return 'auto';
}

/**
 * Maximum width for help text and formatted output. We cap at 88 characters
 * for readability, but use narrower if the terminal is smaller.
 */
export const MAX_HELP_WIDTH = 88;

/**
 * Get terminal width capped at MAX_HELP_WIDTH.
 * Use this for all formatted CLI output to ensure consistent width handling.
 */
export function getTerminalWidth(): number {
  return Math.min(MAX_HELP_WIDTH, process.stdout.columns ?? 80);
}

/**
 * Create colored help configuration for Commander.js.
 * Uses Commander's built-in configureHelp() style functions (requires v14+).
 *
 * @param colorOption - Color option to determine if colors should be enabled
 * @returns Help configuration object for program.configureHelp()
 */
export function createColoredHelpConfig(colorOption: ColorOption = 'auto') {
  const colors = pc.createColors(shouldColorize(colorOption));

  return {
    helpWidth: getTerminalWidth(),
    styleTitle: (str: string) => colors.bold(colors.cyan(str)),
    styleCommandText: (str: string) => colors.green(str),
    styleOptionText: (str: string) => colors.yellow(str),
    showGlobalOptions: true,
  };
}

/**
 * Create the help epilog text with color.
 * Includes "Getting Started" section and prominent agent guidance per spec.
 *
 * @param colorOption - Color option to determine if colors should be enabled
 * @returns Colored epilog string
 */
export function createHelpEpilog(colorOption: ColorOption = 'auto'): string {
  const colors = pc.createColors(shouldColorize(colorOption));
  const lines = [
    colors.bold(colors.yellow('IMPORTANT:')),
    `  Agents unfamiliar with tbd should run ${colors.green('`tbd prime`')} for full workflow context.`,
    '',
    colors.bold('Getting Started:'),
    `  ${colors.green('npm install -g get-tbd@latest && tbd setup --auto --prefix=<name>')}`,
    '',
    '  This initializes tbd and configures your coding agents automatically.',
    `  To refresh setup (idempotent, safe anytime): ${colors.green('`tbd setup --auto`')}`,
    `  For interactive setup: ${colors.dim('`tbd setup --interactive`')}`,
    '',
    colors.blue('For more on tbd, see: https://github.com/jlevy/tbd'),
  ];
  return lines.join('\n');
}

/**
 * Configure Commander.js with colored help text.
 * Call this on the program before adding commands.
 */
export function configureColoredHelp(program: Command): Command {
  const colorOption = getColorOptionFromArgv();
  return program.configureHelp(createColoredHelpConfig(colorOption));
}

/**
 * Color utilities with conditional colorization.
 *
 * Uses picocolors' createColors() for manual color support control,
 * which is the recommended approach per picocolors documentation.
 * This allows --color=always to work even when stdout is not a TTY.
 */
export function createColors(colorOption: ColorOption) {
  const enabled = shouldColorize(colorOption);

  // Use picocolors' createColors() for proper manual control
  // This overrides picocolors' automatic TTY detection
  const colors = pc.createColors(enabled);

  return {
    // Status colors
    success: colors.green,
    error: colors.red,
    warn: colors.yellow,
    info: colors.blue,

    // Text formatting
    bold: colors.bold,
    dim: colors.dim,
    italic: colors.italic,
    underline: colors.underline,

    // Semantic colors
    id: colors.cyan,
    label: colors.magenta,
    path: colors.blue,
  };
}

/**
 * Render Markdown to colorized terminal output.
 *
 * Uses marked-terminal for colorized output when colors are enabled,
 * falls back to plain Markdown when colors are disabled or piped.
 * Respects the --color option and TTY detection.
 *
 * @param content - Markdown string to render
 * @param colorOption - Color option to determine if colors should be enabled
 * @returns Rendered string (colorized or plain)
 */
export function renderMarkdown(content: string, colorOption: ColorOption = 'auto'): string {
  const useColors = shouldColorize(colorOption);

  if (!useColors) {
    // Return plain markdown when colors are disabled
    return content;
  }

  // Configure marked with terminal renderer for this parse
  // Note: @types/marked-terminal is outdated; markedTerminal returns MarkedExtension in v7+
  // but types still claim it returns TerminalRenderer. Cast to work around this.
  marked.use(
    markedTerminal({
      width: getTerminalWidth(),
      reflowText: true,
    }) as unknown as Parameters<typeof marked.use>[0],
  );

  // marked.parse returns string with sync renderer
  return marked.parse(content) as string;
}

/**
 * Render YAML frontmatter with custom styling.
 * Keys are dimmed, values are bold.
 *
 * @param frontmatter - Raw YAML frontmatter string (without --- delimiters)
 * @returns Styled frontmatter string
 */
function renderYamlFrontmatter(frontmatter: string): string {
  const lines = frontmatter.split('\n');
  const styledLines = lines.map((line) => {
    // Match YAML key: value pattern
    const match = /^(\s*)([^:]+:)(.*)$/.exec(line);
    if (match) {
      const [, indent, key, value] = match;
      // Key (including colon) is dim, value is bold
      return indent + pc.dim(key) + pc.bold(value);
    }
    // Lines without key: pattern (e.g., continuation lines) stay as-is but bold
    return pc.bold(line);
  });
  return styledLines.join('\n');
}

/**
 * Render markdown with proper YAML frontmatter handling.
 *
 * Separates YAML frontmatter from markdown body and renders them appropriately:
 * - Frontmatter keys are dimmed, values are bold (no indentation)
 * - Body is rendered as regular markdown
 *
 * Works with or without frontmatter - if no frontmatter exists, renders as plain markdown.
 *
 * @param content - Markdown string (possibly with YAML frontmatter) to render
 * @param colorOption - Color option to determine if colors should be enabled
 * @returns Rendered string (colorized or plain)
 */
export function renderMarkdownWithFrontmatter(
  content: string,
  colorOption: ColorOption = 'auto',
): string {
  const useColors = shouldColorize(colorOption);

  if (!useColors) {
    // Return plain markdown when colors are disabled
    return content;
  }

  const { frontmatter, body } = parseMarkdown(content);

  let result = '';

  // Render frontmatter with custom YAML styling if present
  if (frontmatter !== null && frontmatter.length > 0) {
    result += pc.dim('---') + '\n';
    result += renderYamlFrontmatter(frontmatter) + '\n';
    result += pc.dim('---') + '\n\n';
  }

  // Render body as markdown
  if (body) {
    result += renderMarkdown(body, colorOption);
  }

  return result;
}

/**
 * Output content with pagination if it exceeds threshold and TTY is interactive.
 * Uses PAGER env var or falls back to 'less -R' (supports colors).
 *
 * When output is piped or not a TTY, outputs directly without pagination.
 * This ensures agents and scripts get clean output.
 *
 * @param content - Content to output
 * @param hasColors - If true, content has ANSI colors (use -R flag for less)
 * @returns Promise that resolves when output is complete
 */
export async function paginateOutput(content: string, hasColors = false): Promise<void> {
  const lines = content.split('\n').length;

  // Don't paginate short content or non-TTY
  if (lines < PAGINATION_LINE_THRESHOLD || !process.stdout.isTTY) {
    console.log(content);
    return;
  }

  const pager = process.env.PAGER ?? (hasColors ? 'less -R' : 'less');
  const [cmd, ...args] = pager.split(' ');

  return new Promise((resolve) => {
    const child = spawn(cmd!, args, {
      stdio: ['pipe', 'inherit', 'inherit'],
    });

    // Handle EPIPE error when user quits pager (e.g., pressing 'q' in less).
    // This is expected behavior - the pager closes stdin when the user exits early.
    child.stdin.on('error', (err: NodeJS.ErrnoException) => {
      if (err.code === 'EPIPE') {
        // User quit the pager early - this is fine, not an error
        return;
      }
      // For other errors, log only in debug scenarios
      // (but don't throw - let the process exit cleanly)
    });

    child.stdin.write(content);
    child.stdin.end();

    child.on('close', () => {
      resolve();
    });
    child.on('error', () => {
      // Fall back to direct output if pager fails (e.g., less not installed)
      console.log(content);
      resolve();
    });
  });
}

/**
 * Spinner interface for progress indication.
 */
export interface Spinner {
  message(msg: string): void;
  stop(msg?: string): void;
}

/**
 * No-op spinner for non-TTY or quiet mode.
 */
const noopSpinner: Spinner = {
  message: () => {},
  stop: () => {},
};

/**
 * OutputManager handles all CLI output with format switching.
 */
export class OutputManager {
  private ctx: CommandContext;
  private colors: ReturnType<typeof createColors>;

  constructor(ctx: CommandContext) {
    this.ctx = ctx;
    this.colors = createColors(ctx.color);
  }

  /**
   * Output structured data - always goes to stdout.
   * In JSON mode, outputs JSON. In text mode, calls the formatter.
   */
  data<T>(data: T, textFormatter?: (data: T) => void): void {
    if (this.ctx.json) {
      console.log(JSON.stringify(data, null, 2));
    } else if (textFormatter) {
      textFormatter(data);
    }
  }

  /**
   * Output success message - text mode only, stdout.
   * Suppressed by --quiet and --json.
   */
  success(message: string): void {
    if (!this.ctx.json && !this.ctx.quiet) {
      console.log(this.colors.success(`${ICONS.SUCCESS} ${message}`));
    }
  }

  /**
   * Output notice message - noteworthy events during normal operation.
   * Blue bullet, shown at default level. Suppressed by --quiet and --json.
   */
  notice(message: string): void {
    if (!this.ctx.json && !this.ctx.quiet) {
      console.log(this.colors.info(`${ICONS.NOTICE} ${message}`));
    }
  }

  /**
   * Output info message - operational progress.
   * Requires --verbose or --debug. Suppressed by --json.
   */
  info(message: string): void {
    if (!this.ctx.json && (this.ctx.verbose || this.ctx.debug)) {
      console.error(this.colors.dim(message));
    }
  }

  /**
   * Output warning - issues that didn't stop operation.
   * Yellow warning icon, stderr. Suppressed by --quiet.
   */
  warn(message: string): void {
    if (this.ctx.json) {
      console.error(JSON.stringify({ warning: message }));
    } else if (!this.ctx.quiet) {
      console.error(this.colors.warn(`${ICONS.WARN} ${message}`));
    }
  }

  /**
   * Output error - failures that stop operation.
   * Red X icon, always shown (even in --quiet), stderr.
   */
  error(message: string, err?: Error): void {
    if (this.ctx.json) {
      console.error(JSON.stringify({ error: message, details: err?.message }));
    } else {
      console.error(this.colors.error(`${ICONS.ERROR} ${message}`));
      if (this.ctx.verbose && err?.stack) {
        console.error(this.colors.dim(err.stack));
      }
    }
  }

  /**
   * Output command being executed - shows external commands.
   * Requires --verbose or --debug. Suppressed by --json.
   */
  command(cmd: string, args?: string[]): void {
    if (!this.ctx.json && (this.ctx.verbose || this.ctx.debug)) {
      const fullCmd = args ? `${cmd} ${args.join(' ')}` : cmd;
      console.error(this.colors.dim(`> ${fullCmd}`));
    }
  }

  /**
   * Output debug message - internal state for troubleshooting.
   * Requires --debug only (not --verbose). Suppressed by --json.
   */
  debug(message: string): void {
    if (this.ctx.debug && !this.ctx.json) {
      console.error(this.colors.dim(`[debug] ${message}`));
    }
  }

  /**
   * Output dry-run indication.
   */
  dryRun(message: string, details?: object): void {
    if (this.ctx.json) {
      console.log(JSON.stringify({ dryRun: true, action: message, ...details }));
    } else {
      console.log(this.colors.warn(`[DRY-RUN] ${message}`));
      if (details && (this.ctx.verbose || this.ctx.debug)) {
        console.log(this.colors.dim(JSON.stringify(details, null, 2)));
      }
    }
  }

  /**
   * Output a table with headers and rows.
   * Headers are dimmed. Rows are formatted with consistent column widths.
   * Suppressed in JSON mode.
   *
   * @param headers - Array of column headers with widths
   * @param rows - Array of row data arrays (each row = array of strings)
   */
  table(
    headers: { label: string; width: number }[],
    rows: (string | { value: string; color?: (s: string) => string })[][],
  ): void {
    if (this.ctx.json) return;

    // Output header row
    const headerLine = headers.map((h) => h.label.padEnd(h.width)).join('');
    console.log(this.colors.dim(headerLine));

    // Output data rows
    for (const row of rows) {
      const cells = row.map((cell, i) => {
        const width = headers[i]?.width ?? 0;
        if (typeof cell === 'string') {
          return cell.padEnd(width);
        }
        // Cell with custom color
        const paddedValue = cell.value.padEnd(width);
        return cell.color ? cell.color(paddedValue) : paddedValue;
      });
      console.log(cells.join(''));
    }
  }

  /**
   * Output a bulleted list.
   * Uses NOTICE icon (•) as bullet. Suppressed in JSON mode.
   *
   * @param items - Array of items to list
   * @param options - Optional indent level (default 0)
   */
  list(items: string[], options?: { indent?: number }): void {
    if (this.ctx.json) return;

    const indent = '  '.repeat(options?.indent ?? 0);
    for (const item of items) {
      console.log(`${indent}${ICONS.NOTICE} ${item}`);
    }
  }

  /**
   * Output a count summary in standard format.
   * Format: "N item(s)" with dim color. Suppressed in JSON mode.
   *
   * @param count - The count to display
   * @param singular - Singular form of the item (e.g., "issue")
   * @param plural - Optional plural form (defaults to singular + "s")
   */
  count(count: number, singular: string, plural?: string): void {
    if (this.ctx.json) return;

    const pluralForm = plural ?? `${singular}s`;
    const label = count === 1 ? singular : pluralForm;
    console.log(this.colors.dim(`${count} ${label}`));
  }

  /**
   * Create a spinner for progress indication.
   * Returns no-op in JSON/quiet mode or non-TTY.
   */
  spinner(message: string): Spinner {
    // Never show spinners in JSON mode, quiet mode, or non-TTY
    if (this.ctx.json || this.ctx.quiet || !process.stderr.isTTY) {
      return noopSpinner;
    }

    // Simple inline spinner (no external dependency for now)
    let frame = 0;
    const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
    let currentMessage = message;

    const spinnerColor = this.colors.info;
    const write = () => {
      process.stderr.write(`\r${spinnerColor(frames[frame] ?? '⠋')} ${currentMessage}`);
      frame = (frame + 1) % frames.length;
    };

    write();
    const interval = setInterval(write, 80);

    return {
      message: (msg: string) => {
        currentMessage = msg;
      },
      stop: (msg?: string) => {
        clearInterval(interval);
        process.stderr.write('\r' + ' '.repeat(currentMessage.length + 3) + '\r');
        if (msg) {
          console.error(msg);
        }
      },
    };
  }

  /**
   * Create an OperationLogger wired to this OutputManager and a spinner.
   *
   * Eliminates the boilerplate of manually wiring spinner.message, output.info,
   * output.warn, and output.debug in every command that calls a core function.
   */
  logger(spinner: Spinner): OperationLogger {
    return {
      progress: (msg) => {
        spinner.message(msg);
      },
      info: (msg) => {
        this.info(msg);
      },
      warn: (msg) => {
        this.warn(msg);
      },
      debug: (msg) => {
        this.debug(msg);
      },
    };
  }

  /**
   * Get colors instance for direct use.
   */
  getColors() {
    return this.colors;
  }

  /**
   * Check if quiet mode is enabled.
   */
  isQuiet(): boolean {
    return this.ctx.quiet;
  }
}

// ============================================================================
// Component Helper Functions
// ============================================================================

/**
 * Format command header with version.
 * Used at start of orientation commands (status, doctor, stats).
 *
 * @example formatCommandHeader('tbd', '0.1.9', colors) → "tbd v0.1.9" (bold name)
 */
export function formatCommandHeader(
  name: string,
  version: string,
  colors: ReturnType<typeof createColors>,
): string {
  return `${colors.bold(name)} v${version}`;
}

/**
 * Format key-value line with dim key.
 * Used for configuration display.
 *
 * @example formatKeyValue('Sync branch', 'tbd-sync', colors) → "Sync branch: tbd-sync"
 */
export function formatKeyValue(
  key: string,
  value: string,
  colors: ReturnType<typeof createColors>,
): string {
  return `${colors.dim(key + ':')} ${value}`;
}

/**
 * Format aligned statistic block.
 * Returns array of formatted lines with aligned values.
 *
 * @param stats - Array of {label, value} pairs
 * @param colors - Color functions (unused but kept for consistency)
 * @returns Array of formatted lines
 *
 * @example
 * formatStatBlock([{label: 'Ready', value: 12}, {label: 'In progress', value: 4}], colors)
 * → ['  Ready:       12', '  In progress: 4']
 */
export function formatStatBlock(
  stats: { label: string; value: number | string }[],
  _colors: ReturnType<typeof createColors>,
): string[] {
  const maxLabelLen = Math.max(...stats.map((s) => s.label.length));

  return stats.map((stat) => {
    const padding = ' '.repeat(maxLabelLen - stat.label.length + 1);
    return `  ${stat.label}:${padding}${stat.value}`;
  });
}

/**
 * Format multi-line warning block.
 * Returns array of lines for a warning with headline, details, and suggestion.
 *
 * @param headline - Warning headline (shown with ⚠ icon)
 * @param details - Detail lines
 * @param suggestion - Optional suggestion with command (bolded)
 * @param colors - Color functions
 * @returns Array of formatted lines
 */
export function formatWarningBlock(
  headline: string,
  details: string[],
  suggestion: { text: string; command: string } | undefined,
  colors: ReturnType<typeof createColors>,
): string[] {
  const lines: string[] = [];

  // Headline with warning icon
  lines.push(`${colors.warn(ICONS.WARN)} ${headline}`);

  // Detail lines
  for (const detail of details) {
    lines.push(detail);
  }

  // Suggestion with bolded command
  if (suggestion) {
    lines.push(`${suggestion.text} ${colors.bold(suggestion.command)}`);
  }

  return lines;
}

/**
 * Format footer with command suggestions.
 * Returns a formatted string like "Use 'tbd stats' for statistics, 'tbd doctor' for health checks."
 *
 * @param suggestions - Array of {command, description} pairs
 * @param colors - Color functions
 * @returns Formatted footer string
 */
export function formatFooter(
  suggestions: { command: string; description: string }[],
  colors: ReturnType<typeof createColors>,
): string {
  if (suggestions.length === 0) {
    return '';
  }

  const parts = suggestions.map((s) => `${colors.bold(`'${s.command}'`)} for ${s.description}`);

  if (parts.length === 1) {
    return `Use ${parts[0]}.`;
  }

  return `Use ${parts.join(', ')}.`;
}
