#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 "core/time.h"
#include "gui/translate/anilist.h"
#include "services/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>

using namespace nlohmann::literals::json_literals;

static constexpr std::string_view BASE_API_PATH = "https://kitsu.io/api/edge";
static constexpr std::string_view OAUTH_PATH = "https://kitsu.io/api/oauth/token";

namespace Services {
namespace Kitsu {

/* This nifty little function basically handles authentication AND reauthentication. */
static bool SendAuthRequest(const nlohmann::json &data)
{
	static const std::vector<std::string> headers = {{"Content-Type: application/json"}};

	const std::string ret =
	    Strings::ToUtf8String(HTTP::Request(std::string(OAUTH_PATH), headers, data.dump(), HTTP::Type::Post));
	if (ret.empty()) {
		session.SetStatusBar(Strings::Translate("Kitsu: Request returned empty data!"));
		return false;
	}

	nlohmann::json result;
	try {
		result = nlohmann::json::parse(ret, nullptr, false);
	} catch (const std::exception &ex) {
		session.SetStatusBar(
		    fmt::format(Strings::Translate("Kitsu: Failed to parse authorization data with error \"{}\""), ex.what()));
		return false;
	}

	if (result.contains("/error"_json_pointer)) {
		session.SetStatusBar(fmt::format(Strings::Translate("Kitsu: Failed with error \"{}\"!"),
		                                 result["/error"_json_pointer].get<std::string>()));
		return false;
	}

	const std::vector<nlohmann::json::json_pointer> required = {
	    "/access_token"_json_pointer,  "/created_at"_json_pointer, "/expires_in"_json_pointer,
	    "/refresh_token"_json_pointer, "/scope"_json_pointer,      "/token_type"_json_pointer};

	for (const auto &ptr : required) {
		if (!result.contains(ptr)) {
			session.SetStatusBar(Strings::Translate("Kitsu: Authorization request returned bad data!"));
			return false;
		}
	}

	session.config.auth.kitsu.access_token = result["/access_token"_json_pointer].get<std::string>();
	session.config.auth.kitsu.access_token_expiration = result["/created_at"_json_pointer].get<Time::Timestamp>() +
	                                                    result["/expires_in"_json_pointer].get<Time::Timestamp>();
	session.config.auth.kitsu.refresh_token = result["/refresh_token"_json_pointer].get<std::string>();

	/* the next two are not that important */

	return true;
}

static bool RefreshAccessToken(void)
{
	const nlohmann::json request = {
	    {"grant_type",    "refresh_token"                        },
	    {"refresh_token", session.config.auth.kitsu.refresh_token},
	};

	if (!SendAuthRequest(request))
		return false;

	return true;
}

/* ----------------------------------------------------------------------------- */

static std::optional<std::string> AccountAccessToken()
{
	auto &auth = session.config.auth.kitsu;

	if (Time::GetSystemTime() >= session.config.auth.kitsu.access_token_expiration)
		if (!RefreshAccessToken())
			return std::nullopt;

	return auth.access_token;
}

/* ----------------------------------------------------------------------------- */

static void AddAnimeFilters(std::map<std::string, std::string> &map)
{
	static const std::vector<std::string> fields = {
	    "abbreviatedTitles", "averageRating",    "episodeCount", "episodeLength", "posterImage",
	    "startDate",         "status",           "subtype",      "titles",        "categories",
	    "synopsis",          "animeProductions",
	};
	static const std::string imploded = Strings::Implode(fields, ",");

	map["fields[anime]"] = imploded;
	map["fields[animeProductions]"] = "producer";
	map["fields[categories]"] = "title";
	map["fields[producers]"] = "name";
}

static void AddLibraryEntryFilters(std::map<std::string, std::string> &map)
{
	static const std::vector<std::string> fields = {
	    "anime",        "startedAt",      "finishedAt",  "notes",  "progress",
	    "ratingTwenty", "reconsumeCount", "reconsuming", "status", "updatedAt",
	};
	static const std::string imploded = Strings::Implode(fields, ",");

	map["fields[libraryEntries]"] = imploded;
}

/* ----------------------------------------------------------------------------- */

static std::optional<nlohmann::json> SendJSONAPIRequest(const std::string &path,
                                                        const std::map<std::string, std::string> &params = {})
{
	std::optional<std::string> token = AccountAccessToken();
	if (!token)
		return std::nullopt;

	const std::vector<std::string> headers = {"Accept: application/vnd.api+json",
	                                          "Authorization: Bearer " + token.value(),
	                                          "Content-Type: application/vnd.api+json"};

	const std::string url = HTTP::EncodeParamsList(std::string(BASE_API_PATH) + path, params);

	const std::string response = Strings::ToUtf8String(HTTP::Request(url, headers, "", HTTP::Type::Get));
	if (response.empty())
		return std::nullopt;

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

	if (json.contains("/errors"_json_pointer)) {
		for (const auto &item : json["/errors"])
			std::cerr << "Kitsu: API returned error \"" << json["/errors/title"_json_pointer] << "\" with detail \""
			          << json["/errors/detail"] << std::endl;

		session.SetStatusBar(Strings::Translate("Kitsu: Request failed with errors!"));
		return std::nullopt;
	}

	return json;
}

static void ParseTitleJson(Anime::Anime &anime, const nlohmann::json &json)
{
	static const std::map<std::string, Anime::TitleLanguage> lookup = {
	    {"en",    Anime::TitleLanguage::English},
	    {"en_jp", Anime::TitleLanguage::Romaji },
	    {"ja_jp", Anime::TitleLanguage::Native }
    };

	for (const auto &[string, title] : lookup)
		if (json.contains(string))
			anime.SetTitle(title, json[string].get<std::string>());
}

static void ParseSubtype(Anime::Anime &anime, const std::string &str)
{
	static const std::map<std::string, Anime::SeriesFormat> lookup = {
	    {"ONA",     Anime::SeriesFormat::Ona    },
        {"OVA",     Anime::SeriesFormat::Ova    },
	    {"TV",      Anime::SeriesFormat::Tv     },
        {"movie",   Anime::SeriesFormat::Movie  },
	    {"music",   Anime::SeriesFormat::Music  },
        {"special", Anime::SeriesFormat::Special}
    };

	if (lookup.find(str) == lookup.end())
		return;

	anime.SetFormat(lookup.at(str));
}

static void ParseListStatus(Anime::Anime &anime, const std::string &str)
{
	static const std::map<std::string, Anime::ListStatus> lookup = {
	    {"completed", Anime::ListStatus::Completed},
	    {"current",   Anime::ListStatus::Current  },
	    {"dropped",   Anime::ListStatus::Dropped  },
	    {"on_hold",   Anime::ListStatus::Paused   },
	    {"planned",   Anime::ListStatus::Planning }
    };

	if (lookup.find(str) == lookup.end())
		return;

	anime.SetUserStatus(lookup.at(str));
}

static void ParseSeriesStatus(Anime::Anime &anime, const std::string &str)
{
	static const std::map<std::string, Anime::SeriesStatus> lookup = {
	    {"current",    Anime::SeriesStatus::Releasing     },
        {"finished",   Anime::SeriesStatus::Finished      },
	    {"tba",        Anime::SeriesStatus::Hiatus        }, // is this right?
	    {"unreleased", Anime::SeriesStatus::Cancelled     },
        {"upcoming",   Anime::SeriesStatus::NotYetReleased},
	};

	if (lookup.find(str) == lookup.end())
		return;

	anime.SetAiringStatus(lookup.at(str));
}

static int ParseAnimeJson(const nlohmann::json &json)
{
	const std::string FAILED_TO_PARSE = Strings::Translate("Kitsu: Failed to parse anime object! {}");

	const std::string service_id = json["/id"_json_pointer].get<std::string>();
	if (service_id.empty()) {
		session.SetStatusBar(fmt::format(FAILED_TO_PARSE, "(/id)"));
		return 0;
	}

	if (!json.contains("/attributes"_json_pointer)) {
		session.SetStatusBar(fmt::format(FAILED_TO_PARSE, "(/attributes)"));
		return 0;
	}

	const auto &attributes = json["/attributes"_json_pointer];

	int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id);

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

	anime.SetId(id);
	anime.SetServiceId(Anime::Service::Kitsu, service_id);

	if (attributes.contains("/synopsis"_json_pointer) && attributes["/synopsis"_json_pointer].is_string())
		anime.SetSynopsis(attributes["/synopsis"_json_pointer].get<std::string>());

	if (attributes.contains("/titles"_json_pointer) && attributes["/titles"_json_pointer].is_object())
		ParseTitleJson(anime, attributes["/titles"_json_pointer]);

	if (attributes.contains("/abbreviatedTitles"_json_pointer) &&
	    attributes["/abbreviatedTitles"_json_pointer].is_array())
		for (const auto &title : attributes["/abbreviatedTitles"_json_pointer])
			anime.AddTitleSynonym(title.get<std::string>());

	if (attributes.contains("/averageRating"_json_pointer) && attributes["/averageRating"_json_pointer].is_string())
		anime.SetAudienceScore(Strings::ToInt<double>(attributes["/averageRating"_json_pointer].get<std::string>()));

	if (attributes.contains("/startDate"_json_pointer) && attributes["/startDate"_json_pointer].is_string())
		anime.SetStartedDate(attributes["/startDate"_json_pointer].get<std::string>());

	anime.SetCompletedDate(attributes.contains("/endDate"_json_pointer) &&
	                               attributes["/endDate"_json_pointer].is_string()
	                           ? attributes["/endDate"_json_pointer].get<std::string>()
	                           : anime.GetStartedDate());

	if (attributes.contains("/subtype"_json_pointer) && attributes["/subtype"_json_pointer].is_string())
		ParseSubtype(anime, attributes["/subtype"_json_pointer].get<std::string>());

	if (attributes.contains("/status"_json_pointer) && attributes["/status"_json_pointer].is_string())
		ParseSeriesStatus(anime, attributes["/status"_json_pointer].get<std::string>());

	if (attributes.contains("/posterImage/original"_json_pointer) &&
	    attributes["/posterImage/original"_json_pointer].is_string())
		anime.SetPosterUrl(attributes["/posterImage/original"_json_pointer].get<std::string>());

	if (attributes.contains("/episodeCount"_json_pointer) && attributes["/episodeCount"_json_pointer].is_number())
		anime.SetEpisodes(attributes["/episodeCount"_json_pointer].get<int>());

	if (attributes.contains("/episodeLength"_json_pointer) && attributes["/episodeLength"_json_pointer].is_number())
		anime.SetDuration(attributes["/episodeLength"_json_pointer].get<int>());

	return id;
}

static int ParseLibraryJson(const nlohmann::json &json)
{
	static const std::vector<nlohmann::json::json_pointer> required = {
	    "/id"_json_pointer,
	    "/relationships/anime/data/id"_json_pointer,
	    "/attributes"_json_pointer,
	};

	for (const auto &ptr : required) {
		if (!json.contains(ptr)) {
			session.SetStatusBar(fmt::format(Strings::Translate("Kitsu: Failed to parse library object! (missing {})"),
			                                 ptr.to_string()));
			return 0;
		}
	}

	std::string service_id = json["/relationships/anime/data/id"_json_pointer].get<std::string>();
	if (service_id.empty()) {
		session.SetStatusBar(Strings::Translate("Kitsu: Failed to parse library object (missing service ID)!"));
		return 0;
	}

	int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id);

	const auto &attributes = json["/attributes"_json_pointer];

	const std::string library_id = json["/id"_json_pointer].get<std::string>();

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

	anime.AddToUserList();

	anime.SetId(id);
	anime.SetServiceId(Anime::Service::Kitsu, service_id);

	anime.SetUserId(library_id);

	if (attributes.contains("/startedAt"_json_pointer) && attributes["/startedAt"_json_pointer].is_string())
		anime.SetUserDateStarted(
		    Date(Time::ParseISO8601Time(attributes["/startedAt"_json_pointer].get<std::string>())));

	if (attributes.contains("/finishedAt"_json_pointer) && attributes["/finishedAt"_json_pointer].is_string())
		anime.SetUserDateCompleted(
		    Date(Time::ParseISO8601Time(attributes["/finishedAt"_json_pointer].get<std::string>())));

	if (attributes.contains("/notes"_json_pointer) && attributes["/notes"_json_pointer].is_string())
		anime.SetUserNotes(attributes["/notes"_json_pointer].get<std::string>());

	if (attributes.contains("/progress"_json_pointer) && attributes["/progress"_json_pointer].is_number())
		anime.SetUserProgress(attributes["/progress"_json_pointer].get<int>());

	if (attributes.contains("/ratingTwenty"_json_pointer) && attributes["/ratingTwenty"_json_pointer].is_number())
		anime.SetUserScore(attributes["/ratingTwenty"_json_pointer].get<int>() * 5);

	if (attributes.contains("/private"_json_pointer) && attributes["/private"_json_pointer].is_boolean())
		anime.SetUserIsPrivate(attributes["/private"_json_pointer].get<bool>());

	if (attributes.contains("/reconsumeCount"_json_pointer) && attributes["/reconsumeCount"_json_pointer].is_number())
		anime.SetUserRewatchedTimes(attributes["/reconsumeCount"_json_pointer].get<int>());

	if (attributes.contains("/reconsuming"_json_pointer) && attributes["/reconsuming"_json_pointer].is_boolean())
		anime.SetUserIsRewatching(attributes["/reconsuming"_json_pointer].get<bool>()); /* lmfao "reconsuming" */

	if (attributes.contains("/status"_json_pointer) && attributes["/status"_json_pointer].is_string())
		ParseListStatus(anime, attributes["/status"_json_pointer].get<std::string>());

	if (attributes.contains("/progressedAt"_json_pointer) && attributes["/progressedAt"_json_pointer].is_string())
		anime.SetUserTimeUpdated(Time::ParseISO8601Time(attributes["/progressedAt"_json_pointer].get<std::string>()));

	return id;
}

static void ParseMetadataJson(Anime::Anime &anime, const nlohmann::json &json)
{
	std::vector<std::string> categories;
	std::vector<std::string> producers;

	for (const auto &item : json) {
		std::string variant;
		{
			static const nlohmann::json::json_pointer p = "/type"_json_pointer;

			if (!item.contains(p) || !item[p].is_string())
				continue;

			variant = item[p].get<std::string>();
		}

		/* now parse variants */
		if (variant == "categories") {
			static const nlohmann::json::json_pointer p = "/attributes/title"_json_pointer;

			if (!item.contains(p) || !item[p].is_string())
				continue;

			categories.push_back(item[p].get<std::string>());
		} else if (variant == "producers") {
			static const nlohmann::json::json_pointer p = "/attributes/name"_json_pointer;

			if (!item.contains(p) || !item[p].is_string())
				continue;

			producers.push_back(item[p].get<std::string>());
		}
	}

	anime.SetGenres(categories);
	anime.SetProducers(producers);
}

static bool ParseAnyJson(const nlohmann::json &json)
{
	static const nlohmann::json::json_pointer required = "/type"_json_pointer;
	if (!json.contains(required) && !json[required].is_string()) {
		session.SetStatusBar(
		    fmt::format(Strings::Translate("Kitsu: Failed to generic object! (missing {})"), required.to_string()));
		return 0;
	}

	std::string variant = json["/type"_json_pointer].get<std::string>();

	if (variant == "anime") {
		return !!ParseAnimeJson(json);
	} else if (variant == "libraryEntries") {
		return !!ParseLibraryJson(json);
	} else if (variant == "categories" || variant == "producers") {
		/* do nothing */
	} else {
		std::cerr << "Kitsu: received unknown type " << variant << std::endl;
	}

	return true;
}

int GetAnimeList()
{
	static constexpr int LIBRARY_MAX_SIZE = 500;

	const auto &auth = session.config.auth.kitsu;

	if (auth.user_id.empty()) {
		session.SetStatusBar(Strings::Translate("Kitsu: User ID is unavailable!"));
		return 0;
	}

	int page = 0;
	bool have_next_page = true;

	std::map<std::string, std::string> params = {
	    {"filter[user_id]", auth.user_id                           },
	    {"filter[kind]",    "anime"	                            },
	    {"include",         "anime"	                            },
	    {"page[offset]",    Strings::ToUtf8String(page)            },
	    {"page[limit]",     Strings::ToUtf8String(LIBRARY_MAX_SIZE)}
    };
	AddAnimeFilters(params);
	AddLibraryEntryFilters(params);

	Anime::db.RemoveAllUserData();

	bool success = true;

	while (have_next_page) {
		std::optional<nlohmann::json> response = SendJSONAPIRequest("/library-entries", params);
		if (!response)
			return 0;

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

		if (root.contains("/next"_json_pointer) && root["/next"_json_pointer].is_number()) {
			page += root["/next"_json_pointer].get<int>();
			if (page <= 0)
				have_next_page = false;
		} else
			have_next_page = false;

		for (const auto &item : root["/data"_json_pointer])
			if (!ParseLibraryJson(item))
				success = false;

		for (const auto &variant : root["/included"_json_pointer])
			if (!ParseAnyJson(variant))
				success = false;

		params["page[offset]"] = Strings::ToUtf8String(page);
	}

	if (success)
		session.SetStatusBar(Strings::Translate("Kitsu: Successfully received library data!"));

	return 1;
}

bool RetrieveAnimeMetadata(int id)
{
	/* TODO: the genres should *probably* be a std::optional */
	Anime::Anime &anime = Anime::db.items[id];
	if (anime.GetGenres().size() > 0 && anime.GetProducers().size())
		return false;

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

	session.SetStatusBar(Strings::Translate("Kitsu: Retrieving anime metadata..."));

	static const std::map<std::string, std::string> params = {
	    {"include", Strings::Implode(
	                    {
	                        "categories",
	                        "animeProductions",
	                        "animeProductions.producer",
	                    }, ",")}
    };

	std::optional<nlohmann::json> response = SendJSONAPIRequest("/anime/" + service_id.value(), params);
	if (!response)
		return false;

	const auto &json = response.value();

	if (!json.contains("/included"_json_pointer) || !json["/included"_json_pointer].is_array()) {
		session.SetStatusBar(
		    Strings::Translate("Kitsu: Server returned bad data when trying to retrieve anime metadata!"));
		return false;
	}

	ParseMetadataJson(anime, json["/included"_json_pointer]);

	session.SetStatusBar(Strings::Translate("Kitsu: Successfully retrieved anime metadata!"));

	return true;
}

/* unimplemented for now */
std::vector<int> Search(const std::string &search)
{
	return {};
}

bool GetSeason(Anime::Season season)
{
	return false;
}

int UpdateAnimeEntry(int id)
{
	return 0;
}

bool AuthorizeUser(const std::string &email, const std::string &password)
{
	const nlohmann::json body = {
	    {"grant_type", "password"               },
        {"username",   email                    },
        {"password",   HTTP::UrlEncode(password)}
    };

	if (!SendAuthRequest(body))
		return false;

	static const std::map<std::string, std::string> params = {
	    {"filter[self]", "true"}
    };

	std::optional<nlohmann::json> response = SendJSONAPIRequest("/users", params);
	if (!response)
		return false; // whuh?

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

	if (!json.contains("/data/0/id"_json_pointer)) {
		session.SetStatusBar(Strings::Translate("Kitsu: Failed to retrieve user ID!"));
		return false;
	}

	session.SetStatusBar(Strings::Translate("Kitsu: Successfully authorized user!"));
	session.config.auth.kitsu.user_id = json["/data/0/id"_json_pointer].get<std::string>();

	return true;
}

} // namespace Kitsu
} // namespace Services
