/*
 * 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/adminapi/common/instance_validations.h"

#include <algorithm>
#include <memory>
#include <string>
#include <vector>

#include "modules/adminapi/common/dba_errors.h"
#include "modules/adminapi/common/metadata_management_mysql.h"
#include "modules/adminapi/common/metadata_storage.h"
#include "modules/adminapi/common/provision.h"
#include "mysqlshdk/include/shellcore/console.h"
#include "mysqlshdk/libs/mysql/replication.h"
#include "mysqlshdk/libs/textui/textui.h"
#include "mysqlshdk/libs/utils/utils_net.h"
#include "mysqlshdk/libs/utils/utils_string.h"
#include "mysqlshdk/libs/utils/version.h"

// Naming convention for validations:
//
// - check_ for generic check functions, which should not print anything and
// instead should return the result of its check so that the caller can take
// action on it.
//
// - validate_ for validation functions that print messages for issues, but
// not contextual messages. They should return the general result of the
// validation (e.g. pass/fail) for the caller to decide what to do.
//
// - ensure_ performs a validation, prints out errors, contextual info
// and throws an exception on validation error. They should usually be wrappers
// around a validate_ or check_ functions.
//
// checks and validations are mostly generic code, ensure is AdminAPI specific
// and thus should be in mod_dba_common.cc (or whatever it's split into)

namespace mysqlsh {
namespace dba {
namespace checks {

namespace {
/**
 * Validate if a supported InnoDB page size value is used.
 *
 * Currently, innodb_page_size > 4k is required due to the size of some
 * required information in the Metadata.
 *
 * @param instance target instance to check.
 */
void validate_innodb_page_size(const mysqlshdk::mysql::IInstance &instance) {
  log_info("Validating InnoDB page size of instance '%s'.",
           instance.descr().c_str());

  auto page_size = *instance.get_sysvar_string(
      "innodb_page_size", mysqlshdk::mysql::Var_qualifier::GLOBAL);

  if (std::stoul(page_size) > 4096) return;

  mysqlsh::current_console()->print_error(shcore::str_format(
      "Instance '%s' is using a non-supported InnoDB page size "
      "(innodb_page_size=%s). Only instances with innodb_page_size greater "
      "than 4k (4096) can be used with InnoDB Cluster.",
      instance.descr().c_str(), page_size.c_str()));
  throw shcore::Exception::runtime_error(shcore::str_format(
      "Unsupported innodb_page_size value: %s", page_size.c_str()));
}
}  // namespace

/**
 * Perform validation of schemas for compatibility issues with group
 * replication. If any issues are found, they're printed to the console.
 *
 * Checks performed are:
 * - GR compatible storage engines only (InnoDB and MEMORY)
 * - all tables must have a PK or a UNIQUE NOT NULL key
 *
 * @param  instance target instance for the validation. Must be authenticated
 *         with an account with SELECT access to all schemas.
 * @param  skip_check_tables_pk true to skip checking tables without PKs
 * @return true if no issues found.
 */
bool validate_schemas(const mysqlshdk::mysql::IInstance &instance,
                      bool skip_check_tables_pk) {
  bool ok = true;
  std::string k_gr_compliance_skip_schemas =
      "('mysql', 'sys', 'performance_schema', 'information_schema')";
  std::string k_gr_compliance_skip_engines = "('InnoDB', 'MEMORY')";

  auto console = mysqlsh::current_console();

  {
    std::shared_ptr<mysqlshdk::db::IResult> result = instance.query(
        "SELECT table_schema, table_name, engine "
        " FROM information_schema.tables "
        " WHERE engine NOT IN " +
        k_gr_compliance_skip_engines + " AND table_schema NOT IN " +
        k_gr_compliance_skip_schemas);

    auto row = result->fetch_one();
    if (row) {
      console->print_error(
          "The following tables use a storage engine that are not supported by "
          "Group Replication:");
      ok = false;
      std::string tables;
      while (row) {
        if (!tables.empty()) tables.append(", ");
        tables.append(row->get_string(0))
            .append(".")
            .append(row->get_string(1));
        row = result->fetch_one();
      }
      tables.append("\n");
      console->print_info(tables);
    }
  }

  if (!skip_check_tables_pk) {
    std::shared_ptr<mysqlshdk::db::IResult> result = instance.query(
        "SELECT t.table_schema, t.table_name "
        "FROM information_schema.tables t "
        "    LEFT JOIN (SELECT table_schema, table_name "
        "               FROM information_schema.statistics "
        "               GROUP BY table_schema, table_name, index_name "
        "               HAVING SUM(CASE "
        "                   WHEN non_unique = 0 AND nullable != 'YES' "
        "                   THEN 1 ELSE 0 END) = COUNT(*) "
        "              ) puks "
        "    ON t.table_schema = puks.table_schema "
        "        AND t.table_name = puks.table_name "
        "WHERE puks.table_name IS NULL "
        "    AND t.table_type = 'BASE TABLE' "
        "    AND t.table_schema NOT IN " +
        k_gr_compliance_skip_schemas);

    auto row = result->fetch_one();
    if (row) {
      console->print_error(
          "The following tables do not have a Primary Key or equivalent "
          "column: ");
      ok = false;
      std::string tables;
      while (row) {
        if (!tables.empty()) tables.append(", ");
        tables.append(row->get_string(0))
            .append(".")
            .append(row->get_string(1));
        row = result->fetch_one();
      }
      tables.append("\n");
      console->print_info(tables);
    }
  }

  if (!ok) {
    console->print_info(
        "Group Replication requires tables to use InnoDB and have a PRIMARY "
        "KEY or PRIMARY KEY Equivalent (non-null unique key). Tables that do "
        "not follow these requirements will be readable but not updateable "
        "when used with Group Replication. If your applications make updates "
        "(INSERT, UPDATE or DELETE) to these tables, ensure they use the "
        "InnoDB storage engine and have a PRIMARY KEY or PRIMARY KEY "
        "Equivalent.");
    console->print_info(
        "If you can't change the tables structure to include an extra visible "
        "key to be used as PRIMARY KEY, you can make use of the INVISIBLE "
        "COLUMN feature available since 8.0.23: "
        "https://dev.mysql.com/doc/refman/8.0/en/invisible-columns.html");
  }
  return ok;
}

/**
 * Validate that the target instances host is correctly configured in MySQL.
 * The check will be based on the @@hostname and @@report_host sysvars,
 * where @@report_host takes precedence if it's not NULL.
 *
 * The host address will be printed to the console along with an informational
 * message.
 *
 * If the target host name resolves to a loopback address (127.*) an error
 * will be printed and the function returns false. Unless the target is a
 * sandbox, detected by checking that the name of the datadir is sandboxdata.
 *
 * If an IPv6 address is used and the instance version is < 8.0.14
 * throw an exception.
 *
 * @param  instance target instance with cached global sysvars
 * @param  verbose  if 1 additional informational messages are shown, if 2
 * further explanations are provided
 * @throw  an exception if the host name configuration is not suitable
 */
void validate_host_address(const mysqlshdk::mysql::IInstance &instance,
                           int verbose) {
  auto console = mysqlsh::current_console();

  bool report_host_set;
  std::string hostname;
  std::string report_host;

  auto result = instance.query("SELECT @@hostname, @@report_host");
  auto row = result->fetch_one_or_throw();
  hostname = row->get_string(0);
  report_host = row->get_string(1, "");
  report_host_set = !row->is_null(1);

  if (report_host_set) {
    log_debug("Target has report_host=%s", report_host.c_str());
    log_debug("Target has hostname=%s", hostname.c_str());
    hostname = report_host;
  } else {
    log_debug("Target has report_host=NULL");
    log_debug("Target has hostname=%s", hostname.c_str());
  }

  if (report_host_set && report_host.empty()) {
    console->print_error("Invalid 'report_host' value for instance '" +
                         instance.get_connection_options().uri_endpoint() +
                         "'. The value cannot be empty if defined.");
    // NOTE: The value for report_host can be set to an empty string which is
    // invalid. If defined the report_host value should not be an empty
    // string, otherwise it is used by replication as an empty string "".
    throw std::runtime_error(
        "The value for variable 'report_host' cannot be empty.");
  }

  if (verbose > 0) {
    console->print_info();
    console->print_info(
        "This instance reports its own address as " +
        mysqlshdk::textui::bold(instance.get_canonical_address()));
    if (!report_host_set && verbose > 1) {
      console->print_info(
          "Clients and other cluster members will communicate with it through "
          "this address by default. "
          "If this is not correct, the report_host MySQL "
          "system variable should be changed.");
    }
  }

  // Validate the IP of the hostname used for GR.
  try {
    std::string seed_ip =
        mysqlshdk::utils::Net::resolve_hostname_ipv4(hostname);
    const bool is_loopback = mysqlshdk::utils::Net::is_loopback(hostname);
    if (is_loopback && seed_ip != "127.0.0.1") {
      // Loopback IPv4 addresses except for 127.0.0.1 are not supported by GCS
      // leading to errors.
      console->print_error(shcore::str_format(
          "Cannot use host '%s' for instance '%s' because it resolves to an IP "
          "address (%s) that does not "
          "match a real network interface, thus it is not supported. Change "
          "your system settings and/or "
          "set the MySQL server 'report_host' variable to a hostname that "
          "resolves to a supported IP address.",
          hostname.c_str(), instance.descr().c_str(), seed_ip.c_str()));

      throw std::runtime_error(shcore::str_format(
          "Invalid host/IP '%s' resolves to '%s' which is not "
          "supported.",
          hostname.c_str(), seed_ip.c_str()));
    }
  } catch (const mysqlshdk::utils::net_error &) {
    // if it is an IPv6 address and the instance version does not support IPv6
    // throw an error
    // Do not try to resolve hostnames. Even if we can resolve them, there is
    // no guarantee that name resolution is the same across cluster instances
    // and the instance where we are running the ngshell
    bool is_ipv6 = mysqlshdk::utils::Net::is_ipv6(hostname);
    bool supports_ipv6 =
        instance.get_version() >= mysqlshdk::utils::Version(8, 0, 14);
    if (is_ipv6 && !supports_ipv6) {
      // Using an IPv6 address and the instance does not support it.
      console->print_error(shcore::str_format(
          "Cannot use host '%s' for instance '%s' "
          "because it is an IPv6 address which is only supported by "
          "Group Replication from MySQL version >= 8.0.14. Set the MySQL "
          "server 'report_host' "
          "variable to an IPv4 address or hostname that resolves an IPv4 "
          "address.",
          hostname.c_str(), instance.descr().c_str()));
      throw std::runtime_error(shcore::str_format(
          "Unsupported IP address '%s'. IPv6 is only "
          "supported by Group Replication on MySQL version >= 8.0.14.",
          hostname.c_str()));
    }
  }
}

void process_row_values(const mysqlshdk::mysql::Invalid_config &cfg,
                        shcore::Value::Map_type_ref row) {
  // adjust the displayed string for "log_bin"
  if (cfg.var_name == "log_bin") {
    if (cfg.current_val == mysqlshdk::mysql::k_value_not_set)
      (*row)["current"] = shcore::Value("<not present>");
    else
      (*row)["current"] = shcore::Value(cfg.current_val);

    if (cfg.required_val == mysqlshdk::mysql::k_value_not_set ||
        cfg.required_val == mysqlshdk::mysql::k_no_value)
      (*row)["required"] = shcore::Value("ON");
    else
      (*row)["required"] = shcore::Value(cfg.required_val);

    return;
  }

  if (cfg.var_name == "skip_log_bin" || cfg.var_name == "disable_log_bin") {
    if (cfg.current_val == mysqlshdk::mysql::k_value_not_set)
      (*row)["current"] = shcore::Value("<present>");
    else
      (*row)["current"] = shcore::Value(cfg.current_val);

    if (cfg.required_val == mysqlshdk::mysql::k_value_not_set)
      (*row)["required"] = shcore::Value("<not present>");
    else
      (*row)["required"] = shcore::Value(cfg.required_val);

    return;
  }

  // store as is
  (*row)["current"] = shcore::Value(cfg.current_val);
  (*row)["required"] = shcore::Value(cfg.required_val);
}

void process_row_action(const mysqlshdk::mysql::Invalid_config &cfg,
                        shcore::Value::Map_type_ref row,
                        std::string_view action) {
  if (cfg.var_name == "skip_log_bin" || cfg.var_name == "disable_log_bin") {
    (*row)["action"] = shcore::Value("remove_opt+restart");
    return;
  }

  // store as is
  (*row)["action"] = shcore::Value(action);
}

/**
 * Validate configuration of the target MySQL server instance.
 *
 * Configuration settings are checked through sysvars, but if mycnf_path is
 * given the config file will also be checked. If any changes are required,
 * they will be sent to the console. Also returns flags indicating the types
 * of changes that are required, if any.
 *
 * @param  instance             target instance
 * @param  mycnf_path           optional config file path, for local instances
 * @param  config               pointer to config handler
 * @param  cluster_type         target cluster/replicaset type
 * @param  can_persist          true if instance has support to persist
 *                              variables, false if has but it is disabled and
 *                              null if it is not supported.
 * @param  restart_needed[out]  true if instance needs to be restarted
 * @param  mycnf_change_needed[out]  true if my.cnf has to be updated
 * @param  sysvar_change_needed[out] true if sysvars have to be updated
 * @param  ret_val[out]         assigned to check results
 * @return                      [description]
 */
std::vector<mysqlshdk::mysql::Invalid_config> validate_configuration(
    const mysqlshdk::mysql::IInstance &instance, const std::string &mycnf_path,
    mysqlshdk::config::Config *const config, Cluster_type cluster_type,
    std::optional<bool> can_persist, bool *restart_needed,
    bool *mycnf_change_needed, bool *sysvar_change_needed,
    shcore::Value *ret_val) {
  // Check supported innodb_page_size (must be > 4k). See: BUG#27329079
  validate_innodb_page_size(instance);

  // Check if performance_schema is enabled
  // BUG#25867733
  validate_performance_schema_enabled(instance);

  log_info("Validating configuration of %s (mycnf = %s)",
           instance.descr().c_str(), mycnf_path.c_str());
  // Perform check with no update
  std::vector<mysqlshdk::mysql::Invalid_config> invalid_cfs_vec =
      check_instance_config(instance, *config, cluster_type);

  // Sort invalid cfs_vec by the name of the variable
  std::sort(invalid_cfs_vec.begin(), invalid_cfs_vec.end());

  // Build the map to be used by the print_validation results
  shcore::Value::Map_type_ref map_val(new shcore::Value::Map_type());
  shcore::Value check_result = shcore::Value(map_val);
  *restart_needed = false;
  *mycnf_change_needed = false;
  *sysvar_change_needed = false;
  bool log_bin_present = false;

  if (invalid_cfs_vec.empty()) {
    (*map_val)["status"] = shcore::Value("ok");
  } else {
    (*map_val)["status"] = shcore::Value("error");
    shcore::Value::Array_type_ref config_errors(
        new shcore::Value::Array_type());
    for (const mysqlshdk::mysql::Invalid_config &cfg : invalid_cfs_vec) {
      shcore::Value::Map_type_ref error(new shcore::Value::Map_type());
      std::string_view action;
      if (!log_bin_present &&
          (cfg.var_name == "log_bin" || cfg.var_name == "skip_log_bin" ||
           cfg.var_name == "disable_log_bin") &&
          cfg.types.is_set(mysqlshdk::mysql::Config_type::CONFIG)) {
        log_bin_present = true;
      }

      if (log_bin_present ||
          (cfg.types.is_set(mysqlshdk::mysql::Config_type::CONFIG) &&
           cfg.types.is_set(mysqlshdk::mysql::Config_type::SERVER))) {
        *mycnf_change_needed = true;
        if (cfg.restart) {
          *restart_needed = true;
          action = "config_update+restart";
        } else {
          action = "server_update+config_update";
          *sysvar_change_needed = true;
        }
      } else if (cfg.types.is_set(mysqlshdk::mysql::Config_type::CONFIG)) {
        *mycnf_change_needed = true;
        action = "config_update";
      } else if (cfg.types.is_set(mysqlshdk::mysql::Config_type::SERVER)) {
        *sysvar_change_needed = true;
        if (cfg.restart) {
          *restart_needed = true;
          action = "server_update+restart";
        } else {
          action = "server_update";
        }
      } else if (cfg.types.is_set(
                     mysqlshdk::mysql::Config_type::RESTART_ONLY)) {
        *restart_needed = true;
        action = "restart";
      } else if (cfg.types.empty()) {
        throw std::runtime_error("Unexpected change type");
      }

      (*error)["option"] = shcore::Value(cfg.var_name);
      process_row_values(cfg, error);
      process_row_action(cfg, error, action);
      if (cfg.persisted_val.has_value()) {
        (*error)["persisted"] = shcore::Value(*cfg.persisted_val);
      }
      config_errors->push_back(shcore::Value(std::move(error)));
    }
    (*map_val)["config_errors"] = shcore::Value(config_errors);
  }
  if (ret_val) *ret_val = check_result;

  log_debug("Check command returned: %s", check_result.descr().c_str());

  if (invalid_cfs_vec.empty()) return {};  // nothing else to do

  auto console = mysqlsh::current_console();
  console->print_info();

  print_validation_results(check_result.as_map(), true);

  if (!(*restart_needed)) return invalid_cfs_vec;

  // if we have to change some read only variables, print a message to the
  // user.
  {
    std::string base_msg(
        "Some variables need to be changed, but cannot be done dynamically on "
        "the server");
    if (log_bin_present && *mycnf_change_needed) {
      base_msg += ": an option file is required";
    } else if (*mycnf_change_needed) {
      if (!can_persist.has_value()) {
        // set persist is not supported
        base_msg += ": an option file is required";
      } else if (!*can_persist) {
        // set persist support disabled
        base_msg +=
            ": set persist support is disabled. Enable it or provide an "
            "option file";
      }
    }

    base_msg += ".";
    console->print_info(base_msg);
  }

  return invalid_cfs_vec;
}

void validate_performance_schema_enabled(
    const mysqlshdk::mysql::IInstance &instance) {
  log_info("Checking if performance_schema is enabled on instance '%s'.",
           instance.descr().c_str());

  if (instance.get_sysvar_bool("performance_schema", false)) {
    return;
  }

  mysqlsh::current_console()->print_error(shcore::str_format(
      "Instance '%s' has the performance_schema disabled "
      "(performance_schema=OFF). Instances must have the performance_schema "
      "enabled to for InnoDB Cluster usage.",
      instance.descr().c_str()));

  throw shcore::Exception::runtime_error(
      "performance_schema disabled on target instance.");
}

void ensure_instance_not_belong_to_cluster(
    const mysqlsh::dba::Instance &instance,
    const mysqlsh::dba::Instance &cluster_instance, std::string_view cluster_id,
    bool omit_missing_instance_warn) {
  MetadataStorage metadata{instance};
  auto type = mysqlsh::dba::get_instance_type(metadata, instance);

  switch (type) {
    // these types don't need any more checks
    case TargetType::Standalone:
    case TargetType::StandaloneWithMetadata:
    case TargetType::AsyncReplication:
      return;
    default:
      break;
  }

  if (type == TargetType::StandaloneInMetadata) {
    // Check if the instance is (MISSING)
    try {
      // Double-check that the instance is part of this Cluster
      MetadataStorage metadata_cluster{cluster_instance};

      metadata_cluster.get_instance_by_uuid(instance.get_uuid());

      if (!omit_missing_instance_warn)
        current_console()->print_error(
            shcore::str_format("Instance '%s' is already part of this InnoDB "
                               "Cluster but is (MISSING). Please use "
                               "<Cluster>.<<<rejoinInstance()>>> to rejoin it.",
                               instance.descr().c_str()));

      throw shcore::Exception("The instance '" + instance.descr() +
                                  "' is already part of this InnoDB Cluster",
                              SHERR_DBA_BADARG_INSTANCE_MANAGED_IN_CLUSTER);
    } catch (const shcore::Exception &exp) {
      if (exp.code() != SHERR_DBA_MEMBER_METADATA_MISSING) throw;
      // If not part of the Metadata it can be considered a standalone
      // instance (might have been forcefully removed)
      return;
    }
  } else if (type != TargetType::InnoDBClusterSetOffline) {
    // Retrieves the new instance UUID
    std::string uuid = instance.get_uuid();

    // Verifies if this UUID is part of the current replication group
    std::vector<mysqlshdk::gr::Member> members =
        mysqlshdk::gr::get_members(cluster_instance);

    if (std::find_if(members.begin(), members.end(),
                     [&uuid](const auto &member) {
                       return member.uuid == uuid;
                     }) != members.end()) {
      if (type == TargetType::InnoDBCluster ||
          type == TargetType::InnoDBClusterSet) {
        log_debug("Instance '%s' already managed by InnoDB Cluster",
                  instance.descr().c_str());

        throw shcore::Exception("The instance '" + instance.descr() +
                                    "' is already part of this InnoDB Cluster",
                                SHERR_DBA_BADARG_INSTANCE_MANAGED_IN_CLUSTER);
      }
      current_console()->print_error(
          "Instance '" + instance.descr() +
          "' is part of the Group Replication group but is not in the "
          "metadata. Please use <Cluster>.rescan() to update the "
          "metadata.");
      throw shcore::Exception(
          "Metadata inconsistent: " + instance.descr() +
              " is a group member but is not in the metadata.",
          SHERR_DBA_ASYNC_MEMBER_INCONSISTENT);
    }
  } else {
    MetadataStorage metadata_cluster{cluster_instance};

    try {
      // if instance is in the clusters metadata, it's valid
      metadata_cluster.get_instance_by_uuid(instance.get_uuid());
      return;
    } catch (const shcore::Exception &exp) {
      if (exp.code() != SHERR_DBA_MEMBER_METADATA_MISSING) throw;
    }

    // check if the instance cluster belongs to the target's cluster set

    Cluster_metadata instance_cluster_meta;
    if (metadata.get_cluster_for_server_uuid(instance.get_uuid(),
                                             &instance_cluster_meta)) {
      if (instance_cluster_meta.cluster_id == cluster_id) return;

      if (std::string cs_id; metadata_cluster.check_cluster_set(
              nullptr, nullptr, nullptr, &cs_id)) {
        std::vector<Cluster_set_member_metadata> cs_members;
        if (metadata_cluster.get_cluster_set(cs_id, false, nullptr,
                                             &cs_members)) {
          auto it = std::find_if(
              cs_members.begin(), cs_members.end(),
              [&cs_id = instance_cluster_meta.cluster_id](const auto &member) {
                return (cs_id == member.cluster.cluster_id);
              });
          if (it != cs_members.end()) return;
        }
      }
    }

    // reaching this point, we have a cluster set that doesn't belong to the
    // target cluster or its cluster doesn't belong to the target's cluster set,
    // so treat it as if it was an (unknown) cluster set
    type = TargetType::InnoDBClusterSet;
  }

  switch (type) {
    case TargetType::InnoDBCluster:
    case TargetType::InnoDBClusterSet:
      // Check if instance is running auto-rejoin and warn user.
      if (mysqlshdk::gr::is_running_gr_auto_rejoin(instance)) {
        throw shcore::Exception::runtime_error(
            "The instance '" + instance.descr() +
            "' is currently attempting to rejoin the cluster. Use <cluster>."
            "rejoinInstance() if you want to override the auto-rejoin "
            "process.");
      } else {
        throw shcore::Exception::runtime_error(
            "The instance '" + instance.descr() +
            "' is already part of another InnoDB Cluster");
      }
      break;
    case TargetType::AsyncReplicaSet:
      throw shcore::Exception::runtime_error(
          "The instance '" + instance.descr() +
          "' is already part of an InnoDB ReplicaSet");
      break;
    case TargetType::AsyncReplication:
      throw shcore::Exception::runtime_error(
          "The instance '" + instance.descr() +
          "' is already part of another Asynchronous Replication Topology");
      break;
    default:
      throw shcore::Exception::runtime_error(
          "The instance '" + instance.descr() +
          "' is already part of another Replication Group");
      break;
  }
}

size_t check_illegal_async_channels(
    const mysqlshdk::mysql::IInstance &instance,
    std::unordered_set<std::string> allowed_channels) {
  auto channels = get_incoming_channels(instance);
  if (channels.empty()) return 0;

  auto console = mysqlsh::current_console();

  allowed_channels.insert("group_replication_applier");
  allowed_channels.insert("group_replication_recovery");
  allowed_channels.insert("mysqlsh.test");
  allowed_channels.insert(metadata::kClusterSetupIndicatorTag);

  size_t illegal_channels = 0;
  for (const auto &ch : channels) {
    if (allowed_channels.count(ch.channel_name) > 0) continue;

    illegal_channels++;
    console->print_note("Found unexpected replication channel '" +
                        ch.channel_name + "' at " + instance.descr());
  }

  return illegal_channels;
}

/**
 * Validate if asynchronous replication is configured on the target instance.
 *
 * This function validates if replication is configured on the target
 * instance and depending on the type of check, prints a warning message,
 * or an error message and terminates with an exception.
 *
 * @param instance The instance to validate.
 * @param allowed_channels List of channel names that can appear at the
 * instance
 * @param type     The type of check (Check_type)
 */
void validate_async_channels(
    const mysqlshdk::mysql::IInstance &instance,
    const std::unordered_set<std::string> &allowed_channels, Check_type type) {
  log_debug(
      "Checking if instance '%s' has asynchronous (source-replica) "
      "replication configured.",
      instance.descr().c_str());

  size_t illegal_channels =
      check_illegal_async_channels(instance, allowed_channels);

  if (illegal_channels > 0) {
    std::string error_msg;

    switch (type) {
      case Check_type::CHECK:
        error_msg += "The";
        break;
      case Check_type::CREATE:
        error_msg += "Cannot create cluster on";
        break;
      case Check_type::BOOTSTRAP:
        error_msg += "Cannot bootstrap cluster on";
        break;
      case Check_type::JOIN:
        error_msg += "Cannot join";
        break;
      case Check_type::REJOIN:
        error_msg += "Cannot rejoin";
        break;
      case Check_type::READ_REPLICA: {
        mysqlsh::current_console()->print_error(
            "The target instance '" + instance.get_canonical_address() +
            "' has asynchronous (source-replica) replication channel(s) "
            "configured which is not supported in InnoDB Cluster "
            "Read-Replicas.");

        throw shcore::Exception(
            "Unsupported active replication channel.",
            SHERR_DBA_CLUSTER_UNSUPPORTED_REPLICATION_CHANNEL);
      }
    }

    error_msg +=
        " instance '" + instance.descr() + "' " +
        std::string((type == Check_type::JOIN || type == Check_type::REJOIN)
                        ? "to the cluster "
                        : "") +
        std::string(type == Check_type::CHECK
                        ? "cannot be added to an InnoDB cluster "
                        : "") +
        "because it has asynchronous (source-replica) "
        "replication channel(s) configured. "
        "MySQL InnoDB Cluster does not support manually configured channels "
        "as they are not managed using the AdminAPI (e.g. when PRIMARY moves "
        "to another member) which may cause cause replication to break or "
        "even create split-brain scenarios (data loss).";

    if (type == Check_type::CREATE) {
      error_msg +=
          " Use the 'force' option to skip this validation on a temporary "
          "scenario (e.g. migrating from a replication topology to InnoDB "
          "Cluster).";
    }

    if (type == Check_type::CHECK) {
      mysqlsh::current_console()->print_warning(error_msg);
    } else {
      mysqlsh::current_console()->print_error(error_msg);

      throw shcore::Exception::runtime_error(
          "The instance '" + instance.descr() +
          "' has asynchronous replication configured.");
    }
  }
}

}  // namespace checks
}  // namespace dba
}  // namespace mysqlsh
