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

  Copyright 2019, Oath Inc.
*/

#include <string>
#include <map>
#include <numeric>
#include <glob.h>

#include <swoc/TextView.h>
#include <swoc/MemSpan.h>
#include <swoc/swoc_file.h>
#include <swoc/bwf_std.h>
#include <swoc/Scalar.h>
#include <yaml-cpp/yaml.h>

#include "txn_box/Directive.h"
#include "txn_box/Extractor.h"
#include "txn_box/Modifier.h"
#include "txn_box/Expr.h"
#include "txn_box/Config.h"
#include "txn_box/Context.h"

#include "txn_box/ts_util.h"
#include "txn_box/yaml_util.h"

using swoc::BufferWriter;
using swoc::Errata;
using swoc::Rv;
using swoc::TextView;
namespace bwf = swoc::bwf;
using namespace swoc::literals;

static constexpr char ARG_PREFIX = '<';
static constexpr char ARG_SUFFIX = '>';

/* ------------------------------------------------------------------------------------ */
swoc::Lexicon<Hook> HookName{
  {{Hook::POST_LOAD, {"post-load"}},
   {Hook::TXN_START, {"txn-open"}},
   {Hook::CREQ, {"ua-req", "creq", "client-req"}},
   {Hook::PREQ, {"proxy-req", "preq", "upstream-req", "up-req"}},
   {Hook::URSP, {"upstream-rsp", "ursp", "up-rsp"}},
   {Hook::PRSP, {"proxy-rsp", "prsp"}},
   {Hook::PRE_REMAP, {"pre-remap"}},
   {Hook::POST_REMAP, {"post-remap"}},
   {Hook::TXN_CLOSE, {"txn-close"}},
   {Hook::REMAP, {"remap"}},
   {Hook::MSG, {"msg"}}}
};

std::array<TSHttpHookID, std::tuple_size<Hook>::value> TS_Hook;

BufferWriter &
bwformat(BufferWriter &w, bwf::Spec const &spec, Hook hook)
{
  if (spec.has_numeric_type()) {
    return bwformat(w, spec, IndexFor(hook));
  }
  return bwformat(w, spec, HookName[hook]);
}

namespace
{
[[maybe_unused]] bool INITIALIZED = []() -> bool {
  HookName.set_default(Hook::INVALID);

  TS_Hook[IndexFor(Hook::TXN_START)]  = TS_HTTP_TXN_START_HOOK;
  TS_Hook[IndexFor(Hook::CREQ)]       = TS_HTTP_READ_REQUEST_HDR_HOOK;
  TS_Hook[IndexFor(Hook::PREQ)]       = TS_HTTP_SEND_REQUEST_HDR_HOOK;
  TS_Hook[IndexFor(Hook::URSP)]       = TS_HTTP_READ_RESPONSE_HDR_HOOK;
  TS_Hook[IndexFor(Hook::PRSP)]       = TS_HTTP_SEND_RESPONSE_HDR_HOOK;
  TS_Hook[IndexFor(Hook::PRE_REMAP)]  = TS_HTTP_PRE_REMAP_HOOK;
  TS_Hook[IndexFor(Hook::POST_REMAP)] = TS_HTTP_POST_REMAP_HOOK;
  TS_Hook[IndexFor(Hook::TXN_CLOSE)]  = TS_HTTP_TXN_CLOSE_HOOK;

  return true;
}();
}; // namespace

swoc::Lexicon<ValueType> const ValueTypeNames{
  {{ValueType::NIL, "nil"},
   {ValueType::STRING, "string"},
   {ValueType::INTEGER, "integer"},
   {ValueType::BOOLEAN, "boolean"},
   {ValueType::FLOAT, "float"},
   {ValueType::IP_ADDR, "IP address"},
   {ValueType::DURATION, "duration"},
   {ValueType::TIMEPOINT, "time point"},
   {ValueType::CONS, "cons"},
   {ValueType::TUPLE, "tuple"},
   {ValueType::GENERIC, "generic"}}
};

// --------------------------------------------------------------------------
Config::~Config()
{
  // Invoke all the finalizers to do additional cleanup.
  for (auto &&f : _finalizers) {
    f._f(f._ptr);
    std::destroy_at(&f._f); // clean up the cleaner too, just in case.
  }
}

template <typename F> struct on_scope_exit {
  F _f;
  explicit on_scope_exit(F &&f) : _f(std::move(f)) {}
  ~on_scope_exit() { _f(); }
};

swoc::Rv<swoc::TextView>
parse_arg(TextView &key)
{
  TextView arg{key};
  TextView name{arg.take_prefix_at(ARG_PREFIX)};
  if (name.size() == key.size()) { // no arg prefix, it's just the name.
    return {};
  }
  if (arg.empty() || arg.back() != ARG_SUFFIX) {
    return Errata(S_ERROR, R"(Argument for "{}" is not properly terminated with '{}'.)", name, ARG_SUFFIX);
  }
  key = name;
  return arg.remove_suffix(1);
}

/* ------------------------------------------------------------------------------------ */
Config::Config() : _arena(_cfg_storage_required + 2048)
{
  _cfg_store = _arena.alloc(_cfg_storage_required);
  // Set up the run time type information for the directives.
  _drtv_info = this->alloc_span<Directive::CfgStaticData>(_factory.size());
  for (auto const &[name, factory_info] : _factory) {
    auto &di = _drtv_info[factory_info._idx];
    new (&di) Directive::CfgStaticData;
    di._static = &factory_info;
  }
}

swoc::MemSpan<void>
Config::allocate_cfg_storage(size_t n, size_t align)
{
  if (align == 1) {
    return _arena.alloc(n);
  }

  // Need to add this logic to @c MemArena - shouldn't have to do it here.
  // Should only loop twice at most - if it doesn't return it requires the arena to have
  // enough free space to work on the second try.
  while (true) {
    auto span = _arena.remnant();
    if (auto remainder = reinterpret_cast<uintptr_t>(span.data()) % align; remainder) {
      auto pad = align - remainder;
      if (span.size() >= n + pad) {
        return _arena.alloc(n + pad).remove_prefix(pad);
      }
      _arena.require(n + pad); // Guarantee enough space to align correctly.
    } else if (span.size() >= n) {
      return _arena.alloc(n); // already aligned and big enough.
    } else {
      _arena.require(n + align); // bump up to make sure new space can be aligned.
    }
  }
}

ReservedSpan
Config::reserve_ctx_storage(size_t n)
{
  using Align = swoc::Scalar<8>;
  // Pre-block to store status of the reserved memory.
  _ctx_storage_required += Align(swoc::round_up(sizeof(Context::ReservedStatus)));
  // The actual reservation.
  ReservedSpan span{_ctx_storage_required, n};
  _ctx_storage_required += Align(swoc::round_up(n));
  return span;
}

Config::self_type &
Config::localize(Feature &feature)
{
  std::visit([&](auto &t) { this->localize(t); }, static_cast<Feature::variant_type &>(feature));
  return *this;
}

TextView &
Config::localize(TextView &text, LocalOpt opt)
{
  if (text.size()) {
    if (LOCAL_CSTR == opt) {
      auto span{_arena.alloc(text.size() + 1).rebind<char>()};
      memcpy(span, text);
      span[text.size()] = 0;
      text              = span.subspan(0, text.size());
    } else {
      auto span{_arena.alloc(text.size()).rebind<char>()};
      memcpy(span, text);
      text = span;
    }
  }
  return text;
};

Rv<ActiveType>
Config::validate(Extractor::Spec &spec)
{
  if (spec._name.empty()) {
    return Errata(S_ERROR, R"(Extractor name required but not found.)");
  }

  if (spec._idx < 0) {
    auto name = TextView{spec._name};
    auto &&[arg, arg_errata]{parse_arg(name)};
    if (!arg_errata.is_ok()) {
      return std::move(arg_errata);
    }

    Extractor *ex = nullptr;
    if (_local_extractors) {
      if (auto spot = _local_extractors->find(name); spot != _local_extractors->end()) {
        ex = spot->second;
      }
    }

    if (nullptr == ex) {
      ex = Extractor::find(name);
    }

    if (nullptr != ex) {
      spec._exf  = ex;
      spec._name = this->localize(name);
      spec._ext  = this->localize(spec._ext);
      auto &&[vt, errata]{ex->validate(*this, spec, arg)};
      if (!errata.is_ok()) {
        return std::move(errata);
      }
      return vt;
    }
    return Errata(S_ERROR, R"(Extractor "{}" not found.)", name);
  }
  return {STRING}; // non-negative index => capture group => always a string
}

Rv<Expr>
Config::parse_unquoted_expr(swoc::TextView const &text)
{
  // Integer?
  TextView parsed;
  auto     n = swoc::svtoi(text, &parsed);
  if (parsed.size() == text.size()) {
    return Expr{Feature{n}};
  }

  // bool?
  auto b = BoolNames[text];
  if (b != BoolTag::INVALID) {
    return Expr{Feature{b == BoolTag::True}};
  }

  // float?
  double f = swoc::svtod(text, &parsed);
  if (parsed.size() == text.size()) {
    return Expr{Feature{f}};
  }

  // IP Address?
  swoc::IPAddr addr;
  if (addr.load(text)) {
    return Expr{Feature{addr}};
  }

  // Presume an extractor.
  Extractor::Spec spec;
  bool            valid_p = spec.parse(text);
  if (!valid_p) {
    return Errata(S_ERROR, R"(Invalid syntax for extractor "{}" - not a valid specifier.)", text);
  }
  auto &&[vt, errata] = this->validate(spec);
  if (!errata.is_ok()) {
    return std::move(errata);
  }

  if (vt.is_cfg_const()) {
    return Expr{spec._exf->extract(*this, spec)};
  }

  return Expr{spec, vt};
}

Rv<Expr>
Config::parse_composite_expr(TextView const &text)
{
  ActiveType                   single_vt;
  auto                         parser{swoc::bwf::Format::bind(text)};
  std::vector<Extractor::Spec> specs; // hold the specifiers during parse.
  // Used to handle literals in @a format_string. Can't be const because it must be updated
  // for each literal.
  Extractor::Spec literal_spec;

  literal_spec._type = Extractor::Spec::LITERAL_TYPE;

  while (parser) {
    Extractor::Spec  spec;
    std::string_view literal;
    bool             spec_p = false;
    try {
      spec_p = parser(literal, spec);
    } catch (std::exception const &exp) {
      return Errata(S_ERROR, "Invalid syntax - {}", exp.what());
    } catch (...) {
      return Errata(S_ERROR, "Invalid syntax.");
    }

    if (!literal.empty()) { // leading literal text, add as specifier.
      literal_spec._ext = this->localize(literal, LOCAL_CSTR);
      specs.push_back(literal_spec);
    }

    if (spec_p) { // specifier present.
      if (spec._idx >= 0) {
        specs.push_back(spec); // group reference, store it.
      } else {
        auto &&[vt, errata] = this->validate(spec);
        if (errata.is_ok()) {
          single_vt = vt; // Save for singleton case.
          specs.push_back(spec);
        } else {
          errata.note(R"(While parsing specifier at offset {}.)", text.size() - parser._fmt.size());
          return std::move(errata);
        }
      }
    }
  }

  // If it is a singleton, return it as one of the singleton types.
  if (specs.size() == 1) {
    if (specs[0]._exf) {
      return Expr{specs[0], single_vt};
    } else if (specs[0]._type == Extractor::Spec::LITERAL_TYPE) {
      FeatureView f{specs[0]._ext};
      f._literal_p = true; // because it was localized when parsed from the composite.
      f._cstr_p    = true; // and automatically null terminated.
      return Expr{f};
    }
    // else it's an indexed specifier, treat as a composite.
  }
  // Multiple specifiers, check for overall properties.
  Expr  expr;
  auto &cexpr = expr._raw.emplace<Expr::COMPOSITE>();
  cexpr._specs.swap(specs);

  for (auto const &s : specs) {
    expr._max_arg_idx = std::max(expr._max_arg_idx, s._idx);
  }

  return expr;
}

Rv<Expr>
Config::parse_scalar_expr(YAML::Node node)
{
  Rv<Expr> zret;
  TextView text{node.Scalar()};
  if (node.IsNull()) {
    return Expr{};
  } else if (node.Tag() == "?"_tv) { // unquoted, must be extractor.
    zret = this->parse_unquoted_expr(text);
  } else {
    zret = this->parse_composite_expr(text);
  }

  if (zret.is_ok()) {
    auto &expr = zret.result();
    if (expr._max_arg_idx >= 0) {
      if (_active_capture._count == 0) {
        return Errata(S_ERROR, R"(Regular expression capture group used at {} but no regular expression is active.)", node.Mark());
      } else if (expr._max_arg_idx >= int(_active_capture._count)) {
        return Errata(
          S_ERROR,
          R"(Regular expression capture group {} used at {} but the maximum capture group is {} in the active regular expression from line {}.)",
          expr._max_arg_idx, node.Mark(), _active_capture._count - 1, _active_capture._line);
      }
    }
  }
  return zret;
}

Rv<Expr>
Config::parse_expr_with_mods(YAML::Node node)
{
  auto &&[expr, expr_errata]{this->parse_expr(node[0])};
  if (!expr_errata.is_ok()) {
    expr_errata.note("While processing the expression at {}.", node.Mark());
    return std::move(expr_errata);
  }
  auto scope{this->feature_scope(expr.result_type())};
  for (unsigned idx = 1; idx < node.size(); ++idx) {
    auto child{node[idx]};
    auto &&[mod, mod_errata]{Modifier::load(*this, child, expr.result_type())};
    if (!mod_errata.is_ok()) {
      mod_errata.note(R"(While parsing feature expression at {}.)", child.Mark(), node.Mark());
      return std::move(mod_errata);
    }
    _active_feature._type = mod->result_type(_active_feature._type);
    expr._mods.emplace_back(std::move(mod));
  }

  return std::move(expr);
}

Rv<Expr>
Config::parse_expr(YAML::Node expr_node)
{
  std::string_view expr_tag(expr_node.Tag());

  // This is the base entry method, so it needs to handle all cases, although most of them
  // will be delegated. Handle the direct / simple special cases here.

  if (expr_node.IsNull()) { // explicit NULL
    return Expr{NIL_FEATURE};
  }

  // If explicitly marked a literal, then no further processing should be done.
  if (0 == strcasecmp(expr_tag, LITERAL_TAG)) {
    if (!expr_node.IsScalar()) {
      return Errata(S_ERROR, R"("!{}" tag used on value at {} which is not a string as required for a literal.)", LITERAL_TAG,
                    expr_node.Mark());
    }
    return Expr{FeatureView::Literal(this->localize(expr_node.Scalar()))};
  } else if (0 == strcasecmp(expr_tag, DURATION_TAG)) {
    if (!expr_node.IsScalar()) {
      return Errata(S_ERROR, R"("!{}" tag used on value at {} which is not a string as required for a literal.)", LITERAL_TAG,
                    expr_node.Mark());
    }
    auto &&[dt, dt_errata]{Feature{expr_node.Scalar()}.as_duration()};
    return {Expr(dt), std::move(dt_errata)};
  } else if (0 != strcasecmp(expr_tag, "?"_sv) && 0 != strcasecmp(expr_tag, "!"_sv)) {
    return Errata(S_ERROR, R"("{}" tag for extractor expression is not supported.)", expr_tag);
  }

  if (expr_node.IsScalar()) {
    return this->parse_scalar_expr(expr_node);
  }
  if (!expr_node.IsSequence()) {
    return Errata(S_ERROR, "Feature expression is not properly structured.");
  }

  // It's a sequence, handle the various cases.
  if (expr_node.size() == 0) {
    return Expr{NIL_FEATURE};
  }
  if (expr_node.size() == 1) {
    return this->parse_scalar_expr(expr_node[0]);
  }

  if (expr_node[1].IsMap()) { // base expression with modifiers.
    return this->parse_expr_with_mods(expr_node);
  }

  // Else, after all this, it's a tuple, treat each element as an expression.
  ActiveType        l_types;
  bool              literal_p = true; // Is the entire sequence literal?
  std::vector<Expr> xa;
  xa.reserve(expr_node.size());
  for (auto const &child : expr_node) {
    auto &&[expr, errata]{this->parse_expr(child)};
    if (!errata.is_ok()) {
      errata.note("While parsing feature expression list at {}.", expr_node.Mark());
      return std::move(errata);
    }
    l_types |= expr.result_type().base_types();
    if (!expr.is_literal()) {
      literal_p = false;
    }
    xa.emplace_back(std::move(expr));
  }

  Expr expr;
  if (literal_p) {
    FeatureTuple t   = this->alloc_span<Feature>(xa.size());
    unsigned     idx = 0;
    for (auto &f : t) {
      f = std::get<Expr::LITERAL>(xa[idx++]._raw);
    }
    expr._raw = t;
  } else {
    auto &list  = expr._raw.emplace<Expr::LIST>();
    list._types = l_types;
    list._exprs = std::move(xa);
  }
  return expr;
}

Rv<Directive::Handle>
Config::load_directive(YAML::Node const &drtv_node)
{
  YAML::Node key_node;
  for (auto const &[key_name, key_value] : drtv_node) {
    TextView name{key_name.Scalar()};
    auto &&[arg, arg_errata]{parse_arg(name)};
    if (!arg_errata.is_ok()) {
      return std::move(arg_errata);
    }

    // Ignorable keys in the directive. Currently just one, so hand code it. Make this better
    // if there is ever more than one.
    if (name == Global::DO_KEY) {
      continue;
    }
    // See if this is in the factory. It's not an error if it's not, to enable adding extra
    // keys to directives. First key that is in the factory determines the directive type.
    // If none of the keys are in the factory, that's an error and is reported after the loop.
    if (auto spot{_factory.find(name)}; spot != _factory.end()) {
      auto &info = spot->second;
      auto  rtti = &_drtv_info[info._idx];

      if (!info._hook_mask[IndexFor(this->current_hook())]) {
        return Errata(S_ERROR, R"(Directive "{}" at {} is not allowed on hook "{}".)", name, drtv_node.Mark(),
                      this->current_hook());
      }

      // If this is the first use of the directive, do config level setup for the directive type.
      if (rtti->_count == 0) {
        info._cfg_init_cb(*this, rtti);
      }
      ++(rtti->_count);

      auto &&[drtv, drtv_errata]{info._load_cb(*this, rtti, drtv_node, name, arg, key_value)};
      if (!drtv_errata.is_ok()) {
        drtv_errata.note(R"(While parsing directive at {}.)", drtv_node.Mark());
        return std::move(drtv_errata);
      }
      drtv->_rtti = rtti;

      return std::move(drtv);
    }
  }
  return Errata(S_ERROR, R"(Directive at {} has no recognized tag.)", drtv_node.Mark());
}

Rv<Directive::Handle>
Config::parse_directive(YAML::Node const &drtv_node)
{
  if (drtv_node.IsMap()) {
    return this->load_directive(drtv_node);
  } else if (drtv_node.IsSequence()) {
    Errata            zret;
    auto              list{new DirectiveList};
    Directive::Handle drtv_list{list};
    for (auto child : drtv_node) {
      auto &&[handle, errata]{this->load_directive(child)};
      if (errata.is_ok()) {
        list->push_back(std::move(handle));
      } else {
        errata.note(R"(While loading directives at {}.)", drtv_node.Mark());
        return std::move(errata);
      }
    }
    return drtv_list;
  } else if (drtv_node.IsNull()) {
    return Directive::Handle(new NilDirective);
  }
  return Errata(S_ERROR, R"(Directive at {} is not an object or a sequence as required.)", drtv_node.Mark());
}

Errata
Config::load_top_level_directive(YAML::Node drtv_node)
{
  if (drtv_node.IsMap()) {
    YAML::Node key_node{drtv_node[When::KEY]};
    if (key_node) {
      auto &&[handle, errata]{When::load(*this, this->drtv_info(When::KEY), drtv_node, When::KEY, {}, key_node)};
      if (errata.is_ok()) {
        auto when = static_cast<When *>(handle.get());
        // Steal the directive out of the When.
        _roots[IndexFor(when->_hook)].emplace_back(std::move(when->_directive));
        if (Hook::POST_LOAD != _hook) { // post load hooks don't count.
          _has_top_level_directive_p = true;
        }
      } else {
        return std::move(errata);
      }
    } else {
      return Errata(S_ERROR, R"(Top level directive at {} is not a "when" directive as required.)", drtv_node.Mark());
    }
  } else {
    return Errata(S_ERROR, R"(Top level directive at {} is not an object as required.)", drtv_node.Mark());
  }
  return {};
}

Errata
Config::load_remap_directive(YAML::Node drtv_node)
{
  if (drtv_node.IsMap()) {
    auto &&[drtv, errata]{this->parse_directive(drtv_node)};
    if (errata.is_ok()) {
      // Don't unpack @c when - should not be that common and therefore better to defer them to the
      // context callbacks, avoids having to cart around the remap rule config to call them later.
      _roots[IndexFor(Hook::REMAP)].emplace_back(std::move(drtv));
      _has_top_level_directive_p = true;
    } else {
      return std::move(errata);
    }
  } else {
    return Errata(S_ERROR, R"(Configuration at {} is not a directive object as required.)", drtv_node.Mark());
  }
  return {};
}

Errata
Config::parse_yaml(YAML::Node root, TextView path)
{
  static constexpr TextView ROOT_PATH{"."};
  // Walk the key path and find the target. If the path is the special marker for ROOT_PATH
  // do not walk at all.
  for (auto p = (path == ROOT_PATH ? TextView{} : path); p;) {
    auto key{p.take_prefix_at('.')};
    if (auto node{root[key]}; node) {
      root.reset(node);
    } else {
      return Errata(S_ERROR, R"(Key "{}" not found - no such key "{}".)", path, path.prefix(path.size() - p.size()).rtrim('.'));
    }
  }

  Errata errata;

  // Special case remap loading.
  auto drtv_loader = &self_type::load_top_level_directive; // global loader.
  if (_hook == Hook::REMAP) {                              // loading only remap directives.
    drtv_loader = &self_type::load_remap_directive;
  }

  if (root.IsSequence()) {
    for (auto child : root) {
      errata.note((this->*drtv_loader)(child));
    }
    if (!errata.is_ok()) {
      errata.note(R"(While loading list of top level directives for "{}" at {}.)", path, root.Mark());
    }
  } else if (root.IsMap()) {
    errata = (this->*drtv_loader)(root);
  } else {
  }
  return errata;
};

Errata
Config::define(swoc::TextView name, HookMask const &hooks, Directive::InstanceLoader &&worker,
               Directive::CfgInitializer &&cfg_init_cb)
{
  auto &info{_factory[name]};
  info._idx         = _factory.size() - 1;
  info._hook_mask   = hooks;
  info._load_cb     = std::move(worker);
  info._cfg_init_cb = std::move(cfg_init_cb);
  return {};
}

Directive::CfgStaticData const *
Config::drtv_info(swoc::TextView const &name) const
{
  auto spot = _factory.find(name);
  return spot == _factory.end() ? nullptr : &_drtv_info[spot->second._idx];
}

/* ------------------------------------------------------------------------------------ */
Errata
Config::load_file(swoc::file::path const &cfg_path, TextView cfg_key, YamlCache *cache)
{
  if (auto spot = _cfg_files.find(cfg_path); spot != _cfg_files.end()) {
    if (spot->second.has_cfg_key(cfg_key)) {
      ts::DebugMsg(R"(Skipping "{}":{} - already loaded)", cfg_path, cfg_key);
      return {};
    } else {
      spot->second.add_cfg_key(cfg_key);
    }
  } else { // not found - put it in the table.
    auto [iter, flag] = _cfg_files.emplace(FileInfoMap::value_type{cfg_path, {}});
    iter->second.add_cfg_key(cfg_key);
  }

  YAML::Node root;
  // Try loading and parsing the file.
  if (cache) {
    if (auto cache_spot = cache->find(cfg_path); cache_spot != cache->end()) {
      root = cache_spot->second;
    }
  }

  if (root.IsNull()) {
    auto &&[yaml_node, yaml_errata]{yaml_load(cfg_path)};
    if (!yaml_errata.is_ok()) {
      yaml_errata.note(R"(While loading file "{}".)", cfg_path);
      return std::move(yaml_errata);
    }
    root = yaml_node;
    if (cache) {
      cache->emplace(cfg_path, root);
    }
  }

  // Process the YAML data.
  auto errata = this->parse_yaml(root, cfg_key);
  if (!errata.is_ok()) {
    errata.note(R"(While parsing key "{}" in configuration file "{}".)", cfg_key, cfg_path);
    return errata;
  }

  return {};
}
/* ------------------------------------------------------------------------------------ */
Errata
Config::load_file_glob(TextView pattern, swoc::TextView cfg_key, YamlCache *cache)
{
  int              flags = 0;
  glob_t           files;
  auto             err_f       = [](char const *, int) -> int { return 0; };
  swoc::file::path abs_pattern = ts::make_absolute(pattern);
  int              result      = glob(abs_pattern.c_str(), flags, err_f, &files);
  if (result == GLOB_NOMATCH) {
    return Errata(S_WARN, R"(The pattern "{}" did not match any files.)", abs_pattern);
  }
  for (size_t idx = 0; idx < files.gl_pathc; ++idx) {
    auto errata = this->load_file(swoc::file::path(files.gl_pathv[idx]), cfg_key, cache);
    if (!errata.is_ok()) {
      errata.note(R"(While processing pattern "{}".)", pattern);
      return errata;
    }
  }
  globfree(&files);
  return {};
}
/* ------------------------------------------------------------------------------------ */
Errata
Config::load_cli_args(Handle handle, const std::vector<std::string> &args, int arg_idx, YamlCache *cache)
{
  using argv_type = char const *; // Overall clearer in context of pointers to pointers.
  std::unique_ptr<argv_type[]> buff{new argv_type[args.size()]};
  swoc::MemSpan<argv_type>     argv{buff.get(), args.size()};
  int                          idx = 0;
  for (auto const &arg : args) {
    argv[idx++] = arg.c_str();
  }
  return this->load_cli_args(std::move(handle), argv, arg_idx, cache);
}

Errata
Config::load_cli_args(Handle handle, swoc::MemSpan<char const *> argv, int arg_idx, YamlCache *cache)
{
  static constexpr TextView KEY_OPT    = "key";
  static constexpr TextView CONFIG_OPT = "config"; // An archaism for BC - take out someday.

  TextView cfg_key{_hook == Hook::REMAP ? REMAP_ROOT_KEY : GLOBAL_ROOT_KEY};
  for (unsigned idx = arg_idx; idx < argv.count(); ++idx) {
    TextView arg{std::string_view(argv[idx])};
    if (arg.empty()) {
      continue;
    }
    if (arg.front() == '-') {
      arg.ltrim('-');
      if (arg.empty()) {
        return Errata(S_ERROR, "Arg {} has an option prefix but no name.", idx);
      }

      TextView value;
      if (auto prefix = arg.prefix_at('='); !prefix.empty()) {
        value = arg.substr(prefix.size() + 1);
        arg   = prefix;
      } else if (++idx >= argv.count()) {
        return Errata(S_ERROR, "Arg {} is an option '{}' that requires a value but none was found.", idx, arg);
      } else {
        value = std::string_view{argv[idx]};
      }

      if (arg.starts_with_nocase(KEY_OPT)) {
        cfg_key = value;
      } else if (arg.starts_with_nocase(CONFIG_OPT)) {
        auto errata = this->load_file_glob(value, cfg_key, cache);
        if (!errata.is_ok()) {
          return errata;
        }
      } else {
        return Errata(S_ERROR, "Arg {} is an unrecognized option '{}'.", idx, arg);
      }
      continue;
    }
    auto errata = this->load_file_glob(arg, cfg_key, cache);
    if (!errata.is_ok()) {
      return errata;
    }
  }

  // Config loaded, run the post load directives and enable them to break the load by reporting
  // errors.
  auto &post_load_directives = this->hook_directives(Hook::POST_LOAD);
  if (post_load_directives.size() > 0) {
    std::unique_ptr<Context> ctx{new Context(handle)};
    for (auto &&drtv : post_load_directives) {
      auto errata = drtv->invoke(*ctx);
      if (!errata.is_ok()) {
        errata.note("While processing post-load directives.");
        return errata;
      }
    }
  }
  // Done with the files - clear them out.
  _cfg_file_count = _cfg_files.size();
  _cfg_files.clear();
  return {};
}

Config::ActiveFeatureScope
Config::feature_scope(ActiveType const &ex_type)
{
  ActiveFeatureScope scope(*this);
  _active_feature._ref_p = false;
  _active_feature._type  = ex_type;
  return scope;
}

Config::ActiveCaptureScope
Config::capture_scope(unsigned int count, unsigned int line_no)
{
  ActiveCaptureScope scope(*this);
  _active_capture._count = count;
  _active_capture._line  = line_no;
  _active_capture._ref_p = false;
  return scope;
}

Config::ActiveFeatureScope::~ActiveFeatureScope()
{
  if (_cfg) {
    _cfg->_active_feature = _state;
  }
}
