/*
 * Copyright 2017 Guilherme Chaguri
 *
 * 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.
 */

package com.guichaguri.minimalftp.handler;

import com.guichaguri.minimalftp.FTPConnection;
import com.guichaguri.minimalftp.FTPServer;
import com.guichaguri.minimalftp.Utils;
import com.guichaguri.minimalftp.api.IUserAuthenticator;
import com.guichaguri.minimalftp.api.IUserAuthenticator.AuthException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

/**
 * Handles special connection-based commands
 * @author Guilherme Chaguri
 */
public class ConnectionHandler {

    private final FTPConnection con;

    private InetAddress address = null;
    private boolean authenticated = false;
    private String username = null;

    private boolean passive = false;
    private ServerSocket passiveServer = null;
    private String activeHost = null;
    private int activePort = 0;

    private boolean ascii = true;
    private boolean secureData = false;
    private boolean stop = false;

    public ConnectionHandler(FTPConnection connection) {
        this.con = connection;
    }

    public boolean shouldStop() {
        return stop;
    }

    public boolean isAsciiMode() {
        return ascii;
    }

    public boolean isAuthenticated() {
        return authenticated;
    }

    public String getUsername() {
        return username;
    }

    public Socket createDataSocket() throws IOException {
        if(passive && passiveServer != null) {
            return passiveServer.accept();
        } else if(secureData) {
            SSLSocketFactory factory = con.getServer().getSSLContext().getSocketFactory();
            SSLSocket socket = (SSLSocket)factory.createSocket(activeHost, activePort);
            socket.setUseClientMode(false);
            return socket;
        } else {
            return new Socket(activeHost, activePort);
        }
    }

    public void onConnected() throws IOException {
        IUserAuthenticator auth = con.getServer().getAuthenticator();

        if(!auth.needsUsername(con)) {
            if(authenticate(auth, null)) {
                con.sendResponse(230, "Ready!");
            } else {
                con.sendResponse(421, "Authentication failed");
                con.close();
            }
        } else {
            con.sendResponse(220, "Waiting for authentication...");
        }
    }

    public void onDisconnected() throws IOException {
        if(passiveServer != null) {
            Utils.closeQuietly(passiveServer);
            passiveServer = null;
        }
    }

    public void registerCommands() {
        con.registerCommand("NOOP", "NOOP", this::noop, false); // Ping
        con.registerCommand("HELP", "HELP <command>", this::help, false); // Command Help
        con.registerCommand("QUIT", "QUIT", this::quit, false); // Quit
        con.registerCommand("REIN", "REIN", this::rein, false); // Logout
        con.registerCommand("USER", "USER <username>", this::user, false); // Set Username
        con.registerCommand("PASS", "PASS <password>", this::pass, false); // Set Password
        con.registerCommand("ACCT", "ACCT <info>", this::acct, false); // Account Info
        con.registerCommand("SYST", "SYST", this::syst); // System Information
        con.registerCommand("PASV", "PASV", this::pasv); // Passive Mode
        con.registerCommand("PORT", "PORT <address>", this::port); // Active Mode
        con.registerCommand("TYPE", "TYPE <type>", this::type); // Binary Flag
        con.registerCommand("STRU", "STRU <type>", this::stru); // Structure Type
        con.registerCommand("MODE", "MODE <mode>", this::mode); // Change Mode
        con.registerCommand("STAT", "STAT", this::stat); // Statistics

        con.registerCommand("AUTH", "AUTH <mechanism>", this::auth, false); // Security Mechanism (RFC 2228)
        con.registerCommand("PBSZ", "PBSZ <size>", this::pbsz, false); // Protection Buffer Size (RFC 2228)
        con.registerCommand("PROT", "PROT <level>", this::prot, false); // Data Channel Protection Level (RFC 2228)

        con.registerCommand("LPSV", "LPSV", this::lpsv); // Long Passive Mode (RFC 1639) (Obsolete)
        con.registerCommand("LPRT", "LPRT <address>", this::lprt); // Long Active Mode (RFC 1639) (Obsolete)

        con.registerCommand("EPSV", "EPSV", this::epsv); // Extended Passive Mode (RFC 2428)
        con.registerCommand("EPRT", "EPRT <address>", this::eprt); // Extended Active Mode (RFC 2428)

        con.registerCommand("HOST", "HOST <address>", this::host, false); // Custom Virtual Hosts (RFC 7151)

        con.registerFeature("base"); // Base Commands (RFC 5797)
        con.registerFeature("secu"); // Security Commands (RFC 5797)
        con.registerFeature("hist"); // Obsolete Commands (RFC 5797)
        con.registerFeature("nat6"); // Extended Passive/Active Commands (RFC 5797)
        con.registerFeature("TYPE A;AN;AT;AC;L;I"); // Supported Types (RFC 5797)
        con.registerFeature("AUTH TLS"); // SSL/TLS support (RFC 4217)
        con.registerFeature("PBSZ"); // Protection Buffer Size (RFC 2228)
        con.registerFeature("PROT"); // Protection Level (RFC 2228)
        con.registerFeature("EPSV"); // Extended Passive Mode (RFC 2428)
        con.registerFeature("EPRT"); // Extended Active Mode (RFC 2428)
        con.registerFeature("HOST"); // Custom Virtual Hosts (RFC 7151)
    }

    private void noop() {
        con.sendResponse(200, "OK");
    }

    private void help(String[] cmd) {
        if(cmd.length < 1) {
            con.sendResponse(501, "Missing parameters");
        }

        String command = cmd[0].toUpperCase();
        String help;

        if(cmd.length > 1 && command.equals("SITE")) {
            help = "SITE " + con.getSiteHelpMessage(cmd[1].toUpperCase());
        } else {
            help = con.getHelpMessage(command);
        }
        con.sendResponse(214, help);
    }

    private void type(String type) throws IOException {
        type = type.toUpperCase();

        if(type.startsWith("A")) {
            ascii = true;
        } else if(type.startsWith("L") || type.startsWith("I")) {
            ascii = false;
        } else {
            con.sendResponse(500, "Unknown type " + type);
            return;
        }
        con.sendResponse(200, "Type set to " + type);
    }

    private void stru(String structure) throws IOException {
        if(structure.equalsIgnoreCase("F")) {
            con.sendResponse(200, "The structure type was set to file");
        } else {
            con.sendResponse(504, "Unsupported structure type");
        }
    }

    private void mode(String mode) throws IOException {
        if(mode.equalsIgnoreCase("S")) {
            con.sendResponse(200, "The mode was set to stream");
        } else {
            con.sendResponse(504, "Unsupported mode");
        }
    }

    private void host(String host) throws IOException {
        if(authenticated) {
            con.sendResponse(503, "The user is already authenticated");
            return;
        }

        try {
            IUserAuthenticator auth = con.getServer().getAuthenticator();
            InetAddress address = InetAddress.getByName(host);

            if(auth.acceptsHost(con, address)) {
                this.address = address;
                con.sendResponse(220, "Host accepted");
            } else {
                this.address = null;
                con.sendResponse(504, "Host denied");
            }
        } catch(UnknownHostException ex) {
            con.sendResponse(501, "Invalid host");
        }
    }

    private void user(String username) throws IOException {
        if(authenticated) {
            con.sendResponse(230, "Logged in!");
            return;
        }

        this.username = username;

        IUserAuthenticator auth = con.getServer().getAuthenticator();
        if(auth.needsPassword(con, username, address)) {
            // Requests a password for the authentication
            con.sendResponse(331, "Needs a password");
        } else {
            // Tries to authenticate using the given username
            boolean success = authenticate(auth, null);

            if(success) {
                con.sendResponse(230, "Logged in!");
            } else {
                con.sendResponse(530, "Authentication failed");
                con.close();
            }
        }
    }

    private void pass(String password) throws IOException {
        if(authenticated) {
            con.sendResponse(230, "Logged in!");
            return;
        }

        // Tries to authenticate using the given username and password
        boolean success = authenticate(con.getServer().getAuthenticator(), password);

        if(success) {
            con.sendResponse(230, "Logged in!");
        } else {
            con.sendResponse(530, "Authentication failed");
            con.close();
        }
    }

    private void acct(String info) {
        if(authenticated) {
            con.sendResponse(230, "Logged in!");
            return;
        }

        // Many clients don't even support this command, it's not needed in most cases
        // A simple "username and password" combination is the most common system in the internet anyway
        // The authenticator can also handle special formatted usernames, if really needed (for instance: "username|account")

        // Although this is pretty simple to implement, I would have to store the password
        // in a field instead of directly sending it to the authenticator. I prefer to keep
        // things the way they are for security reasons.

        con.sendResponse(530, "Account information is not supported");
    }

    private void syst() {
        con.sendResponse(215, "UNIX Type: L8"); // Generic System Info
    }

    private void rein() {
        authenticated = false;
        username = null;
        address = null;
        con.sendResponse(220, "Ready for a new user");
    }

    private void quit() {
        con.sendResponse(221, "Closing connection...");
        stop = true;
    }

    private void pasv() throws IOException {
        FTPServer server = con.getServer();
        passiveServer = Utils.createServer(0, 5, server.getAddress(), server.getSSLContext(), secureData);
        passive = true;

        String host = passiveServer.getInetAddress().getHostAddress();
        int port = passiveServer.getLocalPort();

        if(host.equals("0.0.0.0")) {
            // Sends a valid address instead of a wildcard
            host = InetAddress.getLocalHost().getHostAddress();
        }

        String[] addr = host.split("\\.");

        String address = addr[0] + "," + addr[1] + "," + addr[2] + "," + addr[3];
        String addressPort = port / 256 + "," + port % 256;

        con.sendResponse(227, "Enabled Passive Mode (" + address + "," + addressPort + ")");
    }

    private void port(String data) {
        String[] args = data.split(",");

        activeHost = args[0] + "." + args[1] + "." + args[2] + "." + args[3];
        activePort = Integer.parseInt(args[4]) * 256 + Integer.parseInt(args[5]);
        passive = false;

        if(passiveServer != null) {
            Utils.closeQuietly(passiveServer);
            passiveServer = null;
        }
        con.sendResponse(200, "Enabled Active Mode");
    }

    private void stat() throws IOException {
        con.sendResponse(211, "Sending the status...");

        String ip = con.getAddress().getHostAddress();
        String user = username != null ? "as " + username : "anonymously";
        String type = ascii ? "ASCII" : "Binary";

        String data = "";
        data += "Connected from " + ip + " (" + ip + ")\r\n";
        data += "Logged in " + user + "\r\n";
        data += "TYPE: " + type + ", STRUcture: File, MODE: Stream\r\n";
        data += "Total bytes transferred for session: " + con.getBytesTransferred() + "\r\n";
        con.sendData(data.getBytes("UTF-8"));

        con.sendResponse(211, "Status sent!");
    }

    private void auth(String mechanism) throws IOException {
        mechanism = mechanism.toUpperCase();

        if(mechanism.equals("TLS") || mechanism.equals("TLS-C") ||
            mechanism.equals("SSL") || mechanism.equals("TLS-P")) {
            // No need to distinguish between TLS and SSL, as the protocol self-negotiate its level

            SSLContext ssl = con.getServer().getSSLContext();

            if(ssl == null) {
                con.sendResponse(431, "TLS/SSL is not available");
            } else if(con.isSSLEnabled()) {
                con.sendResponse(503, "TLS/SSL is already enabled");
            } else {
                con.sendResponse(234, "Enabling TLS/SSL...");
                con.enableSSL(ssl);
            }

        } else {
            con.sendResponse(502, "Unsupported mechanism");
        }
    }

    private void pbsz(String size) {
        if(con.isSSLEnabled()) {
            // For SSL, the buffer size should always be 0
            // Any other size should be accepted
            con.sendResponse(200, "The protection buffer size was set to 0");
        } else {
            con.sendResponse(503, "You can't set the protection buffer size in an insecure connection");
        }
    }

    private void prot(String level) {
        level = level.toUpperCase();

        if(!con.isSSLEnabled()) {
            con.sendResponse(503, "You can't update the protection level in an insecure connection");
        } else if(level.equals("C")) {
            secureData = false;
            con.sendResponse(200, "Protection level set to clear");
        } else if(level.equals("P")) {
            secureData = true;
            con.sendResponse(200, "Protection level set to private");
        } else if(level.equals("S") || level.equals("E")) {
            con.sendResponse(521, "Unsupported protection level");
        } else {
            con.sendResponse(502, "Unknown protection level");
        }
    }

    private void lpsv() throws IOException { // Obsolete Command
        FTPServer server = con.getServer();
        passiveServer = Utils.createServer(0, 5, server.getAddress(), server.getSSLContext(), secureData);
        passive = true;

        String host = passiveServer.getInetAddress().getHostAddress();
        int port = passiveServer.getLocalPort();

        if(host.equals("0.0.0.0")) {
            // Sends a valid address instead of a wildcard
            host = InetAddress.getLocalHost().getHostAddress();
        }

        String[] addr = host.split("\\.");

        String address = addr[0] + "," + addr[1] + "," + addr[2] + "," + addr[3];
        String addressPort = port / 256 + "," + port % 256;

        con.sendResponse(229, "Enabled Passive Mode (4,4," + address + ",2," + addressPort + ")");
    }

    private void lprt(String data) { // Obsolete Command
        String[] args = data.split(",");

        int hostLength = Integer.parseInt(args[1]);
        int portLength = Integer.parseInt(args[hostLength + 2]);

        String host = "";
        for(int i = 0; i < hostLength; i++) {
            host += "." + args[i + 2];
        }
        activeHost = host.substring(1);

        int port = 0;
        for(int i = 0; i < portLength; i++) {
            int num = Integer.parseInt(args[i + hostLength + 3]);
            int pos = (portLength - i - 1) * 8;
            port |= num << pos;
        }
        activePort = port;

        passive = false;

        if(passiveServer != null) {
            Utils.closeQuietly(passiveServer);
            passiveServer = null;
        }
        con.sendResponse(200, "Enabled Active Mode");
    }

    private void epsv() throws IOException {
        FTPServer server = con.getServer();
        passiveServer = Utils.createServer(0, 5, server.getAddress(), server.getSSLContext(), secureData);
        passive = true;

        con.sendResponse(229, "Enabled Passive Mode (|||" + passiveServer.getLocalPort() + "|)");
    }

    private void eprt(String data) {
        char delimiter = data.charAt(0);
        String[] args = data.split(String.format("\\%s", delimiter));

        activeHost = args[2];
        activePort = Integer.parseInt(args[3]);
        passive = false;

        if(passiveServer != null) {
            Utils.closeQuietly(passiveServer);
            passiveServer = null;
        }

        con.sendResponse(200, "Enabled Active Mode");
    }

    private boolean authenticate(IUserAuthenticator auth, String password) {
        try {
            con.setFileSystem(auth.authenticate(con, address, username, password));
            authenticated = true;
            return true;
        } catch(AuthException ex) {
            return false;
        } catch(Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }
}