const is = require('@sindresorhus/is');
const safe = require('safe-regex');
const options = require('./definitions').getOptions();
const { resolveConfigPresets } = require('./presets');
const {
  hasValidSchedule,
  hasValidTimezone,
} = require('../workers/branch/schedule');

let optionTypes;

module.exports = {
  validateConfig,
};

async function validateConfig(config, isPreset, parentPath) {
  if (!optionTypes) {
    optionTypes = {};
    options.forEach(option => {
      optionTypes[option.name] = option.type;
    });
  }
  let errors = [];
  let warnings = [];

  function getDeprecationMessage(option) {
    const deprecatedOptions = {
      branchName: `Direct editing of branchName is now deprecated. Please edit branchPrefix, managerBranchPrefix, or branchTopic instead`,
      commitMessage: `Direct editing of commitMessage is now deprecated. Please edit commitMessage's subcomponents instead.`,
      prTitle: `Direct editing of prTitle is now deprecated. Please edit commitMessage subcomponents instead as they will be passed through to prTitle.`,
    };
    return deprecatedOptions[option];
  }

  function isIgnored(key) {
    const ignoredNodes = [
      '$schema',
      'prBanner',
      'depType',
      'npmToken',
      'packageFile',
      'forkToken',
      'repository',
      'vulnerabilityAlertsOnly',
      'copyLocalLibs', // deprecated - functinoality is now enabled by default
      'prBody', // deprecated
    ];
    return ignoredNodes.includes(key);
  }

  for (const [key, val] of Object.entries(config)) {
    const currentPath = parentPath ? `${parentPath}.${key}` : key;
    if (
      !isIgnored(key) && // We need to ignore some reserved keys
      !is.function(val) // Ignore all functions
    ) {
      if (getDeprecationMessage(key)) {
        warnings.push({
          depName: 'Deprecation Warning',
          message: getDeprecationMessage(key),
        });
      }
      if (!optionTypes[key]) {
        errors.push({
          depName: 'Configuration Error',
          message: `Invalid configuration option: \`${currentPath}\``,
        });
      } else if (key === 'schedule') {
        const [validSchedule, errorMessage] = hasValidSchedule(val);
        if (!validSchedule) {
          errors.push({
            depName: 'Configuration Error',
            message: `Invalid ${currentPath}: \`${errorMessage}\``,
          });
        }
      } else if (key === 'timezone' && val !== null) {
        const [validTimezone, errorMessage] = hasValidTimezone(val);
        if (!validTimezone) {
          errors.push({
            depName: 'Configuration Error',
            message: `${currentPath}: ${errorMessage}`,
          });
        }
      } else if (val != null) {
        const type = optionTypes[key];
        if (type === 'boolean') {
          if (val !== true && val !== false) {
            errors.push({
              depName: 'Configuration Error',
              message: `Configuration option \`${currentPath}\` should be boolean. Found: ${JSON.stringify(
                val
              )} (${typeof val})`,
            });
          }
        } else if (type === 'list' && val) {
          if (!is.array(val)) {
            errors.push({
              depName: 'Configuration Error',
              message: `Configuration option \`${currentPath}\` should be a list (Array)`,
            });
          } else {
            for (const [subIndex, subval] of val.entries()) {
              if (is.object(subval)) {
                const subValidation = await module.exports.validateConfig(
                  subval,
                  isPreset,
                  `${currentPath}[${subIndex}]`
                );
                warnings = warnings.concat(subValidation.warnings);
                errors = errors.concat(subValidation.errors);
              }
            }
            if (key === 'extends') {
              for (const subval of val) {
                if (is.string(subval) && subval.match(/^:timezone(.+)$/)) {
                  const [, timezone] = subval.match(/^:timezone\((.+)\)$/);
                  const [validTimezone, errorMessage] = hasValidTimezone(
                    timezone
                  );
                  if (!validTimezone) {
                    errors.push({
                      depName: 'Configuration Error',
                      message: `${currentPath}: ${errorMessage}`,
                    });
                  }
                }
              }
            }

            const selectors = [
              'paths',
              'depTypeList',
              'packageNames',
              'packagePatterns',
              'excludePackageNames',
              'excludePackagePatterns',
              'updateTypes',
            ];
            if (key === 'packageRules') {
              for (const packageRule of val) {
                let hasSelector = false;
                if (is.object(packageRule)) {
                  const resolvedRule = await resolveConfigPresets(packageRule);
                  for (const pKey of Object.keys(resolvedRule)) {
                    if (selectors.includes(pKey)) {
                      hasSelector = true;
                    }
                  }
                  if (!hasSelector) {
                    const message = `${currentPath}: Each packageRule must contain at least one selector (${selectors.join(
                      ', '
                    )}). If you wish for configuration to apply to all packages, it is not necessary to place it inside a packageRule at all.`;
                    errors.push({
                      depName: 'Configuration Error',
                      message,
                    });
                  }
                } else {
                  errors.push({
                    depName: 'Configuration Error',
                    message: `${currentPath} must contain JSON objects`,
                  });
                }
              }
            }
            if (
              (key === 'packagePatterns' || key === 'excludePackagePatterns') &&
              !(val && val.length === 1 && val[0] === '*')
            ) {
              try {
                RegExp(val);
                if (!safe(val)) {
                  errors.push({
                    depName: 'Configuration Error',
                    message: `Unsafe regExp for ${currentPath}: \`${val}\``,
                  });
                }
              } catch (e) {
                errors.push({
                  depName: 'Configuration Error',
                  message: `Invalid regExp for ${currentPath}: \`${val}\``,
                });
              }
            }
            if (key === 'fileMatch') {
              try {
                for (const fileMatch of val) {
                  RegExp(fileMatch);
                  if (!safe(fileMatch)) {
                    errors.push({
                      depName: 'Configuration Error',
                      message: `Unsafe regExp for ${currentPath}: \`${fileMatch}\``,
                    });
                  }
                }
              } catch (e) {
                errors.push({
                  depName: 'Configuration Error',
                  message: `Invalid regExp for ${currentPath}: \`${val}\``,
                });
              }
            }
            if (
              (selectors.includes(key) || key === 'matchCurrentVersion') &&
              !(parentPath && parentPath.match(/p.*Rules\[\d+\]$/)) && // Inside a packageRule
              (parentPath || !isPreset) // top level in a preset
            ) {
              errors.push({
                depName: 'Configuration Error',
                message: `${currentPath}: ${key} should be inside a \`packageRule\` only`,
              });
            }
          }
        } else if (type === 'string') {
          if (!is.string(val)) {
            errors.push({
              depName: 'Configuration Error',
              message: `Configuration option \`${currentPath}\` should be a string`,
            });
          }
        } else if (type === 'json') {
          if (is.object(val)) {
            const subValidation = await module.exports.validateConfig(
              val,
              isPreset,
              currentPath
            );
            warnings = warnings.concat(subValidation.warnings);
            errors = errors.concat(subValidation.errors);
          } else {
            errors.push({
              depName: 'Configuration Error',
              message: `Configuration option \`${currentPath}\` should be a json object`,
            });
          }
        }
      }
    }
  }
  function sortAll(a, b) {
    if (a.depName === b.depName) {
      return a.message > b.message;
    }
    // istanbul ignore next
    return a.depName > b.depName;
  }
  errors.sort(sortAll);
  warnings.sort(sortAll);
  return { errors, warnings };
}