const { Partials, IntentsBitField, Client } = require("discord.js");
const { DefaultWebSocketManagerOptions } = require("@discordjs/ws");
const { VariableManager } = require("./Variables.js");
const InteractionManager = require("./Interaction.js");
const LoadCommands = require("./LoadCommands.js");
const { ActivityTypeAvailables, IntentOptions, EventsToIntents, EventsToDjsEvents, EventstoFile } = require("../utils/Constants.js");
const Database = require("./Database.js");
const { MacrosManager } = require("./Macros.js");
const CacheManager = require("./CacheManager.js");
const { CommandManager } = require("./Commands.js");
const { Group } = require("@aoijs/aoi.structures");
const AoiError = require("./AoiError.js");
const { functions: parser } = require("../core/AoiReader.js");

class BaseClient extends Client {
    constructor(options) {
        if (options.cache) {
            options.makeCache = CacheManager._setDjsCacheManagers(options.cache);
        }

        options.partials = options.partials || [Partials.GuildMember, Partials.Channel, Partials.Message, Partials.Reaction, Partials.User, Partials.GuildScheduledEvent, Partials.ThreadMember];

        const aoiOptions = {};
        Object.assign(aoiOptions, options);

        if (options?.mobilePlatform === true) {
            DefaultWebSocketManagerOptions.identifyProperties.browser = "Discord iOS";
        }

        if (!options.intents) {
            throw new TypeError("Client intents must be provided.");
        }

        options.intents = options.intents.map((x) => IntentOptions[x] || x);

        super(options);

        this.aoiOptions = aoiOptions;

        this.cmd = new CommandManager(this);
        this.interactionManager = new InteractionManager(this);
        this.cacheManager = new CacheManager(this);

        this.variableManager = new VariableManager(this);
        this.macros = new MacrosManager();

        if (options.disableAoiDB !== true && (["default", "aoi.db"].includes(options?.database?.type) || !options?.database)) {
            const dbData = options?.database;

            this.db = new Database(
                dbData?.type ?? "aoi.db",
                dbData?.db ?? require("@aoijs/aoi.db"),
                dbData?.dbType ?? "KeyValue",
                {
                    dataConfig: {
                        path: dbData?.path ?? "./database",
                        tables: dbData?.tables?.length ? [...dbData?.tables, "__aoijs_vars__"] : ["main", "__aoijs_vars__"]
                    },
                    cacheConfig: {
                        sortFunction: (a, b) => {
                            return a.value - b.value;
                        }
                    },
                    encryptionConfig: {
                        securityKey: dbData?.securityKey ?? "a-32-characters-long-string-here",
                        encriptData: dbData?.encriptData ?? false
                    },
                    ...dbData?.extraOptions
                },
                aoiOptions
            );
        }

        if (Array.isArray(options?.disableFunctions) && options?.disableFunctions.length) {
            options?.disableFunctions.forEach((func) => {
                const index = parser.findIndex((f) => f === func);
                if (index !== -1) {
                    parser.splice(index, 1);
                }
            });
        }

        this.prefix = options.prefix;
        this.#bindEvents();

        Object.defineProperty(this, "statuses", { value: new Group() });

        this.once("clientReady", async () => {
            await require("../events/NonIntents/clientReady.js")(this);
            await require("../events/status.js")(this.statuses, this);
            await require("../events/AoiStart.js")(this);
        });
        this.login(options.token);
    }

    loadCommands(directory, debug = true) {
        const loader = new LoadCommands(this);
        loader.load(this.cmd, directory, debug);
    }

    /**
     * Adds many macros to the manager.
     * @param {import("..").MacroOptions[]} macros - The macros to be added.
     * @returns {void}
     */
    macro(...macros) {
        this.macros.addMany([...macros]);
    }

    status(...statuses) {
        for (const status of statuses) {
            status.type =
                Object.keys(ActivityTypeAvailables).includes(status.type.toLowerCase()) || Object.values(ActivityTypeAvailables).includes(status.type)
                    ? ActivityTypeAvailables[status.type.toLowerCase()]
                    : ActivityTypeAvailables.playing;

            const option = {
                name: status.name,
                type: status.type,
                url: status.url
            };

            this.statuses.set(this.statuses.size, {
                status: status.status || "online",
                time: isNaN(status.time) ? 12 : status.time,
                activity: option,
                afk: status.afk || false,
                shardID: status.shardId || 0
            });
        }
    }

    /**
     * Adds variables to the variable manager.
     * @param {Object} d - The object containing the variables to be added.
     * @param {Object} [table=this.db?.tables?.[0]] - The table to which the variables belong.
     * @throws {TypeError} If a database is not provided.
     */
    variables(d, table = this.db?.tables?.[0]) {
        if (this.db === undefined) {
            throw new TypeError("A database must be provided to use the variables method.");
        }

        for (const [name, value] of Object.entries(d)) {
            this.variableManager.add({ name, value, table });
        }
    }

    async _createCacheFactory(options) {
        options.makeCache = CacheManager._setDjsCacheManagers(options.cache);
    }

    #bindEvents() {
        const bits = new IntentsBitField(this.options.intents);
        for (const event of this.aoiOptions.events ?? []) {
            let intent = EventsToIntents[event];
            const filedir = intent;
            const eventName = EventsToDjsEvents[event];
            const file = EventstoFile[event];
            if (intent === "GuildEmojis") intent = "GuildEmojisAndStickers";
            if (intent === "GuildMessageTypings") intent = "GuildMessageTyping";
            if (intent && !["Custom", "NonIntents"].includes(intent) && !bits.has(intent[0].toUpperCase() + intent.slice(1))) {
                return AoiError.EventError(event, intent, 357);
            }

            try {
                const func = ["shardDisconnect", "shardError", "shardReconnecting", "shardResume", "shardReady", "shardCreate"].includes(event)
                    ? require(`../sharding/${event}.js`)
                    : Array.isArray(file)
                      ? // TODO: remove 'ready' once djs v15 releases
                        file.map((x) => require(`../events/${filedir}/${x === "ready" ? "clientReady" : x}.js`))
                      : require(`../events/${filedir}/${file === "ready" ? "clientReady" : file}.js`);

                this.on(eventName, (...args) => {
                    if (Array.isArray(func)) func.forEach((x) => x(...args, this));
                    else func(...args, this);
                });
            } catch (error) {
                throw new TypeError(`Error loading "${event}" event, does not exist!`);
            }
        }
    }
}

module.exports = BaseClient;
