/*
 * 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.hadoop.hdds.scm.container.replication.health;

import java.util.Set;
import java.util.stream.Collectors;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.ContainerReplicaProto;
import org.apache.hadoop.hdds.scm.container.ContainerHealthState;
import org.apache.hadoop.hdds.scm.container.ContainerInfo;
import org.apache.hadoop.hdds.scm.container.ContainerReplica;
import org.apache.hadoop.hdds.scm.container.replication.ContainerCheckRequest;
import org.apache.hadoop.hdds.scm.container.replication.ReplicationManager;
import org.apache.ratis.protocol.exceptions.NotLeaderException;
import org.apache.ratis.util.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This handler deletes a container if it's closed or quasi-closed and empty (0 key count)
 * and all its replicas are empty.
 */
public class EmptyContainerHandler extends AbstractCheck {
  private static final Logger LOG =
      LoggerFactory.getLogger(EmptyContainerHandler.class);

  private final ReplicationManager replicationManager;

  public EmptyContainerHandler(ReplicationManager replicationManager) {
    this.replicationManager = replicationManager;
  }

  /**
   * Deletes a container if it's closed or quasi-closed and empty (0 key count) and all its
   * replicas are empty.
   * @param request ContainerCheckRequest object representing the container
   * @return true if the specified container is empty, otherwise false
   */
  @Override
  public boolean handle(ContainerCheckRequest request) {
    ContainerInfo containerInfo = request.getContainerInfo();
    Set<ContainerReplica> replicas = request.getContainerReplicas();

    if (isContainerEmptyAndClosed(containerInfo, replicas)) {
      request.getReport().incrementAndSample(ContainerHealthState.EMPTY, containerInfo);
      if (!request.isReadOnly()) {
        LOG.debug("Container {} is empty and closed, marking as DELETING",
            containerInfo);
        // delete replicas if they are closed and empty
        deleteContainerReplicas(containerInfo, replicas);

        if (containerInfo.getReplicationType() == HddsProtos.ReplicationType.RATIS) {
          if (replicas.stream().filter(r -> r.getSequenceId() != null)
              .noneMatch(r -> r.getSequenceId() == containerInfo.getSequenceId())) {
            // don't update container state if replica seqid don't match with container seq id
            return true;
          }
        }
        // Update the container's state
        replicationManager.updateContainerState(
            containerInfo.containerID(), HddsProtos.LifeCycleEvent.DELETE);
      }
      return true;
    } else if (isContainerEmptyAndQuasiClosed(containerInfo, replicas)) {
      request.getReport().incrementAndSample(ContainerHealthState.EMPTY, containerInfo);
      if (!request.isReadOnly()) {
        String originIds = replicas.stream()
            .map(r -> r.getOriginDatanodeId().toString())
            .collect(Collectors.joining(", "));
        long maxReplicaBCSID = replicas.stream()
            .filter(r -> r.getSequenceId() != null)
            .mapToLong(ContainerReplica::getSequenceId)
            .max()
            .orElse(containerInfo.getSequenceId());
        
        // Update container bcsId to max replica bcsId before deletion
        // This ensures resurrection logic uses the correct bcsId for stale replica detection
        if (maxReplicaBCSID > containerInfo.getSequenceId()) {
          LOG.info("Updating bcsId for empty QUASI_CLOSED container {} from {} to {} before deletion",
              containerInfo.containerID(), containerInfo.getSequenceId(), maxReplicaBCSID);
          containerInfo.updateSequenceId(maxReplicaBCSID);
        }
        
        LOG.info("Deleting empty QUASI_CLOSED container {} (container BCSID={}, max replica BCSID={}) with {} " +
            "replicas from originIds: [{}]. If resurrected, container will transition to QUASI_CLOSED",
            containerInfo.containerID(), containerInfo.getSequenceId(), maxReplicaBCSID,
            replicas.size(), originIds);
        // Update the container's state
        replicationManager.updateContainerState(
                containerInfo.containerID(), HddsProtos.LifeCycleEvent.DELETE);
        // Delete replicas AFTER transitioning to DELETING state
        deleteContainerReplicas(containerInfo, replicas);
      }
      return true;
    } else if (containerInfo.getState() == HddsProtos.LifeCycleState.CLOSED
        && containerInfo.getNumberOfKeys() == 0 && replicas.isEmpty()) {
      // If the container is empty and has no replicas, it is possible it was
      // a container which stuck in the closing state which never got any
      // replicas created on the datanodes. In this case, we don't have enough
      // information to delete the container, so we just log it as EMPTY,
      // leaving it as CLOSED and return true, otherwise, it will end up marked
      // as missing in the replication check handlers.
      request.getReport().incrementAndSample(ContainerHealthState.EMPTY, containerInfo);
      LOG.debug("Container {} appears empty and is closed, but cannot be " +
              "deleted because it has no replicas. Marking as EMPTY.",
          containerInfo);
      return true;
    }

    return false;
  }

  /**
   * Returns true if the container is empty and CLOSED.
   * A container is empty if its key count is 0. The usedBytes counter is not
   * checked here because usedBytes is not an accurate representation of the
   * committed blocks. There could be orphaned chunks in the container which
   * contribute to usedBytes.
   *
   * @param container Container to check
   * @param replicas Set of ContainerReplica
   * @return true if the container is considered empty, false otherwise
   */
  private boolean isContainerEmptyAndClosed(final ContainerInfo container,
      final Set<ContainerReplica> replicas) {
    return container.getState() == HddsProtos.LifeCycleState.CLOSED &&
        !replicas.isEmpty() &&
        replicas.stream().allMatch(
            r -> r.getState() == ContainerReplicaProto.State.CLOSED &&
                r.isEmpty());
  }

  /**
   * Returns true if the container is empty and QUASI_CLOSED.
   * For QUASI_CLOSED containers, replicas can be in QUASI_CLOSED, OPEN,
   * CLOSING, or UNHEALTHY states. We check if all replicas are empty regardless
   * of their state.
   *
   * @param container Container to check
   * @param replicas Set of ContainerReplica
   * @return true if the container is considered empty and quasi-closed, false otherwise
   */
  private boolean isContainerEmptyAndQuasiClosed(final ContainerInfo container,
      final Set<ContainerReplica> replicas) {
    return container.getState() == HddsProtos.LifeCycleState.QUASI_CLOSED &&
            !replicas.isEmpty() &&
            replicas.stream().allMatch(ContainerReplica::isEmpty);
  }

  /**
   * Deletes the specified container's replicas if they are empty.
   * For CLOSED containers, replicas must also be CLOSED.
   * For QUASI_CLOSED containers, replicas can be in any state (QUASI_CLOSED, OPEN, CLOSING, UNHEALTHY),
   * but delete commands should be sent to replicas in stable states (QUASI_CLOSED or CLOSED).
   *
   * @param containerInfo ContainerInfo to delete
   * @param replicas Set of ContainerReplica
   */
  private void deleteContainerReplicas(final ContainerInfo containerInfo,
      final Set<ContainerReplica> replicas) {
    for (ContainerReplica rp : replicas) {
      Preconditions.assertSame(true, rp.isEmpty(), "replica empty");

      // Only send delete commands to replicas in CLOSED/QUASI_CLOSED states
      if (rp.getState() != ContainerReplicaProto.State.QUASI_CLOSED
          && rp.getState() != ContainerReplicaProto.State.CLOSED) {
        LOG.info("Skipping delete command for replica in {} state for empty container {} on datanode {}. " +
            "Will retry after replica transitions to QUASI_CLOSED or CLOSED.",
            rp.getState(), containerInfo.containerID(), rp.getDatanodeDetails());
        continue;
      }

      try {
        replicationManager.sendDeleteCommand(containerInfo,
            rp.getReplicaIndex(), rp.getDatanodeDetails(), false);
      } catch (NotLeaderException e) {
        LOG.warn("Failed to delete empty replica with index {} for container" +
                " {} on datanode {}",
            rp.getReplicaIndex(),
            containerInfo.containerID(),
            rp.getDatanodeDetails(), e);
      }
    }
  }

}
