package fr.univ.lyon1.client;

import fr.univ.lyon1.common.Channel;
import fr.univ.lyon1.common.ChatSSL;
import fr.univ.lyon1.common.Message;
import fr.univ.lyon1.common.command.Command;
import fr.univ.lyon1.common.command.CommandType;
import fr.univ.lyon1.common.exception.ChatException;
import fr.univ.lyon1.common.exception.NotInChannel;
import fr.univ.lyon1.common.exception.UnknownCommand;

import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * The core of the client side
 */
public class Client {
    private final int port;
    private final String address;
    private final String username;
    private final String password;
    protected final Socket socket;
    protected final ObjectOutputStream out;
    private ObjectInputStream in;
    private final List<Channel> channels = new ArrayList<>();
    protected boolean started = false;

    /**
     * A client need a server and login
     * @param address the server address
     * @param port the server port
     * @param username thr username
     * @param password the password
     * @throws IOException When the initial communication with the server fail
     */
    public Client(String address, int port, String username, String password) throws IOException {
        this.address = address;
        this.port = port;
        this.username = username;
        this.password = password;
        socket = initSSL();
        out = new ObjectOutputStream(socket.getOutputStream());
        getIn();
    }

    /**
     * Initialise the SSL WebSocket connection with the server
     * @return the socket
     * @throws IOException when unable to connect with the server
     */
    private Socket initSSL() throws IOException {
        SSLContext ctx = ChatSSL.getSSLContext();

        SocketFactory factory = ctx.getSocketFactory();

        Socket connection = factory.createSocket(address, port);
        ((SSLSocket) connection).setEnabledProtocols(new String[] {ChatSSL.tlsVersion});
        SSLParameters sslParams = new SSLParameters();
        sslParams.setEndpointIdentificationAlgorithm("HTTPS");
        ((SSLSocket) connection).setSSLParameters(sslParams);
        return connection;
    }

    /**
     * Close all connection to the server
     * @throws IOException if fail to close the connection to the server
     */
    public void disconnectedServer() throws IOException {
        socket.close();
        out.close();
        if (in != null)
            in.close();

        System.exit(0);
    }

    /**
     * Send a command to the server
     * @param cmd the command
     */
    private void send(Command cmd) {
        try {
            out.writeObject(cmd);
            out.flush();
        } catch (IOException e) {
            System.err.println("Fail to send command !");
            e.printStackTrace();
        }
    }

    /**
     * Send a message in the first channel
     * @param content the content of the message
     */
    public void sendMessage(String content) {
        send(new Command(CommandType.message, List.of(new Message(content, channels.get(0)))));
    }

    /**
     * Send a command
     * @param content the command name an args (/commandName arg1 arh2 arg3)
     * @throws UnknownCommand if the command can't be found
     */
    public void sendCommand(String content) throws UnknownCommand {
        List<String> args = Arrays.asList(content.split(" "));
        String commandName = args.get(0).replace("/", "");
        CommandType commandType;

        try {
            commandType = CommandType.valueOf(commandName);
        } catch (IllegalArgumentException e) {
            throw new UnknownCommand(commandName);
        }

        send(new Command(commandType, new ArrayList<>(args.subList(1, args.size()))));
    }

    /**
     * Send a message with a specific channel
     * @param content the channel name and the message content (#chanemName content of the message)
     * @throws NotInChannel if the client isn't in the channel
     */
    public void sendMessageChannel(String content) throws NotInChannel {
        String[] args = content.split(" ");
        String channelName = args[0].replace("#", "");
        content = String.join(" ", Arrays.stream(args).toList().subList(1, args.length));
        Channel channel = channels.stream().filter(c -> c.getName().equals(channelName)).findFirst().orElse(null);

        if (channel == null)
            throw new NotInChannel(channelName);

        send(new Command(CommandType.message, List.of(new Message(content, channel))));
    }

    /**
     * Manage income data from the server
     * @param data the data
     * @throws IOException if a connection error occur with the server
     */
    public void action(Object data) throws IOException {
        if (data instanceof Command)
            command((Command) data);
        else if (data instanceof ChatException)
            ((ChatException) data).printStackTrace();
        else {
            out.writeObject(new UnknownCommand());
            out.flush();
        }
    }

    /**
     * Command manager
     * @param cmd the command
     * @throws IOException if a connection error occur with the server
     */
    private void command(Command cmd) throws IOException {
        switch (cmd.getType()) {
            case login -> commandLogin();
            case message -> commandMessage(cmd);
            case list -> commandList(cmd);
            case listChannels -> commandListChannels(cmd);
            case join -> commandJoin(cmd);
        }
    }

    /**
     * After login handler
     * @throws IOException if a connection error occur with the server
     */
    private void commandLogin() throws IOException {
        out.writeObject(new Command(CommandType.list, null));
        out.flush();
        out.writeObject(new Command(CommandType.listChannels, null));
        out.flush();
        out.writeObject(new Command(CommandType.join, List.of("general")));
        out.flush();
    }

    /**
     * Receiving message from the server
     * @param cmd the message command
     */
    protected void commandMessage(Command cmd) {
        System.out.println();
        System.out.println(cmd.getArgs().get(0));
    }

    /**
     * User list handler
     * @param cmd the command list
     */
    private void commandList(Command cmd) {
        System.out.println("Users: "+cmd.getArgs().stream().map(Object::toString).collect(Collectors.joining(", ")));
    }

    /**
     * Channel list handler
     * @param cmd the command channel list
     */
    private void commandListChannels(Command cmd) {
        System.out.println("Channels: "+cmd.getArgs().stream().map(Object::toString).collect(Collectors.joining(", ")));
    }

    /**
     * The channel join handler
     * @param cmd the command join
     */
    private void commandJoin(Command cmd) {
        Channel chan = (Channel) cmd.getArgs().get(0);
        channels.add(chan);
        System.out.println("You join "+chan);
    }

    /**
     * Main thread function, creating sub thread for user input and output CLI management
     * @throws InterruptedException If the client force exit
     * @throws IOException if a connection error occur with the server
     */
    public void run() throws InterruptedException, IOException {
        if (started)
            return;

        Thread clientSendThread = new Thread(new ClientSend(this, out, socket));
        clientSendThread.start();

        Thread clientReceiveThread = new Thread(new ClientReceive(this, socket));
        clientReceiveThread.start();

        started = true;

        out.writeObject(new Command(CommandType.login, List.of(username, password)));
        out.flush();

        clientSendThread.join();
        socket.close();
        clientReceiveThread.interrupt();
    }

    /**
     * Get the in stream of the WebSocket
     * @return the in stream
     * @throws IOException if a connection error occur with the server
     */
    public ObjectInputStream getIn() throws IOException {
        if (in == null)
            in = new ObjectInputStream(socket.getInputStream());
        return in;
    }
}