/*
 * Copyright (c) 2017, 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/util/upgrade_check.h"

#include <algorithm>
#include <exception>
#include <string>

#include "modules/util/upgrade_checker/common.h"
#include "modules/util/upgrade_checker/manual_check.h"
#include "modules/util/upgrade_checker/upgrade_check_registry.h"
#include "mysqlshdk/libs/utils/utils_string.h"
#include "mysqlshdk/libs/utils/version.h"

namespace mysqlsh {
namespace upgrade_checker {

using mysqlshdk::utils::Version;
namespace suggested_version {
/*
 * RULE: Do NOT skip bugfix or LTS versions.
 *
 * bugfix:     5.7.whatever    want-to: 9.0   suggestion=8.0
 * bugfix:     8.0.whatever    want-to: 9.0   suggestion=8.4
 * Innovation: 8.1..8.3        want-to: 9.0   suggestion=8.4
 * LTS:        8.4.whatever    want-to: 10.0  suggestion=9.7
 * Innovation: 9.0..9.6        want-to: 10.0	suggestion=9.7
 * LTS:        9.7.whatever    want-to: 10.7	suggestion=10.7
 * Innovation: 10.0..10.6
 * LTS:        10.7.whatever
 */
std::optional<Version> get_next_suggested_lts_version(
    const Version &version, std::string *out_key = nullptr) {
  std::optional<Version> next_lts;
  decltype(k_latest_versions)::const_iterator item = k_latest_versions.end();

  // versions before 8.0.0 must be first updated to 8.0 series
  if (version < Version(8, 0, 0)) {
    item = k_latest_versions.find("8.0");
  } else if (mysqlshdk::utils::is_lts_release(version) &&
             !(version.get_major() == 8 && version.get_minor() == 0)) {
    // We might be already on the LTS release of the series, so it jumps to
    // the LTS in the next series (If they exist)
    auto first_lts = get_first_lts_version(Version(version.get_major() + 1, 0));
    item = k_latest_versions.find(first_lts.get_short());
  } else {
    // in other cases lets find the latest LTS version in the series
    item = k_latest_versions.find(get_first_lts_version(version).get_short());
  }

  if (item != k_latest_versions.end()) {
    if (out_key) {
      *out_key = item->first;
      return item->second;
    }
  }

  return {};
}

std::string format_suggested_version_message(
    const Version &serverVersion, const Version &targetVersion,
    const std::string &suggestedTargetVersion) {
  return shcore::str_format(
      "Upgrading MySQL Server from version %s to %s is not supported. Please "
      "consider running the check using the following option: targetVersion=%s",
      serverVersion.get_base().c_str(), targetVersion.get_base().c_str(),
      suggestedTargetVersion.c_str());
}
}  // namespace suggested_version

std::string check_for_version_suggestion(const Version &serverVersion,
                                         const Version &targetVersion) {
  std::string suggested_target_version;
  auto suggested = suggested_version::get_next_suggested_lts_version(
      serverVersion, &suggested_target_version);
  if (suggested.value_or(targetVersion) >= targetVersion) return {};

  return suggested_version::format_suggested_version_message(
      serverVersion, targetVersion, suggested_target_version);
}

bool run_checks_for_upgrade(const Upgrade_check_config &config,
                            Upgrade_check_output_formatter &print) {
  assert(config.session());

  print.check_info(
      config.session()->get_connection_options().uri_endpoint(),
      config.upgrade_info().server_version_long,
      config.upgrade_info().target_version.get_base(),
      config.upgrade_info().explicit_target_version,
      check_for_version_suggestion(config.upgrade_info().server_version,
                                   config.upgrade_info().target_version));
  config.upgrade_info().validate();

  Upgrade_check_registry::Upgrade_check_vec rejected;
  const auto checklist = Upgrade_check_registry::create_checklist(
      config, false, config.warn_on_excludes() ? &rejected : nullptr);

  int errors = 0, warnings = 0, notices = 0;
  const auto update_counts = [&errors, &warnings,
                              &notices](Upgrade_issue::Level level) {
    switch (level) {
      case Upgrade_issue::ERROR:
        errors++;
        break;
      case Upgrade_issue::WARNING:
        warnings++;
        break;
      default:
        notices++;
        break;
    }
  };

  Checker_cache cache{config.db_filters()};

  // Workaround for 5.7 "No database selected/Corrupted" UPGRADE bug present
  // up to 5.7.39
  config.session()->execute("USE mysql;");

  for (const auto &check : checklist)
    if (check->is_runnable()) {
      try {
        print.check_title(*check);

        const auto issues = config.filter_issues(
            check->run(config.session(), config.upgrade_info(), &cache));
        for (const auto &issue : issues) update_counts(issue.level);
        print.check_results(*check, issues);
      } catch (const Check_configuration_error &e) {
        print.check_error(*check, e.what(), false);
      } catch (const std::exception &e) {
        print.check_error(*check, e.what());
      }
    } else {
      update_counts(dynamic_cast<Manual_check *>(check.get())->get_level());
      print.manual_check(*check);
    }

  std::string summary;
  if (errors > 0) {
    summary = shcore::str_format(
        "%i errors were found. Please correct these issues before upgrading "
        "to avoid compatibility issues.",
        errors);
  } else if ((warnings > 0) || (notices > 0)) {
    summary =
        "No fatal errors were found that would prevent an upgrade, "
        "but some potential issues were detected. Please ensure that the "
        "reported issues are not significant before upgrading.";
  } else {
    summary = "No known compatibility errors or issues were found.";
  }

  std::map<std::string, std::string> excluded;
  if (config.warn_on_excludes() && !config.exclude_list().empty()) {
    for (const auto &check : rejected) {
      if (!config.exclude_list().contains(check->get_name())) continue;

      excluded[check->get_name()] = check->get_title();
    }
  }

  print.summarize(errors, warnings, notices, summary, excluded);

  return 0 == errors;
}

bool list_checks_for_upgrade(const Upgrade_check_config &config,
                             Upgrade_check_output_formatter &print) {
  if (config.session()) {
    print.list_info(config.session()->get_connection_options().uri_endpoint(),
                    config.upgrade_info().server_version_long,
                    config.upgrade_info().target_version.get_base(),
                    config.upgrade_info().explicit_target_version);
  } else {
    print.list_info();
  }

  config.upgrade_info().validate(true);

  Upgrade_check_registry::Upgrade_check_vec rejected;

  const auto accepted = Upgrade_check_registry::create_checklist(
      config, config.session() == nullptr, &rejected);

  print.list_item_infos("Included", accepted);
  print.list_item_infos("Excluded", rejected);

  print.list_summarize(accepted.size(), rejected.size());

  return true;
}

bool check_for_upgrade(const Upgrade_check_config &config) {
  if (!config.list_checks()) {
    if (config.user_privileges()) {
      if (config.user_privileges()
              ->validate({"PROCESS", "SELECT"})
              .has_missing_privileges()) {
        throw std::runtime_error(
            "The upgrade check needs to be performed by user with PROCESS, and "
            "SELECT privileges.");
      }
    } else {
      log_warning(
          "User privileges were not validated, upgrade check may fail.");
    }
  }

  const auto print = config.formatter();
  assert(print);

  if (config.list_checks()) {
    return list_checks_for_upgrade(config, *print);
  }
  return run_checks_for_upgrade(config, *print);
}

}  // namespace upgrade_checker
}  // namespace mysqlsh
