/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import type React from 'react';
import {
  useCallback,
  useMemo,
  useRef,
  useEffect,
  useReducer,
  useContext,
  useState,
} from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import type { Question } from '@google/gemini-cli-core';
import { BaseSelectionList } from './shared/BaseSelectionList.js';
import type { SelectionListItem } from '../hooks/useSelectionList.js';
import { TabHeader, type Tab } from './shared/TabHeader.js';
import { useKeypress, type Key } from '../hooks/useKeypress.js';
import { keyMatchers, Command } from '../keyMatchers.js';
import { checkExhaustive } from '@google/gemini-cli-core';
import { TextInput } from './shared/TextInput.js';
import { useTextBuffer } from './shared/text-buffer.js';
import { getCachedStringWidth } from '../utils/textUtils.js';
import { useTabbedNavigation } from '../hooks/useTabbedNavigation.js';
import { DialogFooter } from './shared/DialogFooter.js';
import { MarkdownDisplay } from '../utils/MarkdownDisplay.js';
import { RenderInline } from '../utils/InlineMarkdownRenderer.js';
import { MaxSizedBox } from './shared/MaxSizedBox.js';
import { UIStateContext } from '../contexts/UIStateContext.js';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';

/** Padding for dialog content to prevent text from touching edges. */
const DIALOG_PADDING = 4;

/**
 * Checks if text is a single line without markdown identifiers.
 */
function isPlainSingleLine(text: string): boolean {
  // Must be a single line (no newlines)
  if (text.includes('\n') || text.includes('\r')) {
    return false;
  }

  // Check for common markdown identifiers
  const markdownPatterns = [
    /^#{1,6}\s/, // Headers
    /^[`~]{3,}/, // Code fences
    /^[-*+]\s/, // Unordered lists
    /^\d+\.\s/, // Ordered lists
    /^[-*_]{3,}$/, // Horizontal rules
    /\|/, // Tables
    /\*\*|__/, // Bold
    /(?<!\*)\*(?!\*)/, // Italic (single asterisk not part of bold)
    /(?<!_)_(?!_)/, // Italic (single underscore not part of bold)
    /`[^`]+`/, // Inline code
    /\[.*?\]\(.*?\)/, // Links
    /!\[/, // Images
  ];

  for (const pattern of markdownPatterns) {
    if (pattern.test(text)) {
      return false;
    }
  }

  return true;
}

/**
 * Auto-bolds plain single-line text by wrapping in **.
 * Returns the text unchanged if it already contains markdown.
 */
function autoBoldIfPlain(text: string): string {
  if (isPlainSingleLine(text)) {
    return `**${text}**`;
  }
  return text;
}

interface AskUserDialogState {
  answers: { [key: string]: string };
  isEditingCustomOption: boolean;
  submitted: boolean;
}

type AskUserDialogAction =
  | {
      type: 'SET_ANSWER';
      payload: {
        index: number;
        answer: string;
        submit?: boolean;
      };
    }
  | { type: 'SET_EDITING_CUSTOM'; payload: { isEditing: boolean } }
  | { type: 'SUBMIT' };

const initialState: AskUserDialogState = {
  answers: {},
  isEditingCustomOption: false,
  submitted: false,
};

function askUserDialogReducerLogic(
  state: AskUserDialogState,
  action: AskUserDialogAction,
): AskUserDialogState {
  if (state.submitted) {
    return state;
  }

  switch (action.type) {
    case 'SET_ANSWER': {
      const { index, answer, submit } = action.payload;
      const hasAnswer =
        answer !== undefined && answer !== null && answer.trim() !== '';
      const newAnswers = { ...state.answers };

      if (hasAnswer) {
        newAnswers[index] = answer;
      } else {
        delete newAnswers[index];
      }

      return {
        ...state,
        answers: newAnswers,
        submitted: submit ? true : state.submitted,
      };
    }
    case 'SET_EDITING_CUSTOM': {
      if (state.isEditingCustomOption === action.payload.isEditing) {
        return state;
      }
      return {
        ...state,
        isEditingCustomOption: action.payload.isEditing,
      };
    }
    case 'SUBMIT': {
      return {
        ...state,
        submitted: true,
      };
    }
    default:
      checkExhaustive(action);
      return state;
  }
}

/**
 * Props for the AskUserDialog component.
 */
interface AskUserDialogProps {
  /**
   * The list of questions to ask the user.
   */
  questions: Question[];
  /**
   * Callback fired when the user submits their answers.
   * Returns a map of question index to answer string.
   */
  onSubmit: (answers: { [questionIndex: string]: string }) => void;
  /**
   * Callback fired when the user cancels the dialog (e.g. via Escape).
   */
  onCancel: () => void;
  /**
   * Optional callback to notify parent when text input is active.
   * Useful for managing global keypress handlers.
   */
  onActiveTextInputChange?: (active: boolean) => void;
  /**
   * Width of the dialog.
   */
  width: number;
  /**
   * Height constraint for scrollable content.
   */
  availableHeight?: number;
  /**
   * Custom keyboard shortcut hints (e.g., ["Ctrl+P to edit"])
   */
  extraParts?: string[];
}

interface ReviewViewProps {
  questions: Question[];
  answers: { [key: string]: string };
  onSubmit: () => void;
  progressHeader?: React.ReactNode;
  extraParts?: string[];
}

const ReviewView: React.FC<ReviewViewProps> = ({
  questions,
  answers,
  onSubmit,
  progressHeader,
  extraParts,
}) => {
  const unansweredCount = questions.length - Object.keys(answers).length;
  const hasUnanswered = unansweredCount > 0;

  // Handle Enter to submit
  useKeypress(
    (key: Key) => {
      if (keyMatchers[Command.RETURN](key)) {
        onSubmit();
        return true;
      }
      return false;
    },
    { isActive: true },
  );

  return (
    <Box flexDirection="column">
      {progressHeader}
      <Box marginBottom={1}>
        <Text bold color={theme.text.primary}>
          Review your answers:
        </Text>
      </Box>

      {hasUnanswered && (
        <Box marginBottom={1}>
          <Text color={theme.status.warning}>
            ⚠ You have {unansweredCount} unanswered question
            {unansweredCount > 1 ? 's' : ''}
          </Text>
        </Box>
      )}

      <Box flexDirection="column">
        {questions.map((q, i) => (
          <Box key={i} marginBottom={0}>
            <Text color={theme.text.secondary}>{q.header}</Text>
            <Text color={theme.text.secondary}> → </Text>
            <Text
              color={answers[i] ? theme.text.primary : theme.status.warning}
            >
              {answers[i] || '(not answered)'}
            </Text>
          </Box>
        ))}
      </Box>
      <DialogFooter
        primaryAction="Enter to submit"
        navigationActions="Tab/Shift+Tab to edit answers"
        extraParts={extraParts}
      />
    </Box>
  );
};

// ============== Text Question View ==============

interface TextQuestionViewProps {
  question: Question;
  onAnswer: (answer: string) => void;
  onSelectionChange?: (answer: string) => void;
  onEditingCustomOption?: (editing: boolean) => void;
  availableWidth: number;
  availableHeight?: number;
  initialAnswer?: string;
  progressHeader?: React.ReactNode;
  keyboardHints?: React.ReactNode;
}

const TextQuestionView: React.FC<TextQuestionViewProps> = ({
  question,
  onAnswer,
  onSelectionChange,
  onEditingCustomOption,
  availableWidth,
  availableHeight,
  initialAnswer,
  progressHeader,
  keyboardHints,
}) => {
  const isAlternateBuffer = useAlternateBuffer();
  const prefix = '> ';
  const horizontalPadding = 1; // 1 for cursor
  const bufferWidth =
    availableWidth - getCachedStringWidth(prefix) - horizontalPadding;

  const buffer = useTextBuffer({
    initialText: initialAnswer,
    viewport: { width: Math.max(1, bufferWidth), height: 3 },
    singleLine: false,
  });

  const { text: textValue } = buffer;

  // Sync state change with parent - only when it actually changes
  const lastTextValueRef = useRef(textValue);
  useEffect(() => {
    if (textValue !== lastTextValueRef.current) {
      onSelectionChange?.(textValue);
      lastTextValueRef.current = textValue;
    }
  }, [textValue, onSelectionChange]);

  // Handle Ctrl+C to clear all text
  const handleExtraKeys = useCallback(
    (key: Key) => {
      if (keyMatchers[Command.QUIT](key)) {
        if (textValue === '') {
          return false;
        }
        buffer.setText('');
        return true;
      }
      return false;
    },
    [buffer, textValue],
  );

  useKeypress(handleExtraKeys, { isActive: true, priority: true });

  const handleSubmit = useCallback(
    (val: string) => {
      onAnswer(val.trim());
    },
    [onAnswer],
  );

  // Notify parent that we're in text input mode (for Ctrl+C handling)
  useEffect(() => {
    onEditingCustomOption?.(true);
    return () => {
      onEditingCustomOption?.(false);
    };
  }, [onEditingCustomOption]);

  const placeholder = question.placeholder || 'Enter your response';

  const HEADER_HEIGHT = progressHeader ? 2 : 0;
  const INPUT_HEIGHT = 2; // TextInput + margin
  const FOOTER_HEIGHT = 2; // DialogFooter + margin
  const overhead = HEADER_HEIGHT + INPUT_HEIGHT + FOOTER_HEIGHT;
  const questionHeight =
    availableHeight && !isAlternateBuffer
      ? Math.max(1, availableHeight - overhead)
      : undefined;

  return (
    <Box flexDirection="column">
      {progressHeader}
      <Box marginBottom={1}>
        <MaxSizedBox
          maxHeight={questionHeight}
          maxWidth={availableWidth}
          overflowDirection="bottom"
        >
          <MarkdownDisplay
            text={autoBoldIfPlain(question.question)}
            terminalWidth={availableWidth - DIALOG_PADDING}
            isPending={false}
          />
        </MaxSizedBox>
      </Box>

      <Box flexDirection="row" marginBottom={1}>
        <Text color={theme.status.success}>{'> '}</Text>
        <TextInput
          buffer={buffer}
          placeholder={placeholder}
          onSubmit={handleSubmit}
        />
      </Box>

      {keyboardHints}
    </Box>
  );
};

// ============== Choice Question View ==============

interface OptionItem {
  key: string;
  label: string;
  description: string;
  type: 'option' | 'other' | 'done';
  index: number;
}

interface ChoiceQuestionState {
  selectedIndices: Set<number>;
  isCustomOptionSelected: boolean;
  isCustomOptionFocused: boolean;
}

type ChoiceQuestionAction =
  | { type: 'TOGGLE_INDEX'; payload: { index: number; multiSelect: boolean } }
  | {
      type: 'SET_CUSTOM_SELECTED';
      payload: { selected: boolean; multiSelect: boolean };
    }
  | { type: 'TOGGLE_CUSTOM_SELECTED'; payload: { multiSelect: boolean } }
  | { type: 'SET_CUSTOM_FOCUSED'; payload: { focused: boolean } };

function choiceQuestionReducer(
  state: ChoiceQuestionState,
  action: ChoiceQuestionAction,
): ChoiceQuestionState {
  switch (action.type) {
    case 'TOGGLE_INDEX': {
      const { index, multiSelect } = action.payload;
      const newIndices = new Set(multiSelect ? state.selectedIndices : []);
      if (newIndices.has(index)) {
        newIndices.delete(index);
      } else {
        newIndices.add(index);
      }
      return {
        ...state,
        selectedIndices: newIndices,
        // In single select, selecting an option deselects custom
        isCustomOptionSelected: multiSelect
          ? state.isCustomOptionSelected
          : false,
      };
    }
    case 'SET_CUSTOM_SELECTED': {
      const { selected, multiSelect } = action.payload;
      return {
        ...state,
        isCustomOptionSelected: selected,
        // In single-select, selecting custom deselects others
        selectedIndices: multiSelect ? state.selectedIndices : new Set(),
      };
    }
    case 'TOGGLE_CUSTOM_SELECTED': {
      const { multiSelect } = action.payload;
      if (!multiSelect) return state;

      return {
        ...state,
        isCustomOptionSelected: !state.isCustomOptionSelected,
      };
    }
    case 'SET_CUSTOM_FOCUSED': {
      return {
        ...state,
        isCustomOptionFocused: action.payload.focused,
      };
    }
    default:
      checkExhaustive(action);
      return state;
  }
}

interface ChoiceQuestionViewProps {
  question: Question;
  onAnswer: (answer: string) => void;
  onSelectionChange?: (answer: string) => void;
  onEditingCustomOption?: (editing: boolean) => void;
  availableWidth: number;
  availableHeight?: number;
  initialAnswer?: string;
  progressHeader?: React.ReactNode;
  keyboardHints?: React.ReactNode;
}

const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
  question,
  onAnswer,
  onSelectionChange,
  onEditingCustomOption,
  availableWidth,
  availableHeight,
  initialAnswer,
  progressHeader,
  keyboardHints,
}) => {
  const isAlternateBuffer = useAlternateBuffer();
  const numOptions =
    (question.options?.length ?? 0) + (question.type !== 'yesno' ? 1 : 0);
  const numLen = String(numOptions).length;
  const radioWidth = 2; // "● "
  const numberWidth = numLen + 2; // e.g., "1. "
  const checkboxWidth = question.multiSelect ? 4 : 1; // "[x] " or " "
  const checkmarkWidth = question.multiSelect ? 0 : 2; // "" or " ✓"
  const cursorPadding = 1; // Extra character for cursor at end of line

  const horizontalPadding =
    radioWidth + numberWidth + checkboxWidth + checkmarkWidth + cursorPadding;

  const bufferWidth = availableWidth - horizontalPadding;

  const questionOptions = useMemo(
    () => question.options ?? [],
    [question.options],
  );

  // Initialize state from initialAnswer if returning to a previously answered question
  const initialReducerState = useMemo((): ChoiceQuestionState => {
    if (!initialAnswer) {
      return {
        selectedIndices: new Set<number>(),
        isCustomOptionSelected: false,
        isCustomOptionFocused: false,
      };
    }

    // Check if initialAnswer matches any option labels
    const selectedIndices = new Set<number>();
    let isCustomOptionSelected = false;

    if (question.multiSelect) {
      const answers = initialAnswer.split(', ');
      answers.forEach((answer) => {
        const index = questionOptions.findIndex((opt) => opt.label === answer);
        if (index !== -1) {
          selectedIndices.add(index);
        } else {
          isCustomOptionSelected = true;
        }
      });
    } else {
      const index = questionOptions.findIndex(
        (opt) => opt.label === initialAnswer,
      );
      if (index !== -1) {
        selectedIndices.add(index);
      } else {
        isCustomOptionSelected = true;
      }
    }

    return {
      selectedIndices,
      isCustomOptionSelected,
      isCustomOptionFocused: false,
    };
  }, [initialAnswer, questionOptions, question.multiSelect]);

  const [state, dispatch] = useReducer(
    choiceQuestionReducer,
    initialReducerState,
  );
  const { selectedIndices, isCustomOptionSelected, isCustomOptionFocused } =
    state;

  const initialCustomText = useMemo(() => {
    if (!initialAnswer) return '';
    if (question.multiSelect) {
      const answers = initialAnswer.split(', ');
      const custom = answers.find(
        (a) => !questionOptions.some((opt) => opt.label === a),
      );
      return custom || '';
    } else {
      const isPredefined = questionOptions.some(
        (opt) => opt.label === initialAnswer,
      );
      return isPredefined ? '' : initialAnswer;
    }
  }, [initialAnswer, questionOptions, question.multiSelect]);

  const customBuffer = useTextBuffer({
    initialText: initialCustomText,
    viewport: { width: Math.max(1, bufferWidth), height: 3 },
    singleLine: false,
  });

  const customOptionText = customBuffer.text;

  // Helper to build answer string from selections
  const buildAnswerString = useCallback(
    (
      indices: Set<number>,
      includeCustomOption: boolean,
      customOption: string,
    ) => {
      const answers: string[] = [];
      questionOptions.forEach((opt, i) => {
        if (indices.has(i)) {
          answers.push(opt.label);
        }
      });
      if (includeCustomOption && customOption.trim()) {
        answers.push(customOption.trim());
      }
      return answers.join(', ');
    },
    [questionOptions],
  );

  // Synchronize selection changes with parent - only when it actually changes
  const lastBuiltAnswerRef = useRef('');
  useEffect(() => {
    const newAnswer = buildAnswerString(
      selectedIndices,
      isCustomOptionSelected,
      customOptionText,
    );
    if (newAnswer !== lastBuiltAnswerRef.current) {
      onSelectionChange?.(newAnswer);
      lastBuiltAnswerRef.current = newAnswer;
    }
  }, [
    selectedIndices,
    isCustomOptionSelected,
    customOptionText,
    buildAnswerString,
    onSelectionChange,
  ]);

  // Handle "Type-to-Jump" and Ctrl+C for custom buffer
  const handleExtraKeys = useCallback(
    (key: Key) => {
      // If focusing custom option, handle Ctrl+C
      if (isCustomOptionFocused && keyMatchers[Command.QUIT](key)) {
        if (customOptionText === '') {
          return false;
        }
        customBuffer.setText('');
        return true;
      }

      // Don't jump if a navigation or selection key is pressed
      if (
        keyMatchers[Command.DIALOG_NAVIGATION_UP](key) ||
        keyMatchers[Command.DIALOG_NAVIGATION_DOWN](key) ||
        keyMatchers[Command.DIALOG_NEXT](key) ||
        keyMatchers[Command.DIALOG_PREV](key) ||
        keyMatchers[Command.MOVE_LEFT](key) ||
        keyMatchers[Command.MOVE_RIGHT](key) ||
        keyMatchers[Command.RETURN](key) ||
        keyMatchers[Command.ESCAPE](key) ||
        keyMatchers[Command.QUIT](key)
      ) {
        return false;
      }

      // Check if it's a numeric quick selection key (if numbers are shown)
      const isNumeric = /^[0-9]$/.test(key.sequence);
      if (isNumeric) {
        return false;
      }

      // Type-to-jump: if printable characters are typed and not focused, jump to custom
      const isPrintable =
        key.sequence &&
        !key.ctrl &&
        !key.alt &&
        (key.sequence.length > 1 || key.sequence.charCodeAt(0) >= 32);

      if (isPrintable && !isCustomOptionFocused) {
        dispatch({ type: 'SET_CUSTOM_FOCUSED', payload: { focused: true } });
        onEditingCustomOption?.(true);
        // For IME or multi-char sequences, we want to capture the whole thing.
        // If it's a single char, we start the buffer with it.
        customBuffer.setText(key.sequence);
        return true;
      }
      return false;
    },
    [
      isCustomOptionFocused,
      customBuffer,
      onEditingCustomOption,
      customOptionText,
    ],
  );

  useKeypress(handleExtraKeys, { isActive: true, priority: true });

  const selectionItems = useMemo((): Array<SelectionListItem<OptionItem>> => {
    const list: Array<SelectionListItem<OptionItem>> = questionOptions.map(
      (opt, i) => {
        const item: OptionItem = {
          key: `opt-${i}`,
          label: opt.label,
          description: opt.description,
          type: 'option',
          index: i,
        };
        return { key: item.key, value: item };
      },
    );

    // Only add custom option for choice type, not yesno
    if (question.type !== 'yesno') {
      const otherItem: OptionItem = {
        key: 'other',
        label: customOptionText || '',
        description: '',
        type: 'other',
        index: list.length,
      };
      list.push({ key: 'other', value: otherItem });
    }

    if (question.multiSelect) {
      const doneItem: OptionItem = {
        key: 'done',
        label: 'Done',
        description: 'Finish selection',
        type: 'done',
        index: list.length,
      };
      list.push({ key: doneItem.key, value: doneItem, hideNumber: true });
    }

    return list;
  }, [questionOptions, question.multiSelect, question.type, customOptionText]);

  const handleHighlight = useCallback(
    (itemValue: OptionItem) => {
      const nowFocusingCustomOption = itemValue.type === 'other';
      dispatch({
        type: 'SET_CUSTOM_FOCUSED',
        payload: { focused: nowFocusingCustomOption },
      });
      // Notify parent when we start/stop focusing custom option (so navigation can resume)
      onEditingCustomOption?.(nowFocusingCustomOption);
    },
    [onEditingCustomOption],
  );

  const handleSelect = useCallback(
    (itemValue: OptionItem) => {
      if (question.multiSelect) {
        if (itemValue.type === 'option') {
          dispatch({
            type: 'TOGGLE_INDEX',
            payload: { index: itemValue.index, multiSelect: true },
          });
        } else if (itemValue.type === 'other') {
          dispatch({
            type: 'TOGGLE_CUSTOM_SELECTED',
            payload: { multiSelect: true },
          });
        } else if (itemValue.type === 'done') {
          // Done just triggers navigation, selections already saved via useEffect
          onAnswer(
            buildAnswerString(
              selectedIndices,
              isCustomOptionSelected,
              customOptionText,
            ),
          );
        }
      } else {
        if (itemValue.type === 'option') {
          onAnswer(itemValue.label);
        } else if (itemValue.type === 'other') {
          // In single select, selecting other submits it if it has text
          if (customOptionText.trim()) {
            onAnswer(customOptionText.trim());
          }
        }
      }
    },
    [
      question.multiSelect,
      selectedIndices,
      isCustomOptionSelected,
      customOptionText,
      onAnswer,
      buildAnswerString,
    ],
  );

  // Auto-select custom option when typing in it
  useEffect(() => {
    if (customOptionText.trim() && !isCustomOptionSelected) {
      dispatch({
        type: 'SET_CUSTOM_SELECTED',
        payload: { selected: true, multiSelect: !!question.multiSelect },
      });
    }
  }, [customOptionText, isCustomOptionSelected, question.multiSelect]);

  const [actualQuestionHeight, setActualQuestionHeight] = useState(0);

  const HEADER_HEIGHT = progressHeader ? 2 : 0;
  const TITLE_MARGIN = 1;
  const FOOTER_HEIGHT = 2; // DialogFooter + margin
  const overhead = HEADER_HEIGHT + TITLE_MARGIN + FOOTER_HEIGHT;

  const listHeight = availableHeight
    ? Math.max(1, availableHeight - overhead)
    : undefined;

  const idealOptionsHeight = selectionItems.reduce(
    (acc, item) => acc + (item.value.description ? 2 : 1),
    0,
  );

  // Use remaining height for the question, reserving space for the options list
  const maxQuestionHeight =
    question.unconstrainedHeight && listHeight
      ? Math.max(5, listHeight - Math.min(idealOptionsHeight, 10))
      : 15;

  const questionHeightLimit =
    listHeight && !isAlternateBuffer
      ? Math.min(maxQuestionHeight, listHeight)
      : undefined;

  let maxItemsToShow = selectionItems.length;
  if (listHeight && actualQuestionHeight) {
    const remainingHeight = Math.max(0, listHeight - actualQuestionHeight);
    let linesUsed = 0;
    let itemsThatFit = 0;
    for (const item of selectionItems) {
      const itemLines = item.value.description ? 2 : 1;
      if (linesUsed + itemLines <= remainingHeight) {
        linesUsed += itemLines;
        itemsThatFit++;
      } else {
        break;
      }
    }
    maxItemsToShow = Math.max(1, itemsThatFit);
  }

  return (
    <Box flexDirection="column">
      {progressHeader}
      <Box marginBottom={TITLE_MARGIN}>
        <MaxSizedBox
          maxHeight={questionHeightLimit}
          maxWidth={availableWidth}
          overflowDirection="bottom"
          onHeightChange={setActualQuestionHeight}
        >
          <Box flexDirection="column">
            <MarkdownDisplay
              text={autoBoldIfPlain(question.question)}
              terminalWidth={availableWidth - DIALOG_PADDING}
              isPending={false}
            />
            {question.multiSelect && (
              <Text color={theme.text.secondary} italic>
                (Select all that apply)
              </Text>
            )}
          </Box>
        </MaxSizedBox>
      </Box>

      <BaseSelectionList<OptionItem>
        items={selectionItems}
        onSelect={handleSelect}
        onHighlight={handleHighlight}
        focusKey={isCustomOptionFocused ? 'other' : undefined}
        maxItemsToShow={maxItemsToShow}
        showScrollArrows={true}
        renderItem={(item, context) => {
          const optionItem = item.value;
          const isChecked =
            selectedIndices.has(optionItem.index) ||
            (optionItem.type === 'other' && isCustomOptionSelected);
          const showCheck =
            question.multiSelect &&
            (optionItem.type === 'option' || optionItem.type === 'other');

          // Render inline text input for custom option
          if (optionItem.type === 'other') {
            const placeholder = question.placeholder || 'Enter a custom value';
            return (
              <Box flexDirection="row">
                {showCheck && (
                  <Text
                    color={
                      isChecked ? theme.status.success : theme.text.secondary
                    }
                  >
                    [{isChecked ? 'x' : ' '}]
                  </Text>
                )}
                <Text color={theme.text.primary}> </Text>
                <TextInput
                  buffer={customBuffer}
                  placeholder={placeholder}
                  focus={context.isSelected}
                  onSubmit={(val) => {
                    if (question.multiSelect) {
                      const fullAnswer = buildAnswerString(
                        selectedIndices,
                        true,
                        val,
                      );
                      if (fullAnswer) {
                        onAnswer(fullAnswer);
                      }
                    } else if (val.trim()) {
                      onAnswer(val.trim());
                    }
                  }}
                />
                {isChecked && !question.multiSelect && !context.isSelected && (
                  <Text color={theme.status.success}> ✓</Text>
                )}
              </Box>
            );
          }

          // Determine label color: checked (previously answered) uses success, selected uses accent, else primary
          const labelColor =
            isChecked && !question.multiSelect
              ? theme.status.success
              : context.isSelected
                ? context.titleColor
                : theme.text.primary;

          return (
            <Box flexDirection="column">
              <Box flexDirection="row">
                {showCheck && (
                  <Text
                    color={
                      isChecked ? theme.status.success : theme.text.secondary
                    }
                  >
                    [{isChecked ? 'x' : ' '}]
                  </Text>
                )}
                <Text color={labelColor} bold={optionItem.type === 'done'}>
                  {' '}
                  {optionItem.label}
                </Text>
                {isChecked && !question.multiSelect && (
                  <Text color={theme.status.success}> ✓</Text>
                )}
              </Box>
              {optionItem.description && (
                <Text color={theme.text.secondary} wrap="wrap">
                  {' '}
                  <RenderInline
                    text={optionItem.description}
                    defaultColor={theme.text.secondary}
                  />
                </Text>
              )}
            </Box>
          );
        }}
      />
      {keyboardHints}
    </Box>
  );
};

export const AskUserDialog: React.FC<AskUserDialogProps> = ({
  questions,
  onSubmit,
  onCancel,
  onActiveTextInputChange,
  width,
  availableHeight: availableHeightProp,
  extraParts,
}) => {
  const uiState = useContext(UIStateContext);
  const availableHeight =
    availableHeightProp ??
    (uiState?.constrainHeight !== false
      ? uiState?.availableTerminalHeight
      : undefined);

  const [state, dispatch] = useReducer(askUserDialogReducerLogic, initialState);
  const { answers, isEditingCustomOption, submitted } = state;

  const reviewTabIndex = questions.length;
  const tabCount =
    questions.length > 1 ? questions.length + 1 : questions.length;

  const { currentIndex, goToNextTab, goToPrevTab } = useTabbedNavigation({
    tabCount,
    isActive: !submitted && questions.length > 1,
    enableArrowNavigation: false, // We'll handle arrows via textBuffer callbacks or manually
    enableTabKey: false, // We'll handle tab manually to match existing behavior
  });

  const currentQuestionIndex = currentIndex;

  const handleEditingCustomOption = useCallback((isEditing: boolean) => {
    dispatch({ type: 'SET_EDITING_CUSTOM', payload: { isEditing } });
  }, []);

  useEffect(() => {
    onActiveTextInputChange?.(isEditingCustomOption);
    return () => {
      onActiveTextInputChange?.(false);
    };
  }, [isEditingCustomOption, onActiveTextInputChange]);

  const handleCancel = useCallback(
    (key: Key) => {
      if (submitted) return false;
      if (keyMatchers[Command.ESCAPE](key)) {
        onCancel();
        return true;
      } else if (keyMatchers[Command.QUIT](key)) {
        if (!isEditingCustomOption) {
          onCancel();
        }
        // Return false to let ctrl-C bubble up to AppContainer for exit flow
        return false;
      }
      return false;
    },
    [onCancel, submitted, isEditingCustomOption],
  );

  useKeypress(handleCancel, {
    isActive: !submitted,
  });

  const isOnReviewTab = currentQuestionIndex === reviewTabIndex;

  const handleNavigation = useCallback(
    (key: Key) => {
      if (submitted || questions.length <= 1) return false;

      const isNextKey = keyMatchers[Command.DIALOG_NEXT](key);
      const isPrevKey = keyMatchers[Command.DIALOG_PREV](key);

      const isRight = keyMatchers[Command.MOVE_RIGHT](key);
      const isLeft = keyMatchers[Command.MOVE_LEFT](key);

      // Tab keys always trigger navigation.
      // Arrows trigger navigation if NOT in a text input OR if the input bubbles the event (already at edge).
      const shouldGoNext = isNextKey || isRight;
      const shouldGoPrev = isPrevKey || isLeft;

      if (shouldGoNext) {
        goToNextTab();
        return true;
      } else if (shouldGoPrev) {
        goToPrevTab();
        return true;
      }
      return false;
    },
    [questions.length, submitted, goToNextTab, goToPrevTab],
  );

  useKeypress(handleNavigation, {
    isActive: questions.length > 1 && !submitted,
  });

  useEffect(() => {
    if (submitted) {
      onSubmit(answers);
    }
  }, [submitted, answers, onSubmit]);

  const handleAnswer = useCallback(
    (answer: string) => {
      if (submitted) return;

      if (questions.length > 1) {
        dispatch({
          type: 'SET_ANSWER',
          payload: {
            index: currentQuestionIndex,
            answer,
          },
        });
        goToNextTab();
      } else {
        dispatch({
          type: 'SET_ANSWER',
          payload: {
            index: currentQuestionIndex,
            answer,
            submit: true,
          },
        });
      }
    },
    [currentQuestionIndex, questions, submitted, goToNextTab],
  );

  const handleReviewSubmit = useCallback(() => {
    if (submitted) return;
    dispatch({ type: 'SUBMIT' });
  }, [submitted]);

  const handleSelectionChange = useCallback(
    (answer: string) => {
      if (submitted) return;
      dispatch({
        type: 'SET_ANSWER',
        payload: {
          index: currentQuestionIndex,
          answer,
        },
      });
    },
    [submitted, currentQuestionIndex],
  );

  const answeredIndices = useMemo(
    () => new Set(Object.keys(answers).map(Number)),
    [answers],
  );

  const currentQuestion = questions[currentQuestionIndex];

  const effectiveQuestion = useMemo(() => {
    if (currentQuestion?.type === 'yesno') {
      return {
        ...currentQuestion,
        options: [
          { label: 'Yes', description: '' },
          { label: 'No', description: '' },
        ],
        multiSelect: false,
      };
    }
    return currentQuestion;
  }, [currentQuestion]);

  const tabs = useMemo((): Tab[] => {
    const questionTabs: Tab[] = questions.map((q, i) => ({
      key: String(i),
      header: q.header,
    }));
    if (questions.length > 1) {
      questionTabs.push({
        key: 'review',
        header: 'Review',
        isSpecial: true,
      });
    }
    return questionTabs;
  }, [questions]);

  const progressHeader =
    questions.length > 1 ? (
      <TabHeader
        tabs={tabs}
        currentIndex={currentQuestionIndex}
        completedIndices={answeredIndices}
      />
    ) : null;

  if (isOnReviewTab) {
    return (
      <Box aria-label="Review your answers">
        <ReviewView
          questions={questions}
          answers={answers}
          onSubmit={handleReviewSubmit}
          progressHeader={progressHeader}
          extraParts={extraParts}
        />
      </Box>
    );
  }

  if (!currentQuestion) return null;

  const keyboardHints = (
    <DialogFooter
      primaryAction={
        currentQuestion.type === 'text' || isEditingCustomOption
          ? 'Enter to submit'
          : 'Enter to select'
      }
      navigationActions={
        questions.length > 1
          ? currentQuestion.type === 'text' || isEditingCustomOption
            ? 'Tab/Shift+Tab to switch questions'
            : '←/→ to switch questions'
          : currentQuestion.type === 'text' || isEditingCustomOption
            ? undefined
            : '↑/↓ to navigate'
      }
      extraParts={extraParts}
    />
  );

  const questionView =
    currentQuestion.type === 'text' ? (
      <TextQuestionView
        key={currentQuestionIndex}
        question={currentQuestion}
        onAnswer={handleAnswer}
        onSelectionChange={handleSelectionChange}
        onEditingCustomOption={handleEditingCustomOption}
        availableWidth={width}
        availableHeight={availableHeight}
        initialAnswer={answers[currentQuestionIndex]}
        progressHeader={progressHeader}
        keyboardHints={keyboardHints}
      />
    ) : (
      <ChoiceQuestionView
        key={currentQuestionIndex}
        question={effectiveQuestion}
        onAnswer={handleAnswer}
        onSelectionChange={handleSelectionChange}
        onEditingCustomOption={handleEditingCustomOption}
        availableWidth={width}
        availableHeight={availableHeight}
        initialAnswer={answers[currentQuestionIndex]}
        progressHeader={progressHeader}
        keyboardHints={keyboardHints}
      />
    );

  return (
    <Box
      flexDirection="column"
      width={width}
      aria-label={`Question ${currentQuestionIndex + 1} of ${questions.length}: ${currentQuestion.question}`}
    >
      {questionView}
    </Box>
  );
};
