/*
 * 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.jclouds.sshj;

import static com.google.common.base.Objects.equal;
import static com.google.common.base.Preconditions.checkNotNull;

import java.io.IOException;
import java.util.List;

import javax.annotation.Resource;
import javax.inject.Named;

import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.Buffer.BufferException;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
import net.schmizz.sshj.userauth.method.AuthMethod;

import org.jclouds.domain.LoginCredentials;
import org.jclouds.logging.Logger;
import org.jclouds.sshj.SshjSshClient.Connection;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.net.HostAndPort;

import com.jcraft.jsch.agentproxy.AgentProxy;
import com.jcraft.jsch.agentproxy.Connector;
import com.jcraft.jsch.agentproxy.Identity;
import com.jcraft.jsch.agentproxy.sshj.AuthAgent;

public class SSHClientConnection implements Connection<SSHClient> {
   private Optional<Connector> agentConnector;

   public static Builder builder() {
      return new Builder();
   }

   public static class Builder {

      protected HostAndPort hostAndPort;
      protected LoginCredentials loginCredentials;
      protected int connectTimeout;
      protected int sessionTimeout;
      protected Optional<Connector> agentConnector;

      /**
       * @see SSHClientConnection#getHostAndPort()
       */
      public Builder hostAndPort(HostAndPort hostAndPort) {
         this.hostAndPort = hostAndPort;
         return this;
      }

      /**
       * @see SSHClientConnection#getLoginCredentials()
       */
      public Builder loginCredentials(LoginCredentials loginCredentials) {
         this.loginCredentials = loginCredentials;
         return this;
      }

      /**
       * @see SSHClientConnection#getConnectTimeout()
       */
      public Builder connectTimeout(int connectTimeout) {
         this.connectTimeout = connectTimeout;
         return this;
      }

      /**
       * @see SSHClientConnection#getConnectTimeout()
       */
      public Builder sessionTimeout(int sessionTimeout) {
         this.sessionTimeout = sessionTimeout;
         return this;
      }

      /**
       * @see SSHClientConnection#getAgentConnector()
       */
      public Builder agentConnector(Optional<Connector> agentConnector) {
         this.agentConnector = agentConnector;
         return this;
      }

      public SSHClientConnection build() {
         return new SSHClientConnection(hostAndPort, loginCredentials, connectTimeout, sessionTimeout, agentConnector);
      }

      protected Builder fromSSHClientConnection(SSHClientConnection in) {
         return hostAndPort(in.getHostAndPort()).connectTimeout(in.getConnectTimeout()).loginCredentials(
                  in.getLoginCredentials()).sessionTimeout(in.getSessionTimeout()).agentConnector(in.getAgentConnector());
      }
   }

   private SSHClientConnection(HostAndPort hostAndPort, LoginCredentials loginCredentials, int connectTimeout,
            int sessionTimeout, Optional<Connector> agentConnector) {
      this.hostAndPort = checkNotNull(hostAndPort, "hostAndPort");
      this.loginCredentials = checkNotNull(loginCredentials, "loginCredentials for %", hostAndPort);
      this.connectTimeout = connectTimeout;
      this.sessionTimeout = sessionTimeout;
      this.agentConnector = checkNotNull(agentConnector, "agentConnector for %", hostAndPort);
   }
   
   @Resource
   @Named("jclouds.ssh")
   protected Logger logger = Logger.NULL;
   
   private final HostAndPort hostAndPort;
   private final LoginCredentials loginCredentials;
   private final int connectTimeout;
   private final int sessionTimeout;

   @VisibleForTesting
   transient SSHClient ssh;

   @Override
   public void clear() {
      if (ssh != null && ssh.isConnected()) {
         try {
            ssh.disconnect();
         } catch (AssertionError e) {
            // already connected
         } catch (IOException e) {
            logger.debug("<< exception disconnecting from %s: %s", e, e.getMessage());
         }
         ssh = null;
      }
   }

   @Override
   public SSHClient create() throws Exception {
      ssh = new net.schmizz.sshj.SSHClient();
      ssh.addHostKeyVerifier(new PromiscuousVerifier());
      if (connectTimeout != 0) {
         ssh.setConnectTimeout(connectTimeout);
      }
      if (sessionTimeout != 0) {
         ssh.setTimeout(sessionTimeout);
      }
      ssh.connect(hostAndPort.getHost(), hostAndPort.getPortOrDefault(22));
      if (loginCredentials.hasUnencryptedPrivateKey()) {
         OpenSSHKeyFile key = new OpenSSHKeyFile();
         key.init(loginCredentials.getOptionalPrivateKey().get(), null);
         ssh.authPublickey(loginCredentials.getUser(), key);
      } else if (loginCredentials.getOptionalPassword().isPresent()) {
         ssh.authPassword(loginCredentials.getUser(), loginCredentials.getOptionalPassword().get());
      } else if (agentConnector.isPresent()) {
         AgentProxy proxy = new AgentProxy(agentConnector.get());
         ssh.auth(loginCredentials.getUser(), getAuthMethods(proxy));
      }
      return ssh;
   }

   /**
    * @return host and port, where port if not present defaults to {@code 22}
    */
   public HostAndPort getHostAndPort() {
      return hostAndPort;
   }

   /**
    * 
    * @return login used in this ssh
    */
   public LoginCredentials getLoginCredentials() {
      return loginCredentials;
   }

   /**
    * 
    * @return how long to wait for the initial connection to be made
    */
   public int getConnectTimeout() {
      return connectTimeout;
   }

   /**
    * 
    * @return how long to keep the ssh open, or {@code 0} for indefinitely
    */
   public int getSessionTimeout() {
      return sessionTimeout;
   }

   /**
    *
    * @return Ssh agent connector
    */
   public Optional<Connector> getAgentConnector() {
      return agentConnector;
   }

   /**
    * 
    * @return the current ssh or {@code null} if not connected
    */
   public SSHClient getSSHClient() {
      return ssh;
   }

   @Override
   public boolean equals(Object o) {
      if (this == o)
         return true;
      if (o == null || getClass() != o.getClass())
         return false;
      SSHClientConnection that = SSHClientConnection.class.cast(o);
      return equal(this.hostAndPort, that.hostAndPort) && equal(this.loginCredentials, that.loginCredentials)
               && equal(this.ssh, that.ssh);
   }

   @Override
   public int hashCode() {
      return Objects.hashCode(hostAndPort, loginCredentials, ssh);
   }

   @Override
   public String toString() {
      return MoreObjects.toStringHelper("").add("hostAndPort", hostAndPort).add("loginUser", loginCredentials.getUser())
               .add("ssh", ssh != null ? ssh.hashCode() : null).add("connectTimeout", connectTimeout).add(
                        "sessionTimeout", sessionTimeout).toString();
   }

   private static List<AuthMethod> getAuthMethods(AgentProxy agent) throws BufferException  {
      ImmutableList.Builder<AuthMethod> identities = ImmutableList.builder();
      for (Identity identity : agent.getIdentities()) {
         identities.add(new AuthAgent(agent, identity));
      }
      return identities.build();
   }

}
