/*
 * 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.agent.unix;

import java.io.Closeable;
import java.io.IOException;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import org.apache.sshd.agent.SshAgent;
import org.apache.sshd.agent.common.AbstractAgentClient;
import org.apache.sshd.agent.local.AgentImpl;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
import org.apache.sshd.common.util.logging.AbstractLoggingBean;
import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
import org.apache.sshd.common.util.threads.ThreadUtils;
import org.apache.tomcat.jni.Local;
import org.apache.tomcat.jni.Pool;
import org.apache.tomcat.jni.Socket;
import org.apache.tomcat.jni.Status;


/**
 * A server for an SSH Agent
 */
public class AgentServer extends AbstractLoggingBean implements Closeable, ExecutorServiceCarrier {

    private final SshAgent agent;
    private final ExecutorService service;
    private final boolean shutdownExecutor;
    private Future<?> agentThread;
    private String authSocket;
    private long pool;
    private long handle;

    public AgentServer() {
        this(null, false);
    }

    public AgentServer(ExecutorService executor, boolean shutdownOnExit) {
        this(new AgentImpl(), executor, shutdownOnExit);
    }

    public AgentServer(SshAgent agent, ExecutorService executor, boolean shutdownOnExit) {
        this.agent = agent;
        this.service = (executor == null) ? ThreadUtils.newSingleThreadExecutor("AgentServer[" + agent + "]") : executor;
        this.shutdownExecutor = service != executor || shutdownOnExit;
    }

    public SshAgent getAgent() {
        return agent;
    }

    @Override
    public ExecutorService getExecutorService() {
        return service;
    }

    @Override
    public boolean isShutdownOnExit() {
        return shutdownExecutor;
    }

    public String start() throws Exception {
        authSocket = AprLibrary.createLocalSocketAddress();
        pool = Pool.create(AprLibrary.getInstance().getRootPool());
        handle = Local.create(authSocket, pool);
        int result = Local.bind(handle, 0);
        if (result != Status.APR_SUCCESS) {
            throwException(result);
        }
        AprLibrary.secureLocalSocket(authSocket, handle);
        result = Local.listen(handle, 0);
        if (result != Status.APR_SUCCESS) {
            throwException(result);
        }

        ExecutorService executor = getExecutorService();
        agentThread = executor.submit(() -> {
            try {
                while (true) {
                    long clientSock = Local.accept(handle);
                    Socket.timeoutSet(clientSock, 10000000);    // TODO make this configurable
                    new SshAgentSession(clientSock, agent).run();
                }
            } catch (Exception e) {
                log.error("Failed to run session", e);
            }
        });
        return authSocket;
    }

    @Override
    public void close() throws IOException {
        IOException err = null;
        try {
            agent.close();
        } catch (IOException e) {
            err = e;
        }

        Socket.close(handle);

        try {
            if ((agentThread != null) && (!agentThread.isDone())) {
                agentThread.cancel(true);
            }
        } finally {
            agentThread = null;
        }

        ExecutorService executor = getExecutorService();
        if ((executor != null) && isShutdownOnExit() && (!executor.isShutdown())) {
            Collection<?> runners = executor.shutdownNow();
            if (log.isDebugEnabled()) {
                log.debug("Shut down runners count=" + GenericUtils.size(runners));
            }
        }

        if (err != null) {
            throw err;
        }
    }

    protected static class SshAgentSession extends AbstractAgentClient implements Runnable {

        private final long socket;

        public SshAgentSession(long socket, SshAgent agent) {
            super(agent);
            this.socket = socket;
        }

        @SuppressWarnings("synthetic-access")
        @Override
        public void run() {
            try {
                byte[] buf = new byte[1024];
                while (true) {
                    int result = Socket.recv(socket, buf, 0, buf.length);
                    if (result == -Status.APR_EOF) {
                        break;
                    } else if (result < Status.APR_SUCCESS) {
                        throwException(result);
                    }
                    messageReceived(new ByteArrayBuffer(buf, 0, result));
                }
            } catch (Exception e) {
                log.error("Failed to process", e);
            } finally {
                Socket.close(socket);
            }
        }

        @SuppressWarnings("synthetic-access")
        @Override
        protected void reply(Buffer buf) throws IOException {
            int result = Socket.send(socket, buf.array(), buf.rpos(), buf.available());
            if (result < Status.APR_SUCCESS) {
                throwException(result);
            }
        }

    }

    /**
     * transform an APR error number in a more fancy exception
     *
     * @param code APR error code
     * @throws java.io.IOException the produced exception for the given APR error number
     */
    private static void throwException(int code) throws IOException {
        throw new IOException(
                org.apache.tomcat.jni.Error.strerror(-code)
                        + " (code: " + code + ")");
    }

}
