/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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.
 */

package org.apache.cassandra.tcm.sequences;

import java.io.IOException;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;

import com.google.common.annotations.VisibleForTesting;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.DynamicEndpointSnitch;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tcm.MultiStepOperation;
import org.apache.cassandra.tcm.Transformation;
import org.apache.cassandra.tcm.membership.Directory;
import org.apache.cassandra.tcm.membership.Location;
import org.apache.cassandra.tcm.membership.NodeId;
import org.apache.cassandra.tcm.membership.NodeState;
import org.apache.cassandra.tcm.ownership.DataPlacements;
import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer;
import org.apache.cassandra.tcm.serialization.MetadataSerializer;
import org.apache.cassandra.tcm.serialization.Version;
import org.apache.cassandra.tcm.transformations.PrepareLeave;
import org.apache.cassandra.utils.JVMStabilityInspector;
import org.apache.cassandra.utils.vint.VIntCoding;

import static com.google.common.collect.ImmutableList.of;
import static org.apache.cassandra.tcm.MultiStepOperation.Kind.LEAVE;
import static org.apache.cassandra.tcm.Transformation.Kind.FINISH_LEAVE;
import static org.apache.cassandra.tcm.Transformation.Kind.MID_LEAVE;
import static org.apache.cassandra.tcm.Transformation.Kind.START_LEAVE;
import static org.apache.cassandra.tcm.sequences.SequenceState.continuable;
import static org.apache.cassandra.tcm.sequences.SequenceState.error;

public class UnbootstrapAndLeave extends MultiStepOperation<Epoch>
{
    private static final Logger logger = LoggerFactory.getLogger(UnbootstrapAndLeave.class);
    public static final Serializer serializer = new Serializer();

    public final LockedRanges.Key lockKey;
    public final Transformation.Kind next;

    public final PrepareLeave.StartLeave startLeave;
    public final PrepareLeave.MidLeave midLeave;
    public final PrepareLeave.FinishLeave finishLeave;
    private final LeaveStreams streams;

    public static UnbootstrapAndLeave newSequence(Epoch preparedAt,
                                                  LockedRanges.Key lockKey,
                                                  PrepareLeave.StartLeave startLeave,
                                                  PrepareLeave.MidLeave midLeave,
                                                  PrepareLeave.FinishLeave finishLeave,
                                                  LeaveStreams streams)
    {
        return new UnbootstrapAndLeave(preparedAt,
                                       lockKey,
                                       START_LEAVE,
                                       startLeave, midLeave, finishLeave,
                                       streams);
    }

    /**
     * Used by factory method for external callers and by Serializer
     */
    @VisibleForTesting
    UnbootstrapAndLeave(Epoch latestModification,
                        LockedRanges.Key lockKey,
                        Transformation.Kind next,
                        PrepareLeave.StartLeave startLeave,
                        PrepareLeave.MidLeave midLeave,
                        PrepareLeave.FinishLeave finishLeave,
                        LeaveStreams streams)
    {
        super(nextToIndex(next), latestModification);
        this.lockKey = lockKey;
        this.next = next;
        this.startLeave = startLeave;
        this.midLeave = midLeave;
        this.finishLeave = finishLeave;
        this.streams = streams;
    }

    /**
     * Used by advance to move forward in the sequence after execution
     */
    private UnbootstrapAndLeave(UnbootstrapAndLeave current, Epoch latestModification)
    {
        super(current.idx + 1, latestModification);
        this.next = indexToNext(current.idx + 1);
        this.lockKey = current.lockKey;
        this.startLeave = current.startLeave;
        this.midLeave = current.midLeave;
        this.finishLeave = current.finishLeave;
        this.streams = current.streams;
    }

    @Override
    public Kind kind()
    {
        switch (streams.kind())
        {
            case UNBOOTSTRAP:
                return LEAVE;
            case REMOVENODE:
                return MultiStepOperation.Kind.REMOVE;
            default:
                throw new IllegalStateException("Invalid stream kind: "+streams.kind());
        }
    }

    @Override
    protected SequenceKey sequenceKey()
    {
        return startLeave.nodeId();
    }

    @Override
    public MetadataSerializer<? extends SequenceKey> keySerializer()
    {
        return NodeId.serializer;
    }

    @Override
    public Transformation.Kind nextStep()
    {
        return indexToNext(idx);
    }

    @Override
    public Transformation.Result applyTo(ClusterMetadata metadata)
    {
        return applyMultipleTransformations(metadata, next, of(startLeave, midLeave, finishLeave));
    }

    @Override
    public Set<NodeId> affectedPeers(Directory directory)
    {
        Set<InetAddressAndPort> affectedEndpoints = new HashSet<>();
        affectedEndpoints.addAll(startLeave.affectedEndpoints());
        affectedEndpoints.addAll(midLeave.affectedEndpoints());
        affectedEndpoints.addAll(finishLeave.affectedEndpoints());
        return endpointsToIds(affectedEndpoints, directory);
    }

    @Override
    public SequenceState executeNext()
    {
        switch (next)
        {
            case START_LEAVE:
                try
                {
                    DatabaseDescriptor.getSeverityDuringDecommission().ifPresent(DynamicEndpointSnitch::addSeverity);
                    ClusterMetadataService.instance().commit(startLeave);
                }
                catch (Throwable t)
                {
                    logger.warn("Exception committing startLeave, will retry", t);
                    JVMStabilityInspector.inspectThrowable(t);
                    return continuable();
                }
                break;
            case MID_LEAVE:
                try
                {
                    streams.execute(startLeave.nodeId(),
                                    startLeave.delta(),
                                    midLeave.delta(),
                                    finishLeave.delta());
                    ClusterMetadataService.instance().commit(midLeave);
                }
                catch (ExecutionException e)
                {
                    if (startLeave.nodeId().equals(ClusterMetadata.current().myNodeId()))
                        StorageService.instance.markDecommissionFailed();
                    JVMStabilityInspector.inspectThrowable(e);
                    logger.error("Error while decommissioning node: {}", e.getCause().getMessage());
                    throw new RuntimeException("Error while decommissioning node: " + e.getCause().getMessage());
                }
                catch (Throwable t)
                {
                    logger.warn("Exception committing midLeave, will retry", t);
                    JVMStabilityInspector.inspectThrowable(t);
                    return continuable();
                }
                break;
            case FINISH_LEAVE:
                try
                {
                    ClusterMetadataService.instance().commit(finishLeave);
                    StorageService.instance.clearTransientMode();
                }
                catch (Throwable t)
                {
                    logger.warn("Exception committing finishLeave, will retry", t);
                    JVMStabilityInspector.inspectThrowable(t);
                    return continuable();
                }
                break;
            default:
                return error(new IllegalStateException("Can't proceed with leave from " + next));
        }

        return continuable();
    }

    @Override
    public UnbootstrapAndLeave advance(Epoch waitUntilAcknowledged)
    {
        return new UnbootstrapAndLeave(this, waitUntilAcknowledged);
    }

    @Override
    public ProgressBarrier barrier()
    {
        ClusterMetadata metadata = ClusterMetadata.current();
        LockedRanges.AffectedRanges affectedRanges = metadata.lockedRanges.locked.get(lockKey);
        Location location = metadata.directory.location(startLeave.nodeId());
        if (kind() == MultiStepOperation.Kind.REMOVE)
            return new ProgressBarrier(latestModification, location, affectedRanges, (e) -> !e.equals(metadata.directory.endpoint(startLeave.nodeId())));
        else
            return new ProgressBarrier(latestModification, location, affectedRanges);
    }

    @Override
    public ClusterMetadata.Transformer cancel(ClusterMetadata metadata)
    {
        DataPlacements placements = metadata.placements();
        switch (next)
        {
            // need to undo MID_LEAVE and START_LEAVE, but PrepareLeave doesn't affect placement
            case FINISH_LEAVE:
                placements = midLeave.inverseDelta().apply(metadata.nextEpoch(), placements);
            case MID_LEAVE:
            case START_LEAVE:
                placements = startLeave.inverseDelta().apply(metadata.nextEpoch(), placements);
                break;
            default:
                throw new IllegalStateException("Can't revert leave from " + next);
        }
        LockedRanges newLockedRanges = metadata.lockedRanges.unlock(lockKey);
        return metadata.transformer()
                       .with(placements)
                       .with(newLockedRanges)
                       .withNodeState(startLeave.nodeId(), NodeState.JOINED);
    }

    @Override
    public String status()
    {
        // Overridden to maintain compatibility with nodetool removenode output
        return String.format("step: %s, streams: %s", next, streams.status());
    }

    private static int nextToIndex(Transformation.Kind next)
    {
        switch (next)
        {
            case START_LEAVE:
                return 0;
            case MID_LEAVE:
                return 1;
            case FINISH_LEAVE:
                return 2;
            default:
                throw new IllegalStateException(String.format("Step %s is invalid for sequence %s ", next, LEAVE));
        }
    }

    private static Transformation.Kind indexToNext(int index)
    {
        switch (index)
        {
            case 0:
                return START_LEAVE;
            case 1:
                return MID_LEAVE;
            case 2:
                return FINISH_LEAVE;
            default:
                throw new IllegalStateException(String.format("Step %s is invalid for sequence %s ", index, LEAVE));
        }
    }

    @Override
    public boolean finishDuringStartup()
    {
        return false;
    }

    @Override
    public String toString()
    {
        return "UnbootstrapAndLeavePlan{" +
               "lastModified=" + latestModification +
               ", lockKey=" + lockKey +
               ", startLeave=" + startLeave +
               ", midLeave=" + midLeave +
               ", finishLeave=" + finishLeave +
               ", next=" + next +
               '}';
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UnbootstrapAndLeave that = (UnbootstrapAndLeave) o;
        return next == that.next &&
               Objects.equals(startLeave, that.startLeave) &&
               Objects.equals(midLeave, that.midLeave) &&
               Objects.equals(finishLeave, that.finishLeave) &&
               Objects.equals(latestModification, that.latestModification) &&
               Objects.equals(lockKey, that.lockKey);
    }

    @Override
    public int hashCode()
    {
        return Objects.hash(startLeave, midLeave, finishLeave, latestModification, lockKey, next);
    }

    public static class Serializer implements AsymmetricMetadataSerializer<MultiStepOperation<?>, UnbootstrapAndLeave>
    {
        public void serialize(MultiStepOperation<?> t, DataOutputPlus out, Version version) throws IOException
        {
            UnbootstrapAndLeave plan = (UnbootstrapAndLeave) t;

            Epoch.serializer.serialize(plan.latestModification, out, version);
            LockedRanges.Key.serializer.serialize(plan.lockKey, out, version);
            VIntCoding.writeUnsignedVInt32(plan.next.ordinal(), out);
            VIntCoding.writeUnsignedVInt32(plan.streams.kind().ordinal(), out);

            PrepareLeave.StartLeave.serializer.serialize(plan.startLeave, out, version);
            PrepareLeave.MidLeave.serializer.serialize(plan.midLeave, out, version);
            PrepareLeave.FinishLeave.serializer.serialize(plan.finishLeave, out, version);
        }

        public UnbootstrapAndLeave deserialize(DataInputPlus in, Version version) throws IOException
        {
            Epoch barrier = Epoch.serializer.deserialize(in, version);
            LockedRanges.Key lockKey = LockedRanges.Key.serializer.deserialize(in, version);

            Transformation.Kind next = Transformation.Kind.values()[VIntCoding.readUnsignedVInt32(in)];
            LeaveStreams.Kind streamKind = LeaveStreams.Kind.values()[VIntCoding.readUnsignedVInt32(in)];
            PrepareLeave.StartLeave startLeave = PrepareLeave.StartLeave.serializer.deserialize(in, version);
            PrepareLeave.MidLeave midLeave = PrepareLeave.MidLeave.serializer.deserialize(in, version);
            PrepareLeave.FinishLeave finishLeave = PrepareLeave.FinishLeave.serializer.deserialize(in, version);

            return new UnbootstrapAndLeave(barrier, lockKey, next,
                                           startLeave, midLeave, finishLeave,
                                           streamKind.supplier.get());
        }

        public long serializedSize(MultiStepOperation<?> t, Version version)
        {
            UnbootstrapAndLeave plan = (UnbootstrapAndLeave) t;
            long size = Epoch.serializer.serializedSize(plan.latestModification, version);
            size += LockedRanges.Key.serializer.serializedSize(plan.lockKey, version);

            size += VIntCoding.computeVIntSize(plan.kind().ordinal());
            size += VIntCoding.computeVIntSize(plan.streams.kind().ordinal());
            size += PrepareLeave.StartLeave.serializer.serializedSize(plan.startLeave, version);
            size += PrepareLeave.StartLeave.serializer.serializedSize(plan.midLeave, version);
            size += PrepareLeave.StartLeave.serializer.serializedSize(plan.finishLeave, version);
            return size;
        }
    }
}
