/**
  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */

#include "ts/apidefs.h"
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <list>
#include <map>
#include <mutex>
#include <string>
#include <utility>
#include <vector>

#include <cassert>
#include <cstring>

#include <tscpp/api/GlobalPlugin.h>
#include <tscpp/api/PluginInit.h>
#include <tscpp/api/TransformationPlugin.h>

#include <ts/ts.h>

#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/pem.h>

#if __has_include(<MagickWand/MagickWand.h>)
#if defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wtype-limits"
#endif
#include <MagickWand/MagickWand.h>
#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
#else
#include <wand/MagickWand.h>
#endif

#define PLUGIN_TAG "ats-magick"

using std::string;
using namespace atscppapi;

using CharVector        = std::vector<char>;
using CharPointerVector = std::vector<char *>;
using StringViewVector  = std::vector<std::string_view>;

using byte = unsigned char;

namespace
{
GlobalPlugin *plugin;

DbgCtl dbg_ctl{PLUGIN_TAG};
} // namespace

struct ThreadPool {
  using Callback = std::function<void(void)>;
  using Queue    = std::list<Callback>;
  using Lock     = std::unique_lock<std::mutex>;

  const size_t    size_;
  bool            running_;
  TSThread *const pool_;

  // a double linked list is used due to the high amount of insertions and deletions
  Queue queue_;

  // This mutex protects operations into the queue
  std::mutex mutex_;

  std::condition_variable semaphore_;

  ~ThreadPool()
  {
    assert(running_);
    running_ = false;

    Lock lock(mutex_);

    queue_.clear();

    semaphore_.notify_all();

    assert(nullptr != pool_);
    for (size_t i = 0; i < size_; ++i) {
      TSThread thread = pool_[i];
      assert(nullptr != thread);
      Dbg(dbg_ctl, "Destroying thread number %lu (%p)", i, thread);
      TSThreadWait(thread);
      TSThreadDestroy(thread);
    }

    delete[] pool_;
    const_cast<TSThread *&>(pool_) = nullptr;
  }

  ThreadPool(const size_t s) : size_(s), running_(true), pool_(new TSThread[s])
  {
    assert(0 < size_);
    assert(nullptr != pool_);
    for (size_t i = 0; i < size_; ++i) {
      pool_[i] = TSThreadCreate(
        [](void *d) -> void * {
          assert(nullptr != d);
          ThreadPool *const self = reinterpret_cast<ThreadPool *>(d);
          assert(self->running_);
          while (self->running_) {
            Callback callback;
            {
              Lock lock(self->mutex_);

              self->semaphore_.wait(lock, [self] { return !self->queue_.empty() || !self->running_; });

              if (!self->queue_.empty()) {
                callback = self->queue_.front();
                self->queue_.pop_front();
              }
            }

            // if a callback was assigned, call it outside of the synchronized scope.
            if (callback) {
              callback();
            }
          }
          return nullptr;
        },
        this);
      assert(nullptr != pool_[i]);
      Dbg(dbg_ctl, "Creating thread number %lu (%p)", i, pool_[i]);
    }
  }

  ThreadPool(ThreadPool &) = delete;

  void
  emplace_back(Callback &&c)
  {
    {
      Lock lock(mutex_);
      /**
       * we check for running_ in case the destructor was simultaneously called
       *from a different thread while waiting.
       */
      if (!running_) {
        return;
      }
      queue_.emplace_back(c);
    }
    semaphore_.notify_one();
  }
};

namespace magick
{
struct EVPContext {
  EVP_MD_CTX *const context;

  ~EVPContext()
  {
    assert(nullptr != context);
    EVP_MD_CTX_free(context);
  }

  EVPContext() : context(EVP_MD_CTX_new()) { assert(nullptr != context); }
};

struct EVPKey {
  EVP_PKEY *const key;

  ~EVPKey()
  {
    assert(nullptr != key);
    EVP_PKEY_free(key);
  }

  EVPKey() : key(EVP_PKEY_new()) { assert(nullptr != key); }

  bool
  assign(RSA *k) const
  {
    assert(nullptr != k);
    const int rc = EVP_PKEY_assign_RSA(key, k);
    assert(1 == rc);
    return 1 == rc;
  }
};

/** Remove the last error from this thread's error queue and print it.
 */
static void
ssl_error()
{
  if (unsigned long error_code{ERR_get_error()}; 0 != error_code) {
    if (char const *reason{ERR_reason_error_string(error_code)}; NULL == reason) {
      Dbg(dbg_ctl, "SSL error: error code %lu", error_code);
    } else {
      Dbg(dbg_ctl, "SSL error: %s", reason);
    }
  }
}

bool
verify(const byte *const msg, const size_t mlen, const byte *const sig, const size_t slen, EVP_PKEY *const pkey)
{
  assert(nullptr != msg);
  assert(0 < mlen);
  assert(nullptr != sig);
  assert(0 < slen);
  assert(nullptr != pkey);

  if (nullptr == msg || 0 == mlen || nullptr == sig || 0 == slen || nullptr == pkey) { // lgtm[cpp/constant-comparison]
    return false;
  }

  EVPContext evp;

  if (const int rc = EVP_DigestVerifyInit(evp.context, nullptr, EVP_sha256(), nullptr, pkey); 1 != rc) {
    ssl_error();
    return false;
  }

  if (const int rc = EVP_DigestVerify(evp.context, sig, slen, msg, mlen); 1 == rc) {
    return true;
  } else {
    // The OpenSSL 1.1.1 API distinguishes between a verification failure and a
    // more serious error with a specific return value for the former, but we
    // have collapsed them into one case because we don't do any special
    // handling for serious errors.
    ssl_error();
    return false;
  }
}

struct Exception {
  ExceptionInfo *info;

  ~Exception()
  {
    assert(nullptr != info);
    info = DestroyExceptionInfo(info);
  }

  Exception() : info(AcquireExceptionInfo()) { assert(nullptr != info); }
};

struct Image {
  ImageInfo *info;

  ~Image()
  {
    assert(nullptr != info);
    info = DestroyImageInfo(info);
  }

  Image() : info(AcquireImageInfo()) { assert(nullptr != info); }
};

struct Wand {
  MagickWand *wand;
  void       *blob = nullptr;

  ~Wand()
  {
    assert(nullptr != wand);
    wand = DestroyMagickWand(wand);
    if (nullptr == blob) {
      blob = MagickRelinquishMemory(blob);
    }
  }

  Wand() : wand(NewMagickWand()) { assert(nullptr != wand); }

  void
  clear() const
  {
    assert(nullptr != wand);
    ClearMagickWand(wand);
  }

  std::string_view
  get()
  {
    assert(nullptr != wand);
    std::size_t length = 0;
    if (nullptr != blob) {
      blob = MagickRelinquishMemory(blob);
    }
    MagickResetIterator(wand);
    blob = MagickGetImagesBlob(wand, &length);
    return std::string_view(reinterpret_cast<char *>(blob), length);
  }

  bool
  read(const char *const s) const
  {
    assert(nullptr != s);
    assert(nullptr != wand);
    return MagickReadImage(wand, s) == MagickTrue;
  }

  bool
  readBlob(const std::vector<char> &v) const
  {
    assert(!v.empty());
    assert(nullptr != wand);
    return MagickReadImageBlob(wand, v.data(), v.size()) == MagickTrue;
  }

  bool
  setFormat(const char *const s) const
  {
    assert(nullptr != s);
    assert(nullptr != wand);
    return MagickSetImageFormat(wand, s) == MagickTrue;
  }

  bool
  write(const char *const s) const
  {
    assert(nullptr != s);
    assert(nullptr != wand);
    return MagickWriteImage(wand, s) == MagickTrue;
  }
};

struct Core {
  ~Core() { MagickCoreTerminus(); }

  Core() { MagickCoreGenesis("/tmp", MagickFalse); }
};

} // namespace magick

struct QueryMap {
  using Vector = StringViewVector;
  using Map    = std::map<std::string_view, Vector>;

  const static Vector emptyValues;

  std::string content_;
  Map         map_;

  QueryMap(std::string &&s) : content_(std::move(s)) { parse(); }

  template <typename T>
  const Vector &
  operator[](T &&k) const
  {
    const auto iterator = map_.find(k);
    if (iterator != map_.end()) {
      return iterator->second;
    }
    return emptyValues;
  }

  void
  parse()
  {
    std::string_view key;
    std::size_t      i = 0, j = 0;
    for (; i < content_.size(); ++i) {
      const char c = content_[i];
      switch (c) {
      case '&':
        if (!key.empty()) {
          map_[key].emplace_back(std::string_view(&content_[j], i - j));
          key = std::string_view();
        }
        j = i + 1;
        break;
      case '=':
        key = std::string_view(&content_[j], i - j);
        j   = i + 1;
        break;
      default:
        break;
      }
    }

    assert(j <= i);

    if (key.empty()) {
      if (j < i) {
        map_[std::string_view(&content_[j], i - j)];
      }

    } else {
      map_[key].emplace_back(std::string_view(&content_[j], i - j));
    }
  }
};

const QueryMap::Vector QueryMap::emptyValues;

bool
QueryParameterToCharVector(CharVector &v)
{
  {
    std::size_t        s  = 0;
    const TSReturnCode rc = TSStringPercentDecode(v.data(), v.size(), v.data(), v.size(), &s);
    assert(TS_SUCCESS == rc);
    if (rc != TS_SUCCESS) {
      return false;
    }
    v.resize(s);
  }

  {
    std::size_t        s  = 0;
    const TSReturnCode rc = TSBase64Decode(v.data(), v.size(), reinterpret_cast<unsigned char *>(v.data()), v.size(), &s);
    assert(TS_SUCCESS == rc);
    if (rc != TS_SUCCESS) {
      return false;
    }
    v.resize(s);
  }

  return true;
}

CharPointerVector
QueryParameterToArguments(CharVector &v)
{
  CharPointerVector result;
  result.reserve(32);

  std::size_t i = 0, j = 0;
  bool        quote = false;

  for (; i < v.size(); ++i) {
    char &c = v[i];
    assert('\0' != c);
    if ('"' == c) {
      if (i > j) {
        result.push_back(&v[j]);
      }
      c     = '\0';
      j     = i + 1;
      quote = !quote;
    } else if (!quote && ' ' == c) {
      if (i > j) {
        result.push_back(&v[j]);
      }
      c = '\0';
      j = i + 1;
    }
  }
  if (i > j) {
    result.push_back(&v[j]);
  }
  return result;
}

struct ImageTransform : TransformationPlugin {
  ~ImageTransform() override = default;

  ImageTransform(Transaction &t, CharVector &&a, CharPointerVector &&m, ThreadPool &p)
    : TransformationPlugin(t, TransformationPlugin::RESPONSE_TRANSFORMATION),
      arguments_(std::move(a)),
      argumentMap_(std::move(m)),
      threadPool_(p)
  {
    Dbg(dbg_ctl, "ImageTransform");
  }

  void
  consume(const std::string_view s) override
  {
    Dbg(dbg_ctl, "consume");
    blob_.insert(blob_.end(), s.begin(), s.end());
  }

  void
  handleInputComplete() override
  {
    Dbg(dbg_ctl, "handleInputComplete");

    threadPool_.emplace_back([this]() {
      magick::Image     image;
      magick::Exception exception;
      magick::Wand      wand;

      assert(!this->blob_.empty());

      wand.readBlob(this->blob_);
      wand.write("mpr:b");

      MagickCommandGenesis(image.info, ConvertImageCommand, this->argumentMap_.size(), this->argumentMap_.data(), nullptr,
                           exception.info);

      wand.clear();
      wand.read("mpr:a");

      const std::string_view output = wand.get();
      this->produce(output);

      Dbg(dbg_ctl, "Background transformation is done, resuming continuation (%p)", this);

      this->setOutputComplete();
    });

    Dbg(dbg_ctl, "Scheduling background transformation (%p)", this);
  }

  CharVector        arguments_;
  CharPointerVector argumentMap_;
  CharVector        blob_;
  ThreadPool       &threadPool_;
};

struct GlobalHookPlugin : GlobalPlugin {
  magick::Core    core_;
  magick::EVPKey *key_ = nullptr;
  ThreadPool      threadPool_;

  ~GlobalHookPlugin() override
  {
    if (nullptr != key_) {
      delete key_;
      key_ = nullptr;
    }
  }

  GlobalHookPlugin(const char *const f = nullptr) : threadPool_(2)
  {
    if (nullptr != f) {
      assert(0 < strlen(f));
      Dbg(dbg_ctl, "public key file: %s", f);
      key_             = new magick::EVPKey();
      FILE *const file = fopen(f, "r");
      assert(nullptr != file);
      RSA *rsa = nullptr;
      PEM_read_RSA_PUBKEY(file, &rsa, nullptr, nullptr);
      assert(nullptr != rsa);
      fclose(file);
      key_->assign(rsa);
    }

    registerHook(HOOK_SEND_REQUEST_HEADERS);
    registerHook(HOOK_READ_RESPONSE_HEADERS);
  }

  void
  handleSendRequestHeaders(Transaction &t) override
  {
    Headers &headers = t.getServerRequest().getHeaders();
    // preventing origin from sending the content in a different non expected encoding.
    headers.erase("Accept-Encoding");
    headers.erase("accept-encoding");
    t.resume();
  }

  void
  handleReadResponseHeaders(Transaction &t) override
  {
    Headers &headers = t.getServerResponse().getHeaders();

    string contentType = headers.values("Content-Type");

    if (contentType.empty()) {
      contentType = headers.values("content-type");
    }

    std::transform(contentType.cbegin(), contentType.cend(), contentType.begin(), ::tolower);

    const bool compatibleContentType = "image/bmp" == contentType || "image/gif" == contentType || "image/jpeg" == contentType ||
                                       "image/jpg" == contentType || "image/png" == contentType || "image/tiff" == contentType ||
                                       "image/webp" == contentType || "image/svg+xml" == contentType ||
                                       "application/pdf" == contentType || "application/postscript" == contentType;

    if (compatibleContentType) {
      Dbg(dbg_ctl, "Content-Type is compatible: %s", contentType.c_str());
      const QueryMap queryMap(t.getServerRequest().getUrl().getQuery());
      const auto    &magickQueryParameter = queryMap["magick"];
      if (!magickQueryParameter.empty()) {
        const auto &view = magickQueryParameter.front();
        CharVector  magick(view.data(), view.data() + view.size());

        bool verified = nullptr == key_;

        if (!verified) {
          const auto &magickSigQueryParameter = queryMap["magickSig"];
          if (!magickSigQueryParameter.empty()) {
            const auto &view2 = magickSigQueryParameter.front();
            CharVector  magickSig(view2.data(), view2.data() + view2.size());
            magickSig.insert(magickSig.end(), '\0');
            Dbg(dbg_ctl, "Magick Signature: %s", magickSig.data());
            QueryParameterToCharVector(magickSig);
            verified = magick::verify(reinterpret_cast<const byte *>(magick.data()), magick.size(),
                                      reinterpret_cast<const byte *>(magickSig.data()), magickSig.size(), key_->key);
          }
        }

        if (verified) {
          magick.insert(magick.end(), '\0');
          QueryParameterToCharVector(magick);
          Dbg(dbg_ctl, "ImageMagick's syntax: %s", magick.data());
          CharPointerVector argumentMap = QueryParameterToArguments(magick);
          t.addPlugin(new ImageTransform(t, std::move(magick), std::move(argumentMap), threadPool_));
        } else {
          Dbg(dbg_ctl, "signature verification failed.");
          TSError("[" PLUGIN_TAG "] signature verification failed.");
          t.setStatusCode(HTTP_STATUS_FORBIDDEN);
          t.error();
        }
      }
    }

    t.resume();
  }
};

void
TSPluginInit(int argc, const char **argv)
{
  if (!RegisterGlobalPlugin("magick", "netlify", "daniel.morilha@netlify.com")) {
    return;
  }

  const char *key = nullptr;

  if (1 < argc) {
    // first argument is the path to the public key used to verify query parameter magick's content.
    key = argv[1];
  }

  plugin = new GlobalHookPlugin(key);
}
