'use strict';

const os = require('os');
const path = require('path');
const fse = require('fs-extra');
const _ = require('lodash');
const chalk = require('chalk');
const semver = require('semver');
const { getTemplatePackageInfo, downloadNpmTemplate } = require('./fetch-npm-template');

// Specify all the files and directories a template can have
const allowFile = Symbol();
const allowChildren = Symbol();
const allowedTemplateContents = {
  'README.md': allowFile,
  '.env.example': allowFile,
  'package.json': allowFile,
  src: allowChildren,
  data: allowChildren,
  database: allowChildren,
  public: allowChildren,
  scripts: allowChildren,
};

/**
 * Merge template with new project being created
 * @param {string} scope  project creation params
 * @param {string} rootPath  project path
 */
module.exports = async function mergeTemplate(scope, rootPath) {
  let templatePath;
  let templateParentPath;
  let templatePackageInfo = {};
  const isLocalTemplate = ['./', '../'].some(filePrefix => scope.template.startsWith(filePrefix));

  if (isLocalTemplate) {
    // Template is a local directory
    console.log('Installing local template.');
    templatePath = path.resolve(rootPath, '..', scope.template);
  } else {
    // Template should be an npm package. Fetch template info
    templatePackageInfo = await getTemplatePackageInfo(scope.template);
    console.log(`Installing ${chalk.yellow(templatePackageInfo.name)} template.`);

    // Download template repository to a temporary directory
    templateParentPath = await fse.mkdtemp(path.join(os.tmpdir(), 'strapi-'));
    templatePath = downloadNpmTemplate(templatePackageInfo, templateParentPath);
  }

  // Make sure the template is compatible with this version of strapi
  checkTemplateCompat(templatePath, scope.strapiVersion);

  // Make sure the downloaded template matches the required format
  const templateConfig = await checkTemplateRootStructure(templatePath, scope);
  await checkTemplateContentsStructure(path.resolve(templatePath, 'template'));

  // Merge contents of the template in the project
  await mergePackageJSON({ rootPath, templateConfig, templatePackageInfo });
  await mergeFilesAndDirectories(rootPath, templatePath);

  // Delete the template directory if it was downloaded
  if (!isLocalTemplate) {
    await fse.remove(templateParentPath);
  }
};

/**
 * Make sure the template is compatible with a specific Strapi version
 * @param {string} templatePath - Where the template is installed
 * @param {string} strapiVersion - Strapi version of the app being created
 */
function checkTemplateCompat(templatePath, strapiVersion) {
  const templateJSON = require(path.resolve(templatePath, 'template.json'));
  const compatibleStrapiConstraint = templateJSON.strapi.supportedVersion;

  // Make sure the Strapi compatibility constraint is set
  if (compatibleStrapiConstraint == null) {
    throw new Error("This template does not specify the Strapi versions it's compatible with");
  }

  // Check that the range is set using proper semver
  const validCompatibleStrapiRange = semver.validRange(compatibleStrapiConstraint);
  if (!validCompatibleStrapiRange) {
    throw new Error(
      'Please use a valid semver constraint to specify which Strapi versions this template is compatible with'
    );
  }

  // Check if the template is compatible with this Strapi version
  const isCompatible = semver.satisfies(strapiVersion, validCompatibleStrapiRange);
  if (!isCompatible) {
    throw new Error(`
      This template is not compatible with Strapi version ${strapiVersion}.
      It will only work with Strapi versions in the constraint ${chalk.green(
        JSON.stringify(compatibleStrapiConstraint)
      )}.
      Try using a different Strapi version, or a different version of this template instead.
    `);
  }
}

/**
 * Make sure the template has the required top-level structure
 * @param {string} templatePath - Path of the locally downloaded template
 * @returns {Object} - The template config object
 */
async function checkTemplateRootStructure(templatePath) {
  // Make sure the root of the repo has a template.json file
  const templateJsonPath = path.join(templatePath, 'template.json');
  if (!fse.existsSync(templateJsonPath)) {
    throw new Error(`A template must have a ${chalk.green('template.json')} root file`);
  }
  if (!fse.statSync(templateJsonPath).isFile()) {
    throw new Error(`A template's ${chalk.green('template.json')} must be a file`);
  }

  const templateConfig = require(templateJsonPath);

  // Make sure the root of the repo has a template folder
  const templateDirPath = path.join(templatePath, 'template');
  try {
    const stat = await fse.stat(templateDirPath);
    if (!stat.isDirectory()) {
      throw Error(`A template must have a root ${chalk.green('template/')} directory`);
    }
  } catch (error) {
    if (error.code === 'ENOENT') {
      throw Error(`A template must have a root ${chalk.green('template/')} directory`);
    }

    throw error;
  }

  return templateConfig;
}

/**
 * Traverse template tree to make sure each file and folder is allowed
 * @param {string} templateContentsPath
 */
async function checkTemplateContentsStructure(templateContentsPath) {
  // Recursively check if each item in a directory is allowed
  const checkPathContents = (pathToCheck, parents) => {
    const contents = fse.readdirSync(pathToCheck);
    contents.forEach(item => {
      const nextParents = [...parents, item];
      const matchingTreeValue = _.get(allowedTemplateContents, nextParents);

      // Treat files and directories separately
      const itemPath = path.resolve(pathToCheck, item);
      const isDirectory = fse.statSync(itemPath).isDirectory();

      if (matchingTreeValue === undefined) {
        // Unknown paths are forbidden
        throw Error(
          `Illegal template structure, unknown path ${chalk.green(nextParents.join('/'))}`
        );
      }

      if (matchingTreeValue === allowFile) {
        if (!isDirectory) {
          // All good, the file is allowed
          return;
        }
        throw Error(
          `Illegal template structure, expected a file and got a directory at ${chalk.green(
            nextParents.join('/')
          )}`
        );
      }

      if (isDirectory) {
        if (matchingTreeValue === allowChildren) {
          // All children are allowed
          return;
        }
        // Check if the contents of the directory are allowed
        checkPathContents(itemPath, nextParents);
      } else {
        throw Error(
          `Illegal template structure, unknow file ${chalk.green(nextParents.join('/'))}`
        );
      }
    });
  };

  checkPathContents(templateContentsPath, []);
}

/**
 * Merge the template's template.json into the Strapi project's package.json
 * @param {Object} config
 * @param {string} config.rootPath
 * @param {string} config.templateConfig
 * @param {Object} config.templatePackageInfo - Info about the template's package on npm
 * @param {Object} config.templatePackageInfo.name - The name of the template's package on npm
 * @param {Object} config.templatePackageInfo.version - The name of the template's package on npm
 */
async function mergePackageJSON({ rootPath, templateConfig, templatePackageInfo }) {
  // Import the package.json as an object
  const packageJSON = require(path.resolve(rootPath, 'package.json'));

  if (!templateConfig.package) {
    // Nothing to overwrite
    return;
  }

  // Make sure the template.json doesn't overwrite the UUID
  if (templateConfig.package.strapi && templateConfig.package.strapi.uuid) {
    throw Error('A template cannot overwrite the Strapi UUID');
  }

  // Use lodash to deeply merge them
  const mergedConfig = _.merge(packageJSON, templateConfig.package);

  // Add starter info to package.json
  if (templatePackageInfo.name) {
    _.set(mergedConfig, 'strapi.template', templatePackageInfo.name);
  }

  // Save the merged config as the new package.json
  const packageJSONPath = path.join(rootPath, 'package.json');
  await fse.writeJSON(packageJSONPath, mergedConfig, { spaces: 2 });
}

// Merge all allowed files and directories
async function mergeFilesAndDirectories(rootPath, templatePath) {
  const templateDir = path.join(templatePath, 'template');
  await fse.copy(templateDir, rootPath, { overwrite: true, recursive: true });
}

@@ -7791,15 +7791,16 @@
 Add 
-star
+templa
 te
-r
  inf

Update packages/generators/app/lib/utils/merge-template.js