/*
 * 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.lucene.replicator;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexCommit;
import org.apache.lucene.replicator.ReplicationClient.ReplicationHandler;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.util.InfoStream;

/**
 * A {@link ReplicationHandler} for replication of an index and taxonomy pair. See {@link
 * IndexReplicationHandler} for more detail. This handler ensures that the search and taxonomy
 * indexes are replicated in a consistent way.
 *
 * <p><b>NOTE:</b> if you intend to recreate a taxonomy index, you should make sure to reopen an
 * IndexSearcher and TaxonomyReader pair via the provided callback, to guarantee that both indexes
 * are in sync. This handler does not prevent replicating such index and taxonomy pairs, and if they
 * are reopened by a different thread, unexpected errors can occur, as well as inconsistency between
 * the taxonomy and index readers.
 *
 * @see IndexReplicationHandler
 * @lucene.experimental
 * @deprecated use org.apache.lucene.replicator.nrt instead
 */
@Deprecated
public class IndexAndTaxonomyReplicationHandler implements ReplicationHandler {

  /**
   * The component used to log messages to the {@link InfoStream#getDefault() default} {@link
   * InfoStream}.
   */
  public static final String INFO_STREAM_COMPONENT = "IndexAndTaxonomyReplicationHandler";

  private final Directory indexDir;
  private final Directory taxoDir;
  private final Callable<Boolean> callback;

  private volatile Map<String, List<RevisionFile>> currentRevisionFiles;
  private volatile String currentVersion;
  private volatile InfoStream infoStream = InfoStream.getDefault();

  /**
   * Constructor with the given index directory and callback to notify when the indexes were
   * updated.
   */
  public IndexAndTaxonomyReplicationHandler(
      Directory indexDir, Directory taxoDir, Callable<Boolean> callback) throws IOException {
    this.callback = callback;
    this.indexDir = indexDir;
    this.taxoDir = taxoDir;
    currentRevisionFiles = null;
    currentVersion = null;
    final boolean indexExists = DirectoryReader.indexExists(indexDir);
    final boolean taxoExists = DirectoryReader.indexExists(taxoDir);
    if (indexExists != taxoExists) {
      throw new IllegalStateException(
          "search and taxonomy indexes must either both exist or not: index="
              + indexExists
              + " taxo="
              + taxoExists);
    }
    if (indexExists) { // both indexes exist
      final IndexCommit indexCommit = IndexReplicationHandler.getLastCommit(indexDir);
      final IndexCommit taxoCommit = IndexReplicationHandler.getLastCommit(taxoDir);
      currentRevisionFiles = IndexAndTaxonomyRevision.revisionFiles(indexCommit, taxoCommit);
      currentVersion = IndexAndTaxonomyRevision.revisionVersion(indexCommit, taxoCommit);
      final InfoStream infoStream = InfoStream.getDefault();
      if (infoStream.isEnabled(INFO_STREAM_COMPONENT)) {
        infoStream.message(
            INFO_STREAM_COMPONENT,
            "constructor(): currentVersion="
                + currentVersion
                + " currentRevisionFiles="
                + currentRevisionFiles);
        infoStream.message(
            INFO_STREAM_COMPONENT,
            "constructor(): indexCommit=" + indexCommit + " taxoCommit=" + taxoCommit);
      }
    }
  }

  @Override
  public String currentVersion() {
    return currentVersion;
  }

  @Override
  public Map<String, List<RevisionFile>> currentRevisionFiles() {
    return currentRevisionFiles;
  }

  @Override
  public void revisionReady(
      String version,
      Map<String, List<RevisionFile>> revisionFiles,
      Map<String, List<String>> copiedFiles,
      Map<String, Directory> sourceDirectory)
      throws IOException {
    Directory taxoClientDir = sourceDirectory.get(IndexAndTaxonomyRevision.TAXONOMY_SOURCE);
    Directory indexClientDir = sourceDirectory.get(IndexAndTaxonomyRevision.INDEX_SOURCE);
    List<String> taxoFiles = copiedFiles.get(IndexAndTaxonomyRevision.TAXONOMY_SOURCE);
    List<String> indexFiles = copiedFiles.get(IndexAndTaxonomyRevision.INDEX_SOURCE);
    String taxoSegmentsFile = IndexReplicationHandler.getSegmentsFile(taxoFiles, true);
    String indexSegmentsFile = IndexReplicationHandler.getSegmentsFile(indexFiles, false);
    String taxoPendingFile = taxoSegmentsFile == null ? null : "pending_" + taxoSegmentsFile;
    String indexPendingFile = "pending_" + indexSegmentsFile;

    boolean success = false;
    try {
      // copy taxonomy files before index files
      IndexReplicationHandler.copyFiles(taxoClientDir, taxoDir, taxoFiles);
      IndexReplicationHandler.copyFiles(indexClientDir, indexDir, indexFiles);

      // fsync all copied files (except segmentsFile)
      if (!taxoFiles.isEmpty()) {
        taxoDir.sync(taxoFiles);
      }
      indexDir.sync(indexFiles);

      // now copy, fsync, and rename segmentsFile, taxonomy first because it is ok if a
      // reader sees a more advanced taxonomy than the index.

      if (taxoSegmentsFile != null) {
        taxoDir.copyFrom(taxoClientDir, taxoSegmentsFile, taxoPendingFile, IOContext.READONCE);
      }
      indexDir.copyFrom(indexClientDir, indexSegmentsFile, indexPendingFile, IOContext.READONCE);

      if (taxoSegmentsFile != null) {
        taxoDir.sync(Collections.singletonList(taxoPendingFile));
      }
      indexDir.sync(Collections.singletonList(indexPendingFile));

      if (taxoSegmentsFile != null) {
        taxoDir.rename(taxoPendingFile, taxoSegmentsFile);
        taxoDir.syncMetaData();
      }

      indexDir.rename(indexPendingFile, indexSegmentsFile);
      indexDir.syncMetaData();

      success = true;
    } finally {
      if (!success) {
        if (taxoSegmentsFile != null) {
          taxoFiles.add(taxoSegmentsFile); // add it back so it gets deleted too
          taxoFiles.add(taxoPendingFile);
        }
        IndexReplicationHandler.cleanupFilesOnFailure(taxoDir, taxoFiles);
        indexFiles.add(indexSegmentsFile); // add it back so it gets deleted too
        indexFiles.add(indexPendingFile);
        IndexReplicationHandler.cleanupFilesOnFailure(indexDir, indexFiles);
      }
    }

    // all files have been successfully copied + sync'd. update the handler's state
    currentRevisionFiles = revisionFiles;
    currentVersion = version;

    if (infoStream.isEnabled(INFO_STREAM_COMPONENT)) {
      infoStream.message(
          INFO_STREAM_COMPONENT,
          "revisionReady(): currentVersion="
              + currentVersion
              + " currentRevisionFiles="
              + currentRevisionFiles);
    }

    // Cleanup the index directory from old and unused index files.
    // NOTE: we don't use IndexWriter.deleteUnusedFiles here since it may have
    // side-effects, e.g. if it hits sudden IO errors while opening the index
    // (and can end up deleting the entire index). It is not our job to protect
    // against those errors, app will probably hit them elsewhere.
    IndexReplicationHandler.cleanupOldIndexFiles(indexDir, indexSegmentsFile, infoStream);
    IndexReplicationHandler.cleanupOldIndexFiles(taxoDir, taxoSegmentsFile, infoStream);

    // successfully updated the index, notify the callback that the index is
    // ready.
    if (callback != null) {
      try {
        callback.call();
      } catch (Exception e) {
        throw new IOException(e);
      }
    }
  }

  /** Sets the {@link InfoStream} to use for logging messages. */
  public void setInfoStream(InfoStream infoStream) {
    if (infoStream == null) {
      infoStream = InfoStream.NO_OUTPUT;
    }
    this.infoStream = infoStream;
  }
}
