#include "updater.h"

#include <fmt/core.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <wx/evtloop.h>
#include <wx/progdlg.h>
#include <wx/webrequest.h>
#include <wx/wfstream.h>
#include <wx/zipstrm.h>
#include <yaml-cpp/yaml.h>

#include <cstdio>
#include <deque>
#include <filesystem>
#include <fstream>

#include "global.h"
#include "logger.h"
#include "version.h"

constexpr const char* kVersionFileUrl =
    "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION.yaml";
constexpr const char* kChangelogUrl =
    "https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md";

namespace {

std::string CalculateStringSha256(const wxString& data) {
  unsigned char hash[SHA256_DIGEST_LENGTH];
  EVP_MD_CTX* sha256 = EVP_MD_CTX_new();
  EVP_DigestInit(sha256, EVP_sha256());
  EVP_DigestUpdate(sha256, data.c_str(), data.length());
  EVP_DigestFinal_ex(sha256, hash, nullptr);
  EVP_MD_CTX_free(sha256);

  char output[65] = {0};
  for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
    sprintf(output + (i * 2), "%02x", hash[i]);
  }

  return std::string(output);
}

}  // namespace

Updater::Updater(wxFrame* parent) : parent_(parent) {
  Bind(wxEVT_WEBREQUEST_STATE, &Updater::OnWebRequestState, this);
}

void Updater::Cleanup() {
  std::filesystem::path oldDir = GetExecutableDirectory() / "old";
  if (std::filesystem::is_directory(oldDir)) {
    std::filesystem::remove_all(oldDir);
  }
}

void Updater::CheckForUpdates(bool invisible) {
  wxWebRequest versionRequest =
      wxWebSession::GetDefault().CreateRequest(this, kVersionFileUrl);

  if (invisible) {
    update_state_ = UpdateState::GetVersionInvisible;

    versionRequest.Start();
  } else {
    update_state_ = UpdateState::GetVersionManual;

    if (DownloadWithProgress(versionRequest)) {
      if (versionRequest.GetState() == wxWebRequest::State_Failed) {
        wxMessageBox("Could not check for updates.", "Error",
                     wxOK | wxICON_ERROR);
      } else if (versionRequest.GetState() == wxWebRequest::State_Completed) {
        ProcessVersionFile(
            versionRequest.GetResponse().AsString().utf8_string());
      }
    }
  }
}

void Updater::OnWebRequestState(wxWebRequestEvent& evt) {
  if (update_state_ == UpdateState::GetVersionInvisible) {
    if (evt.GetState() == wxWebRequest::State_Completed) {
      ProcessVersionFile(evt.GetResponse().AsString().utf8_string());
    } else if (evt.GetState() == wxWebRequest::State_Failed) {
      parent_->SetStatusText("Could not check for updates.");
    }
  }
}

void Updater::ProcessVersionFile(std::string data) {
  try {
    YAML::Node versionInfo = YAML::Load(data);
    Version latestVersion(versionInfo["version"].as<std::string>());

    if (kTrackerVersion < latestVersion) {
      if (versionInfo["packages"]) {
        std::string platformIdentifier;

        if (wxPlatformInfo::Get().GetOperatingSystemId() == wxOS_WINDOWS_NT) {
          platformIdentifier = "win64";
        }

        if (!platformIdentifier.empty() &&
            versionInfo["packages"][platformIdentifier]) {
          wxMessageDialog dialog(
              nullptr,
              fmt::format("There is a newer version of Lingo AP Tracker "
                          "available. You have {}, and the latest version is "
                          "{}. Would you like to update?",
                          kTrackerVersion.ToString(), latestVersion.ToString()),
              "Update available", wxYES_NO | wxCANCEL);
          dialog.SetYesNoLabels("Install update", "Open changelog");

          int dlgResult = dialog.ShowModal();
          if (dlgResult == wxID_YES) {
            const YAML::Node& packageInfo =
                versionInfo["packages"][platformIdentifier];
            std::string packageUrl = packageInfo["url"].as<std::string>();
            std::string packageChecksum =
                packageInfo["checksum"].as<std::string>();

            std::vector<std::filesystem::path> packageFiles;
            if (packageInfo["files"]) {
              for (const YAML::Node& filename : packageInfo["files"]) {
                packageFiles.push_back(filename.as<std::string>());
              }
            }

            std::vector<std::filesystem::path> deletedFiles;
            if (packageInfo["deleted_files"]) {
              for (const YAML::Node& filename : packageInfo["deleted_files"]) {
                deletedFiles.push_back(filename.as<std::string>());
              }
            }

            InstallUpdate(packageUrl, packageChecksum, packageFiles,
                          deletedFiles);
          } else if (dlgResult == wxID_NO) {
            wxLaunchDefaultBrowser(kChangelogUrl);
          }

          return;
        }
      }

      if (wxMessageBox(
              fmt::format("There is a newer version of Lingo AP Tracker "
                          "available. You have {}, and the latest version is "
                          "{}. Would you like to update?",
                          kTrackerVersion.ToString(), latestVersion.ToString()),
              "Update available", wxYES_NO) == wxYES) {
        wxLaunchDefaultBrowser(kChangelogUrl);
      }
    } else if (update_state_ == UpdateState::GetVersionManual) {
      wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker", wxOK);
    }
  } catch (const std::exception& ex) {
    wxMessageBox("Could not check for updates.", "Error", wxOK | wxICON_ERROR);
  }
}

void Updater::InstallUpdate(std::string url, std::string checksum,
                            std::vector<std::filesystem::path> files,
                            std::vector<std::filesystem::path> deletedFiles) {
  update_state_ = UpdateState::GetPackage;

  wxWebRequest packageRequest =
      wxWebSession::GetDefault().CreateRequest(this, url);

  if (!DownloadWithProgress(packageRequest)) {
    return;
  }

  package_path_.clear();
  package_path_.resize(L_tmpnam + 1);
  tmpnam_s(package_path_.data(), L_tmpnam);

  {
    wxFileOutputStream writeOut(package_path_);
    wxString fileData = packageRequest.GetResponse().AsString();
    writeOut.WriteAll(fileData.c_str(), fileData.length());

    std::string downloadedChecksum = CalculateStringSha256(fileData);
    if (downloadedChecksum != checksum) {
      if (wxMessageBox("There was an issue downloading the update. Would you "
                       "like to manually download it instead?",
                       "Error", wxYES_NO | wxICON_ERROR) == wxID_YES) {
        wxLaunchDefaultBrowser(kChangelogUrl);
      }
      return;
    }
  }

  std::filesystem::path newArea = GetExecutableDirectory();
  std::filesystem::path oldArea = newArea / "old";
  std::set<std::filesystem::path> folders;
  std::set<std::filesystem::path> filesToMove;
  for (const std::filesystem::path& existingFile : files) {
    std::filesystem::path movedPath = oldArea / existingFile;
    std::filesystem::path movedDir = movedPath;
    movedDir.remove_filename();
    folders.insert(movedDir);
    filesToMove.insert(existingFile);
  }
  for (const std::filesystem::path& existingFile : deletedFiles) {
    std::filesystem::path movedPath = oldArea / existingFile;
    std::filesystem::path movedDir = movedPath;
    movedDir.remove_filename();
    folders.insert(movedDir);
  }

  for (const std::filesystem::path& newFolder : folders) {
    TrackerLog(fmt::format("Creating directory {}", newFolder.string()));

    std::filesystem::create_directories(newFolder);
  }

  for (const std::filesystem::path& existingFile : files) {
    std::filesystem::path existingPath = newArea / existingFile;

    if (std::filesystem::is_regular_file(existingPath)) {
      std::filesystem::path movedPath = oldArea / existingFile;

      TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
                             movedPath.string()));

      std::filesystem::rename(existingPath, movedPath);
    }
  }
  for (const std::filesystem::path& existingFile : deletedFiles) {
    std::filesystem::path existingPath = newArea / existingFile;

    if (std::filesystem::is_regular_file(existingPath)) {
      std::filesystem::path movedPath = oldArea / existingFile;

      TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
                             movedPath.string()));

      std::filesystem::rename(existingPath, movedPath);
    }
  }

  wxFileInputStream fileInputStream(package_path_);
  wxZipInputStream zipStream(fileInputStream);
  std::unique_ptr<wxZipEntry> zipEntry;
  while ((zipEntry = std::unique_ptr<wxZipEntry>(zipStream.GetNextEntry())) !=
         nullptr) {
    if (zipEntry->IsDir()) {
      continue;
    }

    std::filesystem::path archivePath = zipEntry->GetName().utf8_string();

    TrackerLog(fmt::format("Found {} in archive", archivePath.string()));

    // Cut off the root folder name
    std::filesystem::path subPath;
    for (auto it = std::next(archivePath.begin()); it != archivePath.end();
         it++) {
      subPath /= *it;
    }

    std::filesystem::path pastePath = newArea / subPath;

    wxFileOutputStream fileOutput(pastePath.string());
    zipStream.Read(fileOutput);
  }

  if (wxMessageBox(
      "Update installed! The tracker must be restarted for the changes to take "
      "effect. Do you want to close the tracker?",
                   "Update installed", wxYES_NO) == wxYES) {
    wxExit();
  }
}

bool Updater::DownloadWithProgress(wxWebRequest& request) {
  request.Start();

  wxProgressDialog dialog("Checking for updates...", "Checking for updates...",
                          100, nullptr,
                          wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_CAN_ABORT |
                              wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME);
  while (request.GetState() != wxWebRequest::State_Completed &&
         request.GetState() != wxWebRequest::State_Failed) {
    if (request.GetBytesExpectedToReceive() == -1) {
      if (!dialog.Pulse()) {
        request.Cancel();
        return false;
      }
    } else {
      dialog.SetRange(request.GetBytesExpectedToReceive());
      if (!dialog.Update(request.GetBytesReceived())) {
        request.Cancel();
        return false;
      }
    }
  }

  return true;
}