/* eslint-disable no-underscore-dangle */

const Gateway = require("./Gateway");
const Schema = require("./Schema");
const { resolve } = require("path");
const fs = require("fs-nextra");

class Settings {

  /**
   * Creates a new settings instance.
   * @param {KomadaClient} client The Komada clien
   * @param {string} name The name of these new settings
   * @param {Function} validate The validate function for gateway
   * @param {Schema|Object} schema The schema object
   * @param {SettingResolver} resolver The resolver class
   */
  constructor(client, name, validate, schema, resolver) {
    /**
     * The komada client.
     * @type {KomadaClient}
     */
    Object.defineProperty(this, "client", { value: client });

    /**
     * The name or type of settings
     * @type {string}
     */
    this.type = name;

    /**
     * The gateway for this settings instance.
     * @type {Gateway}
     */
    this.gateway = new Gateway(this, validate);

    /**
     * The cache used to store data for this instance.
     * @type {Cache}
     */
    this.cache = client.config.provider.cache === "js" ? client.providers.get("collection") : client.providers.get(client.config.provider.cache);

    /**
     * The schema that we will use for this instance.
     * @name Settings#schema
     * @type {Schema}
     */
    Object.defineProperty(this, "_schema", { value: schema });
    this.schema = null;

    /**
     * The settings resolver used for this instance.
     * @type {SettingResolver}
     */
    this.resolver = resolver;

    /**
     * The base directory where this instance will save to.
     * @type {string}
     */
    this.baseDir = resolve(this.client.clientBaseDir, "bwd");

    /**
     * The path to the schema for this instance.
     * @type {string}
     */
    this.schemaPath = resolve(this.baseDir, `${this.type}_Schema.json`);
  }

  /**
   * Initializes all of our different components.
   */
  async init() {
    await fs.ensureDir(this.baseDir);
    const schema = await fs.readJSON(this.schemaPath)
      .catch(() => fs.outputJSONAtomic(this.schemaPath, this._schema).then(() => this._schema));
    await this.validateSchema(schema);
    await this.gateway.init(this.schema);
  }

  // BEGIN SCHEMA EXPOSURE //

  /**
   * Validates our schema. Ensures that the object was created correctly and will not break.
   * @param {Object|Schema} schema The schema we are validating.
   */
  validateSchema(schema) {
    if (!(schema instanceof Schema)) schema = new Schema(schema);
    for (const [key, value] of Object.entries(schema)) { // eslint-disable-line
      if (value instanceof Object && "type" in value && "default" in value) {
        if (value.array && !(value.default instanceof Array)) {
          this.client.emit("log", `The default value for ${key} must be an array.`, "error");
          delete schema[key];
          continue;
        }
      } else {
        delete schema[key];
        this.client.emit("log", `The type value for ${key} is not supported. It must be an object with type and default properties.`, "error");
      }
    }
    this.schema = schema;
  }

  /**
   * @param {string} name The name of the key you want to add.
   * @param {Schema.Options} options Schema options.
   * @param {boolean} [force=true] Whether or not we should force update all settings.
   * @returns {Promise<Schema>} The new schema object
   */
  async add(name, options, force = true) {
    this.schema.add(name, options);
    if (force) await this.force("add", name);
    fs.outputJSONAtomic(this.schemaPath, this.schema);
    return this.schema;
  }

  /**
   * Remove a key from the schema.
   * @param {string}  key The key to remove.
   * @param {boolean} [force=false] Whether this change should modify all configurations or not.
   * @returns {Promise<Schema>} The new schema object
   * @example
   * // Remove a key called 'modlog'.
   * await client.settings.guilds.remove("modlog");
   */
  async remove(key, force = true) {
    if (!(key in this.schema)) throw `The key ${key} does not exist in the schema.`;
    delete this.schema[key];
    if (force) await this.force("delete", key);
    fs.outputJSONAtomic(this.schemaPath, this.schema);
    return this.schema;
  }

  /**
   * Modify all configurations. Do NOT use this directly.
   * @param {string} action Whether reset, add, or delete.
   * @param {string} key The key to update.
   * @returns {Promise<void>}
   * @private
   */
  async force(action, key) {
    if (this.gateway.sql) await this.gateway.sql.updateColumns(this.schema, this.schema.defaults, key);
    const data = this.cache.getAll(this.type);
    let value;
    if (action === "add") value = this.schema.defaults[key];
    await Promise.all(data.map(async (obj) => {
      const object = obj;
      if (action === "delete") delete object[key]; else object[key] = value;
      if (obj.id) await this.gateway.provider.replace(this.type, obj.id, object);
      return true;
    }));
    return this.gateway.sync();
  }

  // BEGIN GATEWAY EXPOSURE //

  /**
   * Creates a new entry in the cache.
   * @param {Object|string} input An object containing a id property, like discord.js objects, or a string.
   * @returns {Promisie<void>}
   */
  create(...args) {
    return this.gateway.create(...args);
  }

  /**
   * Removes an entry from the cache.
   * @param {Object|string} input An object containing a id property, like discord.js objects, or a string.
   * @returns {Promise<void>}
   */
  destroy(...args) {
    return this.gateway.destroy(...args);
  }

  /**
   * Gets an entry from the cache
   * @param {string} input The key you are you looking for.
   * @returns {Schema}
   */
  get(...args) {
    return this.gateway.get(...args);
  }

  /**
   * Reset a key's value to default from a entry.
   * @param {Object|string} input An object containing a id property, like Discord.js objects, or a string.
   * @param {string} key The key to reset.
   * @returns {any}
   */
  reset(...args) {
    return this.gateway.reset(...args);
  }

  /**
   * Updates an entry.
   * @param {Object|string} input An object or string that can be parsed by this instance's resolver.
   * @param {Object} object An object with pairs of key/value to update.
   * @param {Object|string} [guild=null] A Guild resolvable, useful for when the instance of SG doesn't aim for Guild settings.
   * @returns {Object}
   */
  update(...args) {
    return this.gateway.update(...args);
  }

  /**
   * Update an array from the a Guild's configuration.
   * @param {Object|string} input An object containing a id property, like discord.js objects, or a string.
   * @param {string} type Either 'add' or 'remove'.
   * @param {string} key The key from the Schema.
   * @param {any} data The value to be added or removed.
   * @param {Object|string} [guild=null] The guild for this new setting change, useful for when settings don't aim for guilds.
   * @returns {boolean}
   */
  updateArray(...args) {
    return this.gateway.updateArray(...args);
  }

}

module.exports = Settings;