/** @file

    ATS plugin to do (simple) regular expression remap rules

    @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 "ts/ts.h"
#include "ts/remap.h"
#include "ts/remap_version.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <cstdio>
#include <ctime>
#include <cstring>

#include <cctype>
#include <unistd.h>

#include <algorithm>
#include <iostream>
#include <fstream>
#include <string>
#include <cctype>
#include <memory>
#include <mutex>
#include <sstream>
#include <unordered_map>
#include <vector>

// Get some specific stuff from libts, yes, we can do that now that we build inside the core.
#include "tscore/ink_platform.h"
#include "tscore/ink_atomic.h"
#include "tscore/ink_time.h"
#include "tscore/ink_inet.h"
#include "tsutil/LocalBuffer.h"
#include "tsutil/Regex.h"

static const char *PLUGIN_NAME = "regex_remap";

// Constants
static const int     MATCHCOUNT        = 15;   // We support $0 - $9 x2 ints, and this needs to be 1.5x that
static const int     MAX_SUBS          = 32;   // No more than 32 substitution variables in the subst string
static const int32_t REGEX_MATCH_LIMIT = 1750; // POOMA - also dependent on actual stack size. Crashes with previous value of 2047

// Substitutions other than regex matches
enum ExtraSubstitutions {
  SUB_HOST      = 11,
  SUB_FROM_HOST = 12,
  SUB_TO_HOST   = 13,
  SUB_PORT      = 14,
  SUB_SCHEME    = 15,
  SUB_PATH      = 16,
  SUB_QUERY     = 17,
  // 18 is unused, used to be matrix parameters
  SUB_CLIENT_IP  = 19,
  SUB_LOWER_PATH = 20,
};

static DbgCtl dbg_ctl{PLUGIN_NAME};

///////////////////////////////////////////////////////////////////////////////
// Class holding one request URL's component, to simplify the code and
// length calculations (we need all of them).
//
struct UrlComponents {
  UrlComponents() = default;

  void
  populate(TSMBuffer bufp, TSMLoc url)
  {
    scheme = TSUrlSchemeGet(bufp, url, &scheme_len);
    host   = TSUrlHostGet(bufp, url, &host_len);
    path   = TSUrlPathGet(bufp, url, &path_len);
    query  = TSUrlHttpQueryGet(bufp, url, &query_len);
    port   = TSUrlPortGet(bufp, url);

    url_len = scheme_len + host_len + path_len + query_len + 32;
  }

  const char *scheme = nullptr;
  const char *host   = nullptr;
  const char *path   = nullptr;
  const char *query  = nullptr;
  int         port   = 0;

  int scheme_len = 0;
  int host_len   = 0;
  int path_len   = 0;
  int query_len  = 0;

  int url_len = 0; // Full length, of all components
};

///////////////////////////////////////////////////////////////////////////////
// One immutable remap rule: the compiled regex plus its substitution template
// and per-rule options (status, timeouts, strategy, config overrides).
//
// A RemapRegex is shared. Once compile_rule_set() hands it to RuleSet::add() it
// is const, and it is read concurrently and without locks by every ET_NET
// thread of every RemapInstance that loaded the same rule file. Nothing here
// may be mutated after that point, and nothing per-instance or per-transaction
// may be stored here. That is why the match context and profiling hit counts
// are passed in as arguments rather than kept as members: they belong to
// RemapInstance. Put new per-instance state on RemapInstance, indexed in
// lockstep with RuleSet::rules(), never on this class.
//
class RemapRegex
{
public:
  RemapRegex()                              = default;
  RemapRegex(RemapRegex const &)            = delete;
  RemapRegex &operator=(RemapRegex const &) = delete;

  ~RemapRegex()
  {
    Dbg(dbg_ctl, "Calling destructor");
    TSfree(_rex_string);
    TSfree(_subst);

    while (_first_override) {
      Override *tmp = _first_override;

      _first_override = _first_override->next;
      if (TS_RECORDDATATYPE_STRING == tmp->type) {
        TSfree(tmp->data.rec_string);
      }
      delete tmp;
    }
  }

  bool initialize(const std::string &reg, const std::string &sub, const std::string &opt);

  // Profiling output for one rule, as a percentage of this instance's matches.
  void
  print(int ix, int total_hits, int rule_hits, const char *now) const
  {
    fprintf(stderr, "[%s]:    Regex %d ( %s ): %.2f%%\n", now, ix, _rex_string, 100.0 * rule_hits / total_hits);
  }

  // Returns '0' on success
  int compile(std::string &error, int &erroffset);

  // number of matches, or negative if failed
  int
  match(std::string_view const str, RegexMatches &matches, RegexMatchContext const *match_context) const
  {
    int const stat = _rex.exec(str, matches, 0, match_context);
    if (0 <= stat) {
      Dbg(dbg_ctl, "Regex match (%d): %.*s", stat, (int)str.length(), str.data());
      return matches.size();
    }
    return stat;
  }

  // Substitutions
  int get_lengths(RegexMatches const &matches, int lengths[], TSRemapRequestInfo *rri, UrlComponents *req_url) const;
  int substitute(char dest[], RegexMatches const &matches, const int lengths[], TSHttpTxn txnp, TSRemapRequestInfo *rri,
                 UrlComponents *req_url, bool lowercase_substitutions) const;

  // Various getters
  inline const char *
  regex() const
  {
    return _rex_string;
  }
  inline bool
  regex_empty() const
  {
    return !_rex_string || !*_rex_string;
  }
  inline TSHttpStatus
  status_option() const
  {
    return _status;
  }
  inline int
  active_timeout_option() const
  {
    return _active_timeout;
  }
  inline int
  no_activity_timeout_option() const
  {
    return _no_activity_timeout;
  }
  inline int
  connect_timeout_option() const
  {
    return _connect_timeout;
  }
  inline int
  dns_timeout_option() const
  {
    return _dns_timeout;
  }
  inline bool
  lowercase_substitutions_option() const
  {
    return _lowercase_substitutions;
  }
  inline bool
  has_strategy() const
  {
    return _has_strategy;
  }
  inline std::string const &
  strategy() const
  {
    return _strategy;
  }

  // Hold an overridable configurations
  struct Override {
    TSOverridableConfigKey key;
    TSRecordDataType       type;
    TSRecordData           data;
    int                    data_len; // Used when data is a string
    Override              *next;
  };

  Override const *
  get_overrides() const
  {
    return _first_override;
  }

private:
  char *_rex_string              = nullptr;
  char *_subst                   = nullptr;
  int   _subst_len               = 0;
  int   _num_subs                = -1;
  int   _options                 = 0;
  bool  _lowercase_substitutions = false;

  Regex        _rex;
  TSHttpStatus _status = static_cast<TSHttpStatus>(0);

  int _active_timeout      = -1;
  int _no_activity_timeout = -1;
  int _connect_timeout     = -1;
  int _dns_timeout         = -1;

  bool        _has_strategy = false;
  std::string _strategy     = {};

  Override *_first_override = nullptr;
  int       _sub_pos[MAX_SUBS];
  int       _sub_ix[MAX_SUBS];
};

bool
RemapRegex::initialize(const std::string &reg, const std::string &sub, const std::string &opt)
{
  if (!reg.empty()) {
    _rex_string = TSstrdup(reg.c_str());
  }

  if (!sub.empty()) {
    _subst     = TSstrdup(sub.c_str());
    _subst_len = sub.length();
  }

  memset(_sub_pos, 0, sizeof(_sub_pos));
  memset(_sub_ix, 0, sizeof(_sub_ix));

  // Parse options
  std::string::size_type start = opt.find_first_of('@');
  std::string::size_type pos1, pos2;
  Override              *last_override = nullptr;

  while (start != std::string::npos) {
    std::string opt_val;

    ++start;
    pos1 = opt.find_first_of('=', start);
    pos2 = opt.find_first_of(" \t\n", pos1);
    if (pos2 == std::string::npos) {
      pos2 = opt.length();
    }

    if (pos1 != std::string::npos) {
      // Get the value as well
      ++pos1;
      opt_val = opt.substr(pos1, pos2 - pos1);
    }

    // These take an option 0|1 value, without value it implies 1
    if (opt.compare(start, 8, "caseless") == 0) {
      _options |= RE_CASE_INSENSITIVE;
    } else if (opt.compare(start, 23, "lowercase_substitutions") == 0) {
      _lowercase_substitutions = true;
    } else if (opt.compare(start, 8, "strategy") == 0) {
      _has_strategy = true;
      _strategy     = std::move(opt_val);
    } else if (opt_val.size() <= 0) {
      // All other options have a required value
      TSError("[%s] Malformed options: %s", PLUGIN_NAME, opt.c_str());
      break;
    } else if (opt.compare(start, 6, "status") == 0) {
      _status = static_cast<TSHttpStatus>(strtol(opt_val.c_str(), nullptr, 10));
    } else if (opt.compare(start, 14, "active_timeout") == 0) {
      _active_timeout = strtol(opt_val.c_str(), nullptr, 10);
    } else if (opt.compare(start, 19, "no_activity_timeout") == 0) {
      _no_activity_timeout = strtol(opt_val.c_str(), nullptr, 10);
    } else if (opt.compare(start, 15, "connect_timeout") == 0) {
      _connect_timeout = strtol(opt_val.c_str(), nullptr, 10);
    } else if (opt.compare(start, 11, "dns_timeout") == 0) {
      _dns_timeout = strtol(opt_val.c_str(), nullptr, 10);
    } else {
      TSOverridableConfigKey key;
      TSRecordDataType       type;
      std::string            opt_name = opt.substr(start, pos1 - start - 1);

      if (TS_SUCCESS == TSHttpTxnConfigFind(opt_name.c_str(), opt_name.length(), &key, &type)) {
        std::unique_ptr<Override> cur(new Override);

        TSReleaseAssert(cur.get());
        switch (type) {
        case TS_RECORDDATATYPE_INT:
          cur->data.rec_int = strtoll(opt_val.c_str(), nullptr, 10);
          break;
        case TS_RECORDDATATYPE_FLOAT:
          cur->data.rec_float = strtof(opt_val.c_str(), nullptr);
          break;
        case TS_RECORDDATATYPE_STRING:
          cur->data.rec_string = TSstrdup(opt_val.c_str());
          cur->data_len        = opt_val.size();
          break;
        default:
          TSError("[%s] configuration variable '%s' is of an unsupported type", PLUGIN_NAME, opt_name.c_str());
          return false;
        }
        Dbg(dbg_ctl, "Overridable config %s=%s", opt_name.c_str(), opt_val.c_str());
        cur->key  = key;
        cur->type = type;
        cur->next = nullptr;
        auto tmp  = cur.get();
        if (nullptr == last_override) {
          _first_override = cur.release();
        } else {
          last_override->next = cur.release();
        }
        last_override = tmp;
      } else {
        TSError("[%s] Unknown options: %s", PLUGIN_NAME, opt.c_str());
      }
    }
    start = opt.find_first_of('@', pos2);
  }

  return true;
}

// Compile and study the regular expression.
int
RemapRegex::compile(std::string &error, int &erroffset)
{
  // Initialize these in case they are not set.
  error     = "unknown error";
  erroffset = -1;

  bool const restat = _rex.compile(_rex_string, error, erroffset, _options);
  if (!restat) {
    TSError("[%s] Error compiling : %s", PLUGIN_NAME, _rex_string);
    return -1;
  }

  int32_t const ccount = _rex.get_capture_count();
  if (ccount < 0) {
    error = "Failure to get capture count for Regex";
    return -1;
  }

  // Get some info for the string substitutions
  char *str = _subst;
  _num_subs = 0;

  while (str && *str) {
    if ('$' == *str) {
      int ix = -1;

      if (isdigit(*(str + 1))) {
        ix = *(str + 1) - '0';
      } else {
        switch (*(str + 1)) {
        case 'h':
          ix = SUB_HOST;
          break;
        case 'f':
          ix = SUB_FROM_HOST;
          break;
        case 't':
          ix = SUB_TO_HOST;
          break;
        case 'p':
          ix = SUB_PORT;
          break;
        case 's':
          ix = SUB_SCHEME;
          break;
        case 'P':
          ix = SUB_PATH;
          break;
        case 'l':
          ix = SUB_LOWER_PATH;
          break;
        case 'q':
          ix = SUB_QUERY;
          break;
        case 'i':
          ix = SUB_CLIENT_IP;
          break;
        default:
          break;
        }
      }

      if (ix > -1) {
        if ((ix < 10) && (ix > ccount)) {
          error = "using unavailable captured substring ($n) in substitution";
          return -1;
        }

        _sub_ix[_num_subs]   = ix;
        _sub_pos[_num_subs]  = (str - _subst);
        str                 += 2;
        ++_num_subs;
      } else { // Not a valid substitution character, so just ignore it
        ++str;
      }
    } else {
      ++str;
    }
  }
  return 0;
}

// Get the lengths of the matching string(s), taking into account variable substitutions.
// We also calculate a total length for the new string, which is the max length the
// substituted string can have (used for the ts::LocalBuffer).
int
RemapRegex::get_lengths(RegexMatches const &matches, int lengths[], TSRemapRequestInfo *rri, UrlComponents *req_url) const
{
  int len = _subst_len + 1; // Bigger then necessary

  for (int i = 0; i < _num_subs; i++) {
    int ix = _sub_ix[i];

    if (ix < 10) {
      lengths[ix]  = matches[ix].length();
      len         += lengths[ix];
    } else {
      int tmp_len;

      switch (ix) {
      case SUB_HOST:
        len += req_url->host_len;
        break;
      case SUB_FROM_HOST:
        TSUrlHostGet(rri->requestBufp, rri->mapFromUrl, &tmp_len);
        len += tmp_len;
        break;
      case SUB_TO_HOST:
        TSUrlHostGet(rri->requestBufp, rri->mapToUrl, &tmp_len);
        len += tmp_len;
        break;
      case SUB_PORT:
        len += 6; // One extra for snprintf()
        break;
      case SUB_SCHEME:
        len += req_url->scheme_len;
        break;
      case SUB_PATH:
      case SUB_LOWER_PATH:
        len += req_url->path_len;
        break;
      case SUB_QUERY:
        len += req_url->query_len;
        break;
      case SUB_CLIENT_IP:
        len += INET6_ADDRSTRLEN;
        break;
      default:
        break;
      }
    }
  }

  return len;
}

// Perform substitution on the $0 - $9 variables in the "src" string. $0 is the entire
// regex that was matches, while $1 - $9 are the corresponding groups. Return the final
// length of the string as written to dest (not including the trailing '0').
int
RemapRegex::substitute(char dest[], RegexMatches const &matches, const int lengths[], TSHttpTxn txnp, TSRemapRequestInfo *rri,
                       UrlComponents *req_url, bool lowercase_substitutions) const
{
  if (_num_subs > 0) {
    char *p1   = dest;
    char *p2   = _subst;
    int   prev = 0;

    for (int i = 0; i < _num_subs; i++) {
      char *start = p1;
      int   ix    = _sub_ix[i];

      memcpy(p1, p2, _sub_pos[i] - prev);
      p1 += (_sub_pos[i] - prev);
      if (ix < 10) {
        memcpy(p1, matches[ix].data(), matches[ix].length());
        p1 += lengths[ix];
      } else {
        char        buff[INET6_ADDRSTRLEN];
        const char *str = nullptr;
        int         len = 0;

        switch (ix) {
        case SUB_HOST:
          str = req_url->host;
          len = req_url->host_len;
          break;
        case SUB_FROM_HOST:
          str = TSUrlHostGet(rri->requestBufp, rri->mapFromUrl, &len);
          break;
        case SUB_TO_HOST:
          str = TSUrlHostGet(rri->requestBufp, rri->mapToUrl, &len);
          break;
        case SUB_PORT:
          p1 += snprintf(p1, 6, "%u", req_url->port);
          break;
        case SUB_SCHEME:
          str = req_url->scheme;
          len = req_url->scheme_len;
          break;
        case SUB_PATH:
        case SUB_LOWER_PATH:
          str = req_url->path;
          len = req_url->path_len;
          break;
        case SUB_QUERY:
          str = req_url->query;
          len = req_url->query_len;
          break;
        case SUB_CLIENT_IP:
          str = ats_ip_ntop(TSHttpTxnClientAddrGet(txnp), buff, INET6_ADDRSTRLEN);
          len = strlen(str);
          break;
        default:
          break;
        }
        // If one of the rules fetched a read-only string, copy it in.
        if (str && len > 0) {
          memcpy(p1, str, len);
          p1 += len;
        }
      }
      p2   += (_sub_pos[i] - prev + 2);
      prev  = _sub_pos[i] + 2;

      if (lowercase_substitutions == true || ix == SUB_LOWER_PATH) {
        while (start < p1) {
          *start = tolower(*start);
          start++;
        }
      }
    }

    memcpy(p1, p2, _subst_len - (p2 - _subst));
    p1  += _subst_len - (p2 - _subst);
    *p1  = 0; // Make sure it's NULL terminated (for safety).
    return p1 - dest;
  } else {
    memcpy(dest, _subst, _subst_len + 1); // No substitutions in the string, copy it all
    return _subst_len;
  }

  return 0; // Shouldn't happen.
}

///////////////////////////////////////////////////////////////////////////////
// One immutable generation of a regex_remap rule file. The exact source bytes
// are retained so a reload can distinguish changed content while the previous
// generation remains live.
//
class RuleSet
{
public:
  using Rules = std::vector<std::unique_ptr<RemapRegex const>>;

  explicit RuleSet(std::string const &source) : _source(source) {}

  bool
  has_source(std::string const &source) const
  {
    // Remap reloads build the new table before releasing the old one. Compare
    // exact bytes so a still-live generation for this filename cannot give new
    // instances stale rules after the file changes.
    return _source == source;
  }

  void
  add(std::unique_ptr<RemapRegex> rule)
  {
    _rules.push_back(std::move(rule));
  }

  Rules const &
  rules() const
  {
    return _rules;
  }

private:
  std::string _source;
  Rules       _rules;
};

using SharedRuleSet = std::shared_ptr<RuleSet const>;

///////////////////////////////////////////////////////////////////////////////
// Compile and publish one immutable rule-file generation.
//
SharedRuleSet
compile_rule_set(std::string const &filename, std::string const &source)
{
  auto               rule_set = std::make_shared<RuleSet>(source);
  std::istringstream input(source);
  int                lineno = 0;
  std::string        line;

  while (getline(input, line)) {
    std::string            regex, subst, options;
    std::string::size_type pos1, pos2;

    ++lineno;
    if (line.empty()) {
      continue;
    }

    pos1 = line.find_first_not_of(" \t\n");
    if (pos1 != std::string::npos) {
      if (line[pos1] == '#') {
        continue;
      }

      pos2 = line.find_first_of(" \t\n", pos1);
      if (pos2 != std::string::npos) {
        regex = line.substr(pos1, pos2 - pos1);
        pos1  = line.find_first_not_of(" \t\n#", pos2);
        if (pos1 != std::string::npos) {
          pos2 = line.find_first_of(" \t\n", pos1);
          if (pos2 == std::string::npos) {
            pos2 = line.length();
          }
          subst = line.substr(pos1, pos2 - pos1);
          pos1  = line.find_first_not_of(" \t\n#", pos2);
          if (pos1 != std::string::npos) {
            pos2 = line.find_first_of("\n#", pos1);
            if (pos2 == std::string::npos) {
              pos2 = line.length();
            }
            options = line.substr(pos1, pos2 - pos1);
          }
        }
      }
    }

    if (regex.empty()) {
      TSError("[%s] no regexp found in %s: line %d", PLUGIN_NAME, filename.c_str(), lineno);
      continue;
    }
    if (subst.empty() && options.empty()) {
      TSError("[%s] no substitution string found in %s: line %d", PLUGIN_NAME, filename.c_str(), lineno);
      continue;
    }

    auto cur = std::make_unique<RemapRegex>();

    if (!cur->initialize(regex, subst, options)) {
      TSError("[%s] can't create a new regex remap rule", PLUGIN_NAME);
      continue;
    }

    std::string error;
    int         erroffset;
    Dbg(dbg_ctl, "Compiling regex: %s", regex.c_str());
    if (0 != cur->compile(error, erroffset)) {
      std::ostringstream oss;
      oss << '[' << PLUGIN_NAME << "] Regex compile failed in " << filename << " (line " << lineno << ')';
      if (erroffset > 0) {
        oss << " at offset " << erroffset;
      }
      oss << ": " << error;
      if (cur->regex_empty()) {
        oss << "  (no regular expression)";
      } else {
        oss << "  regex: \"" << cur->regex() << '"';
      }
      TSError("%s", oss.str().c_str());
      continue;
    }

    Dbg(dbg_ctl, "Added regex=%s with subs=%s and options `%s'", regex.c_str(), subst.c_str(), options.c_str());
    rule_set->add(std::move(cur));
  }

  if (rule_set->rules().empty()) {
    TSError("[%s] no regular expressions from the maps", PLUGIN_NAME);
    return nullptr;
  }

  return rule_set;
}

class RuleSetCache
{
  /// Live generations of one rule file. More than one entry exists only while
  /// an old config generation is still draining and the file content changed.
  /// Weak ownership is deliberate: the cache must never keep a RuleSet alive.
  using Generations = std::vector<std::weak_ptr<RuleSet const>>;
  using Entries     = std::unordered_map<std::string, Generations>;

public:
  static SharedRuleSet
  get(std::string const &filename, std::string const &source)
  {
    std::lock_guard<std::mutex> lock(mutex());
    auto                       &entries = cache();

    auto existing = entries.find(filename);
    if (existing != entries.end()) {
      std::erase_if(existing->second, [](std::weak_ptr<RuleSet const> const &entry) { return entry.expired(); });
      for (auto const &entry : existing->second) {
        if (auto rule_set = entry.lock(); rule_set && rule_set->has_source(source)) {
          Dbg(dbg_ctl, "Reusing cached regular expressions from %s", filename.c_str());
          return rule_set;
        }
      }
      if (existing->second.empty()) {
        entries.erase(existing);
      }
    }

    // Pruning is bookkeeping, not correctness: lock() above cannot return an
    // expired generation. Keep the full sweep off the once-per-mapping hit path.
    prune_expired(entries);

    // Compile with the lock held. Concurrent instances of the same source must
    // serialize here, or each would compile a copy and all but one would be wasted.
    auto rule_set = compile_rule_set(filename, source);
    if (rule_set) {
      entries[filename].push_back(rule_set);
      Dbg(dbg_ctl, "Cached regular expressions from %s", filename.c_str());
    }
    return rule_set;
  }

private:
  static void
  prune_expired(Entries &entries)
  {
    for (auto file = entries.begin(); file != entries.end();) {
      std::erase_if(file->second, [](std::weak_ptr<RuleSet const> const &entry) { return entry.expired(); });
      if (file->second.empty()) {
        file = entries.erase(file);
      } else {
        ++file;
      }
    }
  }

  static Entries &
  cache()
  {
    static Entries entries;
    return entries;
  }

  static std::mutex &
  mutex()
  {
    static std::mutex lock;
    return lock;
  }
};

// Hold one remap instance
struct RemapInstance {
  RemapInstance() : filename("unknown") {}

  SharedRuleSet     rule_set;
  std::vector<int>  rule_hits;
  RegexMatchContext match_context = {};
  bool              pristine_url  = false;
  bool              profile       = false;
  bool              method        = false;
  bool              query_string  = true;
  bool              host          = false;
  int               hits          = 0;
  int               misses        = 0;
  int               failures      = 0;
  std::string       filename;
};

///////////////////////////////////////////////////////////////////////////////
// Initialize the plugin.
//
TSReturnCode
TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
{
  CHECK_REMAP_API_COMPATIBILITY(api_info, errbuf, errbuf_size);
  Dbg(dbg_ctl, "Plugin is successfully initialized");
  return TS_SUCCESS;
}

///////////////////////////////////////////////////////////////////////////////
// We don't have any specific "instances" here, at least not yet.
//
TSReturnCode
TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSED */, int /* errbuf_sizeATS_UNUSED */)
{
  RemapInstance *ri = new RemapInstance();

  *ih = (void *)ri;
  if (ri == nullptr) {
    TSError("[%s] Unable to create remap instance", PLUGIN_NAME);
    return TS_ERROR;
  }

  if (argc < 3) {
    TSError("[%s] missing configuration file", PLUGIN_NAME);
    return TS_ERROR;
  }

  // Really simple (e.g. basic) config parser
  for (int i = 3; i < argc; ++i) {
    if (strncmp(argv[i], "profile", 7) == 0) {
      ri->profile = true;
    } else if (strncmp(argv[i], "no-profile", 10) == 0) {
      ri->profile = false;
    } else if (strncmp(argv[i], "method", 6) == 0) {
      ri->method = true;
    } else if (strncmp(argv[i], "no-method", 9) == 0) {
      ri->method = false;
    } else if (strncmp(argv[i], "query-string", 12) == 0) {
      ri->query_string = true;
    } else if (strncmp(argv[i], "no-query-string", 15) == 0) {
      ri->query_string = false;
    } else if (strncmp(argv[i], "host", 4) == 0) {
      ri->host = true;
    } else if (strncmp(argv[i], "no-host", 7) == 0) {
      ri->host = false;
    } else if (strcmp(argv[i], "pristine") == 0) {
      ri->pristine_url = true;
    } else if (strcmp(argv[i], "no-pristine") == 0) {
      ri->pristine_url = false;
    } else {
      TSError("[%s] invalid option '%s'", PLUGIN_NAME, argv[i]);
    }
  }

  if (*argv[2] == '/') {
    // Absolute path, just use it.
    ri->filename = argv[2];
  } else {
    // Relative path. Make it relative to the configuration directory.
    ri->filename  = TSConfigDirGet();
    ri->filename += "/";
    ri->filename += argv[2];
  }

  struct stat st;
  if (0 != stat(ri->filename.c_str(), &st)) {
    TSError("[%s] failed to stat %s: %s", PLUGIN_NAME, ri->filename.c_str(), strerror(errno));
    return TS_ERROR;
  }
  if (!S_ISREG(st.st_mode)) {
    TSError("[%s] %s is not a regular file", PLUGIN_NAME, ri->filename.c_str());
    return TS_ERROR;
  }

  std::ifstream f((ri->filename).c_str(), std::ios::in | std::ios::binary);
  if (!f.is_open()) {
    TSError("[%s] unable to open %s", PLUGIN_NAME, (ri->filename).c_str());
    return TS_ERROR;
  }
  Dbg(dbg_ctl, "Loading regular expressions from %s", (ri->filename).c_str());

  std::streamsize const expected_size = st.st_size;
  std::string           source(static_cast<size_t>(expected_size), '\0');
  f.read(source.data(), expected_size);
  if (f.bad() || f.gcount() != expected_size) {
    TSError("[%s] short read on %s: got %lld of %lld bytes", PLUGIN_NAME, ri->filename.c_str(), static_cast<long long>(f.gcount()),
            static_cast<long long>(expected_size));
    return TS_ERROR;
  }

  ri->rule_set = RuleSetCache::get(ri->filename, source);
  if (!ri->rule_set) {
    return TS_ERROR;
  }
  ri->match_context.set_match_limit(REGEX_MATCH_LIMIT);
  if (ri->profile) {
    ri->rule_hits.resize(ri->rule_set->rules().size());
  }

  return TS_SUCCESS;
}

void
TSRemapDeleteInstance(void *ih)
{
  Dbg(dbg_ctl, "TSRemapDeleteInstance");
  RemapInstance *ri = static_cast<RemapInstance *>(ih);

  if (ri->profile) {
    char             now[64];
    const ink_time_t tim = time(nullptr);

    if (ink_ctime_r(&tim, now)) {
      now[strlen(now) - 1] = '\0';
    } else {
      strncpy(now, "unknown time", sizeof(now));
    }

    fprintf(stderr, "[%s]: Profiling information for regex_remap file `%s':\n", now, (ri->filename).c_str());
    fprintf(stderr, "[%s]:    Total hits (matches): %d\n", now, ri->hits);
    fprintf(stderr, "[%s]:    Total missed (no regex matches): %d\n", now, ri->misses);
    fprintf(stderr, "[%s]:    Total regex internal errors: %d\n", now, ri->failures);

    if (ri->hits > 0) { // Avoid divide by zeros...
      auto const &rules = ri->rule_set->rules();
      for (size_t ix = 0; ix < rules.size(); ++ix) {
        rules[ix]->print(ix + 1, ri->hits, ri->rule_hits[ix], now);
      }
    }
  }

  delete ri;
}

///////////////////////////////////////////////////////////////////////////////
// This is the main "entry" point for the plugin, called for every request.
//
TSRemapStatus
TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri)
{
  if (nullptr == ih) {
    Dbg(dbg_ctl, "Falling back to default URL on regex remap without rules");
    return TSREMAP_NO_REMAP;
  }
  RemapInstance *ri = static_cast<RemapInstance *>(ih);
  if (!ri->rule_set) {
    Dbg(dbg_ctl, "No rule set on this instance, skipping");
    return TSREMAP_NO_REMAP;
  }

  struct SrcUrl {
    TSMBuffer bufp;
    TSMLoc    loc;
    bool      bad;
  };

  const SrcUrl src_url([=]() -> SrcUrl {
    SrcUrl u;

    if (!ri->pristine_url) {
      u.bufp = rri->requestBufp;
      u.loc  = rri->requestUrl;
      u.bad  = false;

    } else {
      u.bad = TSHttpTxnPristineUrlGet(txnp, &u.bufp, &u.loc) != TS_SUCCESS;
    }
    return u;
  }());

  if (src_url.bad) {
    return TSREMAP_NO_REMAP;
  }

  // Populate the request url
  UrlComponents req_url;
  req_url.populate(src_url.bufp, src_url.loc);

  int           lengths[MATCHCOUNT + 1];
  int           dest_len;
  TSRemapStatus retval    = TSREMAP_NO_REMAP;
  int           match_len = 0;

  // Cap the stack allocation to 16KB, a typical browser upper limit
  ts::LocalBuffer<char, 16384> match_buf(req_url.url_len + 32);

  if (ri->method) { // Prepend the URI path or URL with the HTTP method
    TSMBuffer   mBuf;
    TSMLoc      reqHttpHdrLoc;
    const char *method;

    // Note that Method can not be longer than 16 bytes, or we'll simply truncate it
    if (TS_SUCCESS == TSHttpTxnClientReqGet(static_cast<TSHttpTxn>(txnp), &mBuf, &reqHttpHdrLoc)) {
      method = TSHttpHdrMethodGet(mBuf, reqHttpHdrLoc, &match_len);
      if (method && (match_len > 0)) {
        if (match_len > 16) {
          match_len = 16;
        }
        memcpy(match_buf.data(), method, match_len);
      }
    }
  }

  if (ri->host && req_url.host && req_url.host_len > 0) {
    memcpy(match_buf.data() + match_len, "//", 2);
    memcpy(match_buf.data() + match_len + 2, req_url.host, req_url.host_len);
    match_len += (req_url.host_len + 2);
  }

  *(match_buf.data() + match_len) = '/';
  match_len++;
  if (req_url.path && req_url.path_len > 0) {
    memcpy(match_buf.data() + match_len, req_url.path, req_url.path_len);
    match_len += (req_url.path_len);
  }

  if (ri->query_string && req_url.query && req_url.query_len > 0) {
    *(match_buf.data() + match_len) = '?';
    memcpy(match_buf.data() + match_len + 1, req_url.query, req_url.query_len);
    match_len += (req_url.query_len + 1);
  }
  match_buf.data()[match_len] = '\0'; // NULL terminate the match string
  Dbg(dbg_ctl, "Target match string is `%s'", match_buf.data());

  RegexMatches matches(MATCHCOUNT);

  auto const &rules = ri->rule_set->rules();

  // Apply the rules in file order; the first one that matches wins. This relies
  // on get_lengths() always returning a positive length for a matching rule. If
  // that changes, a match could fall through after applying its options and let
  // a later rule stack its options on top.
  for (size_t rule_ix = 0; rule_ix < rules.size(); ++rule_ix) {
    auto const &re = rules[rule_ix];

    // Since we check substitutions on parse time, we don't need to reset ovector
    auto match_result = re->match(match_buf.data(), matches, &(ri->match_context));
    if (match_result >= 0) {
      int new_len = re->get_lengths(matches, lengths, rri, &req_url);

      // Set timeouts
      if (re->active_timeout_option() > (-1)) {
        Dbg(dbg_ctl, "Setting active timeout to %d", re->active_timeout_option());
        TSHttpTxnActiveTimeoutSet(txnp, re->active_timeout_option());
      }
      if (re->no_activity_timeout_option() > (-1)) {
        Dbg(dbg_ctl, "Setting no activity timeout to %d", re->no_activity_timeout_option());
        TSHttpTxnNoActivityTimeoutSet(txnp, re->no_activity_timeout_option());
      }
      if (re->connect_timeout_option() > (-1)) {
        Dbg(dbg_ctl, "Setting connect timeout to %d", re->connect_timeout_option());
        TSHttpTxnConnectTimeoutSet(txnp, re->connect_timeout_option());
      }
      if (re->dns_timeout_option() > (-1)) {
        Dbg(dbg_ctl, "Setting DNS timeout to %d", re->dns_timeout_option());
        TSHttpTxnDNSTimeoutSet(txnp, re->dns_timeout_option());
      }
      if (re->has_strategy()) {
        auto const &strat = re->strategy();
        if (strat.empty() || "null" == strat) {
          Dbg(dbg_ctl, "Clearing strategy (use parent.config)");
          TSHttpTxnNextHopStrategySet(txnp, nullptr);
        } else {
          void const *const stratptr = TSHttpTxnNextHopNamedStrategyGet(txnp, strat.c_str());
          if (nullptr == stratptr) {
            Dbg(dbg_ctl, "No strategy found with name '%s'", strat.c_str());
          } else {
            Dbg(dbg_ctl, "Setting strategy to %s", strat.c_str());
            TSHttpTxnNextHopStrategySet(txnp, stratptr);
          }
        }
      }
      bool lowercase_substitutions = false;
      if (re->lowercase_substitutions_option() == true) {
        Dbg(dbg_ctl, "Setting lowercasing substitutions on");
        lowercase_substitutions = true;
      }

      RemapRegex::Override const *override = re->get_overrides();

      while (override) {
        switch (override->type) {
        case TS_RECORDDATATYPE_INT:
          TSHttpTxnConfigIntSet(txnp, override->key, override->data.rec_int);
          Dbg(dbg_ctl, "Setting config id %d to `%" PRId64 "'", override->key, override->data.rec_int);
          break;
        case TS_RECORDDATATYPE_FLOAT:
          TSHttpTxnConfigFloatSet(txnp, override->key, override->data.rec_float);
          Dbg(dbg_ctl, "Setting config id %d to `%f'", override->key, override->data.rec_float);
          break;
        case TS_RECORDDATATYPE_STRING:
          TSHttpTxnConfigStringSet(txnp, override->key, override->data.rec_string, override->data_len);
          Dbg(dbg_ctl, "Setting config id %d to `%s'", override->key, override->data.rec_string);
          break;
        default:
          break; // Error ?
        }
        override = override->next;
      }

      // Update profiling if requested
      if (ri->profile) {
        ink_atomic_increment(&(ri->rule_hits[rule_ix]), 1);
        ink_atomic_increment(&(ri->hits), 1);
      }

      if (new_len > 0) {
        retval = TSREMAP_DID_REMAP;

        // Cap the stack allocation to 16KB, a typical browser upper limit
        ts::LocalBuffer<char, 16384> dest(new_len + 8);

        dest_len = re->substitute(dest.data(), matches, lengths, txnp, rri, &req_url, lowercase_substitutions);

        Dbg(dbg_ctl, "New URL is estimated to be %d bytes long, or less", new_len);
        Dbg(dbg_ctl, "New URL is %s (length %d)", dest.data(), dest_len);
        Dbg(dbg_ctl, "    matched rule %zu [%s]", rule_ix + 1, re->regex());

        // Check for a quick response, if the status option is set
        if (re->status_option() > 0) {
          if (re->status_option() != TS_HTTP_STATUS_MOVED_PERMANENTLY && re->status_option() != TS_HTTP_STATUS_MOVED_TEMPORARILY &&
              re->status_option() != TS_HTTP_STATUS_TEMPORARY_REDIRECT &&
              re->status_option() != TS_HTTP_STATUS_PERMANENT_REDIRECT) {
            // Don't set the URL / Location for this.
            TSHttpTxnStatusSet(txnp, re->status_option(), PLUGIN_NAME);
            break;
          }

          Dbg(dbg_ctl, "Redirecting URL, status=%d", re->status_option());
          TSHttpTxnStatusSet(txnp, re->status_option(), PLUGIN_NAME);
          rri->redirect = 1;
        }

        // Now parse the new URL, which can also be the redirect URL
        if (dest_len > 0) {
          const char *start = dest.data();

          // Setup the new URL
          if (TS_PARSE_ERROR == TSUrlParse(rri->requestBufp, rri->requestUrl, &start, start + dest_len)) {
            TSHttpTxnStatusSet(txnp, TS_HTTP_STATUS_INTERNAL_SERVER_ERROR, PLUGIN_NAME);
            TSError("[%s] can't parse substituted URL string", PLUGIN_NAME);
          }
        }
        break;
      }
    } else if (match_result != -1) {
      ink_atomic_increment(&(ri->failures), 1);
      std::string const errmsg = Regex::get_error_string(match_result);
      TSError(R"([%s] Bad regular expression result %d ("%s") from "%s" in file "%s".)", PLUGIN_NAME, match_result, errmsg.c_str(),
              re->regex(), ri->filename.c_str());
    }
  }

  if (retval == TSREMAP_NO_REMAP && ri->profile) {
    ink_atomic_increment(&(ri->misses), 1);
  }

  return retval;
}
