#include "window.h"
#include "json.h"
#include <curl/curl.h>
#include <chrono>
#include <exception>
#include <format>
#include "anilist.h"
#include "anime.h"
#include "config.h"
#include "string_utils.h"
#define CLIENT_ID "13706"

size_t AniList::CurlWriteCallback(void *contents, size_t size, size_t nmemb, void *userdata) {
    ((std::string*)userdata)->append((char*)contents, size * nmemb);
    return size * nmemb;
}

std::string AniList::SendRequest(std::string data) {
	struct curl_slist *list = NULL;
	std::string userdata;
	curl = curl_easy_init();
	if (curl) {
		list = curl_slist_append(list, "Accept: application/json");
		list = curl_slist_append(list, "Content-Type: application/json");
		std::string bearer = "Authorization: Bearer " + session.config.anilist.auth_token;
		list = curl_slist_append(list, bearer.c_str());
		curl_easy_setopt(curl, CURLOPT_URL, "https://graphql.anilist.co");
		curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata);
		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback);
		/* FIXME: This sucks. When using HTTPS, we should ALWAYS make sure that our peer
		   is actually valid. I assume the best way to go about this would be to bundle a
		   certificate file, and if it's not found we should *prompt the user* and ask them
		   if it's okay to contact AniList WITHOUT verification. If so, we're golden, and this
		   flag will be set. If not, we should abort mission.

		   For this program, it's probably fine to just contact AniList without
		   HTTPS verification. However it should still be in the list of things to do... */
		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, FALSE);
		res = curl_easy_perform(curl);
		curl_slist_free_all(list);
		if (res != CURLE_OK) {
			QMessageBox box(QMessageBox::Icon::Critical, "", QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res)));
			box.exec();
			curl_easy_cleanup(curl);
			return "";
		}
		curl_easy_cleanup(curl);
		return userdata;
	}
	return "";
}

int AniList::GetUserId(std::string name) {
#define QUERY "query ($name: String) {\n" \
			  "  User (name: $name) {\n" \
			  "    id\n" \
			  "  }\n" \
			  "}\n"
	nlohmann::json json = {
		{"query", QUERY},
		{"variables", {
			{"name", name}
		}}
	};
	auto ret = nlohmann::json::parse(SendRequest(json.dump()));
	return ret["data"]["User"]["id"].get<int>();
#undef QUERY
}

/* Maps to convert string forms to our internal enums */

std::map<std::string, enum AnimeWatchingStatus> StringToAnimeWatchingMap = {
	{"CURRENT",   CURRENT},
	{"PLANNING",  PLANNING},
	{"COMPLETED", COMPLETED},
	{"DROPPED",   DROPPED},
	{"PAUSED",    PAUSED},
	{"REPEATING", REPEATING}
};

std::map<std::string, enum AnimeAiringStatus> StringToAnimeAiringMap = {
	{"FINISHED",         FINISHED},
	{"RELEASING",        RELEASING},
	{"NOT_YET_RELEASED", NOT_YET_RELEASED},
	{"CANCELLED",        CANCELLED},
	{"HIATUS",           HIATUS}
};

std::map<std::string, enum AnimeSeason> StringToAnimeSeasonMap = {
	{"WINTER", WINTER},
	{"SPRING", SPRING},
	{"SUMMER", SUMMER},
	{"FALL",   FALL}
};

std::map<std::string, enum AnimeFormat> StringToAnimeFormatMap = {
	{"TV",       TV},
	{"TV_SHORT", TV_SHORT},
	{"MOVIE",    MOVIE},
	{"SPECIAL",  SPECIAL},
	{"OVA",      OVA},
	{"ONA",      ONA},
	{"MUSIC",    MUSIC},
	{"MANGA",    MANGA},
	{"NOVEL",    NOVEL},
	{"ONE_SHOT", ONE_SHOT}
};

int AniList::UpdateAnimeList(std::vector<AnimeList>* anime_lists, int id) {
/* NOTE: these should be in the qrc file */
#define QUERY "query ($id: Int) {\n" \
"  MediaListCollection (userId: $id, type: ANIME) {\n" \
"    lists {\n" \
"      name\n" \
"      entries {\n" \
"        score\n" \
"        notes\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" \
"          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" \
"          genres\n" \
"          episodes\n" \
"          duration\n" \
"          synonyms\n" \
"          description(asHtml: false)\n" \
"        }\n" \
"      }\n" \
"    }\n" \
"  }\n" \
"}\n"
	nlohmann::json json = {
		{"query", QUERY},
		{"variables", {
			{"id", id}
		}}
	};
	/* TODO: do a try catch here, catch any json errors and then call
       Authorize() if needed */
	auto res = nlohmann::json::parse(SendRequest(json.dump()));
	/* TODO: make sure that we actually need the wstring converter and see
	   if we can just get wide strings back from nlohmann::json */
	for (const auto& list : res["data"]["MediaListCollection"]["lists"].items()) {
		/* why are the .key() values strings?? */
		int list_key = std::stoi(list.key());
		AnimeList anime_list;
		anime_list.name = StringUtils::Utf8ToWstr(JSON::GetString(list.value(), "name"));
		for (const auto& entry : list.value()["entries"].items()) {
			int entry_key = std::stoi(entry.key());
			Anime anime;
			anime.score = JSON::GetInt(entry.value(), "score");
			anime.progress = JSON::GetInt(entry.value(), "progress");
			anime.status = StringToAnimeWatchingMap[JSON::GetString(entry.value(), "status")];
			anime.notes = StringUtils::Utf8ToWstr(JSON::GetString(entry.value(), "notes"));

			anime.started.SetYear(JSON::GetInt(entry.value()["startedAt"], "year"));
			anime.started.SetMonth(JSON::GetInt(entry.value()["startedAt"], "month"));
			anime.started.SetDay(JSON::GetInt(entry.value()["startedAt"], "day"));

			anime.completed.SetYear(JSON::GetInt(entry.value()["completedAt"], "year"));
			anime.completed.SetMonth(JSON::GetInt(entry.value()["completedAt"], "month"));
			anime.completed.SetDay(JSON::GetInt(entry.value()["completedAt"], "day"));

			anime.updated = JSON::GetInt(entry.value(), "updatedAt");

			anime.title.native  = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "native"));
			anime.title.english = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "english"));
			anime.title.romaji  = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "romaji"));

			anime.id = JSON::GetInt(entry.value()["media"], "id");
			anime.episodes = JSON::GetInt(entry.value()["media"], "episodes");
			anime.type = StringToAnimeFormatMap[JSON::GetString(entry.value()["media"], "format")];

			anime.airing = StringToAnimeAiringMap[JSON::GetString(entry.value()["media"], "status")];

			anime.air_date.SetYear(JSON::GetInt(entry.value()["media"]["startDate"], "year"));
			anime.air_date.SetMonth(JSON::GetInt(entry.value()["media"]["startDate"], "month"));
			anime.air_date.SetDay(JSON::GetInt(entry.value()["media"]["startDate"], "day"));

			anime.audience_score = JSON::GetInt(entry.value()["media"], "averageScore");
			anime.season = StringToAnimeSeasonMap[JSON::GetString(entry.value()["media"], "season")];
			anime.duration = JSON::GetInt(entry.value()["media"], "duration");
			anime.synopsis = StringUtils::TextifySynopsis(StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"], "duration")));

			if (entry.value()["media"]["genres"].is_array())
				anime.genres = entry.value()["media"]["genres"].get<std::vector<std::string>>();
			anime_list.Add(anime);
		}
		anime_lists->push_back(anime_list);
	}
	return 1;
#undef QUERY
}

int AniList::Authorize() {
	if (session.config.anilist.auth_token.empty()) {
		/* Prompt for PIN */
		QDesktopServices::openUrl(QUrl("https://anilist.co/api/v2/oauth/authorize?client_id=" 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()) {
			session.config.anilist.auth_token = token.toStdString();
		} else { // fail
			return 0;
		}
	}
	return 1;
}
