/** @file

  This plugin counts the number of times every header has appeared.
  Maintains separate counts for client and origin headers.

  @section license License

  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 <algorithm>
#include <atomic>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <vector>

#include "ts/apidefs.h"
#include <ts/ts.h>

namespace
{
// plugin registration info
char PLUGIN_NAME[]   = "header_freq";
char VENDOR_NAME[]   = "Apache Software Foundation";
char SUPPORT_EMAIL[] = "dev@trafficserver.apache.org";

// debug messages during one-time initialization
const char DEBUG_TAG_INIT[] = "header_freq.init";

// debug messages in continuation callbacks
const char DEBUG_TAG_HOOK[] = "header_freq.hook";

DbgCtl dbg_ctl{DEBUG_TAG_HOOK};
DbgCtl dbg_ctl_init{DEBUG_TAG_INIT};

// A map from header name to the number of times the header was encountered.
using CountMap_t = std::unordered_map<std::string, std::atomic<unsigned int>>;
CountMap_t        client_freq;
CountMap_t        origin_freq;
std::shared_mutex map_mutex;

// A vector for when we want to sort the map.
using CountVector_t = std::vector<std::pair<std::string, unsigned int>>;

// for traffic_ctl, name is a convenient identifier
const char  *ctl_tag             = PLUGIN_NAME;
const char   CONTROL_MSG_LOG[]   = "log"; // log all data
const size_t CONTROL_MSG_LOG_LEN = sizeof(CONTROL_MSG_LOG) - 1;

void
Log_Sorted_Map(CountMap_t const &map, std::ostream &ss)
{
  CountVector_t sorted_vector;
  {
    std::shared_lock<std::shared_mutex> lock(map_mutex);
    sorted_vector = CountVector_t(map.begin(), map.end());
  }
  std::sort(sorted_vector.begin(), sorted_vector.end(), [](const auto &a, const auto &b) -> bool { return a.second > b.second; });

  for (auto const &[header_name, count] : sorted_vector) {
    ss << header_name << ": " << count << std::endl;
  }
}

void
Log_Data(std::ostream &ss)
{
  ss << std::endl << std::string(100, '+') << std::endl;

  ss << "CLIENT HEADERS" << std::endl;
  Log_Sorted_Map(client_freq, ss);

  ss << std::endl;

  ss << "ORIGIN HEADERS" << std::endl;
  Log_Sorted_Map(origin_freq, ss);

  ss << std::string(100, '+') << std::endl;
}

/**
 * Logs the data collected, first the client, and then
 * the origin headers.
 */
int
CB_Command_Log(TSCont contp, TSEvent /* event ATS_UNUSED */, void * /* edata ATS_UNUSED */)
{
  std::string *command = static_cast<std::string *>(TSContDataGet(contp));
  if (nullptr == command) {
    TSError("[%s] Could not get the message argument from the log handler.", PLUGIN_NAME);
    return TS_ERROR;
  }

  std::string::size_type colon_idx;

  if (std::string::npos != (colon_idx = command->find(':'))) {
    std::string path = command->substr(colon_idx + 1);
    // The length of the data can include a trailing null, clip it.
    if (path.length() > 0 && path.back() == '\0') {
      path.pop_back();
    }
    if (path.length() > 0) {
      std::ofstream out;
      out.open(path, std::ios::out | std::ios::app);
      if (out.is_open()) {
        Log_Data(out);
      } else {
        TSError("[%s] Failed to open file '%s' for logging: %s", PLUGIN_NAME, path.c_str(), strerror(errno));
      }
    } else {
      TSError("[%s] Invalid (zero length) file name for logging", PLUGIN_NAME);
    }
  } else {
    // No filename provided, log to stdout (traffic.out).
    Log_Data(std::cout);
  }

  // cleanup.
  delete command;
  TSContDestroy(contp);
  return TS_SUCCESS;
}

/**
 * Records all headers found in the buffer in the map provided. Comparison
 * against existing entries is case-insensitive.
 */
static void
count_all_headers(TSMBuffer &bufp, TSMLoc &hdr_loc, CountMap_t &map)
{
  TSMLoc hdr, next_hdr;
  hdr           = TSMimeHdrFieldGet(bufp, hdr_loc, 0);
  int n_headers = TSMimeHdrFieldsCount(bufp, hdr_loc);
  Dbg(dbg_ctl, "%d headers found", n_headers);

  // iterate through all headers
  for (int i = 0; i < n_headers && nullptr != hdr; ++i) {
    int         hdr_len;
    const char *hdr_name = TSMimeHdrFieldNameGet(bufp, hdr_loc, hdr, &hdr_len);
    std::string str      = std::string(hdr_name, hdr_len);

    // make case-insensitive by converting to lowercase
    for (auto &c : str) {
      c = tolower(c);
    }

    { // For lock scoping.
      std::shared_lock<std::shared_mutex> reader_lock{map_mutex};
      if (map.find(str) == map.end()) {
        // Upgrade the lock to be exclusive.
        reader_lock.unlock();
        std::unique_lock<std::shared_mutex> ulock{map_mutex};
        // There's a potential race condition here such that another thread may
        // have inserted the key while we were upgrading the lock. Regardless,
        // incrementing the value here always does the right thing.
        ++map[str];
      } else {
        ++map[str];
      }
    }

    next_hdr = TSMimeHdrFieldNext(bufp, hdr_loc, hdr);
    TSHandleMLocRelease(bufp, hdr_loc, hdr);
    hdr = next_hdr;
  }

  TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
}

/** Handle common logic between the request and response headers.
 * @param[in] txnp The transaction pointer for this HTTP message.
 * @param[in] event The event that triggered this callback.
 * @param[out] freq_map The map to update with the header counts.
 * @return TS_SUCCESS if the event was handled successfully, TS_ERROR otherwise.
 */
int
handle_header_event(TSHttpTxn txnp, TSEvent event, CountMap_t &freq_map)
{
  TSMBuffer    bufp;
  TSMLoc       hdr_loc;
  TSReturnCode ret;

  char const *message_type = nullptr;
  if (event == TS_EVENT_HTTP_READ_REQUEST_HDR) {
    message_type = "request";
    ret          = TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc);
  } else { // TS_EVENT_HTTP_SEND_RESPONSE_HDR
    message_type = "response";
    ret          = TSHttpTxnClientRespGet(txnp, &bufp, &hdr_loc);
  }

  if (ret != TS_SUCCESS) {
    TSError("[%s] could not get %s headers", PLUGIN_NAME, message_type);
    TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR);
    return TS_ERROR;
  }

  count_all_headers(bufp, hdr_loc, freq_map);
  TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
  return TS_SUCCESS;
}

/** Continuation callback. Invoked to count headers on READ_REQUEST_HDR and
 * SEND_RESPONSE_HDR hooks.
 */
int
header_handle_hook(TSCont /* contp ATS_UNUSED */, TSEvent event, void *edata)
{
  TSHttpTxn txnp    = reinterpret_cast<TSHttpTxn>(edata);
  int       ret_val = TS_SUCCESS;

  switch (event) {
  case TS_EVENT_HTTP_READ_REQUEST_HDR: // count client headers
    Dbg(dbg_ctl, "event TS_EVENT_HTTP_READ_REQUEST_HDR");
    ret_val = handle_header_event(txnp, event, client_freq);
    break;
  case TS_EVENT_HTTP_SEND_RESPONSE_HDR: // count origin headers
    Dbg(dbg_ctl, "event TS_EVENT_HTTP_SEND_RESPONSE_HDR");
    ret_val = handle_header_event(txnp, event, origin_freq);
    break;
  default:
    TSError("[%s] unexpected event in header handler: %d", PLUGIN_NAME, event);
    break;
  }

  return ret_val;
}

/**
 * Continuation callback. Invoked to handler the LIFE_CYCLE_MSG event to log
 * header stats.
 */
int
msg_handle_hook(TSCont /* contp ATS_UNUSED */, TSEvent event, void *edata)
{
  switch (event) {
  case TS_EVENT_LIFECYCLE_MSG: // Handle external command
  {
    TSPluginMsg *msgp = static_cast<TSPluginMsg *>(edata);

    if (0 == strcasecmp(ctl_tag, msgp->tag)) {
      // identify the command
      if (msgp->data_size >= CONTROL_MSG_LOG_LEN &&
          0 == strncasecmp(CONTROL_MSG_LOG, static_cast<const char *>(msgp->data), CONTROL_MSG_LOG_LEN)) {
        Dbg(dbg_ctl, "Scheduled execution of '%s' command", CONTROL_MSG_LOG);
        TSCont c = TSContCreate(CB_Command_Log, TSMutexCreate());
        TSContDataSet(c, new std::string(static_cast<const char *>(msgp->data), msgp->data_size));
        TSContScheduleOnPool(c, 0, TS_THREAD_POOL_TASK);
      } else if (msgp->data_size == 0) {
        TSError("[%s] No command provided.", PLUGIN_NAME);
      } else {
        TSError("[%s] Unknown command '%.*s'", PLUGIN_NAME, static_cast<int>(msgp->data_size),
                static_cast<const char *>(msgp->data));
      }
    }
  } break;
  default:
    TSError("[%s] unexpected event in message handler: %d", PLUGIN_NAME, event);
    break;
  }

  return TS_SUCCESS;
}

} // anonymous namespace

/// Registration entry point for plugin.
void
TSPluginInit(int /* argc ATS_UNUSED */, const char ** /* argv ATS_UNUSED */)
{
  Dbg(dbg_ctl_init, "initializing plugin");

  TSPluginRegistrationInfo info = {PLUGIN_NAME, VENDOR_NAME, SUPPORT_EMAIL};

  if (TSPluginRegister(&info) != TS_SUCCESS) {
    TSError("[%s](%s) Plugin registration failed. \n", PLUGIN_NAME, __FUNCTION__);
  }

  TSCont header_contp = TSContCreate(header_handle_hook, nullptr);
  if (header_contp == nullptr) {
    // Continuation initialization failed. Unrecoverable, report and exit.
    TSError("[%s](%s) could not create the header handler continuation", PLUGIN_NAME, __FUNCTION__);
    abort();
  }
  // Continuation initialization succeeded
  TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, header_contp);
  TSHttpHookAdd(TS_HTTP_SEND_RESPONSE_HDR_HOOK, header_contp);

  TSCont msg_contp = TSContCreate(msg_handle_hook, nullptr);
  if (msg_contp == nullptr) {
    // Continuation initialization failed. Unrecoverable, report and exit.
    TSError("[%s](%s) could not create the message handler continuation", PLUGIN_NAME, __FUNCTION__);
    abort();
  }
  TSLifecycleHookAdd(TS_LIFECYCLE_MSG_HOOK, msg_contp);
}
