import { cloneDeep, isFunction, isPlainObject, noop, omit } from "lodash-es";
import debugPlugins from "debug";
import getError from "../get-error.js";
import { extractErrors } from "../utils.js";
import PLUGINS_DEFINITIONS from "../definitions/plugins.js";
import { loadPlugin, parseConfig } from "./utils.js";

const debug = debugPlugins("semantic-release:plugins");

export default async (context, type, pluginOpt, pluginsPath) => {
  const { stdout, stderr, options, logger } = context;
  if (!pluginOpt) {
    return noop;
  }

  const [name, config] = parseConfig(pluginOpt);
  const pluginName = name.pluginName ? name.pluginName : isFunction(name) ? `[Function: ${name.name}]` : name;
  const plugin = await loadPlugin(context, name, pluginsPath);

  debug(`options for ${pluginName}/${type}: %O`, config);

  let func;
  if (isFunction(plugin)) {
    func = plugin.bind(null, cloneDeep({ ...options, ...config }));
  } else if (isPlainObject(plugin) && plugin[type] && isFunction(plugin[type])) {
    func = plugin[type].bind(null, cloneDeep({ ...options, ...config }));
  } else {
    throw getError("EPLUGIN", { type, pluginName });
  }

  const validator = async (input) => {
    const { dryRun, outputValidator } = PLUGINS_DEFINITIONS[type] || {};
    try {
      if (!input.options.dryRun || dryRun) {
        logger.log(`Start step "${type}" of plugin "${pluginName}"`);
        const result = await func({
          ...cloneDeep(omit(input, ["stdout", "stderr", "logger"])),
          stdout,
          stderr,
          logger: logger.scope(logger.scopeName, pluginName),
        });
        if (outputValidator && !outputValidator(result)) {
          throw getError(`E${type.toUpperCase()}OUTPUT`, { result, pluginName });
        }

        logger.success(`Completed step "${type}" of plugin "${pluginName}"`);
        return result;
      }

      logger.warn(`Skip step "${type}" of plugin "${pluginName}" in dry-run mode`);
    } catch (error) {
      logger.error(`Failed step "${type}" of plugin "${pluginName}"`);
      extractErrors(error).forEach((err) => Object.assign(err, { pluginName }));
      throw error;
    }
  };

  Reflect.defineProperty(validator, "pluginName", { value: pluginName, writable: false, enumerable: true });

  if (!isFunction(pluginOpt)) {
    if (pluginsPath[name]) {
      logger.success(`Loaded plugin "${type}" from "${pluginName}" in shareable config "${pluginsPath[name]}"`);
    } else {
      logger.success(`Loaded plugin "${type}" from "${pluginName}"`);
    }
  }

  return validator;
};
