/*
 * Copyright (c) 2018, 2024, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is designed to work with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms,
 * as designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have either included with
 * the program or referenced in the documentation.
 *
 * This program is distributed in the hope that it will be useful,  but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
 * the GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "modules/mod_shell_reports.h"

#include <algorithm>
#include <limits>
#include <locale>
#include <set>

#include "modules/reports/query.h"
#include "modules/reports/thread.h"
#include "modules/reports/threads.h"
#include "modules/reports/utils.h"
#include "mysqlshdk/include/shellcore/utils_help.h"
#include "mysqlshdk/libs/textui/textui.h"
#include "mysqlshdk/libs/utils/options.h"
#include "mysqlshdk/libs/utils/threads.h"
#include "mysqlshdk/libs/utils/utils_general.h"
#include "mysqlshdk/libs/utils/utils_string.h"

namespace mysqlsh {

namespace {

const std::set<std::string> kReportOptionTypes = {"string", "integer", "float",
                                                  "bool"};

REGISTER_HELP_OBJECT(reports, shell);
REGISTER_HELP(REPORTS_BRIEF,
              "Gives access to built-in and user-defined reports.");

REGISTER_HELP(REPORTS_DETAIL,
              "The 'reports' object provides access to built-in reports.");
REGISTER_HELP(REPORTS_DETAIL1,
              "All user-defined reports registered using the "
              "shell.<<<registerReport>>>() method are also available here.");
REGISTER_HELP(REPORTS_DETAIL2,
              "The reports are provided as methods of this object, with names "
              "corresponding to the names of the available reports.");
REGISTER_HELP(REPORTS_DETAIL3,
              "All methods have the same signature: <b>Dict report(Session "
              "session, List argv, Dict options)</b>, where:");
REGISTER_HELP(
    REPORTS_DETAIL4,
    "@li session - Session object used by the report to obtain the data.");
REGISTER_HELP(REPORTS_DETAIL5,
              "@li argv (optional) - Array of strings representing additional "
              "arguments.");
REGISTER_HELP(REPORTS_DETAIL6,
              "@li options (optional) - Dictionary with values for various "
              "report-specific options.");
REGISTER_HELP(REPORTS_DETAIL7,
              "Each report returns a dictionary with the following keys:");
REGISTER_HELP(REPORTS_DETAIL8,
              "@li report (required) - List of JSON objects containing the "
              "report. The number and types of items in this list depend on "
              "type of the report.");
REGISTER_HELP(REPORTS_DETAIL9,
              "For more information on a report use: "
              "<b>shell.reports.help('report_name')</b>.");

static constexpr auto k_report_key = "report";
static constexpr auto k_vertical_key = "vertical";
static constexpr auto k_wildcard_character = "*";
static constexpr auto k_report_type_list = "list";
static constexpr auto k_report_type_report = "report";
static constexpr auto k_report_type_print = "print";
static constexpr auto k_report_type_custom = "custom";

Report::Type to_report_type(const std::string &type) {
  if (k_report_type_list == type) {
    return Report::Type::LIST;
  } else if (k_report_type_report == type) {
    return Report::Type::REPORT;
  } else if (k_report_type_print == type) {
    return Report::Type::PRINT;
  } else {
    // user cannot register reports of custom type
    throw shcore::Exception::argument_error(
        "Report type must be one of: " +
        shcore::str_join(
            std::vector<std::string>{k_report_type_list, k_report_type_report,
                                     k_report_type_print},
            ", ") +
        ".");
  }
}

std::string to_string(Report::Type type) {
  switch (type) {
    case Report::Type::LIST:
      return k_report_type_list;

    case Report::Type::REPORT:
      return k_report_type_report;

    case Report::Type::PRINT:
      return k_report_type_print;

    case Report::Type::CUSTOM:
      return k_report_type_custom;
  }

  throw std::logic_error("Unknown value of Report::Type");
}

std::string to_string(const Report::Argc &argc) {
  std::string result;

  if (argc.first == 0 && argc.second == Report::k_asterisk) {
    result += "any number of";
  } else {
    result += std::to_string(argc.first);

    if (argc.first != argc.second) {
      result += "-";

      if (argc.second == Report::k_asterisk) {
        result += k_wildcard_character;
      } else {
        result += std::to_string(argc.second);
      }
    }
  }

  result += " argument";

  if (argc.first != argc.second || argc.first != 1) {
    result += "s";
  }

  return result;
}

void validate_option_type(shcore::Value_type type) {
  switch (type) {
    case shcore::Value_type::String:
    case shcore::Value_type::Bool:
    case shcore::Value_type::Integer:
    case shcore::Value_type::Float:
      // valid type
      return;

    default:
      throw shcore::Exception::argument_error(
          "Option type must be one of: 'string', 'bool', 'integer', 'float'.");
  }
}

uint32_t to_uint32(const std::string &v) {
  try {
    return shcore::lexical_cast<uint32_t>(v);
  } catch (const std::invalid_argument &) {
    throw shcore::Exception::argument_error("Cannot convert '" + v +
                                            "' to an unsigned integer.");
  }
}

Report::Argc get_report_argc(const std::string &argc_s) {
  if (argc_s.empty()) {
    return {0, 0};
  }

  const auto argc = shcore::str_split(argc_s, "-");
  Report::Argc result;

  switch (argc.size()) {
    case 1:
      if (argc[0] == k_wildcard_character) {
        result = std::make_pair(0, Report::k_asterisk);
      } else {
        const auto limit = to_uint32(argc[0]);
        result = std::make_pair(limit, limit);
      }
      break;

    case 2: {
      const auto lower = to_uint32(argc[0]);
      const auto upper = argc[1] == k_wildcard_character ? Report::k_asterisk
                                                         : to_uint32(argc[1]);
      result = std::make_pair(lower, upper);
    } break;

    default:
      throw shcore::Exception::argument_error(
          "The value associated with the key named 'argc' has wrong format.");
  }

  return result;
}

Report::Examples get_report_examples(const shcore::Array_t &ex) {
  if (!ex) return {};

  Report::Examples examples;
  examples.reserve(ex->size());

  for (const auto &e : *ex) {
    if (shcore::Value_type::Map != e.get_type()) {
      throw shcore::Exception::argument_error(
          "The value associated with the key named 'examples' should be a "
          "list of dictionaries.");
    }

    Report::Example example;
    shcore::Option_unpacker{e.as_map()}
        .required("description", &example.description)
        .optional("args", &example.args)
        .optional("options", &example.options)
        .end();

    examples.emplace_back(std::move(example));
  }

  return examples;
}

Report::Option *find_option(const Report::Options &options,
                            const std::string &name) {
  const auto pos =
      std::find_if(options.begin(), options.end(),
                   [&name](const Report::Options::value_type &o) {
                     return o->parameter->name == name || o->short_name == name;
                   });

  return options.end() == pos ? nullptr : pos->get();
}

std::vector<shcore::Help_registry::Example> get_function_examples(
    const Report &report) {
  std::vector<shcore::Help_registry::Example> examples;

  for (const auto &example : report.examples()) {
    shcore::Help_registry::Example e;

    e.code = report.name() + "(session";

    if (report.argc().second > 0 || report.has_options()) {
      e.code.append(", [");

      for (const auto &a : example.args) {
        e.code.append(shcore::quote_string(a, '"')).append(", ");
      }

      if (!example.args.empty()) {
        e.code.pop_back();
        e.code.pop_back();
      }

      e.code.append("]");
    }

    if (report.has_options()) {
      e.code.append(", {");

      for (const auto &o : example.options) {
        const auto spec = find_option(report.options(), o.first);
        assert(spec);

        e.code.append(shcore::quote_string(spec->parameter->name, '"'))
            .append(": ");

        switch (spec->parameter->type()) {
          case shcore::Value_type::String:
            e.code.append(shcore::quote_string(o.second, '"'));
            break;

          case shcore::Value_type::Bool:
          case shcore::Value_type::Integer:
          case shcore::Value_type::Float:
            e.code.append(o.second);
            break;

          default:
            throw std::logic_error("Unknown option type.");
        }

        e.code.append(", ");
      }

      if (!example.options.empty()) {
        e.code.pop_back();
        e.code.pop_back();
      }

      e.code.append("}");
    }

    e.code.append(")");
    e.description = example.description;

    examples.emplace_back(std::move(e));
  }

  return examples;
}

class Native_report_function : public shcore::Cpp_function {
 public:
  Native_report_function(const std::string &name,
                         const Report::Native_report &report)
      : shcore::Cpp_function(
            &m_metadata, [report](const shcore::Argument_list &args) {
              const auto &session = args.object_at<ShellBaseSession>(0);

              if (!session) {
                throw shcore::Exception::argument_error(
                    "Argument #1 is expected to be one of 'ClassicSession, "
                    "Session'.");
              }

              if (!session->is_open()) {
                throw shcore::Exception::argument_error(
                    "Executing the report requires an open session.");
              }

              // it's not an array if it's null
              const auto argv = args.size() < 2 || args[1].get_type() !=
                                                       shcore::Value_type::Array
                                    ? shcore::make_array()
                                    : args.array_at(1);
              // it's not a map if it's null
              const auto options =
                  args.size() < 3 ||
                          args[2].get_type() != shcore::Value_type::Map
                      ? shcore::make_dict()
                      : args.map_at(2);

              return shcore::Value(report(session, argv, options));
            }) {
    for (std::size_t i = 0; i < shcore::array_size(m_metadata.name); ++i) {
      m_metadata.name[i] = name;
    }

    m_metadata.param_types = {{"session", shcore::Value_type::Object},
                              {"?argv", shcore::Value_type::Array},
                              {"?options", shcore::Value_type::Map}};
    m_metadata.signature = gen_signature(m_metadata.param_types);
    m_metadata.return_type = shcore::Value_type::Map;
  }

  ~Native_report_function() override = default;

 private:
  shcore::Cpp_function::Metadata m_metadata;
};

std::string list_formatter(const shcore::Array_t &a,
                           const shcore::Dictionary_t &options) {
  bool is_vertical = false;
  shcore::Option_unpacker{options}.required(k_vertical_key, &is_vertical).end();

  if (a->size() < 1) {
    throw shcore::Exception::runtime_error(
        "List report should contain at least one row.");
  }

  for (const auto &row : *a) {
    if (row.get_type() != shcore::Value_type::Array) {
      throw shcore::Exception::runtime_error(
          "List report should return a list of lists.");
    }
  }

  bool is_gui_mode = mysqlsh::current_shell_options()->get().gui_mode;
  const auto output = is_vertical
                          ? reports::vertical_formatter(a)
                          : (is_gui_mode ? reports::gui_table_formatter(a)
                                         : reports::table_formatter(a));

  // output is empty if report returned just names of columns
  return (!is_gui_mode && (output.empty() || output == "\n"))
             ? "Report returned no data."
             : output;
}

std::string report_formatter(const shcore::Array_t &a,
                             const shcore::Dictionary_t &) {
  if (a->size() != 1) {
    throw shcore::Exception::runtime_error(
        "Report of type 'report' should contain exactly one element.");
  }

  return a->at(0).yaml();
}

std::string print_formatter(const shcore::Array_t &,
                            const shcore::Dictionary_t &) {
  // print formatter suppresses all output
  return "";
}

Parameter_definition::Options upcast(const Report::Options &in) {
  return Parameter_definition::Options(in.begin(), in.end());
}

Report::Options downcast(const Parameter_definition::Options &in) {
  Report::Options out;

  for (const auto &p : in) {
    out.emplace_back(std::dynamic_pointer_cast<Report::Option>(p));
  }

  return out;
}

struct Argv_validator : public shcore::Parameter_validator {
  explicit Argv_validator(const Report::Argc &argc) : m_argc(argc) {}

  void validate(const shcore::Parameter &param, const shcore::Value &data,
                shcore::Parameter_context *context) const override {
    Parameter_validator::validate(param, data, context);

    const auto argv = data.as_array();
    const auto argc = argv ? argv->size() : 0;

    if (argc < m_argc.first || argc > m_argc.second) {
      throw shcore::Exception::argument_error(shcore::str_format(
          "%s 'argv' is expecting %s.", context->str().c_str(),
          to_string(m_argc).c_str()));
    }
  }

 private:
  Report::Argc m_argc;
};

std::string normalize_report_name(const std::string &name) {
  return shcore::str_lower(shcore::str_replace(name, "-", "_"));
}

}  // namespace

class Shell_reports::Report_options : public shcore::Options {
 public:
  explicit Report_options(std::unique_ptr<Report> r)
      : m_report_name(std::move(r->m_name)),
        m_options(std::move(r->m_options)),
        m_argc(std::move(r->m_argc)),
        m_formatter(std::move(r->m_formatter)) {
    {
      // each report has an option to display help
      Report::Option help{"help", shcore::Value_type::Bool};
      help.brief = "Display this help and exit.";
      help.short_name = "h";

      add_startup_options()(&m_show_help, false, help.command_line_names(), "");

      m_command_line_options.emplace_back(std::move(help));
    }

    // list reports can be displayed vertically
    if (Report::Type::LIST == r->type()) {
      Report::Option vertical{"vertical", shcore::Value_type::Bool};
      vertical.brief = "Display records vertically.";
      vertical.short_name = "E";

      add_startup_options()(&m_vertical, false, vertical.command_line_names(),
                            "");

      m_command_line_options.emplace_back(std::move(vertical));
    }

    // add options expected by the report
    for (const auto &o : m_options) {
      const auto &p = o->parameter;

      // register the option
      add_startup_options()(
          o->command_line_names(), "",
          [&o, &p, this](const std::string &, const char *new_value) {
            // convert to the expected type
            shcore::Value value;

            switch (p->type()) {
              case shcore::Value_type::String: {
                value = shcore::Value(new_value ? new_value : "");

                shcore::Parameter_context context{
                    m_report_name + ":",
                    {{"option '--" + o->command_line_name() + "'", {}}}};
                p->validator<shcore::String_validator>()->validate(*p, value,
                                                                   &context);

                break;
              }

              case shcore::Value_type::Bool:
                value = shcore::Value(true);
                break;

              case shcore::Value_type::Integer:
                try {
                  value =
                      shcore::Value(shcore::lexical_cast<int64_t>(new_value));
                } catch (const std::invalid_argument &) {
                  throw prepare_exception("cannot convert '" +
                                          std::string(new_value) +
                                          "' to a signed integer");
                }
                break;

              case shcore::Value_type::Float:
                try {
                  value =
                      shcore::Value(shcore::lexical_cast<double>(new_value));
                } catch (const std::invalid_argument &) {
                  throw prepare_exception("cannot convert '" +
                                          std::string(new_value) +
                                          "' to a floating-point number");
                }
                break;

              default:
                throw std::logic_error("Unexpected type: " +
                                       shcore::type_name(p->type()));
            }

            // store the value
            m_parsed_options->emplace(p->name, value);

            // if option is required, erase it from the list of missing ones
            if (o->is_required()) {
              m_missing_options.erase(
                  std::remove(m_missing_options.begin(),
                              m_missing_options.end(), p->name),
                  m_missing_options.end());
            }
          },
          o->command_line_name());
    }

    initialize_help(r->brief(), r->details(), r->examples());
  }

  const std::string &name() const { return m_report_name; }

  bool show_help() const { return m_show_help; }

  bool vertical() const { return m_vertical; }

  void parse_args(const std::vector<std::string> &args) {
    // reset previous values and prepare for parsing
    reset();

    // prepare arguments
    std::vector<const char *> raw_args;
    raw_args.emplace_back(m_report_name.c_str());

    for (const auto &a : args) {
      raw_args.emplace_back(a.c_str());
    }

    // NULL must be the last argument in the vector
    raw_args.emplace_back(nullptr);

    // validate and parse provided arguments
    // NULL should not be included in the size of arguments
    handle_cmdline_options(raw_args.size() - 1, &raw_args[0], false,
                           [this](Iterator *it) {
                             // handle extra arguments
                             if (it->valid() && '-' != it->option()[0]) {
                               // first option which does not begin with '-'
                               // marks the beginning of arguments
                               const auto cmdline = it->iterator();

                               while (cmdline->valid()) {
                                 m_arguments.emplace_back(cmdline->get());
                               }

                               return true;
                             } else {
                               return false;
                             }
                           });

    // if help was not requested, perform additional validations
    if (!show_help()) {
      const auto usage = "For usage information please run: \\show " +
                         m_report_name + " --help";

      // check if all required options are here
      if (!m_missing_options.empty()) {
        std::transform(m_missing_options.begin(), m_missing_options.end(),
                       m_missing_options.begin(),
                       [](const std::string &o) { return "--" + o; });
        throw prepare_exception("missing required option(s): " +
                                shcore::str_join(m_missing_options, ", ") +
                                ". " + usage);
      }

      // check if there's the right amount of additional arguments
      const auto argc = m_arguments.size();

      if (argc < m_argc.first || argc > m_argc.second) {
        throw prepare_exception("report is expecting " + to_string(m_argc) +
                                ", " + std::to_string(argc) + " provided. " +
                                usage);
      }

      // optionally add additional arguments
      for (const auto &a : m_arguments) {
        m_parsed_arguments->emplace_back(a);
      }
    }
  }

  shcore::Array_t get_parsed_arguments() const {
    if (show_help()) {
      throw std::logic_error(
          "User requested help, arguments are not available.");
    }

    return m_parsed_arguments;
  }

  shcore::Dictionary_t get_parsed_options() const {
    if (show_help()) {
      throw std::logic_error("User requested help, options are not available.");
    }

    return m_parsed_options;
  }

  std::string help() const {
    return shcore::Help_manager{}.get_help(*m_help_topic);
  }

  bool requires_argv() const { return m_argc.second > 0 || requires_options(); }

  bool requires_options() const { return !m_options.empty(); }

  const Report::Formatter &formatter() const { return m_formatter; }

 private:
  void reset() {
    // reset to default values
    m_show_help = false;
    m_vertical = false;

    m_missing_options.clear();
    m_arguments.clear();
    m_parsed_options = shcore::make_dict();
    m_parsed_arguments = shcore::make_array();

    // mark all required options as missing
    for (const auto &o : m_options) {
      if (o->is_required()) {
        m_missing_options.emplace_back(o->parameter->name);
      }
    }
  }

  void initialize_help(const std::string &brief,
                       const std::vector<std::string> &details,
                       const Report::Examples &examples) {
    if (nullptr == m_help_topic) {
      const auto help = shcore::Help_registry::get();
      const auto prefix = "CMD_SHOW_" + shcore::str_upper(m_report_name);
      const auto topic =
          help->add_help_topic(m_report_name, shcore::Topic_type::COMMAND,
                               prefix, "CMD_SHOW",
                               shcore::IShell_core::all_scripting_modes(),
                               shcore::Keyword_location::LOCAL_CTX)
              .back();

      if (!brief.empty())
        help->add_help(prefix, "BRIEF", brief,
                       shcore::Keyword_location::LOCAL_CTX);

      const auto has_arguments = m_argc.second > 0;

      {
        std::vector<std::string> syntax;
        std::string required;

        for (const auto &o : m_options) {
          if (o->is_required()) {
            required.append(" ").append(
                shcore::str_join(o->command_line_names(), "|"));
          }
        }

        for (const auto &command : {"show", "watch"}) {
          std::string line = "\\" + std::string(command) + " <b>" +
                             m_report_name + "</b>" + required + " [OPTIONS]";

          if (has_arguments) {
            line += " [ARGS]";
          }

          syntax.emplace_back(std::move(line));
        }

        help->add_help(prefix, "SYNTAX", syntax,
                       shcore::Keyword_location::LOCAL_CTX);
      }

      {
        std::vector<std::string> contents;

        for (const auto &d : details) {
          contents.emplace_back(d);
        }

        contents.emplace_back("Options:");

        const auto add_option = [&contents](const Report::Option &o) {
          std::string line;

          line.append("@entrynl{");
          line.append(shcore::str_join(o.command_line_names(), ", "));
          line.append("} ").append(o.command_line_brief());

          contents.emplace_back(std::move(line));
        };

        for (const auto &o : m_command_line_options) {
          add_option(o);
        }

        for (const auto &o : m_options) {
          add_option(*o);
        }

        for (const auto &o : m_options) {
          for (const auto &d : o->details) {
            contents.emplace_back(d);
          }
        }

        if (has_arguments) {
          contents.emplace_back("Arguments:");
          contents.emplace_back("This report accepts " + to_string(m_argc) +
                                ".");
        }

        help->add_help(prefix, "DETAIL", contents,
                       shcore::Keyword_location::LOCAL_CTX);
      }

      {
        // examples
        std::vector<shcore::Help_registry::Example> ex;

        for (const auto &example : examples) {
          shcore::Help_registry::Example e;

          e.code = "\\show " + m_report_name;

          for (const auto &o : example.options) {
            const auto spec = find_option(m_options, o.first);
            assert(spec);

            const auto name =
                (o.first.length() > 1 ? "-" : "") + ("-" + o.first);

            switch (spec->parameter->type()) {
              case shcore::Value_type::String:
                e.code.append(" ").append(name);

                if (!o.second.empty()) {
                  e.code.append(" ");

                  if (std::string::npos != o.second.find(" ")) {
                    e.code.append(shcore::quote_string(o.second, '"'));
                  } else {
                    e.code.append(o.second);
                  }
                }
                break;

              case shcore::Value_type::Bool:
                if ("true" == o.second) {
                  e.code.append(" ").append(name);
                }
                break;

              case shcore::Value_type::Integer:
              case shcore::Value_type::Float:
                e.code.append(" ").append(name).append(" ").append(o.second);
                break;

              default:
                throw std::logic_error("Unknown option type.");
            }
          }

          for (const auto &a : example.args) {
            e.code.append(" ");

            if (std::string::npos != a.find(" ")) {
              e.code.append(shcore::quote_string(a, '"'));
            } else {
              e.code.append(a);
            }
          }

          e.description = example.description;

          ex.emplace_back(std::move(e));
        }

        help->add_help(prefix, ex, shcore::Keyword_location::LOCAL_CTX);
      }

      m_help_topic = topic;
    }
  }

  shcore::Exception prepare_exception(const std::string &e) const {
    return shcore::Exception::argument_error(m_report_name + ": " + e);
  }

  const std::string m_report_name;
  const Report::Options m_options;
  const Report::Argc m_argc;
  const Report::Formatter m_formatter;
  shcore::Help_topic *m_help_topic = nullptr;
  bool m_show_help;
  bool m_vertical;
  std::vector<std::string> m_missing_options;
  std::vector<std::string> m_arguments;
  shcore::Dictionary_t m_parsed_options;
  shcore::Array_t m_parsed_arguments;
  std::vector<Report::Option> m_command_line_options;
};

Report::Option::Option(const std::string &n, shcore::Value_type type,
                       bool required)
    : Parameter_definition(n, type,
                           required ? shcore::Param_flag::Mandatory
                                    : shcore::Param_flag::Optional) {}

std::vector<std::string> Report::Option::command_line_names() const {
  std::vector<std::string> names;
  const auto &p = this->parameter;
  std::string long_name = "--" + command_line_name();

  // non-Boolean reports require a value
  if (shcore::Value_type::Bool != p->type()) {
    if (empty) long_name += "[";
    long_name += "=" + shcore::str_lower(shcore::type_name(p->type()));
    if (empty) long_name += "]";
  }

  names.emplace_back(std::move(long_name));

  // options can optionally have short (one letter) form
  if (!this->short_name.empty()) {
    names.emplace_back("-" + this->short_name);
  }

  return names;
}

std::string Report::Option::command_line_name() const {
  return shcore::from_camel_case_to_dashes(parameter->name);
}

std::string Report::Option::command_line_brief() const {
  std::string cmdline_brief;

  if (this->is_required()) {
    cmdline_brief += "(required) ";
  }

  cmdline_brief += this->brief;

  const auto validator = this->parameter->validator<shcore::String_validator>();

  if (validator) {
    const auto &allowed = validator->allowed();

    if (!allowed.empty()) {
      cmdline_brief +=
          " Allowed values: " + shcore::str_join(allowed, ", ") + ".";
    }
  }

  return cmdline_brief;
}

const uint32_t Report::k_asterisk = std::numeric_limits<uint32_t>::max();

Report::Report(const std::string &name, Type type,
               const shcore::Function_base_ref &function)
    : m_name(name), m_type(type), m_function(function) {
  switch (type) {
    case Type::LIST:
      m_formatter = list_formatter;
      break;

    case Type::REPORT:
      m_formatter = report_formatter;
      break;

    case Type::PRINT:
      m_formatter = print_formatter;
      break;

    default:
      // formatter needs to be set explicitly
      break;
  }
}

Report::Report(const std::string &name, Type type,
               const Native_report &function)
    : Report(name, type,
             std::make_shared<Native_report_function>(name, function)) {}

const std::string &Report::name() const { return m_name; }

Report::Type Report::type() const { return m_type; }

const shcore::Function_base_ref &Report::function() const { return m_function; }

const std::string &Report::brief() const { return m_brief; }

void Report::set_brief(const std::string &brief) { m_brief = brief; }

const std::vector<std::string> &Report::details() const { return m_details; }

void Report::set_details(const std::vector<std::string> &details) {
  m_details = details;
}

const Report::Options &Report::options() const { return m_options; }

void Report::set_options(const Options &options) {
  // Validates options are not in the reserved options for the \watch
  // command. Other options such as help and vertical(E) will be properly
  // reported as clashes with command line options
  std::set<std::string> reserved = {"interval", "nocls", "i"};

  for (const auto &o : options) {
    validate_option(*o);

    if (reserved.find(o->parameter->name) != reserved.end()) {
      throw shcore::Exception::argument_error(
          "Option '" + o->parameter->name +
          "' is reserved for use with the \\watch command.");
    }

    if (!o->short_name.empty() &&
        reserved.find(o->short_name) != reserved.end()) {
      throw shcore::Exception::argument_error(
          "Short name '" + o->short_name +
          "' is reserved for use with the \\watch command.");
    }
  }

  m_options = options;
}

const Report::Argc &Report::argc() const { return m_argc; }

void Report::set_argc(const Argc &argc) {
  if (argc.first > argc.second) {
    throw shcore::Exception::argument_error(
        "The lower limit of 'argc' cannot be greater than upper limit.");
  }

  m_argc = argc;
}

const Report::Formatter &Report::formatter() const { return m_formatter; }

void Report::set_formatter(const Report::Formatter &formatter) {
  m_formatter = formatter;
}

const Report::Examples &Report::examples() const { return m_examples; }

void Report::set_examples(Examples &&examples) {
  for (const auto &e : examples) {
    validate_example(e);
  }

  m_examples = std::move(examples);
}

bool Report::requires_options() const {
  return m_options.end() !=
         std::find_if(m_options.begin(), m_options.end(),
                      [](const std::shared_ptr<Report::Option> &o) {
                        return o->parameter->flag ==
                               shcore::Param_flag::Mandatory;
                      });
}

bool Report::has_options() const { return !m_options.empty(); }

void Report::validate_option(const Option &option) {
  if (!option.short_name.empty()) {
    if (option.short_name.length() > 1) {
      throw shcore::Exception::argument_error(
          "Short name of an option must be exactly one character long.");
    }

    if (!std::isalnum(option.short_name[0], std::locale{})) {
      throw shcore::Exception::argument_error(
          "Short name of an option must be an alphanumeric character.");
    }
  }

  validate_option_type(option.parameter->type());

  if (shcore::Value_type::Bool == option.parameter->type() &&
      option.is_required()) {
    throw shcore::Exception::argument_error(
        "Option of type 'bool' cannot be required.");
  }

  if (option.empty && shcore::Value_type::String != option.parameter->type()) {
    throw shcore::Exception::argument_error(
        "Only option of type 'string' can accept empty values.");
  }
}

void Report::validate_example(const Example &example) const {
  if (example.args.size() < m_argc.first ||
      example.args.size() > m_argc.second) {
    throw shcore::Exception::argument_error(
        "Report expects " + to_string(m_argc) +
        ", example has: " + std::to_string(example.args.size()));
  }

  for (const auto &o : example.options) {
    const auto spec = find_option(m_options, o.first);

    if (nullptr == spec) {
      throw shcore::Exception::argument_error(
          "Option named '" + o.first + "' used in example does not exist.");
    }

    const auto type = spec->parameter->type();

    if (type == shcore::Value_type::Bool && "true" != o.second &&
        "false" != o.second) {
      throw shcore::Exception::argument_error(
          "Option named '" + o.first +
          "' used in example is a Boolean, should have value 'true' or "
          "'false'.");
    }

    if (type == shcore::Value_type::String && "" == o.second && !spec->empty) {
      throw shcore::Exception::argument_error(
          "Option named '" + o.first +
          "' used in example cannot be an empty string.");
    }
  }
}

Shell_reports::Shell_reports(const std::string &name,
                             const std::string &qualified_name)
    : Extensible_object(name, qualified_name, true) {
  // Do not register the reports for the second time if we're inside of a
  // thread.
  if (mysqlshdk::utils::in_main_thread()) {
    reports::register_query_report(this);
    reports::register_threads_report(this);
    reports::register_thread_report(this);
  }
}

// needs to be defined here due to Shell_reports::Report_options being
// incomplete type
Shell_reports::~Shell_reports() {
  auto topic = shcore::Help_registry::get()->get_topic(
      get_qualified_name(), true, shcore::Topic_mask::any(), true);

  if (topic && topic->is_enabled()) {
    // disable help for all the children, including methods except the help
    // member This is only needed for the test suite where the Reports object is
    // recreated
    for (const auto &child : topic->m_childs) {
      // Mode does not really matter as "help" is the same in all
      if (child->get_name(shcore::IShell_core::Mode::JavaScript) != "help") {
        child->set_enabled(false);
      }
    }
  }
}

std::shared_ptr<Parameter_definition> Shell_reports::start_parsing_parameter(
    const shcore::Dictionary_t &definition,
    shcore::Option_unpacker *unpacker) const {
  auto option = std::make_shared<Report::Option>("");

  unpacker->optional("shortcut", &option->short_name);
  unpacker->optional("empty", &option->empty);

  // type is optional here, but base class requires it, insert default value if
  // not present
  if (definition->end() == definition->find("type")) {
    definition->emplace("type", "string");
  }

  return option;
}

void Shell_reports::register_report(const std::string &name,
                                    const std::string &type,
                                    const shcore::Function_base_ref &report,
                                    const shcore::Dictionary_t &description) {
  if (!report) {
    throw shcore::Exception::type_error(
        "Argument #3 is expected to be a function");
  }

  auto new_report =
      std::make_unique<Report>(name, to_report_type(type), report);

  if (description) {
    std::string brief;
    std::vector<std::string> details;
    shcore::Array_t options;
    std::string argc;
    shcore::Array_t examples;

    shcore::Option_unpacker{description}
        .optional("brief", &brief)
        .optional("details", &details)
        .optional("options", &options)
        .optional("argc", &argc)
        .optional("examples", &examples)
        .end();

    new_report->set_brief(brief);
    new_report->set_details(details);
    shcore::Parameter_context context{"", {{"option", {}}}};
    new_report->set_options(downcast(
        parse_parameters(options, &context, kReportOptionTypes, false)));
    new_report->set_argc(get_report_argc(argc));
    new_report->set_examples(get_report_examples(examples));
  }

  register_report(std::move(new_report));
}

void Shell_reports::register_report(std::unique_ptr<Report> report) {
  auto normalized_name = normalize_report_name(report->name());

  {
    const auto it = m_reports.find(normalized_name);

    if (m_reports.end() != it) {
      const auto error = report->name() == it->second->name()
                             ? "Duplicate report: " + report->name()
                             : "Name '" + report->name() +
                                   "' conflicts with an existing report: " +
                                   it->second->name();
      throw shcore::Exception::argument_error(error);
    }
  }

  std::vector<std::string> details = {
      "This is a '" + to_string(report->type()) + "' type report."};
  details.insert(details.end(), report->details().begin(),
                 report->details().end());

  std::vector<std::shared_ptr<Parameter_definition>> parameters;

  {
    // first parameter - session (required)
    auto session = std::make_shared<Parameter_definition>(
        "session", shcore::Value_type::Object, shcore::Param_flag::Mandatory);
    session->brief = "A Session object to be used to execute the report.";
    session->parameter->validator<shcore::Object_validator>()->set_allowed(
        {"ClassicSession", "Session"});

    parameters.emplace_back(std::move(session));
  }

  // second parameter - argv - if report expects arguments or has any options
  if (report->argc().second > 0 || report->has_options()) {
    // argv is mandatory if report expects at least one argument or if options
    // are required
    auto argv = std::make_shared<Parameter_definition>(
        "argv", shcore::Value_type::Array,
        report->argc().first > 0 || report->requires_options()
            ? shcore::Param_flag::Mandatory
            : shcore::Param_flag::Optional);
    argv->brief =
        "Extra arguments. Report expects " + to_string(report->argc()) + ".";
    argv->parameter->set_validator(
        std::make_unique<Argv_validator>(report->argc()));

    parameters.emplace_back(std::move(argv));
  }

  // third parameter - options - only if report has any options
  if (report->has_options()) {
    // this parameter is mandatory if any of the options is required
    auto options = std::make_shared<Parameter_definition>(
        "options", shcore::Value_type::Map,
        report->requires_options() ? shcore::Param_flag::Mandatory
                                   : shcore::Param_flag::Optional);
    options->brief = "Options expected by the report.";
    options->set_options(upcast(report->m_options));

    parameters.emplace_back(std::move(options));
  }

  // Make sure the report_option creation does not error before
  // attempting registering both the report and the function, for
  // that, backups the data needed for the function registration
  const auto brief = report->brief();
  const auto function = report->function();
  const auto examples = get_function_examples(*report);

  auto roptions = std::make_unique<Report_options>(std::move(report));

  // Reports must have the same name in both Python and JavaScript
  auto fd = std::make_shared<Function_definition>(
      roptions->name() + "|" + roptions->name(), parameters, brief, details,
      examples);

  // method should have the same name in both JS and Python
  register_function(fd, function);

  m_reports.emplace(std::move(normalized_name), std::move(roptions));
}

std::vector<std::string> Shell_reports::list_reports() const {
  std::vector<std::string> reports;

  std::transform(m_reports.begin(), m_reports.end(),
                 std::back_inserter(reports),
                 [](const decltype(m_reports)::value_type &pair) {
                   return pair.second->name();
                 });

  return reports;
}

std::string Shell_reports::call_report(
    const std::string &name, const std::shared_ptr<ShellBaseSession> &session,
    const std::vector<std::string> &args) {
  // report must exist
  const auto report_iterator = m_reports.find(normalize_report_name(name));

  if (m_reports.end() == report_iterator) {
    throw shcore::Exception::argument_error("Unknown report: " + name);
  }

  // arguments must be valid
  const auto report_options = report_iterator->second.get();
  report_options->parse_args(args);

  if (report_options->show_help()) {
    // get help
    return report_options->help();
  }

  // session must be open
  if (!session || !session->is_open()) {
    throw shcore::Exception::argument_error(
        "Executing the report requires an existing, open session.");
  }

  // prepare arguments
  shcore::Argument_list arguments;
  arguments.push_back(shcore::Value(session));

  if (report_options->requires_argv()) {
    arguments.push_back(shcore::Value(report_options->get_parsed_arguments()));
  }

  if (report_options->requires_options()) {
    // convert args into dictionary
    arguments.push_back(shcore::Value(report_options->get_parsed_options()));
  }

  // call the report
  const auto result = call(report_options->name(), arguments);

  if (shcore::Value_type::Map != result.get_type()) {
    throw shcore::Exception::runtime_error(
        "Report should return a dictionary.");
  }

  shcore::Array_t report;
  shcore::Option_unpacker{result.as_map()}
      .required(k_report_key, &report)
      .end();

  if (!report) {
    throw shcore::Exception::runtime_error(
        "Option 'report' is expected to be of type Array, but is Null");
  }

  const auto display_options = shcore::make_dict();
  display_options->emplace(k_vertical_key, report_options->vertical());
  return report_options->formatter()(report, display_options);
}

}  // namespace mysqlsh
