"use strict";

// =======
// Imports
// =======

const mcproxy = require("@rob9315/mcproxy");
const mc = require("minecraft-protocol");

const { config, status, updateStatus, updateCoordinatorStatus } = require("./util/config.js");
const logger = require("./util/logger.js");
const notifier = require("./util/notifier.js");
const chatty = require("./util/chatty.js");
const ngrok = require("./util/ngrok.js");
const mineflayer = require("./util/mineflayer.js");
const queue = require("./util/queue.js");
const downloader = require("./util/downloader.js");
const webserver = require("./util/webserver.js");


// ===========
// Global Vars
// ===========

let conn;
let client;
let server;

// ==============
// Initialization
// ==============

// Max out the threadpool (if enabled)
if (config.experimental.maxThreadpool.active) {
	process.env.UV_THREADPOOL_SIZE = require("os").cpus().length;
}

// Start proxy
start();

// =================
// Packet Handler(s)
// =================

/**
 * Handle incoming packets
 * @param {object} packetData Packet object data
 * @param {object} packetMeta Packet metadata
 */
function packetHandler(packetData, packetMeta) {
	// Log packets
	logger.packetHandler(packetData, packetMeta, "server");

	// Assorted packet handlers
	switch (packetMeta.name) {
		case "chat": // Forward chat packets to chatty.js for livechat relay and reading server restart messages
			chatty.chatPacketHandler(packetData);
			break;
		case "difficulty": // Difficulty packet handler, checks whether or not we're in queue (explanation: when rerouted by Velocity, the difficulty packet is always sent after the MC|Brand packet.)
			queue.difficultyPacketHandler(packetData, conn);
			break;
		case "playerlist_header": // Playerlist packet handler, checks position in queue
			queue.playerlistHeaderPacketHandler(packetData, server);
			break;
		case "map_chunk":
			if (!config.experimental.worldDownloader.active) break;
			downloader.mapChunkPacketHandler(packetData); // Don't proceed if world downloader isn't enabled
			break;
		default:
			break;
	}

	// Reset uncleanDisconnectMonitor timer
	refreshMonitor();
}

// =================
// Start Proxy Stack
// =================

/** Start proxy stack */
function start() {
	logger.log("proxy", "Starting proxy stack.", "proxy");

	// Delete any leftover coordinator.flag files
	if (config.coordination.active) {
		updateCoordinatorStatus();
	}

	// Create local server
	createLocalServer();

	// Create client (connects to server)
	if (!config.waitForControllerBeforeConnect) { // ... but if waitForControllerBeforeConnect is true, delay the creation of the client until someone connects to the local server
		createClient();
	} else {
		console.log("Waiting for a controller...");
		if (config.ngrok.active) { // Create ngrok tunnel
			ngrok.createTunnel(); // Note: May overwrite MSA instructions in console(?)
		}
	}
}

// ==========================
// Client (Connect to Server)
// ==========================

/**
 * Create the client, initialize Mineflayer, and connect to the server
 */
function createClient() {
	console.log("Creating client (connecting to server)");
	logger.log("proxy", "Creating client.", "proxy");
	// Connect to server
	conn = new mcproxy.Conn({
		"host": config.server.host,
		"version": config.server.version,
		"port": config.server.port,
		"username": config.account.username,
		"password": config.account.password,
		"auth": config.account.auth
	});
	client = conn.bot._client;
	mineflayer.initialize(conn.bot);

	// Log connect and start Mineflayer
	client.on("connect", function () {
		logger.log("connected", "Client connected", "proxy");
		startMineflayer();
		// Create ngrok tunnel (unless waitForControllerBeforeConnect is true, in which case the tunnel already exists)
		if (config.ngrok.active && !config.waitForControllerBeforeConnect) {
			ngrok.createTunnel();
		}
	});

	// Log disconnect
	client.on("disconnect", function (packet) {
		logger.log("disconnected", packet.reason, "proxy");
		notifier.sendWebhook({
			title: "Disconnected from Server: " + packet.reason,
			category: "spam"
		});
		if (JSON.parse(packet.reason).text === "You are already connected to this proxy!") { // Send notifications when the proxy is unable to log on because the account is already in use
			notifier.sendWebhook({
				title: "Someone is already connected to the server using this proxy's account.",
				category: "spam"
			});
			if (typeof server !== "undefined" && typeof server.clients[0] !== "undefined") { // Make sure client exists
				server.clients[0].end("Someone is already connected to the server using this proxy's account."); // Disconnect client from the proxy with a helpful message
			}
		}
		reconnect();
	});

	// Log kick
	client.on("kick_disconnect", function (packet) {
		logger.log("kick/disconnect", packet.reason, "proxy");
		notifier.sendWebhook({
			title: "Kicked from Server: " + packet.reason,
			category: "spam"
		});
		reconnect();
	});

	// Packet handlers
	client.on("packet", (packetData, packetMeta) => {
		packetHandler(packetData, packetMeta);
	});
}

// ============
// Local Server
// ============

/**
 * Create the local server.
 * Handles players connecting & bridges packets between from players to the bridgeClient
 */
function createLocalServer() {
	console.log("Creating local server");
	logger.log("proxy", "Creating local server.", "proxy");
	// Create server
	server = mc.createServer({
		"online-mode": config.proxy.onlineMode,
		"encryption": true,
		"host": config.proxy.loopbackAddress,
		"port": config.proxy.port,
		"version": config.server.version,
		"max-players": 1,
		"beforePing": (function (response, client, answerToPing) {
			if (!config.experimental.spoofPing.active) { // Don't proceed if noPing isn't enabled in config.json
				return;
			}
			if (config.experimental.spoofPing.noResponse) { // Don't return a response
				answerToPing(false);
			} else { // Or return a fake response
				answerToPing(null, config.experimental.spoofPing.fakeResponse);
			}
		})
	});
	// Handle logins
	server.on("login", (bridgeClient) => {
		// Block attempt if...
		if (config.proxy.whitelist.findIndex(needle => bridgeClient.username.toLowerCase() === needle.toLowerCase()) === -1) { // ... player isn't in whitelist
			bridgeClient.end("Your account (" + bridgeClient.username + ") is not whitelisted.\n\nIf you're getting this error in error the Microsoft account token may have expired.");
			logSpam(bridgeClient.username + " (" + bridgeClient.uuid + ")" + " was denied connection to the proxy for not being whitelisted.");
			return;
		} else if (server.playerCount > 1) { // ... and another player isn't already connected
			bridgeClient.end("This proxy is at max capacity.\n\nCurrent Controller: " + status.controller);
			logSpam(bridgeClient.username + " (" + bridgeClient.uuid + ")" + " was denied connection to the proxy despite being whitelisted because " + status.controller + " was already in control.");
			return;
		}

		// Finally, kick the player if the proxy is restarting
		if (status.restart === ("Reconnecting in " + config.reconnectInterval + " seconds...")) {
			if (config.ngrok.active) {
				bridgeClient.end("This proxy is currently restarting.\n\nPlease wait at least " + config.reconnectInterval + " seconds and try again using the new tunnel.");
			} else {
				bridgeClient.end("This proxy is currently restarting.\n\nPlease wait at least " + config.reconnectInterval + " seconds and try again.");
			}
			logSpam(bridgeClient.username + " (" + bridgeClient.uuid + ")" + " was denied connection to the proxy despite being whitelisted because the proxy was restarting.");
			return;
		}

		// Log successful connection attempt
		logSpam(bridgeClient.username + " (" + bridgeClient.uuid + ")" + " has connected to the proxy.");
		updateStatus("controller", bridgeClient.username);
		if (config.notify.whenControlling) { // optional: send message to status webhook
			notifier.sendWebhook({
				title: bridgeClient.username + " is using the proxy.",
				category: "status",
				deleteOnRestart: true
			});
		}

		// Create client if it hasn't been created yet (waitForControllerBeforeConnect)
		if (config.waitForControllerBeforeConnect && typeof conn === "undefined") {
			createClient();
			client.on("packet", (packetData, packetMeta) => {
				if (packetMeta.name === "success") {
					createBridge();
				}
			});
		} else {
			createBridge();
		}

		/** Create bridge between client and local server */
		function createBridge() {
			console.log("Creating packet bridge");
			logger.log("proxy", "Creating packet bridge.", "proxy");
			webserver.updateWebStatus('updateController', status.controller);
			// Start Mineflayer when disconnected
			bridgeClient.on("end", () => {
				// Log disconnect
				logSpam(bridgeClient.username + " (" + bridgeClient.uuid + ")" + " has disconnected from the local server.");
				updateStatus("controller", "None");
				webserver.updateWebStatus('updateController', status.controller);
				if (config.notify.whenControlling) { // optional: send message to status webhook
					notifier.sendWebhook({
						title: bridgeClient.username + " is no longer using the proxy.",
						category: "status",
						deleteOnRestart: true
					});
				}
				// Disconnect if no controller after config.experimental.disconnectIfNoController.delay seconds
				if (config.experimental.disconnectIfNoController.active && status.inQueue === "false") {
					setTimeout(function () {
						if (status.controller === "None") {
							logger.log("proxy", "Restarting proxy because noone was in control " + config.experimental.disconnectIfNoController.delay + " seconds after someone DCed from proxy while it was on the server.", "proxy");
							reconnect();
						}
					}, config.experimental.disconnectIfNoController.delay * 1000);
				}
				// Start Mineflayer
				startMineflayer();
			});

			// Stop Mineflayer
			stopMineflayer();

			// Spoof player_info (skin fix)
			if (config.experimental.spoofPlayerInfo.active) {
				conn.bot.waitForTicks(1).then(() => {
					bridgeClient.write("player_info", { // Add spoofed player to tablist
						action: 0,
						data: [{
							UUID: bridgeClient.uuid,
							name: conn.bot.player.username,
							properties: [{
								"name": "textures", // Remember to get skin info from https://sessionserver.mojang.com/session/minecraft/profile/<uuid>?unsigned=false!
								"value": config.experimental.spoofPlayerInfo.texture.value,
								"signature": config.experimental.spoofPlayerInfo.texture.signature
							}]
						}]
					});
					bridgeClient.write("player_info", { // Remove bot player from tablist
						action: 4,
						data: [{
							UUID: conn.bot.player.uuid,
							name: conn.bot.player.username,
							properties: []
						}]
					});
				});
			}

			// Log packets
			bridgeClient.on("packet", (packetData, packetMeta) => {
				logger.packetHandler(packetData, packetMeta, "bridgeClient");
			});

			// Bridge packets
			bridgeClient.on("packet", (data, meta, rawData) => {
				bridge(rawData, meta, client);
			});
			conn.sendPackets(bridgeClient);
			conn.link(bridgeClient);
		}

		/**
		 * Send message to logger and spam webhook
		 * @param {string} logMsg Message to log
		 */
		function logSpam(logMsg) {
			logger.log("bridgeclient", logMsg, "proxy");
			notifier.sendWebhook({
				title: logMsg,
				category: "spam"
			});
		}
	});
}

// ==========================
// Unclean Disconnect Monitor
// ==========================

let uncleanDisconnectMonitor;
/**
 * If no packets are received for config.uncleanDisconnectInterval seconds, assume we were disconnected uncleanly and reconnect.
 */
function refreshMonitor() {
	if (!uncleanDisconnectMonitor) { // Create timer on the first packet
		uncleanDisconnectMonitor = setTimeout(function () {
			logger.log("proxy", "No packets were received for " + config.uncleanDisconnectInterval + " seconds. Assuming unclean disconnect.", "proxy");
			reconnect();
		}, config.uncleanDisconnectInterval * 1000);
		return;
	}
	uncleanDisconnectMonitor.refresh(); // Restart the timer
}

// =========
// Functions
// =========

/** Reconnect (Remember to read https://github.com/Enchoseon/2based2wait/wiki/How-to-Auto-Reconnect-with-Supervisor or this will just cause the script to shut down!) */
function reconnect() {
	console.log("Reconnecting...");
	logger.log("proxy", "Reconnecting...", "proxy");
	if (typeof conn !== "undefined") { // Make sure connection exists
		conn.disconnect(); // Disconnect proxy from the server
	}
	if (typeof server !== "undefined" && typeof server.clients[0] !== "undefined") { // Make sure client exists
		server.clients[0].end("Proxy restarting..."); // Disconnect client from the proxy
	}
	notifier.sendWebhook({
		title: "Reconnecting...",
		category: "spam"
	});
	updateStatus("restart", "Reconnecting in " + config.reconnectInterval + " seconds...");
	updateStatus("livechatRelay", "false");
	notifier.deleteMarkedMessages();
	setTimeout(function() {
		updateStatus("restart", "Reconnecting now!");
		notifier.sendToast("Reconnecting now!");
		process.exit(1);
	}, config.reconnectInterval * 1000);
}

/** Start Mineflayer */
function startMineflayer() {
	logger.log("mineflayer", "Starting Mineflayer.", "proxy");
	updateStatus("mineflayer", true);
	if (config.mineflayer.active) {
		conn.bot.autoEat.enable();
		conn.bot.afk.start();
	}
}

/** Stop Mineflayer */
function stopMineflayer() {
	logger.log("mineflayer", "Stopping Mineflayer.", "proxy");
	updateStatus("mineflayer", false);
	if (config.mineflayer.active) {
		conn.bot.autoEat.disable();
		conn.bot.afk.stop();
	}
}

/**
 * Send packets from Point A to Point B
 * @param {object} packetData Packet object data
 * @param {object} packetMeta Packet metadata
 * @param {object} dest McProxy client to write data to
 */
function bridge(packetData, packetMeta, dest) {
	if (packetMeta.name !== "keep_alive" && packetMeta.name !== "update_time") { // Keep-alive packets are filtered bc the client already handles them. Sending double would kick us.
		dest.writeRaw(packetData);
	}
}