import { castArray, identity, isNil, isPlainObject, isString, omit } from "lodash-es";
import AggregateError from "aggregate-error";
import getError from "../get-error.js";
import PLUGINS_DEFINITIONS from "../definitions/plugins.js";
import { loadPlugin, parseConfig, validatePlugin, validateStep } from "./utils.js";
import pipeline from "./pipeline.js";
import normalize from "./normalize.js";

export default async (context, pluginsPath) => {
  let { options, logger } = context;
  const errors = [];

  const plugins = options.plugins
    ? await castArray(options.plugins).reduce(async (eventualPluginsList, plugin) => {
        const pluginsList = await eventualPluginsList;
        if (validatePlugin(plugin)) {
          const [name, config] = parseConfig(plugin);
          plugin = isString(name) ? await loadPlugin(context, name, pluginsPath) : name;

          if (isPlainObject(plugin)) {
            Object.entries(plugin).forEach(([type, func]) => {
              if (PLUGINS_DEFINITIONS[type]) {
                Reflect.defineProperty(func, "pluginName", {
                  value: isPlainObject(name) ? "Inline plugin" : name,
                  writable: false,
                  enumerable: true,
                });
                pluginsList[type] = [...(pluginsList[type] || []), [func, config]];
              }
            });
          } else {
            errors.push(getError("EPLUGINSCONF", { plugin }));
          }
        } else {
          errors.push(getError("EPLUGINSCONF", { plugin }));
        }

        return pluginsList;
      }, {})
    : [];

  if (errors.length > 0) {
    throw new AggregateError(errors);
  }

  options = { ...plugins, ...options };

  const pluginsConfig = await Object.entries(PLUGINS_DEFINITIONS).reduce(
    async (
      eventualPluginsConfigAccumulator,
      [type, { required, default: def, pipelineConfig, postprocess = identity, preprocess = identity }]
    ) => {
      let pluginOptions;
      const pluginsConfigAccumulator = await eventualPluginsConfigAccumulator;

      if (isNil(options[type]) && def) {
        pluginOptions = def;
      } else {
        // If an object is passed and the path is missing, merge it with step options
        if (isPlainObject(options[type]) && !options[type].path) {
          options[type] = castArray(plugins[type]).map((plugin) =>
            plugin ? [plugin[0], Object.assign(plugin[1], options[type])] : plugin
          );
        }

        if (!validateStep({ required }, options[type])) {
          errors.push(getError("EPLUGINCONF", { type, required, pluginConf: options[type] }));
          return pluginsConfigAccumulator;
        }

        pluginOptions = options[type];
      }

      const steps = await Promise.all(
        castArray(pluginOptions).map(async (pluginOpt) =>
          normalize(
            { ...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS), "plugins") },
            type,
            pluginOpt,
            pluginsPath
          )
        )
      );

      pluginsConfigAccumulator[type] = async (input) =>
        postprocess(
          await pipeline(
            steps,
            pipelineConfig && pipelineConfig(pluginsConfigAccumulator, logger)
          )(await preprocess(input)),
          input
        );

      return pluginsConfigAccumulator;
    },
    plugins
  );
  if (errors.length > 0) {
    throw new AggregateError(errors);
  }

  return pluginsConfig;
};
