/*
 * 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 "tscore/ink_config.h"
#include <yaml-cpp/yaml.h>

#include "sni_selector.h"

std::atomic<SniSelector *> SniSelector::_instance = nullptr;

///////////////////////////////////////////////////////////////////////////////
// YAML parser for the global YAML configuration (via plugin.config)
//
bool
SniSelector::yamlParser(const std::string &yaml_file)
{
  YAML::Node config;

  try {
    config = YAML::LoadFile(yaml_file);
  } catch (YAML::BadFile const &e) {
    TSError("[%s] Cannot load configuration file: %s.", PLUGIN_NAME, e.what());
    return false;
  } catch (std::exception const &e) {
    TSError("[%s] Unknown error while loading configuration file: %s.", PLUGIN_NAME, e.what());
    return false;
  }

  _yaml_file = yaml_file;

  // First build the Lists, if any
  const YAML::Node &lists = config["lists"];

  if (lists && lists.IsSequence()) {
    for (const auto &i : lists) {
      const YAML::Node &list = i;

      if (list.IsMap() && list["name"]) {
        auto name = list["name"].as<std::string>();

        if (nullptr != findList(name)) {
          TSError("[%s] Duplicate List names being added (%s)", PLUGIN_NAME, name.c_str());
          return false;
        }

        auto ipl = new List::IP(name);

        if (ipl->parseYaml(list)) {
          Dbg(dbg_ctl, "Loaded List rule: %s", name.c_str());
          addList(ipl);
        } else {
          TSError("[%s] Failed to parse the List YAML node", PLUGIN_NAME);
          delete ipl;
          return false;
        }
      } else {
        TSError("[%s] List node is not a map or without a name", PLUGIN_NAME);
        return false;
      }
    }
  }

  // Next, build the IP reputation (if any)
  const YAML::Node &ipreps = config["ip-rep"];

  if (ipreps && ipreps.IsSequence()) {
    for (const auto &i : ipreps) {
      const YAML::Node &ipr = i;

      if (ipr.IsMap() && ipr["name"]) {
        auto name = ipr["name"].as<std::string>();

        if (nullptr != findIpRep(name)) {
          TSError("[%s] Duplicate IP-Reputation names being added (%s)", PLUGIN_NAME, name.c_str());
          return false;
        }

        auto iprep = new IpReputation::SieveLru(name);

        if (iprep->parseYaml(ipr)) {
          Dbg(dbg_ctl, "Loaded IP Reputation rule: %s", name.c_str());
          addIPReputation(iprep);
        } else {
          TSError("[%s] Failed to parse the ip-rep YAML node", PLUGIN_NAME);
          delete iprep;
          return false;
        }
      } else {
        TSError("[%s] ip-rep node is not a map or without a name", PLUGIN_NAME);
        return false;
      }
    }
  }

  // Finally, parse all the SNI selectors (if any)
  const YAML::Node &sel = config["selector"];

  if (sel && sel.IsSequence()) {
    for (const auto &i : sel) {
      const YAML::Node &sni = i;

      if (sni.IsMap() && !sni["sni"].IsSequence()) {
        auto name = sni["sni"].as<std::string>();

        if (nullptr != findLimiter(name)) {
          TSError("[%s] Duplicate SNIs being added (%s)", PLUGIN_NAME, name.c_str());
          return false;
        }

        auto limiter = new SniRateLimiter(name, this);

        if (limiter->parseYaml(sni)) {
          if (name == "*" || name == "default") {
            _default = limiter;
          } else {
            addLimiter(limiter);
          }

          // Setup rate based limit, if configured (this is rate as in "requests per second")
          if (limiter->rate() > 0) {
            limiter->addBucket();
          }

          // Add aliases, if any
          const YAML::Node &aliases = sni["aliases"];

          if (aliases) {
            if (aliases.IsSequence()) {
              for (const auto &aliase : aliases) {
                auto alias = aliase.as<std::string>();

                if (nullptr != findLimiter(alias)) {
                  TSError("[%s] Duplicate SNIs being added (%s)", PLUGIN_NAME, alias.c_str());
                  return false;
                }
                Dbg(dbg_ctl, "Adding alias: %s -> %s", alias.c_str(), name.c_str());
                addAlias(alias, limiter);
              }
            } else {
              TSError("[%s] aliases node is not a sequence", PLUGIN_NAME);
              return false;
            }
          }
        } else {
          TSError("[%s] Failed to parse the selector YAML node", PLUGIN_NAME);
          delete limiter;
          return false;
        }
      } else {
        TSError("[%s] selector node is not a map or without a name", PLUGIN_NAME);
        return false;
      }
    }
  }

  Dbg(dbg_ctl, "Succesfully loaded YAML file: %s", yaml_file.c_str());

  return true;
}

///////////////////////////////////////////////////////////////////////////////
// This is the queue management continuation, which gets called periodically
//
static int
sni_config_cont(TSCont cont, TSEvent /* event ATS_UNUSED */, void * /* edata ATS_UNUSED */)
{
  auto selector = SniSelector::instance(); // Also leases the instance
  auto old_sel  = static_cast<SniSelector *>(TSContDataGet(cont));
  auto new_sel  = new SniSelector();

  // Delete the previous selector, which releases the lease we got at setup / reload
  if (old_sel) {
    old_sel->release();
    TSContDataSet(cont, nullptr);
  }

  if (new_sel->yamlParser(selector->yamlFile())) {
    new_sel->acquire();
    new_sel->setupQueueCont(); // Start the queue processing continuation if needed
    SniSelector::swap(new_sel);
    // Now, save the old selector in the cont data here, such that we do the final release next time
    TSContDataSet(cont, selector);
    Dbg(dbg_ctl, "Reloading YAML file: %s", new_sel->yamlFile().c_str());
  } else {
    delete new_sel;
    TSError("[%s] Failed to reload YAML file: %s", PLUGIN_NAME, selector->yamlFile().c_str());
  }

  selector->release();

  return TS_EVENT_NONE;
}

///////////////////////////////////////////////////////////////////////////////
// This is the queue management continuation, which gets called periodically
//
static int
sni_queue_cont(TSCont cont, TSEvent /* event ATS_UNUSED */, void * /* edata ATS_UNUSED */)
{
  auto *selector = static_cast<SniSelector *>(TSContDataGet(cont));

  for (const auto &[key, entry] : selector->limiters()) {
    auto [owner, limiter] = entry;
    QueueTime now         = std::chrono::system_clock::now(); // Only do this once per limiter

    if (owner) { // Don't operate on the aliases
      // Try to enable some queued VCs (if any) if there are slots available
      while (limiter->size() > 0 && limiter->reserve() != ReserveStatus::RESERVED) { // Can't be UNLIMITED here
        auto [vc, contp, start_time]    = limiter->pop();
        std::chrono::milliseconds delay = std::chrono::duration_cast<std::chrono::milliseconds>(now - start_time);

        (void)contp; // Ugly, but silences some compilers.
        Dbg(dbg_ctl, "SNI=%s: Enabling queued VC after %ldms", key.data(), static_cast<long>(delay.count()));
        TSVConnReenable(vc);
        limiter->incrementMetric(RATE_LIMITER_METRIC_RESUMED);
      }

      // Kill any queued VCs if they are too old
      if (limiter->size() > 0 && limiter->max_age() > std::chrono::milliseconds::zero()) {
        now = std::chrono::system_clock::now(); // Update the "now", for some extra accuracy

        while (limiter->size() > 0 && limiter->hasOldEntity(now)) {
          // The oldest object on the queue is too old on the queue, so "kill" it.
          auto [vc, contp, start_time]  = limiter->pop();
          std::chrono::milliseconds age = std::chrono::duration_cast<std::chrono::milliseconds>(now - start_time);

          (void)contp;
          Dbg(dbg_ctl, "Queued VC is too old (%ldms), erroring out", static_cast<long>(age.count()));
          TSVConnReenableEx(vc, TS_EVENT_ERROR);
          limiter->incrementMetric(RATE_LIMITER_METRIC_EXPIRED);
        }
      }
    }
  }

  return TS_EVENT_NONE;
}

///////////////////////////////////////////////////////////////////////////////
// If needed, create the queue continuation that needs to run for this selector.
//
void
SniSelector::setupQueueCont()
{
  if (_needs_queue_cont && !_queue_cont) {
    _queue_cont = TSContCreate(sni_queue_cont, TSMutexCreate());
    TSReleaseAssert(_queue_cont);
    TSContDataSet(_queue_cont, this);
    _queue_action = TSContScheduleEveryOnPool(_queue_cont, QUEUE_DELAY_TIME.count(), TS_THREAD_POOL_TASK);
  }
}

///////////////////////////////////////////////////////////////////////////////
// Startup of the SNI selector hooks and config reload continuation and
// instance. This should only be called once, after which the configuration
// continuation takes over any reloads.
//
void
SniSelector::startup(const std::string &yaml_file)
{
  auto sni_cont    = TSContCreate(sni_limit_cont, nullptr);
  auto config_cont = TSContCreate(sni_config_cont, TSMutexCreate());

  TSReleaseAssert(sni_cont);
  TSReleaseAssert(config_cont);

  _instance.store(new SniSelector());
  TSHttpHookAdd(TS_SSL_CLIENT_HELLO_HOOK, sni_cont);
  TSHttpHookAdd(TS_VCONN_CLOSE_HOOK, sni_cont);

  auto selector = SniSelector::instance(); // Assure that we don't delete this until next config reload

  if (selector->yamlParser(yaml_file)) {
    selector->setupQueueCont(); // Start the queue processing continuation if needed
    TSMgmtUpdateRegister(config_cont, PLUGIN_NAME, yaml_file.c_str());
  } else {
    selector->release();
    TSFatal("[%s] Failed to parse YAML file '%s'", PLUGIN_NAME, yaml_file.c_str());
  }
}
