/** @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 <iostream>
#include <csignal>

#include "tscore/Layout.h"
#include "tscore/runroot.h"
#include "tscore/ArgParser.h"
#include "tscore/ink_assert.h"
#include "tscore/signals.h"

#include "CtrlCommands.h"
#include "CacheShmCommand.h"
#include "FileConfigCommand.h"
#include "TrafficCtlStatus.h"

// Define the global variable
int App_Exit_Status_Code = CTRL_EX_OK; // Initialize it to a default value
namespace
{
void
handle_signal(int signal_num, siginfo_t *, void *)
{
  CtrlCommand::Signal_Flagged = signal_num;
}

void
signal_register_handler(int signal_num, signal_handler_t handle_signal)
{
  struct sigaction act;

  act.sa_handler   = nullptr;
  act.sa_sigaction = handle_signal;
  act.sa_flags     = SA_NODEFER | SA_RESETHAND;
  sigemptyset(&(act.sa_mask));

  ink_release_assert(sigaction(signal_num, &act, nullptr) == 0);
}
} // namespace

int
main([[maybe_unused]] int argc, const char **argv)
{
  ts::ArgParser parser;

  std::unique_ptr<CtrlCommand> command;

  auto Command_Execute = [&command]() {
    if (command) {
      command->execute();
    } else {
      throw std::runtime_error("No command provided");
    }
  };

  auto CtrlUnimplementedCommand = [](std::string_view cmd) {
    std::cout << "Command " << cmd << " unimplemented.\n";
    App_Exit_Status_Code = CTRL_EX_UNIMPLEMENTED;
  };

  parser.add_description("Apache Traffic Server RPC CLI");
  parser.add_global_usage("traffic_ctl [OPTIONS] CMD [ARGS ...]");
  parser.require_commands();

  parser.add_option("--debug", "", "Enable debugging output - unimplemented")
    .add_option("--version", "-V", "Print version string")
    .add_option("--help", "-h", "Print usage information")
    .add_option("--run-root", "", "using TS_RUNROOT as sandbox", "TS_RUNROOT", 1)
    .add_option("--format", "-f", "Use a specific output format {json|rpc}", "", 1, "", "format")
    .add_option("--read-timeout-ms", "", "Read timeout for RPC (in milliseconds)", "", 1, "10000", "read-timeout")
    .add_option("--read-attempts", "", "Read attempts for RPC", "", 1, "100", "read-attempts")
    .add_option("--watch", "-w", "Execute a program periodically. Watch interval(in seconds) can be passed.", "", 1, "-1", "watch");

  auto &config_command     = parser.add_command("config", "Manipulate configuration records").require_commands();
  auto &cache_command      = parser.add_command("cache", "Manage the document cache").require_commands();
  auto &metric_command     = parser.add_command("metric", "Manipulate performance metrics").require_commands();
  auto &server_command     = parser.add_command("server", "Stop, restart and examine the server").require_commands();
  auto &storage_command    = parser.add_command("storage", "Manipulate cache storage").require_commands();
  auto &plugin_command     = parser.add_command("plugin", "Interact with plugins").require_commands();
  auto &host_command       = parser.add_command("host", "Interact with host status").require_commands();
  auto &hostdb_command     = parser.add_command("hostdb", "Interact with HostDB status").require_commands();
  auto &direct_rpc_command = parser.add_command("rpc", "Interact with the rpc api").require_commands();

  // config commands
  config_command.add_command("defaults", "Show default information configuration values", Command_Execute)
    .add_example_usage("traffic_ctl config defaults [OPTIONS]")
    .add_option("--records", "", "Emit output in YAML format");
  config_command
    .add_command("describe", "Show detailed information about configuration values", "", MORE_THAN_ONE_ARG_N, Command_Execute)
    .add_example_usage("traffic_ctl config describe RECORD [RECORD ...]");
  config_command.add_command("diff", "Show non-default configuration values", Command_Execute)
    .add_example_usage("traffic_ctl config diff [OPTIONS]")
    .add_option("--records", "", "Emit output in YAML format");
  config_command.add_command("get", "Get one or more configuration values", "", MORE_THAN_ONE_ARG_N, Command_Execute)
    .add_example_usage("traffic_ctl config get [OPTIONS] RECORD [RECORD ...]")
    .add_option("--cold", "-c",
                "Save the value in a configuration file. This does not save the value in TS. Local file change only",
                "TS_RECORD_YAML", MORE_THAN_ZERO_ARG_N)
    .add_option("--records", "", "Emit output in YAML format")
    .add_option("--default", "", "Include default value");
  config_command.add_command("match", "Get configuration matching a regular expression", "", MORE_THAN_ONE_ARG_N, Command_Execute)
    .add_example_usage("traffic_ctl config match [OPTIONS] REGEX [REGEX ...]")
    .add_option("--records", "", "Emit output in YAML format")
    .add_option("--default", "", "Include the default value");

  //
  // Start a new reload. If used without any extra options, it will start a new reload
  // or show the details of the current reload if one is in progress.
  // A new token will be assigned by the server if no token is provided.
  config_command.add_command("reload", "Request a configuration reload", [&]() { command->execute(); })
    .add_example_usage("traffic_ctl config reload")
    //
    // Start a new reload with a specific token. If no token is provided, the server will assign one.
    // If a reload is already in progress, it will try to show the details of the current reload.
    // If token already exists, you must use another token, or let the server assign one.
    .add_option("--token", "-t", "Configuration token to reload.", "", 1, "")
    //
    // Start a new reload and monitor its progress until completion.
    // Polls the server at regular intervals (see --refresh-int).
    // If a reload is already in progress, monitors that one instead.
    .add_option("--monitor", "-m", "Monitor reload progress until completion")
    //
    // Start a new reload. if one in progress it will show de details of the current reload.
    // if no reload in progress, it will start a new one and it will show the details of it.
    // This cannot be used with --monitor, if both are set, --show-details will be ignored.
    .add_option("--show-details", "-s", "Show detailed information of the reload.")
    .add_option("--include-logs", "-l", "include logs in the details. only work together with --show-details")

    //
    // Refresh interval in seconds used with --monitor.
    // Controls how often to poll the server for reload status.
    .add_option("--refresh-int", "-r", "Refresh interval in seconds (used with --monitor). Accepts fractional values (e.g. 0.5)",
                "", 1, "0.5")
    //
    // The server will not let you start two reload at the same time. This option will force a new reload
    // even if there is one in progress. Use with caution as this may have unexpected results.
    // This is mostly for debugging and testing purposes. note: Should we keep it here?
    .add_option("--force", "-F", "Force reload even if there are unsaved changes")
    //
    // Pass inline config data for reload. Like curl's -d flag:
    //   -d @file.yaml              - read config from file
    //   -d @file1.yaml @file2.yaml - read multiple files
    //   -d @-                      - read config from stdin
    //   -d "yaml: content"         - inline yaml string
    .add_option("--data", "-d", "Inline config data (@file, @- for stdin, or yaml string)", "", MORE_THAN_ZERO_ARG_N, "")
    .add_option(
      "--initial-wait", "-w",
      "Initial wait before first poll, giving the server time to schedule all handlers (seconds). Accepts fractional values", "", 1,
      "2")
    .add_option("--timeout", "-T",
                "Maximum time to wait for reload completion (used with --monitor). "
                "Accepts duration units: 30s, 1m, 500ms, etc. 0 means no timeout",
                "", 1, "0")
    .with_required("--monitor");

  config_command.add_command("status", "Check the configuration status", [&]() { command->execute(); })
    .add_option("--token", "-t", "Configuration token to check status.", "", 1, "")
    .add_option("--count", "-c", "Number of status records to return. Use numeric or 'all' to get the full history", "", 1, "")
    .add_option("--min-level", "", "Minimum severity level for log entries: debug, note, warning, error", "", 1, "")
    .add_example_usage("traffic_ctl config status");
  config_command.add_command("set", "Set a configuration value", "", 2, Command_Execute)
    .add_option("--cold", "-c",
                "Save the value in a configuration file. This does not save the value in TS. Local file change only",
                "TS_RECORD_YAML", MORE_THAN_ZERO_ARG_N)
    .add_option("--update", "-u", "Update a configuration value. [only relevant if --cold set]")
    .add_option(
      "--type", "-t",
      "Add type tag to the yaml field. This is needed if the record is not registered inside ATS. [only relevant if --cold set]",
      "", 1)
    .add_example_usage("traffic_ctl config set RECORD VALUE");
  config_command
    .add_command("reset", "Reset configuration values matching a path pattern to their defaults", "", MORE_THAN_ZERO_ARG_N,
                 Command_Execute)
    .add_example_usage("traffic_ctl config reset records")
    .add_example_usage("traffic_ctl config reset proxy.config.http")
    .add_example_usage("traffic_ctl config reset proxy.config.http.cache_enabled");

  config_command.add_command("registry", "Show configuration file registry", Command_Execute)
    .add_example_usage("traffic_ctl config registry");

  // cache commands
  cache_command.add_command("clear", "Advance the global cache generation", "", 0, Command_Execute)
    .add_example_usage("traffic_ctl cache clear");
  // host commands
  host_command.add_command("status", "Get one or more host statuses", "", MORE_THAN_ZERO_ARG_N, Command_Execute)
    .add_example_usage("traffic_ctl host status HOST  [HOST  ...]");
  host_command.add_command("down", "Set down one or more host(s)", "", MORE_THAN_ONE_ARG_N, Command_Execute)
    .add_example_usage("traffic_ctl host down HOST [OPTIONS]")
    .add_option("--time", "-I", "number of seconds that a host is marked down", "", 1, "0")
    .add_option("--reason", "", "reason for marking the host down, one of 'manual|active|local", "", 1, "manual");
  host_command.add_command("up", "Set up one or more host(s)", "", MORE_THAN_ONE_ARG_N, Command_Execute)
    .add_example_usage("traffic_ctl host up METRIC value")
    .add_option("--reason", "", "reason for marking the host up, one of 'manual|active|local", "", 1, "manual");

  // metric commands
  metric_command.add_command("get", "Get one or more metric values", "", MORE_THAN_ONE_ARG_N, Command_Execute)
    .add_example_usage("traffic_ctl metric get METRIC [METRIC ...]");
  metric_command.add_command("describe", "Show detailed information about one or more metric values", "", MORE_THAN_ONE_ARG_N,
                             Command_Execute); // not implemented
  metric_command.add_command("match", "Get metrics matching a regular expression", "", MORE_THAN_ZERO_ARG_N, Command_Execute);
  metric_command
    .add_command(
      "monitor",
      "Display the value of a metric(s) over time. Program stops after <count> or with a SIGINT. A brief summary is displayed.", "",
      MORE_THAN_ZERO_ARG_N, Command_Execute)
    .add_example_usage("traffic_ctl metric monitor METRIC -i 3 -c 10")
    .add_option("--count", "-c",
                "Terminate execution after requesting <count> metrics. If 0 is passed, program should be terminated by a SIGINT",
                "", 1, "0")
    .add_option("--interval", "-i", "Wait interval seconds between sending each metric request. Minimum value is 1s.", "", 1, "5");

  // hostdb commands
  hostdb_command.add_command("status", "Get HostDB info", "", MORE_THAN_ZERO_ARG_N, Command_Execute)
    .add_example_usage("traffic_ctl hostdb status");

  // plugin command
  plugin_command
    .add_command("msg", "Send message to plugins - a TAG and the message DATA(optional)", "", MORE_THAN_ONE_ARG_N, Command_Execute)
    .add_example_usage("traffic_ctl plugin msg TAG DATA");

  // server commands
  server_command.add_command("backtrace", "Show a full stack trace of the traffic_server process",
                             [&]() { CtrlUnimplementedCommand("backtrace"); });
  server_command.add_command("status", "Show the proxy status", [&]() { command->execute(); })
    .add_example_usage("traffic_ctl server status");
  auto &drain_cmd = server_command.add_command("drain", "Drain the requests", [&]() { command->execute(); });
  drain_cmd.add_example_usage("traffic_ctl server drain [OPTIONS]");

  drain_cmd.add_mutex_group("drain_mode", false, "Drain mode options");
  drain_cmd.add_option_to_group("drain_mode", "--no-new-connection", "-N",
                                "Wait for new connections down to threshold before starting draining");
  drain_cmd.add_option_to_group("drain_mode", "--undo", "-U", "Recover server from the drain mode");

  auto &debug_command =
    server_command.add_command("debug", "Enable/Disable ATS for diagnostic messages at runtime").require_commands();
  debug_command.add_command("enable", "Enables logging for diagnostic messages at runtime", Command_Execute)
    .add_option("--tags", "-t", "Debug tags", "TS_DEBUG_TAGS", 1)
    .add_option("--append", "-a", "Append tags to existing tags instead of replacing")
    .with_required("--tags")
    .add_option("--client_ip", "-c", "Client's ip", "", 1, "")
    .add_example_usage("traffic_ctl server debug enable -t my_tags -c X.X.X.X")
    .add_example_usage("traffic_ctl server debug enable -t new_tag -a  # append mode");
  debug_command.add_command("disable", "Disables logging for diagnostic messages at runtime", Command_Execute)
    .add_example_usage("traffic_ctl server debug disable");

  // storage commands
  storage_command.add_command("offline", "Take one or more storage volumes offline", "", MORE_THAN_ONE_ARG_N, Command_Execute)
    .add_example_usage("storage offline DEVICE [DEVICE ...]");
  storage_command.add_command("status", "Show the storage configuration", "", MORE_THAN_ONE_ARG_N,
                              Command_Execute); // not implemented

  // direct rpc commands, handy for debug and trouble shooting
  direct_rpc_command
    .add_command("file", "Send direct JSONRPC request to the server from a passed file(s)", "", MORE_THAN_ONE_ARG_N,
                 Command_Execute)
    .add_example_usage("traffic_ctl rpc file request.yaml");
  direct_rpc_command.add_command("get-api", "Request full API from server", "", 0, Command_Execute)
    .add_example_usage("traffic_ctl rpc get-api");
  direct_rpc_command.add_command("input", "Read from standard input. Ctrl-D to send the request", "", 0, Command_Execute)
    .add_option("--raw", "-r",
                "No json/yaml parse validation will take place, the raw content will be directly send to the server.", "", 0, "",
                "raw")
    .add_example_usage("traffic_ctl rpc input ");
  direct_rpc_command
    .add_command("invoke", "Call a method by using the method name as input parameter", "", MORE_THAN_ONE_ARG_N, Command_Execute)
    .add_option("--params", "-p", "Parameters to be passed in the request, YAML or JSON format", "", MORE_THAN_ONE_ARG_N, "", "")
    .add_example_usage("traffic_ctl rpc invoke foo_bar -p \"numbers: [1, 2, 3]\"");

  // cache shm commands - operate directly on POSIX shared memory; no running server required.
  auto &shm_command = cache_command.add_command("shm", "Inspect and manage cache shared-memory segments").require_commands();
  // No parser-level default for --prefix: ArgParser injects defaults into the parsed
  // arguments, which would make an omitted --prefix indistinguishable from an explicit
  // one. CacheShmCommand supplies the runtime default and keys its "did you set
  // name_prefix?" hint off the option being absent.
  shm_command.add_option("--prefix", "-p", "shm name prefix word, framed as /<word>- (default 'ats')", "", 1, "");
  shm_command.add_command("status", "Show the cache shared-memory control segment and stripe table", Command_Execute)
    .add_example_usage("traffic_ctl cache shm status")
    .add_example_usage("traffic_ctl cache shm status --prefix ats-t");
  shm_command.add_command("clear", "Unlink the cache shared-memory control and stripe segments", Command_Execute)
    .add_example_usage("traffic_ctl cache shm clear")
    .add_example_usage("traffic_ctl cache shm clear --prefix ats-t");

  auto create_command = [](ts::Arguments &args) -> std::unique_ptr<CtrlCommand> {
    if (args.get("config")) {
      if (args.get("cold")) {
        return std::make_unique<FileConfigCommand>(&args);
      }
      return std::make_unique<ConfigCommand>(&args);
    }

    if (args.get("cache")) {
      // `cache shm` reads the shm segments directly, so it must not be routed through the RPC-backed CacheCommand.
      if (args.get("shm")) {
        return std::make_unique<CacheShmCommand>(&args);
      }
      return std::make_unique<CacheCommand>(&args);
    }

    static const std::map<std::string, std::function<std::unique_ptr<CtrlCommand>(ts::Arguments *)>> factories = {
      {"metric",  [](ts::Arguments *a) { return std::make_unique<MetricCommand>(a); }   },
      {"server",  [](ts::Arguments *a) { return std::make_unique<ServerCommand>(a); }   },
      {"storage", [](ts::Arguments *a) { return std::make_unique<StorageCommand>(a); }  },
      {"plugin",  [](ts::Arguments *a) { return std::make_unique<PluginCommand>(a); }   },
      {"host",    [](ts::Arguments *a) { return std::make_unique<HostCommand>(a); }     },
      {"hostdb",  [](ts::Arguments *a) { return std::make_unique<HostDBCommand>(a); }   },
      {"rpc",     [](ts::Arguments *a) { return std::make_unique<DirectRPCCommand>(a); }},
    };

    for (const auto &[key, factory] : factories) {
      if (args.get(key)) {
        return factory(&args);
      }
    }
    return nullptr;
  };

  try {
    // for now we only care about SIGINT(SIGQUIT, ... ?)
    signal_register_handler(SIGINT, handle_signal);

    auto args = parser.parse(argv);
    argparser_runroot_handler(args.get("run-root").value(), argv[0]);
    Layout::create();

    if (command = create_command(args); !command) {
      throw std::runtime_error("No valid command provided");
    }
    // Execute
    args.invoke();
  } catch (std::exception const &ex) {
    App_Exit_Status_Code = CTRL_EX_ERROR;
    std::cerr << "Error found:\n" << ex.what() << '\n';
  }

  return App_Exit_Status_Code;
}
