/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * The OpenSearch Contributors require contributions made to
 * this file be licensed under the Apache-2.0 license or a
 * compatible open source license.
 */

/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you under
 * the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/*
 * Modifications Copyright OpenSearch Contributors. See
 * GitHub history for details.
 */

package org.opensearch.cluster.coordination;

import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.opensearch.OpenSearchException;
import org.opensearch.cli.Terminal;
import org.opensearch.cluster.ClusterState;
import org.opensearch.cluster.metadata.IndexMetadata;
import org.opensearch.cluster.metadata.Metadata;
import org.opensearch.cluster.node.DiscoveryNode;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.UUIDs;
import org.opensearch.common.collect.Tuple;
import org.opensearch.common.settings.Setting;
import org.opensearch.common.settings.Settings;
import org.opensearch.env.Environment;
import org.opensearch.gateway.PersistedClusterStateService;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Locale;
import java.util.Objects;

import static org.opensearch.gateway.remote.RemoteClusterStateService.REMOTE_CLUSTER_STATE_ENABLED_SETTING;

/**
 * Tool to run an unsafe bootstrap
 *
 * @opensearch.internal
 */
public class UnsafeBootstrapClusterManagerCommand extends OpenSearchNodeCommand {

    static final String CLUSTER_STATE_TERM_VERSION_MSG_FORMAT = "Current node cluster state (term, version) pair is (%s, %s)";
    static final String CONFIRMATION_MSG = DELIMITER
        + "\n"
        + "You should only run this tool if you have permanently lost half or more\n"
        + "of the cluster-manager-eligible nodes in this cluster, and you cannot restore the\n"
        + "cluster from a snapshot. This tool can cause arbitrary data loss and its\n"
        + "use should be your last resort. If you have multiple surviving cluster-manager\n"
        + "eligible nodes, you should run this tool on the node with the highest\n"
        + "cluster state (term, version) pair.\n"
        + "\n"
        + "Do you want to proceed?\n";

    static final String NOT_CLUSTER_MANAGER_NODE_MSG = "unsafe-bootstrap tool can only be run on cluster-manager eligible node";

    static final String EMPTY_LAST_COMMITTED_VOTING_CONFIG_MSG =
        "last committed voting voting configuration is empty, cluster has never been bootstrapped?";

    static final String CLUSTER_MANAGER_NODE_BOOTSTRAPPED_MSG = "Cluster-manager node was successfully bootstrapped";
    static final Setting<String> UNSAFE_BOOTSTRAP = ClusterService.USER_DEFINED_METADATA.getConcreteSetting(
        "cluster.metadata.unsafe-bootstrap"
    );
    static final String REMOTE_CLUSTER_STATE_ENABLED_NODE =
        "Unsafe bootstrap cannot be performed when remote cluster state is enabled. The cluster state in the remote store is considered the source of truth. "
            + "In case, you still wish to do best effort recovery with unsafe-bootstrap, then please disable the "
            + REMOTE_CLUSTER_STATE_ENABLED_SETTING.getKey()
            + ". For more details, please check the OpenSearch documentation.";
    private OptionSpec<Boolean> applyClusterReadOnlyBlockOption;

    UnsafeBootstrapClusterManagerCommand() {
        super(
            "Forces the successful election of the current node after the permanent loss of the half or more cluster-manager-eligible nodes"
        );
        applyClusterReadOnlyBlockOption = parser.accepts("apply-cluster-read-only-block", "Optional cluster.blocks.read_only setting")
            .withOptionalArg()
            .ofType(Boolean.class);
    }

    @Override
    protected boolean validateBeforeLock(Terminal terminal, Environment env) {
        Settings settings = env.settings();
        terminal.println(Terminal.Verbosity.VERBOSE, "Checking node.roles setting");
        Boolean clusterManager = DiscoveryNode.isClusterManagerNode(settings);
        if (clusterManager == false) {
            throw new OpenSearchException(NOT_CLUSTER_MANAGER_NODE_MSG);
        }
        // During unsafe bootstrap, node will form a cluster with a new cluster UUID but with the existing metadata.
        // This new state will not know about the previous cluster UUIDs and so we will not able to construct
        // the cluster UUID chain to get the last known cluster UUID to restore from.
        // Blocking unsafe-bootstrap below for this reason.
        if (REMOTE_CLUSTER_STATE_ENABLED_SETTING.get(settings) == true) {
            throw new OpenSearchException(REMOTE_CLUSTER_STATE_ENABLED_NODE);
        }

        return true;
    }

    protected void processNodePaths(Terminal terminal, Path[] dataPaths, int nodeLockId, OptionSet options, Environment env)
        throws IOException {
        final PersistedClusterStateService persistedClusterStateService = createPersistedClusterStateService(env.settings(), dataPaths);

        final Tuple<Long, ClusterState> state = loadTermAndClusterState(persistedClusterStateService, env);
        final ClusterState oldClusterState = state.v2();

        final Metadata metadata = oldClusterState.metadata();

        final CoordinationMetadata coordinationMetadata = metadata.coordinationMetadata();
        if (coordinationMetadata == null
            || coordinationMetadata.getLastCommittedConfiguration() == null
            || coordinationMetadata.getLastCommittedConfiguration().isEmpty()) {
            throw new OpenSearchException(EMPTY_LAST_COMMITTED_VOTING_CONFIG_MSG);
        }
        terminal.println(
            String.format(Locale.ROOT, CLUSTER_STATE_TERM_VERSION_MSG_FORMAT, coordinationMetadata.term(), metadata.version())
        );

        CoordinationMetadata newCoordinationMetadata = CoordinationMetadata.builder(coordinationMetadata)
            .clearVotingConfigExclusions()
            .lastAcceptedConfiguration(
                new CoordinationMetadata.VotingConfiguration(Collections.singleton(persistedClusterStateService.getNodeId()))
            )
            .lastCommittedConfiguration(
                new CoordinationMetadata.VotingConfiguration(Collections.singleton(persistedClusterStateService.getNodeId()))
            )
            .build();

        Settings persistentSettings = Settings.builder().put(metadata.persistentSettings()).put(UNSAFE_BOOTSTRAP.getKey(), true).build();

        Boolean applyClusterReadOnlyBlock = applyClusterReadOnlyBlockOption.value(options);
        if (Objects.nonNull(applyClusterReadOnlyBlock)) {
            persistentSettings = Settings.builder()
                .put(persistentSettings)
                .put(Metadata.SETTING_READ_ONLY_SETTING.getKey(), applyClusterReadOnlyBlock)
                .build();
        }

        Metadata.Builder newMetadata = Metadata.builder(metadata)
            .clusterUUID(Metadata.UNKNOWN_CLUSTER_UUID)
            .generateClusterUuidIfNeeded()
            .clusterUUIDCommitted(true)
            .persistentSettings(persistentSettings)
            .coordinationMetadata(newCoordinationMetadata);
        for (final IndexMetadata indexMetadata : metadata.indices().values()) {
            newMetadata.put(
                IndexMetadata.builder(indexMetadata)
                    .settings(
                        Settings.builder()
                            .put(indexMetadata.getSettings())
                            .put(IndexMetadata.SETTING_HISTORY_UUID, UUIDs.randomBase64UUID())
                    )
            );
        }

        final ClusterState newClusterState = ClusterState.builder(oldClusterState).metadata(newMetadata).build();

        terminal.println(
            Terminal.Verbosity.VERBOSE,
            "[old cluster state = " + oldClusterState + ", new cluster state = " + newClusterState + "]"
        );

        confirm(terminal, CONFIRMATION_MSG);

        try (PersistedClusterStateService.Writer writer = persistedClusterStateService.createWriter()) {
            writer.writeFullStateAndCommit(state.v1(), newClusterState);
        }

        terminal.println(CLUSTER_MANAGER_NODE_BOOTSTRAPPED_MSG);
    }
}
