//=========================================================================
//  Copyright (c) Kitware, Inc.
//  All rights reserved.
//  See LICENSE.txt for details.
//
//  This software is distributed WITHOUT ANY WARRANTY; without even
//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
//  PURPOSE.  See the above copyright notice for more information.
//=========================================================================
#include "smtk/newt/qtNewtInterface.h"

#include <QDateTime>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QHttpMultiPart>
#include <QHttpPart>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkCookie>
#include <QNetworkCookieJar>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTimer>
#include <QUrl>
#include <QUrlQuery>

namespace
{
const QString NEWT_BASE_URL("https://newt.nersc.gov/newt");
}

namespace newt
{

static qtNewtInterface* g_instance = nullptr;

qtNewtInterface* qtNewtInterface::instance(QObject* parent)
{
  if (g_instance == nullptr)
  {
    g_instance = new qtNewtInterface(parent);
  }

  if (g_instance->parent() == nullptr && parent)
  {
    g_instance->setParent(parent);
  }

  return g_instance;
}

qtNewtInterface::qtNewtInterface(QObject* parent)
  : QObject(parent)
  , m_isLoggedIn(false)
  , m_networkManager(new QNetworkAccessManager(this))
{
}

qtNewtInterface::~qtNewtInterface()
{
  if (g_instance == this)
  {
    g_instance = nullptr;
  }

  QObject::disconnect(this);
}

void qtNewtInterface::login(const QString& username, const QString& password)
{
  QString url = QString("%1/login/").arg(NEWT_BASE_URL);
  QNetworkRequest request(url);
  request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");

  QUrlQuery params;
  params.addQueryItem("username", username);
  params.addQueryItem("password", password);

  QNetworkReply* reply = m_networkManager->post(request, params.query().toUtf8());
  QObject::connect(reply, &QNetworkReply::finished, this, &qtNewtInterface::onLoginReply);
  QObject::connect(reply, &QNetworkReply::sslErrors, this, &qtNewtInterface::onSslErrors);
}

void qtNewtInterface::mockLogin(const QString& username, const QString& newtSessionId)
{
  m_username = username;
  m_newtSessionId = newtSessionId;

  QNetworkCookie cookie(QString("newt_sessionid").toUtf8(), m_newtSessionId.toUtf8());
  cookie.setDomain("newt.nersc.gov");
  QNetworkCookieJar* jar = m_networkManager->cookieJar();
  bool ok = jar->insertCookie(cookie);
  // qDebug() << "Set cookie to" << m_newtSessionId << "?" << ok;
  // qDebug() << jar->cookiesForUrl(NEWT_BASE_URL);

  m_isLoggedIn = true;
  emit this->loginComplete(username, 300);
}

bool qtNewtInterface::parseReply(QNetworkReply* reply, QJsonObject& jsonObj, QString& error) const
{
  jsonObj = QJsonObject();
  error.clear();
  QByteArray bytes = reply->readAll();

  // qDebug() << "parseReply received:" << bytes.constData();
  if (reply->error())
  {
    error = this->getErrorMessage(reply, bytes);
    return false;
  }

  QJsonDocument jsonDoc = QJsonDocument::fromJson(bytes.constData());
  if (!jsonDoc.isObject())
  {
    error = "Unexpected reply from NEWT - not a json object: " + QString(bytes.constData());
    return false;
  }

  jsonObj = jsonDoc.object();
  QString errorString = jsonObj.value("error").toString();
  if (!errorString.isEmpty())
  {
    error = QString("NEWT error: %1").arg(errorString);
    return false;
  }

  return true;
}

bool qtNewtInterface::parseReply(QNetworkReply* reply, nlohmann::json& j, std::string& error) const
{
  j.clear();
  error.clear();
  QByteArray bytes = reply->readAll();

  // qDebug() << "parseReply received:" << bytes.constData();
  if (reply->error())
  {
    error = this->getErrorMessage(reply, bytes).toStdString();
    return false;
  }

  j = nlohmann::json::parse(bytes.toStdString());
  if (!j.is_object())
  {
    error = "Unexpected reply from NEWT - not a json object: " + std::string(bytes.constData());
    return false;
  }

  auto iter = j.find("error");
  if (iter != j.end())
  {
    std::string errorString = iter->get<std::string>();
    if (!errorString.empty())
    {
      error = std::string("NEWT error: ") + errorString;
      return false;
    }
  }

  return true;
}

void qtNewtInterface::getCommandReply(QNetworkReply* reply, QString& result, QString& error) const
{
  result.clear();
  error.clear();

  QJsonObject j;
  if (!this->parseReply(reply, j, error))
  {
    QByteArray bytes = reply->readAll();
    result = bytes.constData();
    return;
  }

  // Command result is in the "output" field
  if (j.contains("output"))
  {
    result = j.value("output").toString();
  }
}

QNetworkReply* qtNewtInterface::requestDirectoryList(const QString& machine, const QString& path)
{
  QString url = QString("%1/file/%2/%3").arg(NEWT_BASE_URL, machine, path);
  return this->sendGetRequest(url);
}

QNetworkReply* qtNewtInterface::requestFileDownload(
  const QString& machine,
  const QString& remotePath)
{
  QString url = QString("%1/file/%2/%3?view=read").arg(NEWT_BASE_URL, machine, remotePath);
  QNetworkRequest request(url);
  QNetworkReply* reply = m_networkManager->get(request);
  QObject::connect(reply, &QNetworkReply::sslErrors, this, &qtNewtInterface::onSslErrors);
  return reply;
}

QNetworkReply* qtNewtInterface::requestFileUpload(
  const QString& localFilePath,
  const QString& remotePath,
  const QString& machine)
{
  QFile* file = new QFile(localFilePath);
  if (!file->open(QIODevice::ReadOnly))
  {
    qWarning() << "Unable to open" << localFilePath;
    delete file;
    return nullptr;
  }

  QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);

  QHttpPart filePart;
  QFileInfo fileInfo(file->fileName());
  QString filename(fileInfo.fileName());
  QString dispHeader = QString("form-data; name=\"file\"; filename=\"%1\"").arg(filename);
  filePart.setHeader(QNetworkRequest::ContentDispositionHeader, dispHeader);
  filePart.setBodyDevice(file);
  file->setParent(multiPart); // delete file with the multiPart
  multiPart->append(filePart);

  QString url = QString("%1/file/%2%3/").arg(NEWT_BASE_URL, machine, remotePath);
  QNetworkRequest request(url);
  QNetworkReply* reply = m_networkManager->post(request, multiPart);
  QObject::connect(reply, &QNetworkReply::sslErrors, this, &qtNewtInterface::onSslErrors);
  multiPart->setParent(reply); // delete multiPart with the reply
  return reply;
}

QNetworkReply* qtNewtInterface::requestHomePath(const QString& machine)
{
  QString command("echo $HOME");
  return this->sendCommand(command, machine);
}

QNetworkReply* qtNewtInterface::requestJobSubmit(
  const QString& remoteFilePath,
  const QString& machine)
{
  QString url = QString("%1/queue/%2").arg(NEWT_BASE_URL, machine);
  QNetworkRequest request(url);
  request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");

  QUrlQuery params;
  params.addQueryItem("jobfile", remoteFilePath.toUtf8());

  QNetworkReply* reply = m_networkManager->post(request, params.query().toUtf8());
  QObject::connect(reply, &QNetworkReply::sslErrors, this, &qtNewtInterface::onSslErrors);

  return reply;
}

QNetworkReply* qtNewtInterface::requestScratchPath(const QString& machine)
{
  QString command("echo $SCRATCH");
  return this->sendCommand(command, machine);
}

QNetworkReply* qtNewtInterface::requestJob(const QString& machine, const QString& slurmId) const
{
#ifndef NDEBUG
  qDebug() << "Requesting job data " << slurmId;
#endif

  // Grab a timestamp to include in a query parameter.
  // The timestamp is not used by the server, but lag times have been observed between the actual
  // job state and what is reported by NEWT. Adding a different timestamp to each request will
  // prevent any HTTP caching that could potentially increase that lag time.
  qint64 secs = QDateTime::currentSecsSinceEpoch();

  QString url =
    QString("%1/queue/%2/%3/sacct?view=read&ts=%4").arg(NEWT_BASE_URL, machine, slurmId).arg(secs);
  QNetworkRequest request(url);
  return m_networkManager->get(request);
}

QNetworkReply* qtNewtInterface::sendCommand(const QString& command, const QString& machine)
{
  QString url = QString("%1/command/%2").arg(NEWT_BASE_URL, machine);
  QNetworkRequest request(url);
  request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");

  QUrlQuery params;
  params.addQueryItem("executable", command.toUtf8());
  params.addQueryItem("loginenv", "true");

  // qDebug() << "sending command" << command;
  QNetworkReply* reply = m_networkManager->post(request, params.query().toUtf8());
  QObject::connect(reply, &QNetworkReply::sslErrors, this, &qtNewtInterface::onSslErrors);

  return reply;
}

void qtNewtInterface::onLoginReply()
{
  auto* reply = qobject_cast<QNetworkReply*>(this->sender());
  QByteArray bytes = reply->readAll();
  if (reply->error())
  {
    QString msg = this->getErrorMessage(reply, bytes);
    qWarning() << "There was an error logging into NERSC: " << msg;

    // For some reason, signal emmitted here is not reliably routed to destination slot
    emit this->error(msg, reply);
  }
  else
  {
    QJsonDocument jsonResponse = QJsonDocument::fromJson(bytes.constData());
    const QJsonObject& object = jsonResponse.object();
    bool auth = object.value("auth").toBool();
    if (auth)
    {
      m_username = object.value("username").toString();
      m_newtSessionId = object.value("newt_sessionid").toString();
      m_isLoggedIn = true;
      int lifetime = object.value("session_lifetime").toInt();
      qInfo() << "Logged into NERSC with NEWT Session id" << m_newtSessionId
              << "with session lifetime" << lifetime << "secs.";
      emit this->loginComplete(m_username, lifetime);
    }
    else
    {
      emit this->loginFail(QString("Login to NERSC failed"));
    }
  }

  reply->deleteLater();
}

void qtNewtInterface::onSslErrors(const QList<QSslError>& /*errors*/)
{
  auto reply = qobject_cast<QNetworkReply*>(this->sender());
  emit this->error(reply->errorString(), reply);
  reply->deleteLater();
}

QString qtNewtInterface::getErrorMessage(QNetworkReply* reply, const QByteArray& bytes) const
{
  QJsonDocument jsonResponse = QJsonDocument::fromJson(bytes.constData());

  QString errorMessage;

  if (!jsonResponse.isObject())
  {
    errorMessage = reply->errorString();
  }
  else
  {
    const QJsonObject& object = jsonResponse.object();
    QString message = object.value("error").toString();
    if (!message.isEmpty())
      errorMessage = QString("NEWT error: %1").arg(message);
    else
      errorMessage = QString(bytes);
  }

  return errorMessage;
}

QNetworkReply* qtNewtInterface::sendGetRequest(const QString& url)
{
  QNetworkRequest request(url);
  QNetworkReply* reply = m_networkManager->get(request);
  QObject::connect(reply, &QNetworkReply::sslErrors, this, &qtNewtInterface::onSslErrors);
  return reply;
}

} // namespace newt
