'use strict';

import isEmpty from 'lodash/isEmpty';
import fs from 'fs';
import util from 'util';
import AdmZip from 'adm-zip';

import difference from '../utils/getObjectDiff';
import { logMessage } from '../utils';

/**
 * Main services for config import/export.
 */

export default () => ({
  /**
   * Write a single config file.
   *
   * @param {string} configType - The type of the config.
   * @param {string} configName - The name of the config file.
   * @param {string} fileContents - The JSON content of the config file.
   * @returns {void}
   */
  writeConfigFile: async (configType, configName, fileContents) => {
    // Check if the config should be excluded.
    const shouldExclude = !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => `${configType}.${configName}`.startsWith(option)));
    if (shouldExclude) return;

    // Replace reserved characters in filenames.
    configName = configName.replace(/:/g, "#").replace(/\//g, "$");

    // Check if the JSON content should be minified.
    const json = !strapi.config.get('plugin::config-sync').minify
      ? JSON.stringify(fileContents, null, 2)
      : JSON.stringify(fileContents);

    if (!fs.existsSync(strapi.config.get('plugin::config-sync.syncDir'))) {
      fs.mkdirSync(strapi.config.get('plugin::config-sync.syncDir'), { recursive: true });
    }

    const writeFile = util.promisify(fs.writeFile);
    await writeFile(`${strapi.config.get('plugin::config-sync.syncDir')}${configType}.${configName}.json`, json)
      .then(() => {
        // @TODO:
        // Add logging for successfull config export.
      })
      .catch(() => {
        // @TODO:
        // Add logging for failed config export.
      });
  },

  /**
   * Delete config file.
   *
   * @param {string} configName - The name of the config file.
   * @returns {void}
   */
  deleteConfigFile: async (configName) => {
    // Check if the config should be excluded.
    const shouldExclude = !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => configName.startsWith(option)));
    if (shouldExclude) return;

    // Replace reserved characters in filenames.
    configName = configName.replace(/:/g, "#").replace(/\//g, "$");

    fs.unlinkSync(`${strapi.config.get('plugin::config-sync.syncDir')}${configName}.json`);
  },

  /**
   * Zip config files.
   *
   * @param {string} configName - The name of the config file.
   * @returns {void}
   */
  zipConfigFiles: async () => {
    const fileName = `config-sync-${new Date().toJSON()}.zip`;

    const zip = new AdmZip();
    zip.addLocalFolder(strapi.config.get('plugin::config-sync.syncDir'));
    const base64Data = zip.toBuffer().toString('base64');

    return { base64Data, name: fileName, message: 'Success' };
  },

  /**
   * Read from a config file.
   *
   * @param {string} configType - The type of config.
   * @param {string} configName - The name of the config file.
   * @returns {object} The JSON content of the config file.
   */
  readConfigFile: async (configType, configName) => {
    // Replace reserved characters in filenames.
    configName = configName.replace(/:/g, "#").replace(/\//g, "$");

    const readFile = util.promisify(fs.readFile);
    return readFile(`${strapi.config.get('plugin::config-sync.syncDir')}${configType}.${configName}.json`)
      .then((data) => {
        return JSON.parse(data);
      })
      .catch(() => {
        return null;
      });
  },


  /**
   * Get all the config JSON from the filesystem.
   *
   * @param {string} configType - Type of config to gather. Leave empty to get all config.
   * @returns {object} Object with key value pairs of configs.
   */
  getAllConfigFromFiles: async (configType = null) => {
    if (!fs.existsSync(strapi.config.get('plugin::config-sync.syncDir'))) {
      return {};
    }

    const configFiles = fs.readdirSync(strapi.config.get('plugin::config-sync.syncDir'));

    const getConfigs = async () => {
      const fileConfigs = {};

      await Promise.all(configFiles.map(async (file) => {
        const type = file.split('.')[0]; // Grab the first part of the filename.
        const name = file.split(/\.(.+)/)[1].split('.').slice(0, -1).join('.'); // Grab the rest of the filename minus the file extension.

        // Put back reserved characters from filenames.
        const formattedName = name.replace(/#/g, ":").replace(/\$/g, "/");

        if (
          configType && configType !== type
          || !strapi.plugin('config-sync').types[type]
          || !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => `${type}.${name}`.startsWith(option)))
        ) {
          return;
        }

        const fileContents = await strapi.plugin('config-sync').service('main').readConfigFile(type, name);

        if (!fileContents) {
          strapi.log.warn(logMessage(`An empty config file '${file}' was found in the sync directory`));
          return;
        }

        fileConfigs[`${type}.${formattedName}`] = fileContents;
      }));

      return fileConfigs;
    };

    return getConfigs();
  },

  /**
   * Get all the config JSON from the database.
   *
   * @param {string} configType - Type of config to gather. Leave empty to get all config.
   * @returns {object} Object with key value pairs of configs.
   */
  getAllConfigFromDatabase: async (configType = null) => {
    const getConfigs = async () => {
      let databaseConfigs = {};

      await Promise.all(Object.entries(strapi.plugin('config-sync').types).map(async ([name, type]) => {
        if (configType && configType !== name) {
          return;
        }

        const config = await type.getAllFromDatabase();
        databaseConfigs = Object.assign(config, databaseConfigs);
      }));

      return databaseConfigs;
    };

    return getConfigs();
  },

  /**
   * Import all config files into the db.
   *
   * @param {string} configType - Type of config to impor. Leave empty to import all config.
   * @param {object} onSuccess - Success callback to run on each single successfull import.
   * @returns {void}
   */
  importAllConfig: async (configType = null, onSuccess) => {
    const fileConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromFiles();
    const databaseConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromDatabase();

    const diff = difference(databaseConfig, fileConfig);

    await Promise.all(Object.keys(diff).map(async (file) => {
      const type = file.split('.')[0]; // Grab the first part of the filename.
      const name = file.split(/\.(.+)/)[1]; // Grab the rest of the filename.

      if (configType && configType !== type) {
        return;
      }

      await strapi.plugin('config-sync').service('main').importSingleConfig(`${type}.${name}`, onSuccess);
    }));
  },

  /**
   * Export all config files.
   *
   * @param {string} configType - Type of config to export. Leave empty to export all config.
   * @param {object} onSuccess - Success callback to run on each single successfull import.
   * @returns {void}
   */
  exportAllConfig: async (configType = null, onSuccess) => {
    const fileConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromFiles();
    const databaseConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromDatabase();

    const diff = difference(databaseConfig, fileConfig);

    await Promise.all(Object.keys(diff).map(async (file) => {
      const type = file.split('.')[0]; // Grab the first part of the filename.
      const name = file.split(/\.(.+)/)[1]; // Grab the rest of the filename.

      if (configType && configType !== type) {
        return;
      }

      await strapi.plugin('config-sync').service('main').exportSingleConfig(`${type}.${name}`, onSuccess);
    }));
  },

  /**
   * Import a single config file into the db.
   *
   * @param {string} configName - The name of the config file.
   * @param {object} onSuccess - Success callback to run on each single successfull import.
   * @param {boolean} force - Ignore the soft setting.
   * @returns {void}
   */
  importSingleConfig: async (configName, onSuccess, force) => {
    // Check if the config should be excluded.
    const shouldExclude = !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => configName.startsWith(option)));
    if (shouldExclude) return;

    const type = configName.split('.')[0]; // Grab the first part of the filename.
    const name = configName.split(/\.(.+)/)[1]; // Grab the rest of the filename.
    const fileContents = await strapi.plugin('config-sync').service('main').readConfigFile(type, name);

    try {
      const importState = await strapi.plugin('config-sync').types[type].importSingle(name, fileContents, force);
      if (onSuccess && importState !== false) onSuccess(`${type}.${name}`);
    } catch (e) {
      throw new Error(`Error when trying to import ${type}.${name}. ${e}`);
    }
  },

  /**
   * Export a single config file.
   *
   * @param {string} configName - The name of the config file.
   * @param {object} onSuccess - Success callback to run on each single successfull import.
   *
   * @returns {void}
   */
   exportSingleConfig: async (configName, onSuccess) => {
     // Check if the config should be excluded.
    const shouldExclude = !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => configName.startsWith(option)));
    if (shouldExclude) return;

    const type = configName.split('.')[0]; // Grab the first part of the filename.
    const name = configName.split(/\.(.+)/)[1]; // Grab the rest of the filename.

    try {
      await strapi.plugin('config-sync').types[type].exportSingle(configName);
      if (onSuccess) onSuccess(`${type}.${name}`);
    } catch (e) {
      throw new Error(`Error when trying to export ${type}.${name}. ${e}`);
    }
  },

  /**
   * Get the formatted diff.
   *
   * @param {string} configType - Type of config to get the diff of. Leave empty to get the diff of all config.
   *
   * @returns {object} - the formatted diff.
   */
  getFormattedDiff: async (configType = null) => {
    const formattedDiff = {
      fileConfig: {},
      databaseConfig: {},
      diff: {},
    };

    const fileConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromFiles(configType);
    const databaseConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromDatabase(configType);

    const diff = difference(databaseConfig, fileConfig);

    formattedDiff.diff = diff;

    Object.keys(diff).map((changedConfigName) => {
      formattedDiff.fileConfig[changedConfigName] = fileConfig[changedConfigName];
      formattedDiff.databaseConfig[changedConfigName] = databaseConfig[changedConfigName];
    });

    return formattedDiff;
  },
});
