/* eslint-disable no-throw-literal, no-use-before-define, no-restricted-syntax, no-underscore-dangle */
const Discord = require("discord.js");
const path = require("path");
const { performance: { now } } = require("perf_hooks");
const Loader = require("./loader");
const ArgResolver = require("./argResolver");
const PermLevels = require("./PermissionLevels");
const Settings = require("./settingsCache");
const merge = require("../functions/mergeConfig");
const Console = require("./console/Console");


const defaultPermStructure = new PermLevels()
  .add(0, false, () => true)
  .add(2, false, (client, msg) => {
    if (!msg.guild || !msg.guild.settings.modRole) return false;
    const modRole = msg.guild.roles.get(msg.guild.settings.modRole);
    return modRole && msg.member.roles.has(modRole.id);
  })
  .add(3, false, (client, msg) => {
    if (!msg.guild || !msg.guild.settings.adminRole) return false;
    const adminRole = msg.guild.roles.get(msg.guild.settings.adminRole);
    return adminRole && msg.member.roles.has(adminRole.id);
  })
  .add(4, false, (client, msg) => msg.guild && msg.author.id === msg.guild.owner.id)
  .add(9, true, (client, msg) => msg.author.id === client.config.ownerID)
  .add(10, false, (client, msg) => msg.author.id === client.config.ownerID);

/**
 * @typedef  {object}   OptionsDisabled
 * @property {string[]} [commands=Array]    Disabled Commands
 * @property {string[]} [events=Array]      Disabled Events
 * @property {string[]} [functions=Array]   Disabled Functions
 * @property {string[]} [inhibitors=Array]  Disabled Inhibitors
 * @property {string[]} [finalizers=Array]  Disabled Finalizers
 * @property {string[]} [monitors=Array]    Disabled Monitors
 * @property {string[]} [providers=Array]   Disabled Providers
 * @property {string[]} [extendables=Array] Disabled Extendables
 * @memberof Komada
 */

/**
 * @typedef  {object} OptionsProviders
 * @property {string} [engine=json] The Provider Engine SettingGateway will use to store and access to the persistent data.
 * @property {string} [cache=js]    The Provider Cache Engine CacheManager from SettingGateway will use to cache the data.
 * @memberof Komada
 */

/**
 * @typedef  {object}  Options
 * @property {string}  [prefix=?] The prefix for Komada. Defaults to '?'.
 * @property {string}  [ownerID=String] The bot owner's ID, Komada will autofetch it if it's not specified.
 *
 * @property {Komada.OptionsDisabled}  [disabled={}] The disabled pieces.
 * @property {PermissionLevels|Array<{}>} [permStructure=Array<{}>] The PermStructure for Komada.
 *
 * @property {boolean} [selfbot=boolean] Whether the bot is a selfbot or not. Komada detects this automatically.
 * @property {function}  [readyMessage=function] A custom function with a client argument that allows you to customize the ready string when Komada logs in.
 * @property {number}  [commandMessageLifetime=1800] The lifetime for the command messages, in milliseconds.
 * @property {number}  [commandMessageSweep=900] How frequent should Komada sweep the command messages.
 *
 * @property {boolean} [disableLogTimestamps=false] Whether the komada logger should show the timestamps.
 * @property {boolean} [disableLogColor=false] Whether the komada logger should show colours.
 *
 * @property {boolean} [cmdEditing=false] Whether Komada should consider edited messages as potential messages able to fire new commands.
 * @property {boolean} [cmdPrompt=false] Whether Komada should prompt missing/invalid arguments at failed command execution.
 *
 * @property {string} [clientBaseDir=path.dirname(require.main.filename)] Directory where client pieces are stored. Can be an absolute or relative path. Defaults to the location of the index.js/app.js
 *
 * @property {Komada.OptionsProviders}  [provider={}] The engines for SettingGateway, 'engine' for Persistent Data, 'cache' for Cache Engine (defaults to Collection)
 * @memberof Komada
 */

/**
 * The class for the magic behind Komada
 * @extends external:Client
 */
class Komada extends Discord.Client {

  /**
   * Creates a new instance of Komada
   * @param {Komada.Options} [config={}] The configuration options to provide to Komada
   */
  constructor(config = {}) {
    if (typeof config !== "object") throw new TypeError("Configuration for Komada must be an object.");
    super(config.clientOptions);
    /**
     * The configuration used to create Komada
     * @type {Komada.Options}
     */

    this.config = merge(config);

    /**
     * The location of where the core files of Komada rely in, typically inside node_modules
     * @type {String}
     */
    this.coreBaseDir = path.join(__dirname, "../");

    /**
     * The location of where you installed Komada, Can be a absolute/relative path or the path to your app/index.js
     * @type {String}
     */
    this.clientBaseDir = `${this.config.clientBaseDir || path.dirname(require.main.filename)}${path.sep}`;

    /**
     * An object containing all the functions within Komada
     * @type {Loader}
     */
    this.funcs = new Loader(this);

    /**
     * The resolver that resolves arguments in commands into their expected results
     * @type {ArgResolver}
     */
    this.argResolver = new ArgResolver(this);

    /**
     * The collection of commands available for use in Komada
     * @type external:Collection
     */
    this.commands = new Discord.Collection();

    /**
     * The collection of aliases that point to commands in Komada
     * @type external:Collection
     */
    this.aliases = new Discord.Collection();

    /**
     * The collection of inhibitors ran on commands
     * @type external:Collection
     */
    this.commandInhibitors = new Discord.Collection();

    /**
     * The collection of finalizers ran on succcesful commands.
     * @type external:Collection
     */
    this.commandFinalizers = new Discord.Collection();

    /**
     * The collection of monitors that are ran are specific or all messages.
     * @type external:Collection
     */
    this.messageMonitors = new Discord.Collection();

    /**
     * The collection of providers that can be used in Komada
     * @type external:Collection
     */
    this.providers = new Discord.Collection();

    /**
     * The collection of event handlers in Komada, used for reloading
     * @type external:Collection
     */
    this.eventHandlers = new Discord.Collection();

    /**
     * The permStructure Komada will take into account when commands are ran and permLevel is calculated.
     * @type {PermissionStructure}
     */
    this.permStructure = config.permStructure instanceof PermLevels ? config.permStructure : defaultPermStructure;

    /**
     * The collection of stored command messages
     * @type external:Collection
     */
    this.commandMessages = new Discord.Collection();

    /**
     * The lifetime of command messages before they are removed from the cache and not editable anymore.
     * @type {Number}
     */
    this.commandMessageLifetime = config.commandMessageLifetime || 1800;

    /**
     * The amount of time in between each command message sweep in Komada.
     * @type {Number}
     */
    this.commandMessageSweep = config.commandMessageSweep || 900;

    /**
     * Whether or not Komada is completely ready to accept commands from users or not. This will be true after everything is initialized correctly.
     * @type {Boolean}
     */
    this.ready = false;

    /**
     * Additional methods to be used elsewhere in the bot
     * @type {Object}
     * @property {Class} Collection A discord.js collection
     * @property {Class} Embed A discord.js Message Embed
     * @property {Class} MessageCollector A discord.js MessageCollector
     * @property {Class} Webhook A discord.js WebhookClient
     * @property {Function} escapeMarkdown A discord.js escape markdown function
     * @property {Function} splitMessage A discord.js split message function
     */
    this.methods = {
      Collection: Discord.Collection,
      Embed: Discord.MessageEmbed,
      MessageCollector: Discord.MessageCollector,
      Webhook: Discord.WebhookClient,
      escapeMarkdown: Discord.escapeMarkdown,
      splitMessage: Discord.splitMessage,
    };

    /**
     * The object where the gateways are stored settings
     * @type {Object}
     */
    this.settings = null;

    /**
     * The oauth bots application. This will either be a full application object when Komada has finally loaded or null if the bot is a selfbot.
     * @type {Object}
     */
    this.application = null;

    /**
     * The console for this instance of Komada. You can disable timestmaps, colors, and add writable streams as config options to configure this.
     * @type {KomadaConsole}
     */
    this.console = new Console({ stdout: this.config.console.stdout, stderr: this.config.console.stderr, useColor: this.config.console.useColors, colors: this.config.console.colors, timestamps: this.config.console.timestamps });

    this.once("ready", this._ready.bind(this));
  }

  /**
   * The invite link for the bot
   * @readonly
   * @returns {string}
   */
  get invite() {
    if (!this.user.bot) throw "Why would you need an invite link for a selfbot...";
    const permissions = Discord.Permissions.resolve([...new Set(this.commands.reduce((a, b) => a.concat(b.conf.botPerms), ["VIEW_CHANNEL", "SEND_MESSAGES"]))]);
    return `https://discordapp.com/oauth2/authorize?client_id=${this.application.id}&permissions=${permissions}&scope=bot`;
  }

  /**
   * The owner for this bot
   * @readonly
   * @type {external:User}
   */
  get owner() {
    return this.users.get(this.config.ownerID);
  }

  /**
   * Use this to login to Discord with your bot
   * @param {string} token Your bot token
   */
  async login(token) {
    const start = now();
    await this.funcs.loadAll(this);
    this.settings = new Settings(this);
    this.emit("log", `Loaded in ${(now() - start).toFixed(2)}ms.`);
    super.login(token);
  }

  /**
   * The once ready function for the client to init all pieces
   * @private
   */
  async _ready() {
    this.config.prefixMention = new RegExp(`^<@!?${this.user.id}>`);
    if (this.user.bot) this.application = await super.fetchApplication();
    if (!this.config.ownerID) this.config.ownerID = this.user.bot ? this.application.owner.id : this.user.id;
    await Promise.all(this.providers.map((piece) => {
      if (piece.init) return piece.init(this);
      return true;
    }));
    await Promise.all(Object.keys(this.settings).map((key) => {
      if (this.settings[key].init) return this.settings[key].init();
      return true;
    }));
    await Promise.all(Object.keys(this.funcs).map((key) => {
      if (this.funcs[key].init) return this.funcs[key].init(this);
      return true;
    }));
    await Promise.all([
      this.commands.map(piece => (piece.init ? piece.init(this) : true)),
      this.commandInhibitors.map(piece => (piece.init ? piece.init(this) : true)),
      this.commandFinalizers.map(piece => (piece.init ? piece.init(this) : true)),
      this.messageMonitors.map(piece => (piece.init ? piece.init(this) : true)),
    ]);
    this.setInterval(this.sweepCommandMessages.bind(this), this.commandMessageLifetime);
    this.ready = true;
    this.emit("log", this.config.readyMessage(this));
  }

  /**
   * Sweeps command messages based on the lifetime parameter
   * @param {number} lifetime The threshold for how old command messages can be before sweeping since the last edit in seconds
   * @returns {number} The amount of messages swept
   */
  sweepCommandMessages(lifetime = this.commandMessageLifetime) {
    if (typeof lifetime !== "number" || Number.isNaN(lifetime)) throw new TypeError("The lifetime must be a number.");
    if (lifetime <= 0) {
      this.emit("debug", "Didn't sweep messages - lifetime is unlimited");
      return -1;
    }

    const lifetimeMs = lifetime * 1000;
    const rightNow = Date.now();
    const messages = this.commandMessages.size;

    for (const [key, message] of this.commandMessages) {
      if (rightNow - (message.trigger.editedTimestamp || message.trigger.createdTimestamp) > lifetimeMs) this.commandMessages.delete(key);
    }

    this.emit("debug", `Swept ${messages - this.commandMessages.size} commandMessages older than ${lifetime} seconds.`);
    return messages - this.commandMessages.size;
  }

}

module.exports = Komada;

process.on("unhandledRejection", (err) => {
  if (!err) return;
  console.error(`Uncaught Promise Error: \n${err.stack || err}`);
});