/*
 * 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.apache.sshd.server.subsystem.sftp;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import org.apache.sshd.common.config.SshConfigFileReader;
import org.apache.sshd.common.io.IoServiceFactory;
import org.apache.sshd.common.io.mina.MinaServiceFactory;
import org.apache.sshd.common.io.nio2.Nio2ServiceFactory;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.logging.AbstractLoggingBean;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.apache.sshd.common.util.threads.ThreadUtils;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.CommandFactory;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
import org.apache.sshd.server.SessionAware;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.auth.password.AcceptAllPasswordAuthenticator;
import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.scp.ScpCommandFactory;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.shell.InteractiveProcessShellFactory;
import org.apache.sshd.util.test.Utils;

/**
 * A basic implementation to allow remote mounting of the local file system via SFTP
 *
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public final class SshFsMounter {
    public static class MounterCommand extends AbstractLoggingBean implements Command, SessionAware, Runnable {
        private final String command;
        private final String cmdName;
        private final List<String> args;
        private String username;
        private InputStream stdin;
        private PrintStream stdout;
        private PrintStream stderr;
        private ExitCallback callback;
        private ExecutorService executor;
        private Future<?> future;

        public MounterCommand(String command) {
            this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "No command");

            String[] comps = GenericUtils.split(this.command, ' ');
            int numComps = GenericUtils.length(comps);
            cmdName = GenericUtils.trimToEmpty(ValidateUtils.checkNotNullAndNotEmpty(comps[0], "No command name"));
            if (numComps > 1) {
                args = new ArrayList<>(numComps - 1);
                for (int index = 1; index < numComps; index++) {
                    String c = GenericUtils.trimToEmpty(comps[index]);
                    if (GenericUtils.isEmpty(c)) {
                        continue;
                    }

                    args.add(c);
                }
            } else {
                args = Collections.emptyList();
            }

            log.info("<init>(" + command + ")");
        }

        @Override
        public void run() {
            try {
                log.info("run(" + username + ")[" + command + "] start");
                if ("id".equals(cmdName)) {
                    int numArgs = GenericUtils.size(args);
                    if (numArgs <= 0) {
                        stdout.println("uid=0(root) gid=0(root) groups=0(root)");
                    } else if (numArgs == 1) {
                        String modifier = args.get(0);
                        if ("-u".equals(modifier) || "-G".equals(modifier)) {
                            stdout.println("0");
                        } else {
                            throw new IllegalArgumentException("Unknown modifier: " + modifier);
                        }
                    } else {
                        throw new IllegalArgumentException("Unexpected extra command arguments");
                    }
                } else {
                    throw new UnsupportedOperationException("Unknown command");
                }

                log.info("run(" + username + ")[" + command + "] end");
                callback.onExit(0);
            } catch (Exception e) {
                log.error("run(" + username + ")[" + command + "] " + e.getClass().getSimpleName() + ": " + e.getMessage(), e);
                stderr.append(e.getClass().getSimpleName()).append(": ").println(e.getMessage());
                callback.onExit(-1, e.toString());
            }
        }

        @Override
        public void setSession(ServerSession session) {
            username = session.getUsername();
        }

        @Override
        public void setInputStream(InputStream in) {
            this.stdin = in;
        }

        @Override
        public void setOutputStream(OutputStream out) {
            this.stdout = new PrintStream(out, true);
        }

        @Override
        public void setErrorStream(OutputStream err) {
            this.stderr = new PrintStream(err, true);
        }

        @Override
        public void setExitCallback(ExitCallback callback) {
            this.callback = callback;
        }

        @Override
        public void start(Environment env) throws IOException {
            executor = ThreadUtils.newSingleThreadExecutor(getClass().getSimpleName());
            future = executor.submit(this);
        }

        @Override
        public void destroy() {
            stopCommand();

            if (stdout != null) {
                try {
                    log.info("destroy(" + username + ")[" + command + "] close stdout");
                    stdout.close();
                    log.info("destroy(" + username + ")[" + command + "] stdout closed");
                } finally {
                    stdout = null;
                }
            }

            if (stderr != null) {
                try {
                    log.info("destroy(" + username + ")[" + command + "] close stderr");
                    stderr.close();
                    log.info("destroy(" + username + ")[" + command + "] stderr closed");
                } finally {
                    stderr = null;
                }
            }

            if (stdin != null) {
                try {
                    log.info("destroy(" + username + ")[" + command + "] close stdin");
                    stdin.close();
                    log.info("destroy(" + username + ")[" + command + "] stdin closed");
                } catch (IOException e) {
                    log.warn("destroy(" + username + ")[" + command + "] failed (" + e.getClass().getSimpleName() + ") to close stdin: " + e.getMessage());
                    if (log.isDebugEnabled()) {
                        log.debug("destroy(" + username + ")[" + command + "] failure details", e);
                    }
                } finally {
                    stdin = null;
                }
            }
        }

        private void stopCommand() {
            if ((future != null) && (!future.isDone())) {
                try {
                    log.info("stopCommand(" + username + ")[" + command + "] cancelling");
                    future.cancel(true);
                    log.info("stopCommand(" + username + ")[" + command + "] cancelled");
                } finally {
                    future = null;
                }
            }

            if ((executor != null) && (!executor.isShutdown())) {
                try {
                    log.info("stopCommand(" + username + ")[" + command + "] shutdown executor");
                    executor.shutdownNow();
                    log.info("stopCommand(" + username + ")[" + command + "] executor shut down");
                } finally {
                    executor = null;
                }
            }
        }
    }

    public static class MounterCommandFactory implements CommandFactory {
        public static final MounterCommandFactory INSTANCE = new MounterCommandFactory();

        public MounterCommandFactory() {
            super();
        }

        @Override
        public Command createCommand(String command) {
            return new MounterCommand(command);
        }
    }

    private SshFsMounter() {
        throw new UnsupportedOperationException("No instance");
    }

    //////////////////////////////////////////////////////////////////////////

    public static void main(String[] args) throws Exception {
        int port = SshConfigFileReader.DEFAULT_PORT;
        boolean error = false;
        Map<String, String> options = new LinkedHashMap<>();

        int numArgs = GenericUtils.length(args);
        for (int i = 0; i < numArgs; i++) {
            String argName = args[i];
            if ("-p".equals(argName)) {
                if (i + 1 >= numArgs) {
                    System.err.println("option requires an argument: " + argName);
                    break;
                }
                port = Integer.parseInt(args[++i]);
            } else if ("-io".equals(argName)) {
                if (i + 1 >= numArgs) {
                    System.err.println("option requires an argument: " + argName);
                    break;
                }

                String provider = args[++i];
                if ("mina".equals(provider)) {
                    System.setProperty(IoServiceFactory.class.getName(), MinaServiceFactory.class.getName());
                } else if ("nio2".endsWith(provider)) {
                    System.setProperty(IoServiceFactory.class.getName(), Nio2ServiceFactory.class.getName());
                } else {
                    System.err.println("provider should be mina or nio2: " + argName);
                    error = true;
                    break;
                }
            } else if ("-o".equals(argName)) {
                if (i + 1 >= numArgs) {
                    System.err.println("option requires and argument: " + argName);
                    error = true;
                    break;
                }
                String opt = args[++i];
                int idx = opt.indexOf('=');
                if (idx <= 0) {
                    System.err.println("bad syntax for option: " + opt);
                    error = true;
                    break;
                }
                options.put(opt.substring(0, idx), opt.substring(idx + 1));
            } else if (argName.startsWith("-")) {
                System.err.println("illegal option: " + argName);
                error = true;
                break;
            } else {
                System.err.println("extra argument: " + argName);
                error = true;
                break;
            }
        }
        if (error) {
            System.err.println("usage: sshfs [-p port] [-io mina|nio2] [-o option=value]");
            System.exit(-1);
        }

        System.err.println("Starting SSHD on port " + port);

        SshServer sshd = Utils.setupTestServer(SshFsMounter.class);
        Map<String, Object> props = sshd.getProperties();
//        FactoryManagerUtils.updateProperty(props, ServerFactoryManager.WELCOME_BANNER, "Welcome to SSH-FS Mounter\n");
        props.putAll(options);
        sshd.setPort(port);

        File targetFolder = Objects.requireNonNull(Utils.detectTargetFolder(MounterCommandFactory.class), "Failed to detect target folder");
        if (SecurityUtils.isBouncyCastleRegistered()) {
            sshd.setKeyPairProvider(SecurityUtils.createGeneratorHostKeyProvider(new File(targetFolder, "key.pem").toPath()));
        } else {
            sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File(targetFolder, "key.ser")));
        }

        sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE);
        sshd.setPasswordAuthenticator(AcceptAllPasswordAuthenticator.INSTANCE);
        sshd.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE);
        sshd.setCommandFactory(new ScpCommandFactory.Builder().withDelegate(MounterCommandFactory.INSTANCE).build());
        sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
        sshd.start();

        Thread.sleep(Long.MAX_VALUE);
    }
}
