/*
 * ====================================================================
 * Copyright (c) 2004-2012 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core.internal.io.svn;

import com.trilead.ssh2.ServerHostKeyVerifier;
import com.trilead.ssh2.StreamGobbler;
import com.trilead.ssh2.auth.AgentProxy;
import org.tmatesoft.svn.core.SVNAuthenticationException;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.auth.*;
import org.tmatesoft.svn.core.internal.io.svn.ssh.SshAuthenticationException;
import org.tmatesoft.svn.core.internal.io.svn.ssh.SshSession;
import org.tmatesoft.svn.core.internal.io.svn.ssh.SshSessionPool;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;

import java.io.*;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;

/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class SVNSSHConnector implements ISVNConnector {

    private static final String SVNSERVE_COMMAND = "svnserve -t";
    private static final String SVNSERVE_COMMAND_WITH_USER_NAME = "svnserve -t --tunnel-user ";
    
    private static final boolean ourIsUseSessionPing = Boolean.getBoolean("svnkit.ssh2.ping");
    private static SshSessionPool ourSessionPool = new SshSessionPool();
    
    private SshSession mySession;
    private InputStream myInputStream;
    private OutputStream myOutputStream;
    private boolean myIsUseSessionPing;
    
    public SVNSSHConnector() {
        this(true, true);
    }
    
    public SVNSSHConnector(boolean useConnectionPing, boolean useSessionPing) {
        myIsUseSessionPing = useSessionPing;
    }

    public void open(SVNRepositoryImpl repository) throws SVNException {
        ISVNAuthenticationManager authManager = repository.getAuthenticationManager();
        if (authManager == null) {
            SVNErrorManager.authenticationFailed("Authentication required for ''{0}''", repository.getLocation());
            return;
        }

        String realm = repository.getLocation().getProtocol() + "://" + repository.getLocation().getHost();
        if (repository.getLocation().hasPort()) {
            realm += ":" + repository.getLocation().getPort();
        }
        if (repository.getLocation().getUserInfo() != null && !"".equals(repository.getLocation().getUserInfo())) {
            realm = repository.getLocation().getUserInfo() + "@" + realm;
        }

        int reconnect = 1;
        while(true) {
            SVNSSHAuthentication authentication = (SVNSSHAuthentication) authManager.getFirstAuthentication(ISVNAuthenticationManager.SSH, realm, repository.getLocation());
            SshSession connection = null;
            
                while (authentication != null) {
                    try {
                        final ISVNSSHHostVerifier verifier = (ISVNSSHHostVerifier) (authManager instanceof ISVNSSHHostVerifier ? authManager : null);
                        String host = repository.getLocation().getHost();
                        int port = repository.getLocation().hasPort() ? repository.getLocation().getPort() : authentication.getPortNumber();
                        if (port < 0) {
                            port = 22;
                        }
                        String userName = authentication.getUserName();
                        char[] privateKey = authentication.getPrivateKey() != null ? authentication.getPrivateKey() : null;
                        if (privateKey == null && authentication.getPrivateKeyFile() != null) {
                            privateKey = SVNSSHPrivateKeyUtil.readPrivateKey(authentication.getPrivateKeyFile());
                        }
                        char[] passphrase = authentication.getPassphraseValue();
                        if (passphrase != null && passphrase.length == 0) {
                            passphrase = null;
                        }
                        char[] password = authentication.getPasswordValue();
                        if (password != null && password.length == 0) {
                            password = null;
                        }
                        AgentProxy agentProxy = authentication.getAgentProxy();
                        if (privateKey != null && !SVNSSHPrivateKeyUtil.isValidPrivateKey(privateKey, authentication.getPassphraseValue())) {
                            if (password == null) {
                                SVNErrorMessage error = SVNErrorMessage.create(SVNErrorCode.RA_NOT_AUTHORIZED, "File ''{0}'' is not valid OpenSSH DSA or RSA private key file", authentication.getPrivateKeyFile());
                                SVNErrorManager.error(error, SVNLogType.NETWORK);
                            } 
                            privateKey = null;
                        }
                        final int connectTimeout = authManager.getConnectTimeout(repository);
                        final int readTimeout = authManager.getReadTimeout(repository);
                        
                        ServerHostKeyVerifier v = new ServerHostKeyVerifier() {
                            public boolean verifyServerHostKey(String hostname, int port,
                                    String serverHostKeyAlgorithm, byte[] serverHostKey)
                                    throws Exception {
                                if (verifier != null) {
                                    verifier.verifyHostKey(hostname, port, serverHostKeyAlgorithm, serverHostKey);
                                }
                                return true;
                            }
                        };
                        connection = ourSessionPool.openSession(host, port, userName, privateKey, passphrase, password, agentProxy, v, connectTimeout, readTimeout);
                        
                        if (connection == null) {
                            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_SVN_CONNECTION_CLOSED, "Cannot connect to ''{0}''", repository.getLocation().setPath("", false));
                            SVNErrorManager.error(err, SVNLogType.NETWORK);
                        }
                        
                        BasicAuthenticationManager.acknowledgeAuthentication(true, ISVNAuthenticationManager.SSH, realm, null, authentication, repository.getLocation(), authManager);
                        break;
                    } catch (SVNAuthenticationException e) {
                        SVNDebugLog.getDefaultLog().logFine(SVNLogType.NETWORK, e);
                        BasicAuthenticationManager.acknowledgeAuthentication(false, ISVNAuthenticationManager.SSH, realm, e.getErrorMessage(), authentication, repository.getLocation(), authManager);
                        authentication = (SVNSSHAuthentication) authManager.getNextAuthentication(ISVNAuthenticationManager.SSH, realm, repository.getLocation());
                        connection = null;
                    } catch (SshAuthenticationException auth) {
                        SVNDebugLog.getDefaultLog().logFine(SVNLogType.NETWORK, auth);
                        SVNErrorMessage error = SVNErrorMessage.create(SVNErrorCode.RA_NOT_AUTHORIZED, auth.getMessage());
                        BasicAuthenticationManager.acknowledgeAuthentication(false, ISVNAuthenticationManager.SSH, realm, error, authentication, repository.getLocation(), authManager);
                        authentication = (SVNSSHAuthentication) authManager.getNextAuthentication(ISVNAuthenticationManager.SSH, realm, repository.getLocation());
                        connection = null;
                    } catch (IOException e) {
                        connection = null;
                        SVNErrorMessage error = SVNErrorMessage.create(SVNErrorCode.RA_SVN_CONNECTION_CLOSED, e);
                        SVNErrorManager.error(error, SVNLogType.NETWORK);
                    }
                }
                if (authentication == null) {
                    SVNErrorManager.cancel("authentication cancelled", SVNLogType.NETWORK);
                } else if (connection == null) {
                    SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_SVN_CONNECTION_CLOSED, "Can not establish connection to ''{0}''", realm), SVNLogType.NETWORK);
                }
                
                try {
                    mySession = connection;
                    SVNAuthentication author = authManager.getFirstAuthentication(ISVNAuthenticationManager.USERNAME, realm, repository.getLocation());
                    if (author == null) {
                        SVNErrorManager.cancel("authentication cancelled", SVNLogType.NETWORK);
                    }
                    String userName = author.getUserName();
                    if (userName == null || "".equals(userName.trim())) {
                        userName = authentication.getUserName();
                    }
                    if (author.getUserName() == null || author.getUserName().equals(authentication.getUserName()) || 
                            "".equals(author.getUserName())) {
                        repository.setExternalUserName("");
                    } else {
                        repository.setExternalUserName(author.getUserName()); 
                    }
                    author = new SVNUserNameAuthentication(userName, author.isStorageAllowed(), repository.getLocation(), false);
                    BasicAuthenticationManager.acknowledgeAuthentication(true, ISVNAuthenticationManager.USERNAME, realm, null, author, repository.getLocation(), authManager);
    
                    if ("".equals(repository.getExternalUserName())) {
                        mySession.execCommand(SVNSERVE_COMMAND);
                    } else {
                        mySession.execCommand(SVNSERVE_COMMAND_WITH_USER_NAME + "\"" + repository.getExternalUserName() + "\"");
                    }
                    myOutputStream = mySession.getIn();
                    myOutputStream = new BufferedOutputStream(myOutputStream, 16*1024);
                    myInputStream = mySession.getOut();
                    myInputStream = new BufferedInputStream(myInputStream, 16*1024);
                    new StreamGobbler(mySession.getErr());
                    return;
                } catch (SocketTimeoutException e) {
	                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_SVN_IO_ERROR, "timed out waiting for server", null, SVNErrorMessage.TYPE_ERROR, e);
                    SVNErrorManager.error(err, e, SVNLogType.NETWORK);
                } catch (UnknownHostException e) {
	                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_SVN_IO_ERROR, "Unknown host " + e.getMessage(), null, SVNErrorMessage.TYPE_ERROR, e);
                    SVNErrorManager.error(err, e, SVNLogType.NETWORK);
                } catch (ConnectException e) {
	                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_SVN_IO_ERROR, "connection refused by the server", null, SVNErrorMessage.TYPE_ERROR, e);
                    SVNErrorManager.error(err, e, SVNLogType.NETWORK);
                } catch (IOException e) {
                    reconnect--;
                    if (reconnect >= 0) {
                        // try again, but close session first.
                        mySession.close();
                        continue;
                    }
                    repository.getDebugLog().logFine(SVNLogType.NETWORK, e);
                    close(repository);
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_SVN_CONNECTION_CLOSED, "Cannot connect to ''{0}'': {1}", new Object[] {repository.getLocation().setPath("", false), e.getMessage()});
                    SVNErrorManager.error(err, e, SVNLogType.NETWORK);
                }
//            } finally {
//                SVNSSHSession.unlock();
//            }
        }
    }

    public void close(SVNRepositoryImpl repository) throws SVNException {
        SVNFileUtil.closeFile(myOutputStream);
        SVNFileUtil.closeFile(myInputStream);
        if (mySession != null) {
            if (mySession != null) {
                mySession.close();
                mySession = null;
            }
        }
        mySession = null;
        myOutputStream = null;
        myInputStream = null;
    }

    public InputStream getInputStream() throws IOException {
        return myInputStream;
    }

    public OutputStream getOutputStream() throws IOException {
        return myOutputStream;
    }

    public boolean isConnected(SVNRepositoryImpl repos) throws SVNException {
        return mySession != null && !isStale();
    }
    
    public boolean isStale() {
        if (mySession == null) {
            return true;
        }
        if (!ourIsUseSessionPing) {
            return false;
        }
        if (!myIsUseSessionPing) {
            SVNDebugLog.getDefaultLog().logFine(SVNLogType.NETWORK, "SKIPPING CHANNEL PING, IT HAS BEEN DISABLED");
            return false;
        }
        try {
            mySession.ping();
        } catch (IOException e) {
            return true;
        }
        return false;
    }

    public static void shutdown() {
        ourSessionPool.shutdown();
    }

    public void handleExceptionOnOpen(SVNRepositoryImpl repository, SVNException exception) throws SVNException {
        throw exception;
    }
}