package me.despawningbone.discordbot;

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.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;

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.MessageBuilder;
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.TextChannel;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.VoiceChannel;
import net.dv8tion.jda.api.entities.Message.Attachment;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceJoinEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceLeaveEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceMoveEvent;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
import net.dv8tion.jda.api.events.message.guild.GuildMessageUpdateEvent;
import net.dv8tion.jda.api.events.user.UserActivityStartEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;

public class EventListener extends ListenerAdapter {
	
	public static Multimap<String, String> testAnimePicCache = ArrayListMultimap.create();
	
	@Override
	public void onGuildMessageReceived(GuildMessageReceivedEvent event) {
		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
		//String[] words = event.getMessage().getContentDisplay().split(" ");
		User author = event.getAuthor();
		TextChannel channel = event.getChannel();
		Message msg = event.getMessage();
		
		//checkAnimePics(event);

		/*if(author.getId().equals("411350223625912323")) {
			channel.sendMessage("|    ||\n||   |_").queue();
		}*/
		//if(event.getMessage().getAttachments().stream().anyMatch(p -> p.getUrl().equals("https://cdn.discordapp.com/attachments/419464220304867348/432108442036207617/TinyPlainAnemoneshrimp-size_restricted.gif"))) event.getMessage().delete().queue();
		if(event.getMessage().getAttachments().stream().anyMatch(p -> p.getUrl().contains("DeepFryer_20191122_160935.jpg"))) event.getMessage().delete().queue();
		//if(event.getGuild().getId().equals("398140934035996687")) channel.getGuild().getRolesByName("Actual robutt role, this one has perms don't fuck with it", true).get(0).getPermissions().forEach(p -> System.out.println(p));
		
		//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?
		}
		
		/*if (String.join("", words).equalsIgnoreCase("!despacito")) {
			channel.sendMessage("https://www.youtube.com/watch?v=W3GrSMYbkBE").queue();
			return;
		}*/
		CommandResult result = null;
		
		try (Connection con = DiscordBot.db.getConnection(); Statement s = con.createStatement()){
			String prefix = MiscUtils.getPrefix(s, event.getGuild().getId());
			if(msg.getContentDisplay().toLowerCase().startsWith(prefix.toLowerCase())) {
				String msgStripped = msg.getContentDisplay().substring(prefix.length()).replaceAll("\\s\\s+", " ");  //merges space
				String[] args = msgStripped.split(" "); // base on command length?
				Command cmd = DiscordBot.commands.get(args[0].toLowerCase());
				cmd = cmd == null ? DiscordBot.aliases.get(args[0].toLowerCase()) : cmd;
				if (cmd != null) {
					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();
					long perm = MiscUtils.getActivePerms(s, channel, cmd);
					//result = cmd.execute(channel, author, msg, args);
					String perms = cmd.hasSubCommand() ? null : MiscUtils.getMissingPerms(perm, cmd.getRequiredBotUserLevel(), event.getMember(), channel);  //pass it to the subcommand handler to handle instead
					if(perms == null || event.getAuthor().getId().equals(DiscordBot.OwnerID)) {  //owner overrides perms for convenience
						/*if(cmd.isDisabled(channel.getGuild().getId())) {
							channel.sendMessage("This command is disabled!").queue();
							result = new CommandResult(CommandResultType.FAILURE, "Disabled command");
						} else {*/
							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") || cmd.isDisabled()) {
						msg.addReaction("❎").queue();
						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?
				}
			} else if(msg.getContentRaw().matches("<@!?" + DiscordBot.BotID + ">")) result = greet(channel, author, prefix);
		} catch (SQLException e) {
			result = new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
		}
		if(result != null) DiscordBot.logger.info("[" + result.getResultType() + "] " + author.getName() + " (" + author.getId() + ") executed "
				+ msg.getContentDisplay() + (result.getRemarks() == null ? "." : ". (" + result.getRemarks() + ")"));
	}
	
	@SuppressWarnings("unused")
	private void checkAnimePics(GuildMessageReceivedEvent event) {
		//TESTING
		CompletableFuture.runAsync(() -> {
			User author = event.getAuthor();
			TextChannel channel = event.getChannel();
			Message msg = event.getMessage();
			List<String> pics = new ArrayList<>();
			if(channel.getId().equals("615396635383562272") && !DiscordBot.BotID.equals(author.getId())) {
				for(Attachment att : msg.getAttachments()) {
					if(att.isImage()) pics.add(att.getUrl());
				}
				/*if(msg.getContentDisplay().contains("http")) {
					for(String s : msg.getContentDisplay().split("http")) {
						try {
							new URL("http" + s);
							pics.add("http" + s);
						} catch(MalformedURLException e) {
							;
						}
					}					
				}*/
				try {
					Thread.sleep(1500);
					msg = channel.retrieveMessageById(msg.getId()).complete();
				} catch (InterruptedException e2) {
					e2.printStackTrace();
				}
				for(MessageEmbed em : msg.getEmbeds()) {
					//System.out.println(em.toJSONObject());
					if(em.getThumbnail() != null) {
						System.out.println("thumb");
						if(em.getSiteProvider() != null && em.getSiteProvider().getName().equals("pixiv")) {  //pixiv doesnt show whole pic in embed
							pics.add(em.getUrl());
						} else {
							pics.add(em.getThumbnail().getUrl());
						}
					} else if(em.getImage() != null && !author.isBot()) {
						System.out.println("img");
						pics.add(em.getImage().getUrl());
					} else {
						System.out.println("url");
						pics.add(em.getUrl());
					}
					
				}
				//System.out.println(pics);
				//System.out.println(testAnimePicCache);
				for(String pic : pics) {  //handle first since these mustnt be the same urls, therefore cache saucenao sauce instead
					String origPic = pic;
					if(!testAnimePicCache.containsValue(pic)) {
						Element saucenao;
						try {
							saucenao = Jsoup.connect("https://saucenao.com/search.php?url=" + pic).get().body();
							Element result = saucenao.selectFirst(".resulttable");
							try {  //normal pixiv/deviantart handling
								if(result.parent().attr("class").equals("result hidden")) throw new NullPointerException();
								Element source = result.selectFirst("strong:contains(ID:)").nextElementSibling();
								pic = source.attr("href");   //will not run if the cache has the value || if the link specified aint a pic 	
							} catch (NullPointerException e1) {  //weird saucenao card formatting (eg episode info), or no result; we dont handle these
								;
							}
						} catch (IOException e) {
							e.printStackTrace();
						}						
					}
					System.out.println("next" + testAnimePicCache);
					if(testAnimePicCache.containsValue(pic)) {
						try {
							Multimap<String, String> temp = ArrayListMultimap.create();
							System.out.println("temp");
							Message m = channel.retrieveMessageById(Multimaps.invertFrom(testAnimePicCache, temp).get(pic).toArray(new String[1])[0]).complete();
							EmbedBuilder eb = new EmbedBuilder();
							eb.setTitle("Repost detected!");
							eb.setDescription("[This image](" + origPic + ")" + (origPic.equals(pic) ? "" : "([source](" + pic + "))") + " is a repost of [this message](" + m.getJumpUrl() + ") by `" + m.getAuthor().getName() + "#" + m.getAuthor().getDiscriminator() + "` at **" + m.getTimeCreated().format(DateTimeFormatter.RFC_1123_DATE_TIME) + "**.\n");
							eb.setThumbnail(origPic);
							channel.sendMessage(eb.build()).queue();
							continue;							
						} catch(NullPointerException e) {  //if the message is deleted
							e.printStackTrace();
						}
					} else {
						System.out.println("put");
						testAnimePicCache.put(msg.getId(), pic);
					}
					
				}
			}
		}).whenComplete((r, t) -> {if(t != null) t.printStackTrace();});
	}
	
	private CommandResult greet(TextChannel channel, User author, String prefix) {
		MessageBuilder smsg = new MessageBuilder();
		String nick = channel.getGuild().getMemberById(author.getId()).getNickname();
		if (nick != null) {
			smsg.append("Yo " + nick + "!\n");
		} else {
			smsg.append("Yo " + author.getName() + "!\n");
		}
		if (author.getId().equals("237881229876133888")) { // easter eggs
			smsg.append("How art thou, my creator?");
		}
		if (author.getId().equals("179824176847257600")) {
			smsg.append("Still stalking as usual, huh? :smirk:");
			smsg.append("\n\"go fix your potatoes\" - pugger");
		}
		if (author.getId().equals("165403578133905408")) {
			smsg.append(
					"You're probably still dead :D (your status), either that or you are playing with your doodle :P");
		}
		if (author.getId().equals("187714189672841216")) {
			smsg.append("Hello, fearsome coder named Morgan :wink:");
		}
		if (author.getId().equals("201768560345743360")) {
			smsg.append("Still need help to save you from the school? :D");
		}
		if (author.getId().equals("257660112703979521")) {
			smsg.append("I like how you only uses me for `!desp roll 1`. :P");
		}
		if (author.getId().equals("272712701988569090")) {
			smsg.append("This guy is generous :smile:");
		}
		if (author.getId().equals("203861130995695616")) {
			smsg.append("He asked my owner to change it so here it is :smile:");
		}
		if (author.getId().equals("206038522971422721")) {
			smsg.append("Appearently he loves !desp idiot :face_palm:");
		}
		if (author.getId().equals("218377806994866176")) {
			smsg.append("Your potatoes seems better than Dank's :smirk:");
		}
		if (author.getId().equals("139316803582033920")) {
			smsg.append("That ironic name tho :stuck_out_tongue:");
		}
		if (author.getId().equals("237058431272484864")) {
			smsg.append(":b:oi");
		}
		if (author.getId().equals("338258756762730496")) {
			smsg.append("**a e s t h e t i c**");
		}
		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=311086271642599424&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 `despawningbone#4078` if you have any questions!\n");
		eb.appendDescription("To get a list of commands, do `" + prefix + "help`.");
		smsg.setEmbed(eb.build());
		Message fmsg = smsg.build();
		
		channel.sendMessage(fmsg).queue();
		return new CommandResult(CommandResultType.SUCCESS, null);
	}
	
	@Override
	public void onGuildMessageUpdate(GuildMessageUpdateEvent event) {
		//System.out.println("edit");   //debug
		Message msg = event.getMessage();
		User author = event.getAuthor();
		TextChannel channel = event.getChannel();
		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();
			//DiscordBot.guildMemberPresence.put(event.getUser().getId(), game); //so that game update to nothing wont be logged
			/*System.out.println(event.getGuild().getName());
			System.out.println(event.getUser().getName());
			System.out.println(game.getName());
			System.out.println(game.getUrl());
			System.out.println(game.getType());
			System.out.println(game.isRich() ? game.asRichPresence().getDetails() : "no rich");
			System.out.println(game.isRich() ? game.asRichPresence().getState() : "no rich");*/
			//OffsetDateTime timesent = OffsetDateTime.now();
			//System.out.println(toolTip);
			String sGame = (toolTip.lastIndexOf(" (") != -1 ? toolTip.substring(0,  toolTip.lastIndexOf(" (")) : toolTip) + "||" + osu.asRichPresence().getDetails();
			//sGame = sGame.replaceAll("'", "''");
			/*OffsetDateTime timeReceived = OffsetDateTime.now();
			long ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS));
			System.out.println("Time taken: " + ms + "ms");*/
			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();
				//con.createStatement().execute("INSERT INTO users(id, game) VALUES (" + event.getUser().getId() + ", '" + sGame + "') ON CONFLICT(id) DO UPDATE SET game = '" + sGame +  "' WHERE game <> '" + sGame + "';" );   //prevent blank updates
			} catch (SQLException e) { //FIXED if i make this not osu only, be aware of SQL injections through sGame (probably being paranoid tho)
				e.printStackTrace();
			}
			/*timeReceived = OffsetDateTime.now();
			ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS));
			System.out.println("Time taken: " + ms + "ms");*/
		}
	}

	//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) {
			ap.getGuildMusicManager(guild).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 onGuildVoiceLeave(GuildVoiceLeaveEvent 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();
		if (vs.inVoiceChannel()) {
			if(!event.getMember().getUser().getId().equals(DiscordBot.BotID)) {
				if (vs.getChannel().equals(event.getChannelLeft())
						&& event.getChannelLeft().getMembers().size() < 2) {
					TextChannel channel = DiscordBot.lastMusicCmd.get(event.getGuild().getId());
					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 {  //got kicked
				System.out.println("Got kicked");
				((Music) DiscordBot.commands.get("music")).getAudioTrackHandler().stopAndClearQueue(event.getGuild());
			}
		}
	}

	@Override
	public void onGuildVoiceMove(GuildVoiceMoveEvent 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();
		if (vs.inVoiceChannel()) {
			String id = event.getGuild().getId();
			TextChannel channel = DiscordBot.lastMusicCmd.get(id);
			if (!event.getMember().getUser().getId().equals(DiscordBot.BotID)) {
				if (vs.getChannel().equals(event.getChannelLeft())) {
					if (!event.getMember().getUser().getId().equals(DiscordBot.BotID)
							&& 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 (vs.getChannel().equals(event.getChannelJoined())) {
					String type = updateActivity(event.getGuild(), event.getChannelJoined());
					if(type != null) channel.sendMessage((type.equals("loop") ? "Looping" : "Autoplay") + " mode disabled due to new users joining the music channel.").queue();
				}
			} else {  //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
					String type = updateActivity(event.getGuild(), event.getChannelJoined());
					if(type != null) channel.sendMessage((type.equals("loop") ? "Looping" : "Autoplay") + " mode disabled due to new users joining the music channel.").queue();
				}
			}
		}
	}

	@Override
	public void onGuildVoiceJoin(GuildVoiceJoinEvent event) {
		GuildVoiceState vs = event.getGuild().getMemberById(DiscordBot.BotID).getVoiceState();
		String id = event.getGuild().getId();
		TextChannel channel = DiscordBot.lastMusicCmd.get(id);
		if(channel != null) {
			if (vs.inVoiceChannel() && !event.getMember().getUser().getId().equals(DiscordBot.BotID)) {
				if (vs.getChannel().equals(event.getChannelJoined())) {
					String type = updateActivity(event.getGuild(), event.getChannelJoined());
					if(type != null) channel.sendMessage((type.equals("loop") ? "Looping" : "Autoplay") + " mode disabled due to new users joining the music channel.").queue();
				}
				
			}
			
		}
	}
}
