/*
 * 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.solr.core.snapshots;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.apache.lucene.index.IndexCommit;
import org.apache.lucene.index.IndexDeletionPolicy;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.index.NoMergePolicy;
import org.apache.lucene.store.Directory;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.SolrCore;
import org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager.SnapshotMetaData;
import org.apache.solr.update.SolrIndexWriter;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class provides functionality required to handle the data files corresponding to Solr
 * snapshots.
 */
public class SolrSnapshotManager {
  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  public static final String INDEX_DIR_PATH = "indexDirPath";
  public static final String GENERATION_NUM = "generation";
  public static final String SNAPSHOT_STATUS = "status";
  public static final String CREATION_DATE = "creationDate";
  public static final String SNAPSHOT_REPLICAS = "replicas";
  public static final String SNAPSHOTS_INFO = "snapshots";
  public static final String LEADER = "leader";
  public static final String SHARD_ID = "shard_id";
  public static final String FILE_LIST = "files";

  /**
   * This method returns if a named snapshot exists for the specified collection.
   *
   * @param zkClient Zookeeper client
   * @param collectionName The name of the collection
   * @param commitName The name of the snapshot
   * @return true if the named snapshot exists false Otherwise
   * @throws KeeperException In case of Zookeeper error
   * @throws InterruptedException In case of thread interruption.
   */
  public static boolean snapshotExists(
      SolrZkClient zkClient, String collectionName, String commitName)
      throws KeeperException, InterruptedException {
    String zkPath = getSnapshotMetaDataZkPath(collectionName, Optional.ofNullable(commitName));
    return zkClient.exists(zkPath, true);
  }

  /**
   * This method creates an entry for the named snapshot for the specified collection in Zookeeper.
   *
   * @param zkClient Zookeeper client
   * @param collectionName The name of the collection
   * @param meta The {@linkplain CollectionSnapshotMetaData} corresponding to named snapshot
   * @throws KeeperException In case of Zookeeper error
   * @throws InterruptedException In case of thread interruption.
   */
  public static void createCollectionLevelSnapshot(
      SolrZkClient zkClient, String collectionName, CollectionSnapshotMetaData meta)
      throws KeeperException, InterruptedException {
    String zkPath = getSnapshotMetaDataZkPath(collectionName, Optional.of(meta.getName()));
    zkClient.makePath(zkPath, Utils.toJSON(meta), CreateMode.PERSISTENT, true);
  }

  /**
   * This method updates an entry for the named snapshot for the specified collection in Zookeeper.
   *
   * @param zkClient Zookeeper client
   * @param collectionName The name of the collection
   * @param meta The {@linkplain CollectionSnapshotMetaData} corresponding to named snapshot
   * @throws KeeperException In case of Zookeeper error
   * @throws InterruptedException In case of thread interruption.
   */
  public static void updateCollectionLevelSnapshot(
      SolrZkClient zkClient, String collectionName, CollectionSnapshotMetaData meta)
      throws KeeperException, InterruptedException {
    String zkPath = getSnapshotMetaDataZkPath(collectionName, Optional.of(meta.getName()));
    zkClient.setData(zkPath, Utils.toJSON(meta), -1, true);
  }

  /**
   * This method deletes an entry for the named snapshot for the specified collection in Zookeeper.
   *
   * @param zkClient Zookeeper client
   * @param collectionName The name of the collection
   * @param commitName The name of the snapshot
   * @throws InterruptedException In case of thread interruption.
   * @throws KeeperException In case of Zookeeper error
   */
  public static void deleteCollectionLevelSnapshot(
      SolrZkClient zkClient, String collectionName, String commitName)
      throws InterruptedException, KeeperException {
    String zkPath = getSnapshotMetaDataZkPath(collectionName, Optional.of(commitName));
    zkClient.delete(zkPath, -1, true);
  }

  /**
   * This method deletes all snapshots for the specified collection in Zookeeper.
   *
   * @param zkClient Zookeeper client
   * @param collectionName The name of the collection
   * @throws InterruptedException In case of thread interruption.
   * @throws KeeperException In case of Zookeeper error
   */
  public static void cleanupCollectionLevelSnapshots(SolrZkClient zkClient, String collectionName)
      throws InterruptedException, KeeperException {
    String zkPath = getSnapshotMetaDataZkPath(collectionName, Optional.empty());
    try {
      // Delete the meta-data for each snapshot.
      Collection<String> snapshots = zkClient.getChildren(zkPath, null, true);
      for (String snapshot : snapshots) {
        String path = getSnapshotMetaDataZkPath(collectionName, Optional.of(snapshot));
        try {
          zkClient.delete(path, -1, true);
        } catch (KeeperException ex) {
          // Gracefully handle the case when the zk node doesn't exist
          if (ex.code() != KeeperException.Code.NONODE) {
            throw ex;
          }
        }
      }

      // Delete the parent node.
      zkClient.delete(zkPath, -1, true);
    } catch (KeeperException ex) {
      // Gracefully handle the case when the zk node doesn't exist (e.g. if no snapshots were
      // created for this collection).
      if (ex.code() != KeeperException.Code.NONODE) {
        throw ex;
      }
    }
  }

  /**
   * This method returns the {@linkplain CollectionSnapshotMetaData} for the named snapshot for the
   * specified collection in Zookeeper.
   *
   * @param zkClient Zookeeper client
   * @param collectionName The name of the collection
   * @param commitName The name of the snapshot
   * @return (Optional) the {@linkplain CollectionSnapshotMetaData}
   * @throws InterruptedException In case of thread interruption.
   * @throws KeeperException In case of Zookeeper error
   */
  public static Optional<CollectionSnapshotMetaData> getCollectionLevelSnapshot(
      SolrZkClient zkClient, String collectionName, String commitName)
      throws InterruptedException, KeeperException {
    String zkPath = getSnapshotMetaDataZkPath(collectionName, Optional.of(commitName));
    try {
      @SuppressWarnings({"unchecked"})
      Map<String, Object> data =
          (Map<String, Object>) Utils.fromJSON(zkClient.getData(zkPath, null, null, true));
      return Optional.of(new CollectionSnapshotMetaData(data));
    } catch (KeeperException ex) {
      // Gracefully handle the case when the zk node for a specific
      // snapshot doesn't exist (e.g. due to a concurrent delete operation).
      if (ex.code() == KeeperException.Code.NONODE) {
        return Optional.empty();
      }
      throw ex;
    }
  }

  /**
   * This method returns the {@linkplain CollectionSnapshotMetaData} for each named snapshot for the
   * specified collection in Zookeeper.
   *
   * @param zkClient Zookeeper client
   * @param collectionName The name of the collection
   * @return the {@linkplain CollectionSnapshotMetaData} for each named snapshot
   * @throws InterruptedException In case of thread interruption.
   * @throws KeeperException In case of Zookeeper error
   */
  public static Collection<CollectionSnapshotMetaData> listSnapshots(
      SolrZkClient zkClient, String collectionName) throws InterruptedException, KeeperException {
    Collection<CollectionSnapshotMetaData> result = new ArrayList<>();
    String zkPath = getSnapshotMetaDataZkPath(collectionName, Optional.empty());

    try {
      Collection<String> snapshots = zkClient.getChildren(zkPath, null, true);
      for (String snapshot : snapshots) {
        Optional<CollectionSnapshotMetaData> s =
            getCollectionLevelSnapshot(zkClient, collectionName, snapshot);
        if (s.isPresent()) {
          result.add(s.get());
        }
      }
    } catch (KeeperException ex) {
      // Gracefully handle the case when the zk node doesn't exist (e.g. due to a concurrent delete
      // collection operation).
      if (ex.code() != KeeperException.Code.NONODE) {
        throw ex;
      }
    }
    return result;
  }

  /**
   * This method deletes index files of the {@linkplain IndexCommit} for the specified generation
   * number.
   *
   * @param core The Solr core
   * @param dir The index directory storing the snapshot.
   * @param gen The generation number of the {@linkplain IndexCommit} to be deleted.
   * @throws IOException in case of I/O errors.
   */
  public static void deleteSnapshotIndexFiles(SolrCore core, Directory dir, final long gen)
      throws IOException {
    deleteSnapshotIndexFiles(
        core,
        dir,
        new IndexDeletionPolicy() {
          @Override
          public void onInit(List<? extends IndexCommit> commits) throws IOException {
            for (IndexCommit ic : commits) {
              if (gen == ic.getGeneration()) {
                if (log.isInfoEnabled()) {
                  log.info(
                      "Deleting non-snapshotted index commit with generation {}",
                      ic.getGeneration());
                }
                ic.delete();
              }
            }
          }

          @Override
          public void onCommit(List<? extends IndexCommit> commits) throws IOException {}
        });
  }

  /**
   * This method deletes index files not associated with the specified <code>snapshots</code>.
   *
   * @param core The Solr core
   * @param dir The index directory storing the snapshot.
   * @param snapshots The snapshots to be preserved.
   * @throws IOException in case of I/O errors.
   */
  public static void deleteNonSnapshotIndexFiles(
      SolrCore core, Directory dir, Collection<SnapshotMetaData> snapshots) throws IOException {
    final Set<Long> genNumbers = new HashSet<>();
    for (SnapshotMetaData m : snapshots) {
      genNumbers.add(m.getGenerationNumber());
    }

    deleteSnapshotIndexFiles(
        core,
        dir,
        new IndexDeletionPolicy() {
          @Override
          public void onInit(List<? extends IndexCommit> commits) throws IOException {
            for (IndexCommit ic : commits) {
              if (!genNumbers.contains(ic.getGeneration())) {
                if (log.isInfoEnabled()) {
                  log.info(
                      "Deleting non-snapshotted index commit with generation {}",
                      ic.getGeneration());
                }
                ic.delete();
              }
            }
          }

          @Override
          public void onCommit(List<? extends IndexCommit> commits) throws IOException {}
        });
  }

  /**
   * This method deletes index files of the {@linkplain IndexCommit} for the specified generation
   * number.
   *
   * @param core The Solr core
   * @param dir The index directory storing the snapshot.
   * @throws IOException in case of I/O errors.
   */
  @SuppressWarnings({"try", "unused"})
  private static void deleteSnapshotIndexFiles(
      SolrCore core, Directory dir, IndexDeletionPolicy delPolicy) throws IOException {
    IndexWriterConfig conf = core.getSolrConfig().indexConfig.toIndexWriterConfig(core);
    conf.setOpenMode(OpenMode.APPEND);
    conf.setMergePolicy(NoMergePolicy.INSTANCE); // Don't want to merge any commits here!
    conf.setIndexDeletionPolicy(delPolicy);
    conf.setCodec(core.getCodec());
    try (SolrIndexWriter iw = new SolrIndexWriter("SolrSnapshotCleaner", dir, conf)) {
      // Do nothing. The only purpose of opening index writer is to invoke the Lucene
      // IndexDeletionPolicy#onInit method so that we can cleanup the files associated with
      // specified index commit. Note the index writer creates a new commit during the close()
      // operation (which is harmless).
    }
  }

  private static String getSnapshotMetaDataZkPath(
      String collectionName, Optional<String> commitName) {
    if (commitName.isPresent()) {
      return "/snapshots/" + collectionName + "/" + commitName.get();
    }
    return "/snapshots/" + collectionName;
  }
}
