"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.AspectsMerger = void 0;
function _envs() {
  const data = require("@teambit/envs");
  _envs = function () {
    return data;
  };
  return data;
}
function _dependencyResolver() {
  const data = require("@teambit/dependency-resolver");
  _dependencyResolver = function () {
    return data;
  };
  return data;
}
function _extensionData() {
  const data = require("@teambit/legacy/dist/consumer/config/extension-data");
  _extensionData = function () {
    return data;
  };
  return data;
}
function _lodash() {
  const data = require("lodash");
  _lodash = function () {
    return data;
  };
  return data;
}
function _mergeConfigConflict() {
  const data = require("./exceptions/merge-config-conflict");
  _mergeConfigConflict = function () {
    return data;
  };
  return data;
}
function _workspace() {
  const data = require("./workspace");
  _workspace = function () {
    return data;
  };
  return data;
}
function _mergeConflictFile() {
  const data = require("./merge-conflict-file");
  _mergeConflictFile = function () {
    return data;
  };
  return data;
}
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
class AspectsMerger {
  constructor(workspace, harmony) {
    this.workspace = workspace;
    this.harmony = harmony;
    _defineProperty(this, "mergeConflictFile", void 0);
    _defineProperty(this, "mergeConfigDepsResolverDataCache", {});
    this.mergeConflictFile = new (_mergeConflictFile().MergeConflictFile)(workspace.path);
  }
  getDepsDataOfMergeConfig(id) {
    return this.mergeConfigDepsResolverDataCache[id.toString()];
  }

  /**
   * Calculate the component config based on:
   * the config property in the .bitmap file
   * the component.json file in the component folder
   * matching pattern in the variants config
   * defaults extensions from workspace config
   * extensions from the model.
   */
  // eslint-disable-next-line complexity
  async merge(componentId, componentFromScope, excludeOrigins = []) {
    // TODO: consider caching this result
    let configFileExtensions;
    let variantsExtensions;
    const mergeFromScope = true;
    const errors = [];
    const bitMapEntry = this.workspace.consumer.bitMap.getComponentIfExist(componentId);
    const bitMapExtensions = bitMapEntry?.config;
    let configMerge;
    try {
      configMerge = this.mergeConflictFile.getConflictParsed(componentId.toStringWithoutVersion());
    } catch (err) {
      if (!(err instanceof _mergeConfigConflict().MergeConfigConflict)) {
        throw err;
      }
      this.workspace.logger.error(`unable to parse the config file for ${componentId.toString()} due to conflicts`);
      errors.push(err);
    }
    const adjustEnvsOnConfigMerge = conf => {
      const env = conf[_envs().EnvsAspect.id]?.env;
      if (!env) return;
      const [id] = env.split('@');
      conf[_envs().EnvsAspect.id] = {
        env: id
      };
      conf[env] = {};
    };
    const unmergedData = this.getUnmergedData(componentId);
    const unmergedDataMergeConf = unmergedData?.mergedConfig;
    const getMergeConfigCombined = () => {
      if (!configMerge && !unmergedDataMergeConf) return undefined;
      if (!configMerge) return unmergedDataMergeConf;
      if (!unmergedDataMergeConf) return configMerge;
      return (0, _lodash().mergeWith)(configMerge, unmergedDataMergeConf, (objValue, srcValue) => {
        if (Array.isArray(objValue)) {
          // critical for dependencyResolver.policy.*dependencies. otherwise, it will override the array
          return objValue.concat(srcValue);
        }
        return undefined;
      });
    };
    const mergeConfigCombined = getMergeConfigCombined();
    adjustEnvsOnConfigMerge(mergeConfigCombined || {});
    const configMergeExtensions = mergeConfigCombined ? _extensionData().ExtensionDataList.fromConfigObject(mergeConfigCombined) : undefined;
    this.removeAutoDepsFromConfig(componentId, configMergeExtensions);
    const scopeExtensions = this.getComponentFromScopeWithoutDuplications(componentFromScope);
    // backward compatibility. previously, it was saved as an array into the model (when there was merge-config)
    this.removeAutoDepsFromConfig(componentId, scopeExtensions, true);
    const [specific, nonSpecific] = (0, _lodash().partition)(scopeExtensions, entry => entry.config[_workspace().AspectSpecificField] === true);
    const scopeExtensionsNonSpecific = new (_extensionData().ExtensionDataList)(...nonSpecific);
    const scopeExtensionsSpecific = new (_extensionData().ExtensionDataList)(...specific);
    this.addConfigDepsFromModelToConfigMerge(scopeExtensionsSpecific, mergeConfigCombined);
    const componentConfigFile = await this.workspace.componentConfigFile(componentId);
    if (componentConfigFile) {
      configFileExtensions = componentConfigFile.aspects.toLegacy();
    }
    const relativeComponentDir = this.workspace.componentDir(componentId, {
      ignoreVersion: true
    }, {
      relative: true
    });
    const variantConfig = this.workspace.variants.byRootDirAndName(relativeComponentDir, componentId.fullName);
    if (variantConfig) {
      variantsExtensions = variantConfig.extensions.clone();
      // Do not merge from scope when there is specific variant (which is not *) that match the component
      // if (variantConfig.maxSpecificity > 0) {
      //   mergeFromScope = false;
      // }
    }
    // We don't stop on each step because we want to merge the default scope even if propagate=false but the default scope is not defined
    // in the case the same extension pushed twice, the former takes precedence (opposite of Object.assign)
    const extensionsToMerge = [];
    let envWasFoundPreviously = false;
    const removedExtensionIds = [];
    const addExtensionsToMerge = async (extensions, origin, extraData) => {
      if (!extensions.length) {
        return;
      }
      removedExtensionIds.push(...extensions.filter(extData => extData.isRemoved).map(extData => extData.stringId));
      const extsWithoutRemoved = extensions.filterRemovedExtensions();
      const selfInMergedExtensions = extsWithoutRemoved.findExtension(componentId.toStringWithoutVersion(), true);
      const extsWithoutSelf = selfInMergedExtensions?.extensionId ? extsWithoutRemoved.remove(selfInMergedExtensions.extensionId) : extsWithoutRemoved;
      const preferWorkspaceVersion = origin !== 'BitmapFile' && origin !== 'ComponentJsonFile';
      // it's important to do this resolution before the merge, otherwise we have issues with extensions
      // coming from scope with local scope name, as opposed to the same extension comes from the workspace with default scope name
      // also, it's important to do it before filtering the env, because when the env was not exported, it's saved with scope-name
      // inside the "env" prop of teambit.envs/env, but without scope-name in the root.
      const extsWithUpdatedIds = await this.resolveExtensionListIds(extsWithoutSelf, preferWorkspaceVersion);
      const {
        extensionDataListFiltered,
        envIsCurrentlySet
      } = await this.filterEnvsFromExtensionsIfNeeded(extsWithUpdatedIds, envWasFoundPreviously, origin);
      if (envIsCurrentlySet) {
        envWasFoundPreviously = true;
      }
      extensionsToMerge.push({
        origin,
        extensions: extensionDataListFiltered,
        extraData
      });
    };
    const setDataListAsSpecific = extensions => {
      extensions.forEach(dataEntry => dataEntry.config[_workspace().AspectSpecificField] = true);
    };
    if (bitMapExtensions && !excludeOrigins.includes('BitmapFile')) {
      const extensionDataList = _extensionData().ExtensionDataList.fromConfigObject(bitMapExtensions);
      setDataListAsSpecific(extensionDataList);
      await addExtensionsToMerge(extensionDataList, 'BitmapFile');
    }
    // config-merge is after the .bitmap. because normally if you have config set in .bitmap, you won't
    // be able to lane-merge. (unless you specify --ignore-config-changes).
    // so, if there is config in .bitmap, it probably happened after lane-merge.
    if (configMergeExtensions && !excludeOrigins.includes('ConfigMerge')) {
      await addExtensionsToMerge(_extensionData().ExtensionDataList.fromArray(configMergeExtensions), 'ConfigMerge');
    }
    if (configFileExtensions && !excludeOrigins.includes('ComponentJsonFile')) {
      setDataListAsSpecific(configFileExtensions);
      await addExtensionsToMerge(configFileExtensions, 'ComponentJsonFile');
    }
    if (!excludeOrigins.includes('ModelSpecific')) {
      await addExtensionsToMerge(_extensionData().ExtensionDataList.fromArray(scopeExtensionsSpecific), 'ModelSpecific');
    }
    let continuePropagating = componentConfigFile?.propagate ?? true;
    if (variantsExtensions && continuePropagating && !excludeOrigins.includes('WorkspaceVariants')) {
      const appliedRules = variantConfig?.sortedMatches.map(({
        pattern,
        specificity
      }) => ({
        pattern,
        specificity
      }));
      await addExtensionsToMerge(variantsExtensions, 'WorkspaceVariants', {
        appliedRules
      });
    }
    continuePropagating = continuePropagating && (variantConfig?.propagate ?? true);
    if (mergeFromScope && continuePropagating && !excludeOrigins.includes('ModelNonSpecific')) {
      await addExtensionsToMerge(scopeExtensionsNonSpecific, 'ModelNonSpecific');
    }
    const afterMerge = _extensionData().ExtensionDataList.mergeConfigs(extensionsToMerge.map(ext => ext.extensions));
    const withoutRemoved = afterMerge.filter(extData => !removedExtensionIds.includes(extData.stringId));
    const extensions = _extensionData().ExtensionDataList.fromArray(withoutRemoved);
    return {
      extensions,
      beforeMerge: extensionsToMerge,
      errors
    };
  }

  /**
   * before version 0.0.882 it was possible to save Version object with the same extension twice.
   */
  getComponentFromScopeWithoutDuplications(componentFromScope) {
    if (!componentFromScope) return new (_extensionData().ExtensionDataList)();
    const scopeExtensions = componentFromScope.config.extensions;
    const scopeExtIds = scopeExtensions.ids;
    const scopeExtHasDuplications = scopeExtIds.length !== (0, _lodash().uniq)(scopeExtIds).length;
    if (!scopeExtHasDuplications) {
      return scopeExtensions;
    }
    // let's remove this duplicated extension blindly without trying to merge. (no need to merge coz it's old data from scope
    // which will be overridden anyway by the workspace or other config strategies).
    const arr = (0, _lodash().uniqWith)(scopeExtensions, (0, _extensionData().getCompareExtPredicate)(true));
    return _extensionData().ExtensionDataList.fromArray(arr);
  }

  /**
   * from the merge-config we get the dep-resolver policy as an array, because it needs to support "force" prop.
   * however, when we save the config, we want to save it as an object, so we need split the data into two:
   * 1. force: true, which gets saved into the config.
   * 2. force: false, which gets saved into the data.dependencies later on. see the workspace.getAutoDetectOverrides()
   */
  removeAutoDepsFromConfig(componentId, conf, fromScope = false) {
    if (!conf) return;
    const policy = conf.findCoreExtension(_dependencyResolver().DependencyResolverAspect.id)?.config.policy;
    if (!policy) return;
    const mergeConfigObj = {};
    ['dependencies', 'devDependencies', 'peerDependencies'].forEach(key => {
      if (!policy[key]) return;
      // this is only relevant when it is saved as an array. otherwise, it's always force: true.
      if (!Array.isArray(policy[key])) return;
      mergeConfigObj[key] = policy[key].filter(dep => !dep.force);
      policy[key] = policy[key].filter(dep => dep.force);
      if (!policy[key].length) {
        delete policy[key];
        return;
      }
      // convert to object
      policy[key] = policy[key].reduce((acc, current) => {
        acc[current.name] = current.version;
        return acc;
      }, {});
    });
    if (!fromScope) {
      if (!this.mergeConfigDepsResolverDataCache[componentId.toString()]) {
        this.mergeConfigDepsResolverDataCache[componentId.toString()] = {};
      }
      this.mergeConfigDepsResolverDataCache[componentId.toString()] = (0, _lodash().merge)(this.mergeConfigDepsResolverDataCache[componentId.toString()], mergeConfigObj);
    }
  }

  /**
   * this is needed because if the mergeConfig has a policy, it will be used, and any other policy along the line will be ignored.
   * in case the model has some dependencies that were set explicitly they're gonna be ignored.
   * this makes sure to add them to the policy of the mergeConfig.
   * in a way, this is similar to what we do when a user is running `bit deps set` and the component had previous dependencies set,
   * we copy those dependencies along with the current one to the .bitmap file, so they won't get lost.
   */
  addConfigDepsFromModelToConfigMerge(scopeExtensionsSpecific, mergeConfig) {
    const mergeConfigPolicy = mergeConfig?.[_dependencyResolver().DependencyResolverAspect.id]?.policy;
    if (!mergeConfigPolicy) return;
    const scopePolicy = scopeExtensionsSpecific.findCoreExtension(_dependencyResolver().DependencyResolverAspect.id)?.config.policy;
    if (!scopePolicy) return;
    Object.keys(scopePolicy).forEach(key => {
      if (!mergeConfigPolicy[key]) {
        mergeConfigPolicy[key] = scopePolicy[key];
        return;
      }
      // mergeConfigPolicy should take precedence over scopePolicy
      mergeConfigPolicy[key] = _objectSpread(_objectSpread({}, scopePolicy[key]), mergeConfigPolicy[key]);
    });
  }
  getUnmergedData(componentId) {
    return this.workspace.scope.legacyScope.objects.unmergedComponents.getEntry(componentId);
  }
  async filterEnvsFromExtensionsIfNeeded(extensionDataList, envWasFoundPreviously, origin) {
    const envAspect = extensionDataList.findExtension(_envs().EnvsAspect.id);
    const envFromEnvsAspect = envAspect?.config.env || envAspect?.data.id;
    const aspectsRegisteredAsEnvs = extensionDataList.filter(aspect => this.workspace.envs.getEnvDefinitionByStringId(aspect.newExtensionId?.toString() || aspect.stringId)).map(aspect => aspect.stringId);
    if (envWasFoundPreviously && (envAspect || aspectsRegisteredAsEnvs.length)) {
      const nonEnvs = extensionDataList.map(e => {
        // normally the env-id inside the envs aspect doesn't have a version, but the aspect itself has a version.
        // also, the env-id inside the envs aspect includes the default-scope, but the aspect itself doesn't.
        if (envFromEnvsAspect && e.stringId === envFromEnvsAspect || envFromEnvsAspect && e.extensionId?.toStringWithoutVersion() === envFromEnvsAspect || aspectsRegisteredAsEnvs.includes(e.stringId)) {
          return undefined;
        }
        if (e.stringId === envAspect?.stringId) {
          // must clone the env aspect to avoid mutating the original data
          const clonedEnvAspect = e.clone();
          delete clonedEnvAspect.config.env; // aspect env may have other data other then config.env.
          return clonedEnvAspect;
        }
        return e;
      });
      return {
        extensionDataListFiltered: new (_extensionData().ExtensionDataList)(...(0, _lodash().compact)(nonEnvs)),
        envIsCurrentlySet: true
      };
    }
    if (envFromEnvsAspect && (origin === 'ModelNonSpecific' || origin === 'ModelSpecific')) {
      // if env was found, search for this env in the workspace and if found, replace the env-id with the one from the workspace
      const envAspectExt = extensionDataList.find(e => e.extensionId?.toStringWithoutVersion() === envFromEnvsAspect);
      const ids = this.workspace.listIds();
      const envAspectId = envAspectExt?.extensionId;
      const found = envAspectId && ids.find(id => id.isEqualWithoutVersion(envAspectId));
      if (found) {
        envAspectExt.extensionId = found;
      }
    }
    return {
      extensionDataListFiltered: extensionDataList,
      envIsCurrentlySet: Boolean(envFromEnvsAspect)
    };
  }

  /**
   * This will mutate the entries with extensionId prop to have resolved legacy id
   * This should be worked on the extension data list not the new aspect list
   * @param extensionList
   */
  async resolveExtensionListIds(extensionList, preferWorkspaceVersion = false) {
    const promises = extensionList.map(async entry => {
      if (entry.extensionId) {
        // don't pass `entry.extensionId` (as ComponentID) to `resolveComponentId` because then it'll use the optimization
        // of parsing it to ComponentID without checking the workspace. Normally, this optimization is good, but here
        // in case the extension wasn't exported, the ComponentID is wrong, it has the scope-name due to incorrect ComponentID.fromString
        // in configEntryToDataEntry() function. It'd be ideal to fix it from there but it's not easy.
        const componentId = await this.workspace.resolveComponentId(entry.extensionId.toString());
        const idFromWorkspace = preferWorkspaceVersion ? this.workspace.getIdIfExist(componentId) : undefined;
        const id = idFromWorkspace || componentId;
        entry.extensionId = id;
        entry.newExtensionId = id;
      }
      return entry;
    });
    await Promise.all(promises);
    return extensionList;
  }
}
exports.AspectsMerger = AspectsMerger;

//# sourceMappingURL=aspects-merger.js.map