/**
 * Licensed 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.aurora.scheduler.storage.durability;

import java.io.File;
import java.net.InetSocketAddress;
import java.util.Map;

import com.beust.jcommander.IStringConverter;
import com.beust.jcommander.IStringConverterFactory;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;

import org.apache.aurora.common.util.BuildInfo;
import org.apache.aurora.common.util.Clock;
import org.apache.aurora.scheduler.app.LifecycleModule;
import org.apache.aurora.scheduler.config.converters.DataAmountConverter;
import org.apache.aurora.scheduler.config.converters.InetSocketAddressConverter;
import org.apache.aurora.scheduler.config.converters.TimeAmountConverter;
import org.apache.aurora.scheduler.config.types.DataAmount;
import org.apache.aurora.scheduler.config.types.TimeAmount;
import org.apache.aurora.scheduler.discovery.FlaggedZooKeeperConfig;
import org.apache.aurora.scheduler.discovery.ServiceDiscoveryBindings;
import org.apache.aurora.scheduler.log.mesos.MesosLogStreamModule;
import org.apache.aurora.scheduler.storage.Snapshotter;
import org.apache.aurora.scheduler.storage.backup.BackupReader;
import org.apache.aurora.scheduler.storage.log.LogPersistenceModule;
import org.apache.aurora.scheduler.storage.log.SnapshotterImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A utility to recover the contents of one persistence into another.
 */
public final class RecoveryTool {

  private static final Logger LOG = LoggerFactory.getLogger(RecoveryTool.class);

  private RecoveryTool() {
    // Main-only class.
  }

  interface RecoveryEndpoint {
    Iterable<Object> getOptions();

    Persistence create();
  }

  private static class Log implements RecoveryEndpoint {
    private final FlaggedZooKeeperConfig.Options zkOptions = new FlaggedZooKeeperConfig.Options();
    private final MesosLogStreamModule.Options logOptions = new MesosLogStreamModule.Options();
    private final LogPersistenceModule.Options options = new LogPersistenceModule.Options();

    @Override
    public Iterable<Object> getOptions() {
      return ImmutableList.of(logOptions, options, zkOptions);
    }

    @Override
    public Persistence create() {
      Injector injector = Guice.createInjector(
          new MesosLogStreamModule(logOptions, FlaggedZooKeeperConfig.create(zkOptions)),
          new LogPersistenceModule(options),
          new LifecycleModule(),
          new AbstractModule() {
            @Override
            protected void configure() {
              bind(ServiceDiscoveryBindings.ZOO_KEEPER_CLUSTER_KEY)
                  .toInstance(zkOptions.zkEndpoints);
              bind(Snapshotter.class).to(SnapshotterImpl.class);
              bind(Clock.class).toInstance(Clock.SYSTEM_CLOCK);
              bind(BuildInfo.class).toInstance(new BuildInfo());
            }
          });
      return injector.getInstance(Persistence.class);
    }
  }

  private static class Backup implements RecoveryEndpoint {
    @Parameters(separators = "=")
    private static class Options {
      @Parameter(names = "-backup", description = "Backup file to load")
      File backup;
    }

    private final Options options = new Options();

    @Override
    public Iterable<Object> getOptions() {
      return ImmutableList.of(options);
    }

    @Override
    public Persistence create() {
      return new BackupReader(
          options.backup,
          new SnapshotterImpl(new BuildInfo(), Clock.SYSTEM_CLOCK));
    }
  }

  enum Endpoint {
    LOG(new Log()),
    BACKUP(new Backup());

    private final RecoveryEndpoint impl;

    Endpoint(RecoveryEndpoint impl) {
      this.impl = impl;
    }
  }

  @Parameters(separators = "=")
  private static class Options {
    @Parameter(names = "-from",
        required = true,
        description = "Persistence to read state from")
    Endpoint from;

    @Parameter(names = "-to",
        required = true,
        description = "Persistence to write recovered state into")
    Endpoint to;

    @Parameter(names = "-batch-size",
        description = "Write in batches of this may ops.")
    int batchSize = 50;

    @Parameter(names = "--help", description = "Print usage", help = true)
    boolean help;
  }

  private static JCommander configure(Options options, String... args) {
    JCommander.Builder builder = JCommander.newBuilder().programName(RecoveryTool.class.getName());
    builder.addConverterFactory(new IStringConverterFactory() {
      private Map<Class<?>, Class<? extends IStringConverter<?>>> classConverters =
          ImmutableMap.<Class<?>, Class<? extends IStringConverter<?>>>builder()
              .put(DataAmount.class, DataAmountConverter.class)
              .put(InetSocketAddress.class, InetSocketAddressConverter.class)
              .put(TimeAmount.class, TimeAmountConverter.class)
              .build();

      @SuppressWarnings("unchecked")
      @Override
      public <T> Class<? extends IStringConverter<T>> getConverter(Class<T> forType) {
        return (Class<IStringConverter<T>>) classConverters.get(forType);
      }
    });

    builder.addObject(options);
    for (Endpoint endpoint : Endpoint.values()) {
      endpoint.impl.getOptions().forEach(builder::addObject);
    }

    JCommander parser = builder.build();
    parser.parse(args);
    return parser;
  }

  public static void main(String[] args) {
    Options options = new Options();
    JCommander parser = configure(options, args);
    if (options.help) {
      parser.usage();
      System.exit(1);
    }

    LOG.info("Recovering from " + options.from + " to " + options.to);
    Persistence from = options.from.impl.create();
    Persistence to = options.to.impl.create();

    from.prepare();
    to.prepare();

    Recovery.copy(from, to, options.batchSize);
  }
}