const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra');
const { CLIUtils } = require('./cli-utils');
const { CustomHandler } = require('./custom-handler');
const { ExternalModuleManager } = require('./modules/external-manager');
const prompts = require('./prompts');

// Separator class for visual grouping in select/multiselect prompts
// Note: @clack/prompts doesn't support separators natively, they are filtered out
class Separator {
  constructor(text = '────────') {
    this.line = text;
    this.name = text;
  }
  type = 'separator';
}

// Separator for choice lists (compatible interface)
const choiceUtils = { Separator };

/**
 * UI utilities for the installer
 */
class UI {
  /**
   * Prompt for installation configuration
   * @param {Object} options - Command-line options from install command
   * @returns {Object} Installation configuration
   */
  async promptInstall(options = {}) {
    await CLIUtils.displayLogo();

    // Display version-specific start message from install-messages.yaml
    const { MessageLoader } = require('./message-loader');
    const messageLoader = new MessageLoader();
    await messageLoader.displayStartMessage();

    // Get directory from options or prompt
    let confirmedDirectory;
    if (options.directory) {
      // Use provided directory from command-line
      const expandedDir = this.expandUserPath(options.directory);
      const validation = this.validateDirectorySync(expandedDir);
      if (validation) {
        throw new Error(`Invalid directory: ${validation}`);
      }
      confirmedDirectory = expandedDir;
      await prompts.log.info(`Using directory from command-line: ${confirmedDirectory}`);
    } else {
      confirmedDirectory = await this.getConfirmedDirectory();
    }

    const { Installer } = require('./core/installer');
    const installer = new Installer();
    const { bmadDir } = await installer.findBmadDir(confirmedDirectory);

    // Check if there's an existing BMAD installation
    const hasExistingInstall = await fs.pathExists(bmadDir);

    let customContentConfig = { hasCustomContent: false };
    if (!hasExistingInstall) {
      customContentConfig._shouldAsk = true;
    }

    // Track action type (only set if there's an existing installation)
    let actionType;

    // Only show action menu if there's an existing installation
    if (hasExistingInstall) {
      // Get version information
      const { existingInstall, bmadDir } = await this.getExistingInstallation(confirmedDirectory);
      const packageJsonPath = path.join(__dirname, '../../package.json');
      const currentVersion = require(packageJsonPath).version;
      const installedVersion = existingInstall.installed ? existingInstall.version || 'unknown' : 'unknown';

      // Build menu choices dynamically
      const choices = [];

      // Always show Quick Update first (allows refreshing installation even on same version)
      if (installedVersion !== 'unknown') {
        choices.push({
          name: `Quick Update (v${installedVersion} → v${currentVersion})`,
          value: 'quick-update',
        });
      }

      // Common actions
      choices.push({ name: 'Modify BMAD Installation', value: 'update' });

      // Check if action is provided via command-line
      if (options.action) {
        const validActions = choices.map((c) => c.value);
        if (!validActions.includes(options.action)) {
          throw new Error(`Invalid action: ${options.action}. Valid actions: ${validActions.join(', ')}`);
        }
        actionType = options.action;
        await prompts.log.info(`Using action from command-line: ${actionType}`);
      } else if (options.yes) {
        // Default to quick-update if available, otherwise first available choice
        if (choices.length === 0) {
          throw new Error('No valid actions available for this installation');
        }
        const hasQuickUpdate = choices.some((c) => c.value === 'quick-update');
        actionType = hasQuickUpdate ? 'quick-update' : choices[0].value;
        await prompts.log.info(`Non-interactive mode (--yes): defaulting to ${actionType}`);
      } else {
        actionType = await prompts.select({
          message: 'How would you like to proceed?',
          choices: choices,
          default: choices[0].value,
        });
      }

      // Handle quick update separately
      if (actionType === 'quick-update') {
        // Pass --custom-content through so installer can re-cache if cache is missing
        let customContentForQuickUpdate = { hasCustomContent: false };
        if (options.customContent) {
          const paths = options.customContent
            .split(',')
            .map((p) => p.trim())
            .filter(Boolean);
          if (paths.length > 0) {
            const customPaths = [];
            const selectedModuleIds = [];
            const sources = [];
            for (const customPath of paths) {
              const expandedPath = this.expandUserPath(customPath);
              const validation = this.validateCustomContentPathSync(expandedPath);
              if (validation) continue;
              let moduleMeta;
              try {
                const moduleYamlPath = path.join(expandedPath, 'module.yaml');
                moduleMeta = require('yaml').parse(await fs.readFile(moduleYamlPath, 'utf-8'));
              } catch {
                continue;
              }
              if (!moduleMeta?.code) continue;
              customPaths.push(expandedPath);
              selectedModuleIds.push(moduleMeta.code);
              sources.push({ path: expandedPath, id: moduleMeta.code, name: moduleMeta.name || moduleMeta.code });
            }
            if (customPaths.length > 0) {
              customContentForQuickUpdate = {
                hasCustomContent: true,
                selected: true,
                sources,
                selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
                selectedModuleIds,
              };
            }
          }
        }
        return {
          actionType: 'quick-update',
          directory: confirmedDirectory,
          customContent: customContentForQuickUpdate,
          skipPrompts: options.yes || false,
        };
      }

      // If actionType === 'update', handle it with the new flow
      // Return early with modify configuration
      if (actionType === 'update') {
        // Get existing installation info
        const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);

        await prompts.log.message(`Found existing modules: ${[...installedModuleIds].join(', ')}`);

        // Unified module selection - all modules in one grouped multiselect
        let selectedModules;
        if (options.modules) {
          // Use modules from command-line
          selectedModules = options.modules
            .split(',')
            .map((m) => m.trim())
            .filter(Boolean);
          await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
        } else if (options.yes) {
          selectedModules = await this.getDefaultModules(installedModuleIds);
          await prompts.log.info(
            `Non-interactive mode (--yes): using default modules (installed + defaults): ${selectedModules.join(', ')}`,
          );
        } else {
          selectedModules = await this.selectAllModules(installedModuleIds);
        }

        // After module selection, ask about custom modules
        let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };

        if (options.customContent) {
          // Use custom content from command-line
          const paths = options.customContent
            .split(',')
            .map((p) => p.trim())
            .filter(Boolean);
          await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);

          // Build custom content config similar to promptCustomContentSource
          const customPaths = [];
          const selectedModuleIds = [];
          const sources = [];

          for (const customPath of paths) {
            const expandedPath = this.expandUserPath(customPath);
            const validation = this.validateCustomContentPathSync(expandedPath);
            if (validation) {
              await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
              continue;
            }

            // Read module metadata
            let moduleMeta;
            try {
              const moduleYamlPath = path.join(expandedPath, 'module.yaml');
              const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
              const yaml = require('yaml');
              moduleMeta = yaml.parse(moduleYaml);
            } catch (error) {
              await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
              continue;
            }

            if (!moduleMeta) {
              await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
              continue;
            }

            if (!moduleMeta.code) {
              await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
              continue;
            }

            customPaths.push(expandedPath);
            selectedModuleIds.push(moduleMeta.code);
            sources.push({
              path: expandedPath,
              id: moduleMeta.code,
              name: moduleMeta.name || moduleMeta.code,
            });
          }

          if (customPaths.length > 0) {
            customModuleResult = {
              selectedCustomModules: selectedModuleIds,
              customContentConfig: {
                hasCustomContent: true,
                selected: true,
                sources,
                selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
                selectedModuleIds: selectedModuleIds,
              },
            };
          }
        } else if (options.yes) {
          // Non-interactive mode: preserve existing custom modules (matches default: false)
          const cacheDir = path.join(bmadDir, '_config', 'custom');
          if (await fs.pathExists(cacheDir)) {
            const entries = await fs.readdir(cacheDir, { withFileTypes: true });
            for (const entry of entries) {
              if (entry.isDirectory()) {
                customModuleResult.selectedCustomModules.push(entry.name);
              }
            }
            await prompts.log.info(
              `Non-interactive mode (--yes): preserving ${customModuleResult.selectedCustomModules.length} existing custom module(s)`,
            );
          } else {
            await prompts.log.info('Non-interactive mode (--yes): no existing custom modules found');
          }
        } else {
          const changeCustomModules = await prompts.confirm({
            message: 'Modify custom modules, agents, or workflows?',
            default: false,
          });

          if (changeCustomModules) {
            customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules);
          } else {
            // Preserve existing custom modules if user doesn't want to modify them
            const { Installer } = require('./core/installer');
            const installer = new Installer();
            const { bmadDir } = await installer.findBmadDir(confirmedDirectory);

            const cacheDir = path.join(bmadDir, '_config', 'custom');
            if (await fs.pathExists(cacheDir)) {
              const entries = await fs.readdir(cacheDir, { withFileTypes: true });
              for (const entry of entries) {
                if (entry.isDirectory()) {
                  customModuleResult.selectedCustomModules.push(entry.name);
                }
              }
            }
          }
        }

        // Merge any selected custom modules
        if (customModuleResult.selectedCustomModules.length > 0) {
          selectedModules.push(...customModuleResult.selectedCustomModules);
        }

        // Ensure core is in the modules list
        if (!selectedModules.includes('core')) {
          selectedModules.unshift('core');
        }

        // Get tool selection
        const toolSelection = await this.promptToolSelection(confirmedDirectory, options);

        const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules, options);

        return {
          actionType: 'update',
          directory: confirmedDirectory,
          modules: selectedModules,
          ides: toolSelection.ides,
          skipIde: toolSelection.skipIde,
          coreConfig: moduleConfigs.core || {},
          moduleConfigs: moduleConfigs,
          customContent: customModuleResult.customContentConfig,
          skipPrompts: options.yes || false,
        };
      }
    }

    // This section is only for new installations (update returns early above)
    const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);

    // Unified module selection - all modules in one grouped multiselect
    let selectedModules;
    if (options.modules) {
      // Use modules from command-line
      selectedModules = options.modules
        .split(',')
        .map((m) => m.trim())
        .filter(Boolean);
      await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
    } else if (options.yes) {
      // Use default modules when --yes flag is set
      selectedModules = await this.getDefaultModules(installedModuleIds);
      await prompts.log.info(`Using default modules (--yes flag): ${selectedModules.join(', ')}`);
    } else {
      selectedModules = await this.selectAllModules(installedModuleIds);
    }

    // Ask about custom content (local modules/agents/workflows)
    if (options.customContent) {
      // Use custom content from command-line
      const paths = options.customContent
        .split(',')
        .map((p) => p.trim())
        .filter(Boolean);
      await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);

      // Build custom content config similar to promptCustomContentSource
      const customPaths = [];
      const selectedModuleIds = [];
      const sources = [];

      for (const customPath of paths) {
        const expandedPath = this.expandUserPath(customPath);
        const validation = this.validateCustomContentPathSync(expandedPath);
        if (validation) {
          await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
          continue;
        }

        // Read module metadata
        let moduleMeta;
        try {
          const moduleYamlPath = path.join(expandedPath, 'module.yaml');
          const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
          const yaml = require('yaml');
          moduleMeta = yaml.parse(moduleYaml);
        } catch (error) {
          await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
          continue;
        }

        if (!moduleMeta) {
          await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
          continue;
        }

        if (!moduleMeta.code) {
          await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
          continue;
        }

        customPaths.push(expandedPath);
        selectedModuleIds.push(moduleMeta.code);
        sources.push({
          path: expandedPath,
          id: moduleMeta.code,
          name: moduleMeta.name || moduleMeta.code,
        });
      }

      if (customPaths.length > 0) {
        customContentConfig = {
          hasCustomContent: true,
          selected: true,
          sources,
          selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
          selectedModuleIds: selectedModuleIds,
        };
      }
    } else if (!options.yes) {
      const wantsCustomContent = await prompts.confirm({
        message: 'Add custom modules, agents, or workflows from your computer?',
        default: false,
      });

      if (wantsCustomContent) {
        customContentConfig = await this.promptCustomContentSource();
      }
    }

    // Add custom content modules if any were selected
    if (customContentConfig && customContentConfig.selectedModuleIds) {
      selectedModules.push(...customContentConfig.selectedModuleIds);
    }

    // Ensure core is in the modules list
    if (!selectedModules.includes('core')) {
      selectedModules.unshift('core');
    }
    let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
    const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules, options);

    return {
      actionType: 'install',
      directory: confirmedDirectory,
      modules: selectedModules,
      ides: toolSelection.ides,
      skipIde: toolSelection.skipIde,
      coreConfig: moduleConfigs.core || {},
      moduleConfigs: moduleConfigs,
      customContent: customContentConfig,
      skipPrompts: options.yes || false,
    };
  }

  /**
   * Prompt for tool/IDE selection (called after module configuration)
   * Uses a split prompt approach:
   *   1. Recommended tools - standard multiselect for preferred tools
   *   2. Additional tools - autocompleteMultiselect with search capability
   * @param {string} projectDir - Project directory to check for existing IDEs
   * @param {Object} options - Command-line options
   * @returns {Object} Tool configuration
   */
  async promptToolSelection(projectDir, options = {}) {
    const { ExistingInstall } = require('./core/existing-install');
    const { Installer } = require('./core/installer');
    const installer = new Installer();
    const { bmadDir } = await installer.findBmadDir(projectDir || process.cwd());
    const existingInstall = await ExistingInstall.detect(bmadDir);
    const configuredIdes = existingInstall.ides;

    // Get IDE manager to fetch available IDEs dynamically
    const { IdeManager } = require('./ide/manager');
    const ideManager = new IdeManager();
    await ideManager.ensureInitialized(); // IMPORTANT: Must initialize before getting IDEs

    const preferredIdes = ideManager.getPreferredIdes();
    const otherIdes = ideManager.getOtherIdes();

    // Determine which configured IDEs are in "preferred" vs "other" categories
    const configuredPreferred = configuredIdes.filter((id) => preferredIdes.some((ide) => ide.value === id));
    const configuredOther = configuredIdes.filter((id) => otherIdes.some((ide) => ide.value === id));

    // Warn about previously configured tools that are no longer available
    const allKnownValues = new Set([...preferredIdes, ...otherIdes].map((ide) => ide.value));
    const unknownTools = configuredIdes.filter((id) => id && typeof id === 'string' && !allKnownValues.has(id));
    if (unknownTools.length > 0) {
      await prompts.log.warn(`Previously configured tools are no longer available: ${unknownTools.join(', ')}`);
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // UPGRADE PATH: If tools already configured, show all tools with configured at top
    // ─────────────────────────────────────────────────────────────────────────────
    if (configuredIdes.length > 0) {
      const allTools = [...preferredIdes, ...otherIdes];

      // Non-interactive: handle --tools and --yes flags before interactive prompt
      if (options.tools) {
        if (options.tools.toLowerCase() === 'none') {
          await prompts.log.info('Skipping tool configuration (--tools none)');
          return { ides: [], skipIde: true };
        }
        const selectedIdes = options.tools
          .split(',')
          .map((t) => t.trim())
          .filter(Boolean);
        await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
        await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
        return { ides: selectedIdes, skipIde: false };
      }

      if (options.yes) {
        await prompts.log.info(`Non-interactive mode (--yes): keeping configured tools: ${configuredIdes.join(', ')}`);
        await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
        return { ides: configuredIdes, skipIde: false };
      }

      // Sort: configured tools first, then preferred, then others
      const sortedTools = [
        ...allTools.filter((ide) => configuredIdes.includes(ide.value)),
        ...allTools.filter((ide) => !configuredIdes.includes(ide.value)),
      ];

      const upgradeOptions = sortedTools.map((ide) => {
        const isConfigured = configuredIdes.includes(ide.value);
        const isPreferred = preferredIdes.some((p) => p.value === ide.value);
        let label = ide.name;
        if (isPreferred) label += ' ⭐';
        if (isConfigured) label += ' ✅';
        return { label, value: ide.value };
      });

      // Sort initialValues to match display order
      const sortedInitialValues = sortedTools.filter((ide) => configuredIdes.includes(ide.value)).map((ide) => ide.value);

      const upgradeSelected = await prompts.autocompleteMultiselect({
        message: 'Integrate with',
        options: upgradeOptions,
        initialValues: sortedInitialValues,
        required: false,
        maxItems: 8,
      });

      const selectedIdes = upgradeSelected || [];

      if (selectedIdes.length === 0) {
        const confirmNoTools = await prompts.confirm({
          message: 'No tools selected. Continue without installing any tools?',
          default: false,
        });

        if (!confirmNoTools) {
          return this.promptToolSelection(projectDir, options);
        }

        return { ides: [], skipIde: true };
      }

      // Display selected tools
      await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);

      return { ides: selectedIdes, skipIde: false };
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // NEW INSTALL: Show all tools with search
    // ─────────────────────────────────────────────────────────────────────────────
    const allTools = [...preferredIdes, ...otherIdes];

    const allToolOptions = allTools.map((ide) => {
      const isPreferred = preferredIdes.some((p) => p.value === ide.value);
      let label = ide.name;
      if (isPreferred) label += ' ⭐';
      return {
        label,
        value: ide.value,
      };
    });

    let selectedIdes = [];

    // Check if tools are provided via command-line
    if (options.tools) {
      // Check for explicit "none" value to skip tool installation
      if (options.tools.toLowerCase() === 'none') {
        await prompts.log.info('Skipping tool configuration (--tools none)');
        return { ides: [], skipIde: true };
      } else {
        selectedIdes = options.tools
          .split(',')
          .map((t) => t.trim())
          .filter(Boolean);
        await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
        await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
        return { ides: selectedIdes, skipIde: false };
      }
    } else if (options.yes) {
      // If --yes flag is set, skip tool prompt and use previously configured tools or empty
      if (configuredIdes.length > 0) {
        await prompts.log.info(`Using previously configured tools (--yes flag): ${configuredIdes.join(', ')}`);
        await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
        return { ides: configuredIdes, skipIde: false };
      } else {
        await prompts.log.info('Skipping tool configuration (--yes flag, no previous tools)');
        return { ides: [], skipIde: true };
      }
    }

    // Interactive mode
    const interactiveSelectedIdes = await prompts.autocompleteMultiselect({
      message: 'Integrate with:',
      options: allToolOptions,
      initialValues: configuredIdes.length > 0 ? configuredIdes : undefined,
      required: false,
      maxItems: 8,
    });

    selectedIdes = interactiveSelectedIdes || [];

    // ─────────────────────────────────────────────────────────────────────────────
    // STEP 3: Confirm if no tools selected
    // ─────────────────────────────────────────────────────────────────────────────
    if (selectedIdes.length === 0) {
      const confirmNoTools = await prompts.confirm({
        message: 'No tools selected. Continue without installing any tools?',
        default: false,
      });

      if (!confirmNoTools) {
        // User wants to select tools - recurse
        return this.promptToolSelection(projectDir, options);
      }

      return {
        ides: [],
        skipIde: true,
      };
    }

    // Display selected tools
    await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);

    return {
      ides: selectedIdes,
      skipIde: selectedIdes.length === 0,
    };
  }

  /**
   * Prompt for update configuration
   * @returns {Object} Update configuration
   */
  async promptUpdate() {
    const backupFirst = await prompts.confirm({
      message: 'Create backup before updating?',
      default: true,
    });

    const preserveCustomizations = await prompts.confirm({
      message: 'Preserve local customizations?',
      default: true,
    });

    return { backupFirst, preserveCustomizations };
  }

  /**
   * Confirm action
   * @param {string} message - Confirmation message
   * @param {boolean} defaultValue - Default value
   * @returns {boolean} User confirmation
   */
  async confirm(message, defaultValue = false) {
    return await prompts.confirm({
      message,
      default: defaultValue,
    });
  }

  /**
   * Get confirmed directory from user
   * @returns {string} Confirmed directory path
   */
  async getConfirmedDirectory() {
    let confirmedDirectory = null;
    while (!confirmedDirectory) {
      const directoryAnswer = await this.promptForDirectory();
      await this.displayDirectoryInfo(directoryAnswer.directory);

      if (await this.confirmDirectory(directoryAnswer.directory)) {
        confirmedDirectory = directoryAnswer.directory;
      }
    }
    return confirmedDirectory;
  }

  /**
   * Get existing installation info and installed modules
   * @param {string} directory - Installation directory
   * @returns {Object} Object with existingInstall, installedModuleIds, and bmadDir
   */
  async getExistingInstallation(directory) {
    const { ExistingInstall } = require('./core/existing-install');
    const { Installer } = require('./core/installer');
    const installer = new Installer();
    const { bmadDir } = await installer.findBmadDir(directory);
    const existingInstall = await ExistingInstall.detect(bmadDir);
    const installedModuleIds = new Set(existingInstall.moduleIds);

    return { existingInstall, installedModuleIds, bmadDir };
  }

  /**
   * Collect all module configurations (core + selected modules).
   * All interactive prompting happens here in the UI layer.
   * @param {string} directory - Installation directory
   * @param {string[]} modules - Modules to configure (including 'core')
   * @param {Object} options - Command-line options
   * @returns {Object} Collected module configurations keyed by module name
   */
  async collectModuleConfigs(directory, modules, options = {}) {
    const { OfficialModules } = require('./modules/official-modules');
    const configCollector = new OfficialModules();

    // Seed core config from CLI options if provided
    if (options.userName || options.communicationLanguage || options.documentOutputLanguage || options.outputFolder) {
      const coreConfig = {};
      if (options.userName) {
        coreConfig.user_name = options.userName;
        await prompts.log.info(`Using user name from command-line: ${options.userName}`);
      }
      if (options.communicationLanguage) {
        coreConfig.communication_language = options.communicationLanguage;
        await prompts.log.info(`Using communication language from command-line: ${options.communicationLanguage}`);
      }
      if (options.documentOutputLanguage) {
        coreConfig.document_output_language = options.documentOutputLanguage;
        await prompts.log.info(`Using document output language from command-line: ${options.documentOutputLanguage}`);
      }
      if (options.outputFolder) {
        coreConfig.output_folder = options.outputFolder;
        await prompts.log.info(`Using output folder from command-line: ${options.outputFolder}`);
      }

      // Load existing config to merge with provided options
      await configCollector.loadExistingConfig(directory);
      const existingConfig = configCollector.collectedConfig.core || {};
      configCollector.collectedConfig.core = { ...existingConfig, ...coreConfig };

      // If not all options are provided, collect the missing ones interactively (unless --yes flag)
      if (
        !options.yes &&
        (!options.userName || !options.communicationLanguage || !options.documentOutputLanguage || !options.outputFolder)
      ) {
        await configCollector.collectModuleConfig('core', directory, false, true);
      }
    } else if (options.yes) {
      // Use all defaults when --yes flag is set
      await configCollector.loadExistingConfig(directory);
      const existingConfig = configCollector.collectedConfig.core || {};

      if (Object.keys(existingConfig).length === 0) {
        let safeUsername;
        try {
          safeUsername = os.userInfo().username;
        } catch {
          safeUsername = process.env.USER || process.env.USERNAME || 'User';
        }
        const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1);
        configCollector.collectedConfig.core = {
          user_name: defaultUsername,
          communication_language: 'English',
          document_output_language: 'English',
          output_folder: '_bmad-output',
        };
        await prompts.log.info('Using default configuration (--yes flag)');
      }
    }

    // Collect all module configs — core is skipped if already seeded above
    await configCollector.collectAllConfigurations(modules, directory, {
      skipPrompts: options.yes || false,
    });

    return configCollector.collectedConfig;
  }

  /**
   * Get module choices for selection
   * @param {Set} installedModuleIds - Currently installed module IDs
   * @param {Object} customContentConfig - Custom content configuration
   * @returns {Array} Module choices for prompt
   */
  async getModuleChoices(installedModuleIds, customContentConfig = null) {
    const color = await prompts.getColor();
    const moduleChoices = [];
    const isNewInstallation = installedModuleIds.size === 0;

    const customContentItems = [];

    // Add custom content items
    if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
      // Existing installation - show from directory
      const customHandler = new CustomHandler();
      const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);

      for (const customFile of customFiles) {
        const customInfo = await customHandler.getCustomInfo(customFile);
        if (customInfo) {
          customContentItems.push({
            name: `${color.cyan('\u2713')} ${customInfo.name} ${color.dim(`(${customInfo.relativePath})`)}`,
            value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
            checked: true, // Default to selected since user chose to provide custom content
            path: customInfo.path, // Track path to avoid duplicates
            hint: customInfo.description || undefined,
          });
        }
      }
    }

    // Add official modules
    const { OfficialModules } = require('./modules/official-modules');
    const officialModules = new OfficialModules();
    const { modules: availableModules, customModules: customModulesFromCache } = await officialModules.listAvailable();

    // First, add all items to appropriate sections
    const allCustomModules = [];

    // Add custom content items from directory
    allCustomModules.push(...customContentItems);

    // Add custom modules from cache
    for (const mod of customModulesFromCache) {
      // Skip if this module is already in customContentItems (by path)
      const isDuplicate = allCustomModules.some((item) => item.path && mod.path && path.resolve(item.path) === path.resolve(mod.path));

      if (!isDuplicate) {
        allCustomModules.push({
          name: `${color.cyan('\u2713')} ${mod.name} ${color.dim('(cached)')}`,
          value: mod.id,
          checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
          hint: mod.description || undefined,
        });
      }
    }

    // Add separators and modules in correct order
    if (allCustomModules.length > 0) {
      // Add separator for custom content, all custom modules, and official content separator
      moduleChoices.push(
        new choiceUtils.Separator('── Custom Content ──'),
        ...allCustomModules,
        new choiceUtils.Separator('── Official Content ──'),
      );
    }

    // Add official modules (only non-custom ones)
    for (const mod of availableModules) {
      if (!mod.isCustom) {
        moduleChoices.push({
          name: mod.name,
          value: mod.id,
          checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
          hint: mod.description || undefined,
        });
      }
    }

    return moduleChoices;
  }

  /**
   * Select all modules (official + community) using grouped multiselect.
   * Core is shown as locked but filtered from the result since it's always installed separately.
   * @param {Set} installedModuleIds - Currently installed module IDs
   * @returns {Array} Selected module codes (excluding core)
   */
  async selectAllModules(installedModuleIds = new Set()) {
    const { OfficialModules } = require('./modules/official-modules');
    const officialModulesSource = new OfficialModules();
    const { modules: localModules } = await officialModulesSource.listAvailable();

    // Get external modules
    const externalManager = new ExternalModuleManager();
    const externalModules = await externalManager.listAvailable();

    // Build flat options list with group hints for autocompleteMultiselect
    const allOptions = [];
    const initialValues = [];
    const lockedValues = ['core'];

    // Core module is always installed — show it locked at the top
    allOptions.push({ label: 'BMad Core Module', value: 'core', hint: 'Core configuration and shared resources' });
    initialValues.push('core');

    // Helper to build module entry with proper sorting and selection
    const buildModuleEntry = (mod, value, group) => {
      const isInstalled = installedModuleIds.has(value);
      return {
        label: mod.name,
        value,
        hint: mod.description || group,
        // Pre-select only if already installed (not on fresh install)
        selected: isInstalled,
      };
    };

    // Local modules (BMM, BMB, etc.)
    const localEntries = [];
    for (const mod of localModules) {
      if (!mod.isCustom && mod.id !== 'core') {
        const entry = buildModuleEntry(mod, mod.id, 'Local');
        localEntries.push(entry);
        if (entry.selected) {
          initialValues.push(mod.id);
        }
      }
    }
    allOptions.push(...localEntries.map(({ label, value, hint }) => ({ label, value, hint })));

    // Group 2: BMad Official Modules (type: bmad-org)
    const officialModules = [];
    for (const mod of externalModules) {
      if (mod.type === 'bmad-org') {
        const entry = buildModuleEntry(mod, mod.code, 'Official');
        officialModules.push(entry);
        if (entry.selected) {
          initialValues.push(mod.code);
        }
      }
    }
    allOptions.push(...officialModules.map(({ label, value, hint }) => ({ label, value, hint })));

    // Group 3: Community Modules (type: community)
    const communityModules = [];
    for (const mod of externalModules) {
      if (mod.type === 'community') {
        const entry = buildModuleEntry(mod, mod.code, 'Community');
        communityModules.push(entry);
        if (entry.selected) {
          initialValues.push(mod.code);
        }
      }
    }
    allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })));

    const selected = await prompts.autocompleteMultiselect({
      message: 'Select modules to install:',
      options: allOptions,
      initialValues: initialValues.length > 0 ? initialValues : undefined,
      lockedValues,
      required: true,
      maxItems: allOptions.length,
    });

    const result = selected ? [...selected] : [];

    // Display selected modules as bulleted list
    if (result.length > 0) {
      const moduleLines = result.map((moduleId) => {
        const opt = allOptions.find((o) => o.value === moduleId);
        return `  \u2022 ${opt?.label || moduleId}`;
      });
      await prompts.log.message('Selected modules:\n' + moduleLines.join('\n'));
    }

    return result;
  }

  /**
   * Get default modules for non-interactive mode
   * @param {Set} installedModuleIds - Already installed module IDs
   * @returns {Array} Default module codes
   */
  async getDefaultModules(installedModuleIds = new Set()) {
    const { OfficialModules } = require('./modules/official-modules');
    const officialModules = new OfficialModules();
    const { modules: localModules } = await officialModules.listAvailable();

    const defaultModules = [];

    // Add default-selected local modules (typically BMM)
    for (const mod of localModules) {
      if (mod.defaultSelected === true || installedModuleIds.has(mod.id)) {
        defaultModules.push(mod.id);
      }
    }

    // If no defaults found, use 'bmm' as the fallback default
    if (defaultModules.length === 0) {
      defaultModules.push('bmm');
    }

    return defaultModules;
  }

  /**
   * Prompt for directory selection
   * @returns {Object} Directory answer from prompt
   */
  async promptForDirectory() {
    // Use sync validation because @clack/prompts doesn't support async validate
    const directory = await prompts.text({
      message: 'Installation directory:',
      default: process.cwd(),
      placeholder: process.cwd(),
      validate: (input) => this.validateDirectorySync(input),
    });

    // Apply filter logic
    let filteredDir = directory;
    if (!filteredDir || filteredDir.trim() === '') {
      filteredDir = process.cwd();
    } else {
      filteredDir = this.expandUserPath(filteredDir);
    }

    return { directory: filteredDir };
  }

  /**
   * Display directory information
   * @param {string} directory - The directory path
   */
  async displayDirectoryInfo(directory) {
    await prompts.log.info(`Resolved installation path: ${directory}`);

    const dirExists = await fs.pathExists(directory);
    if (dirExists) {
      // Show helpful context about the existing path
      const stats = await fs.stat(directory);
      if (stats.isDirectory()) {
        const files = await fs.readdir(directory);
        if (files.length > 0) {
          // Check for any bmad installation (any folder with _config/manifest.yaml)
          const { Installer } = require('./core/installer');
          const installer = new Installer();
          const bmadResult = await installer.findBmadDir(directory);
          const hasBmadInstall =
            (await fs.pathExists(bmadResult.bmadDir)) && (await fs.pathExists(path.join(bmadResult.bmadDir, '_config', 'manifest.yaml')));

          const bmadNote = hasBmadInstall ? ` including existing BMAD installation (${path.basename(bmadResult.bmadDir)})` : '';
          await prompts.log.message(`Directory exists and contains ${files.length} item(s)${bmadNote}`);
        } else {
          await prompts.log.message('Directory exists and is empty');
        }
      }
    }
  }

  /**
   * Confirm directory selection
   * @param {string} directory - The directory path
   * @returns {boolean} Whether user confirmed
   */
  async confirmDirectory(directory) {
    const dirExists = await fs.pathExists(directory);

    if (dirExists) {
      const proceed = await prompts.confirm({
        message: 'Install to this directory?',
        default: true,
      });

      if (!proceed) {
        await prompts.log.warn("Let's try again with a different path.");
      }

      return proceed;
    } else {
      // Ask for confirmation to create the directory
      const create = await prompts.confirm({
        message: `Create directory: ${directory}?`,
        default: false,
      });

      if (!create) {
        await prompts.log.warn("Let's try again with a different path.");
      }

      return create;
    }
  }

  /**
   * Validate directory path for installation (sync version for clack prompts)
   * @param {string} input - User input path
   * @returns {string|undefined} Error message or undefined if valid
   */
  validateDirectorySync(input) {
    // Allow empty input to use the default
    if (!input || input.trim() === '') {
      return; // Empty means use default, undefined = valid for clack
    }

    let expandedPath;
    try {
      expandedPath = this.expandUserPath(input.trim());
    } catch (error) {
      return error.message;
    }

    // Check if the path exists
    const pathExists = fs.pathExistsSync(expandedPath);

    if (!pathExists) {
      // Find the first existing parent directory
      const existingParent = this.findExistingParentSync(expandedPath);

      if (!existingParent) {
        return 'Cannot create directory: no existing parent directory found';
      }

      // Check if the existing parent is writable
      try {
        fs.accessSync(existingParent, fs.constants.W_OK);
        // Path doesn't exist but can be created - will prompt for confirmation later
        return;
      } catch {
        // Provide a detailed error message explaining both issues
        return `Directory '${expandedPath}' does not exist and cannot be created: parent directory '${existingParent}' is not writable`;
      }
    }

    // If it exists, validate it's a directory and writable
    const stat = fs.statSync(expandedPath);
    if (!stat.isDirectory()) {
      return `Path exists but is not a directory: ${expandedPath}`;
    }

    // Check write permissions
    try {
      fs.accessSync(expandedPath, fs.constants.W_OK);
    } catch {
      return `Directory is not writable: ${expandedPath}`;
    }

    return;
  }

  /**
   * Validate directory path for installation (async version)
   * @param {string} input - User input path
   * @returns {string|true} Error message or true if valid
   */
  async validateDirectory(input) {
    // Allow empty input to use the default
    if (!input || input.trim() === '') {
      return true; // Empty means use default
    }

    let expandedPath;
    try {
      expandedPath = this.expandUserPath(input.trim());
    } catch (error) {
      return error.message;
    }

    // Check if the path exists
    const pathExists = await fs.pathExists(expandedPath);

    if (!pathExists) {
      // Find the first existing parent directory
      const existingParent = await this.findExistingParent(expandedPath);

      if (!existingParent) {
        return 'Cannot create directory: no existing parent directory found';
      }

      // Check if the existing parent is writable
      try {
        await fs.access(existingParent, fs.constants.W_OK);
        // Path doesn't exist but can be created - will prompt for confirmation later
        return true;
      } catch {
        // Provide a detailed error message explaining both issues
        return `Directory '${expandedPath}' does not exist and cannot be created: parent directory '${existingParent}' is not writable`;
      }
    }

    // If it exists, validate it's a directory and writable
    const stat = await fs.stat(expandedPath);
    if (!stat.isDirectory()) {
      return `Path exists but is not a directory: ${expandedPath}`;
    }

    // Check write permissions
    try {
      await fs.access(expandedPath, fs.constants.W_OK);
    } catch {
      return `Directory is not writable: ${expandedPath}`;
    }

    return true;
  }

  /**
   * Find the first existing parent directory (sync version)
   * @param {string} targetPath - The path to check
   * @returns {string|null} The first existing parent directory, or null if none found
   */
  findExistingParentSync(targetPath) {
    let currentPath = path.resolve(targetPath);

    // Walk up the directory tree until we find an existing directory
    while (currentPath !== path.dirname(currentPath)) {
      // Stop at root
      const parent = path.dirname(currentPath);
      if (fs.pathExistsSync(parent)) {
        return parent;
      }
      currentPath = parent;
    }

    return null; // No existing parent found (shouldn't happen in practice)
  }

  /**
   * Find the first existing parent directory (async version)
   * @param {string} targetPath - The path to check
   * @returns {string|null} The first existing parent directory, or null if none found
   */
  async findExistingParent(targetPath) {
    let currentPath = path.resolve(targetPath);

    // Walk up the directory tree until we find an existing directory
    while (currentPath !== path.dirname(currentPath)) {
      // Stop at root
      const parent = path.dirname(currentPath);
      if (await fs.pathExists(parent)) {
        return parent;
      }
      currentPath = parent;
    }

    return null; // No existing parent found (shouldn't happen in practice)
  }

  /**
   * Expands the user-provided path: handles ~ and resolves to absolute.
   * @param {string} inputPath - User input path.
   * @returns {string} Absolute expanded path.
   */
  expandUserPath(inputPath) {
    if (typeof inputPath !== 'string') {
      throw new TypeError('Path must be a string.');
    }

    let expanded = inputPath.trim();

    // Handle tilde expansion
    if (expanded.startsWith('~')) {
      if (expanded === '~') {
        expanded = os.homedir();
      } else if (expanded.startsWith('~' + path.sep)) {
        const pathAfterHome = expanded.slice(2); // Remove ~/ or ~\
        expanded = path.join(os.homedir(), pathAfterHome);
      } else {
        const restOfPath = expanded.slice(1);
        const separatorIndex = restOfPath.indexOf(path.sep);
        const username = separatorIndex === -1 ? restOfPath : restOfPath.slice(0, separatorIndex);
        if (username) {
          throw new Error(`Path expansion for ~${username} is not supported. Please use an absolute path or ~${path.sep}`);
        }
      }
    }

    // Resolve to the absolute path relative to the current working directory
    return path.resolve(expanded);
  }

  /**
   * Get configured IDEs from existing installation
   * @param {string} directory - Installation directory
   * @returns {Array} List of configured IDEs
   */
  async getConfiguredIdes(directory) {
    const { ExistingInstall } = require('./core/existing-install');
    const { Installer } = require('./core/installer');
    const installer = new Installer();
    const { bmadDir } = await installer.findBmadDir(directory);
    const existingInstall = await ExistingInstall.detect(bmadDir);
    return existingInstall.ides;
  }

  /**
   * Validate custom content path synchronously
   * @param {string} input - User input path
   * @returns {string|undefined} Error message or undefined if valid
   */
  validateCustomContentPathSync(input) {
    // Allow empty input to cancel
    if (!input || input.trim() === '') {
      return; // Allow empty to exit
    }

    try {
      // Expand the path
      const expandedPath = this.expandUserPath(input.trim());

      // Check if path exists
      if (!fs.pathExistsSync(expandedPath)) {
        return 'Path does not exist';
      }

      // Check if it's a directory
      const stat = fs.statSync(expandedPath);
      if (!stat.isDirectory()) {
        return 'Path must be a directory';
      }

      // Check for module.yaml in the root
      const moduleYamlPath = path.join(expandedPath, 'module.yaml');
      if (!fs.pathExistsSync(moduleYamlPath)) {
        return 'Directory must contain a module.yaml file in the root';
      }

      // Try to parse the module.yaml to get the module ID
      try {
        const yaml = require('yaml');
        const content = fs.readFileSync(moduleYamlPath, 'utf8');
        const moduleData = yaml.parse(content);
        if (!moduleData.code) {
          return 'module.yaml must contain a "code" field for the module ID';
        }
      } catch (error) {
        return 'Invalid module.yaml file: ' + error.message;
      }

      return; // Valid
    } catch (error) {
      return 'Error validating path: ' + error.message;
    }
  }

  /**
   * Prompt user for custom content source location
   * @returns {Object} Custom content configuration
   */
  async promptCustomContentSource() {
    const customContentConfig = { hasCustomContent: true, sources: [] };

    // Keep asking for more sources until user is done
    while (true) {
      // First ask if user wants to add another module or continue
      if (customContentConfig.sources.length > 0) {
        const action = await prompts.select({
          message: 'Would you like to:',
          choices: [
            { name: 'Add another custom module', value: 'add' },
            { name: 'Continue with installation', value: 'continue' },
          ],
          default: 'continue',
        });

        if (action === 'continue') {
          break;
        }
      }

      let sourcePath;
      let isValid = false;

      while (!isValid) {
        // Use sync validation because @clack/prompts doesn't support async validate
        const inputPath = await prompts.text({
          message: 'Path to custom module folder (press Enter to skip):',
          validate: (input) => this.validateCustomContentPathSync(input),
        });

        // If user pressed Enter without typing anything, exit the loop
        if (!inputPath || inputPath.trim() === '') {
          // If we have no modules yet, return false for no custom content
          if (customContentConfig.sources.length === 0) {
            return { hasCustomContent: false };
          }
          return customContentConfig;
        }

        sourcePath = this.expandUserPath(inputPath);
        isValid = true;
      }

      // Read module.yaml to get module info
      const yaml = require('yaml');
      const moduleYamlPath = path.join(sourcePath, 'module.yaml');
      const moduleContent = await fs.readFile(moduleYamlPath, 'utf8');
      const moduleData = yaml.parse(moduleContent);

      // Add to sources
      customContentConfig.sources.push({
        path: sourcePath,
        id: moduleData.code,
        name: moduleData.name || moduleData.code,
      });

      await prompts.log.success(`Confirmed local custom module: ${moduleData.name || moduleData.code}`);
    }

    // Ask if user wants to add these to the installation
    const shouldInstall = await prompts.confirm({
      message: `Install these ${customContentConfig.sources.length} custom modules?`,
      default: true,
    });

    if (shouldInstall) {
      customContentConfig.selected = true;
      // Store paths to module.yaml files, not directories
      customContentConfig.selectedFiles = customContentConfig.sources.map((s) => path.join(s.path, 'module.yaml'));
      // Also include module IDs for installation
      customContentConfig.selectedModuleIds = customContentConfig.sources.map((s) => s.id);
    }

    return customContentConfig;
  }

  /**
   * Handle custom modules in the modify flow
   * @param {string} directory - Installation directory
   * @param {Array} selectedModules - Currently selected modules
   * @returns {Object} Result with selected custom modules and custom content config
   */
  async handleCustomModulesInModifyFlow(directory, selectedModules) {
    // Get existing installation to find custom modules
    const { existingInstall } = await this.getExistingInstallation(directory);

    // Check if there are any custom modules in cache
    const { Installer } = require('./core/installer');
    const installer = new Installer();
    const { bmadDir } = await installer.findBmadDir(directory);

    const cacheDir = path.join(bmadDir, '_config', 'custom');
    const cachedCustomModules = [];

    if (await fs.pathExists(cacheDir)) {
      const entries = await fs.readdir(cacheDir, { withFileTypes: true });
      for (const entry of entries) {
        if (entry.isDirectory()) {
          const moduleYamlPath = path.join(cacheDir, entry.name, 'module.yaml');
          if (await fs.pathExists(moduleYamlPath)) {
            const yaml = require('yaml');
            const content = await fs.readFile(moduleYamlPath, 'utf8');
            const moduleData = yaml.parse(content);

            cachedCustomModules.push({
              id: entry.name,
              name: moduleData.name || entry.name,
              description: moduleData.description || 'Custom module from cache',
              checked: selectedModules.includes(entry.name),
              fromCache: true,
            });
          }
        }
      }
    }

    const result = {
      selectedCustomModules: [],
      customContentConfig: { hasCustomContent: false },
    };

    // Ask user about custom modules
    await prompts.log.info('Custom Modules');
    if (cachedCustomModules.length > 0) {
      await prompts.log.message('Found custom modules in your installation:');
    } else {
      await prompts.log.message('No custom modules currently installed.');
    }

    // Build choices dynamically based on whether we have existing modules
    const choices = [];
    if (cachedCustomModules.length > 0) {
      choices.push(
        { name: 'Keep all existing custom modules', value: 'keep' },
        { name: 'Select which custom modules to keep', value: 'select' },
        { name: 'Add new custom modules', value: 'add' },
        { name: 'Remove all custom modules', value: 'remove' },
      );
    } else {
      choices.push({ name: 'Add new custom modules', value: 'add' }, { name: 'Cancel (no custom modules)', value: 'cancel' });
    }

    const customAction = await prompts.select({
      message: cachedCustomModules.length > 0 ? 'Manage custom modules?' : 'Add custom modules?',
      choices: choices,
      default: cachedCustomModules.length > 0 ? 'keep' : 'add',
    });

    switch (customAction) {
      case 'keep': {
        // Keep all existing custom modules
        result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
        await prompts.log.message(`Keeping ${result.selectedCustomModules.length} custom module(s)`);
        break;
      }

      case 'select': {
        // Let user choose which to keep
        const selectChoices = cachedCustomModules.map((m) => ({
          name: `${m.name} (${m.id})`,
          value: m.id,
          checked: m.checked,
        }));

        // Add "None / I changed my mind" option at the end
        const choicesWithSkip = [
          ...selectChoices,
          {
            name: '⚠ None / I changed my mind - keep no custom modules',
            value: '__NONE__',
            checked: false,
          },
        ];

        const keepModules = await prompts.multiselect({
          message: 'Select custom modules to keep (use arrow keys, space to toggle):',
          choices: choicesWithSkip,
          required: true,
        });

        // If user selected both "__NONE__" and other modules, honor the "None" choice
        if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) {
          await prompts.log.warn('"None / I changed my mind" was selected, so no custom modules will be kept.');
          result.selectedCustomModules = [];
        } else {
          // Filter out the special '__NONE__' value
          result.selectedCustomModules = keepModules ? keepModules.filter((m) => m !== '__NONE__') : [];
        }
        break;
      }

      case 'add': {
        // By default, keep existing modules when adding new ones
        // User chose "Add new" not "Replace", so we assume they want to keep existing
        result.selectedCustomModules = cachedCustomModules.map((m) => m.id);

        // Then prompt for new ones (reuse existing method)
        const newCustomContent = await this.promptCustomContentSource();
        if (newCustomContent.hasCustomContent && newCustomContent.selected) {
          result.selectedCustomModules.push(...newCustomContent.selectedModuleIds);
          result.customContentConfig = newCustomContent;
        }
        break;
      }

      case 'remove': {
        // Remove all custom modules
        await prompts.log.warn('All custom modules will be removed from the installation');
        break;
      }

      case 'cancel': {
        // User cancelled - no custom modules
        await prompts.log.message('No custom modules will be added');
        break;
      }
    }

    return result;
  }

  /**
   * Display module versions with update availability
   * @param {Array} modules - Array of module info objects with version info
   * @param {Array} availableUpdates - Array of available updates
   */
  async displayModuleVersions(modules, availableUpdates = []) {
    // Group modules by source
    const builtIn = modules.filter((m) => m.source === 'built-in');
    const external = modules.filter((m) => m.source === 'external');
    const custom = modules.filter((m) => m.source === 'custom');
    const unknown = modules.filter((m) => m.source === 'unknown');

    const lines = [];
    const formatGroup = (group, title) => {
      if (group.length === 0) return;
      lines.push(title);
      for (const mod of group) {
        const updateInfo = availableUpdates.find((u) => u.name === mod.name);
        const versionDisplay = mod.version || 'unknown';
        if (updateInfo) {
          lines.push(`  ${mod.name.padEnd(20)} ${versionDisplay} \u2192 ${updateInfo.latestVersion} \u2191`);
        } else {
          lines.push(`  ${mod.name.padEnd(20)} ${versionDisplay} \u2713`);
        }
      }
    };

    formatGroup(builtIn, 'Built-in Modules');
    formatGroup(external, 'External Modules (Official)');
    formatGroup(custom, 'Custom Modules');
    formatGroup(unknown, 'Other Modules');

    await prompts.note(lines.join('\n'), 'Module Versions');
  }

  /**
   * Prompt user to select which modules to update
   * @param {Array} availableUpdates - Array of available updates
   * @returns {Array} Selected module names to update
   */
  async promptUpdateSelection(availableUpdates) {
    if (availableUpdates.length === 0) {
      return [];
    }

    await prompts.log.info('Available Updates');

    const choices = availableUpdates.map((update) => ({
      name: `${update.name} (v${update.installedVersion} \u2192 v${update.latestVersion})`,
      value: update.name,
      checked: true, // Default to selecting all updates
    }));

    // Add "Update All" and "Cancel" options
    const action = await prompts.select({
      message: 'How would you like to proceed?',
      choices: [
        { name: 'Update all available modules', value: 'all' },
        { name: 'Select specific modules to update', value: 'select' },
        { name: 'Skip updates for now', value: 'skip' },
      ],
      default: 'all',
    });

    if (action === 'all') {
      return availableUpdates.map((u) => u.name);
    }

    if (action === 'skip') {
      return [];
    }

    // Allow specific selection
    const selected = await prompts.multiselect({
      message: 'Select modules to update (use arrow keys, space to toggle):',
      choices: choices,
      required: true,
    });

    return selected || [];
  }

  /**
   * Display status of all installed modules
   * @param {Object} statusData - Status data with modules, installation info, and available updates
   */
  async displayStatus(statusData) {
    const { installation, modules, availableUpdates, bmadDir } = statusData;

    // Installation info
    const infoLines = [
      `Version:       ${installation.version || 'unknown'}`,
      `Location:      ${bmadDir}`,
      `Installed:     ${new Date(installation.installDate).toLocaleDateString()}`,
      `Last Updated:  ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : 'unknown'}`,
    ];

    await prompts.note(infoLines.join('\n'), 'BMAD Status');

    // Module versions
    await this.displayModuleVersions(modules, availableUpdates);

    // Update summary
    if (availableUpdates.length > 0) {
      await prompts.log.warn(`${availableUpdates.length} update(s) available`);
      await prompts.log.message('Run \'bmad install\' and select "Quick Update" to update');
    } else {
      await prompts.log.success('All modules are up to date');
    }
  }

  /**
   * Display list of selected tools after IDE selection
   * @param {Array} selectedIdes - Array of selected IDE values
   * @param {Array} preferredIdes - Array of preferred IDE objects
   * @param {Array} allTools - Array of all tool objects
   */
  async displaySelectedTools(selectedIdes, preferredIdes, allTools) {
    if (selectedIdes.length === 0) return;

    const preferredValues = new Set(preferredIdes.map((ide) => ide.value));
    const toolLines = selectedIdes.map((ideValue) => {
      const tool = allTools.find((t) => t.value === ideValue);
      const name = tool?.name || ideValue;
      const marker = preferredValues.has(ideValue) ? ' \u2B50' : '';
      return `  \u2022 ${name}${marker}`;
    });
    await prompts.log.message('Selected tools:\n' + toolLines.join('\n'));
  }
}

module.exports = { UI };
