/** @file

  traffic_ctl

  @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 "traffic_ctl.h"
#include <ctime>
#include "records/I_RecDefs.h"
#include "records/P_RecUtils.h"

struct RecordDescriptionPolicy {
  using entry_type = TSConfigRecordDescription *;

  static void
  free(entry_type e)
  {
    TSConfigRecordDescriptionDestroy(e);
  }

  static entry_type
  cast(void *ptr)
  {
    return (entry_type)ptr;
  }
};

struct CtrlMgmtRecordDescriptionList : CtrlMgmtList<RecordDescriptionPolicy> {
  TSMgmtError
  match(const char *regex)
  {
    return TSConfigRecordDescribeMatchMlt(regex, 0u /* flags */, this->list);
  }
};

// Record data type names, indexed by TSRecordT.
static const char *
rec_typeof(int rec_type)
{
  switch (rec_type) {
  case TS_REC_INT:
    return "INT";
  case TS_REC_COUNTER:
    return "COUNTER";
  case TS_REC_FLOAT:
    return "FLOAT";
  case TS_REC_STRING:
    return "STRING";
  case TS_REC_UNDEFINED: /* fallthru */
  default:
    return "UNDEFINED";
  }
}

// Record type name, indexed by RecT.
static const char *
rec_classof(int rec_class)
{
  switch (rec_class) {
  case RECT_CONFIG:
    return "standard config";
  case RECT_LOCAL:
    return "local config";
  case RECT_PROCESS:
    return "process metric";
  case RECT_NODE:
    return "node metric";
  case RECT_PLUGIN:
    return "plugin metric";
  default:
    return "undefined";
  }
}

// Record access control, indexed by RecAccessT.
static const char *
rec_accessof(int rec_access)
{
  switch (rec_access) {
  case RECA_NO_ACCESS:
    return "no access";
  case RECA_READ_ONLY:
    return "read only";
  case RECA_NULL: /* fallthru */
  default:
    return "default";
  }
}

// Record access control, indexed by RecUpdateT.
static const char *
rec_updateof(int rec_updatetype)
{
  switch (rec_updatetype) {
  case RECU_DYNAMIC:
    return "dynamic, no restart";
  case RECU_RESTART_TS:
    return "static, restart traffic_server";
  case RECU_RESTART_TM:
    return "static, restart traffic_manager";
  case RECU_NULL: /* fallthru */
  default:
    return "none";
  }
}

// Record check type, indexed by RecCheckT.
static const char *
rec_checkof(int rec_checktype)
{
  switch (rec_checktype) {
  case RECC_STR:
    return "string matching a regular expression";
  case RECC_INT:
    return "integer with a specified range";
  case RECC_IP:
    return "IP address";
  case RECC_NULL: /* fallthru */
  default:
    return "none";
  }
}

static const char *
rec_sourceof(int rec_source)
{
  switch (rec_source) {
  case REC_SOURCE_DEFAULT:
    return "built in default";
  case REC_SOURCE_EXPLICIT:
    return "administratively set";
  case REC_SOURCE_PLUGIN:
    return "plugin default";
  case REC_SOURCE_ENV:
    return "environment";
  default:
    return "unknown";
  }
}

static const char *
rec_labelof(int rec_class)
{
  switch (rec_class) {
  case RECT_CONFIG:
    return "CONFIG";
  case RECT_LOCAL:
    return "LOCAL";
  default:
    return nullptr;
  }
}

static std::string
timestr(time_t tm)
{
  char buf[32];
  return std::string(ctime_r(&tm, buf));
}

static void
format_record(const CtrlMgmtRecord &record, bool recfmt)
{
  CtrlMgmtRecordValue value(record);

  if (recfmt) {
    printf("%s %s %s %s\n", rec_labelof(record.rclass()), record.name(), rec_typeof(record.type()), value.c_str());
  } else {
    printf("%s: %s\n", record.name(), value.c_str());
  }
}

static int
config_get(unsigned argc, const char **argv)
{
  int recfmt                       = 0;
  const ArgumentDescription opts[] = {
    {"records", '-', "Emit output in records.config format", "F", &recfmt, nullptr, nullptr},
  };

  if (!CtrlProcessArguments(argc, argv, opts, countof(opts)) || n_file_arguments < 1) {
    return CtrlCommandUsage("config get [OPTIONS] RECORD [RECORD ...]", opts, countof(opts));
  }

  for (unsigned i = 0; i < n_file_arguments; ++i) {
    CtrlMgmtRecord record;
    TSMgmtError error;

    error = record.fetch(file_arguments[i]);
    if (error != TS_ERR_OKAY) {
      CtrlMgmtError(error, "failed to fetch %s", file_arguments[i]);
      return CTRL_EX_ERROR;
    }

    if (REC_TYPE_IS_CONFIG(record.rclass())) {
      format_record(record, recfmt);
    }
  }

  return CTRL_EX_OK;
}

static int
config_describe(unsigned argc, const char **argv)
{
  if (!CtrlProcessArguments(argc, argv, nullptr, 0) || n_file_arguments < 1) {
    return CtrlCommandUsage("config describe RECORD [RECORD ...]");
  }

  for (unsigned i = 0; i < n_file_arguments; ++i) {
    TSConfigRecordDescription desc;
    TSMgmtError error;

    ink_zero(desc);
    error = TSConfigRecordDescribe(file_arguments[i], 0 /* flags */, &desc);
    if (error != TS_ERR_OKAY) {
      CtrlMgmtError(error, "failed to describe %s", file_arguments[i]);
      return CTRL_EX_ERROR;
    }

    printf("%-16s: %s\n", "Name", desc.rec_name);
    printf("%-16s: %s\n", "Current Value", CtrlMgmtRecordValue(desc.rec_type, desc.rec_value).c_str());
    printf("%-16s: %s\n", "Default Value", CtrlMgmtRecordValue(desc.rec_type, desc.rec_default).c_str());
    printf("%-16s: %s\n", "Record Type", rec_classof(desc.rec_class));
    printf("%-16s: %s\n", "Data Type", rec_typeof(desc.rec_type));
    printf("%-16s: %s\n", "Access Control ", rec_accessof(desc.rec_access));
    printf("%-16s: %s\n", "Update Type", rec_updateof(desc.rec_updatetype));
    printf("%-16s: 0x%" PRIx64 "\n", "Update Status", desc.rec_update);
    printf("%-16s: %s\n", "Source", rec_sourceof(desc.rec_source));

    if (strlen(desc.rec_checkexpr)) {
      printf("%-16s: %s, '%s'\n", "Syntax Check", rec_checkof(desc.rec_checktype), desc.rec_checkexpr);
    } else {
      printf("%-16s: %s\n", "Syntax Check", rec_checkof(desc.rec_checktype));
    }

    printf("%-16s: %" PRId64 "\n", "Version", desc.rec_version);
    printf("%-16s: %" PRId64 "\n", "Order", desc.rec_order);
    printf("%-16s: %" PRId64 "\n", "Raw Stat Block", desc.rec_rsb);

    TSConfigRecordDescriptionFree(&desc);
  }

  return CTRL_EX_OK;
}

static int
config_set(unsigned argc, const char **argv)
{
  TSMgmtError error;
  TSActionNeedT action;

  if (!CtrlProcessArguments(argc, argv, nullptr, 0) || n_file_arguments != 2) {
    return CtrlCommandUsage("config set RECORD VALUE");
  }

  error = TSRecordSet(file_arguments[0], file_arguments[1], &action);
  if (error != TS_ERR_OKAY) {
    CtrlMgmtError(error, "failed to set %s", file_arguments[0]);
    return CTRL_EX_ERROR;
  }

  switch (action) {
  case TS_ACTION_SHUTDOWN:
    printf("set %s, full shutdown required\n", file_arguments[0]);
    break;
  case TS_ACTION_RESTART:
    printf("set %s, restart required\n", file_arguments[0]);
    break;
  case TS_ACTION_RECONFIGURE:
    printf("set %s, please wait 10 seconds for traffic server to sync configuration, restart is not required\n", file_arguments[0]);
    break;
  case TS_ACTION_DYNAMIC:
  default:
    printf("set %s\n", file_arguments[0]);
    break;
  }

  return CTRL_EX_OK;
}

static int
config_match(unsigned argc, const char **argv)
{
  int recfmt                       = 0;
  const ArgumentDescription opts[] = {
    {"records", '-', "Emit output in records.config format", "F", &recfmt, nullptr, nullptr},
  };

  if (!CtrlProcessArguments(argc, argv, opts, countof(opts)) || n_file_arguments < 1) {
    return CtrlCommandUsage("config match [OPTIONS] REGEX [REGEX ...]", opts, countof(opts));
  }

  for (unsigned i = 0; i < n_file_arguments; ++i) {
    CtrlMgmtRecordList reclist;
    TSMgmtError error;

    // XXX filter the results to only match configuration records.

    error = reclist.match(file_arguments[i]);
    if (error != TS_ERR_OKAY) {
      CtrlMgmtError(error, "failed to fetch %s", file_arguments[i]);
      return CTRL_EX_ERROR;
    }

    while (!reclist.empty()) {
      CtrlMgmtRecord record(reclist.next());
      if (REC_TYPE_IS_CONFIG(record.rclass())) {
        format_record(record, recfmt);
      }
    }
  }

  return CTRL_EX_OK;
}

static int
config_reload(unsigned argc, const char **argv)
{
  if (!CtrlProcessArguments(argc, argv, nullptr, 0) || n_file_arguments != 0) {
    return CtrlCommandUsage("config reload");
  }

  TSMgmtError error = TSReconfigure();
  if (error != TS_ERR_OKAY) {
    CtrlMgmtError(error, "configuration reload request failed");
    return CTRL_EX_ERROR;
  }

  return CTRL_EX_OK;
}

static int
config_status(unsigned argc, const char **argv)
{
  if (!CtrlProcessArguments(argc, argv, nullptr, 0) || n_file_arguments != 0) {
    return CtrlCommandUsage("config status");
  }

  CtrlMgmtRecord version;
  CtrlMgmtRecord configtime;
  CtrlMgmtRecord starttime;
  CtrlMgmtRecord reconfig;
  CtrlMgmtRecord proxy;
  CtrlMgmtRecord manager;

  CTRL_MGMT_CHECK(version.fetch("proxy.process.version.server.long"));
  CTRL_MGMT_CHECK(starttime.fetch("proxy.node.restarts.proxy.start_time"));
  CTRL_MGMT_CHECK(configtime.fetch("proxy.node.config.reconfigure_time"));
  CTRL_MGMT_CHECK(reconfig.fetch("proxy.node.config.reconfigure_required"));
  CTRL_MGMT_CHECK(proxy.fetch("proxy.node.config.restart_required.proxy"));
  CTRL_MGMT_CHECK(manager.fetch("proxy.node.config.restart_required.manager"));

  printf("%s\n", CtrlMgmtRecordValue(version).c_str());
  printf("Started at %s", timestr((time_t)starttime.as_int()).c_str());
  printf("Last reconfiguration at %s", timestr((time_t)configtime.as_int()).c_str());
  printf("%s\n", reconfig.as_int() ? "Reconfiguration required" : "Configuration is current");

  if (proxy.as_int()) {
    printf("traffic_server requires restarting\n");
  }
  if (manager.as_int()) {
    printf("traffic_manager requires restarting\n");
  }

  return CTRL_EX_OK;
}

static int
config_defaults(unsigned argc, const char **argv)
{
  int recfmt                       = 0;
  const ArgumentDescription opts[] = {
    {"records", '-', "Emit output in records.config format", "F", &recfmt, nullptr, nullptr},
  };

  if (!CtrlProcessArguments(argc, argv, opts, countof(opts)) || n_file_arguments != 0) {
    return CtrlCommandUsage("config diff [OPTIONS]");
  }

  TSMgmtError error;
  CtrlMgmtRecordDescriptionList descriptions;

  error = descriptions.match(".*");
  if (error != TS_ERR_OKAY) {
    CtrlMgmtError(error, "failed to fetch record metadata");
    return CTRL_EX_ERROR;
  }

  while (!descriptions.empty()) {
    TSConfigRecordDescription *desc = descriptions.next();
    CtrlMgmtRecordValue deflt(desc->rec_type, desc->rec_default);

    if (recfmt) {
      printf("%s %s %s %s\n", rec_labelof(desc->rec_class), desc->rec_name, rec_typeof(desc->rec_type), deflt.c_str());
    } else {
      printf("%s: %s\n", desc->rec_name, deflt.c_str());
    }

    TSConfigRecordDescriptionDestroy(desc);
  }

  return CTRL_EX_OK;
}

static int
config_diff(unsigned argc, const char **argv)
{
  int recfmt                       = 0;
  const ArgumentDescription opts[] = {
    {"records", '-', "Emit output in records.config format", "F", &recfmt, nullptr, nullptr},
  };

  if (!CtrlProcessArguments(argc, argv, opts, countof(opts)) || n_file_arguments != 0) {
    return CtrlCommandUsage("config diff [OPTIONS]");
  }

  TSMgmtError error;
  CtrlMgmtRecordDescriptionList descriptions;

  error = descriptions.match(".*");
  if (error != TS_ERR_OKAY) {
    CtrlMgmtError(error, "failed to fetch record metadata");
    return CTRL_EX_ERROR;
  }

  while (!descriptions.empty()) {
    TSConfigRecordDescription *desc;
    bool changed = false;

    desc = descriptions.next();

    switch (desc->rec_type) {
    case TS_REC_INT:
      changed = (desc->rec_value.int_val != desc->rec_default.int_val);
      break;
    case TS_REC_COUNTER:
      changed = (desc->rec_value.counter_val != desc->rec_default.counter_val);
      break;
    case TS_REC_FLOAT:
      changed = (desc->rec_value.float_val != desc->rec_default.float_val);
      break;
    case TS_REC_STRING:
      changed = (strcmp(desc->rec_value.string_val, desc->rec_default.string_val) != 0);
      break;
    default:
      break;
    }

    if (changed) {
      CtrlMgmtRecordValue current(desc->rec_type, desc->rec_value);
      CtrlMgmtRecordValue deflt(desc->rec_type, desc->rec_default);

      if (recfmt) {
        printf("%s %s %s %s # default: %s\n", rec_labelof(desc->rec_class), desc->rec_name, rec_typeof(desc->rec_type),
               current.c_str(), deflt.c_str());
      } else {
        printf("%s has changed\n", desc->rec_name);
        printf("\t%-16s: %s\n", "Current Value", current.c_str());
        printf("\t%-16s: %s\n", "Default Value", deflt.c_str());
      }
    }

    TSConfigRecordDescriptionDestroy(desc);
  }

  return CTRL_EX_OK;
}

int
subcommand_config(unsigned argc, const char **argv)
{
  const subcommand commands[] = {
    {config_defaults, "defaults", "Show default information configuration values"},
    {config_describe, "describe", "Show detailed information about configuration values"},
    {config_diff, "diff", "Show non-default configuration values"},
    {config_get, "get", "Get one or more configuration values"},
    {config_match, "match", "Get configuration matching a regular expression"},
    {config_reload, "reload", "Request a configuration reload"},
    {config_set, "set", "Set a configuration value"},
    {config_status, "status", "Check the configuration status"},
  };

  return CtrlGenericSubcommand("config", commands, countof(commands), argc, argv);
}
