package com.cloudhopper.commons.rfs.provider;

/*
 * #%L
 * ch-commons-rfs
 * %%
 * Copyright (C) 2012 - 2013 Cloudhopper by Twitter
 * %%
 * 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.
 * #L%
 */

import com.cloudhopper.commons.rfs.*;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.UserInfo;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * SFTP remote filesystem.
 *
 * The URL configuration for SFTP is fairly simple.  However, the path component
 * is flexible.  To stay in the default directory after logging in, do not include
 * a path component on the URL such as "sftp://user@host".  If a path is included,
 * it will be treated as an absolute path on the remote system, such that the
 * URL "sftp://user@host/" will result in an attempt to change directories to "/"
 * after connecting.
 * 
 * @author joelauer
 */
public class SftpRemoteFileSystem extends BaseRemoteFileSystem {
    private static final Logger logger = LoggerFactory.getLogger(SftpRemoteFileSystem.class);

    private JSch jsch;
    private Session session;
    private ChannelSftp channel;

    public SftpRemoteFileSystem() {
        super();
    }

    /**
     * Best attempt to find a default .ssh directory on this particular server.
     * While more directories may be attempted, for now the user's home directory
     * will be scanned for a .ssh directory.
     * @return An array of .ssh directories to search or null if none were
     *      found.
     */
    protected File[] findSshDirs() {
        ArrayList<File> dirs = new ArrayList<File>();

        // user's home directory and .ssh subdir
        File sshHomeDir = new File(System.getProperty("user.home"), ".ssh");
        if (sshHomeDir.exists() && sshHomeDir.isDirectory()) {
            dirs.add(sshHomeDir);
        }

        // FIXME: any other directories we should try to scan?

        return dirs.toArray(new File[0]);
    }

    /**
     * Best attempt to find all .ssh private keys (identity) by searching inside
     * every provided .ssh directory.  Currently searches for any "id_rsa" or
     * "id_dsa" files.
     */
    protected File[] findSshPrivateKeys(File[] sshDirs) {
        ArrayList<File> files = new ArrayList<File>();

        // search every directory
        for (File sshDir : sshDirs) {
            File f0 = new File(sshDir, "id_dsa");
            if (f0.exists() && f0.canRead() && f0.isFile()) {
                files.add(f0);
            }

            File f1 = new File(sshDir, "id_rsa");
            if (f1.exists() && f1.canRead() && f1.isFile()) {
                files.add(f1);
            }
        }

        return files.toArray(new File[0]);
    }



    @Override
    public void validateURL() throws FileSystemException {
        // a username and host must have been configured
        if (getURL().getUsername() == null) {
            throw new FileSystemException("The SFTP protocol requires a username");
        }
        if (getURL().getHost() == null) {
            throw new FileSystemException("The SFTP protocol requires a host");
        }
    }

    public void connect() throws FileSystemException {
        // make sure we don't connect twice
        if (session != null) {
            throw new FileSystemException("Already connected to SFTP server");
        }

        // validate the url -- required for sftp (user and host)

        // setup a new SFTP session
        jsch = new JSch();

        // attempt to load identities from the operating system
        // find any .ssh directories we'll scan
        File[] sshDirs = findSshDirs();
        for (File sshDir : sshDirs) {
            logger.info("Going to scan directory for .ssh private keys: " + sshDir.getAbsolutePath());
        }

        // find any identities that we'll then load
        File[] sshPrivateKeys = findSshPrivateKeys(sshDirs);
        for (File sshPrivateKeyFile : sshPrivateKeys) {
            logger.info("Attempting to load .ssh private key (identity): " + sshPrivateKeyFile.getAbsolutePath());
            try {
                jsch.addIdentity(sshPrivateKeyFile.getAbsolutePath());
            } catch (JSchException e) {
                logger.warn("Failed to load private key file " + sshPrivateKeyFile + " - going to ignore");
            }
        }

        try {
            session = jsch.getSession(getURL().getUsername(), getURL().getHost(), (getURL().getPort() == null ? 22 : getURL().getPort().intValue()));
        } catch (JSchException e) {
            throw new FileSystemException("Unable to create SSH session: " + e.getMessage(), e);
        }

        // create fully trusted instance -- any hosts will be accepted
        session.setUserInfo(new UserInfo() {
            public String getPassphrase() {
                return null;
            }
            public String getPassword() {
                return null;
            }
            public boolean promptPassphrase(String string) {
                return false;
            }
            public boolean promptPassword(String string) {
                return false;
            }
            // called when a host's authenticity is questioned
            public boolean promptYesNo(String string) {
                //logger.debug("Jsch promptYesNo: " + string);
                return true;
            }
            public void showMessage(String string) {
                //logger.debug("Jsch showMessage: " + string);
            }
        });

        // if the password is set
        if (getURL().getPassword() != null) {
            session.setPassword(getURL().getPassword());
        }

        // don't cause app to hang
        session.setDaemonThread(true);

        try {
            session.connect();
        } catch (JSchException e) {
            session = null;
            throw new FileSystemException("Unable to connect to SSH server: " + e.getMessage(), e);
        }

        logger.info("Connected to remote SSH server " + getURL().getUsername() + "@" + getURL().getHost());

        // create an SFTP channel
        try {
            channel = (ChannelSftp) session.openChannel("sftp");
            channel.connect();
        } catch (JSchException e) {
            // in case the channel failed, always close the parent session first
            try { session.disconnect(); } catch (Exception ex) { }
            session = null;
            throw new FileSystemException("Unable to create SFTP channel on SSH session: " + e.getMessage(), e);
        }

        // based on the URL, make a decision if we should attempt to change dirs
        if (getURL().getPath() != null) {
            logger.info("Changing SFTP directory to: " + getURL().getPath());
            try {
                channel.cd(getURL().getPath());
            } catch (SftpException e) {
                // make sure we disconnect
                try { disconnect(); } catch (Exception ex) { }
                session = null;
                throw new FileSystemException("Unable to change directory on SFTP channel to " + getURL().getPath(), e);
            }
        } else {
            // staying in whatever directory we were assigned by default
            // for information purposeds, let's try to print out that dir
            try {
                String currentDir = channel.pwd();
                logger.info("Current SFTP directory: " + currentDir);
            } catch (SftpException e) {
                // ignore this error
                logger.warn("Unable to get current directory -- safe to ignore");
            }
        }
    }

    public void disconnect() throws FileSystemException {
        // we can't disconnect twice
        if (session == null) {
            throw new FileSystemException("Already disconnected from SFTP server");
        }

        // close channel
        if (channel != null) {
            try {
                channel.disconnect();
            } catch (Exception e) {
                logger.warn("", e);
            }
            channel = null;
        }

        if (session != null) {
            try {
                session.disconnect();
            } catch (Exception e) {
                logger.warn("", e);
            }
            session = null;
        }

        logger.info("Disconnected to remote SSH server " + getURL().getUsername() + "@" + getURL().getHost());
    }

    public boolean exists(String filename) throws FileSystemException {
        // we have to be connected
        if (channel == null) {
            throw new FileSystemException("Not yet connected to SFTP server");
        }

        // easiest way to check if a file already exists is to do a file stat
        // this method will error out if the remote file does not exist!
        try {
            SftpATTRS attrs = channel.stat(filename);
            // if we get here, then file exists
            return true;
        } catch (SftpException e) {
            // map "no file" message to return correct result
            if (e.getMessage().toLowerCase().indexOf("no such file") >= 0) {
                return false;
            }
            // otherwise, this means an underlying error occurred
            throw new FileSystemException("Underlying error with SFTP session while checking if file exists", e);
        }
    }


    public void copy(InputStream in, String filename) throws FileSystemException {
        // does this filename already exist?
        if (exists(filename)) {
            throw new FileSystemException("File " + filename + " already exists on SFTP server");
        }

        // copy the file
        try {
            channel.put(in, filename);
        } catch (SftpException e) {
            throw new FileSystemException("Failed to copy data during PUT with SFTP server", e);
        }
    }
    
}