/*
 * 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.hive.ptest.execution;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.apache.hive.ptest.execution.LocalCommand.CollectLogPolicy;
import org.apache.hive.ptest.execution.ssh.NonZeroExitCodeException;
import org.apache.hive.ptest.execution.ssh.RemoteCommandResult;
import org.apache.hive.ptest.execution.ssh.SSHExecutionException;
import org.apache.hive.ptest.execution.ssh.SSHResult;
import org.slf4j.Logger;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;

public abstract class Phase {

  protected final List<HostExecutor> hostExecutors;
  private final LocalCommandFactory localCommandFactory;
  private final ImmutableMap<String, String> templateDefaults;
  protected final Logger logger;

  public Phase(List<HostExecutor> hostExecutors,
      LocalCommandFactory localCommandFactory,
      ImmutableMap<String, String> templateDefaults, Logger logger) {
    super();
    this.hostExecutors = hostExecutors;
    this.localCommandFactory = localCommandFactory;
    this.templateDefaults = templateDefaults;
    this.logger = logger;
  }

  public abstract void execute() throws Throwable;

  // clean prep
  protected void execLocally(String command)
      throws IOException, InterruptedException, NonZeroExitCodeException {
    CollectLogPolicy localCollector = new CollectLogPolicy(logger);
    command = Templates.getTemplateResult(command, templateDefaults);
    LocalCommand localCmd = localCommandFactory.create(localCollector, command);
    if(localCmd.getExitCode() != 0) {
      throw new NonZeroExitCodeException(String.format(
          "Command '%s' failed with exit status %d and output '%s'",
          command, localCmd.getExitCode(), localCollector.getOutput()));
    }
  }
  // prep
  protected List<RemoteCommandResult> rsyncFromLocalToRemoteInstances(String localFile, String remoteFile)
      throws Exception {
    List<ListenableFuture<List<ListenableFuture<RemoteCommandResult>>>> futures = Lists.newArrayList();
    for(HostExecutor hostExecutor : hostExecutors) {
      futures.add(hostExecutor.rsyncFromLocalToRemoteInstances(localFile, remoteFile));
    }
    return flatten(futures);
  }

  // clean
  protected List<SSHResult> execHosts(String command)
      throws Exception {
    List<ListenableFuture<SSHResult>> futures = Lists.newArrayList();
    for(HostExecutor hostExecutor : hostExecutors) {
      futures.add(hostExecutor.exec(command));
    }
    return toListOfResults(futures);
  }
  protected List<SSHResult> execHostsIgnoreErrors(String command)
      throws Exception {
    List<ListenableFuture<SSHResult>> futures = Lists.newArrayList();
    for(HostExecutor hostExecutor : hostExecutors) {
      futures.add(hostExecutor.execIgnoreAllErrors(command));
    }
    return toListOfResults(futures, false);
  }
  // clean prep
  protected List<RemoteCommandResult> execInstances(String command)
      throws Exception {
    List<ListenableFuture<RemoteCommandResult>> futures = Lists.newArrayList();
    for(HostExecutor hostExecutor : hostExecutors) {
      futures.addAll(hostExecutor.execInstances(command));
    }
    return toListOfResults(futures);
  }
  protected List<RemoteCommandResult> initalizeHosts()
      throws Exception {
    List<ListenableFuture<List<RemoteCommandResult>>> futures = Lists.newArrayList();
    ListeningExecutorService executor = MoreExecutors.
        listeningDecorator(Executors.newFixedThreadPool(hostExecutors.size()));
    try {
      for(final HostExecutor hostExecutor : hostExecutors) {
        futures.add(executor.submit(new Callable<List<RemoteCommandResult>>() {
          @Override
          public List<RemoteCommandResult> call() throws Exception {
            return initalizeHost(hostExecutor);
          }
        }));
      }
      List<RemoteCommandResult> results = Lists.newArrayList();
      for(ListenableFuture<List<RemoteCommandResult>> future : futures) {
        List<RemoteCommandResult> result = future.get();
        if(result != null) {
          results.addAll(result);
        }
      }
      executor.shutdown();
      return results;
    } finally {
      if(executor.isShutdown()) {
        executor.shutdownNow();
      }
    }
  }
  protected List<RemoteCommandResult> initalizeHost(HostExecutor hostExecutor)
      throws Exception {
    List<RemoteCommandResult> results = Lists.newArrayList();
    results.add(hostExecutor.exec("killall -q -9 java || true").get());
    TimeUnit.SECONDS.sleep(1);
    // order matters in all of these so block
    results.addAll(toListOfResults(hostExecutor.execInstances("rm -rf $localDir/$instanceName/scratch $localDir/$instanceName/logs")));
    results.addAll(toListOfResults(hostExecutor.execInstances("mkdir -p $localDir/$instanceName/logs " +
        "$localDir/$instanceName/maven " +
        "$localDir/$instanceName/scratch " +
        "$localDir/$instanceName/ivy " +
        "$localDir/$instanceName/${repositoryName}-source")));
    // order does not matter below, so go wide
    List<ListenableFuture<List<ListenableFuture<RemoteCommandResult>>>> futures = Lists.newArrayList();
    futures.add(hostExecutor.rsyncFromLocalToRemoteInstances("$workingDir/${repositoryName}-source", "$localDir/$instanceName/"));
    futures.add(hostExecutor.rsyncFromLocalToRemoteInstances("$workingDir/maven", "$localDir/$instanceName/"));
    futures.add(hostExecutor.rsyncFromLocalToRemoteInstances("$workingDir/ivy", "$localDir/$instanceName/"));
    results.addAll(flatten(futures));
    return results;
  }
  private <T extends RemoteCommandResult> List<T> flatten(List<ListenableFuture<List<ListenableFuture<T>>>> futures)
      throws Exception {
    List<T> results = Lists.newArrayList();
    for(ListenableFuture<List<ListenableFuture<T>>> future : futures) {
      List<ListenableFuture<T>> result = future.get();
      if(result != null) {
        results.addAll(toListOfResults(result));
      }
    }
    return results;
  }
  private <T extends RemoteCommandResult> List<T> toListOfResults(List<ListenableFuture<T>> futures)
      throws Exception {
    return toListOfResults(futures, true);
  }
  private <T extends RemoteCommandResult> List<T> toListOfResults(List<ListenableFuture<T>> futures,
      boolean reportErrors)
      throws Exception {
    List<T> results = Lists.newArrayList();
    for(T result : Futures.allAsList(futures).get()) {
      if(result != null) {
        if(reportErrors && (result.getException() != null || result.getExitCode() != 0)) {
          throw new SSHExecutionException(result);
        }
        results.add(result);
      }
    }
    return results;
  }
  protected ImmutableMap<String, String> getTemplateDefaults() {
    return templateDefaults;
  }
}
