import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, Guild, IntentsBitField, Snowflake, TextChannel } from "discord.js";
import fs from 'fs';
import { ScheduledTask, schedule } from "node-cron";
import { v4 as uuid } from 'uuid';
import { manageAnnouncementRoles } from "../commands/announce";
import { config } from "../configuration";
import { Maybe } from "../interfaces";
import { JellyfinHandler } from "../jellyfin/handler";
import { logger } from "../logger";
import { CommandType } from "../types/commandTypes";
import { isInitialAnnouncement } from "../helper/messageIdentifiers";
import VoteController from "../helper/vote.controller";
import { yavinJellyfinHandler } from "../..";



export class ExtendedClient extends Client {
	private eventFilePath = `${__dirname}/../events`
	private commandFilePath = `${__dirname}/../commands`
	private jellyfin: JellyfinHandler
	public voteController: VoteController = new VoteController(this, yavinJellyfinHandler)
	public commands: Collection<string, CommandType> = new Collection()
	private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel
	private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection() //one task per guild
	private pollCloseBackgroundTasks: Collection<string, ScheduledTask> = new Collection()
	public constructor(jf: JellyfinHandler) {
		const intents: IntentsBitField = new IntentsBitField()
		intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildMessageReactions, IntentsBitField.Flags.GuildVoiceStates)
		const options: ClientOptions = { intents }
		super(options)
		this.jellyfin = jf
	}
	public async start() {
		if (process.env.NODE_ENV === 'test') return
		const promises: Promise<any>[] = []
		promises.push(this.registerSlashCommands())
		promises.push(this.registerEventCallback())
		Promise.all(promises).then(() => {
			this.login(config.bot.token)
		})
	}
	private async importFile(filepath: string): Promise<any> {
		logger.debug(`Importing ${filepath}`)
		const imported = await import(filepath)
		logger.debug(`Imported ${JSON.stringify(imported)}`)
		return imported.default ?? imported
	}
	public async registerCommands(cmds: ApplicationCommandDataResolvable[], guildIds: Collection<Snowflake, Guild>) {
		if (guildIds) {
			guildIds.forEach(guild => {
				this.guilds.cache.get(guild.id)?.commands.set(cmds)
				logger.info(`Registering commands to ${guild.name}|${guild.id}`)
			})
		} else {
			this.application?.commands.set(cmds)
			logger.info(`Registering global commands`)
		}
		return
	}
	public async registerSlashCommands(): Promise<void> {
		try {
			const slashCommands: ApplicationCommandDataResolvable[] = []
			const commandFiles = fs.readdirSync(this.commandFilePath).filter(file => file.endsWith('.ts') || file.endsWith('.js'))
			for (const commandFile of commandFiles) {
				const filePath = `${this.commandFilePath}/${commandFile}`
				const command = await this.importFile(filePath)
				logger.debug(JSON.stringify(command))
				if (!command.name) return
				this.commands.set(command.name, command)
				slashCommands.push(command)
			}
			this.on("ready", async (client: Client) => {
				//logger.info(`Ready processing ${JSON.stringify(client)}`)
				logger.info(`SlashCommands: ${JSON.stringify(slashCommands)}`)
				const guilds = client.guilds.cache

				this.registerCommands(slashCommands, guilds)
				this.cacheUsers(guilds)
				await this.cacheAnnouncementServer(guilds)
				this.fetchAnnouncementChannelMessage(this.announcementChannels)
				this.startAnnouncementRoleBackgroundTask(guilds)
				this.startPollCloseBackgroundTasks()
			})
		} catch (error) {
			logger.info(`Error refreshing slash commands: ${error}`)
		}
	}
	/**
		* Fetches all messages from the provided channel collection.
		* This is necessary for announcementChannels, because 'old' messages don't receive
		* messageReactionAdd Events, only messages that were sent while the bot is online are tracked
		* automatically.
		* To prevent the need for a dedicated 'Collector' implementation which would listen on specific
		* it's easiest to just fetch all messages from the backlog, which automatically makes the bot track them
		* again.
		* @param {Collection<string, TextChannel>} channels - All channels which should be fecthed for reactionTracking	
		*/
	private async fetchAnnouncementChannelMessage(channels: Collection<string, TextChannel>): Promise<void> {
		channels.each(async ch => {
			ch.messages.fetch()
		})
	}
	private async cacheAnnouncementServer(guilds: Collection<Snowflake, Guild>) {
		for (const guild of guilds.values()) {
			const channels: TextChannel[] = <TextChannel[]>(await guild.channels.fetch())
				?.filter(channel => channel?.id === config.bot.announcement_channel_id)
				.map((value) => value)

			if (!channels || channels.length != 1) {
				logger.error(`Could not find announcement channel for guild ${guild.name} with guildId ${guild.id}. Found ${channels}`)
				continue
			}
			logger.info(`Fetched announcement channel: ${JSON.stringify(channels[0])}`)
			this.announcementChannels.set(guild.id, channels[0])
		}
	}
	public getAnnouncementChannelForGuild(guildId: string): Maybe<TextChannel> {
		return this.announcementChannels.get(guildId)
	}
	public async cacheUsers(guilds: Collection<Snowflake, Guild>) {
		guilds.forEach((guild: Guild, id: Snowflake) => {
			logger.info(`Fetching members for ${guild.name}|${id}`)
			guild.members.fetch()
			logger.info(`Fetched: ${guild.memberCount} members`)
		})
	}
	public async registerEventCallback() {
		try {
			const eventFiles = fs.readdirSync(this.eventFilePath).filter(file => file.endsWith('.ts') || file.endsWith('.js'));
			for (const file of eventFiles) {
				const filePath = `${this.eventFilePath}/${file}`
				const event = await this.importFile(filePath)
				if (event.once) {
					logger.info(`Registering once ${file}`)
					this.once(event.name, (...args: any[]) => event.execute(...args))
				}
				else {
					logger.info(`Registering on ${file}`)
					this.on(event.name, (...args: any[]) => event.execute(...args))
				}
			}
			logger.info(`Registered event names ${this.eventNames()}`)
		} catch (error) {
			logger.error(error)
		}
	}

	public async startAnnouncementRoleBackgroundTask(guilds: Collection<string, Guild>) {
		for (const guild of guilds.values()) {
			logger.info("Starting background task for announcement role", { guildId: guild.id })
			const textChannel: Maybe<TextChannel> = this.getAnnouncementChannelForGuild(guild.id)
			if (!textChannel) {
				logger.error("Could not find announcement channel. Aborting", { guildId: guild.id })
				return
			}
			this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
				const requestId = uuid()
				const messages = (await textChannel.messages.fetchPinned()).filter(message => isInitialAnnouncement(message))

				if (messages.size > 1) {
					logger.error("More than one pinned announcement Messages found. Unable to know which one people react to. Please fix!", { guildId: guild.id, requestId })
					return
				} else if (messages.size == 0) {
					logger.error("Could not find any pinned announcement messages. Unable to manage roles!", { guildId: guild.id, requestId })
					return
				}

				const message = await messages.at(0)?.fetch()
				if (!message) {
					logger.error(`No pinned message found`, { guildId: guild.id, requestId })
					return
				}
				//logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId })

				const reactions = message.reactions.resolve("🎫")
				//logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId })
				if (reactions) {
					manageAnnouncementRoles(message.guild, reactions, requestId)
				} else {
					logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId })
				}
			}))
		}
	}

	public stopAnnouncementRoleBackgroundTask(guildId: string, requestId: string) {
		const task: Maybe<ScheduledTask> = this.announcementRoleHandlerTask.get(guildId)
		if (!task) {
			logger.error(`No task found for guildID ${guildId}.`, { guildId, requestId })
			return
		}
		task.stop()
	}

	private async startPollCloseBackgroundTasks() {
		for (const guild of this.guilds.cache) {
			this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => this.voteController.checkForPollsToClose(guild[1])))
		}
	}
}
