/*
 *  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.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

import org.apache.ivy.core.settings.TimeoutConstraint;
import org.apache.ivy.plugins.repository.Resource;
import org.apache.ivy.util.Message;

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

/**
 * Ivy Repository based on SSH
 */
public class SshRepository extends AbstractSshBasedRepository {

    private static final int BUFFER_SIZE = 64 * 1024;

    private static final String ARGUMENT_PLACEHOLDER = "%arg";

    private static final int POLL_SLEEP_TIME = 500;

    private char fileSeparator = '/';

    private String listCommand = "ls -1";

    private String existCommand = "ls";

    private String createDirCommand = "mkdir";

    private String publishPermissions = null;

    public SshRepository() {

    }

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

    /**
     * create a new resource with lazy initializing
     *
     * @param source String
     * @return Resource
     */
    public Resource getResource(String source) {
        Message.debug("SShRepository:getResource called: " + source);
        return new SshResource(this, source);
    }

    /**
     * Fetch the needed file information for a given file (size, last modification time) and report
     * it back in a SshResource
     *
     * @param source
     *            ssh uri for the file to get info for
     * @return SshResource filled with the needed information
     * @see org.apache.ivy.plugins.repository.Repository#getResource(java.lang.String)
     */
    public SshResource resolveResource(String source) {
        Message.debug("SShRepository:resolveResource called: " + source);
        SshResource result = null;
        Session session = null;
        try {
            session = getSession(source);
            Scp myCopy = new Scp(session);
            Scp.FileInfo fileInfo = myCopy.getFileinfo(new URI(source).getPath());
            result = new SshResource(this, source, true, fileInfo.getLength(),
                    fileInfo.getLastModified());
        } catch (IOException | URISyntaxException e) {
            if (session != null) {
                releaseSession(session, source);
            }
            result = new SshResource();
        } catch (RemoteScpException e) {
            result = new SshResource();
        }
        Message.debug("SShRepository:resolveResource end.");
        return result;
    }

    /**
     * Reads out the output of a ssh session exec
     *
     * @param channel
     *            Channel to read from
     * @param strStdout
     *            StringBuilder that receives Session Stdout output
     * @param strStderr
     *            StringBuilder that receives Session Stderr output
     * @throws IOException
     *             in case of trouble with the network
     */
    private void readSessionOutput(ChannelExec channel, StringBuilder strStdout,
            StringBuilder strStderr) throws IOException {
        InputStream stdout = channel.getInputStream();
        InputStream stderr = channel.getErrStream();

        try {
            channel.connect();
        } catch (JSchException jsche) {
            throw new IOException("Channel connection problems", jsche);
        }

        byte[] buffer = new byte[BUFFER_SIZE];
        while (true) {
            int avail = 0;
            while ((avail = stdout.available()) > 0) {
                int len = stdout.read(buffer, 0, (avail > BUFFER_SIZE - 1 ? BUFFER_SIZE : avail));
                strStdout.append(new String(buffer, 0, len));
            }
            while ((avail = stderr.available()) > 0) {
                int len = stderr.read(buffer, 0, (avail > BUFFER_SIZE - 1 ? BUFFER_SIZE : avail));
                strStderr.append(new String(buffer, 0, len));
            }
            if (channel.isClosed()) {
                break;
            }
            try {
                Thread.sleep(POLL_SLEEP_TIME);
            } catch (Exception ee) {
                // ignored
            }
        }
        int avail = 0;
        while ((avail = stdout.available()) > 0) {
            int len = stdout.read(buffer, 0, (avail > BUFFER_SIZE - 1 ? BUFFER_SIZE : avail));
            strStdout.append(new String(buffer, 0, len));
        }
        while ((avail = stderr.available()) > 0) {
            int len = stderr.read(buffer, 0, (avail > BUFFER_SIZE - 1 ? BUFFER_SIZE : avail));
            strStderr.append(new String(buffer, 0, len));
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.apache.ivy.repository.Repository#list(java.lang.String)
     */
    public List<String> list(String parent) throws IOException {
        Message.debug("SShRepository:list called: " + parent);
        List<String> result = new ArrayList<>();
        Session session = null;
        ChannelExec channel = null;
        session = getSession(parent);
        channel = getExecChannel(session);
        URI parentUri = null;
        try {
            parentUri = new URI(parent);
        } catch (URISyntaxException e) {
            throw new IOException("The uri '" + parent + "' is not valid!", e);
        }
        String fullCmd = replaceArgument(listCommand, parentUri.getPath());
        channel.setCommand(fullCmd);
        StringBuilder stdOut = new StringBuilder();
        StringBuilder stdErr = new StringBuilder();
        readSessionOutput(channel, stdOut, stdErr);
        if (channel.getExitStatus() != 0) {
            Message.error("Ssh ListCommand exited with status != 0");
            Message.error(stdErr.toString());
            return null;
        } else {
            BufferedReader br = new BufferedReader(new StringReader(stdOut.toString()));
            String line = null;
            while ((line = br.readLine()) != null) {
                result.add(line);
            }
        }
        return result;
    }

    /**
     * @param session Session
     * @return ChannelExec
     */
    private ChannelExec getExecChannel(Session session) throws IOException {
        ChannelExec channel;
        try {
            channel = (ChannelExec) session.openChannel("exec");
        } catch (JSchException e) {
            throw new IOException();
        }
        return channel;
    }

    /**
     * Replace the argument placeholder with argument or append the argument if no placeholder is
     * present
     *
     * @param command
     *            with argument placeholder or not
     * @param argument ditto
     * @return replaced full command
     */
    private String replaceArgument(String command, String argument) {
        String fullCmd;
        if (!command.contains(ARGUMENT_PLACEHOLDER)) {
            fullCmd = command + " " + argument;
        } else {
            fullCmd = command.replaceAll(ARGUMENT_PLACEHOLDER, argument);
        }
        return fullCmd;
    }

    /*
     * (non-Javadoc)
     *
     * @see org.apache.ivy.repository.Repository#put(java.io.File, java.lang.String, boolean)
     */
    public void put(File source, String destination, boolean overwrite) throws IOException {
        Message.debug("SShRepository:put called: " + destination);
        Session session = getSession(destination);

        URI destinationUri = null;
        try {
            destinationUri = new URI(destination);
        } catch (URISyntaxException e) {
            throw new IOException("The uri '" + destination + "' is not valid!", e);
        }

        try {
            String filePath = destinationUri.getPath();
            int lastSep = filePath.lastIndexOf(fileSeparator);
            String path;
            String name;
            if (lastSep == -1) {
                name = filePath;
                path = null;
            } else {
                name = filePath.substring(lastSep + 1);
                path = filePath.substring(0, lastSep);
            }
            if (!overwrite) {
                if (checkExistence(filePath, session)) {
                    throw new IOException("destination file exists and overwrite == false");
                }
            }
            if (path != null) {
                makePath(path, session);
            }
            Scp myCopy = new Scp(session);
            myCopy.put(source.getCanonicalPath(), path, name, publishPermissions);
        } catch (IOException e) {
            if (session != null) {
                releaseSession(session, destination);
            }
            throw e;
        } catch (RemoteScpException e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * Tries to create a directory path on the target system
     *
     * @param path
     *            to create
     * @param session
     *            to use
     */
    private void makePath(String path, Session session) throws IOException {
        ChannelExec channel = null;
        String trimmed = path;
        try {
            while (trimmed.length() > 0 && trimmed.charAt(trimmed.length() - 1) == fileSeparator) {
                trimmed = trimmed.substring(0, trimmed.length() - 1);
            }
            if (trimmed.length() == 0 || checkExistence(trimmed, session)) {
                return;
            }
            int nextSlash = trimmed.lastIndexOf(fileSeparator);
            if (nextSlash > 0) {
                String parent = trimmed.substring(0, nextSlash);
                makePath(parent, session);
            }
            channel = getExecChannel(session);
            String mkdir = replaceArgument(createDirCommand, trimmed);
            Message.debug("SShRepository: trying to create path: " + mkdir);
            channel.setCommand(mkdir);
            StringBuilder stdOut = new StringBuilder();
            StringBuilder stdErr = new StringBuilder();
            readSessionOutput(channel, stdOut, stdErr);
        } finally {
            if (channel != null) {
                channel.disconnect();
            }
        }
    }

    /**
     * check for existence of file or dir on target system
     *
     * @param filePath
     *            to the object to check
     * @param session
     *            to use
     * @return true: object exists, false otherwise
     */
    private boolean checkExistence(String filePath, Session session) throws IOException {
        Message.debug("SShRepository: checkExistence called: " + filePath);
        ChannelExec channel = null;
        channel = getExecChannel(session);
        String fullCmd = replaceArgument(existCommand, filePath);
        channel.setCommand(fullCmd);
        StringBuilder stdOut = new StringBuilder();
        StringBuilder stdErr = new StringBuilder();
        readSessionOutput(channel, stdOut, stdErr);
        return channel.getExitStatus() == 0;
    }

    /*
     * (non-Javadoc)
     *
     * @see org.apache.ivy.repository.Repository#get(java.lang.String, java.io.File)
     */
    public void get(String source, File destination) throws IOException {
        Message.debug("SShRepository:get called: " + source + " to "
                + destination.getCanonicalPath());
        if (destination.getParentFile() != null) {
            destination.getParentFile().mkdirs();
        }
        Session session = getSession(source);

        URI sourceUri = null;
        try {
            sourceUri = new URI(source);
        } catch (URISyntaxException e) {
            throw new IOException("The uri '" + source + "' is not valid!", e);
        }

        try {
            Scp myCopy = new Scp(session);
            myCopy.get(sourceUri.getPath(), destination.getCanonicalPath());
        } catch (IOException e) {
            if (session != null) {
                releaseSession(session, source);
            }
            throw e;
        } catch (RemoteScpException e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * sets the list command to use for a directory listing listing must be only the filename and
     * each filename on a separate line
     *
     * @param cmd
     *            to use. default is "ls -1"
     */
    public void setListCommand(String cmd) {
        this.listCommand = cmd.trim();
    }

    /**
     * @return the list command to use
     */
    public String getListCommand() {
        return listCommand;
    }

    /**
     * @return the createDirCommand
     */
    public String getCreateDirCommand() {
        return createDirCommand;
    }

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

    /**
     * @return the existCommand
     */
    public String getExistCommand() {
        return existCommand;
    }

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

    /**
     * The file separator is the separator to use on the target system On a unix system it is '/',
     * but I don't know, how this is solved on different ssh implementations. Using the default
     * might be fine
     *
     * @param fileSeparator
     *            The fileSeparator to use. default '/'
     */
    public void setFileSeparator(char fileSeparator) {
        this.fileSeparator = fileSeparator;
    }

    /**
     * A four digit string (e.g., 0644, see "man chmod", "man open") specifying the permissions of
     * the published files.
     *
     * @param permissions String
     */
    public void setPublishPermissions(String permissions) {
        this.publishPermissions = permissions;
    }

    /**
     * return ssh as scheme use the Resolver type name here? would be nice if it would be static, so
     * we could use SshResolver.getTypeName()
     *
     * @return String
     */
    protected String getRepositoryScheme() {
        return "ssh";
    }

    /**
     * Not really streaming...need to implement a proper streaming approach?
     *
     * @param resource
     *            to stream
     * @return InputStream of the resource data
     * @throws IOException if something goes wrong
     */
    public InputStream openStream(SshResource resource) throws IOException {
        Session session = getSession(resource.getName());
        Scp scp = new Scp(session);
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        try {
            scp.get(resource.getName(), os);
        } catch (IOException e) {
            if (session != null) {
                releaseSession(session, resource.getName());
            }
            throw e;
        } catch (RemoteScpException e) {
            throw new IOException(e.getMessage());
        }
        return new ByteArrayInputStream(os.toByteArray());
    }
}
