#!/usr/bin/env node

const inquirer = require('inquirer');
const fs = require("fs-extra");
const path = require('path');
const chalk = require('chalk');
const { spawn } = require('child_process');

const NONE = 'None'
const REACT = 'React'
const SHADCN = 'Shadcn'
const VANILLA = 'Vanilla'
const MATERIAL_UI = 'Material UI'
const ANT_DESIGN = 'Ant Design'
const QUASAR = 'Quasar'
const VUE = 'Vue'
const JS = 'JavaScript';
const TS = 'TypeScript';

const VANILLA_LIBRARIES = [
  { library: VANILLA, variants: [JS, TS] }
];

const REACT_LIBRARIES = [
  { library: SHADCN, variants: [TS] },
  { library: MATERIAL_UI, variants: [JS, TS] },
  { library: ANT_DESIGN, variants: [JS, TS] },
  { library: REACT, variants: [JS, TS] }
];

const VUE_LIBRARIES = [
  { library: QUASAR, variants: [JS, TS] },
  { library: VUE, variants: [JS, TS] }
];

const FRAMEWORKS = [
  { framework: REACT, libraries: REACT_LIBRARIES },
  { framework: VUE, libraries: VUE_LIBRARIES },
  { framework: VANILLA, libraries: VANILLA_LIBRARIES },
  { framework: NONE, libraries: [] }
];

function folderName(name) {
  return name.toLowerCase().replace(/ /g, '-');
}

function templatePath(frameworkName, templateFolderName) {
  return path.resolve(__dirname, 'templates', folderName(frameworkName), templateFolderName);
}

function template(frameworkName, libraryName, variant, native, path) {
  return { framework: frameworkName, library: libraryName, variant: variant, native: native, path: path };
}

const TEMPLATES = [
  template(NONE, undefined, undefined, false, templatePath(NONE, folderName(NONE))),
  template(NONE, undefined, undefined, true, templatePath(NONE, `${folderName(NONE)}-native`)),
  template(REACT, SHADCN, TS, false, templatePath(REACT, folderName(SHADCN))),
  template(REACT, SHADCN, TS, true, templatePath(REACT, `${folderName(SHADCN)}-native`)),
  template(REACT, MATERIAL_UI, JS, false, templatePath(REACT, folderName(MATERIAL_UI))),
  template(REACT, MATERIAL_UI, JS, true, templatePath(REACT, `${folderName(MATERIAL_UI)}-native`)),
  template(REACT, MATERIAL_UI, TS, false, templatePath(REACT, `${folderName(MATERIAL_UI)}-ts`)),
  template(REACT, MATERIAL_UI, TS, true, templatePath(REACT, `${folderName(MATERIAL_UI)}-ts-native`)),
  template(REACT, ANT_DESIGN, JS, false, templatePath(REACT, folderName(ANT_DESIGN))),
  template(REACT, ANT_DESIGN, JS, true, templatePath(REACT, `${folderName(ANT_DESIGN)}-native`)),
  template(REACT, ANT_DESIGN, TS, false, templatePath(REACT, `${folderName(ANT_DESIGN)}-ts`)),
  template(REACT, ANT_DESIGN, TS, true, templatePath(REACT, `${folderName(ANT_DESIGN)}-ts-native`)),
  template(REACT, REACT, JS, false, templatePath(REACT, folderName(REACT))),
  template(REACT, REACT, JS, true, templatePath(REACT, `${folderName(REACT)}-native`)),
  template(REACT, REACT, TS, false, templatePath(REACT, `${folderName(REACT)}-ts`)),
  template(REACT, REACT, TS, true, templatePath(REACT, `${folderName(REACT)}-ts-native`)),
  template(VANILLA, VANILLA, JS, false, templatePath(VANILLA, folderName(VANILLA))),
  template(VANILLA, VANILLA, JS, true, templatePath(VANILLA, `${folderName(VANILLA)}-native`)),
  template(VANILLA, VANILLA, TS, false, templatePath(VANILLA, `${folderName(VANILLA)}-ts`)),
  template(VANILLA, VANILLA, TS, true, templatePath(VANILLA, `${folderName(VANILLA)}-ts-native`)),
  template(VUE, QUASAR, JS, false, templatePath(VUE, folderName(QUASAR))),
  template(VUE, QUASAR, JS, true, templatePath(VUE, `${folderName(QUASAR)}-native`)),
  template(VUE, QUASAR, TS, false, templatePath(VUE, `${folderName(QUASAR)}-ts`)),
  template(VUE, QUASAR, TS, true, templatePath(VUE, `${folderName(QUASAR)}-ts-native`)),
  template(VUE, VUE, JS, false, templatePath(VUE, folderName(VUE))),
  template(VUE, VUE, JS, true, templatePath(VUE, `${folderName(VUE)}-native`)),
  template(VUE, VUE, TS, false, templatePath(VUE, `${folderName(VUE)}-ts`)),
  template(VUE, VUE, TS, true, templatePath(VUE, `${folderName(VUE)}-ts-native`)),
];

// Custom symbols and colors
const symbols = {
  question: chalk.cyan('?'),
  success: chalk.greenBright('✓'),
};

let answers = new Map();
let questions = [];
let cancelGeneration = false;

/**
 * Compares two version arrays lexicographically.
 * @param {number[]} a - The first version as [major, minor, patch].
 * @param {number[]} b - The second version as [major, minor, patch].
 * @returns {number} Negative if a < b, positive if a > b, zero if equal.
 */
function compareVersions(a, b) {
  for (let i = 0; i < 3; i++) {
    const diff = (a[i] || 0) - (b[i] || 0);
    if (diff !== 0) return diff;
  }
  return 0;
}

/**
 * Checks whether a version string satisfies a single semver constraint such as
 * `^20.20.2` or `>=24.14.1`.
 * @param {string} versionStr - The current version, e.g. `"20.20.2"`.
 * @param {string} constraint - A single constraint token.
 * @returns {boolean} True if the version satisfies the constraint.
 */
function satisfiesConstraint(versionStr, constraint) {
  const parse = (s) => s.trim().split('.').map(Number);
  const current = parse(versionStr);
  if (constraint.startsWith('^')) {
    const min = parse(constraint.slice(1));
    const max = [min[0] + 1, 0, 0];
    return compareVersions(current, min) >= 0 && compareVersions(current, max) < 0;
  }
  if (constraint.startsWith('>=')) {
    return compareVersions(current, parse(constraint.slice(2))) >= 0;
  }
  if (constraint.startsWith('>')) {
    return compareVersions(current, parse(constraint.slice(1))) > 0;
  }
  if (constraint.startsWith('<=')) {
    return compareVersions(current, parse(constraint.slice(2))) <= 0;
  }
  if (constraint.startsWith('<')) {
    return compareVersions(current, parse(constraint.slice(1))) < 0;
  }
  return compareVersions(current, parse(constraint)) === 0;
}

/**
 * Checks whether a version string satisfies a semver range that may contain
 * `||`-separated alternatives, e.g. `"^20.20.2 || ^22.22.2 || >=24.14.1"`.
 * @param {string} versionStr - The current version.
 * @param {string} range - The full semver range string.
 * @returns {boolean} True if the version satisfies at least one alternative.
 */
function satisfiesRange(versionStr, range) {
  return range.split('||').map(s => s.trim()).some(c => satisfiesConstraint(versionStr, c));
}

/**
 * Validates the running Node.js version against the `engines.node` field in
 * package.json and exits with a descriptive error if the version is
 * incompatible.
 */
function checkNodeVersion() {
  const pkg = require('./package.json');
  const requiredRange = pkg.engines && pkg.engines.node;
  if (!requiredRange) return;

  const current = process.versions.node;
  if (!satisfiesRange(current, requiredRange)) {
    console.error(chalk.redBright(`You are using Node.js ${current}, but the required version is: ${requiredRange}.`));
    console.error('Please upgrade Node.js and try again. You can download and install a compatible version from https://nodejs.org/en/download/');
    process.exit(1);
  }
}

/**
 * Prints help text listing all available options and exits.
 */
function printHelp() {
  const frameworkLines = FRAMEWORKS.map(f => {
    if (f.libraries.length === 0) {
      return `  ${f.framework}`;
    }
    const libDetails = f.libraries.map(l => {
      const variants = l.variants.length > 1 ? ` (${l.variants.join(' or ')})` : '';
      return `   - ${l.library}${variants}`;
    }).join('\n');
    return `  ${f.framework}\n${libDetails}`;
  }).join('\n');

  console.log(`
${chalk.bold('Usage:')}
  npm create mobrowser-app@latest -- [options]

${chalk.bold('Options:')}
  ${chalk.cyan('--name')} <name>         Project name (required)
  ${chalk.cyan('--framework')} <name>    Framework: ${FRAMEWORKS.map(f => f.framework).join(', ')}
  ${chalk.cyan('--library')} <name>      Component library (required for React/Vue, see below)
  ${chalk.cyan('--variant')} <name>      Language: JavaScript, TypeScript
  ${chalk.cyan('--native')} <yes|no>     Add support for native C++ modules
  ${chalk.cyan('--agents')} <yes|no>     Include AGENTS.md to guide AI coding agents (default: yes)
  ${chalk.cyan('--setup')} <yes|no>      Set up the project (install deps, run code generation)
  ${chalk.cyan('--help')}, ${chalk.cyan('-h')}            Show this help

${chalk.bold('Frameworks & libraries:')}
${frameworkLines}

${chalk.bold('Examples:')}
  npm create mobrowser-app@latest -- --name App --framework React --library Shadcn --native yes --setup yes
  npm create mobrowser-app@latest -- --name App --framework React --library "Material UI" --variant JavaScript --native no
  npm create mobrowser-app@latest -- --name App --framework Vue --library Quasar --variant TypeScript --native yes
  npm create mobrowser-app@latest -- --name App --framework None --native no
`);
}

/**
 * Parses command-line arguments into a key-value map.
 *
 * @example
 * ```bash
 * npm create mobrowser-app@latest -- --name App --framework React --library Shadcn --native yes --setup yes
 * npm create mobrowser-app@latest -- --name App --framework React --library "Material UI" --variant JavaScript --native no
 * npm create mobrowser-app@latest -- --name App --framework Vue --library Quasar --variant TypeScript --native yes
 * npm create mobrowser-app@latest -- --name App --framework None --native no
 * ```
 *
 * @returns {Map<string, string>} Map of argument names to values.
 */
function parseArgs() {
  const args = new Map();
  const argv = process.argv.slice(2);

  for (let i = 0; i < argv.length; i++) {
    if (argv[i].startsWith('--')) {
      const key = argv[i].substring(2);
      const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : '';
      args.set(key, value);
      if (value) i++; // Skip next item as it's the value.
    }
  }

  return args;
}

/**
 * Validates and normalizes command-line arguments.
 * @param {Map<string, string>} args - The parsed arguments.
 * @returns {Object|null} Object with validated answers or null if validation fails.
 */
function validateArgs(args) {
  const errors = [];

  // Validate name.
  const name = args.get('name');
  if (!name || name.trim() === '') {
    errors.push('--name is required and cannot be empty');
  }

  // Validate framework.
  const frameworkValue = args.get('framework');
  if (!frameworkValue) {
    errors.push('--framework is required');
  } else {
    const validFramework = FRAMEWORKS.find(f => f.framework === frameworkValue);
    if (!validFramework) {
      errors.push(`Invalid framework "${frameworkValue}". Valid options: ${FRAMEWORKS.map(f => f.framework).join(', ')}`);
    }
  }

  // Validate library (required if framework is not None or Vanilla).
  const libraryValue = args.get('library');
  if (frameworkValue && frameworkValue !== NONE && frameworkValue !== VANILLA) {
    if (!libraryValue) {
      errors.push('--library is required for the selected framework');
    } else {
      const frameworkObj = FRAMEWORKS.find(f => f.framework === frameworkValue);
      if (frameworkObj) {
        const validLibrary = frameworkObj.libraries.find(l => l.library === libraryValue);
        if (!validLibrary) {
          errors.push(`Invalid library "${libraryValue}" for framework "${frameworkValue}". Valid options: ${frameworkObj.libraries.map(l => l.library).join(', ')}`);
        }
      }
    }
  }

  // Validate variant (may be auto-determined).
  const variantValue = args.get('variant');
  if (frameworkValue && frameworkValue !== NONE && libraryValue) {
    const frameworkObj = FRAMEWORKS.find(f => f.framework === frameworkValue);
    if (frameworkObj) {
      const actualLibrary = frameworkValue === VANILLA ? VANILLA : libraryValue;
      const libraryObj = frameworkObj.libraries.find(l => l.library === actualLibrary);
      if (libraryObj) {
        // Check if variant is needed.
        if (libraryObj.variants.length > 1 && variantValue) {
          if (!libraryObj.variants.includes(variantValue)) {
            errors.push(`Invalid variant "${variantValue}" for library "${actualLibrary}". Valid options: ${libraryObj.variants.join(', ')}`);
          }
        }
      }
    }
  }

  // Validate native.
  const nativeValue = args.get('native');
  if (nativeValue && !['yes', 'no', 'true', 'false'].includes(nativeValue.toLowerCase())) {
    errors.push('--native must be "yes", "no", "true", or "false"');
  }

  // Validate setup.
  const setupValue = args.get('setup');
  if (setupValue && !['yes', 'no', 'true', 'false'].includes(setupValue.toLowerCase())) {
    errors.push('--setup must be "yes", "no", "true", or "false"');
  }

  // Validate agents.
  const agentsValue = args.get('agents');
  if (agentsValue && !['yes', 'no', 'true', 'false'].includes(agentsValue.toLowerCase())) {
    errors.push('--agents must be "yes", "no", "true", or "false"');
  }

  if (errors.length > 0) {
    console.error(chalk.redBright('Argument validation errors:'));
    errors.forEach(err => console.error(chalk.redBright(`  - ${err}`)));
    return null;
  }

  return {
    name: name,
    framework: frameworkValue,
    library: libraryValue,
    variant: variantValue,
    native: nativeValue ? ['yes', 'true'].includes(nativeValue.toLowerCase()) : false,
    agents: agentsValue ? ['yes', 'true'].includes(agentsValue.toLowerCase()) : true,
    setup: setupValue ? ['yes', 'true'].includes(setupValue.toLowerCase()) : false
  };
}

/**
 * Runs the script in non-interactive mode using provided arguments.
 * @param {Object} validatedArgs - The validated arguments.
 */
async function runNonInteractive(validatedArgs) {
  answers.set('name', validatedArgs.name);
  answers.set('framework', validatedArgs.framework);

  // Auto-determine library for Vanilla and None frameworks.
  if (validatedArgs.framework === VANILLA) {
    answers.set('library', VANILLA);
  } else if (validatedArgs.framework === NONE) {
    answers.set('library', undefined);
  } else {
    answers.set('library', validatedArgs.library);
  }

  // Auto-determine variant if only one is supported.
  let finalVariant = validatedArgs.variant;
  if (validatedArgs.framework !== NONE) {
    const frameworkObj = FRAMEWORKS.find(f => f.framework === validatedArgs.framework);
    const actualLibrary = validatedArgs.framework === VANILLA ? VANILLA : validatedArgs.library;
    const libraryObj = frameworkObj.libraries.find(l => l.library === actualLibrary);

    if (libraryObj.variants.length === 1) {
      finalVariant = libraryObj.variants[0];
    } else if (!finalVariant && libraryObj.variants.length > 1) {
      console.error(chalk.redBright(`--variant is required for ${actualLibrary}. Valid options: ${libraryObj.variants.join(', ')}`));
      process.exit(1);
    }
  }

  answers.set('variant', finalVariant);
  answers.set('native', validatedArgs.native);
  answers.set('agents', validatedArgs.agents);

  // Check if directory exists and is not empty.
  if (!validateProjectDir(validatedArgs.name)) {
    console.error(chalk.redBright(`The "${validatedArgs.name}" directory is not empty.`));
    console.error(chalk.redBright('Please remove it or choose a different name.'));
    process.exit(1);
  }

  // Generate project.
  try {
    generateProject(projectName(), framework(), library(), variant(), native());
    if (!agents()) {
      const projectDir = path.resolve(projectName());
      removeProjectFile(projectDir, 'AGENTS.md');
      removeProjectFile(projectDir, 'CLAUDE.md');
    }
    const cd = projectName().includes(' ') ? `"${projectName()}"` : projectName();

    if (validatedArgs.setup) {
      try {
        await installAndGenerate(projectName());
        console.log('');
        console.log(`${chalk.green('Done!')} To get started run:
  cd ${cd}
  npm run dev`);
      } catch (installError) {
        console.log('');
        console.log(`${chalk.yellow('Project created, but setup failed.')} To complete setup manually:
  cd ${cd}
  npm install
  npm run dev`);
      }
    } else {
      console.log('');
      console.log(`${chalk.green('Done!')} To get started run:
  cd ${cd}
  npm install
  npm run dev`);
    }
  } catch (error) {
    console.error(`${chalk.redBright('Failed to generate the project.')}`);
    console.error(`${chalk.redBright(error.toString())}`);
    if (fs.existsSync(path.resolve(projectName()))) {
      fs.removeSync(path.resolve(projectName()));
    }
    process.exit(1);
  }
}

/**
 * Gets the project name from the answers map.
 * @returns {string} The project name.
 */
const projectName = () => answers.get('name');

/**
 * Gets the selected framework from the answers map.
 * @returns {string} The framework name.
 */
const framework = () => answers.get('framework');

/**
 * Gets the selected component library from the answers map.
 * @returns {string} The library name.
 */
const library = () => answers.get('library');

/**
 * Gets the selected variant (JavaScript or TypeScript) from the answers map.
 * @returns {string} The variant type.
 */
const variant = () => answers.get('variant');

/**
 * Gets the native module support flag from the answers map.
 * @returns {boolean} True if native module support is enabled.
 */
const native = () => answers.get('native');

/**
 * Gets the AGENTS.md inclusion flag from the answers map.
 * @returns {boolean} True if AGENTS.md should be kept in the project.
 */
const agents = () => answers.get('agents');

// Customize inquirer theme
inquirer.ui.Prompt.prototype.render = function () {
  // Store original render
  if (!this._originalRender) {
    this._originalRender = inquirer.ui.Prompt.prototype.render;
  }
  return this._originalRender.apply(this, arguments);
};

/**
 * Creates a confirmation question for overwriting an existing directory.
 * @param {string} projectName - The name of the project directory.
 * @returns {Object} The inquirer question object.
 */
const confirmationQuestion = (projectName) => {
  return {
    name: 'confirm',
    type: 'confirm',
    message: `The \"${projectName}\" directory is not empty. Do you want to overwrite it?`,
    default: false,
    onAnswer: answer => {
      if (answer.confirm) {
        fs.removeSync(path.resolve(projectName));
      } else {
        clearQuestions();
        cancelGeneration = true;
        console.log(`${chalk.redBright('Operation cancelled')}`);
      }
    }
  }
}

/**
 * Clears the questions array.
 */
function clearQuestions() {
  questions = [];
}

/**
 * Recursively prompts the user with questions from the questions array.
 * Processes each answer and displays a success message with custom formatting.
 * @returns {Promise} A promise that resolves when all questions are answered.
 */
function askQuestions() {
  const question = questions.shift();
  if (question) {
    // Set custom prefix for the question
    if (!question.prefix) {
      question.prefix = symbols.question;
    }

    return inquirer
      .prompt(question)
      .then(answer => {
        const value = answer[question.name];

        // Only display if the value is not undefined (question was actually answered)
        if (value !== undefined) {
          // Clear the last line and show success symbol
          process.stdout.write('\x1b[1A'); // Move cursor up one line
          process.stdout.write('\x1b[2K'); // Clear the line

          const displayValue = typeof value === 'boolean' ? (value ? 'Yes' : 'No') : value;
          console.log(`${symbols.success} ${chalk.bold(question.message)} ${chalk.green(displayValue)}`);
        }

        if (value !== undefined) {
          answers.set(question.name, value);
        }
        if (question.onAnswer) {
          question.onAnswer(answer);
        }
        return askQuestions();
      });
  }
}

// Ask for project name
questions.push({
  name: 'name',
  type: 'input',
  message: 'Project name:',
  default: 'App',
  validate: (input) => {
    if (input.trim() === '') {
      return 'The project name cannot be empty. Please enter a valid project name.';
    }
    if (!validateProjectDir(input)) {
      questions.unshift(confirmationQuestion(input));
    }
    return true;
  },
});

// Ask for a framework
questions.push({
  name: 'framework',
  type: 'list',
  message: 'Select a framework:',
  choices: FRAMEWORKS.map(it => it.framework)
});

// Ask for a component library
questions.push({
  name: 'library',
  type: 'list',
  message: 'Select a component library:',
  when: () => {
    if (framework() === NONE) {
      return false;
    }
    if (framework() === VANILLA) {
      answers.set('library', VANILLA);
      return false;
    }
    return true;
  },
  choices: () => {
    return FRAMEWORKS.find(it => it.framework === framework()).libraries.map(it => it.library);
  },
});

// Ask for a variant
questions.push({
  name: 'variant',
  type: 'list',
  message: 'Select a variant:',
  choices: [JS, TS],
  when: () => {
    if (framework() === NONE) {
      return false;
    }
    const frameworkObject = FRAMEWORKS.find(it => it.framework === framework());
    const libraryObject = frameworkObject.libraries.find(it => it.library === library());
    // If only one variant is supported, auto-set it
    if (libraryObject.variants.length === 1) {
      answers.set('variant', libraryObject.variants[0]);
      return false;
    }
    return true;
  },
});

// Ask for the native C++ module support
questions.push({
  name: 'native',
  type: 'confirm',
  message: 'Add a native C++ module?',
  default: false,
});

// Ask whether to include AGENTS.md to guide AI coding agents
questions.push({
  name: 'agents',
  type: 'confirm',
  message: 'Include AGENTS.md to guide AI coding agents on how to use MōBrowser?',
  default: true,
});

// Ask whether to set up the project so it is ready to open in an IDE
questions.push({
  name: 'setup',
  type: 'confirm',
  message: 'Install dependencies and set up the project?',
  default: true,
});

checkNodeVersion();

// Check if command-line arguments are provided.
const cliArgs = parseArgs();
const argv = process.argv.slice(2);
if (argv.includes('--help') || argv.includes('-h')) {
  printHelp();
  process.exit(0);
}
if (cliArgs.size > 0) {
  // Non-interactive mode.
  const validatedArgs = validateArgs(cliArgs);
  if (validatedArgs) {
    runNonInteractive(validatedArgs).catch((err) => {
      console.error(chalk.redBright(err.toString()));
      process.exit(1);
    });
  } else {
    process.exit(1);
  }
} else {
  // Interactive mode.
  askQuestions()
    .then(async () => {
      if (!cancelGeneration) {
        generateProject(projectName(), framework(), library(), variant(), native());
        if (!agents()) {
          const projectDir = path.resolve(projectName());
          removeProjectFile(projectDir, 'AGENTS.md');
          removeProjectFile(projectDir, 'CLAUDE.md');
        }
        const cd = projectName().includes(' ') ? `"${projectName()}"` : projectName();

        if (answers.get('setup')) {
          try {
            await installAndGenerate(projectName());
            console.log('');
            console.log(`${chalk.green('Done!')} To get started run:
  cd ${cd}
  npm run dev`);
          } catch (error) {
            console.log('');
            console.log(`${chalk.yellow('Project created, but setup failed.')} To finish setup and get started, run:
  cd ${cd}
  npm install
  npm run dev`);
          }
        } else {
          console.log('');
          console.log(`${chalk.green('Done!')} To get started run:
  cd ${cd}
  npm install
  npm run dev`);
        }
      }
    })
    .catch(error => {
      console.error(`${chalk.redBright('Failed to generate the project.')}`);
      console.error(`${chalk.redBright(error.toString())}`);
      fs.removeSync(path.resolve(projectName()));
    });
}

/**
 * Validates whether a project directory is empty or doesn't exist.
 * @param {string} projectName - The name of the project directory to validate.
 * @returns {boolean} True if the directory is empty or doesn't exist, false otherwise.
 */
function validateProjectDir(projectName) {
  if (!projectName) {
    return true;
  }
  const projectPath = path.resolve(projectName);
  return !(fs.existsSync(projectPath) && fs.readdirSync(projectPath).length > 0);
}

/**
 * Constructs the path to the appropriate project template directory.
 * @param {string} framework - The selected framework.
 * @param {string} library - The selected component library.
 * @param {string} variant - The selected variant (JavaScript or TypeScript).
 * @param {boolean} native - Whether native C++ module support is enabled.
 * @returns {string} The absolute path to the template directory.
 */
function getTemplatePath(framework, library, variant, native) {
  const template = TEMPLATES.find(
    it => it.framework === framework &&
      it.library === library &&
      it.variant === variant &&
      it.native === native);
  if (!template) {
    throw new Error(`Template not found for framework: ${framework}, library: ${library}, variant: ${variant}, native: ${native}`);
  }
  return template.path;
}

/**
 * Generates a new MoBrowser project from the selected template.
 * Creates the project directory, copies template files, and updates placeholder values.
 * @param {string} projectName - The name of the project.
 * @param {string} framework - The selected framework.
 * @param {string} library - The selected component library.
 * @param {string} variant - The selected variant (JavaScript or TypeScript).
 * @param {boolean} native - Whether native C++ module support is enabled.
 */
function generateProject(projectName, framework, library, variant, native) {
  const projectDir = path.resolve(projectName);

  // Create the project directory if it doesn't exist.
  if (!fs.existsSync(projectDir)) {
    fs.mkdirSync(projectDir);
  }

  // Select and copy the template project files.
  fs.copySync(getTemplatePath(framework, library, variant, native), projectDir);

  // Rename gitignore to .gitignore (npm pack excludes .gitignore, so we store as gitignore in templates)
  const gitignorePath = path.join(projectDir, 'gitignore');
  if (fs.existsSync(gitignorePath)) {
    fs.renameSync(gitignorePath, path.join(projectDir, '.gitignore'));
  }

  // Read the latest available SDK version from the 'create-mobrowser-app/package.json' file.
  // The create-mobrowser-app and SDK versions are the same. Every time
  // we release a new version of the SDK, we release a new version of
  // the create-mobrowser-app scaffolding tool.
  const packageJsonPath = path.resolve(__dirname, 'package.json');
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
  const version = packageJson.version;

  // Update the placeholder values in the project files.
  updatePlaceholderValues(path.resolve(projectDir, 'package.json'), projectName, version);
  updatePlaceholderValues(path.resolve(projectDir, 'mobrowser.conf.json'), projectName, version);
}

/**
 * Updates placeholder values in a file with actual project values.
 * Replaces ${APP_NAME}, ${BUNDLE_APP_NAME}, and ${VERSION} placeholders.
 * @param {string} filePath - The path to the file to update.
 * @param {string} appName - The application name.
 * @param {string} version - The SDK version number.
 */
function updatePlaceholderValues(filePath, appName, version) {
  const updatedContent = fs.readFileSync(filePath, 'utf8')
    .replaceAll('${APP_NAME}', appName)
    .replaceAll('${PACKAGE_ID}', appName.replaceAll(/\s+/g, ''))
    .replaceAll('${BUNDLE_APP_NAME}', appName.replaceAll(/\s+/g, '-'))
    .replaceAll('${VERSION}', `${version}`);
  fs.writeFileSync(filePath, updatedContent);
}

/**
 * Removes a file from the project directory if it exists.
 * @param {string} projectDir - The absolute path to the project directory.
 * @param {string} filename - The name of the file to remove.
 */
function removeProjectFile(projectDir, filename) {
  const filePath = path.join(projectDir, filename);
  if (fs.existsSync(filePath)) {
    fs.removeSync(filePath);
  }
}

/**
 * Runs a command while showing an animated braille spinner with elapsed time.
 * Streams the child process output to the terminal. On first output, clears the
 * spinner line so logs do not get garbled.
 *
 * @param {string} message - Base message (e.g. "Installing dependencies").
 * @param {string} command - The shell command to run.
 * @param {Object} options - Options passed to spawn (e.g. cwd).
 */
function runWithProgress(message, command, options = {}) {
  const startTime = Date.now();
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
  let i = 0;
  const stream = process.stderr.isTTY ? process.stderr : process.stdout;
  let spinnerActive = true;

  const elapsedTime = () => {
    const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
    return chalk.dim(`[${elapsed}s]`);
  };

  const clearStatusLine = () => {
    stream.write('\r\x1b[0K');
  };

  const interval = setInterval(() => {
    if (spinnerActive) {
      stream.write(`\r${chalk.yellow(frames[i % frames.length])} ${message}... ${elapsedTime()}`);
    }
    i++;
  }, 80);

  const pipe = (childStream, targetStream) => {
    childStream.on('data', (chunk) => {
      if (spinnerActive) {
        spinnerActive = false;
        clearStatusLine();
      }
      targetStream.write(chunk);
    });
  };

  return new Promise((resolve, reject) => {
    const child = spawn(command, [], {
      ...options,
      shell: true,
      stdio: ['inherit', 'pipe', 'pipe'],
    });

    if (child.stdout) pipe(child.stdout, process.stdout);
    if (child.stderr) pipe(child.stderr, process.stderr);

    child.on('close', (code, signal) => {
      clearInterval(interval);
      clearStatusLine();
      if (code === 0) {
        console.log(`${chalk.green('✓')} ${message} ${elapsedTime()}`);
        resolve();
      } else {
        stream.write(`${chalk.red('✗')} ${message} ${elapsedTime()}\n`);
        const err = signal
          ? new Error(`Command failed with signal ${signal}`)
          : new Error(`Command failed with exit code ${code}`);
        err.code = code;
        err.signal = signal;
        reject(err);
      }
    });

    child.on('error', (err) => {
      clearInterval(interval);
      clearStatusLine();
      reject(err);
    });
  });
}

/**
 * Runs a command with stdin/stdout/stderr inherited from this process.
 * Uses a real TTY when available so nested CLIs (e.g. mobrowser gen) can show
 * spinners and colors; piped stdio would make child stdout non-TTY.
 *
 * @param {string} command - The shell command to run.
 * @param {Object} options - Options passed to spawn (e.g. cwd).
 */
function run(command, options = {}) {
  return new Promise((resolve, reject) => {
    const child = spawn(command, [], {
      ...options,
      shell: true,
      stdio: 'inherit',
    });

    child.on('close', (code, signal) => {
      if (code === 0) {
        resolve();
      } else {
        const err = signal
          ? new Error(`Command failed with signal ${signal}`)
          : new Error(`Command failed with exit code ${code}`);
        err.code = code;
        err.signal = signal;
        reject(err);
      }
    });

    child.on('error', (err) => {
      reject(err);
    });
  });
}

/**
 * Installs npm dependencies and generates TypeScript and, for native
 * templates, C++ code from Protobuf definitions in the given project
 * directory.
 *
 * @param {string} name - The project name (used to resolve the project directory).
 * @throws {Error} If `npm install` or code generation fails.
 */
async function installAndGenerate(name) {
  const projectDir = path.resolve(name);
  console.log('');
  await runWithProgress('Installing dependencies', 'npm install', { cwd: projectDir });
  await run('npm run gen', { cwd: projectDir });
}
