/* Copyright (C) 2019 MariaDB Corporation

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; version 2 of
   the License.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
   MA 02110-1301, USA. */

#include "Ownership.h"
#include "Config.h"
#include "Cache.h"
#include "Synchronizer.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <boost/filesystem.hpp>

using namespace std;
namespace bf = boost::filesystem;

namespace storagemanager
{
Ownership::Ownership()
{
  Config* config = Config::get();
  logger = SMLogging::get();

  string sPrefixDepth = config->getValue("ObjectStorage", "common_prefix_depth");
  if (sPrefixDepth.empty())
  {
    const char* msg =
        "Ownership: Need to specify ObjectStorage/common_prefix_depth in the storagemanager.cnf file";
    logger->log(LOG_CRIT, msg);
    throw runtime_error(msg);
  }
  try
  {
    prefixDepth = stoul(sPrefixDepth, NULL, 0);
  }
  catch (invalid_argument& e)
  {
    const char* msg = "Ownership: Invalid value in ObjectStorage/common_prefix_depth";
    logger->log(LOG_CRIT, msg);
    throw runtime_error(msg);
  }

  metadataPrefix = config->getValue("ObjectStorage", "metadata_path");
  if (metadataPrefix.empty())
  {
    const char* msg = "Ownership: Need to specify ObjectStorage/metadata_path in the storagemanager.cnf file";
    logger->log(LOG_CRIT, msg);
    throw runtime_error(msg);
  }
  monitor = new Monitor(this);
}

Ownership::~Ownership()
{
  delete monitor;
  for (auto& it : ownedPrefixes)
    releaseOwnership(it.first, true);
}

bf::path Ownership::get(const bf::path& p, bool getOwnership)
{
  bf::path ret, prefix, normalizedPath(p);
  bf::path::const_iterator pit;
  int i, levels;

  normalizedPath.lexically_normal();
  // cerr << "Ownership::get() param = " << normalizedPath.string() << endl;
  if (prefixDepth > 0)
  {
    for (i = 0, pit = normalizedPath.begin(); i <= prefixDepth && pit != normalizedPath.end(); ++i, ++pit)
      ;
    if (pit != normalizedPath.end())
      prefix = *pit;
    // cerr << "prefix is " << prefix.string() << endl;
    for (levels = 0; pit != normalizedPath.end(); ++levels, ++pit)
      ret /= *pit;
    if (!getOwnership)
      return ret;
    if (levels <= 1)
      throw runtime_error("Ownership: given path " + normalizedPath.string() +
                          " does not have minimum number of directories");
  }
  else
  {
    ret = normalizedPath;
    prefix = *(normalizedPath.begin());
  }

  if (!getOwnership)
    return ret;

  mutex.lock();
  if (ownedPrefixes.find(prefix) == ownedPrefixes.end())
  {
    mutex.unlock();
    takeOwnership(prefix);
  }
  else
  {
    // todo...  replace this polling, and the similar polling in Cache, with proper condition vars.
    while (ownedPrefixes[prefix] == false)
    {
      mutex.unlock();
      sleep(1);
      mutex.lock();
    }
    mutex.unlock();
  }
  // cerr << "returning " << ret.string() << endl;
  return ret;
}

// minor timesaver
#define TOUCH(p, f)                                                                                       \
  {                                                                                                       \
    int fd = ::open((metadataPrefix / p / f).string().c_str(), O_TRUNC | O_CREAT | O_WRONLY, 0660);       \
    if (fd >= 0)                                                                                          \
      ::close(fd);                                                                                        \
    else                                                                                                  \
    {                                                                                                     \
      char buf[80];                                                                                       \
      int saved_errno = errno;                                                                            \
      cerr << "failed to touch " << metadataPrefix / p / f << " got " << strerror_r(saved_errno, buf, 80) \
           << endl;                                                                                       \
    }                                                                                                     \
  }

#define DELETE(p, f) ::unlink((metadataPrefix / p / f).string().c_str());

void Ownership::touchFlushing(const bf::path& prefix, std::atomic<bool>* doneFlushing) const
{
  while (!doneFlushing->load())
  {
    TOUCH(prefix, "FLUSHING");
    try
    {
      boost::this_thread::sleep_for(boost::chrono::seconds(1));
    }
    catch (boost::thread_interrupted&)
    {
    }
  }
}

void Ownership::releaseOwnership(const bf::path& p, bool isDtor)
{
  logger->log(LOG_DEBUG, "Ownership: releasing ownership of %s", p.string().c_str());
  boost::unique_lock<boost::mutex> s(mutex);

  auto it = ownedPrefixes.find(p);
  if (it == ownedPrefixes.end())
  {
    logger->log(LOG_DEBUG, "Ownership::releaseOwnership(): told to disown %s, but do not own it",
                p.string().c_str());
    return;
  }

  if (isDtor)
  {
    // This is a quick release.  If this is being destroyed, then it is through the graceful
    // shutdown mechanism, which will flush data separately.
    DELETE(p, "OWNED");
    DELETE(p, "FLUSHING");
    return;
  }
  else
    ownedPrefixes.erase(it);

  s.unlock();

  std::atomic<bool> done{false};

  // start flushing
  boost::thread xfer([this, &p, &done] { this->touchFlushing(p, &done); });
  Synchronizer::get()->dropPrefix(p);
  Cache::get()->dropPrefix(p);
  done.store(true);
  xfer.interrupt();
  xfer.join();

  // update state
  DELETE(p, "OWNED");
  DELETE(p, "FLUSHING");
}

void Ownership::_takeOwnership(const bf::path& p)
{
  logger->log(LOG_DEBUG, "Ownership: taking ownership of %s", p.string().c_str());
  DELETE(p, "FLUSHING");
  DELETE(p, "REQUEST_TRANSFER");
  // TODO: need to consider errors taking ownership
  TOUCH(p, "OWNED");
  mutex.lock();
  ownedPrefixes[p] = true;
  mutex.unlock();
  Synchronizer::get()->newPrefix(p);
  Cache::get()->newPrefix(p);
}

void Ownership::takeOwnership(const bf::path& p)
{
  // If the prefix doesn't exist, ownership isn't possible yet.
  if (!bf::is_directory(metadataPrefix / p))
    return;

  boost::unique_lock<boost::mutex> s(mutex);

  auto it = ownedPrefixes.find(p);
  if (it != ownedPrefixes.end())
    return;
  ownedPrefixes[p] = false;
  s.unlock();

  bool okToTransfer = false;
  struct stat statbuf;
  int err;
  char buf[80];
  bf::path ownedPath = metadataPrefix / p / "OWNED";
  bf::path flushingPath = metadataPrefix / p / "FLUSHING";

  // if it's not already owned, then we can take possession
  err = ::stat(ownedPath.string().c_str(), &statbuf);
  if (err && errno == ENOENT)
  {
    _takeOwnership(p);
    return;
  }

  TOUCH(p, "REQUEST_TRANSFER");
  time_t lastFlushTime = time(NULL);
  while (!okToTransfer && time(NULL) < lastFlushTime + 10)
  {
    // if the OWNED file is deleted or if the flushing file isn't touched after 10 secs
    // it is ok to take possession.
    err = ::stat(ownedPath.string().c_str(), &statbuf);
    if (err)
    {
      if (errno == ENOENT)
        okToTransfer = true;
      else
        logger->log(LOG_CRIT, "Ownership::takeOwnership(): got '%s' doing stat of %s",
                    strerror_r(errno, buf, 80), ownedPath.string().c_str());
    }
    err = ::stat(flushingPath.string().c_str(), &statbuf);
    if (err && errno != ENOENT)
      logger->log(LOG_CRIT, "Ownership::takeOwnership(): got '%s' doing stat of %s",
                  strerror_r(errno, buf, 80), flushingPath.string().c_str());
    else
    {
      logger->log(LOG_DEBUG, "Ownership: waiting to get %s", p.string().c_str());
      if (!err)
        lastFlushTime = statbuf.st_mtime;
    }
    if (!okToTransfer)
      sleep(1);
  }
  _takeOwnership(p);
}

Ownership::Monitor::Monitor(Ownership* _owner) : owner(_owner), stop{false}
{
  thread = boost::thread([this] { this->watchForInterlopers(); });
}

Ownership::Monitor::~Monitor()
{
  stop.store(true);
  thread.interrupt();
  thread.join();
}

void Ownership::Monitor::watchForInterlopers()
{
  // look for requests to transfer ownership
  struct stat statbuf;
  int err;
  char buf[80];
  vector<bf::path> releaseList;

  while (!stop.load())
  {
    releaseList.clear();
    boost::unique_lock<boost::mutex> s(owner->mutex);

    for (auto& prefix : owner->ownedPrefixes)
    {
      if (stop.load())
        break;
      if (prefix.second == false)
        continue;
      bf::path p(owner->metadataPrefix / (prefix.first) / "REQUEST_TRANSFER");
      const char* cp = p.string().c_str();

      err = ::stat(cp, &statbuf);
      // release it if there's a release request only.  Log it if there's an error other than
      // that the file isn't there.
      if (err == 0)
        releaseList.push_back(prefix.first);
      if (err < 0 && errno != ENOENT)
        owner->logger->log(LOG_ERR, "Runner::watchForInterlopers(): failed to stat %s, got %s", cp,
                           strerror_r(errno, buf, 80));
    }
    s.unlock();

    for (auto& prefix : releaseList)
      owner->releaseOwnership(prefix);
    if (stop.load())
      break;
    try
    {
      boost::this_thread::sleep_for(boost::chrono::seconds(1));
    }
    catch (boost::thread_interrupted&)
    {
    }
  }
}

}  // namespace storagemanager
