const SQL = require("../sql");

/**
 * The gateway for this settings instance. The gateway handles all the creation and setting of non-default entries, along with saving.
 */

class Gateway {

  /**
   * Constructs our instance of Gateway
   * @param {any} settings The settings that created this gateway.
   * @param {any} validateFunction The validation function used to validate user input.
   */
  constructor(settings, validateFunction) {
    /**
     * The Settings class that this gateway is a part of.
     * @name Gateway.settings
     * @type {Settings}
     * @readonly
     */
    Object.defineProperty(this, "settings", { value: settings });

    /**
     * The provider engine that will handle saving and getting all data for this instance.
     * @type {string}
     */
    this.engine = this.client.config.provider.engine;

    if (!this.provider) throw `This provider(${this.engine}) does not exist in your system.`;

    /**
     * If the provider is SQL, this property will ensure data is serialized and deserialized.
     * @type {string}
     */
    this.sql = this.provider.conf.sql ? new SQL(this.client, this) : null;

    /**
     * The function validator for this gateway.
     * @type {function}
     */
    this.validate = validateFunction;
  }

  /**
   * Initializes the gateway, creating tables, ensuring the schema exists, and caching values.
   * @param {Schema} schema The Schema object, validated from settings.
   * @returns {void}
   */
  async init(schema) {
    if (!(await this.provider.hasTable(this.type))) await this.provider.createTable(this.type, this.sql ? this.sql.buildSQLSchema(schema) : undefined);
    const data = await this.provider.getAll(this.type);
    if (this.sql) {
      this.sql.initDeserialize();
      for (let i = 0; i < data.length; i++) this.sql.deserializer(data[i]);
    }
    for (const key of data) this.cache.set(this.type, key.id, key); // eslint-disable-line
  }

  /**
   * Creates a new entry in the cache.
   * @param {Object|string} input An object containing a id property, like discord.js objects, or a string.
   */
  async create(input) {
    const target = await this.validate(input).then(output => (output.id || output));
    await this.provider.create(this.type, target, this.schema.defaults);
    this.cache.set(this.type, target, this.schema.defaults);
  }

  /**
   * Removes an entry from the cache.
   * @param {Object|string} input An object containing a id property, like discord.js objects, or a string.
   */
  async destroy(input) {
    const target = await this.validate(input).then(output => (output.id || output));
    await this.provider.delete(this.type, target);
    this.cache.delete(this.type, target);
  }

  /**
   * Gets an entry from the cache
   * @param {string} input The key you are you looking for.
   * @returns {Schema}
   */
  get(input) {
    return input !== "default" ? this.cache.get(this.type, input) || this.schema.defaults : this.schema.defaults;
  }

  /**
   * Sync either all entries from the provider, or a single one.
   * @param {Object|string} [input=null] An object containing a id property, like discord.js objects, or a string.
   * @returns {void}
   */
  async sync(input = null) {
    if (!input) {
      const data = await this.provider.getAll(this.type);
      if (this.sql) for (let i = 0; i < data.length; i++) this.sql.deserializer(data[i]);
      for (const key of data) this.cache.set(this.type, key.id, key); // eslint-disable-line
      return;
    }
    const target = await this.validate(input).then(output => (output.id || output));
    const data = await this.provider.get(this.type, target);
    if (this.sql) this.sql.deserializer(data);
    await this.cache.set(this.type, target, data);
  }

  /**
   * 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}
   */
  async reset(input, key) {
    const target = await this.validate(input).then(output => (output.id || output));
    if (!(key in this.schema)) throw `The key ${key} does not exist in the current data schema.`;
    const defaultKey = this.schema[key].default;
    await this.provider.update(this.type, target, { [key]: defaultKey });
    this.sync(target);
    return defaultKey;
  }

  /**
   * 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}
   */
  async update(input, object, guild = null) {
    const target = await this.validate(input).then(output => output.id || output);
    guild = await this.resolver.guild(guild || target);

    const resolved = await Promise.all(Object.entries(object).map(async ([key, value]) => {
      if (!(key in this.schema)) throw `The key ${key} does not exist in the current data schema.`;
      return this.resolver[this.schema[key].type.toLowerCase()](value, guild, this.schema[key])
        .then(res => ({ [key]: res.id || res }));
    }));

    const result = Object.assign({}, ...resolved);

    await this.ensureCreate(target);
    await this.provider.update(this.type, target, result);
    await this.sync(target);
    return result;
  }

  /**
   * Creates the settings if it did not exist previously.
   * @param {Object|string} target An object or string that can be parsed by this instance's resolver.
   * @returns {true}
   */
  async ensureCreate(target) {
    if (typeof target !== "string") throw `Expected input type string, got ${typeof target}`;
    let exists = this.cache.has(this.type, target);
    if (exists instanceof Promise) exists = await exists;
    if (exists === false) return this.create(target);
    return true;
  }

  /**
   * 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 setting, useful for when the settings aren't aimed for guilds
   * @returns {boolean}
   */
  async updateArray(input, type, key, data, guild = null) {
    if (!["add", "remove"].includes(type)) throw "The type parameter must be either add or remove.";
    if (!(key in this.schema)) throw `The key ${key} does not exist in the current data schema.`;
    if (!this.schema[key].array) throw `The key ${key} is not an Array.`;
    if (data === undefined) throw "You must specify the value to add or filter.";
    const target = await this.validate(input).then(output => (output.id || output));
    guild = await this.resolver.guild(guild || target);
    let result = await this.resolver[this.schema[key].type.toLowerCase()](data, guild, this.schema[key]);
    if (result.id) result = result.id;
    let cache = this.cache.get(this.type, target);
    if (cache instanceof Promise) cache = await cache;
    if (type === "add") {
      if (cache[key].includes(result)) throw `The value ${data} for the key ${key} already exists.`;
      cache[key].push(result);
      await this.provider.update(this.type, target, { [key]: cache[key] });
      await this.sync(target);
      return result;
    }
    if (!cache[key].includes(result)) throw `The value ${data} for the key ${key} does not exist.`;
    cache[key] = cache[key].filter(v => v !== result);

    await this.ensureCreate(target);
    await this.provider.update(this.type, target, { [key]: cache[key] });
    await this.sync(target);
    return true;
  }

  /**
   * The client this SettingGateway was created with.
   * @type {KomadaClient}
   * @readonly
   */
  get client() {
    return this.settings.client;
  }

  /**
   * The resolver instance this SettingGateway uses to parse the data.
   * @type {Resolver}
   * @readonly
   */
  get resolver() {
    return this.settings.resolver;
  }

  /**
   * The provider this SettingGateway instance uses for the persistent data operations.
   * @type {Provider}
   * @readonly
   */
  get provider() {
    return this.client.providers.get(this.engine);
  }

  /**
   * The schema this gateway instance is handling.
   * @type {Schema}
   * @readonly
   */
  get schema() {
    return this.settings.schema;
  }

  /**
   * The cache created with this instance
   * @type {Cache}
   * @readonly
   */

  get cache() {
    return this.settings.cache;
  }

  /**
   * The type of settings (or name).
   * @type {string}
   * @readonly
   */
  get type() {
    return this.settings.type;
  }

}

module.exports = Gateway;