import {
    ApplicationCommand,
    ApplicationCommandSubCommandData,
    ApplicationCommandSubGroupData,
    ChatInputApplicationCommandData,
    ChatInputCommandInteraction,
} from "discord.js"

export const noSubcommandError = "No subcommand was provided, but one is required."
export const invalidSubcommandGroupError = "The subcommand group provided does not exist."
export const invalidSubcommandError = "The subcommand provided does not exist."

export const adminId = "126936953789743104"

export abstract class CommandData {
    private _cachedGlobal: ApplicationCommand | null = null
    private _cacheByGuild: Record<string, ApplicationCommand> = {}

    abstract get definition(): ChatInputApplicationCommandData

    getCached(guildId: string | null): ApplicationCommand {
        if (guildId === null) {
            if (this._cachedGlobal === null) {
                throw Error("Not yet cached")
            }
            return this._cachedGlobal
        } else if (this._cacheByGuild.hasOwnProperty(guildId)) {
            return this._cacheByGuild[guildId]
        } else {
            throw Error("Not yet cached")
        }
    }

    setCached(command: ApplicationCommand) {
        if (command.guildId === null) {
            this._cachedGlobal = command
        } else {
            this._cacheByGuild[command.guildId] = command
        }
    }

    abstract execute(b: ChatInputCommandInteraction): Promise<void>
}

export type BaseChatInputCommandData = Omit<ChatInputApplicationCommandData, "options">

export abstract class CommandWithSubcommandsData extends CommandData {
    private _subcommandGroupsCache: Record<string, SubcommandGroupData> | null = null
    private _subcommandsCache: Record<string, SubcommandData> | null = null

    get definition(): ChatInputApplicationCommandData {
        return {
            options: this.subcommands.map((s) => s.definition),
            ...this.baseDefinition,
        }
    }

    protected abstract get baseDefinition(): BaseChatInputCommandData

    protected abstract get subcommands(): (SubcommandData | SubcommandGroupData)[]

    async execute(b: ChatInputCommandInteraction) {
        const group = b.options.getSubcommandGroup()
        const subcommand = b.options.getSubcommand()
        if (group !== null) {
            await this.resolveSubcommandGroup(group).execute(b)
        } else if (subcommand !== null) {
            await this.resolveSubcommand(subcommand).execute(b)
        } else {
            throw noSubcommandError
        }
    }

    protected resolveSubcommandGroup(name: string): SubcommandGroupData {
        let cache = this._subcommandGroupsCache
        if (cache === null) {
            cache = {}
            for (const item of this.subcommands) {
                if (item instanceof SubcommandGroupData) {
                    cache[item.definition.name] = item
                }
            }
            this._subcommandGroupsCache = cache
        }
        if (!cache.hasOwnProperty(name)) {
            throw invalidSubcommandGroupError
        }
        return cache[name]
    }

    protected resolveSubcommand(name: string): SubcommandData {
        let cache = this._subcommandsCache
        if (cache === null) {
            cache = {}
            for (const item of this.subcommands) {
                if (item instanceof SubcommandData) {
                    cache[item.definition.name] = item
                }
            }
            this._subcommandsCache = cache
        }
        if (!cache.hasOwnProperty(name)) {
            throw invalidSubcommandGroupError
        }
        return cache[name]
    }
}

export type BaseSubcommandGroupData = Omit<ApplicationCommandSubGroupData, "options">

export abstract class SubcommandGroupData {
    private _subcommandsCache: Record<string, SubcommandData> | null = null

    get definition(): ApplicationCommandSubGroupData {
        return {
            ...this.baseDefinition,
            options: this.subcommands.map((s) => s.definition),
        }
    }

    protected abstract get baseDefinition(): BaseSubcommandGroupData

    protected abstract get subcommands(): SubcommandData[]

    async execute(b: ChatInputCommandInteraction): Promise<void> {
        const subcommand = b.options.getSubcommand()
        if (subcommand !== null) {
            await this.resolveSubcommand(subcommand).execute(b)
        } else {
            throw noSubcommandError
        }
    }

    protected resolveSubcommand(name: string): SubcommandData {
        let cache = this._subcommandsCache
        if (cache === null) {
            cache = {}
            for (const item of this.subcommands) {
                cache[item.definition.name] = item
            }
            this._subcommandsCache = cache
        }
        if (!cache.hasOwnProperty(name)) {
            throw invalidSubcommandError
        }
        return cache[name]
    }
}

export abstract class SubcommandData {
    abstract get definition(): ApplicationCommandSubCommandData

    abstract execute(b: ChatInputCommandInteraction): Promise<void>
}