/**
 * Terminal UI utilities for formatted, colorized output
 *
 * Provides semantic helpers for console output with automatic icons and colors.
 * Uses Bun's built-in color support and ANSI escape codes.
 */
import { stringWidth } from 'bun';
import { resolve } from 'node:path';
import { colorize } from 'json-colorizer';
import enquirer from 'enquirer';
import { type OrganizationList, projectList } from '@agentuity/server';
import * as readline from 'readline';
import type { ColorScheme } from './terminal';
import type { Profile } from './types';
import { type APIClient as APIClientType } from './api';
import { getExitCode } from './errors';
import { maskSecret } from './env-util';
import { getExecutingAgent } from './agent-detection';

// Install global exit handler to always restore terminal cursor
// This ensures cursor is restored even when process.exit() is called directly
let exitHandlerInstalled = false;
function ensureCursorRestoration(): void {
	if (exitHandlerInstalled) return;
	exitHandlerInstalled = true;

	const restoreCursor = () => {
		// Only write ANSI escape sequences when stderr is a real terminal.
		// Writing to non-TTY streams (pipes, command substitution, etc.)
		// pollutes captured output with invisible control characters.
		if (!process.stderr.isTTY) {
			return;
		}
		// Skip cursor restoration in CI - terminals don't support these sequences
		if (process.env.CI) {
			return;
		}
		// Skip cursor restoration when running from an AI coding agent
		if (getExecutingAgent()) {
			return;
		}
		// Restore cursor visibility
		process.stderr.write('\x1B[?25h');
	};

	// Handle process exit
	process.on('exit', restoreCursor);
}

// Install handler immediately when module loads
ensureCursorRestoration();

// Re-export maskSecret for convenience
export { maskSecret };

// Export new TUI components
export { createPrompt, PromptFlow } from './tui/prompt';
export { group } from './tui/group';
export { note, drawBox, errorBox, warningBox } from './tui/box';
export { symbols } from './tui/symbols';
export { colors as tuiColors } from './tui/colors';
export type {
	TextOptions,
	ConfirmOptions,
	SelectOptions,
	SelectOption,
	MultiSelectOptions,
} from './tui/prompt';

// Icons - use plain text alternatives when running from an AI coding agent
function getIcons() {
	if (getExecutingAgent()) {
		return {
			success: '[OK]',
			error: '[ERROR]',
			warning: '[WARN]',
			info: '[INFO]',
			arrow: '->',
			bullet: '-',
		} as const;
	}
	return {
		success: '✓',
		error: '✗',
		warning: '⚠',
		info: 'ℹ',
		arrow: '→',
		bullet: '•',
	} as const;
}

// Export ICONS as a getter for backward compatibility
// Proxy with full traps for enumeration and serialization support
export const ICONS = new Proxy({} as ReturnType<typeof getIcons>, {
	get(_target, prop: keyof ReturnType<typeof getIcons>) {
		return getIcons()[prop];
	},
	has(_target, prop) {
		return prop in getIcons();
	},
	ownKeys() {
		return Object.keys(getIcons());
	},
	getOwnPropertyDescriptor(_target, prop) {
		const icons = getIcons();
		if (prop in icons) {
			return { configurable: true, enumerable: true, value: icons[prop as keyof typeof icons] };
		}
	},
});

/**
 * Check if we should treat the terminal as TTY-like for interactive output
 * (real TTY on stdout or stderr, or FORCE_COLOR set by fork wrapper).
 * Returns false in CI environments since CI terminals often don't support
 * cursor control sequences reliably.
 * Returns false when running from an AI coding agent (no interactive prompts/spinners).
 */
export function isTTYLike(): boolean {
	if (process.env.CI) {
		return false;
	}
	// Disable interactive features when running from an AI coding agent
	if (getExecutingAgent()) {
		return false;
	}
	return !!process.stdout.isTTY || !!process.stderr.isTTY || process.env.FORCE_COLOR === '1';
}

/**
 * Get terminal width, respecting COLUMNS env var for piped processes
 */
export function getTerminalWidth(defaultWidth = 80): number {
	if (process.stdout.columns) {
		return process.stdout.columns;
	}
	if (process.env.COLUMNS) {
		const cols = parseInt(process.env.COLUMNS, 10);
		if (!Number.isNaN(cols) && cols > 0) {
			return cols;
		}
	}
	return defaultWidth;
}

export function shouldUseColors(): boolean {
	// FORCE_COLOR overrides TTY detection (used by fork wrapper)
	if (process.env.FORCE_COLOR === '1') {
		return true;
	}
	// Disable colors when running from an AI coding agent (cleaner output for parsing)
	if (getExecutingAgent()) {
		return false;
	}
	return (
		!process.env.NO_COLOR &&
		!process.env.CI &&
		process.env.TERM !== 'dumb' &&
		!!process.stdout.isTTY
	);
}

// Color definitions (light/dark adaptive)
// Note: We use direct ANSI codes instead of Bun.color() because Bun.color()
// returns corrupted sequences when stdout is not a TTY (even with FORCE_COLOR=1)
function getColors() {
	const USE_COLORS = shouldUseColors();
	if (!USE_COLORS) {
		return {
			success: { light: '', dark: '' },
			error: { light: '', dark: '' },
			warning: { light: '', dark: '' },
			info: { light: '', dark: '' },
			muted: { light: '', dark: '' },
			bold: { light: '', dark: '' },
			link: { light: '', dark: '' },
			primary: { light: '', dark: '' },
			reset: '',
		} as const;
	}

	return {
		success: {
			light: '\x1b[32m', // green
			dark: '\x1b[92m', // bright green
		},
		error: {
			light: '\x1b[31m', // red
			dark: '\x1b[91m', // bright red
		},
		warning: {
			light: '\x1b[33m', // yellow
			dark: '\x1b[93m', // bright yellow
		},
		info: {
			light: '\x1b[36m', // dark cyan
			dark: '\x1b[96m', // bright cyan
		},
		muted: {
			light: '\x1b[90m', // gray
			dark: '\x1b[90m', // gray
		},
		bold: {
			light: '\x1b[1m',
			dark: '\x1b[1m',
		},
		link: {
			light: '\x1b[34;4m', // blue underline
			dark: '\x1b[94;4m', // bright blue underline
		},
		primary: {
			light: '\x1b[30m', // black
			dark: '\x1b[97m', // white
		},
		reset: '\x1b[0m',
	} as const;
}

let currentColorScheme: ColorScheme = process.env.CI ? 'light' : 'dark';

export function setColorScheme(scheme: ColorScheme): void {
	currentColorScheme = scheme;
	process.env.COLOR_SCHEME = scheme;
}

export function isDarkMode(): boolean {
	return currentColorScheme === 'dark';
}

export function getColor(colorKey: keyof ReturnType<typeof getColors>): string {
	const COLORS = getColors();
	const color = COLORS[colorKey];
	if (typeof color === 'string') {
		return color;
	}
	return color[currentColorScheme];
}

/**
 * Color helpers that return colored strings (for inline use, no icons)
 */
export function colorSuccess(text: string): string {
	const color = getColor('success');
	const reset = getColor('reset');
	return `${color}${text}${reset}`;
}

export function colorError(text: string): string {
	const color = getColor('error');
	const reset = getColor('reset');
	return `${color}${text}${reset}`;
}

export function colorWarning(text: string): string {
	const color = getColor('warning');
	const reset = getColor('reset');
	return `${color}${text}${reset}`;
}

export function colorInfo(text: string): string {
	const color = getColor('info');
	const reset = getColor('reset');
	return `${color}${text}${reset}`;
}

export function colorMuted(text: string): string {
	const color = getColor('muted');
	const reset = getColor('reset');
	return `${color}${text}${reset}`;
}

export function colorPrimary(text: string): string {
	const color = getColor('primary');
	const reset = getColor('reset');
	return `${color}${text}${reset}`;
}

/**
 * Get the appropriate color function for a log severity level
 */
export function getSeverityColor(severity: string): (text: string) => string {
	switch (severity.toUpperCase()) {
		case 'ERROR':
			return colorError;
		case 'WARN':
			return colorWarning;
		case 'INFO':
			return colorInfo;
		case 'DEBUG':
			return colorMuted;
		default:
			return (text: string) => text;
	}
}

/**
 * Print a success message with a green checkmark
 */
export function success(message: string): void {
	const color = getColor('success');
	const reset = getColor('reset');
	if (process.stderr.isTTY) {
		// Clear line first to ensure no leftover content from previous output
		process.stderr.write(`\r\x1b[2K${color}${ICONS.success} ${message}${reset}\n`);
	} else {
		// No ANSI control sequences for non-TTY streams (pipes, command substitution)
		process.stderr.write(`${ICONS.success} ${message}\n`);
	}
}

/**
 * Print an error message with a red X
 */
export function error(message: string): void {
	const color = getColor('error');
	const reset = getColor('reset');
	process.stderr.write(`${color}${ICONS.error} ${message}${reset}\n`);
}

/**
 * Print an error message with a red X and then exit
 */
export function fatal(message: string, errorCode?: import('./errors').ErrorCode): never {
	const color = getColor('error');
	const reset = getColor('reset');
	process.stderr.write(`${color}${ICONS.error} ${message}${reset}\n`);

	if (errorCode) {
		const exitCode = getExitCode(errorCode);
		process.exit(exitCode);
	} else {
		process.exit(1);
	}
}

/**
 * Print a warning message with a yellow warning icon
 */
export function warning(message: string, asError = false): void {
	const color = asError ? getColor('error') : getColor('warning');
	const reset = getColor('reset');
	process.stderr.write(`${color}${ICONS.warning} ${message}${reset}\n`);
}

/**
 * Print an info message with a cyan info icon
 */
export function info(message: string): void {
	const color = getColor('info');
	const reset = getColor('reset');
	process.stderr.write(`${color}${ICONS.info} ${message}${reset}\n`);
}

/**
 * Format text in muted/gray color
 */
export function muted(text: string): string {
	const color = getColor('muted');
	const reset = getColor('reset');
	return `${color}${text}${reset}`;
}

/**
 * Format text in warn color
 */
export function warn(text: string): string {
	const color = getColor('warning');
	const reset = getColor('reset');
	return `${color}${text}${reset}`;
}

/**
 * Format text in bold
 */
export function bold(text: string): string {
	const color = getColor('bold');
	const reset = getColor('reset');
	return `${color}${text}${reset}`;
}

/**
 * Format text with white bold (or inverse for light mode)
 * Used for table headings
 */
export function heading(text: string): string {
	const color = getColor('info');
	const reset = getColor('reset');
	return `${color}${text}${reset}`;
}

/**
 * Format text as a link (blue and underlined)
 */
export function link(url: string, title?: string, color = getColor('link')): string {
	const reset = getColor('reset');

	// Check if terminal supports hyperlinks (OSC 8) and colors are enabled
	if (shouldUseColors() && supportsHyperlinks()) {
		return `\x1b]8;;${url}\x07${color}${title ?? url}${reset}\x1b]8;;\x07`;
	}

	return `${color}${url}${reset}`;
}

/**
 * Check if terminal supports OSC 8 hyperlinks
 */
export function supportsHyperlinks(): boolean {
	// No hyperlink support without a TTY
	if (!process.stdout.isTTY) {
		return false;
	}

	const term = process.env.TERM || '';
	const termProgram = process.env.TERM_PROGRAM || '';
	const wtSession = process.env.WT_SESSION || '';

	// Known terminal programs that support OSC 8
	return (
		termProgram.includes('iTerm.app') ||
		termProgram.includes('WezTerm') ||
		termProgram.includes('ghostty') ||
		termProgram.includes('Apple_Terminal') ||
		termProgram.includes('Hyper') ||
		term.includes('xterm-kitty') ||
		wtSession !== '' // Windows Terminal
	);
}

export function fileUrl(file: string, line?: number, col?: number): string {
	const abs = resolve(file);

	// VS Code understands both file:// and vscode://,
	// but vscode:// allows line + column everywhere
	let url = `vscode://file/${abs}`;

	if (line != null) {
		url += `:${line}`;
		if (col != null) url += `:${col}`;
	}

	return url;
}

export function sourceLink(
	file: string,
	line: number,
	col: number,
	display?: string,
	color?: string
): string {
	const label = `${file}:${line}:${col}`;
	const url = fileUrl(file, line, col);

	if (supportsHyperlinks()) {
		return link(url, display ?? label, color);
	}

	// Cmd/Ctrl-click fallback
	if (color) {
		return color + label + getColor('reset');
	}

	return label;
}

/**
 * Print a bulleted list item
 */
export function bullet(message: string): void {
	process.stderr.write(`${ICONS.bullet} ${message}\n`);
}

/**
 * Print an arrow item (for showing next steps)
 */
export function arrow(message: string): void {
	process.stderr.write(`${ICONS.arrow} ${message}\n`);
}

/**
 * Print a blank line
 */
export function newline(): void {
	process.stderr.write('\n');
}

/**
 * Print a section header without any prefix icon
 */
export function header(message: string): void {
	const color = getColor('bold');
	const reset = getColor('reset');
	process.stderr.write(`${color}${message}${reset}\n`);
}

/**
 * Print plain text output without any prefix or icon
 * Use for primary command output that shouldn't have semantic formatting
 */
export function output(message: string): void {
	console.log(message);
}

/**
 * Get the display width of a string, handling ANSI codes and OSC 8 hyperlinks
 *
 * Note: Bun.stringWidth() counts OSC 8 hyperlink escape sequences in the width,
 * which causes incorrect alignment. We strip OSC 8 codes first, then use Bun.stringWidth()
 * to handle regular ANSI codes and unicode characters correctly.
 */
export function getDisplayWidth(str: string): number {
	// Remove OSC-8 hyperlink sequences using Unicode escapes (\u001b = ESC, \u0007 = BEL) to satisfy linter
	// eslint-disable-next-line no-control-regex
	const withoutOSC8 = str.replace(/\u001b\]8;;[^\u0007]*\u0007/g, '');
	return Bun.stringWidth(withoutOSC8);
}

/**
 * Strip all ANSI escape sequences from a string
 */
export function stripAnsi(str: string): string {
	/* eslint-disable no-control-regex */
	return str
		.replace(/\u001b\[[0-9;]*m/g, '') // SGR sequences (colors, bold, etc.)
		.replace(/\u001b\[\?[0-9;]*[a-zA-Z]/g, '') // DEC private mode (cursor show/hide, etc.)
		.replace(/\u001b\]8;;[^\u0007]*\u0007/g, ''); // OSC 8 hyperlinks
	/* eslint-enable no-control-regex */
}

/**
 * Truncate a string to a maximum display width, handling ANSI codes and Unicode correctly
 * Preserves ANSI escape sequences and doesn't break multi-byte characters or grapheme clusters
 */
export function truncateToWidth(str: string, maxWidth: number, ellipsis = '...'): string {
	const totalWidth = getDisplayWidth(str);
	if (totalWidth <= maxWidth) {
		return str;
	}

	// Strip ANSI to get visible text
	const visible = stripAnsi(str);

	// Use Intl.Segmenter for grapheme-aware iteration
	const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
	const segments = Array.from(segmenter.segment(visible));

	// Find the cutoff point by accumulating display width
	let currentWidth = 0;
	let cutIndex = 0;
	const targetWidth = maxWidth - ellipsis.length;

	for (let i = 0; i < segments.length; i++) {
		const seg = segments[i];
		if (!seg) continue;
		const segment = seg.segment;
		const segmentWidth = Bun.stringWidth(segment);

		if (currentWidth + segmentWidth > targetWidth) {
			break;
		}

		currentWidth += segmentWidth;
		cutIndex = seg.index + segment.length;
	}

	// Now reconstruct with ANSI codes preserved
	// Walk through original string and copy characters + ANSI codes until we hit cutIndex in visible content
	let result = '';
	let visibleIndex = 0;
	let i = 0;

	while (i < str.length && visibleIndex < cutIndex) {
		// Check for ANSI escape sequence
		if (str[i] === '\u001b') {
			/* eslint-disable no-control-regex */
			// Copy entire SGR sequence (colors, bold, etc.)
			const match = str.slice(i).match(/^\u001b\[[0-9;]*m/);
			if (match) {
				result += match[0];
				i += match[0].length;
				continue;
			}

			// Check for DEC private mode (cursor show/hide, etc.)
			const decMatch = str.slice(i).match(/^\u001b\[\?[0-9;]*[a-zA-Z]/);
			if (decMatch) {
				result += decMatch[0];
				i += decMatch[0].length;
				continue;
			}

			// Check for OSC 8 hyperlink
			const oscMatch = str.slice(i).match(/^\u001b\]8;;[^\u0007]*\u0007/);
			if (oscMatch) {
				result += oscMatch[0];
				i += oscMatch[0].length;
				continue;
			}
			/* eslint-enable no-control-regex */
		}

		// Copy visible character
		result += str[i];
		visibleIndex++;
		i++;
	}

	return result + ellipsis;
}

/**
 * Pad a string to a specific length on the right
 */
export function padRight(str: string, length: number, pad = ' '): string {
	const displayWidth = getDisplayWidth(str);
	if (displayWidth >= length) {
		return str;
	}
	return str + pad.repeat(length - displayWidth);
}

/**
 * Pad a string to a specific length on the left
 */
export function padLeft(str: string, length: number, pad = ' '): string {
	const displayWidth = getDisplayWidth(str);
	if (displayWidth >= length) {
		return str;
	}
	return pad.repeat(length - displayWidth) + str;
}

interface BannerOptions {
	padding?: number;
	minWidth?: number;
	topSpacer?: boolean;
	middleSpacer?: boolean;
	bottomSpacer?: boolean;
	centerTitle?: boolean;
}

/**
 * Display a formatted banner with title and body content
 * Creates a bordered box around the content
 *
 * Uses Bun.stringWidth() for accurate width calculation with ANSI codes and unicode
 * Responsive to terminal width - adapts to narrow terminals
 */
export function banner(title: string, body: string, options?: BannerOptions): void {
	// Get terminal width, default to 120 if not available
	const termWidth = getTerminalWidth(120);

	const border = {
		topLeft: '╭',
		topRight: '╮',
		bottomLeft: '╰',
		bottomRight: '╯',
		horizontal: '─',
		vertical: '│',
	};

	// Calculate content width first (before wrapping)
	const titleWidth = getDisplayWidth(title);
	const bodyLines = body.split('\n');
	const maxBodyWidth = Math.max(0, ...bodyLines.map((line) => getDisplayWidth(line)));
	const requiredContentWidth = Math.max(titleWidth, maxBodyWidth);

	// Box width = content + borders (2) + side spaces (2)
	const boxWidth = Math.min(requiredContentWidth + 4, termWidth);

	// If required content width exceeds terminal width, skip box and print plain text
	if (requiredContentWidth + 4 > termWidth) {
		console.log('\n' + bold(title));
		console.log(body + '\n');
		return;
	}

	// Inner width is box width minus borders (2) and side spaces (2)
	const innerWidth = boxWidth - 4;

	// Wrap text to fit box width
	const wrappedBodyLines = wrapText(body, innerWidth);

	// Colors
	const borderColor = getColor('muted');
	const titleColor = getColor('info');
	const reset = getColor('reset');

	// Build banner
	const lines: string[] = [];

	// Top border
	lines.push(
		`${borderColor}${border.topLeft}${border.horizontal.repeat(boxWidth - 2)}${border.topRight}${reset}`
	);

	if (options?.topSpacer === true || options?.topSpacer === undefined) {
		// Empty line
		lines.push(
			`${borderColor}${border.vertical}${' '.repeat(boxWidth - 2)}${border.vertical}${reset}`
		);
	}

	// Title (centered and bold)
	const titleDisplayWidth = getDisplayWidth(title);
	if (options?.centerTitle === true || options?.centerTitle === undefined) {
		const titlePadding = Math.max(0, Math.floor((innerWidth - titleDisplayWidth) / 2));
		const titleRightPadding = Math.max(0, innerWidth - titlePadding - titleDisplayWidth);
		const titleLine =
			' '.repeat(titlePadding) +
			`${titleColor}${bold(title)}${reset}` +
			' '.repeat(titleRightPadding);
		lines.push(
			`${borderColor}${border.vertical} ${reset}${titleLine}${borderColor} ${border.vertical}${reset}`
		);
	} else {
		const titleRightPadding = Math.max(0, innerWidth - titleDisplayWidth);
		const titleLine = `${titleColor}${bold(title)}${reset}` + ' '.repeat(titleRightPadding);
		lines.push(
			`${borderColor}${border.vertical} ${reset}${titleLine}${borderColor} ${border.vertical}${reset}`
		);
	}

	if (options?.middleSpacer === true || options?.middleSpacer === undefined) {
		// Empty line
		lines.push(
			`${borderColor}${border.vertical}${' '.repeat(boxWidth - 2)}${border.vertical}${reset}`
		);
	}

	// Body lines
	for (const line of wrappedBodyLines) {
		const lineWidth = getDisplayWidth(line);
		const linePadding = Math.max(0, innerWidth - lineWidth);
		lines.push(
			`${borderColor}${border.vertical} ${reset}${line}${' '.repeat(linePadding)}${borderColor} ${border.vertical}${reset}`
		);
	}

	if (options?.bottomSpacer === true || options?.bottomSpacer === undefined) {
		// Empty line
		lines.push(
			`${borderColor}${border.vertical}${' '.repeat(boxWidth - 2)}${border.vertical}${reset}`
		);
	}

	// Bottom border
	lines.push(
		`${borderColor}${border.bottomLeft}${border.horizontal.repeat(boxWidth - 2)}${border.bottomRight}${reset}`
	);

	// Print the banner
	console.log('\n' + lines.join('\n') + '\n');
}

/**
 * Wait for any key press before continuing
 * Displays a prompt message and waits for user input
 * Exits the process on CTRL+C
 */
export async function waitForAnyKey(message = 'Press Enter to continue...'): Promise<void> {
	process.stdout.write(muted(message));

	// Check if we're in a TTY environment
	if (!process.stdin.isTTY) {
		// Not a TTY (CI/piped), just write newline and exit
		console.log('');
		return Promise.resolve();
	}

	// Set stdin to raw mode to read a single keypress
	process.stdin.setRawMode(true);
	process.stdin.resume();
	let rawModeSet = true;

	return new Promise((resolve) => {
		process.stdin.once('data', (data: Buffer) => {
			if (rawModeSet && process.stdin.isTTY) {
				process.stdin.setRawMode(false);
				rawModeSet = false;
			}
			process.stdin.pause();

			// Check for CTRL+C (character code 3)
			if (data.length === 1 && data[0] === 3) {
				console.log('\n');
				process.exit(0);
			}

			console.log('');
			resolve();
		});
	});
}

/**
 * Prompts user with a yes/no question
 * Returns true for yes, false for no
 * Exits process if CTRL+C is pressed
 */
export async function confirm(message: string, defaultValue = true): Promise<boolean> {
	const suffix = defaultValue ? '[Y/n]' : '[y/N]';
	process.stdout.write(`${message} ${muted(suffix)} `);

	// Check if we're in a TTY environment
	if (!process.stdin.isTTY) {
		console.log('');
		return defaultValue;
	}

	// Set stdin to raw mode to read a single keypress
	process.stdin.setRawMode(true);
	process.stdin.resume();
	let rawModeSet = true;

	return new Promise((resolve) => {
		process.stdin.once('data', (data: Buffer) => {
			if (rawModeSet && process.stdin.isTTY) {
				process.stdin.setRawMode(false);
				rawModeSet = false;
			}
			process.stdin.pause();

			// Check for CTRL+C (character code 3)
			if (data.length === 1 && data[0] === 3) {
				console.log('\n');
				process.exit(0);
			}

			const input = data.toString().trim().toLowerCase();
			console.log('');

			// Enter key (just newline) uses default
			if (input === '') {
				resolve(defaultValue);
				return;
			}

			// Check first character for y/n
			const char = input.charAt(0);
			if (char === 'y') {
				resolve(true);
			} else if (char === 'n') {
				resolve(false);
			} else {
				// Invalid input, use default
				resolve(defaultValue);
			}
		});
	});
}

/**
 * Display a signup benefits box with cyan border
 * Shows the value proposition for creating an Agentuity account
 */
export function showSignupBenefits(): void {
	const CYAN = Bun.color('cyan', 'ansi-16m');
	const TEXT =
		currentColorScheme === 'dark' ? Bun.color('white', 'ansi') : Bun.color('black', 'ansi');
	const RESET = '\x1b[0m';

	const lines = [
		'╔════════════════════════════════════════════╗',
		`║ ⨺ Signup for Agentuity             ${muted('free')}${CYAN}    ║`,
		'║                                            ║',
		`║ ✓ ${TEXT}Cloud deployment, previews and CI/CD${CYAN}     ║`,
		`║ ✓ ${TEXT}AI Gateway, KV, Vector and more${CYAN}          ║`,
		`║ ✓ ${TEXT}Observability, Tracing and Logging${CYAN}       ║`,
		`║ ✓ ${TEXT}Organization and Team support${CYAN}            ║`,
		`║ ✓ ${TEXT}And much more!${CYAN}                           ║`,
		'╚════════════════════════════════════════════╝',
	];

	console.log('');
	lines.map((line) => console.log(CYAN + line + RESET));
	console.log('');
}

/**
 * Display a message when unauthenticated to let the user know certain capabilities are disabled
 * @param hasProfile - If true, user has logged in before so only show "Login" instead of "Sign up / Login"
 */
export function showLoggedOutMessage(appBaseUrl: string, hasProfile = false): void {
	const YELLOW = Bun.color('yellow', 'ansi-16m');
	const TEXT =
		currentColorScheme === 'dark' ? Bun.color('white', 'ansi') : Bun.color('black', 'ansi');
	const RESET = '\x1b[0m';

	const signupTitle = hasProfile ? 'Login' : 'Sign up / Login';
	const signupURL = hasProfile ? `${appBaseUrl}/sign-in` : `${appBaseUrl}/sign-up`;
	const showInline = supportsHyperlinks();
	const signupLink = showInline
		? link(signupURL, signupTitle)
		: ' '.repeat(stringWidth(signupTitle));
	// Box inner width is 46 chars, "unauthenticated. " = 17 chars
	// Padding needed: 46 - 17 - signupTitle.length - 1 (space before link) = 28 - signupTitle.length
	const paddingLength = 28 - signupTitle.length;
	const padding = ' '.repeat(paddingLength);
	// When not showing inline hyperlink, show URL on separate line with proper padding
	// Box format: "║ " + content + "║" = 48 chars total
	// Content area = 46 chars, with leading space = 45 chars for URL + padding
	const urlPadding = Math.max(0, 45 - signupURL.length);
	const showNewLine = showInline
		? ''
		: `║ ${RESET}${link(signupURL)}${YELLOW}${' '.repeat(urlPadding)}║`;

	const lines = [
		'╔══════════════════════════════════════════════╗',
		`║ ⨺ Unauthenticated (local mode)               ║`,
		'║                                              ║',
		`║ ${TEXT}Certain capabilities such as the AI services${YELLOW} ║`,
		`║ ${TEXT}and devmode remote are unavailable when${YELLOW}      ║`,
		`║ ${TEXT}unauthenticated.${YELLOW} ${signupLink}${YELLOW}${padding}║`,
		showNewLine,
		'╚══════════════════════════════════════════════╝',
	];

	console.log('');
	lines.filter(Boolean).map((line) => console.log(YELLOW + line + RESET));
	console.log('');
	console.log('');
}

/**
 * Display a warning when running in local-only mode (no agentuity.json project config)
 * This is shown during `agentuity dev` when the project hasn't been registered with Agentuity Cloud
 */
export function showLocalOnlyWarning(): void {
	const YELLOW = Bun.color('yellow', 'ansi-16m');
	const TEXT =
		currentColorScheme === 'dark' ? Bun.color('white', 'ansi') : Bun.color('black', 'ansi');
	const RESET = '\x1b[0m';

	const lines = [
		'╔═══════════════════════════════════════════════════════════════╗',
		`║ ⨺ Local-only mode                                             ║`,
		'║                                                               ║',
		`║ ${TEXT}This project is not registered with Agentuity Cloud.${YELLOW}          ║`,
		`║ ${TEXT}The following features are disabled:${YELLOW}                          ║`,
		`║   ${TEXT}• AI Gateway (LLM calls require provider API keys)${YELLOW}          ║`,
		`║   ${TEXT}• Public URL / Remote access${YELLOW}                                ║`,
		`║   ${TEXT}• Dashboard / Tracing / Observability${YELLOW}                       ║`,
		'║                                                               ║',
		`║ ${TEXT}To enable cloud features, create a project and login.${YELLOW}         ║`,
		`║ ${TEXT}Or set provider API keys (e.g. OPENAI_API_KEY) in .env${YELLOW}        ║`,
		'╚═══════════════════════════════════════════════════════════════╝',
	];

	console.log('');
	lines.map((line) => console.log(YELLOW + line + RESET));
	console.log('');
}

/**
 * Copy text to clipboard
 * Returns true if successful, false otherwise
 */
export async function copyToClipboard(text: string): Promise<boolean> {
	try {
		const platform = process.platform;

		if (platform === 'darwin') {
			// macOS - use pbcopy
			const proc = Bun.spawn(['pbcopy'], {
				stdin: 'pipe',
			});
			proc.stdin.write(text);
			proc.stdin.end();
			await proc.exited;
			return proc.exitCode === 0;
		} else if (platform === 'win32') {
			// Windows - use clip
			const proc = Bun.spawn(['clip'], {
				stdin: 'pipe',
			});
			proc.stdin.write(text);
			proc.stdin.end();
			await proc.exited;
			return proc.exitCode === 0;
		} else {
			// Linux - try xclip first, then xsel
			try {
				const proc = Bun.spawn(['xclip', '-selection', 'clipboard'], {
					stdin: 'pipe',
				});
				proc.stdin.write(text);
				proc.stdin.end();
				await proc.exited;
				return proc.exitCode === 0;
			} catch {
				// Try xsel as fallback
				const proc = Bun.spawn(['xsel', '--clipboard', '--input'], {
					stdin: 'pipe',
				});
				proc.stdin.write(text);
				proc.stdin.end();
				await proc.exited;
				return proc.exitCode === 0;
			}
		}
	} catch {
		return false;
	}
}

/**
 * Extract ANSI codes from the beginning of a string
 */
function extractLeadingAnsiCodes(str: string): string {
	// Match ANSI escape sequences at the start of the string
	// eslint-disable-next-line no-control-regex
	const match = str.match(/^(\x1b\[[0-9;]*m)+/);
	return match ? match[0] : '';
}

/**
 * Strip ANSI codes from a string
 */
function stripAnsiCodes(str: string): string {
	// Remove all ANSI escape sequences
	/* eslint-disable no-control-regex */
	return str
		.replace(/\x1b\[[0-9;]*m/g, '') // SGR sequences
		.replace(/\x1b\[\?[0-9;]*[a-zA-Z]/g, ''); // DEC private mode
	/* eslint-enable no-control-regex */
}

/**
 * Check if a string ends with ANSI reset code
 */
function endsWithReset(str: string): boolean {
	return str.endsWith('\x1b[0m') || str.endsWith(getColor('reset'));
}

/**
 * Wrap text to a maximum width
 * Handles explicit newlines and word wrapping
 * Preserves ANSI color codes across wrapped lines
 */
export function wrapText(text: string, maxWidth: number): string[] {
	const allLines: string[] = [];

	// First split by explicit newlines
	const paragraphs = text.split('\n');

	for (const paragraph of paragraphs) {
		// Skip empty paragraphs (they become blank lines)
		if (paragraph.trim() === '') {
			allLines.push('');
			continue;
		}

		// Record starting index for this paragraph's lines
		const paragraphStart = allLines.length;

		// Extract any leading ANSI codes from the paragraph
		const leadingCodes = extractLeadingAnsiCodes(paragraph);
		const hasReset = endsWithReset(paragraph);

		// Wrap each paragraph
		const words = paragraph.split(' ');
		let currentLine = '';

		for (const word of words) {
			const testLine = currentLine ? `${currentLine} ${word}` : word;
			const testLineWidth = getDisplayWidth(testLine);

			if (testLineWidth <= maxWidth) {
				currentLine = testLine;
			} else {
				// If current line has content, save it
				if (currentLine) {
					allLines.push(currentLine);
				}
				// If the word itself is longer than maxWidth, just use it as is
				// (better to have a long line than break in the middle)
				// But if we have leading codes and this isn't the first line, apply them
				if (leadingCodes && currentLine) {
					// Strip any existing codes from the word to avoid duplication
					const strippedWord = stripAnsiCodes(word);
					currentLine = leadingCodes + strippedWord;
				} else {
					currentLine = word;
				}
			}
		}

		if (currentLine) {
			allLines.push(currentLine);
		}

		// If the original paragraph had ANSI codes and ended with reset,
		// ensure each wrapped line ends with reset (only for this paragraph's lines)
		if (leadingCodes && hasReset) {
			for (let i = paragraphStart; i < allLines.length; i++) {
				const line = allLines[i];
				if (line !== undefined && !endsWithReset(line)) {
					allLines[i] = line + getColor('reset');
				}
			}
		}
	}

	return allLines.length > 0 ? allLines : [''];
}

/**
 * Progress callback for spinner
 */
export type SpinnerProgressCallback = (progress: number) => void;

/**
 * Log callback for spinner
 */
export type SpinnerLogCallback = (message: string) => void;

/**
 * Spinner options (simple without progress)
 */
export interface SimpleSpinnerOptions<T> {
	type?: 'simple';
	message: string;
	callback: (() => Promise<T>) | Promise<T>;
	/**
	 * If true, clear the spinner output on success (no icon, no message)
	 * Defaults to false
	 */
	clearOnSuccess?: boolean;
	/**
	 * If true, suppress the error message display on failure (for custom error handling)
	 * Defaults to false
	 */
	clearOnError?: boolean;
}

/**
 * Spinner options (with progress tracking)
 */
export interface ProgressSpinnerOptions<T> {
	type: 'progress';
	message: string;
	callback: (progress: SpinnerProgressCallback) => Promise<T>;
	/**
	 * If true, clear the spinner output on success (no icon, no message)
	 * Defaults to false
	 */
	clearOnSuccess?: boolean;
	/**
	 * If true, suppress the error message display on failure (for custom error handling)
	 * Defaults to false
	 */
	clearOnError?: boolean;
}

/**
 * Spinner options (with logger streaming)
 */
export interface LoggerSpinnerOptions<T> {
	type: 'logger';
	message: string;
	callback: (log: SpinnerLogCallback) => Promise<T>;
	/**
	 * If true, clear the spinner output on success (no icon, no message)
	 * Defaults to false
	 */
	clearOnSuccess?: boolean;
	/**
	 * Maximum number of log lines to show while running
	 * If < 0, shows all lines. Defaults to 3.
	 */
	maxLines?: number;
}

/**
 * Spinner options (with countdown timer)
 */
export interface CountdownSpinnerOptions<T> {
	type: 'countdown';
	message: string;
	timeoutMs: number;
	callback: () => Promise<T>;
	/**
	 * If true, clear the spinner output on success (no icon, no message)
	 * Defaults to false
	 */
	clearOnSuccess?: boolean;
	/**
	 * Optional callback to handle Enter key press
	 * Can be used to open a URL in the browser
	 */
	onEnterPress?: () => void;
}

/**
 * Spinner options (discriminated union)
 */
export type SpinnerOptions<T> =
	| SimpleSpinnerOptions<T>
	| ProgressSpinnerOptions<T>
	| LoggerSpinnerOptions<T>
	| CountdownSpinnerOptions<T>;

/**
 * Run a callback with an animated spinner (simple overload)
 *
 * Shows a spinner animation while the callback executes.
 * On success, shows a checkmark. On error, shows an X and re-throws.
 *
 * @param message - The message to display next to the spinner
 * @param callback - Async function or Promise to execute
 */
export async function spinner<T>(
	message: string,
	callback: (() => Promise<T>) | Promise<T>
): Promise<T>;

/**
 * Run a callback with an animated spinner (options overload)
 *
 * Shows a spinner animation while the callback executes.
 * On success, shows a checkmark. On error, shows an X and re-throws.
 *
 * @param options - Spinner options with optional progress tracking
 */
export async function spinner<T>(options: SpinnerOptions<T>): Promise<T>;

export async function spinner<T>(
	messageOrOptions: string | SpinnerOptions<T>,
	callback?: (() => Promise<T>) | Promise<T>
): Promise<T> {
	// Normalize to options format
	let options: SpinnerOptions<T>;
	if (typeof messageOrOptions === 'string') {
		if (callback === undefined) {
			throw new Error('callback is required when first argument is a string');
		}
		options = { type: 'simple', message: messageOrOptions, callback };
	} else {
		options = messageOrOptions;
	}

	// assume true by default
	if (options.clearOnSuccess === undefined) {
		options.clearOnSuccess = true;
	}

	const message = options.message;
	const reset = getColor('reset');

	// Check if progress should be disabled (from global options)
	const { getOutputOptions, shouldDisableProgress } = await import('./output');
	const outputOptions = getOutputOptions();
	const noProgress = outputOptions ? shouldDisableProgress(outputOptions) : false;
	const isJsonMode = outputOptions?.json === true;

	// If stderr is not a real terminal or progress disabled, just execute
	// the callback without animation. We check stderr specifically because
	// the spinner writes ANSI sequences to stderr — isTTYLike() may return
	// true when stdout is a TTY but stderr is piped (e.g. 2>&1 in $()).
	if (!process.stderr.isTTY || noProgress) {
		try {
			const result =
				options.type === 'progress'
					? await options.callback(() => {})
					: options.type === 'logger'
						? await options.callback((logMessage: string) => {
								// In JSON mode, don't write logs to stdout
								if (!isJsonMode) {
									process.stdout.write(logMessage + '\n');
								}
							})
						: options.type === 'countdown'
							? await options.callback()
							: typeof options.callback === 'function'
								? await options.callback()
								: await options.callback;

			// If clearOnSuccess is true, don't show success message
			// Also skip success message in JSON mode
			if (!options.clearOnSuccess && !isJsonMode) {
				const successColor = getColor('success');
				console.error(`${successColor}${ICONS.success} ${message}${reset}`);
			}

			return result;
		} catch (err) {
			const clearOnError =
				(options.type === 'progress' || options.type === 'simple') && options.clearOnError;
			if (!clearOnError && !isJsonMode) {
				const errorColor = getColor('error');
				console.error(`${errorColor}${ICONS.error} ${message}${reset}`);
			}
			throw err;
		}
	}

	const frames = ['◐', '◓', '◑', '◒'];
	const spinnerColors = [
		{ light: '\x1b[36m', dark: '\x1b[96m' }, // cyan
		{ light: '\x1b[34m', dark: '\x1b[94m' }, // blue
		{ light: '\x1b[35m', dark: '\x1b[95m' }, // magenta
		{ light: '\x1b[36m', dark: '\x1b[96m' }, // cyan
	];
	const bold = '\x1b[1m';
	const cyanColor = { light: '\x1b[36m', dark: '\x1b[96m' }[currentColorScheme];

	let frameIndex = 0;
	let currentProgress: number | undefined;
	let remainingTime: number | undefined;
	const logLines: string[] = [];
	const maxLines = options.type === 'logger' ? (options.maxLines ?? 3) : 0;
	const mutedColor = getColor('muted');
	let linesRendered = 0;

	// Get terminal width for truncation
	const termWidth = getTerminalWidth(80);
	const maxLineWidth = Math.min(80, termWidth);

	// Function to render spinner with optional log lines
	const renderSpinner = () => {
		// Move cursor up to start of our output area if we've rendered before
		if (linesRendered > 0) {
			process.stderr.write(`\x1b[${linesRendered}A`);
		}

		const colorDef = spinnerColors[frameIndex % spinnerColors.length] ?? spinnerColors[0];
		const color = colorDef?.[currentColorScheme] ?? '';
		const frame = `${color}${bold}${frames[frameIndex % frames.length] ?? ''}${reset}`;

		// Add progress indicator or countdown timer if available
		let indicator = '';
		if (currentProgress !== undefined) {
			indicator = ` ${cyanColor}${Math.floor(currentProgress)}%${reset}`;
		} else if (remainingTime !== undefined) {
			const minutes = Math.floor(remainingTime / 60);
			const seconds = Math.floor(remainingTime % 60);
			const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
			indicator = ` ${mutedColor}(${timeStr} remaining)${reset}`;
		}

		// Render spinner line
		process.stderr.write(`\r\x1b[K${frame} ${message}${indicator}\n`);

		// Render log lines if in logger mode
		if (options.type === 'logger') {
			const displayLines = maxLines < 0 ? logLines : logLines.slice(-maxLines);
			for (const line of displayLines) {
				const displayLine =
					getDisplayWidth(line) > maxLineWidth ? truncateToWidth(line, maxLineWidth) : line;
				process.stderr.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
			}
			linesRendered = 1 + displayLines.length;
		} else {
			linesRendered = 1;
		}

		frameIndex++;
	};

	// Save cursor position and hide cursor
	process.stderr.write('\x1B[s\x1B[?25l');

	// Initial render
	renderSpinner();

	// Start animation
	const interval = setInterval(renderSpinner, 120);

	// Progress callback
	const progressCallback: SpinnerProgressCallback = (progress: number) => {
		currentProgress = Math.min(100, Math.max(0, progress));
	};

	// Log callback
	const logCallback: SpinnerLogCallback = (logMessage: string) => {
		logLines.push(logMessage);
	};

	// Countdown interval tracking
	let countdownInterval: NodeJS.Timeout | undefined;
	let keypressListener: ((chunk: Buffer) => void) | undefined;

	// Helper to clean up all resources
	const cleanup = () => {
		if (countdownInterval) {
			clearInterval(countdownInterval);
		}
		if (keypressListener) {
			process.stdin.off('data', keypressListener);
			if (process.stdin.isTTY) {
				process.stdin.setRawMode(false);
				process.stdin.pause();
			}
		}
		process.off('SIGINT', cleanupAndExit);
	};

	// Set up SIGINT handler for clean exit
	const cleanupAndExit = () => {
		cleanup();

		// Stop animation
		clearInterval(interval);

		// Move cursor to start of output, clear only our lines (not to end of screen)
		if (linesRendered > 0) {
			process.stderr.write(`\x1b[${linesRendered}A`);
			for (let i = 0; i < linesRendered; i++) {
				process.stderr.write('\x1b[2K'); // Clear entire line
				if (i < linesRendered - 1) {
					process.stderr.write('\x1b[B'); // Move down one line
				}
			}
			process.stderr.write(`\x1b[${linesRendered}A\r`); // Move back up
		}
		process.stderr.write('\x1B[?25h'); // Show cursor

		process.exit(130); // Standard exit code for SIGINT
	};

	process.on('SIGINT', cleanupAndExit);

	try {
		// For countdown, set up timer tracking and optional keyboard listener
		if (options.type === 'countdown') {
			const startTime = Date.now();
			remainingTime = options.timeoutMs / 1000;
			countdownInterval = setInterval(() => {
				const elapsed = Date.now() - startTime;
				remainingTime = Math.max(0, (options.timeoutMs - elapsed) / 1000);
			}, 100);

			// Set up Enter key listener if callback provided
			if (options.onEnterPress && process.stdin.isTTY) {
				process.stdin.setRawMode(true);
				process.stdin.resume();

				keypressListener = (chunk: Buffer) => {
					const key = chunk.toString();
					// Check for Enter key (both \r and \n)
					if (key === '\r' || key === '\n') {
						options.onEnterPress!();
					}
					// Check for Ctrl+C - let it propagate as SIGINT
					if (key === '\x03') {
						process.kill(process.pid, 'SIGINT');
					}
				};

				process.stdin.on('data', keypressListener);
			}
		}

		// Execute callback
		const result =
			options.type === 'countdown'
				? await options.callback()
				: options.type === 'progress'
					? await options.callback(progressCallback)
					: options.type === 'logger'
						? await options.callback(logCallback)
						: typeof options.callback === 'function'
							? await options.callback()
							: await options.callback;

		cleanup();

		// Stop animation first
		clearInterval(interval);

		// Move cursor to start of output, clear only our lines (not to end of screen)
		if (linesRendered > 0) {
			process.stderr.write(`\x1b[${linesRendered}A`);
			for (let i = 0; i < linesRendered; i++) {
				process.stderr.write('\r\x1b[2K'); // Clear entire line
				if (i < linesRendered - 1) {
					process.stderr.write('\x1b[B'); // Move down one line
				}
			}
			// After loop, cursor is at last cleared line (linesRendered - 1 from start)
			// Move up (linesRendered - 1) to get back to start position
			if (linesRendered > 1) {
				process.stderr.write(`\x1b[${linesRendered - 1}A`);
			}
			process.stderr.write('\r');
		}
		process.stderr.write('\x1B[?25h'); // Show cursor

		// If clearOnSuccess is false, show success message
		if (!options.clearOnSuccess) {
			// Show success
			const successColor = getColor('success');
			console.error(`${successColor}${ICONS.success} ${message}${reset}`);
		}

		return result;
	} catch (err) {
		cleanup();

		// Stop animation first
		clearInterval(interval);

		// Move cursor to start of output, clear only our lines (not to end of screen)
		if (linesRendered > 0) {
			process.stderr.write(`\x1b[${linesRendered}A`);
			for (let i = 0; i < linesRendered; i++) {
				process.stderr.write('\r\x1b[2K'); // Clear entire line
				if (i < linesRendered - 1) {
					process.stderr.write('\x1b[B'); // Move down one line
				}
			}
			// After loop, cursor is at last cleared line (linesRendered - 1 from start)
			// Move up (linesRendered - 1) to get back to start position
			if (linesRendered > 1) {
				process.stderr.write(`\x1b[${linesRendered - 1}A`);
			}
			process.stderr.write('\r');
		}
		process.stderr.write('\x1B[?25h'); // Show cursor

		// Show error (unless clearOnError is set for custom error handling)
		const clearOnError =
			(options.type === 'progress' || options.type === 'simple') && options.clearOnError;
		if (!clearOnError) {
			const errorColor = getColor('error');
			const errorMessage = err instanceof Error ? err.message : String(err);
			console.error(`${errorColor}${ICONS.error} ${message}: ${errorMessage}${reset}`);
		}

		throw err;
	}
}

/**
 * Alias for spinner function (for better semantics when using progress/logger types)
 */
export const progress = spinner;

/**
 * Options for running a command with streaming output
 */
export interface CommandRunnerOptions {
	/**
	 * The command to run (displayed in the UI)
	 */
	command: string;
	/**
	 * The actual command and arguments to execute
	 */
	cmd: string[];
	/**
	 * Current working directory
	 */
	cwd?: string;
	/**
	 * Environment variables
	 */
	env?: Record<string, string>;
	/**
	 * If true, clear output on success and only show command + success icon
	 * Defaults to false
	 */
	clearOnSuccess?: boolean;
	/**
	 * If true or undefined, will truncate each line of output
	 */
	truncate?: boolean;
	/**
	 * If undefined, will show up to 3 last lines of output while running. Customize the number with this property.
	 */
	maxLinesOutput?: number;
	/**
	 * If undefined, will show up to 10 last lines on failure. Customize the number with this property.
	 */
	maxLinesOnFailure?: number;
}

/**
 * Run an external command and stream its output with a live UI
 *
 * Displays the command with a colored $ prompt:
 * - Blue while running
 * - Green on successful exit (code 0)
 * - Red on failed exit (code != 0)
 *
 * Shows the last 3 lines of output as it streams.
 */
export async function runCommand(options: CommandRunnerOptions): Promise<number> {
	const {
		command,
		cmd,
		cwd,
		env,
		clearOnSuccess = false,
		truncate = true,
		maxLinesOutput = 3,
		maxLinesOnFailure = 10,
	} = options;
	const isTTY = process.stdout.isTTY;

	// If not a TTY, just run the command normally and log output
	if (!isTTY) {
		const proc = Bun.spawn(cmd, {
			cwd,
			env: { ...process.env, ...env },
			stdout: 'inherit',
			stderr: 'inherit',
		});
		return await proc.exited;
	}

	// Colors using Bun.color
	const blue =
		currentColorScheme === 'light'
			? Bun.color('#0000FF', 'ansi') || '\x1b[34m'
			: Bun.color('#5C9CFF', 'ansi') || '\x1b[94m';
	const green = getColor('success');
	const red = getColor('error');
	const cmdColor =
		currentColorScheme === 'light'
			? '\x1b[1m' + (Bun.color('#00008B', 'ansi') || '\x1b[34m')
			: Bun.color('#FFFFFF', 'ansi') || '\x1b[97m'; // bold dark blue / white
	const mutedColor = Bun.color('#808080', 'ansi') || '\x1b[90m';
	const reset = getColor('reset');

	// Get terminal width
	const termWidth = getTerminalWidth(80);
	const maxCmdWidth = Math.min(40, termWidth);
	const maxLineWidth = Math.min(80, termWidth);

	// Truncate command if needed
	const displayCmd =
		getDisplayWidth(command) > maxCmdWidth ? truncateToWidth(command, maxCmdWidth) : command;

	// Store all output lines, display subset based on context
	const allOutputLines: string[] = [];
	let linesRendered = 0;

	// Hide cursor
	process.stdout.write('\x1B[?25l');

	// Render the command and output lines in place
	const renderOutput = (linesToShow: number) => {
		// Move cursor up to start of our output area
		if (linesRendered > 0) {
			process.stdout.write(`\x1b[${linesRendered}A`);
		}

		// Render command line
		process.stdout.write(`\r\x1b[K${blue}$${reset} ${cmdColor}${displayCmd}${reset}\n`);

		// Get last N lines to display
		const displayLines = allOutputLines.slice(-linesToShow);

		// Render output lines
		for (const line of displayLines) {
			const displayLine =
				getDisplayWidth(line) > maxLineWidth ? truncateToWidth(line, maxLineWidth) : line;
			process.stdout.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
		}

		// Update count of lines we've rendered (command + output lines)
		linesRendered = 1 + displayLines.length;
	};

	// Initial display
	renderOutput(maxLinesOutput);

	try {
		// Spawn the command
		const proc = Bun.spawn(cmd, {
			cwd,
			env: { ...process.env, ...env },
			stdout: 'pipe',
			stderr: 'pipe',
		});

		// Process output streams
		const processStream = async (stream: ReadableStream<Uint8Array>) => {
			const reader = stream.getReader();
			const decoder = new TextDecoder();
			let buffer = '';

			try {
				while (true) {
					const { done, value } = await reader.read();
					if (done) break;

					buffer += decoder.decode(value, { stream: true });
					const lines = buffer.split('\n');
					buffer = lines.pop() || ''; // Keep incomplete line in buffer

					for (const line of lines) {
						if (line.trim()) {
							// Strip ANSI codes from command output to prevent cursor/display issues
							allOutputLines.push(stripAnsi(line));
							renderOutput(maxLinesOutput); // Show last N lines while streaming
						}
					}
				}
			} finally {
				reader.releaseLock();
			}
		};

		// Process both stdout and stderr
		await Promise.all([processStream(proc.stdout), processStream(proc.stderr)]);

		// Wait for process to exit
		const exitCode = await proc.exited;

		// If clearOnSuccess is true and command succeeded, clear everything
		if (clearOnSuccess && exitCode === 0) {
			if (linesRendered > 0) {
				// Move up to the command line
				process.stdout.write(`\x1b[${linesRendered}A`);
				// Clear each line (entire line)
				for (let i = 0; i < linesRendered; i++) {
					process.stdout.write('\r\x1b[2K'); // Clear entire line
					if (i < linesRendered - 1) {
						process.stdout.write('\x1b[B'); // Move down one line
					}
				}
				// After loop, cursor is at last cleared line (linesRendered - 1 from start)
				// Move up (linesRendered - 1) to get back to start position
				if (linesRendered > 1) {
					process.stdout.write(`\x1b[${linesRendered - 1}A`);
				}
				process.stdout.write('\r');
			}
			return exitCode;
		}

		// Determine how many lines to show in final output
		const finalLinesToShow = exitCode === 0 ? maxLinesOutput : maxLinesOnFailure;
		const finalOutputLines = allOutputLines.slice(-finalLinesToShow);

		// Clear all rendered lines completely (only our lines, not previous output)
		if (linesRendered > 0) {
			// Move up to the command line (first line of our output)
			process.stdout.write(`\x1b[${linesRendered}A`);
			// Clear the lines we rendered during streaming
			for (let i = 0; i < linesRendered; i++) {
				process.stdout.write('\r\x1b[2K'); // Clear entire line
				if (i < linesRendered - 1) {
					process.stdout.write('\x1b[B'); // Move down one line
				}
			}
			// After loop, cursor is at last cleared line (linesRendered - 1 from start)
			// Move up (linesRendered - 1) to get back to start position
			if (linesRendered > 1) {
				process.stdout.write(`\x1b[${linesRendered - 1}A`);
			}
			process.stdout.write('\r');
		}

		// Determine icon based on exit code
		const icon = exitCode === 0 ? ICONS.success : ICONS.error;
		const statusColor = exitCode === 0 ? green : red;

		// Show final status: icon + command
		process.stdout.write(
			`\r\x1b[K${statusColor}${icon}${reset} ${cmdColor}${displayCmd}${reset}\n`
		);

		// Show final output lines (clearing each line first in case we're using more lines than before)
		for (const line of finalOutputLines) {
			const displayLine =
				truncate && getDisplayWidth(line) > maxLineWidth
					? truncateToWidth(line, maxLineWidth)
					: line;
			process.stdout.write(`\x1b[2K${mutedColor}${displayLine}${reset}\n`);
		}

		// If we're showing more lines than we had before, the extra lines may contain old content
		// We've already written over them, so they're clean now

		return exitCode;
	} catch (err) {
		// Move cursor up to clear our UI
		if (linesRendered > 0) {
			process.stdout.write(`\x1b[${linesRendered}A`);
			// Clear all our lines
			for (let i = 0; i < linesRendered; i++) {
				process.stdout.write('\r\x1b[K\n');
			}
			process.stdout.write(`\x1b[${linesRendered}A`);
		}

		// Show error status
		process.stdout.write(`\r\x1b[K${red}$${reset} ${cmdColor}${displayCmd}${reset}\n`);

		// Log the error
		const errorMsg = err instanceof Error ? err.message : String(err);
		console.error(`${red}${ICONS.error} Failed to spawn command: ${errorMsg}${reset}`);
		if (cwd) {
			console.error(`${mutedColor}  cwd: ${cwd}${reset}`);
		}
		console.error(`${mutedColor}  cmd: ${cmd.join(' ')}${reset}`);

		return 1; // Return non-zero exit code
	} finally {
		// Always restore cursor visibility
		process.stdout.write('\x1B[?25h');
	}
}

/**
 * Prompt user for text input
 * Returns the input string
 */
export async function prompt(message: string): Promise<string> {
	process.stdout.write(message);

	// Check if we're in a TTY environment
	if (!process.stdin.isTTY) {
		console.log('');
		return '';
	}

	// Use readline for full line input
	const rl = readline.createInterface({
		input: process.stdin,
		output: process.stdout,
	});

	return new Promise((resolve) => {
		rl.question('', (answer: string) => {
			rl.close();
			resolve(answer);
		});
	});
}

/**
 * Select an organization from a list.
 *
 * @param orgs - List of organizations to choose from
 * @param initial - Preferred org ID to pre-select (from saved preferences)
 * @param autoSelect - If true, auto-select preferred org without prompting (for --confirm or non-interactive)
 * @returns The selected organization ID
 */
export async function selectOrganization(
	orgs: OrganizationList,
	initial?: string,
	autoSelect?: boolean
): Promise<string> {
	if (orgs.length === 0) {
		fatal(
			'You do not belong to any organizations.\n' +
				'Please contact support or create an organization at https://agentuity.com'
		);
	}

	// 1. Environment variable always takes precedence
	if (process.env.AGENTUITY_CLOUD_ORG_ID) {
		const org = orgs.find((o) => o.id === process.env.AGENTUITY_CLOUD_ORG_ID);
		if (org) {
			return org.id;
		}
	}

	// 2. Auto-select if only one org (regardless of TTY mode or autoSelect)
	if (orgs.length === 1 && orgs[0]) {
		return orgs[0].id;
	}

	// 3. Auto-select mode (--confirm flag or explicit autoSelect)
	// Use preferred org if set, otherwise fall back to first org
	if (autoSelect) {
		if (initial) {
			const initialOrg = orgs.find((o) => o.id === initial);
			if (initialOrg) {
				return initialOrg.id;
			}
		}
		// Fall back to first org with warning
		const firstOrg = orgs[0];
		if (firstOrg) {
			warning(
				`Multiple organizations found. Auto-selecting first org: ${firstOrg.name}. ` +
					`Set AGENTUITY_CLOUD_ORG_ID, use --org-id, or run 'agentuity auth org select' to set a default.`
			);
			return firstOrg.id;
		}
	}

	// 4. Check for non-interactive environment (check both stdin and stdout)
	const isNonInteractive = !process.stdin.isTTY || !process.stdout.isTTY;
	if (isNonInteractive) {
		// In non-interactive mode, use preferred org if set
		if (initial) {
			const initialOrg = orgs.find((o) => o.id === initial);
			if (initialOrg) {
				return initialOrg.id;
			}
		}
		// Fall back to first org with warning
		const firstOrg = orgs[0];
		if (firstOrg) {
			warning(
				`Multiple organizations found. Auto-selecting first org: ${firstOrg.name}. ` +
					`Set AGENTUITY_CLOUD_ORG_ID, use --org-id, or run 'agentuity auth org select' to set a default.`
			);
			return firstOrg.id;
		}
	}

	// 5. Interactive mode - show selector with preferred org pre-selected
	const initialIndex = initial ? orgs.findIndex((o) => o.id === initial) : 0;
	const response = await enquirer.prompt<{ action: string }>({
		type: 'select',
		name: 'action',
		message: 'Select an organization',
		initial: initialIndex >= 0 ? initialIndex : 0,
		choices: orgs.map((o) => ({ message: o.name, name: o.id })),
	});

	return response.action;
}

/**
 * show a project list picker
 *
 * @param apiClient
 * @param showDeployment
 * @returns
 */
export async function showProjectList(
	apiClient: APIClientType,
	showDeploymentId = false
): Promise<string> {
	const projects = await spinner({
		message: 'Fetching projects',
		clearOnSuccess: true,
		callback: () => {
			return projectList(apiClient, showDeploymentId);
		},
	});

	if (projects.length === 0) {
		return '';
	}

	// TODO: might want to sort by the last org_id we used
	if (projects) {
		projects.sort((a, b) => {
			return a.name.localeCompare(b.name);
		});
	}

	const response = await enquirer.prompt<{ id: string }>({
		type: 'select',
		name: 'id',
		message: 'Select a project:',
		choices: projects.map((p) => ({
			name: p.id,
			message: `${p.name.padEnd(25, ' ')} ${muted(p.id)} ${showDeploymentId ? muted(p.latestDeploymentId ?? 'no deployment') : ''}`,
		})),
	});

	return response.id;
}

/**
 * Show a profile list picker
 *
 * @param profiles List of profiles to choose from
 * @param message Prompt message
 * @returns The name of the selected profile
 */
export async function showProfileList(
	profiles: Profile[],
	message = 'Select a profile:'
): Promise<string> {
	if (profiles.length === 0) {
		warning('No profiles found');
		process.exit(0);
	}

	// If only one profile, just return it? No, let them confirm/see it if they asked to pick?
	// But for "use" it implies switching. If only one, you are already on it or it's the only choice.
	// But for delete, you might want to delete the only one.
	// So always show list.

	// Find currently selected profile for initial selection
	const selectedProfile = profiles.find((p) => p.selected);
	const initial = selectedProfile ? selectedProfile.name : undefined;

	// If non-interactive, return initial or first
	if (!process.stdin.isTTY) {
		if (initial) return initial;
		const firstProfile = profiles[0];
		if (profiles.length === 1 && firstProfile) {
			return firstProfile.name;
		}
		fatal(
			'Profile selection required but cannot prompt in non-interactive environment. ' +
				'Pass a profile name explicitly when running non-interactively.'
		);
	}

	const response = await enquirer.prompt<{ name: string }>({
		type: 'select',
		name: 'name',
		message: message,
		initial: initial,
		choices: profiles.map((p) => ({
			name: p.name,
			message: p.selected ? `${p.name.padEnd(15, ' ')} ${muted('(current)')}` : p.name,
		})),
	});

	return response.name;
}

export function json(value: unknown) {
	const stringValue = typeof value === 'string' ? value : JSON.stringify(value, null, 2);

	if (shouldUseColors() && process.stdout.isTTY) {
		try {
			console.log(colorize(stringValue));
			return;
		} catch {
			/* */
		}
	}
	console.log(stringValue);
}

export function plural(count: number, singular: string, plural: string): string {
	switch (count) {
		case 0:
			return plural;
		case 1:
			return singular;
		default:
			return plural;
	}
}

/**
 * Table column definition
 */
export interface TableColumn {
	/** Column name */
	name: string;
	/** Column alignment */
	alignment?: 'left' | 'right' | 'center';
}

/**
 * Table options
 */
export interface TableOptions {
	/**
	 * If true, returns the table as a string instead of printing to stdout
	 */
	render?: boolean;
	/**
	 * Force a specific layout mode
	 * - 'horizontal': Traditional table with columns side by side
	 * - 'vertical': Stacked format with "Column: value" on separate lines
	 * - 'auto': Automatically choose based on terminal width (default)
	 */
	layout?: 'horizontal' | 'vertical' | 'auto';

	/**
	 * the padding before any label
	 */
	padStart?: string;
}

/**
 * Calculate the minimum width needed to display a horizontal table
 * Accounts for column padding, borders, and content width
 */
function calculateTableWidth<T extends Record<string, unknown>>(
	data: T[],
	columnNames: string[],
	padStart = ''
): number {
	const columnWidths = columnNames.map((colName) => {
		let maxWidth = getDisplayWidth(padStart + colName);
		for (const row of data) {
			const value = row[colName];
			const valueStr = value !== undefined && value !== null ? String(value) : '';
			const valueWidth = getDisplayWidth(valueStr);
			if (valueWidth > maxWidth) {
				maxWidth = valueWidth;
			}
		}
		return maxWidth;
	});

	// Add padding (1 space each side) and border characters per column
	// cli-table3 uses: │ col1 │ col2 │ = 3 chars per column + 1 for final border
	const paddingPerColumn = 3;
	const totalWidth = columnWidths.reduce((sum, w) => sum + w + paddingPerColumn, 0) + 1;

	return totalWidth;
}

/**
 * Render table in vertical (stacked) format for narrow terminals
 */
function renderVerticalTable<T extends Record<string, unknown>>(
	data: T[],
	columnNames: string[],
	padStart = ''
): string {
	const lines: string[] = [];
	const mutedColor = getColor('muted');
	const reset = getColor('reset');

	// Calculate max column name width for alignment
	const maxLabelWidth = Math.max(
		...columnNames.map((name) => 1 + getDisplayWidth(padStart + name))
	);

	for (let i = 0; i < data.length; i++) {
		const row = data[i];
		if (!row) continue;

		for (const colName of columnNames) {
			const value = row[colName];
			const valueStr = value !== undefined && value !== null ? String(value) : '';
			const paddedLabel = `${padStart}${colName}:`.padEnd(maxLabelWidth);
			lines.push(`${mutedColor}${paddedLabel}${reset}  ${valueStr}`);
		}

		// Add empty line between rows (but not after last row)
		if (i < data.length - 1) {
			lines.push('');
		}
	}

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

/**
 * Display data in a formatted table using cli-table3
 *
 * Supports two modes:
 * 1. Simple mode: Pass data array and optional column names
 * 2. Advanced mode: Pass column configurations with custom names and alignment
 *
 * Automatically switches between horizontal (wide) and vertical (narrow) layouts
 * based on terminal width. Use the `layout` option to force a specific mode.
 *
 * @param data - Array of data objects to display
 * @param columns - Column names or column configurations
 * @param options - Additional options
 * @returns If render=true, returns the table as a string, otherwise prints to stdout
 */
export function table<T extends Record<string, unknown>>(
	data: T[],
	columns?: (keyof T)[] | TableColumn[],
	options?: TableOptions
): string | void {
	if (!data || data.length === 0) {
		return options?.render ? '' : undefined;
	}

	// Determine if we're using advanced column config or simple column names
	const isAdvancedMode = columns && columns.length > 0 && typeof columns[0] === 'object';

	let columnNames: string[];
	let colAligns: Array<'left' | 'right' | 'center'>;

	if (isAdvancedMode) {
		// Advanced mode: use provided column configurations
		const columnConfigs = columns as TableColumn[];
		columnNames = columnConfigs.map((col) => col.name);
		colAligns = columnConfigs.map((col) => col.alignment || 'left');
	} else {
		// Simple mode: determine column names from data or columns parameter
		const firstRow = data[0];
		columnNames = columns
			? (columns as (keyof T)[]).map((c) => String(c))
			: data.length > 0 && firstRow
				? Object.keys(firstRow)
				: [];
		colAligns = columnNames.map(() => 'left' as const);
	}

	// Determine layout mode
	const layout = options?.layout ?? 'auto';
	const termWidth = getTerminalWidth(80);
	const tableWidth = calculateTableWidth(data, columnNames, options?.padStart);
	const useVertical = layout === 'vertical' || (layout === 'auto' && tableWidth > termWidth);

	let output: string;

	if (useVertical) {
		output = renderVerticalTable(data, columnNames, options?.padStart);
	} else {
		// eslint-disable-next-line @typescript-eslint/no-require-imports
		const Table = require('cli-table3') as new (options?: {
			head?: string[];
			colAligns?: Array<'left' | 'right' | 'center'>;
			wordWrap?: boolean;
			style?: {
				head?: string[];
				border?: string[];
			};
			colors?: boolean;
		}) => {
			push(row: unknown[]): void;
			toString(): string;
		};

		const headings = columnNames.map((name) => heading(name));

		const t = new Table({
			head: headings,
			colAligns,
			wordWrap: true,
			style: {
				head: [], // Disable cli-table3's default red styling - we apply our own via heading()
				border: [], // Disable default border styling too
			},
			colors: false, // Completely disable cli-table3's color system to preserve our ANSI codes
		});

		// Add rows to table
		for (const row of data) {
			const rowData: unknown[] = [];
			for (const colName of columnNames) {
				const value = row[colName];
				rowData.push(value !== undefined && value !== null ? String(value) : '');
			}
			t.push(rowData);
		}

		output = t.toString();
	}

	if (options?.render) {
		return output;
	} else {
		console.log(output);
	}
}

export function formatBytes(bytes: number): string {
	if (bytes === 0) return '0 B';
	if (bytes < 1024) return `${bytes} B`;
	if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
	if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
	if (bytes < 1024 * 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
	return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
}

export function clearLastLines(n: number, s?: (v: string) => void) {
	const x = s ?? ((v: string) => process.stdout.write(v));
	for (let i = 0; i < n; i++) {
		x('\x1b[2K'); // clear line
		x('\x1b[0G'); // cursor to col 0
		if (i < n - 1) {
			x('\x1b[1A'); // move up
		}
	}
}
