#include "services/anilist.h"
#include "core/anime.h"
#include "core/anime_db.h"
#include "core/config.h"
#include "core/date.h"
#include "core/http.h"
#include "core/json.h"
#include "core/session.h"
#include "core/strings.h"
#include "gui/translate/anilist.h"

#include <QByteArray>
#include <QDate>
#include <QDesktopServices>
#include <QInputDialog>
#include <QLineEdit>
#include <QMessageBox>
#include <QUrl>

#include <chrono>
#include <exception>
#include <string_view>

#include <fmt/core.h>

#include <iostream>

namespace Services {
namespace AniList {

static constexpr std::string_view CLIENT_ID = "13706";

/* This is used in multiple queries, so just put it here I guess. */
#define MEDIA_FIELDS \
	"coverImage {\n" \
	"  large\n" \
	"}\n" \
	"id\n" \
	"title {\n" \
	"  romaji\n" \
	"  english\n" \
	"  native\n" \
	"}\n" \
	"format\n" \
	"status\n" \
	"averageScore\n" \
	"season\n" \
	"startDate {\n" \
	"  year\n" \
	"  month\n" \
	"  day\n" \
	"}\n" \
	"endDate {\n" \
	"  year\n" \
	"  month\n" \
	"  day\n" \
	"}\n" \
	"studios {\n" \
	"  edges {\n" \
	"    node {\n" \
	"      name\n" \
	"    }\n" \
	"  }\n" \
	"}\n" \
	"genres\n" \
	"episodes\n" \
	"duration\n" \
	"synonyms\n" \
	"description(asHtml: false)\n"

/* FIXME: why is this here */

static std::optional<nlohmann::json> SendJSONRequest(const nlohmann::json &data)
{
	std::vector<std::string> headers = {
	    "Accept: application/json",
	    "Content-Type: application/json",
	};

	if (!session.config.auth.anilist.auth_token.empty())
		headers.push_back("Authorization: Bearer " + session.config.auth.anilist.auth_token);

	const std::string response =
	    Strings::ToUtf8String(HTTP::Request("https://graphql.anilist.co", headers, data.dump(), HTTP::Type::Post));
	if (response.empty()) {
		session.SetStatusBar(Strings::Translate("AniList: JSON request returned an empty result!"));
		return std::nullopt;
	}

	nlohmann::json out;

	try {
		out = nlohmann::json::parse(response);
	} catch (const std::exception &ex) {
		session.SetStatusBar(
		    fmt::format(Strings::Translate("AniList: Failed to parse request JSON with error \"{}\"!"), ex.what()));
		return std::nullopt;
	}

	if (out.contains("/errors"_json_pointer) && out.at("/errors"_json_pointer).is_array()) {
		for (const auto &error : out.at("/errors"_json_pointer))
			std::cerr << "AniList: Received an error in response: "
			          << JSON::GetString<std::string>(error, "/message"_json_pointer, "") << std::endl;

		session.SetStatusBar(Strings::Translate("AniList: Received an error in response!"));
		return std::nullopt;
	}

	return out;
}

static void ParseListStatus(std::string status, Anime::Anime &anime)
{
	static const std::unordered_map<std::string, Anime::ListStatus> map = {
	    {"CURRENT",   Anime::ListStatus::Current  },
	    {"PLANNING",  Anime::ListStatus::Planning },
	    {"COMPLETED", Anime::ListStatus::Completed},
	    {"DROPPED",   Anime::ListStatus::Dropped  },
	    {"PAUSED",    Anime::ListStatus::Paused   }
    };

	if (status == "REPEATING") {
		anime.SetUserIsRewatching(true);
		anime.SetUserStatus(Anime::ListStatus::Current);
		return;
	}

	if (map.find(status) == map.end()) {
		anime.SetUserStatus(Anime::ListStatus::NotInList);
		return;
	}

	anime.SetUserStatus(map.at(status));
}

static std::string ListStatusToString(const Anime::Anime &anime)
{
	if (anime.GetUserIsRewatching() && anime.GetUserStatus() == Anime::ListStatus::Current)
		return "REWATCHING";

	switch (anime.GetUserStatus()) {
	case Anime::ListStatus::Planning:
		return "PLANNING";
	case Anime::ListStatus::Completed:
		return "COMPLETED";
	case Anime::ListStatus::Dropped:
		return "DROPPED";
	case Anime::ListStatus::Paused:
		return "PAUSED";
	default:
		break;
	}
	return "CURRENT";
}

static void ParseTitle(const nlohmann::json &json, Anime::Anime &anime)
{
	static const std::unordered_map<Anime::TitleLanguage, nlohmann::json::json_pointer> map = {
	    {Anime::TitleLanguage::Native,  "/native"_json_pointer },
	    {Anime::TitleLanguage::English, "/english"_json_pointer},
	    {Anime::TitleLanguage::Romaji,  "/romaji"_json_pointer },
	};

	for (const auto &[language, ptr] : map)
		if (json.contains(ptr) && json[ptr].is_string())
			anime.SetTitle(language, json[ptr]);
}

static int ParseMediaJson(const nlohmann::json &json)
{
	if (!json.contains("/id"_json_pointer) || !json["/id"_json_pointer].is_number()) {
		session.SetStatusBar(Strings::Translate("AniList: Failed to parse anime object!"));
		return 0;
	}

	std::string service_id = Strings::ToUtf8String(json["/id"_json_pointer].get<int>());

	int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::AniList, service_id);
	if (!id) {
		session.SetStatusBar(Strings::Translate("AniList: Failed to parse anime object!"));
		return 0;
	}

	Anime::Anime &anime = Anime::db.items[id];
	anime.SetId(id);
	anime.SetServiceId(Anime::Service::AniList, service_id);

	if (json.contains("/idMal"_json_pointer) && json["/idMal"_json_pointer].is_number())
		anime.SetServiceId(Anime::Service::MyAnimeList, Strings::ToUtf8String(json["/idMal"_json_pointer].get<int>()));

	ParseTitle(json.at("/title"_json_pointer), anime);

	anime.SetEpisodes(JSON::GetNumber(json, "/episodes"_json_pointer, 0));
	anime.SetFormat(Translate::AniList::ToSeriesFormat(JSON::GetString<std::string>(json, "/format"_json_pointer, "")));

	anime.SetAiringStatus(
	    Translate::AniList::ToSeriesStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, "")));

	if (json.contains("/startDate"_json_pointer) && json["/startDate"_json_pointer].is_object())
		anime.SetStartedDate(Date(json["/startDate"_json_pointer]));

	anime.SetCompletedDate(json.contains("/endDate"_json_pointer) && json["/endDate"_json_pointer].is_object()
	                           ? Date(json["/endDate"_json_pointer])
	                           : anime.GetStartedDate());

	anime.SetPosterUrl(JSON::GetString<std::string>(json, "/coverImage/large"_json_pointer, ""));

	anime.SetAudienceScore(JSON::GetNumber(json, "/averageScore"_json_pointer, 0));
	// anime.SetSeason(Translate::AniList::ToSeriesSeason(JSON::GetString<std::string>(json, "/season"_json_pointer,
	// "")));
	anime.SetDuration(JSON::GetNumber(json, "/duration"_json_pointer, 0));

	std::string synopsis = JSON::GetString<std::string>(json, "/description"_json_pointer, "");
	Strings::TextifySynopsis(synopsis);
	anime.SetSynopsis(synopsis);

	anime.SetGenres(JSON::GetArray<std::vector<std::string>>(json, "/genres"_json_pointer, {}));
	anime.SetTitleSynonyms(JSON::GetArray<std::vector<std::string>>(json, "/synonyms"_json_pointer, {}));

	{
		std::vector<std::string> producers;

		if (json.contains("/studios/edges"_json_pointer) && json["/studios/edges"_json_pointer].is_array())
			for (const auto &edge : json["/studios/edges"_json_pointer])
				if (edge.contains("/node/name"_json_pointer) && edge["/node/name"_json_pointer].is_string())
					producers.push_back(edge["/node/name"_json_pointer].get<std::string>());

		anime.SetProducers(producers);
	}

	return id;
}

static int ParseListItem(const nlohmann::json &json)
{
	int id = ParseMediaJson(json["/media"_json_pointer]);
	if (!id)
		return 0;

	Anime::Anime &anime = Anime::db.items[id];

	anime.AddToUserList();

	anime.SetUserScore(JSON::GetNumber(json, "/score"_json_pointer, 0));
	anime.SetUserProgress(JSON::GetNumber(json, "/progress"_json_pointer, 0));
	ParseListStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, ""), anime);
	anime.SetUserNotes(JSON::GetString<std::string>(json, "/notes"_json_pointer, ""));

	anime.SetUserDateStarted(Date(json["/startedAt"_json_pointer]));
	anime.SetUserDateCompleted(Date(json["/completedAt"_json_pointer]));

	anime.SetUserTimeUpdated(JSON::GetNumber(json, "/updatedAt"_json_pointer, 0));

	return id;
}

static bool ParseList(const nlohmann::json &json)
{
	bool success = true;

	for (const auto &entry : json["entries"].items())
		if (!ParseListItem(entry.value()))
			success = false;

	return success;
}

int GetAnimeList()
{
	auto &auth = session.config.auth.anilist;

	static constexpr std::string_view query = "query ($id: Int) {\n"
	                                          "  MediaListCollection (userId: $id, type: ANIME) {\n"
	                                          "    lists {\n"
	                                          "      name\n"
	                                          "      entries {\n"
	                                          "        score\n"
	                                          "        notes\n"
	                                          "        status\n"
	                                          "        progress\n"
	                                          "        startedAt {\n"
	                                          "          year\n"
	                                          "          month\n"
	                                          "          day\n"
	                                          "        }\n"
	                                          "        completedAt {\n"
	                                          "          year\n"
	                                          "          month\n"
	                                          "          day\n"
	                                          "        }\n"
	                                          "        updatedAt\n"
	                                          "        media {\n" MEDIA_FIELDS "        }\n"
	                                          "      }\n"
	                                          "    }\n"
	                                          "  }\n"
	                                          "}\n";

	// clang-format off
	nlohmann::json request = {
		{"query", query},
		{"variables", {
			{"id", auth.user_id}
		}}
	};
	// clang-format on

	session.SetStatusBar(Strings::Translate("AniList: Parsing anime list..."));

	const std::optional<nlohmann::json> response = SendJSONRequest(request);
	if (!response)
		return 0;

	Anime::db.RemoveAllUserData();

	const nlohmann::json &json = response.value();

	bool success = true;

	for (const auto &list : json["data"]["MediaListCollection"]["lists"].items())
		if (!ParseList(list.value()))
			success = false;

	if (success)
		session.SetStatusBar(Strings::Translate("AniList: Retrieved anime list successfully!"));

	return 1;
}

/* return is a vector of anime ids */
std::vector<int> Search(const std::string &search)
{
	static constexpr std::string_view query = "query ($search: String) {\n"
	                                          "  Page (page: 1, perPage: 50) {\n"
	                                          "    media (search: $search, type: ANIME) {\n" MEDIA_FIELDS "    }\n"
	                                          "  }\n"
	                                          "}\n";

	// clang-format off
	nlohmann::json json = {
		{"query", query},
		{"variables", {
			{"search", search}
		}}
	};
	// clang-format on

	const std::optional<nlohmann::json> response = SendJSONRequest(json);
	if (!response)
		return {};

	const nlohmann::json &result = response.value();

	/* FIXME: error handling here */
	std::vector<int> ret;
	ret.reserve(result["/data/Page/media"_json_pointer].size());

	for (const auto &media : result["/data/Page/media"_json_pointer].items())
		ret.push_back(ParseMediaJson(media.value()));

	return ret;
}

bool GetSeason(Anime::Season season)
{
	static constexpr std::string_view query =
	    "query ($season: MediaSeason!, $season_year: Int!, $page: Int) {\n"
	    "  Page(page: $page) {\n"
	    "    media(season: $season, seasonYear: $season_year, type: ANIME, sort: START_DATE) {\n" MEDIA_FIELDS "    }\n"
	    "    pageInfo {\n"
	    "      total\n"
	    "      perPage\n"
	    "      currentPage\n"
	    "      lastPage\n"
	    "      hasNextPage\n"
	    "    }\n"
	    "  }\n"
	    "}\n";

	int page = 0;
	bool has_next_page = true;

	while (has_next_page) {
		nlohmann::json json = {
		    {"query",     query},
		    {"variables",
		     {
		         {"season", Translate::AniList::ToString(season.season)},
		         {"season_year", Strings::ToUtf8String(season.year)},
		         {"page", page},
		     }		         },
		};

		const std::optional<nlohmann::json> res = SendJSONRequest(json);
		if (!res)
			return false;

		const nlohmann::json &result = res.value();

		for (const auto &media : result["/data/Page/media"_json_pointer].items())
			ParseMediaJson(media.value());

		has_next_page = JSON::GetBoolean(result, "/data/Page/pageInfo/hasNextPage"_json_pointer, false);
		if (has_next_page)
			page++;
	}

	return true;
}

int UpdateAnimeEntry(int id)
{
	Anime::Anime &anime = Anime::db.items[id];
	if (!anime.IsInUserList())
		return 0;

	std::optional<std::string> service_id = anime.GetServiceId(Anime::Service::AniList);
	if (!service_id)
		return 0;

	static constexpr std::string_view query =
	    "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String,"
	    "     $start: FuzzyDateInput, $comp: FuzzyDateInput, $repeat: Int) {\n"
	    "  SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, scoreRaw: $score,"
	    "        notes: $notes, startedAt: $start, completedAt: $comp, repeat: $repeat) {\n"
	    "    id\n"
	    "  }\n"
	    "}\n";
	// clang-format off
	nlohmann::json json = {
		{"query", query},
		{"variables", {
			{"media_id", Strings::ToInt<int64_t>(service_id.value())},
			{"progress", anime.GetUserProgress()},
			{"status",   ListStatusToString(anime)},
			{"score",    anime.GetUserScore()},
			{"notes",    anime.GetUserNotes()},
			{"start",    anime.GetUserDateStarted().GetAsAniListJson()},
			{"comp",     anime.GetUserDateCompleted().GetAsAniListJson()},
			{"repeat",   anime.GetUserRewatchedTimes()}
		}}
	};
	// clang-format on

	const std::optional<nlohmann::json> res = SendJSONRequest(json);
	if (!res)
		return 0;

	const nlohmann::json &result = res.value();

	session.SetStatusBar(Strings::Translate("AniList: Anime entry updated successfully!"));

	return JSON::GetNumber(result, "/data/SaveMediaListEntry/id"_json_pointer, 0);
}

static int ParseUser(const nlohmann::json &json)
{
	auto &auth = session.config.auth.anilist;

	return auth.user_id = JSON::GetNumber(json, "/id"_json_pointer, 0);
}

bool AuthorizeUser()
{
	auto &auth = session.config.auth.anilist;

	/* Prompt for PIN */
	QDesktopServices::openUrl(QUrl(Strings::ToQString(
	    "https://anilist.co/api/v2/oauth/authorize?client_id=" + std::string(CLIENT_ID) + "&response_type=token")));

	bool ok;
	QString token = QInputDialog::getText(
	    0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal,
	    "", &ok);

	if (!ok || token.isEmpty())
		return false;

	auth.auth_token = Strings::ToUtf8String(token);

	session.SetStatusBar(Strings::Translate("AniList: Requesting user ID..."));

	static constexpr std::string_view query = "query {\n"
	                                          "  Viewer {\n"
	                                          "    id\n"
	                                          "  }\n"
	                                          "}\n";

	nlohmann::json json = {
	    {"query", query}
    };

	/* SendJSONRequest handles status errors */
	const std::optional<nlohmann::json> ret = SendJSONRequest(json);
	if (!ret)
		return 0;

	const nlohmann::json &result = ret.value();

	if (ParseUser(result["data"]["Viewer"]))
		session.SetStatusBar(Strings::Translate("AniList: Successfully retrieved user data!"));
	else
		session.SetStatusBar(Strings::Translate("AniList: Failed to retrieve user ID!"));

	return true;
}

} // namespace AniList
} // namespace Services
