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

/**
 * @file config.cc
 * @brief Access Control Plug-in Configuration.
 * @see config.h
 */

#include <algorithm> /* find_if */
#include <fstream>   /* std::ifstream */
#include <getopt.h>  /* getopt_long() */

#include "common.h"
#include "config.h"

static bool
isTrue(const char *arg)
{
  return (nullptr == arg || 0 == strncasecmp("true", arg, 4) || 0 == strncasecmp("1", arg, 1) || 0 == strncasecmp("yes", arg, 3));
}

/**
 * trim from start
 */
static inline std::string &
ltrim(std::string &s)
{
  s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
  return s;
}

/**
 *  trim from end
 */
static inline std::string &
rtrim(std::string &s)
{
  s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end());
  return s;
}

/**
 * trim from both ends
 */
static inline std::string &
trim(std::string &s)
{
  return ltrim(rtrim(s));
}

/**
 * @brief Rebase a relative path onto the configuration directory.
 */
static String
makeConfigPath(const String &path)
{
  if (path.empty() || path[0] == '/') {
    return path;
  }

  return String(TSConfigDirGet()) + "/" + path;
}

/**
 * @brief parse and load a single line (base template - does nothing, see specializations for maps and vectors below)
 */
template <class T>
void
loadLine(T &container, const String &line)
{
}

/**
 * @brief parse and load a single line into a map.
 */
template <>
void
loadLine<StringMap>(StringMap &map, const String &line)
{
  String key;
  String value;
  std::istringstream ss(line);
  std::getline(ss, key, '=');
  std::getline(ss, value, '=');
  trim(key);
  trim(value);
  map[key] = value;

#ifdef ACCESS_CONTROL_LOG_SECRETS
  AccessControlDebug("Adding secrets[%s]='%s'", key.c_str(), value.c_str());
#endif
}

/**
 * @brief parse and load a single line into a vector.
 */
template <>
void
loadLine<StringVector>(StringVector &vector, const String &line)
{
  String trimmedLine(line);
  trim(trimmedLine);
  vector.push_back(trimmedLine);

#ifdef ACCESS_CONTROL_LOG_SECRETS
  AccessControlDebug("Adding secrets[%d]='%s'", (int)(vector.size() - 1), trimmedLine.c_str());
#endif
}

/**
 * @brief parse and load a secrets into a container (i.e. map or vector).
 */
template <typename T>
static bool
load(T &container, const String &filename)
{
  String line;
  String::size_type pos;

  String path(makeConfigPath(filename));

  AccessControlDebug("reading file %s", path.c_str());

  std::ifstream infile;
  infile.open(path.c_str());
  if (!infile.is_open()) {
    AccessControlError("failed to load file '%s'", path.c_str());
    return false;
  }

  while (std::getline(infile, line)) {
    // Allow #-prefixed comments.
    pos = line.find_first_of('#');
    if (pos != String::npos) {
      line.resize(pos);
    }
    if (line.empty()) {
      continue;
    }

    loadLine(container, line);
  }
  infile.close();

  return true;
}

/**
 * @brief initializes plugin configuration.
 * @param argc number of plugin parameters
 * @param argv plugin parameters
 */
bool
AccessControlConfig::init(int argc, char *argv[])
{
  static const struct option longopt[] = {{const_cast<char *>("invalid-syntax-status-code"), optional_argument, 0, 'a'},
                                          {const_cast<char *>("invalid-signature-status-code"), optional_argument, 0, 'b'},
                                          {const_cast<char *>("invalid-timing-status-code"), optional_argument, 0, 'c'},
                                          {const_cast<char *>("invalid-scope-status-code"), optional_argument, 0, 'd'},
                                          {const_cast<char *>("invalid-origin-response"), optional_argument, 0, 'e'},
                                          {const_cast<char *>("internal-error-status-code"), optional_argument, 0, 'f'},
                                          {const_cast<char *>("check-cookie"), optional_argument, 0, 'g'},
                                          {const_cast<char *>("symmetric-keys-map"), optional_argument, 0, 'h'},
                                          {const_cast<char *>("reject-invalid-token-requests"), optional_argument, 0, 'i'},
                                          {const_cast<char *>("extract-subject-to-header"), optional_argument, 0, 'j'},
                                          {const_cast<char *>("extract-tokenid-to-header"), optional_argument, 0, 'k'},
                                          {const_cast<char *>("extract-status-to-header"), optional_argument, 0, 'l'},
                                          {const_cast<char *>("token-response-header"), optional_argument, 0, 'm'},
                                          {const_cast<char *>("use-redirects"), optional_argument, 0, 'n'},
                                          {const_cast<char *>("include-uri-paths-file"), optional_argument, nullptr, 'o'},
                                          {const_cast<char *>("exclude-uri-paths-file"), optional_argument, nullptr, 'p'},
                                          {0, 0, 0, 0}};

  bool status = true;
  optind      = 0;

  /* argv contains the "to" and "from" URLs. Skip the first so that the second one poses as the program name. */
  argc--;
  argv++;

  for (;;) {
    int opt;
    opt = getopt_long(argc, (char *const *)argv, "", longopt, nullptr);

    if (opt == -1) {
      break;
    }
    AccessControlDebug("processing %s", argv[optind - 1]);

    switch (opt) {
    case 'a': /* invalid-syntax-status-code */
    {
      _invalidSignature = (TSHttpStatus)string2int(optarg);
    } break;
    case 'b': /* invalid-signature-status-code */
    {
      _invalidSignature = (TSHttpStatus)string2int(optarg);
    } break;
    case 'c': /* invalid-timing-status-code */
    {
      _invalidTiming = (TSHttpStatus)string2int(optarg);
    } break;
    case 'd': /* invalid-scope-status-code */
    {
      _invalidScope = (TSHttpStatus)string2int(optarg);
    } break;
    case 'e': /* invalid-origin-response */
    {
      _invalidOriginResponse = (TSHttpStatus)string2int(optarg);
    } break;
    case 'f': /* internal-error-status-code */
    {
      _internalError = (TSHttpStatus)string2int(optarg);
    } break;
    case 'g': /* check-cookie */
    {
      _cookieName.assign(optarg);
    } break;
    case 'h': /* symmetric-keys-map */
    {
      load(_symmetricKeysMap, optarg);
    } break;
    case 'i': /* reject-invalid-token-requests */
    {
      _rejectRequestsWithInvalidTokens = ::isTrue(optarg);
    } break;
    case 'j': /* extract-subject-to-header */
    {
      _extrSubHdrName.assign(optarg);
    } break;
    case 'k': /* extract-tokenid-to-header */
    {
      _extrTokenIdHdrName.assign(optarg);
    } break;
    case 'l': /* extract-status-to-header */
    {
      _extrValidationHdrName.assign(optarg);
    } break;
    case 'm': /* token-response-header */
    {
      _respTokenHeaderName.assign(optarg);
    } break;
    case 'n': /* use-redirects */
    {
      _useRedirects = ::isTrue(optarg);
    } break;
    case 'o': /* include-uri-paths-file */
      if (!loadMultiPatternsFromFile(optarg, /* blacklist = */ false)) {
        AccessControlError("failed to load uri-path multi-pattern white-list '%s'", optarg);
        status = false;
      }
      break;
    case 'p': /* exclude-uri-paths-file */
      if (!loadMultiPatternsFromFile(optarg, /* blacklist = */ true)) {
        AccessControlError("failed to load uri-path multi-pattern black-list '%s'", optarg);
        status = false;
      }
      break;

    default: {
      status = false;
    }
    }
  }

  /* Make sure at least 1 secret source is specified */
  if (_symmetricKeysMap.empty()) {
    AccessControlDebug("no secrets' source provided");
    return false;
  }

  /* Support only KeyValuePair syntax for now */
  _tokenFactory = new AccessTokenFactory(_kvpAccessTokenConfig, _symmetricKeysMap, _debugLevel);
  if (nullptr == _tokenFactory) {
    AccessControlDebug("failed to initialize the access token factory");
    return false;
  }
  return status;
}

/**
 * @brief a helper function which loads the classifier from files.
 * @param filename file name
 * @param blacklist true - load as a blacklist of patterns, false - white-list of patterns
 * @return true if successful, false otherwise.
 */
bool
AccessControlConfig::loadMultiPatternsFromFile(const String &filename, bool blacklist)
{
  if (filename.empty()) {
    AccessControlError("filename cannot be empty");
    return false;
  }

  String path(makeConfigPath(filename));

  std::ifstream ifstr;
  String regex;
  unsigned lineno = 0;

  ifstr.open(path.c_str());
  if (!ifstr) {
    AccessControlError("failed to load uri-path multi-pattern from '%s'", path.c_str());
    return false;
  }

  /* Have the multiplattern be named as same as the filename, would be used only for debugging. */
  MultiPattern *multiPattern;
  if (blacklist) {
    multiPattern = new NonMatchingMultiPattern(filename);
    AccessControlDebug("NonMatchingMultiPattern('%s')", filename.c_str());
  } else {
    multiPattern = new MultiPattern(filename);
    AccessControlDebug("MultiPattern('%s')", filename.c_str());
  }
  if (nullptr == multiPattern) {
    AccessControlError("failed to allocate multi-pattern from '%s'", filename.c_str());
    return false;
  }

  AccessControlDebug("loading multi-pattern '%s' from '%s'", filename.c_str(), path.c_str());

  while (std::getline(ifstr, regex)) {
    Pattern *p;
    String::size_type pos;

    ++lineno;

    // Allow #-prefixed comments.
    pos = regex.find_first_of('#');
    if (pos != String::npos) {
      regex.resize(pos);
    }

    if (regex.empty()) {
      continue;
    }

    p = new Pattern();

    if (nullptr != p && p->init(regex)) {
      if (blacklist) {
        AccessControlDebug("Added pattern '%s' to black list uri-path multi-pattern '%s'", regex.c_str(), filename.c_str());
        multiPattern->add(p);
      } else {
        AccessControlDebug("Added pattern '%s' to white list uri-path multi-pattern '%s'", regex.c_str(), filename.c_str());
        multiPattern->add(p);
      }
    } else {
      AccessControlError("%s:%u: failed to parse regex '%s'", path.c_str(), lineno, regex.c_str());
      delete p;
    }
  }

  ifstr.close();

  if (!multiPattern->empty()) {
    _uriPathScope.add(multiPattern);
  } else {
    delete multiPattern;
  }

  return true;
}
