package me.despawningbone.discordbot;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.exception.ExceptionUtils;

import me.despawningbone.discordbot.command.Command;
import me.despawningbone.discordbot.command.CommandResult;
import me.despawningbone.discordbot.command.CommandResult.CommandResultType;
import me.despawningbone.discordbot.command.music.AudioTrackHandler;
import me.despawningbone.discordbot.command.music.GuildMusicManager;
import me.despawningbone.discordbot.command.music.Music;
import me.despawningbone.discordbot.utils.MiscUtils;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.entities.Message.Attachment;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
import net.dv8tion.jda.api.events.user.UserActivityStartEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;

public class EventListener extends ListenerAdapter {
	
	@Override
	public void onMessageReceived(MessageReceivedEvent event) {
		//do not support DMs for now
		if(!event.isFromGuild()) return;
		
		DiscordBot.mainJDA = event.getJDA(); // refresh it?  //no need, everything's getJDA() is the same jda basis but audioplayer's still using it; need to migrate to store in guildmusicmanager
		User author = event.getAuthor();
		TextChannel channel = event.getChannel().asTextChannel();
		Message msg = event.getMessage();

		//DONE use log4j
		//logging
		if (!DiscordBot.logExcemptID.contains(author.getId())) {
			String guildinfo = "[" + event.getGuild().getName() + " #" + channel.getName() + "]";
			String payload = "[INFO] " + guildinfo + System.lineSeparator() + "  " + msg + System.lineSeparator() + "    Full msg: " + msg.getContentDisplay();
			List<Attachment> att = event.getMessage().getAttachments();
			if (!att.isEmpty()) {
				payload += System.lineSeparator() + "    Attachments:";
				for (int i = 0; i < att.size(); i++) {
					payload += System.lineSeparator() + "      " + att.get(i).getUrl();
				}
			}
			DiscordBot.logger.trace(payload);   //TODO log embeds and add user id too?
		}
		
		//parse cmd
		CommandResult result = null;
		try (Connection con = DiscordBot.db.getConnection(); Statement s = con.createStatement()){
			//check prefix
			String prefix = MiscUtils.getPrefix(s, event.getGuild().getId());
			if(msg.getContentDisplay().toLowerCase().startsWith(prefix.toLowerCase())) {
				//preprocess args
				String msgStripped = msg.getContentDisplay().substring(prefix.length()).replaceAll("\\s\\s+", " ");  //merges space
				String[] args = msgStripped.split(" "); // base on command length?
				
				//get cmd from args
				Command cmd = DiscordBot.commands.get(args[0].toLowerCase());
				cmd = cmd == null ? DiscordBot.aliases.get(args[0].toLowerCase()) : cmd;
				if (cmd != null) {
					
					//check if banned
					ResultSet uRs = s.executeQuery("SELECT reports FROM users WHERE id = " + author.getId() + ";");
					if(uRs.next()) {   //checks null at the same time
						if(uRs.getString(1).split("\n").length >= 5) {
							channel.sendMessage("You are banned from using the bot.").queue();
							DiscordBot.logger.info("[WARN] " + author.getName() + " (" + author.getId() + ") tried to execute " + msg.getContentDisplay() + " but was banned.");
							return;
						}
					}
					uRs.close();
					
					//check perms
					long perm = MiscUtils.getActivePerms(s, channel, cmd);
					String perms = cmd.hasSubCommand() ? null : MiscUtils.getMissingPerms(perm, cmd.getRequiredBotUserLevel(), event.getMember(), channel);  //pass it to the subcommand handler to handle instead
					if(cmd.isDisabled()) perms = "DISABLED";   //override if disabled by code
							
					if(perms == null || event.getAuthor().getId().equals(DiscordBot.OwnerID)) {  //owner overrides perms for convenience
						
						//execute async
						cmd.executeAsync(channel, author, msg, Arrays.copyOfRange(args, 1, args.length), r -> {   //catch all exceptions?  //should have actually
							DiscordBot.logger.info("[" + r.getResultType() + "] " + author.getName() + " (" + author.getId() + ") executed "
									+ msg.getContentDisplay() + (r.getRemarks() == null ? "." : ". (" + r.getRemarks() + ")"));  //logging has to be before sendMessage, or else if no permission it will just quit
							if(r.getMessage() != null) channel.sendMessage(r.getMessage()).queue();
						});  //dont know if async will screw anything up   //wont, TODO log date and which server executed the command also?
						return;
						
					} else if(perms.equals("DISABLED")) {
						author.openPrivateChannel().queue(c ->  //notify user whats wrong if possible
							c.sendMessage("Sorry, but the command `" + msg.getContentDisplay() + "` is disabled in the channel `#" + channel.getName() + "`.").queue());
						msg.addReaction(Emoji.fromUnicode("❎")).queue();  //instead of sending messages, react instead to avoid clutter (which is what most ppl disable a bot in a channel for)
						result = new CommandResult(CommandResultType.DISABLED);						
					} else {
						result = new CommandResult(CommandResultType.NOPERMS, perms);
						channel.sendMessage(result.getMessage()).queue();
					}		
				} else {
					result = new CommandResult(CommandResultType.FAILURE, "Invalid command");
					//do more stuff?
				}
				
			//non prefix "command" - greetings with tagging
			} else if(msg.getContentRaw().matches("<@!?" + DiscordBot.BotID + ">")) {
				result = greet(channel, author, prefix);
			}
		} catch (SQLException e) {
			result = new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
		}
		
		//log non async results
		if(result != null) DiscordBot.logger.info("[" + result.getResultType() + "] " + author.getName() + " (" + author.getId() + ") executed "
				+ msg.getContentDisplay() + (result.getRemarks() == null ? "." : ". (" + result.getRemarks() + ")"));
	}
	
	
	//init easter eggs file
	private Properties greetEasterEggs = new Properties();
	{
		try(FileInputStream in = new FileInputStream(new File(System.getProperty("user.dir") + File.separator + "eastereggs.properties"))){
			greetEasterEggs.load(in);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	private CommandResult greet(TextChannel channel, User author, String prefix) {
		MessageCreateBuilder smsg = new MessageCreateBuilder();
		
		//main greet
		String nick = channel.getGuild().getMemberById(author.getId()).getNickname();
		if (nick != null) {
			smsg.addContent("Yo " + nick + "!\n");
		} else {
			smsg.addContent("Yo " + author.getName() + "!\n");
		}
		
		//easter eggs
		String easterEgg = greetEasterEggs.getProperty(author.getId());
		if(easterEgg != null) {
			smsg.addContent(easterEgg.replace("\\n", "\n") + "\n");
		}
		
		//bot info
		EmbedBuilder eb = new EmbedBuilder();
		eb.setColor(0x051153);   //TODO version info based on git commits 
		eb.appendDescription("This bot is running **despbot v1.5.0**, Shard `" + DiscordBot.mainJDA.getShardInfo().getShardString() + "`. ([invite me!](https://discordapp.com/oauth2/authorize?&client_id=" + DiscordBot.BotID + "&scope=bot&permissions=0))\n");
		eb.appendDescription("Connected guilds: `" + DiscordBot.mainJDA.getGuildCache().size() + (DiscordBot.mainJDA.getShardManager() == null ? "" : "/" + DiscordBot.mainJDA.getShardManager().getShards().stream().mapToLong(jda -> jda.getGuildCache().size()).sum()) + "`; ");
		eb.appendDescription("Total members (cached): `" + DiscordBot.mainJDA.getUserCache().size() + (DiscordBot.mainJDA.getShardManager() == null ? "" : "/" + DiscordBot.mainJDA.getShardManager().getShards().stream().mapToLong(jda -> jda.getUserCache().size()).sum()) + "`\n");
		eb.appendDescription("DM `" + DiscordBot.mainJDA.getUserById(DiscordBot.OwnerID).getAsTag() + "` if you have any questions!\n");
		eb.appendDescription("To get a list of commands, do `" + prefix + "help`.");
		smsg.setEmbeds(eb.build());
		MessageCreateData fmsg = smsg.build();
		
		channel.sendMessage(fmsg).queue();
		return new CommandResult(CommandResultType.SUCCESS, null);
	}
	
	
	@Override
	public void onMessageUpdate(MessageUpdateEvent event) { //log edits
		//do not support DMs for now
		if(!event.isFromGuild()) return;
		
		Message msg = event.getMessage();
		User author = event.getAuthor();
		TextChannel channel = event.getChannel().asTextChannel();
		if (!DiscordBot.logExcemptID.contains(author.getId())) {
			String guildinfo = "[" + event.getGuild().getName() + " #" + channel.getName() + "]";
			DiscordBot.logger.trace("[EDIT] " + guildinfo + System.lineSeparator() + "  " + msg + System.lineSeparator() + "    Full edited msg: " + msg.getContentDisplay());
		}
	}

	@Override
	public void onUserActivityStart(UserActivityStartEvent event) { //store presence for checking osu pp
		Activity osu = event.getNewActivity();
		if (osu != null && osu.getName().equals("osu!") && osu.isRich()  //if need to include other games and details, just remove this if clause and change sGame below
				&& osu.asRichPresence().getDetails() != null) {
			String toolTip = osu.asRichPresence().getLargeImage().getText();
			String sGame = (toolTip.lastIndexOf(" (") != -1 ? toolTip.substring(0,  toolTip.lastIndexOf(" (")) : toolTip) + "||" + osu.asRichPresence().getDetails();
			
			//update db
			try (Connection con = DiscordBot.db.getConnection()) {  //DONE do i need to close the statement?
				PreparedStatement s = con.prepareStatement("INSERT INTO users(id, game) VALUES (" + event.getUser().getId() + ", ?) ON CONFLICT(id) DO UPDATE SET game = ? WHERE game <> ?;" );   //prevent blank updates
				s.setString(1, sGame); s.setString(2, sGame); s.setString(3, sGame);
				s.execute();
			} catch (SQLException e) { //FIXED if i make this not osu only, be aware of SQL injections through sGame (probably being paranoid tho)
				e.printStackTrace();
			}
		}
	}

	
	//TODO only update when not paused?
	//TODO update on member deafen?
	private void waitActivity(Guild guild) {
		AudioTrackHandler ap = ((Music) DiscordBot.commands.get("music")).getAudioTrackHandler();
		ap.getGuildMusicManager(guild).player.setPaused(true);
		ap.getGuildMusicManager(guild).clearQueueCleanup = ap.ex.schedule(() -> {
			ap.stopAndClearQueue(guild);
			DiscordBot.lastMusicCmd.get(guild.getId()).sendMessage("The queue has been cleared.").queue();
			ap.getGuildMusicManager(guild).clearQueueCleanup = null;
		}, 1, TimeUnit.MINUTES);
	}
	
	private String updateActivity(Guild guild, VoiceChannel vc) {
		AudioTrackHandler ap = ((Music) DiscordBot.commands.get("music")).getAudioTrackHandler();
		GuildMusicManager mm = ap.getGuildMusicManager(guild);
		if(mm.clearQueueCleanup != null) {
			mm.player.setPaused(false);
			mm.clearQueueCleanup.cancel(true);
			mm.clearQueueCleanup = null;
		}
		
		String type = mm.scheduler.loop;  //mm cannot be null
		if (type != null
				&& vc.getMembers().size() > 2 
				&& vc.getMembers().contains(guild.getMemberById(DiscordBot.OwnerID))) { 
			ap.toggleLoopQueue(guild, type);
			return type;
		}	
		return null;
	}
	
	@Override
	public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) { // theoretically i dont need to check if lastMusicCmd has the entry or not, as it must have one to trigger this
		GuildVoiceState vs = event.getGuild().getMemberById(DiscordBot.BotID).getVoiceState();
		String id = event.getGuild().getId();
		TextChannel channel = DiscordBot.lastMusicCmd.get(id);
		if(!event.getMember().getUser().getId().equals(DiscordBot.BotID)) {
			if(event.getChannelJoined() != null) {
				if (vs.getChannel().equals(event.getChannelJoined())) {
					String type = updateActivity(event.getGuild(), event.getChannelJoined().asVoiceChannel());
					if(type != null) channel.sendMessage((type.equals("loop") ? "Looping" : "Autoplay") + " mode disabled due to new users joining the music channel.").queue();
				}
			} else if(event.getChannelLeft() != null) {
				if (vs.getChannel().equals(event.getChannelLeft())
						&& event.getChannelLeft().getMembers().size() < 2) {
					channel.sendMessage(
							"All users have left the music channel, the player is now paused.\nThe queue will be cleared in 1 minute if there is no activity.")
							.queue();
					waitActivity(event.getGuild());
				}	
			}
		} else {
			if(event.getChannelJoined() != null && event.getChannelLeft() != null) {   //got moved
				//moved bot to empty channel
				if(event.getChannelJoined().getMembers().size() < 2) {
					channel.sendMessage(
							"The bot has been moved to an empty channel, the player is now paused.\nThe queue will be cleared in 1 minute if there is no activity.")
							.queue();
					waitActivity(event.getGuild());
				} else {  //moved bot to channel with ppl
					event.getGuild().getAudioManager().openAudioConnection(event.getChannelJoined());   //seems to need explicit reconnect on bot move, it gets stuck on attempting to reconnect otherwise; probably would be fixed soon but hot patch for now
					String type = updateActivity(event.getGuild(), event.getChannelJoined().asVoiceChannel());
					if(type != null) channel.sendMessage((type.equals("loop") ? "Looping" : "Autoplay") + " mode disabled due to new users joining the music channel.").queue();
				}
			} else if(event.getChannelLeft() != null) {  //got kicked
				AudioTrackHandler ap = ((Music) DiscordBot.commands.get("music")).getAudioTrackHandler();
				if(ap != null) {  //if not destroyed
					ap.stopAndClearQueue(event.getGuild());  //can double fire on normal end; shouldnt be too big of a problem				
				}
			}
			//we dont care about the bot joining a channel
		}
	}
}
