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

/*
 * This plugin looks for range requests and then creates a new
 * cache key url so that each individual range requests is written
 * to the cache as a individual object so that subsequent range
 * requests are read across different disk drives reducing I/O
 * wait and load averages when there are large numbers of range
 * requests.
 */

#include "ts/ts.h"
#include "ts/remap.h"
#include "ts/remap_version.h"

#include <cinttypes>
#include <cstdio>
#include <cstring>
#include <getopt.h>
#include <string>
#include <string_view>

#define PLUGIN_NAME         "cache_range_requests"
#define DEBUG_LOG(fmt, ...) Dbg(dbg_ctl, fmt, ##__VA_ARGS__)
#define ERROR_LOG(fmt, ...) TSError("[%s:%d] %s(): " fmt, __FILE__, __LINE__, __func__, ##__VA_ARGS__)

namespace
{
DbgCtl dbg_ctl{PLUGIN_NAME};

enum parent_select_mode_t {
  PS_DEFAULT,      // Default ATS parent selection mode
  PS_CACHEKEY_URL, // Set parent selection url to cache_key url
};

constexpr std::string_view DefaultImsHeader  = {"X-Crr-Ims"};
constexpr std::string_view SLICE_CRR_HEADER  = {"Slice-Crr-Status"};
constexpr std::string_view SLICE_CRR_VAL     = "1";
constexpr std::string_view SKIP_CRR_HDR_NAME = {"X-Skip-Crr"};

struct pluginconfig {
  parent_select_mode_t ps_mode{PS_DEFAULT};
  bool                 consider_ims_header{false};
  bool                 modify_cache_key{true};
  bool                 verify_cacheability{false};
  bool                 cache_complete_responses{false};
  std::string          ims_header;
};

struct txndata {
  std::string  range_value;
  TSHttpStatus origin_status{TS_HTTP_STATUS_NONE};
  time_t       ims_time{0};
  bool         verify_cacheability{false};
  bool         cache_complete_responses{false};
  bool         slice_response{false};
  bool         slice_request{false};
};

// pluginconfig struct (global plugin only)
pluginconfig *gPluginConfig = {nullptr};

int                  handle_read_request_header(TSCont, TSEvent, void *);
void                 range_header_check(TSHttpTxn, pluginconfig *const);
void                 handle_send_origin_request(TSCont, TSHttpTxn, txndata *const);
void                 handle_client_send_response(TSHttpTxn, txndata *const);
void                 handle_server_read_response(TSHttpTxn, txndata *const);
int                  remove_header(TSMBuffer, TSMLoc, const char *, int);
bool                 set_header(TSMBuffer, TSMLoc, const char *, int, const char *, int);
int                  transaction_handler(TSCont, TSEvent, void *);
struct pluginconfig *create_pluginconfig(int argc, char *const argv[]);
void                 delete_pluginconfig(pluginconfig *const);
static bool          has_skip_crr_header(TSHttpTxn);

/**
 * Creates pluginconfig data structure
 * Sets default parent url selection mode
 * Walk plugin argument list and updates config
 */
pluginconfig *
create_pluginconfig(int argc, char *const argv[])
{
  DEBUG_LOG("Number of arguments: %d", argc);
  for (int index = 0; index < argc; ++index) {
    DEBUG_LOG("args[%d] = %s", index, argv[index]);
  }

  pluginconfig *const pc = new pluginconfig;

  if (nullptr == pc) {
    ERROR_LOG("Can't allocate pluginconfig");
    return nullptr;
  }

  static const struct option longopts[] = {
    {const_cast<char *>("consider-ims"),             no_argument,       nullptr, 'c'},
    {const_cast<char *>("ims-header"),               required_argument, nullptr, 'i'},
    {const_cast<char *>("no-modify-cachekey"),       no_argument,       nullptr, 'n'},
    {const_cast<char *>("ps-cachekey"),              no_argument,       nullptr, 'p'},
    {const_cast<char *>("verify-cacheability"),      no_argument,       nullptr, 'v'},
    {const_cast<char *>("cache-complete-responses"), no_argument,       nullptr, 'r'},
    {nullptr,                                        0,                 nullptr, 0  },
  };

  // getopt assumes args start at '1'
  ++argc;
  --argv;

  for (;;) {
    int const opt = getopt_long(argc, argv, "i:", longopts, nullptr);
    if (-1 == opt) {
      break;
    }

    switch (opt) {
    case 'c': {
      DEBUG_LOG("Plugin considers the ims header");
      pc->consider_ims_header = true;
    } break;
    case 'i': {
      DEBUG_LOG("Plugin uses custom ims header: %s", optarg);
      pc->ims_header.assign(optarg);
      pc->consider_ims_header = true;
    } break;
    case 'n': {
      DEBUG_LOG("Plugin doesn't modify cache key");
      pc->modify_cache_key = false;
    } break;
    case 'p': {
      DEBUG_LOG("Plugin modifies parent selection key");
      pc->ps_mode = PS_CACHEKEY_URL;
    } break;
    case 'v': {
      DEBUG_LOG("Plugin verifies whether the object in the transaction is cacheable");
      pc->verify_cacheability = true;
    } break;
    case 'r': {
      DEBUG_LOG("Plugin allows complete responses (200 OK) to be cached");
      pc->cache_complete_responses = true;
    } break;
    default: {
    } break;
    }
  }

  // Backwards compatibility
  if (optind < argc && 0 == strcmp("ps_mode:cache_key_url", argv[optind])) {
    DEBUG_LOG("Plugin modifies parent selection key (deprecated)");
    pc->ps_mode = PS_CACHEKEY_URL;
  }

  if (pc->consider_ims_header && pc->ims_header.empty()) {
    pc->ims_header = DefaultImsHeader;
    DEBUG_LOG("Plugin uses default ims header: %s", pc->ims_header.c_str());
  }

  return pc;
}

/**
 * Destroy pluginconfig data structure.
 */
void
delete_pluginconfig(pluginconfig *const pc)
{
  if (nullptr != pc) {
    DEBUG_LOG("Delete struct pluginconfig");
    delete pc;
  }
}

/**
 * Entry point when used as a global plugin.
 */
int
handle_read_request_header(TSCont /* txn_contp ATS_UNUSED */, TSEvent /* event ATS_UNUSED */, void *edata)
{
  TSHttpTxn txnp = static_cast<TSHttpTxn>(edata);

  if (!has_skip_crr_header(txnp)) {
    range_header_check(txnp, gPluginConfig);
  }

  TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
  return 0;
}

static bool
has_skip_crr_header(TSHttpTxn txnp)
{
  TSMBuffer hdr_buf = nullptr;
  TSMLoc    hdr_loc = TS_NULL_MLOC;
  bool      ret     = false;

  if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &hdr_buf, &hdr_loc)) {
    TSMLoc const skip_crr_loc = TSMimeHdrFieldFind(hdr_buf, hdr_loc, SKIP_CRR_HDR_NAME.data(), SKIP_CRR_HDR_NAME.length());
    if (TS_NULL_MLOC != skip_crr_loc) {
      TSHandleMLocRelease(hdr_buf, hdr_loc, skip_crr_loc);
      ret = true;
    }
    TSHandleMLocRelease(hdr_buf, TS_NULL_MLOC, hdr_loc);
  }
  return ret;
}

/**
 * Reads the client request header and if this is a range request:
 *
 * 1. creates a new cache key url using the range request information.
 * 2. Saves the range request information and then removes the range
 *    header so that the response retrieved from the origin will
 *    be written to cache.
 * 3. Schedules TS_HTTP_SEND_REQUEST_HDR_HOOK, TS_HTTP_SEND_RESPONSE_HDR_HOOK,
 *    and TS_HTTP_TXN_CLOSE_HOOK for further processing.
 */
void
range_header_check(TSHttpTxn txnp, pluginconfig *const pc)
{
  TSMBuffer hdr_buf = nullptr;
  TSMLoc    hdr_loc = TS_NULL_MLOC;

  if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &hdr_buf, &hdr_loc)) {
    TSMLoc const range_loc = TSMimeHdrFieldFind(hdr_buf, hdr_loc, TS_MIME_FIELD_RANGE, TS_MIME_LEN_RANGE);
    if (TS_NULL_MLOC != range_loc) {
      int               len       = 0;
      char const *const hdr_value = TSMimeHdrFieldValueStringGet(hdr_buf, hdr_loc, range_loc, 0, &len);

      if (!hdr_value || len <= 0) {
        DEBUG_LOG("Not a range request.");
      } else {
        txndata *const txn_state = new txndata;
        txn_state->range_value.assign(hdr_value, len);

        std::string const &rv = txn_state->range_value;
        DEBUG_LOG("txn_state->range_value: '%s'", rv.c_str());

        // Consider config options
        if (nullptr != pc) {
          char cache_key_url[16384] = {0};
          int  cache_key_url_len    = 0;

          if (pc->modify_cache_key || PS_CACHEKEY_URL == pc->ps_mode) {
            int         url_len = 0;
            char *const req_url = TSHttpTxnEffectiveUrlStringGet(txnp, &url_len);
            cache_key_url_len   = snprintf(cache_key_url, sizeof(cache_key_url), "%s-%s", req_url, rv.c_str());
            DEBUG_LOG("Forming new cache URL for '%s': '%.*s'", req_url, cache_key_url_len, cache_key_url);
            if (req_url != nullptr) {
              TSfree(req_url);
            }
          }

          // Modify the cache_key
          if (pc->modify_cache_key) {
            DEBUG_LOG("Setting cache key to '%.*s'", cache_key_url_len, cache_key_url);
            if (TS_SUCCESS != TSCacheUrlSet(txnp, cache_key_url, cache_key_url_len)) {
              ERROR_LOG("Failed to change the cache url, disabling cache for this transaction to avoid cache poisoning.");
              TSHttpTxnCntlSet(txnp, TS_HTTP_CNTL_SERVER_NO_STORE, true);
              TSHttpTxnCntlSet(txnp, TS_HTTP_CNTL_RESPONSE_CACHEABLE, false);
              TSHttpTxnCntlSet(txnp, TS_HTTP_CNTL_REQUEST_CACHEABLE, false);
            }
          }

          // Set the parent_selection_url to the modified cache_key.
          if (PS_CACHEKEY_URL == pc->ps_mode) {
            TSMLoc      ps_loc = TS_NULL_MLOC;
            const char *start  = cache_key_url;
            const char *end    = cache_key_url + cache_key_url_len;
            if (TS_SUCCESS == TSUrlCreate(hdr_buf, &ps_loc)) {
              if (TS_PARSE_DONE == TSUrlParse(hdr_buf, ps_loc, &start, end) && // This should always succeed.
                  TS_SUCCESS == TSHttpTxnParentSelectionUrlSet(txnp, hdr_buf, ps_loc)) {
                DEBUG_LOG("Setting Parent Selection URL to '%.*s'", cache_key_url_len, cache_key_url);
              }
              TSHandleMLocRelease(hdr_buf, TS_NULL_MLOC, ps_loc);
            }
          }

          // optionally consider an ims header
          if (pc->consider_ims_header) {
            TSMLoc const imsloc = TSMimeHdrFieldFind(hdr_buf, hdr_loc, pc->ims_header.data(), pc->ims_header.size());
            if (TS_NULL_MLOC != imsloc) {
              time_t const itime = TSMimeHdrFieldValueDateGet(hdr_buf, hdr_loc, imsloc);
              DEBUG_LOG("Servicing the '%s' header", pc->ims_header.c_str());
              TSHandleMLocRelease(hdr_buf, hdr_loc, imsloc);
              if (0 < itime) {
                txn_state->ims_time = itime;
              }
            }
          }

          txn_state->verify_cacheability      = pc->verify_cacheability;
          txn_state->cache_complete_responses = pc->cache_complete_responses;
        }

        // remove the range request header.
        if (remove_header(hdr_buf, hdr_loc, TS_MIME_FIELD_RANGE, TS_MIME_LEN_RANGE) > 0) {
          DEBUG_LOG("Removed the Range: header from the request.");
        }

        // Set up the continuation
        TSCont const txn_contp = TSContCreate(transaction_handler, nullptr);
        TSContDataSet(txn_contp, txn_state);
        TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_REQUEST_HDR_HOOK, txn_contp);
        TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, txn_contp);
        TSHttpTxnHookAdd(txnp, TS_HTTP_TXN_CLOSE_HOOK, txn_contp);
        DEBUG_LOG("Added TS_HTTP_SEND_REQUEST_HDR_HOOK, TS_HTTP_SEND_RESPONSE_HDR_HOOK, and TS_HTTP_TXN_CLOSE_HOOK");

        if (0 < txn_state->ims_time) {
          TSHttpTxnHookAdd(txnp, TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK, txn_contp);
          DEBUG_LOG("Also Added TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK");
        }

        // check if slice requests for cache lookup status
        TSMLoc const locfield(TSMimeHdrFieldFind(hdr_buf, hdr_loc, SLICE_CRR_HEADER.data(), SLICE_CRR_HEADER.size()));
        if (nullptr != locfield) {
          TSHandleMLocRelease(hdr_buf, hdr_loc, locfield);
          txn_state->slice_request = true;
        }
      }
      TSHandleMLocRelease(hdr_buf, hdr_loc, range_loc);
    } else {
      DEBUG_LOG("No range request header.");
    }
    TSHandleMLocRelease(hdr_buf, TS_NULL_MLOC, hdr_loc);
  }
}

/**
 * Restores the range request header if the request must be
 * satisfied from the origin and schedules the TS_READ_RESPONSE_HDR_HOOK.
 */
void
handle_send_origin_request(TSCont contp, TSHttpTxn txnp, txndata *const txn_state)
{
  TSMBuffer hdr_buf;
  TSMLoc    hdr_loc = TS_NULL_MLOC;

  std::string const &rv = txn_state->range_value;
  if (rv.empty()) {
    ERROR_LOG("txn_state->range_value unexpectedly empty!");
    return;
  }

  if (TS_SUCCESS == TSHttpTxnServerReqGet(txnp, &hdr_buf, &hdr_loc) && !rv.empty()) {
    if (set_header(hdr_buf, hdr_loc, TS_MIME_FIELD_RANGE, TS_MIME_LEN_RANGE, rv.data(), rv.size())) {
      DEBUG_LOG("Added range header: %s", rv.c_str());
      TSHttpTxnHookAdd(txnp, TS_HTTP_READ_RESPONSE_HDR_HOOK, contp);
    }
  }
  TSHandleMLocRelease(hdr_buf, TS_NULL_MLOC, hdr_loc);
}

/**
 * Changes the response status back to a 206 before
 * replying to the client that requested a range.
 */
void
handle_client_send_response(TSHttpTxn txnp, txndata *const txn_state)
{
  bool partial_content_reason = false;

  // Detect header modified by this plugin (200 response)
  TSMBuffer resp_buf = nullptr;
  TSMLoc    resp_loc = TS_NULL_MLOC;
  if (TS_SUCCESS == TSHttpTxnClientRespGet(txnp, &resp_buf, &resp_loc)) {
    TSHttpStatus const status = TSHttpHdrStatusGet(resp_buf, resp_loc);
    // a cached status will be 200 with expected parent response status of 206
    if (TS_HTTP_STATUS_OK == status) {
      if (txn_state->origin_status == TS_HTTP_STATUS_NONE ||
          txn_state->origin_status == TS_HTTP_STATUS_NOT_MODIFIED) { // cache hit or revalidation
        // status is always TS_HTTP_STATUS_NONE on cache hit; its value is only set during handle_server_read_response()
        TSMLoc content_range_loc = TSMimeHdrFieldFind(resp_buf, resp_loc, TS_MIME_FIELD_CONTENT_RANGE, TS_MIME_LEN_CONTENT_RANGE);

        if (content_range_loc) {
          DEBUG_LOG("Got TS_HTTP_STATUS_OK on cache hit or revalidation and Content-Range header present in response");
          partial_content_reason = true;
          TSHandleMLocRelease(resp_buf, resp_loc, content_range_loc);
        } else {
          DEBUG_LOG("Got TS_HTTP_STATUS_OK on cache hit and Content-Range header is NOT present in response");
        }
      } else if (txn_state->origin_status ==
                 TS_HTTP_STATUS_PARTIAL_CONTENT) { // only set on cache miss in handle_server_read_response()
        DEBUG_LOG("Got TS_HTTP_STATUS_OK with origin TS_HTTP_STATUS_PARTIAL_CONTENT");
        partial_content_reason = true;
      } else {
        DEBUG_LOG("Allowing TS_HTTP_STATUS_OK in response due to origin status code %d", txn_state->origin_status);
      }

      if (partial_content_reason) {
        DEBUG_LOG("Restoring response header to TS_HTTP_STATUS_PARTIAL_CONTENT.");
        TSHttpHdrStatusSet(resp_buf, resp_loc, TS_HTTP_STATUS_PARTIAL_CONTENT);
      }

      remove_header(resp_buf, resp_loc, SLICE_CRR_HEADER.data(), SLICE_CRR_HEADER.size());
      if (txn_state->slice_response) {
        set_header(resp_buf, resp_loc, SLICE_CRR_HEADER.data(), SLICE_CRR_HEADER.size(), SLICE_CRR_VAL.data(),
                   SLICE_CRR_VAL.size());
      }
    } else {
      DEBUG_LOG("Ignoring status code %d; txn_state->origin_status=%d", status, txn_state->origin_status);
    }
    TSHandleMLocRelease(resp_buf, TS_NULL_MLOC, resp_loc);
  }

  if (partial_content_reason) {
    DEBUG_LOG("Attempting to restore the Range header");
    std::string const &rv = txn_state->range_value;
    // Restore the range request header
    if (!rv.empty()) {
      TSMBuffer req_buf = nullptr;
      TSMLoc    req_loc = TS_NULL_MLOC;
      if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &req_buf, &req_loc)) {
        DEBUG_LOG("Adding range header: %s", rv.c_str());
        if (!set_header(req_buf, req_loc, TS_MIME_FIELD_RANGE, TS_MIME_LEN_RANGE, rv.data(), rv.size())) {
          DEBUG_LOG("set_header() failed.");
        }
        TSHandleMLocRelease(req_buf, TS_NULL_MLOC, req_loc);
      }
    }
  }
}

/**
 * After receiving a range request response from the origin, change
 * the response status from a 206 to a 200 so that
 * the response will be written to cache.
 */
void
handle_server_read_response(TSHttpTxn txnp, txndata *const txn_state)
{
  TSMBuffer resp_buf = nullptr;
  TSMLoc    resp_loc = TS_NULL_MLOC;
  int       cache_lookup;

  if (TS_SUCCESS == TSHttpTxnServerRespGet(txnp, &resp_buf, &resp_loc)) {
    TSHttpStatus const status = TSHttpHdrStatusGet(resp_buf, resp_loc);
    txn_state->origin_status  = status;
    if (TS_HTTP_STATUS_PARTIAL_CONTENT == status) {
      DEBUG_LOG("Got TS_HTTP_STATUS_PARTIAL_CONTENT.");
      // changing the status code from 206 to 200 forces the object into cache
      TSHttpHdrStatusSet(resp_buf, resp_loc, TS_HTTP_STATUS_OK);
      DEBUG_LOG("Set response header to TS_HTTP_STATUS_OK.");

      if (txn_state->verify_cacheability && !TSHttpTxnIsCacheable(txnp, nullptr, resp_buf)) {
        DEBUG_LOG("transaction is not cacheable; resetting status code to 206");
        TSHttpHdrStatusSet(resp_buf, resp_loc, TS_HTTP_STATUS_PARTIAL_CONTENT);
      }
    } else if (TS_HTTP_STATUS_OK == status) {
      bool cacheable = txn_state->cache_complete_responses;

      if (cacheable && txn_state->verify_cacheability) {
        DEBUG_LOG("Received a cacheable complete response from the origin; verifying cacheability");
        cacheable = TSHttpTxnIsCacheable(txnp, nullptr, resp_buf);
      }

      // 200s are cached by default; only cache if configured to do so
      if (!cacheable && TS_SUCCESS == TSHttpTxnCntlSet(txnp, TS_HTTP_CNTL_SERVER_NO_STORE, true)) {
        DEBUG_LOG("Cache write has been disabled for this transaction.");
      } else {
        DEBUG_LOG("Allowing object to be cached.");
      }
    }

    // slice requesting cache lookup status and cacheability (only on miss or validation)
    if ((txn_state->origin_status == TS_HTTP_STATUS_PARTIAL_CONTENT || txn_state->origin_status == TS_HTTP_STATUS_NOT_MODIFIED) &&
        txn_state->slice_request && TSHttpTxnIsCacheable(txnp, nullptr, resp_buf) &&
        TSHttpTxnCacheLookupStatusGet(txnp, &cache_lookup) == TS_SUCCESS &&
        (cache_lookup == TS_CACHE_LOOKUP_MISS || cache_lookup == TS_CACHE_LOOKUP_HIT_STALE)) {
      txn_state->slice_response = true;
    }

    TSHandleMLocRelease(resp_buf, TS_NULL_MLOC, resp_loc);
  }
}

/**
 * Remove a header (fully) from an TSMLoc / TSMBuffer. Return the number
 * of fields (header values) we removed.
 *
 * From background_fetch.cc
 */
int
remove_header(TSMBuffer buf, TSMLoc hdr_loc, const char *header, int len)
{
  TSMLoc field = TSMimeHdrFieldFind(buf, hdr_loc, header, len);
  int    cnt   = 0;

  while (TS_NULL_MLOC != field) {
    TSMLoc const tmp = TSMimeHdrFieldNextDup(buf, hdr_loc, field);

    ++cnt;
    TSMimeHdrFieldDestroy(buf, hdr_loc, field);
    TSHandleMLocRelease(buf, hdr_loc, field);
    field = tmp;
  }

  return cnt;
}

/**
 * Set a header to a specific value. This will avoid going to through a
 * remove / add sequence in case of an existing header.
 * but clean.
 *
 * From background_fetch.cc
 */
bool
set_header(TSMBuffer buf, TSMLoc hdr_loc, const char *header, int len, const char *val, int val_len)
{
  if (nullptr == buf || TS_NULL_MLOC == hdr_loc || nullptr == header || len <= 0 || nullptr == val || val_len <= 0) {
    return false;
  }

  DEBUG_LOG("header: %s, len: %d, val: %s, val_len: %d", header, len, val, val_len);
  bool   ret       = false;
  TSMLoc field_loc = TSMimeHdrFieldFind(buf, hdr_loc, header, len);

  if (TS_NULL_MLOC == field_loc) {
    // No existing header, so create one
    if (TS_SUCCESS == TSMimeHdrFieldCreateNamed(buf, hdr_loc, header, len, &field_loc)) {
      if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(buf, hdr_loc, field_loc, -1, val, val_len)) {
        TSMimeHdrFieldAppend(buf, hdr_loc, field_loc);
        ret = true;
      }
      TSHandleMLocRelease(buf, hdr_loc, field_loc);
    }
  } else {
    bool first = true;

    while (TS_NULL_MLOC != field_loc) {
      TSMLoc const tmp = TSMimeHdrFieldNextDup(buf, hdr_loc, field_loc);
      if (first) {
        first = false;
        if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(buf, hdr_loc, field_loc, -1, val, val_len)) {
          ret = true;
        }
      } else {
        TSMimeHdrFieldDestroy(buf, hdr_loc, field_loc);
      }
      TSHandleMLocRelease(buf, hdr_loc, field_loc);
      field_loc = tmp;
    }
  }

  return ret;
}

time_t
get_date_from_cached_hdr(TSHttpTxn txn)
{
  TSMBuffer buf     = nullptr;
  TSMLoc    hdr_loc = TS_NULL_MLOC;
  time_t    date    = 0;

  if (TSHttpTxnCachedRespGet(txn, &buf, &hdr_loc) == TS_SUCCESS) {
    TSMLoc const date_loc = TSMimeHdrFieldFind(buf, hdr_loc, TS_MIME_FIELD_DATE, TS_MIME_LEN_DATE);
    if (TS_NULL_MLOC != date_loc) {
      date = TSMimeHdrFieldValueDateGet(buf, hdr_loc, date_loc);
      TSHandleMLocRelease(buf, hdr_loc, date_loc);
    }
    TSHandleMLocRelease(buf, TS_NULL_MLOC, hdr_loc);
  }

  return date;
}

/**
 * Handle a special IMS request.
 */
void
handle_cache_lookup_complete(TSHttpTxn txnp, txndata *const txn_state)
{
  int cachestat;
  if (TS_SUCCESS == TSHttpTxnCacheLookupStatusGet(txnp, &cachestat)) {
    if (TS_CACHE_LOOKUP_HIT_FRESH == cachestat) {
      time_t const ch_time = get_date_from_cached_hdr(txnp);
      DEBUG_LOG("IMS Cached header time %jd vs IMS %jd", static_cast<intmax_t>(ch_time),
                static_cast<intmax_t>(txn_state->ims_time));
      if (ch_time < txn_state->ims_time) {
        TSHttpTxnCacheLookupStatusSet(txnp, TS_CACHE_LOOKUP_HIT_STALE);
        if (dbg_ctl.on()) {
          int         url_len = 0;
          char *const req_url = TSHttpTxnEffectiveUrlStringGet(txnp, &url_len);
          if (nullptr != req_url) {
            std::string const &rv = txn_state->range_value;
            DEBUG_LOG("Forced revalidate %.*s-%s", url_len, req_url, rv.c_str());

            TSfree(req_url);
          }
        }
      }
    }
  }
}

/**
 * Transaction event handler.
 */
int
transaction_handler(TSCont contp, TSEvent event, void *edata)
{
  TSHttpTxn      txnp      = static_cast<TSHttpTxn>(edata);
  txndata *const txn_state = static_cast<txndata *>(TSContDataGet(contp));

  switch (event) {
  case TS_EVENT_HTTP_READ_RESPONSE_HDR:
    handle_server_read_response(txnp, txn_state);
    break;
  case TS_EVENT_HTTP_SEND_REQUEST_HDR:
    handle_send_origin_request(contp, txnp, txn_state);
    break;
  case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
    handle_client_send_response(txnp, txn_state);
    break;
  case TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE:
    handle_cache_lookup_complete(txnp, txn_state);
    break;
  case TS_EVENT_HTTP_TXN_CLOSE:
    if (txn_state != nullptr) {
      TSContDataSet(contp, nullptr);
      delete txn_state;
    }
    TSContDestroy(contp);
    break;
  default:
    TSAssert(!"Unexpected event");
    break;
  }
  TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
  return 0;
}

} // namespace

/**
 * Remap initialization.
 */
TSReturnCode
TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
{
  CHECK_REMAP_API_COMPATIBILITY(api_info, errbuf, errbuf_size);
  DEBUG_LOG("cache_range_requests remap is successfully initialized.");
  return TS_SUCCESS;
}

/**
 * New Remap instance
 */
TSReturnCode
TSRemapNewInstance(int argc, char *argv[], void **ih, char * /*errbuf */, int /* errbuf_size */)
{
  if (argc < 2) {
    ERROR_LOG("Remap argument list should contain at least 2 params"); // Should never happen..
    return TS_ERROR;
  }

  // Skip over the Remap params
  char *const *plugin_argv = const_cast<char *const *>(argv);

  // Parse the argument list.
  *ih = static_cast<void *>(create_pluginconfig(argc - 2, plugin_argv + 2));

  if (*ih == nullptr) {
    ERROR_LOG("Can't create pluginconfig");
  }

  return TS_SUCCESS;
}

/**
 * Delete Remap instance
 */
void
TSRemapDeleteInstance(void *ih)
{
  pluginconfig *const pc = static_cast<pluginconfig *>(ih);

  if (nullptr != pc) {
    delete_pluginconfig(pc);
  }
}

/**
 * Remap entry point.
 */
TSRemapStatus
TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo * /* rri */)
{
  pluginconfig *const pc = static_cast<pluginconfig *>(ih);

  if (!has_skip_crr_header(txnp)) {
    range_header_check(txnp, pc);
  }

  return TSREMAP_NO_REMAP;
}

/**
 * Global plugin initialization.
 */
void
TSPluginInit(int argc, const char *argv[])
{
  TSPluginRegistrationInfo info;
  TSCont                   txnp_cont;

  info.plugin_name   = (char *)PLUGIN_NAME;
  info.vendor_name   = (char *)"Comcast";
  info.support_email = (char *)"John_Rushford@cable.comcast.com";

  if (TSPluginRegister(&info) != TS_SUCCESS) {
    ERROR_LOG("Plugin registration failed.\n");
    ERROR_LOG("Unable to initialize plugin (disabled).");
    return;
  }

  if (nullptr == gPluginConfig) {
    if (1 < argc) {
      char *const *plugin_argv = const_cast<char *const *>(argv);
      gPluginConfig            = create_pluginconfig(argc - 1, plugin_argv + 1);
    }
  }

  if (nullptr == (txnp_cont = TSContCreate(static_cast<TSEventFunc>(handle_read_request_header), nullptr))) {
    ERROR_LOG("failed to create the transaction continuation handler.");
    return;
  } else {
    TSHttpHookAdd(TS_HTTP_POST_REMAP_HOOK, txnp_cont);
  }
}
