/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2025 SteamWar.de-Serverteam
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.comphenix.tinyprotocol;

import de.steamwar.Reflection;
import de.steamwar.Reflection.Field;
import de.steamwar.core.Core;
import io.netty.channel.*;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.server.PluginDisableEvent;
import org.bukkit.plugin.Plugin;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
import java.util.logging.Level;

public class TinyProtocol implements Listener {

	private static final Class<?> craftServer = Reflection.getClass("org.bukkit.craftbukkit.CraftServer");
	private static final Class<?> dedicatedPlayerList = Reflection.getClass("net.minecraft.server.dedicated.DedicatedPlayerList");
	private static final Field<?> getPlayerList = Reflection.getField(craftServer, dedicatedPlayerList, 0);
	private static final Class<?> playerList = Reflection.getClass("net.minecraft.server.players.PlayerList");
	private static final Class<?> minecraftServer = Reflection.getClass("net.minecraft.server.MinecraftServer");
	private static final Field<?> getMinecraftServer = Reflection.getField(playerList, minecraftServer, 0);
	public static final Class<?> serverConnection = Reflection.getClass("net.minecraft.server.network.ServerConnectionListener");
	private static final Field<?> getServerConnection = Reflection.getField(minecraftServer, serverConnection, 0);
	public static Object getServerConnection(Plugin plugin) {
		return getServerConnection.get(getMinecraftServer.get(getPlayerList.get(plugin.getServer())));
	}
	private static final Class<?> networkManager = Reflection.getClass("net.minecraft.network.NetworkManager");
	public static final Field<List> networkManagers = Reflection.getField(serverConnection, List.class, 0, networkManager);

	private static final String HANDLER_NAME = "tiny-steamwar";
	public static final TinyProtocol instance = new TinyProtocol(Core.getInstance());

	public static void init() {
		//enforce init
	}

	private final Plugin plugin;
	private final List<?> connections;
	private boolean closed;

	private final Map<Class<?>, List<BiFunction<Player, Object, Object>>> packetFilters = new HashMap<>();
	@Getter
	private final Map<Player, PacketInterceptor> playerInterceptors = new HashMap<>();

	@Override
	public String toString() {
		return "TinyProtocol{" +
				"plugin=" + plugin +
				", connections=" + connections +
				", closed=" + closed +
				", packetFilters=" + packetFilters +
				", playerInterceptors=" + playerInterceptors +
				'}';
	}

	private TinyProtocol(final Plugin plugin) {
		this.plugin = plugin;
		this.connections = networkManagers.get(getServerConnection(plugin));

		plugin.getServer().getPluginManager().registerEvents(this, plugin);

		for (Player player : plugin.getServer().getOnlinePlayers()) {
			new PacketInterceptor(player);
		}
	}

	@EventHandler(priority = EventPriority.LOWEST)
	public void onPlayerLogin(PlayerLoginEvent e) {
		plugin.getLogger().info("Creating PacketInterceptor for: " + e.getPlayer().getName() + " (" + closed + ")");
		if(closed)
			return;
		new PacketInterceptor(e.getPlayer());
	}

	@EventHandler(priority = EventPriority.MONITOR)
	public void onPlayerDisconnect(PlayerQuitEvent e) {
		getInterceptor(e.getPlayer()).ifPresent(PacketInterceptor::close);
	}

	@EventHandler
	public void onPluginDisable(PluginDisableEvent e) {
		if (e.getPlugin().equals(plugin)) {
			close();
		}
	}

	public void addFilter(Class<?> packetType, BiFunction<Player, Object, Object> filter) {
		packetFilters.computeIfAbsent(packetType, c -> new CopyOnWriteArrayList<>()).add(filter);
	}

	public void removeFilter(Class<?> packetType, BiFunction<Player, Object, Object> filter) {
		packetFilters.getOrDefault(packetType, Collections.emptyList()).remove(filter);
	}

	public void sendPacket(Player player, Object packet) {
		getInterceptor(player).ifPresent(i -> i.sendPacket(packet));
	}

	public void receivePacket(Player player, Object packet) {
		getInterceptor(player).ifPresent(i -> i.receivePacket(packet));
	}

	public final void close() {
		plugin.getLogger().log(Level.INFO, "Closing PacketInterceptor", new Exception("Stacktrace"));

		if(closed)
			return;
		closed = true;

		HandlerList.unregisterAll(this);

		for (Player player : plugin.getServer().getOnlinePlayers()) {
			getInterceptor(player).ifPresent(PacketInterceptor::close);
		}
	}

	private Optional<PacketInterceptor> getInterceptor(Player player) {
		synchronized (playerInterceptors) {
			return Optional.ofNullable(playerInterceptors.get(player));
		}
	}

	private static final Field<Channel> getChannel = Reflection.getField(networkManager, Channel.class, 0);
	private static final Field<UUID> getUUID = Reflection.getField(networkManager, UUID.class, 0);

	public final class PacketInterceptor extends ChannelDuplexHandler {
		private final Player player;
		@Getter
		private final Channel channel;

		private PacketInterceptor(Player player) {
			this.player = player;

			channel = connections.stream().filter(connection -> player.getUniqueId().equals(getUUID.get(connection))).map(getChannel::get).filter(Channel::isActive).findAny().orElseThrow(() -> {
				Bukkit.getScheduler().runTask(plugin, () -> player.kickPlayer("Connection failure."));
				return new SecurityException("Could not find channel for player " + player.getName());
			});

			if(!channel.isActive())
				return;

			synchronized (playerInterceptors) {
				playerInterceptors.put(player, this);
			}
			plugin.getLogger().info("Adding Techhider for: " + player.getName());

			try {
				channel.pipeline().addBefore("packet_handler", HANDLER_NAME, this);
			} catch (IllegalArgumentException | NoSuchElementException e) {
				Bukkit.getScheduler().runTask(plugin, () -> player.kickPlayer("Connection failure."));
				throw new SecurityException(e);
			}
        }

		private void sendPacket(Object packet) {
			channel.pipeline().writeAndFlush(packet);
		}

		private void receivePacket(Object packet) {
			channel.pipeline().context("encoder").fireChannelRead(packet);
		}

		private void close() {
			if(channel.isActive()) {
				channel.eventLoop().execute(() -> {
					try {
						channel.pipeline().remove(HANDLER_NAME);
					} catch (NoSuchElementException e) {
						// ignore
					}
				});
			}

			synchronized (playerInterceptors) {
				playerInterceptors.remove(player, this);
			}
		}

		@Override
		public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
			try {
				msg = filterPacket(player, msg);
			} catch (Exception e) {
				plugin.getLogger().log(Level.SEVERE, "Error during incoming packet processing", e);
			}

			if (msg != null) {
				super.channelRead(ctx, msg);
			}
		}

		@Override
		public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
			try {
				msg = filterPacket(player, msg);
			} catch (Exception e) {
				plugin.getLogger().log(Level.SEVERE, "Error during outgoing packet processing", e);
			}

			if (msg != null) {
				super.write(ctx, msg, promise);
			}
		}

		private Object filterPacket(Player player, Object packet) {
			List<BiFunction<Player, Object, Object>> filters = packetFilters.getOrDefault(packet.getClass(), Collections.emptyList());

			for(BiFunction<Player, Object, Object> filter : filters) {
				packet = filter.apply(player, packet);

				if(packet == null)
					break;
			}

			return packet;
		}
	}
}
