import path from "path";
import fs from "fs";
import { Message, Client, ClientEvents } from "discord.js";
import { EventType, BotEvent, ICommand, isPlugin, BotEventData, isCustomEvent, IPlugin, CommandType } from "./model/plugin";
import { isAuthorisedAsync } from "./util";


interface IDocumentationData {
    type: CommandType;
    name: string;
    doc?: string;
    example?: string;
    auth: boolean;
}

type BotEventCollection = { [event in EventType]?: BotEvent[] };

export class PluginManager {
    private plugins: IPlugin[] = [];
    private commands: ICommand[] = [];
    private botEvents: BotEventCollection = {};

    constructor(private cmdPath: string) {
        this.init();
    }

    private init(): void {
        const files = fs.readdirSync(this.cmdPath);

        for (const file of files) {
            const ext = path.extname(file);
            if (ext != ".js")
                continue;

            // eslint-disable-next-line @typescript-eslint/no-var-requires
            this.loadCommand(require(path.resolve(this.cmdPath, file)));
        }
    }

    private loadCommand(mod: Record<string, unknown>) {
        const getBotEventArray = (evtType: EventType) => {
            if (!this.botEvents[evtType])
                this.botEvents[evtType] = [];
            return this.botEvents[evtType];
        };

        for (const i in mod) {
            if (!Object.prototype.hasOwnProperty.call(mod, i))
                continue;
    
            const commandClass = mod[i] as unknown;
            // Ensure this is indeed a command class
            if (!isPlugin(commandClass))
                continue;
    
            const cmd = new commandClass();
            this.plugins.push(cmd);
    
            if (cmd.botCommands)
                this.commands.push(...cmd.botCommands.map(c => ({ ...c, action: c.action.bind(cmd) })));
    
            if (cmd.botEvents)
                for (const [i, event] of Object.entries(cmd.botEvents)) {
                    getBotEventArray(i as EventType)?.push((event as BotEvent).bind(cmd));
                }
        }
    }

    get documentation(): IDocumentationData[] {
        return this.commands.filter(m => m.documentation !== undefined).map(m => ({
            type: m.type,
            name: m.pattern.toString(),
            doc: m.documentation?.description,
            example: m.documentation?.example,
            auth: m.auth || false
        }));
    }

    async start(client: Client): Promise<void> {
        for (const evtName of Object.keys(this.botEvents)) {
            if (!Object.prototype.hasOwnProperty.call(this.botEvents, evtName) || isCustomEvent(evtName))
                continue;
            client.on(evtName as keyof ClientEvents, async (...args: unknown[]) =>
                await this.trigger(evtName as EventType, ...args)
            );
        }
    
        for (const plugin of this.plugins) {
            if (plugin.start)
                await plugin.start();
        }
    }

    async trigger(event: EventType, ...params: unknown[]): Promise<boolean> {
        const eventData: BotEventData = {
            actionsDone: false
        };
        const eventHandlers = this.botEvents[event];
        if (!eventHandlers)
            return eventData.actionsDone;

        for (const handler of eventHandlers) {
            const actionResult = handler(eventData, ...params);
            if (actionResult instanceof Promise)
                await actionResult;
        }
        
        return eventData.actionsDone;
    }

    async runCommand(type: CommandType, m: Message, content: string): Promise<boolean> {
        const lowerCaseContent = content.toLowerCase();
        for (const c of this.commands) {
            if (c.type != type)
                continue;
            let match = false;
            let matchData: string | RegExpMatchArray = "";
            if (typeof (c.pattern) == "string" && lowerCaseContent.startsWith(c.pattern)) {
                match = true;
                matchData = c.pattern;
            }
            else if (c.pattern instanceof RegExp) {
                const result = c.pattern.exec(content);
                if (result != null) {
                    match = true;
                    matchData = result;
                }
            }

            if (match) {
                if (c.auth && !(await isAuthorisedAsync(m.member)))
                    return false;
                const eventResult = c.action({ message: m, contents: matchData });
                if (eventResult instanceof Promise)
                    await eventResult;
                return true;
            }
        }
        return false;
    }
}