// SPDX-License-Identifier: Unlicense

#include "spotify.h"

#ifdef SHR3D_SPOTIFY

#include "file.h"
#include "global.h"
#include "ini.h"
#include "json.h"

//#include "boost/beast.hpp"
#include "curl/curl.h"
#include "openssl/sha.h"
#include "openssl/evp.h"

//#include <future>
#include <random>

static const char* AUTH_URL = "https://accounts.spotify.com/authorize";
static const char* SCOPE = "user-read-currently-playing%20app-remote-control%20playlist-read-private%20user-library-modify%20user-read-playback-position%20user-modify-playback-state";
static const char* PKCE_METHOD = "S256"; // You can also use "plain" if preferred


static std::string authorizationCode;
static std::string codeVerifier;

static std::string accessToken;
static std::string refreshToken;
static TimeNS expireDate{};


static size_t writeStdString(void* contents, size_t size, size_t nmemb, void* userp)
{
  std::string& response = *reinterpret_cast<std::string*>(userp);

  response.append(reinterpret_cast<char*>(contents), size * nmemb);
  return size * nmemb;
}

static std::string generateRandomCodeVerifier()
{
  const std::string charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
  const i32 codeVerifierLength = 64; // Adjust the length as needed (must be between 43 and 128 characters)

  std::random_device rd;
  std::mt19937 generator(rd());
  std::uniform_int_distribution<int> distribution(0, charset.size() - 1);

  std::string codeVerifier;
  for (i32 i = 0; i < codeVerifierLength; ++i)
  {
    codeVerifier += charset[distribution(generator)];
  }

  return codeVerifier;
}

static std::string generateCodeChallenge(const std::string& codeVerifier)
{
  // SHA-256 hashing
  //unsigned char digest[SHA256_DIGEST_LENGTH];
  //SHA256_CTX sha256;
  //SHA256_Init(&sha256);
  //SHA256_Update(&sha256, codeVerifier.c_str(), codeVerifier.size());
  //SHA256_Final(digest, &sha256);


  unsigned char digest[SHA256_DIGEST_LENGTH];
  EVP_MD_CTX* mdctx;
  mdctx = EVP_MD_CTX_new();
  EVP_DigestInit(mdctx, EVP_sha256());
  EVP_DigestUpdate(mdctx, codeVerifier.c_str(), codeVerifier.size());
  EVP_DigestFinal(mdctx, digest, NULL);
  EVP_MD_CTX_free(mdctx);


  // Base64 URL encoding
  BIO* b64 = BIO_new(BIO_f_base64());
  BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
  BIO* mem = BIO_new(BIO_s_mem());
  BIO_push(b64, mem);
  BIO_write(b64, digest, sizeof(digest));
  BIO_flush(b64);

  char* codeChallengeBuffer = nullptr;
  long codeChallengeLength = BIO_get_mem_data(mem, &codeChallengeBuffer);

  // Remove padding '=' characters from the base64 encoding
  std::string codeChallenge(codeChallengeBuffer, codeChallengeLength - 1);

  // Cleanup
  BIO_free_all(b64);

  return codeChallenge;
}

static void token(std::string& response)
{
  CURL* hnd = curl_easy_init();

  std::string postFields = "grant_type=authorization_code"
    "&code=" + authorizationCode +
    "&client_id=" + Settings::spotifyClientId +
    "&client_secret=" + Settings::spotifyClientSecret +
    "&redirect_uri=" + Settings::spotifyRedirectUri +
    "&code_verifier=" + codeVerifier;

  curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L);
  curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L);
  curl_easy_setopt(hnd, CURLOPT_FOLLOWLOCATION, 1L);
  curl_easy_setopt(hnd, CURLOPT_USERAGENT, "");
  curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L);
  curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYPEER, 0L);
  curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYHOST, 0L);
  curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L);
  curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L);
  curl_easy_setopt(hnd, CURLOPT_URL, "https://accounts.spotify.com/api/token");
  curl_easy_setopt(hnd, CURLOPT_POSTFIELDS, postFields.c_str());
  curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "POST");

  curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, writeStdString);
  curl_easy_setopt(hnd, CURLOPT_WRITEDATA, &response);

  CURLcode res = curl_easy_perform(hnd);
  if (res != CURLE_OK)
  {
    ASSERT(false);
  }

  curl_easy_cleanup(hnd);
}

static void parseToken(const std::string& response)
{
  Json::value* root = Json::parse(response.data(), response.size());
  ASSERT(root != nullptr);
  ASSERT(root->type == Json::type_object);

  Json::object* object = (Json::object*)root->payload;
  ASSERT(object->length == 5);

  Json::object_element* entries = object->start;

  {
    ASSERT(strcmp(entries->name->string, "access_token") == 0);

    Json::value* entries_value = entries->value;
    ASSERT(entries_value->type == Json::type_string);

    Json::string* id_o = (Json::string*)entries_value->payload;
    accessToken = id_o->string;
  }

  entries = entries->next;

  {
    ASSERT(strcmp(entries->name->string, "token_type") == 0);

    Json::value* entries_value = entries->value;
    ASSERT(entries_value->type == Json::type_string);

    Json::string* id_o = (Json::string*)entries_value->payload;
    ASSERT(strcmp(id_o->string, "Bearer") == 0);
  }

  entries = entries->next;

  {
    ASSERT(strcmp(entries->name->string, "expires_in") == 0);

    Json::value* entries_value = entries->value;
    ASSERT(entries_value->type == Json::type_number);

    Json::number* id_o = (Json::number*)entries_value->payload;

    const TimeNS unixTimestampNS = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch()).count();

    expireDate = unixTimestampNS + (atoi(id_o->number) * 1_s);
  }

  entries = entries->next;

  {
    ASSERT(strcmp(entries->name->string, "refresh_token") == 0);

    Json::value* entries_value = entries->value;
    ASSERT(entries_value->type == Json::type_string);

    Json::string* id_o = (Json::string*)entries_value->payload;
    refreshToken = id_o->string;
  }

  entries = entries->next;

  {
    ASSERT(strcmp(entries->name->string, "scope") == 0);

    Json::value* entries_value = entries->value;
    ASSERT(entries_value->type == Json::type_string);

    Json::string* id_o = (Json::string*)entries_value->payload;

    ASSERT(strcmp(id_o->string, "playlist-read-private app-remote-control user-modify-playback-state user-library-modify user-read-currently-playing user-read-playback-position") == 0);
  }
}

static void listenHttpForCallback()
{
  WSADATA wsa;
  SOCKET server_socket, client_socket;
  struct sockaddr_in server_addr, client_addr;
  int addr_size = sizeof(struct sockaddr_in);
  char request_buffer[2048];

  // Initialize Winsock
  if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
    ASSERT(false); // WSAStartup failed

  // Create socket
  if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
    ASSERT(false); // Socket creation failed

  server_addr.sin_family = AF_INET;
  server_addr.sin_addr.s_addr = INADDR_ANY;
  server_addr.sin_port = htons(8888);

  // Bind the socket
  if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR)
    ASSERT(false); // Socket bind failed

  // Listen for incoming connections
  if (listen(server_socket, 10) == SOCKET_ERROR)
    ASSERT(false); // Listen failed

  // Accept a connection
  client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &addr_size);
  if (client_socket == INVALID_SOCKET)
    ASSERT(false); // Accept failed

  // Receive the HTTP request
  recv(client_socket, request_buffer, sizeof(request_buffer), 0);

  {
    const i32 beginChar = sizeof("GET /callback?code=") - 1;
    ASSERT(memcmp(request_buffer, "GET /callback?code=", beginChar) == 0);
    i32 endChar = beginChar;
    while (request_buffer[endChar] != ' ')
      ++endChar;

    authorizationCode.assign(&request_buffer[beginChar], endChar - beginChar);
  }

  { // send response
    const char* response = R"(<!DOCTYPE html>
<html>
  <head>
    <title>Shr3D</title>
    <style>
      body {
        background-color:#000000;
      }
      h1 {
        font-family: sans-serif;
        font-size: 10rem;
        background-image: linear-gradient(to left, violet, indigo, blue, green, yellow, orange, red);
        -webkit-background-clip: text;
        color: transparent;
      }
    </style>
  </head>
  <body>
    <h1>Shr3D</h1>
    <h1>Authorization code received.</h1>
    <h1>Back to shredding!</h1>
  </body>
</html>)";

    char header[100];
    sprintf(header, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n", i32(strlen(response)));
    send(client_socket, header, strlen(header), 0);
    send(client_socket, response, strlen(response), 0);
  }

  closesocket(client_socket);

  closesocket(server_socket);
  WSACleanup();
}

static void initSpotify()
{
  if (expireDate == 0 || expireDate < std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch()).count())
  {
    if (File::exists("secrets.ini"))
    {
      const std::vector<u8> oauth2IniData = File::read("secrets.ini");
      const std::map<std::string, std::map<std::string, std::string>> iniContent = Ini::loadIniContent(reinterpret_cast<const char*>(oauth2IniData.data()), oauth2IniData.size());
      if (iniContent.contains("Spotify"))
      {
        const std::map<std::string, std::string>& spotify = iniContent.at("Spotify");

        if (spotify.contains("AccessToken"))
          accessToken = spotify.at("AccessToken");
        if (spotify.contains("ExpireDate"))
          expireDate = timeNS_From_String(spotify.at("ExpireDate"));
        if (spotify.contains("RefreshToken"))
          refreshToken = spotify.at("RefreshToken");
      }
    }

    if (accessToken.empty()
      || refreshToken.empty()
      || expireDate == 0 || expireDate < std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch()).count())
    {
      codeVerifier = generateRandomCodeVerifier();

      {
        const std::string authUrl = std::string(AUTH_URL)
          + "?response_type=code"
          + "&client_id=" + Settings::spotifyClientId
          + "&scope=" + SCOPE
          + "&redirect_uri=" + Settings::spotifyRedirectUri
          + "&code_challenge_method=" + PKCE_METHOD
          + "&code_challenge=" + generateCodeChallenge(codeVerifier);

        ShellExecuteA(0, 0, authUrl.c_str(), 0, 0, SW_SHOW);
      }

      listenHttpForCallback();
      //std::future<void> webserver = std::async(std::launch::async, listenHttpForCallback);

      //while (authorizationCode.empty())
      //  std::this_thread::sleep_for(std::chrono::duration<double, std::milli>(100));

      //ioc.stop();

      std::string response;
      token(response);
      parseToken(response);

      std::map<std::string, std::map<std::string, std::string>> iniContent;
      iniContent["Spotify"]["AccessToken"] = accessToken;
      iniContent["Spotify"]["ExpireDate"] = TimeNS_To_String(expireDate);
      iniContent["Spotify"]["RefreshToken"] = refreshToken;

      Ini::saveIniFile("secrets.ini", iniContent);
    }
  }
}

void Spotify::playSong(const char spotifyTrackId[23])
{
  ASSERT(strlen(spotifyTrackId) == 22);

  char postField[] = "{\"uris\":[\"spotify:track:XXXXXXXXXXXXXXXXXXXXXX\"]}";
  memcpy(&postField[24], spotifyTrackId, 22);

  initSpotify();

  CURL* hnd = curl_easy_init();

  curl_slist* slist1;
  slist1 = NULL;
  slist1 = curl_slist_append(slist1, "Content-Type: application/json");
  slist1 = curl_slist_append(slist1, ("Authorization: Bearer " + accessToken).c_str());

  curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L);
  curl_easy_setopt(hnd, CURLOPT_URL, "https://api.spotify.com/v1/me/player/play");
  curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L);
  curl_easy_setopt(hnd, CURLOPT_POSTFIELDS, postField);
  curl_easy_setopt(hnd, CURLOPT_POSTFIELDSIZE_LARGE, sizeof(postField) - 1);
  curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1);
  curl_easy_setopt(hnd, CURLOPT_USERAGENT, "");
  curl_easy_setopt(hnd, CURLOPT_FOLLOWLOCATION, 1L);
  curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L);
  curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "PUT");
  curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L);
  curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L);
  curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYPEER, 0L);
  curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYHOST, 0L);

  CURLcode res = curl_easy_perform(hnd);
  if (res != CURLE_OK)
  {
    ASSERT(false);
  }

  curl_easy_cleanup(hnd);
}

void Spotify::pauseSong()
{
  initSpotify();

  CURL* hnd = curl_easy_init();

  curl_slist* slist1;
  slist1 = NULL;
  slist1 = curl_slist_append(slist1, "Content-Type: application/json");
  slist1 = curl_slist_append(slist1, ("Authorization: Bearer " + accessToken).c_str());

  curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L);
  curl_easy_setopt(hnd, CURLOPT_URL, "https://api.spotify.com/v1/me/player/pause");
  curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L);
  curl_easy_setopt(hnd, CURLOPT_POSTFIELDS, "");
  curl_easy_setopt(hnd, CURLOPT_POSTFIELDSIZE_LARGE, 0);
  curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1);
  curl_easy_setopt(hnd, CURLOPT_USERAGENT, "");
  curl_easy_setopt(hnd, CURLOPT_FOLLOWLOCATION, 1L);
  curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L);
  curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "PUT");
  curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L);
  curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L);
  curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYPEER, 0L);
  curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYHOST, 0L);

  CURLcode res = curl_easy_perform(hnd);
  if (res != CURLE_OK)
  {
    ASSERT(false);
  }

  curl_easy_cleanup(hnd);
}

void Spotify::setVolume(const i8 volumePercent)
{
  ASSERT(volumePercent >= 0);
  ASSERT(volumePercent <= 100);

  initSpotify();

  std::string postFields = "volume_percent=" + std::to_string(volumePercent);

  CURL* hnd = curl_easy_init();

  curl_slist* slist1;
  slist1 = NULL;
  slist1 = curl_slist_append(slist1, "Content-Type: application/json");
  slist1 = curl_slist_append(slist1, ("Authorization: Bearer " + accessToken).c_str());

  curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L);
  curl_easy_setopt(hnd, CURLOPT_URL, "https://api.spotify.com/v1/me/player/play");
  curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L);
  curl_easy_setopt(hnd, CURLOPT_POSTFIELDS, postFields.c_str());
  curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1);
  curl_easy_setopt(hnd, CURLOPT_USERAGENT, "");
  curl_easy_setopt(hnd, CURLOPT_FOLLOWLOCATION, 1L);
  curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L);
  curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "PUT");
  curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L);
  curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L);
  curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYPEER, 0L);
  curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYHOST, 0L);

  CURLcode res = curl_easy_perform(hnd);
  if (res != CURLE_OK)
  {
    ASSERT(false);
  }

  curl_easy_cleanup(hnd);
}

void Spotify::seekPosition(const i32 positionMs)
{
  ASSERT(positionMs >= 0);

  initSpotify();

  std::string postFields = "position_ms=" + std::to_string(positionMs);

  CURL* hnd = curl_easy_init();

  curl_slist* slist1;
  slist1 = NULL;
  slist1 = curl_slist_append(slist1, "Content-Type: application/json");
  slist1 = curl_slist_append(slist1, ("Authorization: Bearer " + accessToken).c_str());

  curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L);
  curl_easy_setopt(hnd, CURLOPT_URL, "https://api.spotify.com/v1/me/player/seek");
  curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L);
  curl_easy_setopt(hnd, CURLOPT_POSTFIELDS, postFields.c_str());
  curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1);
  curl_easy_setopt(hnd, CURLOPT_USERAGENT, "");
  curl_easy_setopt(hnd, CURLOPT_FOLLOWLOCATION, 1L);
  curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L);
  curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "PUT");
  curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L);
  curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L);
  curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYPEER, 0L);
  curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYHOST, 0L);

  CURLcode res = curl_easy_perform(hnd);
  if (res != CURLE_OK)
  {
    ASSERT(false);
  }

  curl_easy_cleanup(hnd);
}

#endif // SHR3D_SPOTIFY
