// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.

#include "Downloader.hpp"

#ifdef _WIN32
#include <fcntl.h>
#include <io.h>
#else
#include <sys/fcntl.h>
#endif

#include <algorithm>
#include <filesystem>
#include <memory>
#include <string>
#include <vector>

#include <fmt/format.h>
#include <imgui.h>
#include <imgui_stdlib.h>
#include <libssh/sftp.h>

#include "Sftp.hpp"
#include "wpi/glass/Storage.hpp"
#include "wpi/gui/portable-file-dialogs.h"
#include "wpi/util/StringExtras.hpp"
#include "wpi/util/fs.hpp"

Downloader::Downloader(wpi::glass::Storage& storage)
    : m_serverTeam{storage.GetString("serverTeam")},
      m_remoteDir{storage.GetString("remoteDir", "/home/systemcore/logs")},
      m_username{storage.GetString("username", "systemcore")},
      m_password{"systemcore"},
      m_localDir{storage.GetString("localDir")},
      m_deleteAfter{storage.GetBool("deleteAfter", true)},
      m_thread{[this] { ThreadMain(); }} {}

Downloader::~Downloader() {
  {
    std::scoped_lock lock{m_mutex};
    m_state = kExit;
  }
  m_cv.notify_all();
  m_thread.join();
}

void Downloader::DisplayConnect() {
  // IP or Team Number text box
  ImGui::SetNextItemWidth(ImGui::GetFontSize() * 12);
  ImGui::InputText("Team Number / Address", &m_serverTeam);

  // Username/password
  ImGui::SetNextItemWidth(ImGui::GetFontSize() * 12);
  ImGui::InputText("Username", &m_username);
  ImGui::SetNextItemWidth(ImGui::GetFontSize() * 12);
  ImGui::InputText("Password", &m_password, ImGuiInputTextFlags_Password);

  // Connect button
  if (ImGui::Button("Connect")) {
    m_state = kConnecting;
    m_cv.notify_all();
  }
}

void Downloader::DisplayDisconnectButton() {
  if (ImGui::Button("Disconnect")) {
    m_state = kDisconnecting;
    m_cv.notify_all();
  }
}

void Downloader::DisplayRemoteDirSelector() {
  ImGui::SameLine();
  if (ImGui::Button("Refresh")) {
    m_state = kGetFiles;
    m_cv.notify_all();
  }

  ImGui::SameLine();
  if (ImGui::Button("Deselect All")) {
    for (auto&& download : m_fileList) {
      download.selected = false;
    }
  }

  ImGui::SameLine();
  if (ImGui::Button("Select All")) {
    for (auto&& download : m_fileList) {
      download.selected = true;
    }
  }

  // Remote directory text box
  ImGui::SetNextItemWidth(ImGui::GetFontSize() * 20);
  if (ImGui::InputText("Remote Dir", &m_remoteDir,
                       ImGuiInputTextFlags_EnterReturnsTrue)) {
    m_state = kGetFiles;
    m_cv.notify_all();
  }

  // List directories
  for (auto&& dir : m_dirList) {
    if (ImGui::Selectable(dir.c_str())) {
      if (dir == "..") {
        if (wpi::util::ends_with(m_remoteDir, '/')) {
          m_remoteDir.resize(m_remoteDir.size() - 1);
        }
        m_remoteDir = wpi::util::rsplit(m_remoteDir, '/').first;
        if (m_remoteDir.empty()) {
          m_remoteDir = "/";
        }
      } else {
        if (!wpi::util::ends_with(m_remoteDir, '/')) {
          m_remoteDir += '/';
        }
        m_remoteDir += dir;
      }
      m_state = kGetFiles;
      m_cv.notify_all();
    }
  }
}

void Downloader::DisplayLocalDirSelector() {
  // Local directory text / select button
  if (ImGui::Button("Select Download Folder...")) {
    m_localDirSelector =
        std::make_unique<pfd::select_folder>("Select Download Folder");
  }
  ImGui::TextUnformatted(m_localDir.c_str());

  // Delete after download (checkbox)
  ImGui::Checkbox("Delete after download", &m_deleteAfter);

  // Download button
  if (!m_localDir.empty()) {
    if (ImGui::Button("Download")) {
      m_state = kDownload;
      m_cv.notify_all();
    }
  }

  ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(180, 0, 0, 255));
  ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(210, 0, 0, 255));
  ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(255, 0, 0, 255));
  if (ImGui::Button("Delete WITHOUT Downloading")) {
    ImGui::OpenPopup("DeleteConfirm");
  }
  ImGui::PopStyleColor(3);
  if (ImGui::BeginPopup("DeleteConfirm")) {
    ImGui::TextUnformatted("Are you sure? This will NOT download the files");
    if (ImGui::Button("DELETE")) {
      m_state = kDelete;
      m_cv.notify_all();
      ImGui::CloseCurrentPopup();
    }
    ImGui::SameLine();
    if (ImGui::Button("Cancel")) {
      ImGui::CloseCurrentPopup();
    }
    ImGui::EndPopup();
  }
}

size_t Downloader::DisplayFiles() {
  // List of files (multi-select) (changes to progress bar for downloading)
  size_t fileCount = 0;
  if (ImGui::BeginTable(
          "files", 3,
          ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) {
    ImGui::TableSetupColumn("File");
    ImGui::TableSetupColumn("Size");
    ImGui::TableSetupColumn((m_state == kDownload || m_state == kDownloadDone ||
                             m_state == kDelete || m_state == kDeleteDone)
                                ? "Status"
                                : "Selected");
    ImGui::TableHeadersRow();
    for (auto&& file : m_fileList) {
      if ((m_state == kDownload || m_state == kDownloadDone ||
           m_state == kDelete || m_state == kDeleteDone) &&
          !file.selected) {
        continue;
      }

      ++fileCount;

      ImGui::TableNextRow();
      ImGui::TableNextColumn();
      ImGui::TextUnformatted(file.name.c_str());
      ImGui::TableNextColumn();
      auto sizeText = fmt::format("{}", file.size);
      ImGui::TextUnformatted(sizeText.c_str());
      ImGui::TableNextColumn();
      if (m_state == kDownload || m_state == kDownloadDone) {
        if (!file.status.empty()) {
          ImGui::TextUnformatted(file.status.c_str());
        } else {
          ImGui::ProgressBar(file.complete);
        }
      } else if (m_state == kDelete || m_state == kDeleteDone) {
        if (!file.status.empty()) {
          ImGui::TextUnformatted(file.status.c_str());
        }
      } else {
        auto checkboxLabel = fmt::format("##{}", file.name);
        ImGui::Checkbox(checkboxLabel.c_str(), &file.selected);
      }
    }
    ImGui::EndTable();
  }

  return fileCount;
}

void Downloader::Display() {
  if (m_localDirSelector && m_localDirSelector->ready(0)) {
    m_localDir = m_localDirSelector->result();
    m_localDirSelector.reset();
  }

  std::scoped_lock lock{m_mutex};

  if (!m_error.empty()) {
    ImGui::TextUnformatted(m_error.c_str());
  }

  switch (m_state) {
    case kDisconnected:
      DisplayConnect();
      break;
    case kConnecting:
      DisplayDisconnectButton();
      ImGui::Text("Connecting to %s...", m_serverTeam.c_str());
      break;
    case kDisconnecting:
      ImGui::TextUnformatted("Disconnecting...");
      break;
    case kConnected:
    case kGetFiles:
      DisplayDisconnectButton();
      DisplayRemoteDirSelector();
      if (DisplayFiles() > 0) {
        DisplayLocalDirSelector();
      }
      break;
    case kDownload:
    case kDownloadDone:
      DisplayDisconnectButton();
      DisplayFiles();
      if (m_state == kDownloadDone) {
        if (ImGui::Button("Download complete!")) {
          m_state = kGetFiles;
          m_cv.notify_all();
        }
      }
      break;
    case kDelete:
    case kDeleteDone:
      DisplayDisconnectButton();
      DisplayFiles();
      if (m_state == kDeleteDone) {
        if (ImGui::Button("Deletion complete!")) {
          m_state = kGetFiles;
          m_cv.notify_all();
        }
      }
      break;
    default:
      break;
  }
}

void Downloader::ThreadMain() {
  std::unique_ptr<sftp::Session> session;

  static constexpr size_t kBufSize = 32 * 1024;
  std::unique_ptr<uint8_t[]> copyBuf = std::make_unique<uint8_t[]>(kBufSize);

  std::unique_lock lock{m_mutex};
  while (m_state != kExit) {
    State prev = m_state;
    m_cv.wait(lock, [&] { return m_state != prev; });
    m_error.clear();
    try {
      switch (m_state) {
        case kConnecting:
          if (auto team =
                  wpi::util::parse_integer<unsigned int>(m_serverTeam, 10)) {
            // team number
            session = std::make_unique<sftp::Session>("robot.local", 22,
                                                      m_username, m_password);
          } else {
            session = std::make_unique<sftp::Session>(m_serverTeam, 22,
                                                      m_username, m_password);
          }
          lock.unlock();
          try {
            session->Connect();
          } catch (...) {
            lock.lock();
            throw;
          }
          lock.lock();
          // FALLTHROUGH
        case kGetFiles: {
          std::string dir = m_remoteDir;
          std::vector<sftp::Attributes> fileList;
          lock.unlock();
          try {
            fileList = session->ReadDir(dir);
          } catch (sftp::Exception& ex) {
            lock.lock();
            if (ex.err == SSH_FX_OK || ex.err == SSH_FX_CONNECTION_LOST) {
              throw;
            }
            m_error = ex.what();
            m_dirList.clear();
            m_fileList.clear();
            m_state = kConnected;
            break;
          }
          std::sort(
              fileList.begin(), fileList.end(),
              [](const auto& l, const auto& r) { return l.name < r.name; });
          lock.lock();

          m_dirList.clear();
          m_fileList.clear();
          for (auto&& attr : fileList) {
            if (attr.type == SSH_FILEXFER_TYPE_DIRECTORY) {
              if (attr.name != ".") {
                m_dirList.emplace_back(attr.name);
              }
            } else if (attr.type == SSH_FILEXFER_TYPE_REGULAR &&
                       (attr.flags & SSH_FILEXFER_ATTR_SIZE) != 0 &&
                       wpi::util::ends_with(attr.name, ".wpilog")) {
              m_fileList.emplace_back(attr.name, attr.size);
            }
          }

          m_state = kConnected;
          break;
        }
        case kDisconnecting:
          session.reset();
          m_state = kDisconnected;
          break;
        case kDownload: {
          for (auto&& file : m_fileList) {
            if (m_state != kDownload) {
              // user aborted
              break;
            }
            if (!file.selected) {
              continue;
            }

            auto remoteFilename = fmt::format(
                "{}{}{}", m_remoteDir,
                wpi::util::ends_with(m_remoteDir, '/') ? "" : "/", file.name);
            auto localFilename = fs::path{m_localDir} / file.name;
            uint64_t fileSize = file.size;

            lock.unlock();

            // open local file
            std::error_code ec;
            fs::file_t of = fs::OpenFileForWrite(localFilename, ec,
                                                 fs::CD_CreateNew, fs::OF_None);
            if (ec) {
              // failed to open
              lock.lock();
              file.status = ec.message();
              continue;
            }
            int ofd = fs::FileToFd(of, ec, fs::OF_None);
            if (ofd == -1 || ec) {
              // failed to convert to fd
              lock.lock();
              file.status = ec.message();
              continue;
            }

            try {
              // open remote file
              sftp::File f = session->Open(remoteFilename, O_RDONLY, 0);

              // copy in chunks
              uint64_t total = 0;
              while (total < fileSize) {
                uint64_t toCopy = (std::min)(fileSize - total,
                                             static_cast<uint64_t>(kBufSize));
                auto copied = f.Read(copyBuf.get(), toCopy);
                if (write(ofd, copyBuf.get(), copied) !=
                    static_cast<int64_t>(copied)) {
                  // error writing
                  close(ofd);
                  fs::remove(localFilename, ec);
                  lock.lock();
                  file.status = "error writing local file";
                  goto err;
                }
                total += copied;
                lock.lock();
                file.complete = static_cast<float>(total) / fileSize;
                lock.unlock();
              }

              // close local file
              close(ofd);
              ofd = -1;

              // delete remote file (if enabled)
              if (m_deleteAfter) {
                f = sftp::File{};
                session->Unlink(remoteFilename);
              }
            } catch (sftp::Exception& ex) {
              if (ofd != -1) {
                // close local file and delete it (due to failure)
                close(ofd);
                fs::remove(localFilename, ec);
              }
              lock.lock();
              file.status = ex.what();
              if (ex.err == SSH_FX_OK || ex.err == SSH_FX_CONNECTION_LOST) {
                throw;
              }
              continue;
            }
            lock.lock();
          err: {}
          }
          if (m_state == kDownload) {
            m_state = kDownloadDone;
          }
          break;
        }
        case kDelete: {
          for (auto&& file : m_fileList) {
            if (m_state != kDelete) {
              // user aborted
              break;
            }
            if (!file.selected) {
              continue;
            }

            auto remoteFilename = fmt::format(
                "{}{}{}", m_remoteDir,
                wpi::util::ends_with(m_remoteDir, '/') ? "" : "/", file.name);

            lock.unlock();
            try {
              session->Unlink(remoteFilename);
            } catch (sftp::Exception& ex) {
              lock.lock();
              file.status = ex.what();
              if (ex.err == SSH_FX_OK || ex.err == SSH_FX_CONNECTION_LOST) {
                throw;
              }
              continue;
            }
            lock.lock();
            file.status = "Deleted";
          }
          if (m_state == kDelete) {
            m_state = kDeleteDone;
          }
          break;
        }
        default:
          break;
      }
    } catch (sftp::Exception& ex) {
      m_error = ex.what();
      session.reset();
      m_state = kDisconnected;
    }
  }
}
