/*
 * Copyright (c) 2018, 2025, 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/cluster/rescan.h"

#include <algorithm>
#include <iterator>
#include <set>

#include "modules/adminapi/common/common.h"
#include "modules/adminapi/common/instance_pool.h"
#include "modules/adminapi/common/metadata_storage.h"
#include "modules/adminapi/common/preconditions.h"
#include "modules/adminapi/common/server_features.h"
#include "modules/adminapi/common/validations.h"
#include "mysqlshdk/include/scripting/types.h"
#include "mysqlshdk/include/shellcore/console.h"
#include "mysqlshdk/libs/config/config.h"
#include "mysqlshdk/libs/utils/debug.h"
#include "mysqlshdk/libs/utils/utils_net.h"
#include "utils/utils_string.h"
#include "utils/version.h"

namespace mysqlsh {
namespace dba {
namespace cluster {

constexpr const char *k_xplugin_disabled = "<disabled>";

Rescan::Rescan(Rescan_options options, Cluster_impl *cluster)
    : m_options(std::move(options)),
      m_cluster(cluster),
      m_repl_account_mng{*cluster} {
  assert(cluster);
}

void Rescan::ensure_unavailable_instances_not_auto_rejoining(
    std::vector<MissingInstanceInfo> &unavailable_instances) const {
  mysqlshdk::db::Connection_options group_conn_opt =
      m_cluster->get_cluster_server()->get_connection_options();

  auto console = mysqlsh::current_console();
  auto it = unavailable_instances.begin();
  while (it != unavailable_instances.end()) {
    auto instance_conn_opt = mysqlshdk::db::Connection_options(it->endpoint);
    instance_conn_opt.set_login_options_from(group_conn_opt);
    bool is_rejoining = false;
    try {
      auto instance = Instance::connect_raw(instance_conn_opt);
      is_rejoining = mysqlshdk::gr::is_running_gr_auto_rejoin(*instance);
    } catch (const std::exception &) {
      // if you cant connect to the instance then we assume it really is offline
      // or unreachable and it is not auto-rejoining
    }

    if (is_rejoining) {
      console->print_warning(
          "The instance '" + instance_conn_opt.uri_endpoint() +
          "' is MISSING but currently trying to auto-rejoin.");
      it = unavailable_instances.erase(it);
    } else {
      ++it;
    }
  }
}

std::vector<std::string> Rescan::detect_invalid_members(
    const std::vector<mysqlshdk::db::Connection_options> &instances_list,
    bool is_active) {
  std::shared_ptr<Instance> cluster_instance = m_cluster->get_cluster_server();

  std::vector<std::string> invalid_instances;
  for (const auto &cnx_opt : instances_list) {
    std::string instance_address =
        cnx_opt.as_uri(mysqlshdk::db::uri::formats::only_transport());

    const char *check_type = (is_active) ? "" : "not ";
    log_debug(
        "Checking if the instance '%s' is %san active member of the group.",
        instance_address.c_str(), check_type);

    if (mysqlshdk::gr::is_active_member(*cluster_instance, cnx_opt.get_host(),
                                        cnx_opt.get_port()) != is_active) {
      invalid_instances.push_back(instance_address);
    }
  }

  return invalid_instances;
}

void Rescan::prepare() {
  auto console = mysqlsh::current_console();

  // can't add instances if recovery auth isn't password
  if (m_options.auto_add && (m_cluster->query_cluster_auth_type() !=
                             Replication_auth_type::PASSWORD)) {
    console->print_error(
        "Unrecognized members were detected in the group, but the cluster is "
        "configured to require SSL certificate authentication. Please stop GR "
        "on those members and then add them to the cluster using "
        "cluster.<<<addInstance>>>() with the appropriate authentication "
        "options.");
    console->print_info();

    throw shcore::Exception::runtime_error(shcore::str_format(
        "Can't automatically add unrecognized members to the cluster when "
        "memberAuthType is '%s'.",
        to_string(m_cluster->query_cluster_auth_type()).c_str()));
  }

  // Check if group_replication_view_change_uuid is supported
  m_is_view_change_uuid_supported = m_cluster->get_lowest_instance_version() >=
                                    Precondition_checker::k_min_cs_version;

  // Validate the usage of the option 'updateViewChangeUuid'
  if (m_options.update_view_change_uuid.value_or(false) &&
      !m_is_view_change_uuid_supported) {
    throw shcore::Exception::argument_error(
        "The Cluster cannot be configured to use "
        "group_replication_view_change_uuid: unsupported version.");
  }
}

void Rescan::check_mismatched_hostnames_addresses(
    shcore::Array_t instances) const {
  m_cluster->execute_in_members(
      [&instances](const std::shared_ptr<Instance> &instance,
                   const Cluster_impl::Instance_md_and_gr_member &info) {
        // skip members that are already in the update list
        for (const auto &i : *instances) {
          if (i.as_map()->get_string("member_id") == instance->get_uuid())
            return true;
        }

        bool grendpoint_valid = true;
        try {
          mysqlshdk::utils::split_host_and_port(info.first.grendpoint);
        } catch (const std::invalid_argument &) {
          log_debug("Unable to parse endpoint: %s",
                    info.first.grendpoint.c_str());
          grendpoint_valid = false;
        }

        auto address = instance->get_canonical_address();

        std::optional<int> xport = instance->get_xport();

        auto res = instance->query(
            "select @@hostname, @@report_host, @@port, @@report_port");
        auto row = res->fetch_one();

        log_debug(
            "Hostname metadata check for %s: address=%s @@hostname=%s "
            "@@report_host=%s @@port=%s @@report_port=%s @@mysqlx_port=%s "
            " md.instance_id=%u md.endpoint=%s md.address=%s md.label=%s "
            "md.xendpoint=%s md.grendpoint=%s member_id=%s member_host=%s "
            "member_port=%i",
            instance->get_uuid().c_str(), address.c_str(),
            row->get_as_string(0).c_str(), row->get_as_string(1).c_str(),
            row->get_as_string(2).c_str(), row->get_as_string(3).c_str(),
            xport.has_value() ? std::to_string(*xport).c_str() : "null",
            info.first.id, info.first.endpoint.c_str(),
            info.first.address.c_str(), info.first.label.c_str(),
            info.first.xendpoint.c_str(), info.first.grendpoint.c_str(),
            info.second.uuid.c_str(), info.second.host.c_str(),
            info.second.port);

        bool xport_equal = false;
        bool address_equal = false;
        std::string x_address;

        auto canonical_hostname = instance->get_canonical_hostname();

        if (xport.has_value()) {
          x_address =
              mysqlshdk::utils::make_host_and_port(canonical_hostname, *xport);
        }

        if (mysqlshdk::utils::are_endpoints_equal(info.first.xendpoint,
                                                  x_address)) {
          xport_equal = true;
        }

        if (grendpoint_valid &&
            mysqlshdk::utils::are_endpoints_equal(info.first.endpoint,
                                                  address) &&
            mysqlshdk::utils::are_endpoints_equal(info.first.address,
                                                  address)) {
          address_equal = true;
        }

        if (xport_equal && address_equal) return true;

        shcore::Dictionary_t dict = shcore::make_dict();

        dict->set("member_id", shcore::Value(instance->get_uuid()));
        dict->set("id", shcore::Value(info.first.id));
        dict->set("label", shcore::Value(info.first.label));
        dict->set("host", shcore::Value(instance->get_canonical_address()));
        dict->set(
            "xport_endpoint",
            shcore::Value(x_address.empty() ? k_xplugin_disabled : x_address));

        instances->emplace_back(shcore::Value(dict));

        if (!address_equal) {
          dict->set("old_host", shcore::Value(info.first.endpoint));
        }

        if (!xport_equal) {
          dict->set("old_xport_endpoint", shcore::Value(info.first.xendpoint));
        }

        return true;
      },
      [](const shcore::Error &,
         const Cluster_impl::Instance_md_and_gr_member &) { return true; });
}

bool Rescan::is_clusterset_metadata_consistent() {
  // All metadata update are atomic, to ensure consistency. On top of that, the
  // AdminAPI reverts any failing operation to ensure there are no leftovers.
  // To achieve that, operations are executed inside transactions:
  //   - Create topology (Cluster, ReplicaSet, ClusterSet)
  //   - Add instances to the topology
  //   - Remove instances from the topology
  //   - etc.
  //
  // However, there is 1 exception:
  //   - Adding a new Replica Cluster to a ClusterSet
  //
  //  That operations is split into 2 transactions: Creating a new Cluster (with
  //  its Metadata entries) and adding that Cluster to the ClusterSet's
  //  metadata (set the ClusterSet id, etc.). If one fails and the changes are
  //  not reverted, the Metadata becomes inconsistent.

  // Check if this Cluster belongs to a ClusterSet first
  if (!m_cluster->is_cluster_set_member()) return true;

  auto cs_id = m_cluster->get_metadata().cluster_set_id;

  // Iterate all Cluster's metadata
  for (const auto &c : m_cluster->get_metadata_storage()->get_all_clusters()) {
    // If cluster_set_id is set and the same continue looking
    if (c.cluster_set_id == cs_id) continue;

    auto console = mysqlsh::current_console();
    console->print_warning(
        shcore::str_format("The Cluster '%s' was detected in the ClusterSet "
                           "Metadata, but it does not appear to be a valid "
                           "member of the ClusterSet.",
                           c.full_cluster_name().c_str()));
    console->print_info();

    m_invalid_clusterset_clusters.emplace_back(c);

    return false;
  }

  return true;
}

void Rescan::repair_metadata() const {
  auto metadata = m_cluster->get_metadata_storage();

  mysqlsh::current_console()->print_note(
      "Repairing Metadata inconsistencies...");

  for (const auto &c : m_invalid_clusterset_clusters) {
    log_info("Removing Cluster '%s' from the Metadata.",
             c.full_cluster_name().c_str());

    for (const auto &i : metadata->get_all_instances(c.cluster_id)) {
      log_info("Removing Instance '%s' from the Metadata.", i.endpoint.c_str());
    }
  }

  auto clusterset_id = m_cluster->get_clusterset_metadata().cluster_set_id;

  MetadataStorage::Transaction trx(metadata);

  metadata->prune_clusterset_metadata(clusterset_id);

  trx.commit();
}

shcore::Value::Map_type_ref Rescan::get_rescan_report() {
  auto cluster_map = std::make_shared<shcore::Value::Map_type>();

  // Set the Cluster name on the result map
  (*cluster_map)["name"] = shcore::Value(m_cluster->get_name());

  std::vector<NewInstanceInfo> newly_discovered_instances_list =
      get_newly_discovered_instances(*m_cluster->get_cluster_server(),
                                     m_cluster->get_metadata_storage(),
                                     m_cluster->get_id());

  // Creates the newlyDiscoveredInstances map
  auto newly_discovered_instances =
      std::make_shared<shcore::Value::Array_type>();

  for (auto &instance : newly_discovered_instances_list) {
    auto newly_discovered_instance = shcore::make_dict();
    (*newly_discovered_instance)["member_id"] =
        shcore::Value(instance.member_id);
    (*newly_discovered_instance)["name"] = shcore::Value::Null();

    (*newly_discovered_instance)["host"] =
        shcore::Value(mysqlshdk::utils::make_host_and_port(
            instance.host, static_cast<uint16_t>(instance.port)));

    if (!instance.version.empty()) {
      (*newly_discovered_instance)["version"] = shcore::Value(instance.version);
    }

    newly_discovered_instances->push_back(
        shcore::Value(std::move(newly_discovered_instance)));
  }

  std::sort(newly_discovered_instances->begin(),
            newly_discovered_instances->end(),
            [](const shcore::Value &a, const shcore::Value &b) {
              return a.as_map()->get_string("member_id") <
                     b.as_map()->get_string("member_id");
            });

  // Add the newly_discovered_instances list to the result Map
  (*cluster_map)["newlyDiscoveredInstances"] =
      shcore::Value(newly_discovered_instances);

  shcore::Value unavailable_instances_result;

  std::vector<MissingInstanceInfo> unavailable_instances_list =
      get_unavailable_instances(*m_cluster->get_cluster_server(),
                                m_cluster->get_metadata_storage(),
                                m_cluster->get_id());

  // Registers instances that are both new/unavailable but have the same
  // host:port as updated instances.
  auto updated_instances = std::make_shared<shcore::Value::Array_type>();

  // Creates the unavailableInstances array
  auto unavailable_instances = std::make_shared<shcore::Value::Array_type>();
  ensure_unavailable_instances_not_auto_rejoining(unavailable_instances_list);
  for (auto &instance : unavailable_instances_list) {
    auto unavailable_instance = std::make_shared<shcore::Value::Map_type>();
    auto endpoint = instance.endpoint;
    (*unavailable_instance)["member_id"] = shcore::Value(instance.id);
    (*unavailable_instance)["label"] = shcore::Value(instance.label);
    (*unavailable_instance)["host"] = shcore::Value(endpoint);

    // Instance is both new and unavailable, means its UUID changed
    auto new_instance = std::find_if(
        newly_discovered_instances->begin(), newly_discovered_instances->end(),
        [endpoint](const shcore::Value &existing) {
          return endpoint == existing.as_map()->at("host").as_string();
        });

    if (new_instance != newly_discovered_instances->end()) {
      (*unavailable_instance)["old_member_id"] =
          (*unavailable_instance)["member_id"];
      (*unavailable_instance)["member_id"] =
          new_instance->as_map()->at("member_id");
      updated_instances->push_back(shcore::Value(unavailable_instance));
      newly_discovered_instances->erase(new_instance);
    } else {
      unavailable_instances->push_back(shcore::Value(unavailable_instance));
    }
  }

  check_mismatched_hostnames_addresses(updated_instances);

  // Add the missing_instances list to the result Map
  (*cluster_map)["unavailableInstances"] = shcore::Value(unavailable_instances);

  // Add the missing_instances list to the result Map
  (*cluster_map)["updatedInstances"] = shcore::Value(updated_instances);

  // Get the topology mode from the metadata.
  mysqlshdk::gr::Topology_mode metadata_topology_mode =
      m_cluster->get_metadata_storage()->get_cluster_topology_mode(
          m_cluster->get_id());

  // Get the primary UUID value to determine GR mode:
  // UUID (not empty) -> single-primary or "" (empty) -> multi-primary
  bool single_primary;
  mysqlshdk::gr::get_group_primary_uuid(*m_cluster->get_cluster_server(),
                                        &single_primary);

  // Check if the topology mode match and report needed change in the metadata.
  if (!single_primary &&
      metadata_topology_mode == mysqlshdk::gr::Topology_mode::SINGLE_PRIMARY) {
    // Topology mode need to be changed to multi-primary on the metadata.
    (*cluster_map)["newTopologyMode"] = shcore::Value(
        mysqlshdk::gr::to_string(mysqlshdk::gr::Topology_mode::MULTI_PRIMARY));
  } else if (single_primary &&
             metadata_topology_mode ==
                 mysqlshdk::gr::Topology_mode::MULTI_PRIMARY) {
    // Topology mode need to be changed to single-primary on the metadata.
    (*cluster_map)["newTopologyMode"] = shcore::Value(
        mysqlshdk::gr::to_string(mysqlshdk::gr::Topology_mode::SINGLE_PRIMARY));
  } else {
    // Topology mode is the same (no change needed).
    (*cluster_map)["newTopologyMode"] = shcore::Value::Null();
  }

  // Check if the metadata is consistent
  (*cluster_map)["metadataConsistent"] =
      shcore::Value(is_clusterset_metadata_consistent());

  return cluster_map;
}

void Rescan::update_topology_mode(
    const mysqlshdk::gr::Topology_mode &topology_mode) {
  auto console = mysqlsh::current_console();
  console->print_info("Updating topology mode in the cluster metadata...");

  // Update auto-increment settings.

  // Get the Cluster Config Object
  auto cfg = m_cluster->create_config_object();

  // Call update_auto_increment to do the job in all instances
  mysqlshdk::gr::update_auto_increment(cfg.get(), topology_mode);

  cfg->apply();

  // Update topology mode in metadata.
  m_cluster->get_metadata_storage()->update_cluster_topology_mode(
      m_cluster->get_id(), topology_mode);

  // Update the topology mode on the Cluster object.
  m_cluster->set_topology_type(topology_mode);

  console->print_info("Topology mode was successfully updated to '" +
                      mysqlshdk::gr::to_string(topology_mode) +
                      "' in the cluster metadata.");
  console->print_info();
}

void Rescan::add_instance_to_metadata(
    const mysqlshdk::db::Connection_options &instance_cnx_opts) {
  auto console = mysqlsh::current_console();
  console->print_info("Adding instance to the cluster metadata...");

  // Create a copy of the connection options to set the cluster user
  // credentials if needed (should not change the value passed as parameter).
  mysqlshdk::db::Connection_options cnx_opts = instance_cnx_opts;
  if (!cnx_opts.has_user()) {
    // It is assumed that the same login options of the cluster can be
    // used to connect to the instance, if no login (user) was provided.
    cnx_opts.set_login_options_from(
        m_cluster->get_cluster_server()->get_connection_options());
  }

  // If the instance is a potential source for a Read-Replica, check its
  // replication compatibility
  m_cluster->execute_in_read_replicas(
      [this, &instance_cnx_opts](const std::shared_ptr<Instance> &instance,
                                 const Instance_metadata &) {
        // Get the replication sources
        auto sources = m_cluster->get_read_replica_replication_sources(
            instance->get_uuid());

        // If replicationSources is "primary" or "secondary", any added instance
        // can be a potential. Otherwise, it cannot be a potential source
        if (sources.source_type != Source_type::CUSTOM) {
          auto target_instance = Scoped_instance(
              m_cluster->connect_target_instance(instance_cnx_opts));

          Base_cluster_impl::check_compatible_replication_sources(
              *target_instance, *instance, nullptr, true);
        }

        return true;
      },
      [](const shcore::Error &, const Instance_metadata &) { return true; });

  m_cluster->add_metadata_for_instance(cnx_opts, Instance_type::GROUP_MEMBER);

  console->print_info(
      "The instance '" +
      instance_cnx_opts.as_uri(mysqlshdk::db::uri::formats::only_transport()) +
      "' was successfully added to the cluster metadata.");
  console->print_info();
}

void Rescan::update_metadata_for_instance(
    const mysqlshdk::db::Connection_options &instance_cnx_opts,
    Instance_id instance_id, const std::string &label) {
  // Create a copy of the connection options to set the cluster user
  // credentials if needed (should not change the value passed as parameter).
  mysqlshdk::db::Connection_options cnx_opts = instance_cnx_opts;
  if (!cnx_opts.has_user()) {
    // It is assumed that the same login options of the cluster can be
    // used to connect to the instance, if no login (user) was provided.
    cnx_opts.set_login_options_from(
        m_cluster->get_cluster_server()->get_connection_options());
  }

  m_cluster->update_metadata_for_instance(cnx_opts, instance_id, label);
}

void Rescan::remove_instance_from_metadata(
    const std::string &instance_address) {
  mysqlshdk::db::Connection_options instance_cnx_opts(instance_address);

  auto console = mysqlsh::current_console();
  console->print_info("Removing instance from the cluster metadata...");

  m_cluster->remove_instance_metadata(instance_cnx_opts);

  console->print_info(
      "The instance '" +
      instance_cnx_opts.as_uri(mysqlshdk::db::uri::formats::only_transport()) +
      "' was successfully removed from the cluster metadata.");
  console->print_info();
}

void Rescan::add_metadata_for_instances(
    const std::shared_ptr<shcore::Value::Array_type> &instance_list) {
  if (instance_list->empty()) return;

  auto console = mysqlsh::current_console();

  if (m_cluster->query_cluster_auth_type() != Replication_auth_type::PASSWORD) {
    console->print_info(
        "New instances were discovered in the cluster but ignore because the "
        "cluster requires SSL certificate authentication.\nPlease stop GR on "
        "those members and then add them to the cluster using "
        "cluster.addInstance() with the appropriate authentication options.");
    return;
  }

  for (const auto &instance : *instance_list) {
    auto instance_map = instance.as_map();

    std::string instance_address = instance_map->get_string("host");
    // Report that a new instance was discovered (not in the metadata).
    console->print_info("A new instance '" + instance_address +
                        "' was discovered in the cluster.");

    // Decide to add/remove instance based on given operation options.
    mysqlshdk::db::Connection_options instance_cnx_opts;
    if (m_options.auto_add) {
      instance_cnx_opts = mysqlshdk::db::Connection_options(instance_address);
    } else if (current_shell_options()->get().wizards) {
      if (console->confirm("Would you like to add it to the cluster metadata?",
                           Prompt_answer::YES) == Prompt_answer::YES) {
        instance_cnx_opts = mysqlshdk::db::Connection_options(instance_address);
      }
    }

    if (instance_cnx_opts.has_data()) {
      add_instance_to_metadata(instance_cnx_opts);
    }
  }
}

void Rescan::remove_metadata_for_instances(
    const std::shared_ptr<shcore::Value::Array_type> &instance_list) {
  auto console = mysqlsh::current_console();

  for (const auto &instance : *instance_list) {
    auto instance_map = instance.as_map();

    std::string instance_address = instance_map->get_string("host");
    // Report that an obsolete instance was found (in the metadata).
    console->print_info("The instance '" + instance_address +
                        "' is no longer part of the cluster.");

    if (m_options.auto_remove) {
      // Remove instance automatically if "auto" was used.
      remove_instance_from_metadata(instance_address);
    } else if (current_shell_options()->get().wizards) {
      console->print_info(
          "The instance is either offline or left the HA group. You can try "
          "to add it to the cluster again with the cluster.rejoinInstance('" +
          instance_map->get_string("host") +
          "') command or you can remove it from the cluster configuration.");

      // Ask to remove instance if prompts are enabled.
      if (console->confirm(
              "Would you like to remove it from the cluster metadata?",
              Prompt_answer::YES) == Prompt_answer::YES) {
        remove_instance_from_metadata(instance_address);
      }
    }
  }
}

void Rescan::update_metadata_for_instances(
    const std::shared_ptr<shcore::Value::Array_type> &instance_list) {
  auto console = mysqlsh::current_console();

  for (const auto &instance : *instance_list) {
    auto instance_map = instance.as_map();
    std::string label;
    std::string xport_endpoint_str;

    std::string instance_address = instance_map->get_string("host");
    // Report that an obsolete instance was found (in the metadata).
    if (instance_map->has_key("old_member_id")) {
      // member UUID changed
      console->print_info(shcore::str_format(
          "The instance '%s' is part of the cluster but its UUID has changed. "
          "Old UUID: %s. Current UUID: %s.",
          instance_address.c_str(),
          instance_map->get_string("old_member_id").c_str(),
          instance_map->get_string("member_id").c_str()));
    } else {
      // member address changed
      if (instance_map->has_key("old_host")) {
        console->print_info(shcore::str_format(
            "The instance '%s' is part of the cluster but its "
            "reported address has changed. "
            "Old address: %s. Current address: %s.",
            instance_address.c_str(),
            instance_map->get_string("old_host").c_str(),
            instance_map->get_string("host").c_str()));
      }

      // update instance label if it's the default value
      if (instance_map->get_string("label") ==
          instance_map->get_string("old_host")) {
        label = instance_map->get_string("host");
      }

      // xport has changed
      if (instance_map->has_key("old_xport_endpoint")) {
        xport_endpoint_str = instance_map->get_string("xport_endpoint");

        console->print_info(shcore::str_format(
            "The instance '%s' is part of the Cluster but the reported X "
            "plugin port has changed. "
            "Old xport: %s. Current xport: %s.",
            instance_address.c_str(),
            instance_map->get_string("old_xport_endpoint").c_str(),
            xport_endpoint_str.c_str()));
      }
    }

    // Decide to add/remove instance based on given operation options.
    auto instance_cnx_opts =
        mysqlshdk::db::Connection_options(instance_address);

    update_metadata_for_instance(instance_cnx_opts,
                                 instance_map->get_uint("id"), label);

    // The xplugin was disabled, remove the endpoint from the metadata
    if (xport_endpoint_str == k_xplugin_disabled) {
      m_cluster->get_metadata_storage()->update_instance_addresses(
          instance_map->get_string("member_id"), "mysqlX",
          shcore::Value::Null());
    }
  }
}

void Rescan::upgrade_comm_protocol() {
  auto console = mysqlsh::current_console();

  std::shared_ptr<Instance> cluster_instance = m_cluster->get_cluster_server();

  mysqlshdk::utils::Version gr_protocol_version_to_upgrade;

  // After removing instance, the remove command must set
  // the GR protocol of the group to the lowest MySQL version on the group.
  if (m_options.upgrade_comm_protocol) {
    if (mysqlshdk::gr::is_protocol_upgrade_possible(
            *cluster_instance, "", &gr_protocol_version_to_upgrade)) {
      console->print_note(
          "Upgrading Group Replication communication protocol to version " +
          gr_protocol_version_to_upgrade.get_full());

      mysqlshdk::gr::set_group_protocol_version(*cluster_instance,
                                                gr_protocol_version_to_upgrade);
    }
  } else {
    check_protocol_upgrade_possible(*cluster_instance, "");
  }
}

void Rescan::ensure_view_change_uuid_set() {
  // Only generate and set the value when all Cluster members are running a
  // version >= k_min_cs_version
  // NOTE: group_replication_view_change_uuid must be the same on all Cluster
  // members, otherwise the members won't join the cluster. Members that do not
  // know about the sysvar (<= 8.0.26) will be considered as having the sysvar
  // set to AUTOMATIC so we must ensure the sysvar is only set when all members
  // support it
  if (m_cluster->get_lowest_instance_version() <
      Precondition_checker::k_min_cs_version)
    return;

  // Check if group_replication_view_change_uuid is already set on the cluster
  std::shared_ptr<Instance> cluster_instance = m_cluster->get_cluster_server();
  auto view_change_uuid = cluster_instance->get_sysvar_string(
      "group_replication_view_change_uuid", "");

  bool skip_md_mismatch_check = false;

  // If not set (AUTOMATIC), generate a new value and set it on all members
  if (view_change_uuid == "AUTOMATIC") {
    // Check if the variable was persisted so it needs a restart
    std::string view_change_uuid_persisted =
        cluster_instance
            ->get_persisted_value("group_replication_view_change_uuid")
            .value_or("");

    auto console = mysqlsh::current_console();

    if (!view_change_uuid_persisted.empty() &&
        view_change_uuid_persisted != view_change_uuid) {
      console->print_warning(
          "The current Cluster group_replication_view_change_uuid setting does "
          "not allow ClusterSet to be implemented, because it's set but not "
          "yet effective.");

      skip_md_mismatch_check = true;
    } else {
      console->print_note(
          "The Cluster's group_replication_view_change_uuid is not set.");
      console->print_info(
          "Generating and setting a value for "
          "group_replication_view_change_uuid...");

      // Generate a value for group_replication_view_change_uuid
      view_change_uuid = cluster_instance->generate_uuid();

      // Get the Cluster Config Object
      // This ensures the config is set on ONLINE || RECOVERING members
      auto cfg = m_cluster->create_config_object({}, false, true);

      cfg->set("group_replication_view_change_uuid", view_change_uuid);
      cfg->apply();
    }

    console->print_note(
        "The Cluster must be completely taken OFFLINE and restarted "
        "(<<<dba.rebootClusterFromCompleteOutage>>>()) for the settings to "
        "be effective");
  }

  ensure_view_change_uuid_set_stored_metadata(view_change_uuid,
                                              skip_md_mismatch_check);
}

void Rescan::ensure_view_change_uuid_set_stored_metadata(
    const std::string &view_change_uuid, bool skip_md_mismatch_check) {
  auto console = mysqlsh::current_console();

  // Check if view_change_uuid is stored in the Metadata
  if (m_cluster->get_view_change_uuid().empty()) {
    console->print_info(
        "Updating group_replication_view_change_uuid in the Cluster's "
        "metadata...");

    if (view_change_uuid.empty()) {
      m_cluster->get_metadata_storage()->update_cluster_attribute(
          m_cluster->get_id(), "group_replication_view_change_uuid",
          shcore::Value(m_cluster->get_cluster_server()->get_sysvar_string(
              "group_replication_view_change_uuid", "")));
    } else {
      m_cluster->get_metadata_storage()->update_cluster_attribute(
          m_cluster->get_id(), "group_replication_view_change_uuid",
          shcore::Value(view_change_uuid));
    }
  } else if (!skip_md_mismatch_check) {
    // Ensure that the value of group_replication_view_change_uuid set on the
    // cluster matches the value stored in the Metadata. Do not check when the
    // Cluster was just updated with a new value for view_change_uuid and needs
    // to be restarted
    auto view_change_uuid_cluster =
        m_cluster->get_cluster_server()->get_sysvar_string(
            "group_replication_view_change_uuid", "");

    if (view_change_uuid_cluster != m_cluster->get_view_change_uuid()) {
      console->print_info(
          "Updating group_replication_view_change_uuid in the Cluster's "
          "metadata...");

      m_cluster->get_metadata_storage()->update_cluster_attribute(
          m_cluster->get_id(), "group_replication_view_change_uuid",
          shcore::Value(view_change_uuid_cluster));
    }
  }
}

void Rescan::ensure_transaction_size_limit_stored_metadata() {
  auto console = mysqlsh::current_console();
  shcore::Value value;

  // Check if transaction_size_limit is stored in the Metadata
  if (!m_cluster->get_metadata_storage()->query_cluster_attribute(
          m_cluster->get_id(), k_cluster_attribute_transaction_size_limit,
          &value)) {
    console->print_info(
        "Updating group_replication_transaction_size_limit in the Cluster's "
        "metadata...");

    m_cluster->get_metadata_storage()->update_cluster_attribute(
        m_cluster->get_id(), k_cluster_attribute_transaction_size_limit,
        shcore::Value(m_cluster->get_cluster_server()
                          ->get_sysvar_int(kGrTransactionSizeLimit)
                          .value_or(0)));
  }
}

void Rescan::ensure_transaction_size_limit_consistency() {
  // Get the Cluster's transaction size_limit stored in the Metadata if the
  // Cluster is standlone or a Primary. Otherwise, it's a Replica so it should
  // be zero
  int64_t cluster_transaction_size_limit = 0;

  if (!m_cluster->is_cluster_set_member() ||
      (m_cluster->is_cluster_set_member() && m_cluster->is_primary_cluster())) {
    shcore::Value value;
    m_cluster->get_metadata_storage()->query_cluster_attribute(
        m_cluster->get_id(), k_cluster_attribute_transaction_size_limit,
        &value);

    cluster_transaction_size_limit = value.as_int();
  }

  m_cluster->execute_in_members(
      [cluster_transaction_size_limit](
          const std::shared_ptr<Instance> &instance,
          const Cluster_impl::Instance_md_and_gr_member &info) {
        if (info.second.state != mysqlshdk::gr::Member_state::RECOVERING &&
            info.second.state != mysqlshdk::gr::Member_state::UNREACHABLE) {
          // Get the instance's value for transaction_size_limit
          int64_t instance_transaction_size_limit =
              instance->get_sysvar_int(kGrTransactionSizeLimit).value_or(0);

          if (instance_transaction_size_limit !=
              cluster_transaction_size_limit) {
            auto console = mysqlsh::current_console();

            console->print_note(
                "'group_replication_transaction_size_limit' value at '" +
                instance->descr() + "' does not match the Cluster's value: '" +
                std::to_string(cluster_transaction_size_limit) + "'");

            console->print_info("Updating the member's value...");

            log_info(
                "Updating group_replication_transaction_size_limit on "
                "instance '%s' to the Cluster's value '%" PRId64 "'",
                instance->descr().c_str(), cluster_transaction_size_limit);

            auto cfg = mysqlsh::dba::create_server_config(
                instance.get(), mysqlshdk::config::k_dft_cfg_server_handler);

            cfg->set(kGrTransactionSizeLimit,
                     std::optional<int64_t>(cluster_transaction_size_limit));

            cfg->apply();
          }
        }
        return true;
      },
      [](const shcore::Error &,
         const Cluster_impl::Instance_md_and_gr_member &) { return true; });
}

void Rescan::ensure_recovery_accounts_match() {
  m_cluster->execute_in_members(
      [this](const std::shared_ptr<Instance> &instance,
             const Cluster_impl::Instance_md_and_gr_member &info) {
        switch (info.second.state) {
          case mysqlshdk::gr::Member_state::RECOVERING:
          case mysqlshdk::gr::Member_state::UNREACHABLE:
            return true;
          default:
            break;
        }

        // Get the instance's recovery account
        auto user_hosts =
            m_repl_account_mng.get_replication_user(*instance, false);

        // Check if the account is using the old prefix
        auto old_prefix = shcore::str_beginswith(
            user_hosts.user,
            Replication_account::k_group_recovery_old_user_prefix);

        // Generate the recovery account string for this instance
        auto recovery_user_generated =
            Replication_account::make_replication_user_name(
                instance->get_server_id(),
                old_prefix
                    ? Replication_account::k_group_recovery_old_user_prefix
                    : Replication_account::k_group_recovery_user_prefix);

        // If the recovery user being used is different than the generated one
        // it means the instance was added using clone as the recovery method
        // and waitRecovery set to zero so the AdminAPI couldn't handle the fact
        // that clone copies everything including mysql.slave_master_info where
        // the replication credentials for the recovery channel are stored. Or
        // if the recovery user has changed / created explicitly by the user.
        if (user_hosts.user == recovery_user_generated) return true;

        auto console = mysqlsh::current_console();
        console->print_info(shcore::str_format(
            "Fixing incorrect recovery account '%s' in instance '%s'",
            user_hosts.user.c_str(), instance->descr().c_str()));

        if (!m_cluster->get_primary_master()->user_exists(
                recovery_user_generated, user_hosts.hosts.front())) {
          log_info("Recovery account '%s' does not exist in the cluster.",
                   recovery_user_generated.c_str());

          recovery_user_generated =
              Replication_account::make_replication_user_name(
                  instance->get_server_id(),
                  Replication_account::k_group_recovery_user_prefix);

          auto new_user_host = m_repl_account_mng.recreate_replication_user(
              *instance,
              m_cluster->query_cluster_instance_auth_cert_subject(*instance));

          m_cluster->get_metadata_storage()->update_instance_repl_account(
              instance->get_uuid(), Cluster_type::GROUP_REPLICATION,
              Replica_type::GROUP_MEMBER, new_user_host.user,
              new_user_host.host);

          console->print_warning(shcore::str_format(
              "A new recovery user for instance '%s' was created, which leaves "
              "the previous user, '%s', unused, so it should be removed.",
              instance->descr().c_str(), user_hosts.user.c_str()));

        } else {
          log_info("Updating recovery account '%s' in metadata.",
                   recovery_user_generated.c_str());

          m_cluster->reset_recovery_password(instance, recovery_user_generated,
                                             user_hosts.hosts);

          m_cluster->get_metadata_storage()->update_instance_repl_account(
              instance->get_uuid(), Cluster_type::GROUP_REPLICATION,
              Replica_type::GROUP_MEMBER, recovery_user_generated,
              user_hosts.hosts.front());
        }

        return true;
      },
      [](const shcore::Error &,
         const Cluster_impl::Instance_md_and_gr_member &) { return true; });
}

shcore::Value Rescan::execute() {
  auto console = mysqlsh::current_console();
  console->print_info("Rescanning the cluster...");
  console->print_info();

  shcore::Value::Map_type_ref result = get_rescan_report();

  console->print_info("Result of the rescanning operation for the '" +
                      result->get_string("name") + "' cluster:");
  console->print_value(shcore::Value(result), "");
  console->print_info();

  // Check if there are unknown instances
  if (result->has_key("newlyDiscoveredInstances")) {
    add_metadata_for_instances(result->get_array("newlyDiscoveredInstances"));
  }

  // Check if there are missing instances
  if (result->has_key("unavailableInstances")) {
    remove_metadata_for_instances(result->get_array("unavailableInstances"));
  }

  if (result->has_key("updatedInstances")) {
    update_metadata_for_instances(result->get_array("updatedInstances"));
  }

  // Fix all missing recovery accounts from metadata.
  m_repl_account_mng.ensure_metadata_has_recovery_accounts(true);

  // This calls ensures all of the instances have server_id as instance
  // attribute, including the case were rescan is executed and no new/updated
  // instances were found
  m_cluster->ensure_metadata_has_server_id(*m_cluster->get_cluster_server());

  // Check if group_replication_view_change_uuid is set on the Cluster and all
  // of its members when running MySQL >= 8.0.27. Also verify the Metadata
  // consistency regarding group_replication_view_change_uuid when already in
  // use.
  // Skip if running MySQL >= 8.3.0, since the option is no longer needed.

  if (m_is_view_change_uuid_supported) {
    auto view_change_uuid = m_cluster->get_cluster_server()->get_sysvar_string(
        "group_replication_view_change_uuid", "");

    if (view_change_uuid == "AUTOMATIC") {
      auto lowest_version_cluster = m_cluster->get_lowest_instance_version();

      DBUG_EXECUTE_IF("dba_view_change_uuid_needed", {
        lowest_version_cluster = mysqlshdk::utils::Version(8, 1, 0);
      });

      if (!m_options.update_view_change_uuid.has_value() &&
          lowest_version_cluster < k_view_change_uuid_deprecated) {
        console->print_note(
            "The Cluster is not configured to use "
            "'group_replication_view_change_uuid', which is required "
            "for InnoDB ClusterSet. Configuring it requires a full Cluster "
            "reboot.");
        if (current_shell_options()->get().wizards) {
          // Prompt the user to update the View Change UUID
          if (console->confirm(
                  "Would you like 'group_replication_view_change_uuid' to be "
                  "configured automatically?",
                  Prompt_answer::YES) == Prompt_answer::YES) {
            ensure_view_change_uuid_set();
          }
        } else {
          console->print_info(
              "Use the 'updateViewChangeUuid' option to generate and "
              "configure a value for the Cluster.");
        }
      } else if (m_options.update_view_change_uuid.value_or(false)) {
        ensure_view_change_uuid_set();
      }
    } else {
      // Ensures the value is stored in the metadata schema
      ensure_view_change_uuid_set_stored_metadata();
    }
  }

  // Ensures the value of group_replication_transaction_size_limit is stored
  // in the metadata
  ensure_transaction_size_limit_stored_metadata();

  // Ensures group_replication_transaction_size_limit is set to the same value
  // in all cluster members
  ensure_transaction_size_limit_consistency();

  // Check if the topology mode changed.
  // NOTE: This must be performed after updating the instances in the metadata,
  //       in order for the list of instance to be retrieved properly to update
  //       the auto-increment settings on all of them.
  if (result->has_key("newTopologyMode") &&
      !result->is_null("newTopologyMode")) {
    auto new_topology_mode = result->get_string("newTopologyMode");

    if (!new_topology_mode.empty()) {
      console->print_note("The topology mode of the cluster changed to '" +
                          new_topology_mode + "'.");

      // Update the topology mode in the Metadata
      update_topology_mode(mysqlshdk::gr::to_topology_mode(new_topology_mode));
    }
  }

  if (m_options.upgrade_comm_protocol) {
    upgrade_comm_protocol();
  }

  // Check for instances using a different account than its own for the
  // recovery channel and do a reset+update of the credentials to ensure the
  // right account is used. Finally, update the MD schema to store the right
  // account in use.
  //
  // A member rejoining needs to be able to connect to itself and to all
  // cluster members, as well as all cluster members need to be able to
  // connect to the joining member.
  ensure_recovery_accounts_match();

  // removes unused recovery accounts
  for (const auto &[user, host] :
       m_repl_account_mng.get_unused_recovery_accounts(
           m_repl_account_mng.get_mismatched_recovery_accounts())) {
    try {
      console->print_info(
          shcore::str_format("Dropping unused recovery account: '%s'@'%s'",
                             user.c_str(), host.c_str()));
      m_cluster->get_primary_master()->drop_user(user, host, true);
    } catch (...) {
      console->print_warning(shcore::str_format(
          "Error dropping recovery account '%s'@'%s': %s", user.c_str(),
          host.c_str(), format_active_exception().c_str()));
    }
  }

  // Check if the Metadata is not consistent so it needs a clean-up
  if (result->has_key("metadataConsistent") &&
      !result->get_bool("metadataConsistent")) {
    if (!m_options.repair_metadata.has_value()) {
      console->print_warning(
          "Metadata inconsistencies detected, possibly due to one or more "
          "failed commands. Please review the topology status before "
          "proceeding.");
      if (current_shell_options()->get().wizards) {
        // Prompt the user to confirm the action
        if (console->confirm(
                "The invalid Cluster(s) listed above will be removed from the "
                "metadata. Do you want to proceed?",
                Prompt_answer::YES) == Prompt_answer::YES) {
          repair_metadata();
        }
      } else {
        console->print_info(shcore::str_format(
            "Use the '%s' option to repair the Metadata inconsistencies.",
            kRepairMetadata));
      }
    } else if (*m_options.repair_metadata) {
      repair_metadata();
    }
  }

  return shcore::Value();
}

void Rescan::rollback() {
  // Do nothing right now, but it might be used in the future when
  // transactional command execution feature will be available.
}

void Rescan::finish() {
  // Do nothing.
}

}  // namespace cluster
}  // namespace dba
}  // namespace mysqlsh
