/* SPDX-FileCopyrightText: 2026 Project Tick
 * SPDX-FileContributor: Project Tick
 * SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-MeshMC-MMCO-Module-Exception-1.0
 *
 *   MeshMC - A Custom Launcher for Minecraft
 *   Copyright (C) 2026 Project Tick
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version, with the additional permission
 *   described in the MeshMC MMCO Module Exception 1.0.
 *
 *   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 General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 *   You should have received a copy of the MeshMC MMCO Module Exception 1.0
 *   along with this program.  If not, see <https://projecttick.org/licenses/>.
 */

#include "Parsers.h"

#include <QJsonDocument>
#include <QJsonArray>
#include <QDebug>

namespace Parsers
{

	bool getDateTime(QJsonValue value, QDateTime& out)
	{
		if (!value.isString()) {
			return false;
		}
		out = QDateTime::fromString(value.toString(), Qt::ISODate);
		return out.isValid();
	}

	bool getString(QJsonValue value, QString& out)
	{
		if (!value.isString()) {
			return false;
		}
		out = value.toString();
		return true;
	}

	bool getNumber(QJsonValue value, double& out)
	{
		if (!value.isDouble()) {
			return false;
		}
		out = value.toDouble();
		return true;
	}

	bool getNumber(QJsonValue value, int64_t& out)
	{
		if (!value.isDouble()) {
			return false;
		}
		out = (int64_t)value.toDouble();
		return true;
	}

	bool getBool(QJsonValue value, bool& out)
	{
		if (!value.isBool()) {
			return false;
		}
		out = value.toBool();
		return true;
	}

	/*
	{
	   "IssueInstant":"2020-12-07T19:52:08.4463796Z",
	   "NotAfter":"2020-12-21T19:52:08.4463796Z",
	   "Token":"token",
	   "DisplayClaims":{
		  "xui":[
			 {
				"uhs":"userhash"
			 }
		  ]
	   }
	 }
	*/
	// TODO: handle error responses ...
	/*
	{
		"Identity":"0",
		"XErr":2148916238,
		"Message":"",
		"Redirect":"https://start.ui.xboxlive.com/AddChildToFamily"
	}
	// 2148916233 = missing XBox account
	// 2148916238 = child account not linked to a family
	*/

	bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output,
							 QString name)
	{
		qDebug() << "Parsing" << name << ":";
#ifndef NDEBUG
		qDebug() << data;
#endif
		QJsonParseError jsonError;
		QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
		if (jsonError.error) {
			qWarning() << "Failed to parse response from "
						  "user.auth.xboxlive.com as JSON: "
					   << jsonError.errorString();
			return false;
		}

		auto obj = doc.object();
		if (!getDateTime(obj.value("IssueInstant"), output.issueInstant)) {
			qWarning() << "User IssueInstant is not a timestamp";
			return false;
		}
		if (!getDateTime(obj.value("NotAfter"), output.notAfter)) {
			qWarning() << "User NotAfter is not a timestamp";
			return false;
		}
		if (!getString(obj.value("Token"), output.token)) {
			qWarning() << "User Token is not a string";
			return false;
		}
		auto arrayVal = obj.value("DisplayClaims").toObject().value("xui");
		if (!arrayVal.isArray()) {
			qWarning() << "Missing xui claims array";
			return false;
		}
		bool foundUHS = false;
		for (auto item : arrayVal.toArray()) {
			if (!item.isObject()) {
				continue;
			}
			auto claimObject = item.toObject();
			if (claimObject.contains("uhs")) {
				foundUHS = true;
			} else {
				continue;
			}
			// consume all 'display claims' ... whatever that means
			for (auto iter = claimObject.begin(); iter != claimObject.end();
				 iter++) {
				QString claim;
				if (!getString(claimObject.value(iter.key()), claim)) {
					qWarning() << "display claim " << iter.key()
							   << " is not a string...";
					return false;
				}
				output.extra[iter.key()] = claim;
			}

			break;
		}
		if (!foundUHS) {
			qWarning() << "Missing uhs";
			return false;
		}
		output.validity = Katabasis::Validity::Certain;
		qDebug() << name << "is valid.";
		return true;
	}

	bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output)
	{
		qDebug() << "Parsing Minecraft profile...";
#ifndef NDEBUG
		qDebug() << data;
#endif

		QJsonParseError jsonError;
		QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
		if (jsonError.error) {
			qWarning() << "Failed to parse response from "
						  "user.auth.xboxlive.com as JSON: "
					   << jsonError.errorString();
			return false;
		}

		auto obj = doc.object();
		if (!getString(obj.value("id"), output.id)) {
			qWarning() << "Minecraft profile id is not a string";
			return false;
		}

		if (!getString(obj.value("name"), output.name)) {
			qWarning() << "Minecraft profile name is not a string";
			return false;
		}

		auto skinsArray = obj.value("skins").toArray();
		for (auto skin : skinsArray) {
			auto skinObj = skin.toObject();
			Skin skinOut;
			if (!getString(skinObj.value("id"), skinOut.id)) {
				continue;
			}
			QString state;
			if (!getString(skinObj.value("state"), state)) {
				continue;
			}
			if (state != "ACTIVE") {
				continue;
			}
			if (!getString(skinObj.value("url"), skinOut.url)) {
				continue;
			}
			if (!getString(skinObj.value("variant"), skinOut.variant)) {
				continue;
			}
			// we deal with only the active skin
			output.skin = skinOut;
			break;
		}
		auto capesArray = obj.value("capes").toArray();

		QString currentCape;
		for (auto cape : capesArray) {
			auto capeObj = cape.toObject();
			Cape capeOut;
			if (!getString(capeObj.value("id"), capeOut.id)) {
				continue;
			}
			QString state;
			if (!getString(capeObj.value("state"), state)) {
				continue;
			}
			if (state == "ACTIVE") {
				currentCape = capeOut.id;
			}
			if (!getString(capeObj.value("url"), capeOut.url)) {
				continue;
			}
			if (!getString(capeObj.value("alias"), capeOut.alias)) {
				continue;
			}

			output.capes[capeOut.id] = capeOut;
		}
		output.currentCape = currentCape;
		output.validity = Katabasis::Validity::Certain;
		return true;
	}

	bool parseMinecraftEntitlements(QByteArray& data,
									MinecraftEntitlement& output)
	{
		qDebug() << "Parsing Minecraft entitlements...";
#ifndef NDEBUG
		qDebug() << data;
#endif

		QJsonParseError jsonError;
		QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
		if (jsonError.error) {
			qWarning() << "Failed to parse response from "
						  "user.auth.xboxlive.com as JSON: "
					   << jsonError.errorString();
			return false;
		}

		auto obj = doc.object();
		output.canPlayMinecraft = false;
		output.ownsMinecraft = false;

		auto itemsArray = obj.value("items").toArray();
		for (auto item : itemsArray) {
			auto itemObj = item.toObject();
			QString name;
			if (!getString(itemObj.value("name"), name)) {
				continue;
			}
			if (name == "game_minecraft") {
				output.canPlayMinecraft = true;
			}
			if (name == "product_minecraft") {
				output.ownsMinecraft = true;
			}
		}
		output.validity = Katabasis::Validity::Certain;
		return true;
	}

	bool parseRolloutResponse(QByteArray& data, bool& result)
	{
		qDebug() << "Parsing Rollout response...";
#ifndef NDEBUG
		qDebug() << data;
#endif

		QJsonParseError jsonError;
		QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
		if (jsonError.error) {
			qWarning() << "Failed to parse response from "
						  "https://api.minecraftservices.com/rollout/v1/"
						  "msamigration as JSON: "
					   << jsonError.errorString();
			return false;
		}

		auto obj = doc.object();
		QString feature;
		if (!getString(obj.value("feature"), feature)) {
			qWarning() << "Rollout feature is not a string";
			return false;
		}
		if (feature != "msamigration") {
			qWarning() << "Rollout feature is not what we expected "
						  "(msamigration), but is instead \""
					   << feature << "\"";
			return false;
		}
		if (!getBool(obj.value("rollout"), result)) {
			qWarning() << "Rollout feature is not a string";
			return false;
		}
		return true;
	}

	bool parseMojangResponse(QByteArray& data, Katabasis::Token& output)
	{
		QJsonParseError jsonError;
		qDebug() << "Parsing Mojang response...";
#ifndef NDEBUG
		qDebug() << data;
#endif
		QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
		if (jsonError.error) {
			qWarning() << "Failed to parse response from "
						  "api.minecraftservices.com/launcher/login as JSON: "
					   << jsonError.errorString();
			return false;
		}

		auto obj = doc.object();
		double expires_in = 0;
		if (!getNumber(obj.value("expires_in"), expires_in)) {
			qWarning() << "expires_in is not a valid number";
			return false;
		}
		auto currentTime = QDateTime::currentDateTimeUtc();
		output.issueInstant = currentTime;
		output.notAfter = currentTime.addSecs(expires_in);

		QString username;
		if (!getString(obj.value("username"), username)) {
			qWarning() << "username is not valid";
			return false;
		}

		// TODO: it's a JWT... validate it?
		if (!getString(obj.value("access_token"), output.token)) {
			qWarning() << "access_token is not valid";
			return false;
		}
		output.validity = Katabasis::Validity::Certain;
		qDebug() << "Mojang response is valid.";
		return true;
	}

} // namespace Parsers
