/** @file

  A brief file description

  @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 "tscore/ink_platform.h"
#include "tscore/ink_args.h"
#include "tscore/I_Version.h"
#include "tscore/Tokenizer.h"
#include "tscore/TextBuffer.h"
#include "mgmtapi.h"
#include <cstdio>
#include <cstring>
#include "tscore/Regex.h"

/// XXX Use DFA or Regex wrappers?
#ifdef HAVE_PCRE_PCRE_H
#include <pcre/pcre.h>
#else
#include <pcre.h>
#endif

#define SUBSTRING_VECTOR_COUNT 30 // Should be multiple of 3

static AppVersionInfo appVersionInfo;

struct VIA {
  VIA(const char *t) : title(t), next(nullptr) { memset(viaData, 0, sizeof(viaData)); }
  ~VIA() { delete next; }
  const char *title;
  const char *viaData[128];
  VIA *next;
};

// Function to get via header table for every field/category in the via header
static VIA *
detailViaLookup(char flag)
{
  VIA *viaTable;

  // Detailed via codes after ":"
  switch (flag) {
  case 't':
    viaTable                              = new VIA("Tunnel info");
    viaTable->viaData[(unsigned char)' '] = "no tunneling";
    viaTable->viaData[(unsigned char)'U'] = "tunneling because of url (url suggests dynamic content)";
    viaTable->viaData[(unsigned char)'M'] = "tunneling due to a method (e.g. CONNECT)";
    viaTable->viaData[(unsigned char)'O'] = "tunneling because cache is turned off";
    viaTable->viaData[(unsigned char)'F'] = "tunneling due to a header field (such as presence of If-Range header)";
    viaTable->viaData[(unsigned char)'N'] = "tunneling due to no forward";
    viaTable->viaData[(unsigned char)'A'] = "tunnel authorization";
    break;
  case 'c':
    // Cache type
    viaTable                              = new VIA("Cache Type");
    viaTable->viaData[(unsigned char)'C'] = "cache";
    viaTable->viaData[(unsigned char)'L'] = "cluster, (not used)";
    viaTable->viaData[(unsigned char)'P'] = "parent";
    viaTable->viaData[(unsigned char)'S'] = "server";
    viaTable->viaData[(unsigned char)' '] = "unknown";

    // Cache Lookup Result
    viaTable->next                              = new VIA("Cache Lookup Result");
    viaTable->next->viaData[(unsigned char)'C'] = "cache hit but config forces revalidate";
    viaTable->next->viaData[(unsigned char)'I'] = "conditional miss (client sent conditional, fresh in cache, returned 412)";
    viaTable->next->viaData[(unsigned char)' '] = "cache miss or no cache lookup";
    viaTable->next->viaData[(unsigned char)'U'] = "cache hit, but client forces revalidate (e.g. Pragma: no-cache)";
    viaTable->next->viaData[(unsigned char)'D'] = "cache hit, but method forces revalidated (e.g. ftp, not anonymous)";
    viaTable->next->viaData[(unsigned char)'M'] = "cache miss (url not in cache)";
    viaTable->next->viaData[(unsigned char)'N'] = "conditional hit (client sent conditional, doc fresh in cache, returned 304)";
    viaTable->next->viaData[(unsigned char)'H'] = "cache hit";
    viaTable->next->viaData[(unsigned char)'S'] = "cache hit, but expired";
    viaTable->next->viaData[(unsigned char)'K'] = "cookie miss";
    break;
  case 'p':
    viaTable                              = new VIA("Parent proxy connection status");
    viaTable->viaData[(unsigned char)' '] = "no parent proxy or unknown";
    viaTable->viaData[(unsigned char)'S'] = "connection opened successfully";
    viaTable->viaData[(unsigned char)'F'] = "connection open failed";
    break;
  case 's':
    viaTable                              = new VIA("Origin server connection status");
    viaTable->viaData[(unsigned char)' '] = "no server connection needed";
    viaTable->viaData[(unsigned char)'S'] = "connection opened successfully";
    viaTable->viaData[(unsigned char)'F'] = "connection open failed";
    break;
  default:
    viaTable = nullptr;
    fprintf(stderr, "%s: %s: %c\n", appVersionInfo.AppStr, "Invalid VIA header character", flag);
    break;
  }

  return viaTable;
}

// Function to get via header table for every field/category in the via header
static VIA *
standardViaLookup(char flag)
{
  VIA *viaTable;

  // Via codes before ":"
  switch (flag) {
  case 'u':
    viaTable                              = new VIA("Request headers received from client");
    viaTable->viaData[(unsigned char)'C'] = "cookie";
    viaTable->viaData[(unsigned char)'E'] = "error in request";
    viaTable->viaData[(unsigned char)'S'] = "simple request (not conditional)";
    viaTable->viaData[(unsigned char)'N'] = "no-cache";
    viaTable->viaData[(unsigned char)'I'] = "IMS";
    viaTable->viaData[(unsigned char)' '] = "unknown";
    break;
  case 'c':
    viaTable                              = new VIA("Result of Traffic Server cache lookup for URL");
    viaTable->viaData[(unsigned char)'A'] = "in cache, not acceptable (a cache \"MISS\")";
    viaTable->viaData[(unsigned char)'H'] = "in cache, fresh (a cache \"HIT\")";
    viaTable->viaData[(unsigned char)'S'] = "in cache, stale (a cache \"MISS\")";
    viaTable->viaData[(unsigned char)'R'] = "in cache, fresh Ram hit (a cache \"HIT\")";
    viaTable->viaData[(unsigned char)'M'] = "miss (a cache \"MISS\")";
    viaTable->viaData[(unsigned char)' '] = "no cache lookup";
    break;
  case 's':
    viaTable                              = new VIA("Response information received from origin server");
    viaTable->viaData[(unsigned char)'E'] = "error in response";
    viaTable->viaData[(unsigned char)'S'] = "connection opened successfully";
    viaTable->viaData[(unsigned char)'N'] = "not-modified";
    viaTable->viaData[(unsigned char)' '] = "no server connection needed";
    break;
  case 'f':
    viaTable                              = new VIA("Result of document write-to-cache:");
    viaTable->viaData[(unsigned char)'U'] = "updated old cache copy";
    viaTable->viaData[(unsigned char)'D'] = "cached copy deleted";
    viaTable->viaData[(unsigned char)'W'] = "written into cache (new copy)";
    viaTable->viaData[(unsigned char)' '] = "no cache write performed";
    break;
  case 'p':
    viaTable                              = new VIA("Proxy operation result");
    viaTable->viaData[(unsigned char)'R'] = "origin server revalidated";
    viaTable->viaData[(unsigned char)' '] = "unknown";
    viaTable->viaData[(unsigned char)'S'] = "served or connection opened successfully";
    viaTable->viaData[(unsigned char)'N'] = "not-modified";
    break;
  case 'e':
    viaTable                              = new VIA("Error codes (if any)");
    viaTable->viaData[(unsigned char)'A'] = "authorization failure";
    viaTable->viaData[(unsigned char)'H'] = "header syntax unacceptable";
    viaTable->viaData[(unsigned char)'C'] = "connection to server failed";
    viaTable->viaData[(unsigned char)'T'] = "connection timed out";
    viaTable->viaData[(unsigned char)'S'] = "server related error";
    viaTable->viaData[(unsigned char)'D'] = "dns failure";
    viaTable->viaData[(unsigned char)'N'] = "no error";
    viaTable->viaData[(unsigned char)'F'] = "request forbidden";
    viaTable->viaData[(unsigned char)'R'] = "cache read error";
    viaTable->viaData[(unsigned char)'M'] = "moved temporarily";
    viaTable->viaData[(unsigned char)' '] = "unknown";
    break;
  default:
    viaTable = nullptr;
    fprintf(stderr, "%s: %s: %c\n", appVersionInfo.AppStr, "Invalid VIA header character", flag);
    break;
  }

  return viaTable;
}

// Function to print via header
static void
printViaHeader(const char *header)
{
  VIA *viaTable = nullptr;
  VIA *viaEntry = nullptr;
  bool isDetail = false;

  printf("Via Header Details:\n");

  // Loop through input via header flags
  for (const char *c = header; *c; ++c) {
    if ((*c == ':') || (*c == ';')) {
      isDetail = true;
      continue;
    }

    if (islower(*c)) {
      // Get the via header table
      delete viaTable;
      viaEntry = viaTable = isDetail ? detailViaLookup(*c) : standardViaLookup(*c);
    } else {
      // This is a one of the sequence of (uppercase) VIA codes.
      if (viaEntry) {
        printf("%-55s:", viaEntry->title);
        printf("%s\n", viaEntry->viaData[(unsigned char)*c] ? viaEntry->viaData[(unsigned char)*c] : "Invalid sequence");
        viaEntry = viaEntry->next;
      }
    }
  }
  delete viaTable;
}

// Check validity of via header and then decode it
static TSMgmtError
decodeViaHeader(const char *str)
{
  size_t viaHdrLength = strlen(str);
  char tmp[viaHdrLength + 2];
  char *Via = tmp;

  memcpy(Via, str, viaHdrLength);
  Via[viaHdrLength] = '\0'; // null terminate
  printf("Via header is %s, Length is %zu\n", Via, viaHdrLength);

  // Via header inside square brackets
  if (Via[0] == '[' && Via[viaHdrLength - 1] == ']') {
    viaHdrLength = viaHdrLength - 2;
    Via++;
    Via[viaHdrLength] = '\0'; // null terminate the string after trimming
  }

  if (viaHdrLength == 5) {
    Via = strcat(Via, " "); // Add one space character before decoding via header
    ++viaHdrLength;
  }

  if (viaHdrLength == 23 || viaHdrLength == 6) {
    // Decode via header
    printViaHeader(Via);
    return TS_ERR_OKAY;
  }
  // Invalid header size, come out.
  printf("\nInvalid VIA header. VIA header length should be 6 or 23 characters\n");
  printf("Valid via header format is "
         "[u<client-stuff>c<cache-lookup-stuff>s<server-stuff>f<cache-fill-stuff>p<proxy-stuff>e<error-codes>:t<tunneling-info>c<"
         "cache type><cache-lookup-result>p<parent-proxy-conn-info>s<server-conn-info>]\n");
  return TS_ERR_FAIL;
}

// Read user input from stdin
static TSMgmtError
filterViaHeader()
{
  const pcre *compiledReg;
  const pcre_extra *extraReg = nullptr;
  int subStringVector[SUBSTRING_VECTOR_COUNT];
  const char *err;
  int errOffset;
  int pcreExecCode;
  int i;
  const char *viaPattern =
    R"(\[([ucsfpe]+[^\]]+)\])"; // Regex to match via header with in [] which can start with character class ucsfpe
  char *viaHeaderString;
  char viaHeader[1024];

  // Compile PCRE via header pattern
  compiledReg = pcre_compile(viaPattern, 0, &err, &errOffset, nullptr);

  if (compiledReg == nullptr) {
    printf("PCRE regex compilation failed with error %s at offset %d\n", err, errOffset);
    return TS_ERR_FAIL;
  }

  // Read all lines from stdin
  while (fgets(viaHeader, sizeof(viaHeader), stdin)) {
    // Trim new line character and null terminate it
    char *newLinePtr = strchr(viaHeader, '\n');
    if (newLinePtr) {
      *newLinePtr = '\0';
    }
    // Match for via header pattern
    pcreExecCode =
      pcre_exec(compiledReg, extraReg, viaHeader, (int)sizeof(viaHeader), 0, 0, subStringVector, SUBSTRING_VECTOR_COUNT);

    // Match failed, don't worry. Continue to next line.
    if (pcreExecCode < 0) {
      continue;
    }

    // Match successful, but too many substrings
    if (pcreExecCode == 0) {
      pcreExecCode = SUBSTRING_VECTOR_COUNT / 3;
      printf("Too many substrings were found. %d substrings couldn't fit into subStringVector\n", pcreExecCode - 1);
    }

    // Loop based on number of matches found
    for (i = 1; i < pcreExecCode; i++) {
      // Point to beginning of matched substring
      char *subStringBegin = viaHeader + subStringVector[2 * i];
      // Get length of matched substring
      int subStringLen = subStringVector[2 * i + 1] - subStringVector[2 * i];
      viaHeaderString  = subStringBegin;
      sprintf(viaHeaderString, "%.*s", subStringLen, subStringBegin);
      // Decode matched substring
      decodeViaHeader(viaHeaderString);
    }
  }
  return TS_ERR_OKAY;
}

int
main(int /* argc ATS_UNUSED */, const char **argv)
{
  TSMgmtError status;

  // build the application information structure
  appVersionInfo.setup(PACKAGE_NAME, "traffic_via", PACKAGE_VERSION, __DATE__, __TIME__, BUILD_MACHINE, BUILD_PERSON, "");

  /* see 'ink_args.h' for meanings of the various fields */
  ArgumentDescription argument_descriptions[] = {
    VERSION_ARGUMENT_DESCRIPTION(),
    HELP_ARGUMENT_DESCRIPTION(),
  };

  process_args(&appVersionInfo, argument_descriptions, countof(argument_descriptions), argv);

  for (unsigned i = 0; i < n_file_arguments; ++i) {
    if (strcmp(file_arguments[i], "-") == 0) {
      // Filter arguments provided from stdin
      status = filterViaHeader();
    } else {
      status = decodeViaHeader(file_arguments[i]);
    }

    if (status != TS_ERR_OKAY) {
      return 1;
    }
  }

  return 0;
}
