//=========================================================================
//  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/simulation/ace3p/qt/qtCumulusJobTracker.h"

#include "smtk/cumulus/jobspanel/cumulusproxy.h"
#include "smtk/cumulus/jobspanel/job.h"
#include "smtk/newt/qtNewtInterface.h"
#include "smtk/simulation/ace3p/JobsManifest.h"

#include <QByteArray>
#include <QNetworkReply>
#include <QSharedPointer>
#include <QStringList>
#include <QTimer>
#include <QtGlobal>

#include "nlohmann/json.hpp"

#ifndef NDEBUG
#include <iostream>
#endif

namespace
{
// Macro for printing messages to stdout for debug builds only
#ifndef NDEBUG
#define DebugMessageMacro(msg)                                                                     \
  do                                                                                               \
  {                                                                                                \
    std::cout << __FILE__ << ":" << __LINE__ << " " << msg << std::endl;                           \
  } while (0)
#else
#define DebugMessageMacro(msg)
#endif
} // namespace

namespace smtk
{
namespace simulation
{
namespace ace3p
{

//-----------------------------------------------------------------------------
class qtCumulusJobTracker::Internal
{
public:
  QSharedPointer<cumulus::CumulusProxy> m_cumulus;
  newt::qtNewtInterface* m_newt = nullptr;

  Internal()
    : m_cumulus(cumulus::CumulusProxy::instance())
    , m_newt(newt::qtNewtInterface::instance())
  {
  }

  ~Internal() = default;
};

//-----------------------------------------------------------------------------
qtCumulusJobTracker::qtCumulusJobTracker(QObject* parent)
  : Superclass(parent)
{
  m_internal = new qtCumulusJobTracker::Internal;

  if (m_internal->m_newt->isLoggedIn())
  {
    this->onLogin();
  }
  else
  {
    QObject::connect(
      m_internal->m_newt,
      &newt::qtNewtInterface::loginComplete,
      this,
      &qtCumulusJobTracker::onLogin);
  }

  m_internalTimer->m_timer->callOnTimeout(this, &qtCumulusJobTracker::onTimerEvent);
  QObject::connect(
    m_internal->m_cumulus.get(),
    &cumulus::CumulusProxy::error,
    this,
    &qtCumulusJobTracker::onCumulusError);
}

qtCumulusJobTracker::~qtCumulusJobTracker()
{
  delete m_internal;
}

bool qtCumulusJobTracker::pollOnce()
{
  if (!m_internal->m_newt->isLoggedIn())
  {
    qWarning() << "Cannot poll jobs because not signed into NERSC.";
    return false;
  }

  // Future: confirm sign-in to Girder/Cumulus

  return qtAbstractJobTracker::pollOnce();
}

void qtCumulusJobTracker::onCumulusError(const QString& msg, QNetworkReply* networkReply)
{
  bool wasPolling = this->isPolling();

  if (networkReply)
  {
    networkReply->deleteLater();
  }
  qInfo() << msg;
  emit this->error(msg);
  m_internalTimer->m_busy = false;

  // For now, stop polling
  if (wasPolling)
  {
    emit this->pollingStateChanged(false);
  }
}

void qtCumulusJobTracker::onCumulusReply()
{
  // Check that instance hasn't been cleared
  if (!m_internalTimer->m_busy)
  {
    return;
  }

  auto networkReply = qobject_cast<QNetworkReply*>(this->sender());
  assert(networkReply != nullptr);
  QByteArray bytes = networkReply->readAll();
  nlohmann::json jResponse;
  try
  {
    jResponse = nlohmann::json::parse(bytes.constData());
  }
  catch (std::exception const& ex)
  {
    QString errMessage;
    QTextStream qs(&errMessage);
    qs << "Error parsing cumulus reply: " << bytes.constData();
    qWarning() << __FILE__ << __LINE__ << errMessage;
    emit error(errMessage);
    networkReply->deleteLater();
    return;
  }

  // DebugMessageMacro(jResponse.dump());
  if (networkReply->error() && !jResponse.is_object())
  {
    qWarning() << __FILE__ << __LINE__ << ":" << networkReply->errorString();
    emit error(networkReply->errorString());
    m_internalTimer->m_busy = false;
  }

  else if (networkReply->error() && jResponse.is_object())
  {
    QString errorMessage;

    const auto iter = jResponse.find("message");
    if (iter == jResponse.end())
    {
      errorMessage = QString(bytes);
    }
    else if (jResponse["message"].is_null())
    {
      errorMessage = QString(bytes);
    }
    else
    {
      std::string message = jResponse["message"].get<std::string>();
      if (!message.empty())
      {
        errorMessage = QString("Girder error: %1").arg(message.c_str());
      }
      else
      {
        errorMessage = QString(bytes);
      }
    }

    qWarning() << __FILE__ << __LINE__ << ":" << errorMessage;
    emit error(errorMessage);
    m_internalTimer->m_busy = false;
  }

  else
  {
    // Get fields from jResponse
    QString cumulusJobId;
    QString status;
    QString queueJobId;
    qint64 startTimeStamp = 0L; // seconds since epoch
    nlohmann::json::iterator iter;

    iter = jResponse.find("status");
    if (iter != jResponse.end())
    {
      status = (*iter).get<std::string>().c_str();
    }

    iter = jResponse.find("queueJobId");
    if (iter != jResponse.end())
    {
      queueJobId = (*iter).get<std::string>().c_str();
    }

    // Get timestamp from metadata field
    iter = jResponse.find("metadata");
    if (iter != jResponse.end())
    {
      auto jMetadata = *iter;
      auto metaIter = jMetadata.find("startTimeStamp");
      if (metaIter != jMetadata.end() && metaIter->is_number())
      {
        double ts = metaIter->get<double>();
        startTimeStamp = static_cast<qint64>(ts);
      }
    }

    iter = jResponse.find("_id");
    if (iter == jResponse.end())
    {
      QString errMessage;
      QTextStream qs(&errMessage);
      qs << "Cumulus response missing _id: " << bytes.constData();
      qWarning() << errMessage;
    }
    else
    {
      cumulusJobId = (*iter).get<std::string>().c_str();
      emit this->jobStatus(cumulusJobId, status, queueJobId, startTimeStamp);
    }

    // If job status indicates it is done, remove from the list.
    if (JobsManifest::isJobFinished(status.toStdString()))
    {
      this->m_jobList.removeAt(m_internalTimer->m_pollingIndex);
    }

    // Check for more jobs
    m_internalTimer->m_pollingIndex -= 1;
    if (m_internalTimer->m_pollingIndex >= 0)
    {
      // Schedule next request
      this->requestJob(m_internalTimer->m_pollingIndex);
    }
    else
    {
      // Schedule next polling interval
      this->setNextPoll();
    }
  }

  networkReply->deleteLater();
}

void qtCumulusJobTracker::onLogin()
{
  this->setNextPoll(true);
}

void qtCumulusJobTracker::requestJob(int index)
{
  m_internalTimer->m_busy = true;

  QString cumulusJobId = this->m_jobList[index];
  m_internalTimer->m_pollingIndex = index;
  QNetworkReply* networkReply = m_internal->m_cumulus->requestJob(cumulusJobId);
  QObject::connect(
    networkReply, &QNetworkReply::finished, this, &qtCumulusJobTracker::onCumulusReply);
}

bool qtCumulusJobTracker::setNextPoll(bool pollNow)
{
  if (!m_internal->m_newt->isLoggedIn())
  {
    return false;
  }

  return qtAbstractJobTracker::setNextPoll(pollNow);
}

} // namespace ace3p
} // namespace simulation
} // namespace smtk
