//=========================================================================
//  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 <QByteArray>
#include <QNetworkReply>
#include <QSharedPointer>
#include <QStringList>
#include <QTimer>
#include <QtGlobal>

#include "nlohmann/json.hpp"

#ifndef NDEBUG
#include <iostream>
#endif

namespace
{
const int DefaultPollingSeconds = 15;

// 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:
  bool m_busy = false; // true during network I/O
  bool m_pollingEnabled = false;
  int m_pollingIndex = -1;
  QStringList m_jobList;
  QSharedPointer<cumulus::CumulusProxy> m_cumulus;
  newt::qtNewtInterface* m_newt = nullptr;

  QTimer* m_timer = nullptr;

  Internal()
    : m_cumulus(cumulus::CumulusProxy::instance())
    , m_newt(newt::qtNewtInterface::instance())
    , m_timer(new QTimer())
  {
    m_timer->setSingleShot(true);
    m_timer->setInterval(1000 * DefaultPollingSeconds);
  }

  ~Internal()
  {
    m_timer->stop();
    delete m_timer;
  }
};

//-----------------------------------------------------------------------------
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_internal->m_timer->callOnTimeout(this, &qtCumulusJobTracker::onTimerEvent);
  QObject::connect(
    m_internal->m_cumulus.get(),
    &cumulus::CumulusProxy::error,
    this,
    &qtCumulusJobTracker::onCumulusError);
}

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

void qtCumulusJobTracker::addJob(const QString& cumulusJobId, bool checkUnique)
{
  if (checkUnique && m_internal->m_jobList.contains(cumulusJobId))
  {
    DebugMessageMacro("Job list already contains " << cumulusJobId.toStdString());
    return;
  }

  m_internal->m_jobList.push_back(cumulusJobId);
  this->setNextPoll();
}

void qtCumulusJobTracker::addNewJob(const QString& cumulusJobId)
{
  m_internal->m_jobList.push_back(cumulusJobId);
  this->setNextPoll(true);
}

void qtCumulusJobTracker::clear()
{
  bool wasPolling = this->isPolling();

  m_internal->m_timer->stop();
  m_internal->m_busy = false;
  m_internal->m_pollingIndex = -1;
  m_internal->m_jobList.clear();

  if (wasPolling)
  {
    emit this->pollingStateChanged(false);
  }
}

bool qtCumulusJobTracker::enablePolling(bool enable)
{
  bool wasPolling = this->isPolling();
  m_internal->m_pollingEnabled = enable;
  if (enable)
  {
    bool started = this->setNextPoll(true);
    return started;
  }
  else
  {
    m_internal->m_timer->stop();
    if (wasPolling)
    {
      emit this->pollingStateChanged(false);
    }
    DebugMessageMacro("Polling timer stopped.");
  }

  return false;
}

bool qtCumulusJobTracker::pollOnce()
{
  if (m_internal->m_pollingEnabled)
  {
    qInfo() << "Cannot use pollOnce() when continuous polling enabled.";
    return false;
  }

  if (m_internal->m_busy)
  {
    qInfo() << "Already polling cumulus.";
    return false;
  }

  if (m_internal->m_jobList.isEmpty())
  {
    qInfo() << "No jobs in polling list.";
    return false;
  }

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

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

  // Start with last job in the list and work to the front,
  // so that we can remove jobs form the list without changing the other indices.
  int i = static_cast<int>(m_internal->m_jobList.size() - 1);
  this->requestJob(i);
  return true;
}

bool qtCumulusJobTracker::isPolling() const
{
  if (!m_internal->m_pollingEnabled)
  {
    return false;
  }
  return m_internal->m_busy || m_internal->m_timer->isActive();
}

void qtCumulusJobTracker::setPollingIntervalSeconds(int intervalSec)
{
  m_internal->m_timer->setInterval(1000 * intervalSec);
}

int qtCumulusJobTracker::pollingIntervalSeconds() const
{
  return 1000 * m_internal->m_timer->interval();
}

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

  if (networkReply)
  {
    networkReply->deleteLater();
  }
  qInfo() << msg;
  emit this->error(msg);
  m_internal->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_internal->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_internal->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_internal->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);
    }

    // Check if job status is a final state, remove from the list.
    if ((status == "complete") || (status == "error"))
    {
      m_internal->m_jobList.removeAt(m_internal->m_pollingIndex);
    }

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

  networkReply->deleteLater();
}

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

void qtCumulusJobTracker::onTimerEvent()
{
  DebugMessageMacro("onTimerEvent");
  int i = static_cast<int>(m_internal->m_jobList.size() - 1);
  // Check that index is still valid (i.e., that list hasn't been cleared)
  if (i < 0)
  {
    emit this->pollingStateChanged(false);
    return;
  }
  this->requestJob(i);
}

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

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

bool qtCumulusJobTracker::setNextPoll(bool pollNow)
{
  bool wasPolling = this->isPolling();
  if (!m_internal->m_pollingEnabled)
  {
    return false;
  }

  if (!m_internal->m_newt->isLoggedIn())
  {
    return false;
  }

  if (m_internal->m_jobList.isEmpty())
  {
    DebugMessageMacro("Polling list empty.");
    m_internal->m_busy = false;
    if (wasPolling)
    {
      emit this->pollingStateChanged(false);
    }
    return false;
  }

  if (pollNow)
  {
    // Use 1000 msec to give time for queue id to be assigned
    QTimer::singleShot(1000, this, &qtCumulusJobTracker::onTimerEvent);
  }
  else
  {
    m_internal->m_timer->start();
  }

  DebugMessageMacro("Polling timer started.");
  if (!wasPolling)
  {
    emit this->pollingStateChanged(true);
  }
  return true;
}

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