/**
  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.
 */

/**
 * @file utils_internal.cc
 */
#include "utils_internal.h"
#include <cassert>
#include "ts/ts.h"
#include <pthread.h>
#include <cstdlib>
#include <cassert>
#include <cstddef>
#include <mutex>
#include "tscpp/api/Plugin.h"
#include "tscpp/api/GlobalPlugin.h"
#include "tscpp/api/Transaction.h"
#include "tscpp/api/TransactionPlugin.h"
#include "tscpp/api/TransformationPlugin.h"
#include "tscpp/api/utils.h"
#include "logging_internal.h"

using namespace atscppapi;

namespace
{
/// The index used to store required transaction based data.
int TRANSACTION_STORAGE_INDEX = -1;

void
resetTransactionHandles(Transaction &transaction, TSEvent event)
{
  utils::internal::resetTransactionHandles(transaction);
  return;
}

void
cleanupTransaction(Transaction &transaction, TSHttpTxn ats_txn_handle)
{
  delete &transaction;
  // reset the txn arg to prevent use-after-free
  TSUserArgSet(ats_txn_handle, TRANSACTION_STORAGE_INDEX, nullptr);
}

void
cleanupTransactionPlugin(Plugin *plugin, TSHttpTxn ats_txn_handle)
{
  TransactionPlugin *transaction_plugin = static_cast<TransactionPlugin *>(plugin);
  std::shared_ptr<Mutex> trans_mutex    = utils::internal::getTransactionPluginMutex(*transaction_plugin, ats_txn_handle);
  if (trans_mutex == nullptr) {
    LOG_ERROR("TransactionPlugin use-after-free! plugin %p, txn %p", plugin, ats_txn_handle);
    return;
  }
  LOG_DEBUG("Locking TransactionPlugin mutex to delete transaction plugin at %p", transaction_plugin);
  trans_mutex->lock();
  delete transaction_plugin;
  trans_mutex->unlock();
}

int
handleTransactionEvents(TSCont cont, TSEvent event, void *edata)
{
  // This function is only here to clean up Transaction objects
  TSHttpTxn ats_txn_handle = static_cast<TSHttpTxn>(edata);
  Transaction &transaction = utils::internal::getTransaction(ats_txn_handle);
  LOG_DEBUG("Got event %d on continuation %p for transaction (ats pointer %p, object %p)", event, cont, ats_txn_handle,
            &transaction);

  utils::internal::setTransactionEvent(transaction, event);
  switch (event) {
  case TS_EVENT_HTTP_POST_REMAP:
    // This is here to force a refresh of the cached client request url
    TSMBuffer hdr_buf;
    TSMLoc hdr_loc;
    (void)TSHttpTxnClientReqGet(static_cast<TSHttpTxn>(transaction.getAtsHandle()), &hdr_buf, &hdr_loc);
    break;
  case TS_EVENT_HTTP_SEND_REQUEST_HDR:
  case TS_EVENT_HTTP_READ_RESPONSE_HDR:
  case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
  case TS_EVENT_HTTP_READ_CACHE_HDR:
    // the buffer handles may be destroyed in the core during redirect follow
    resetTransactionHandles(transaction, event);
    break;
  case TS_EVENT_HTTP_TXN_CLOSE: { // opening scope to declare plugins variable below
    resetTransactionHandles(transaction, event);
    const std::list<TransactionPlugin *> &plugins = utils::internal::getTransactionPlugins(transaction);
    for (auto plugin : plugins) {
      cleanupTransactionPlugin(plugin, ats_txn_handle);
    }
    cleanupTransaction(transaction, ats_txn_handle);
  } break;
  default:
    assert(false); /* we should never get here */
    break;
  }
  TSHttpTxnReenable(ats_txn_handle, TS_EVENT_HTTP_CONTINUE);
  return 0;
}

void
setupTransactionManagement()
{
  // Reserve a transaction slot
  TSAssert(TS_SUCCESS == TSUserArgIndexReserve(TS_USER_ARGS_TXN, "atscppapi", "ATS CPP API", &TRANSACTION_STORAGE_INDEX));
  // We must always have a cleanup handler available
  TSMutex mutex = nullptr;
  TSCont cont   = TSContCreate(handleTransactionEvents, mutex);
  TSHttpHookAdd(TS_HTTP_POST_REMAP_HOOK, cont);
  TSHttpHookAdd(TS_HTTP_SEND_REQUEST_HDR_HOOK, cont);
  TSHttpHookAdd(TS_HTTP_READ_RESPONSE_HDR_HOOK, cont);
  TSHttpHookAdd(TS_HTTP_SEND_RESPONSE_HDR_HOOK, cont);
  TSHttpHookAdd(TS_HTTP_READ_CACHE_HDR_HOOK, cont);
  TSHttpHookAdd(TS_HTTP_TXN_CLOSE_HOOK, cont);
}

void inline invokePluginForEvent(Plugin *plugin, TSHttpTxn ats_txn_handle, TSEvent event)
{
  Transaction &transaction = utils::internal::getTransaction(ats_txn_handle);
  switch (event) {
  case TS_EVENT_HTTP_PRE_REMAP:
    plugin->handleReadRequestHeadersPreRemap(transaction);
    break;
  case TS_EVENT_HTTP_POST_REMAP:
    plugin->handleReadRequestHeadersPostRemap(transaction);
    break;
  case TS_EVENT_HTTP_SEND_REQUEST_HDR:
    plugin->handleSendRequestHeaders(transaction);
    break;
  case TS_EVENT_HTTP_READ_RESPONSE_HDR:
    plugin->handleReadResponseHeaders(transaction);
    break;
  case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
    plugin->handleSendResponseHeaders(transaction);
    break;
  case TS_EVENT_HTTP_OS_DNS:
    plugin->handleOsDns(transaction);
    break;
  case TS_EVENT_HTTP_READ_REQUEST_HDR:
    plugin->handleReadRequestHeaders(transaction);
    break;
  case TS_EVENT_HTTP_READ_CACHE_HDR:
    plugin->handleReadCacheHeaders(transaction);
    break;
  case TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE:
    plugin->handleReadCacheLookupComplete(transaction);
    break;
  case TS_EVENT_HTTP_TXN_CLOSE:
    if (plugin) {
      plugin->handleTxnClose(transaction);
      cleanupTransactionPlugin(plugin, ats_txn_handle);
    } else {
      LOG_ERROR("stray event TS_EVENT_HTTP_TXN_CLOSE, no transaction plugin to handle it!");
    }
    cleanupTransaction(transaction, ats_txn_handle);
    break;
  default:
    assert(false); /* we should never get here */
    break;
  }
}

} /* anonymous namespace */

Transaction &
utils::internal::getTransaction(TSHttpTxn ats_txn_handle)
{
  Transaction *transaction = static_cast<Transaction *>(TSUserArgGet(ats_txn_handle, TRANSACTION_STORAGE_INDEX));
  if (!transaction) {
    transaction = new Transaction(static_cast<void *>(ats_txn_handle));
    LOG_DEBUG("Created new transaction object at %p for ats pointer %p", transaction, ats_txn_handle);
    TSUserArgSet(ats_txn_handle, TRANSACTION_STORAGE_INDEX, transaction);
  }
  return *transaction;
}

std::shared_ptr<Mutex>
utils::internal::getTransactionPluginMutex(TransactionPlugin &transaction_plugin, TSHttpTxn txnp)
{
  return transaction_plugin.getMutex(txnp);
}

TSHttpHookID
utils::internal::convertInternalHookToTsHook(Plugin::HookType hooktype)
{
  switch (hooktype) {
  case Plugin::HOOK_READ_REQUEST_HEADERS_POST_REMAP:
    return TS_HTTP_POST_REMAP_HOOK;
  case Plugin::HOOK_READ_REQUEST_HEADERS_PRE_REMAP:
    return TS_HTTP_PRE_REMAP_HOOK;
  case Plugin::HOOK_READ_RESPONSE_HEADERS:
    return TS_HTTP_READ_RESPONSE_HDR_HOOK;
  case Plugin::HOOK_SEND_REQUEST_HEADERS:
    return TS_HTTP_SEND_REQUEST_HDR_HOOK;
  case Plugin::HOOK_SEND_RESPONSE_HEADERS:
    return TS_HTTP_SEND_RESPONSE_HDR_HOOK;
  case Plugin::HOOK_OS_DNS:
    return TS_HTTP_OS_DNS_HOOK;
  case Plugin::HOOK_READ_REQUEST_HEADERS:
    return TS_HTTP_READ_REQUEST_HDR_HOOK;
  case Plugin::HOOK_READ_CACHE_HEADERS:
    return TS_HTTP_READ_CACHE_HDR_HOOK;
  case Plugin::HOOK_CACHE_LOOKUP_COMPLETE:
    return TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK;
  case Plugin::HOOK_SELECT_ALT:
    return TS_HTTP_SELECT_ALT_HOOK;
  case Plugin::HOOK_TXN_CLOSE:
    return TS_HTTP_TXN_CLOSE_HOOK;
  default:
    assert(false); // shouldn't happen, let's catch it early
    break;
  }
  return static_cast<TSHttpHookID>(-1);
}

TSHttpHookID
utils::internal::convertInternalTransformationTypeToTsHook(TransformationPlugin::Type type)
{
  switch (type) {
  case TransformationPlugin::RESPONSE_TRANSFORMATION:
    return TS_HTTP_RESPONSE_TRANSFORM_HOOK;
  case TransformationPlugin::REQUEST_TRANSFORMATION:
    return TS_HTTP_REQUEST_TRANSFORM_HOOK;
  case TransformationPlugin::SINK_TRANSFORMATION:
    return TS_HTTP_RESPONSE_CLIENT_HOOK;
  default:
    assert(false); // shouldn't happen, let's catch it early
    break;
  }
  return static_cast<TSHttpHookID>(-1);
}

void
utils::internal::invokePluginForEvent(TransactionPlugin *plugin, TSHttpTxn ats_txn_handle, TSEvent event)
{
  std::lock_guard<Mutex> scopedLock(*(plugin->getMutex()));
  ::invokePluginForEvent(static_cast<Plugin *>(plugin), ats_txn_handle, event);
}

void
utils::internal::invokePluginForEvent(GlobalPlugin *plugin, TSHttpTxn ats_txn_handle, TSEvent event)
{
  ::invokePluginForEvent(static_cast<Plugin *>(plugin), ats_txn_handle, event);
}

void
utils::internal::invokePluginForEvent(GlobalPlugin *plugin, TSHttpAltInfo altinfo_handle, TSEvent event)
{
  TSMBuffer hdr_buf;
  TSMLoc hdr_loc;

  assert(event == TS_EVENT_HTTP_SELECT_ALT);

  TSHttpAltInfoClientReqGet(altinfo_handle, &hdr_buf, &hdr_loc);
  const Request clientReq(hdr_buf, hdr_loc); // no MLocRelease needed

  TSHttpAltInfoCachedReqGet(altinfo_handle, &hdr_buf, &hdr_loc);
  const Request cachedReq(hdr_buf, hdr_loc); // no MLocRelease needed

  TSHttpAltInfoCachedRespGet(altinfo_handle, &hdr_buf, &hdr_loc);
  Response cachedResp;
  cachedResp.init(hdr_buf, hdr_loc); // no MLocRelease needed

  plugin->handleSelectAlt(clientReq, cachedReq, cachedResp);
}

std::string
utils::internal::consumeFromTSIOBufferReader(TSIOBufferReader reader)
{
  std::string str;
  int avail = TSIOBufferReaderAvail(reader);

  if (avail != TS_ERROR) {
    int consumed = 0;
    if (avail > 0) {
      str.reserve(avail + 1);

      int64_t data_len;
      const char *char_data;
      TSIOBufferBlock block = TSIOBufferReaderStart(reader);
      while (block != nullptr) {
        char_data = TSIOBufferBlockReadStart(block, reader, &data_len);
        str.append(char_data, data_len);
        consumed += data_len;
        block = TSIOBufferBlockNext(block);
      }
    }
    TSIOBufferReaderConsume(reader, consumed);
  } else {
    LOG_ERROR("TSIOBufferReaderAvail returned error code %d for reader %p", avail, reader);
  }

  return str;
}

HttpVersion
utils::internal::getHttpVersion(TSMBuffer hdr_buf, TSMLoc hdr_loc)
{
  int version = TSHttpHdrVersionGet(hdr_buf, hdr_loc);
  if (version != TS_ERROR) {
    if ((TS_HTTP_MAJOR(version) == 0) && (TS_HTTP_MINOR(version) == 0)) {
      return HTTP_VERSION_0_9;
    }
    if ((TS_HTTP_MAJOR(version) == 1) && (TS_HTTP_MINOR(version) == 0)) {
      return HTTP_VERSION_1_0;
    }
    if ((TS_HTTP_MAJOR(version) == 1) && (TS_HTTP_MINOR(version) == 1)) {
      return HTTP_VERSION_1_1;
    } else {
      LOG_ERROR("Unrecognized version %d", version);
    }
  } else {
    LOG_ERROR("Could not get version; hdr_buf %p, hdr_loc %p", hdr_buf, hdr_loc);
  }
  return HTTP_VERSION_UNKNOWN;
}

void
utils::internal::initTransactionManagement()
{
  static pthread_once_t setup_pthread_once_control = PTHREAD_ONCE_INIT;
  pthread_once(&setup_pthread_once_control, setupTransactionManagement);
}
