/*
 *  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
 *
 *      https://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.ivy.plugins.repository.ssh;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import org.apache.ivy.core.settings.TimeoutConstraint;
import org.apache.ivy.plugins.repository.AbstractRepository;
import org.apache.ivy.util.Credentials;
import org.apache.ivy.util.CredentialsUtil;
import org.apache.ivy.util.Message;

import com.jcraft.jsch.ConfigRepository;
import com.jcraft.jsch.ConfigRepository.Config;
import com.jcraft.jsch.OpenSSHConfig;
import com.jcraft.jsch.Session;

public abstract class AbstractSshBasedRepository extends AbstractRepository {

    private File keyFile = null;

    private File passFile = null;

    private String userPassword = null;

    private String keyFilePassword = null;

    private String user = null;

    private String host = null;

    private int port = -1;

    private boolean allowedAgentUse = false;

    private String sshConfig = null;

    public AbstractSshBasedRepository() {
        super();
    }

    public AbstractSshBasedRepository(final TimeoutConstraint timeoutConstraint) {
        super(timeoutConstraint);
    }

    /**
     * hashmap of user/hosts with credentials. key is hostname, value is Credentials
     **/
    private static final Map<String, Credentials> credentialsCache = new HashMap<>();

    private static final int MAX_CREDENTIALS_CACHE_SIZE = 100;

    /**
     * get a new session using the default attributes if the given String is a full uri, use the
     * data from the uri instead
     *
     * @param pathOrUri
     *            might be just a path or a full ssh or sftp uri
     * @return matching Session
     * @throws IOException if something goes wrong
     */
    protected Session getSession(String pathOrUri) throws IOException {
        URI uri = parseURI(pathOrUri);
        String host = getHost();
        int port = getPort();
        String user = getUser();
        String userPassword = getUserPassword();
        String sshConfig = getSshConfig();
        File keyFile = getKeyFile();
        if (uri != null && uri.getScheme() != null) {
            if (uri.getHost() != null) {
                host = uri.getHost();
            }
            if (uri.getPort() != -1) {
                port = uri.getPort();
            }
            if (uri.getUserInfo() != null) {
                String userInfo = uri.getUserInfo();
                if (!userInfo.contains(":")) {
                    user = userInfo;
                } else {
                    user = userInfo.substring(0, userInfo.indexOf(":"));
                    userPassword = userInfo.substring(userInfo.indexOf(":") + 1);
                }
            }
        }

        if (sshConfig != null) {
            ConfigRepository configRepository = OpenSSHConfig.parseFile(sshConfig);
            Config config = configRepository.getConfig(host);
            host = config.getHostname();
            if (user == null) {
                user = config.getUser();
            }
            String keyFilePath = config.getValue("IdentityFile");
            if (keyFilePath != null && keyFile == null) {
                keyFile = new File(keyFilePath);
            }
        }

        if (host == null) {
            throw new IllegalArgumentException(
                    "missing host information. host should be provided either "
                            + "directly on the repository or in the connection URI "
                            + ", or in the openssh config file specified by sshConfig");
        }
        if (user == null) {
            Credentials c = requestCredentials(host);
            if (c != null) {
                user = c.getUserName();
                userPassword = c.getPasswd();
            } else {
                Message.error("username is not set");
            }
        }
        return SshCache.getInstance().getSession(host, port, user, userPassword, keyFile,
            getKeyFilePassword(), getPassFile(), isAllowedAgentUse());
    }

    /**
     * Just check the uri for sanity
     *
     * @param source
     *            String of the uri
     * @return URI object of the String or null
     */
    private URI parseURI(String source) {
        try {
            URI uri = new URI(source);
            if (uri.getScheme() != null
                    && !uri.getScheme().toLowerCase(Locale.US)
                            .equals(getRepositoryScheme().toLowerCase(Locale.US))) {
                throw new URISyntaxException(source, "Wrong scheme in URI. Expected "
                        + getRepositoryScheme() + " as scheme!");
            }
            if (uri.getHost() == null && getHost() == null) {
                throw new URISyntaxException(source, "Missing host in URI or in resolver");
            }
            if (uri.getPath() == null) {
                throw new URISyntaxException(source, "Missing path in URI");
            }
            // if (uri.getUserInfo() == null && getUser() == null) {
            // throw new URISyntaxException(source, "Missing username in URI or in resolver");
            // }
            return uri;
        } catch (URISyntaxException e) {
            Message.error(e.getMessage());
            Message.error("The uri '" + source + "' is in the wrong format.");
            Message.error("Please use " + getRepositoryScheme()
                    + "://user:pass@hostname/path/to/repository");
            return null;
        }
    }

    /**
     * Called, when user was not found in URL. Maintain static hash of credentials and retrieve or
     * ask credentials for host.
     *
     * @param host
     *            host for which we want to get credentials.
     * @return credentials for given host
     **/
    private Credentials requestCredentials(String host) {
        Credentials c = credentialsCache.get(host);
        if (c == null) {
            c = CredentialsUtil.promptCredentials(new Credentials(null, host, user,
                    userPassword), getPassFile());
            if (c != null) {
                if (credentialsCache.size() > MAX_CREDENTIALS_CACHE_SIZE) {
                    credentialsCache.clear();
                }
                credentialsCache.put(host, c);
            }
        }
        return c;
    }

    /**
     * closes the session and remove it from the cache (eg. on case of errors)
     *
     * @param session
     *            key for the cache
     * @param pathOrUri
     *            to release
     */
    protected void releaseSession(Session session, String pathOrUri) {
        session.disconnect();
        SshCache.getInstance().clearSession(session);
    }

    /**
     * set the default user to use for the connection if no user is given or a PEM file is used
     *
     * @param user
     *            to use
     */
    public void setUser(String user) {
        this.user = user;
    }

    /**
     * @return the user to use for the connection if no user is given or a PEM file is used
     */
    public String getUser() {
        return user;
    }

    /**
     * Sets the full file path to use for accessing a PEM key file
     *
     * @param filePath
     *            fully qualified name
     */
    public void setKeyFile(File filePath) {
        this.keyFile = filePath;
        if (!keyFile.exists()) {
            Message.warn("Pemfile " + keyFile.getAbsolutePath() + " doesn't exist.");
            keyFile = null;
        } else if (!keyFile.canRead()) {
            Message.warn("Pemfile " + keyFile.getAbsolutePath() + " not readable.");
            keyFile = null;
        } else {
            Message.debug("Using " + keyFile.getAbsolutePath() + " as keyfile.");
        }
    }

    /**
     * @return the keyFile
     */
    public File getKeyFile() {
        return keyFile;
    }

    /**
     * @param password
     *            password to use for user/password authentication
     */
    public void setUserPassword(String password) {
        this.userPassword = password;
    }

    /**
     * @return the keyFile password for public key based authentication
     */
    public String getKeyFilePassword() {
        return keyFilePassword;
    }

    /**
     * @param keyFilePassword
     *            sets password for public key based authentication
     */
    public void setKeyFilePassword(String keyFilePassword) {
        this.keyFilePassword = keyFilePassword;
    }

    /**
     * @return the user password
     */
    public String getUserPassword() {
        return userPassword;
    }

    /**
     * @return the host
     */
    public String getHost() {
        return host;
    }

    /**
     * @param host
     *            the host to set
     */
    public void setHost(String host) {
        this.host = host;
    }

    /**
     * @return the port
     */
    public int getPort() {
        return port;
    }

    /**
     * @param port
     *            the port to set
     */
    public void setPort(int port) {
        this.port = port;
    }

    /**
     * @param passFile
     *            the passfile to set
     */
    public void setPassFile(File passFile) {
        this.passFile = passFile;
    }

    /**
     * @return the passFile
     */
    public File getPassFile() {
        return passFile;
    }

    /**
     * @return allowedAgentUse Whether use of a local SSH agent for authentication is allowed
     */
    public boolean isAllowedAgentUse() {
        return allowedAgentUse;
    }

    /**
     * @param allowedAgentUse
     *            Whether use of a local SSH agent for authentication is allowed
     */
    public void setAllowedAgentUse(boolean allowedAgentUse) {
        this.allowedAgentUse = allowedAgentUse;
    }

    /**
     * @return sshConfig Path to a local ssh config file
     */
    public String getSshConfig() {
        return sshConfig;
    }

    /**
     * @param sshConfig
     *            Path to a local ssh config file
     */
    public void setSshConfig(String sshConfig) {
        this.sshConfig = sshConfig;
    }

    protected abstract String getRepositoryScheme();

}
