package io.dashbase.clue.client;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import jline.console.ConsoleReader;
import jline.console.completer.ArgumentCompleter;
import jline.console.completer.Completer;
import jline.console.completer.FileNameCompleter;
import jline.console.completer.StringsCompleter;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;

import javax.net.ssl.*;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.URL;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

public class ClueCommandClient {

    private final ClueCommandService svc;
    private final CmdlineHelper cmdlineHelper;

    public ClueCommandClient(URL url) throws Exception {
        final OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();

        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        if ("https".equals(url.getProtocol())) {
            try {
                setSslSocketFactory(okHttpClientBuilder);
            } catch(Exception e) {
                throw new IllegalStateException("cannot create ssl connection: " + e);
            }
        }

        final OkHttpClient okHttpClient = okHttpClientBuilder.readTimeout(10, TimeUnit.SECONDS)
                .connectTimeout(10, TimeUnit.SECONDS)
                .build();

        Retrofit restAdator = new Retrofit.Builder().baseUrl(HttpUrl.get(url))
                .addConverterFactory(ScalarsConverterFactory.create())
                .addConverterFactory(JacksonConverterFactory.create(mapper))
                .client(okHttpClient).build();

        svc = restAdator.create(ClueCommandService.class);

        cmdlineHelper = new CmdlineHelper(new Supplier<Collection<String>>() {
            @Override
            public Collection<String> get() {
                try {
                    return getCommands();
                } catch (Exception e) {
                    System.out.println("unable obtaining command list");
                    return Collections.emptyList();
                }
            }
        }, null);
    }

    public final ClueCommandService service() {
        return svc;
    }

    // Create a trust manager that does not validate certificate chains
    private static final TrustManager[] trustAllCerts = new TrustManager[] {
            new X509TrustManager() {
                @Override
                public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
                }

                @Override
                public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
                }

                @Override
                public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                    return new java.security.cert.X509Certificate[]{};
                }
            }
    };

    private static void setSslSocketFactory(OkHttpClient.Builder builder)  throws Exception {
        // Install the all-trusting trust manager
        final SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
        // Create an ssl socket factory with our all-trusting manager
        final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

        builder.sslSocketFactory(sslSocketFactory, (X509TrustManager)trustAllCerts[0]);
        builder.hostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        });
    }

    public Collection<String> getCommands() throws Exception {
        Call<Collection<String>> call = svc.commands();
        return call.execute().body();
    }


    public void handleCommand(String cmdName, String[] args, PrintStream out) {
        try {
            String argString = CmdlineHelper.toString(Arrays.asList(args));
            Call<ResponseBody> call = svc.command(cmdName, argString);

            Response<ResponseBody> response = call.execute();
            try (ResponseBody responseBody = response.body()) {
                InputStream is = responseBody.byteStream();
                is.transferTo(System.out);
            }
        } catch (Exception e) {
            e.printStackTrace(out);
        }
    }

    public String readCommand() {
        try {
            return cmdlineHelper.readCommand();
        } catch (Exception e) {
            System.err.println("Error! Clue is unable to read line from stdin: " + e.getMessage());
            throw new IllegalStateException("Unable to read command line!", e);
        }
    }

    public void run() throws Exception {
        ConsoleReader consoleReader = new ConsoleReader();
        consoleReader.setBellEnabled(false);

        Collection<String> commands = getCommands();

        LinkedList<Completer> completors = new LinkedList<Completer>();
        completors.add(new StringsCompleter(commands));

        completors.add(new FileNameCompleter());

        consoleReader.addCompleter(new ArgumentCompleter(completors));



        while(true){
            String line = readCommand();
            if (line == null || line.isEmpty()) continue;
            line = line.trim();
            if ("exit".equals(line)) {
                System.exit(0);
            }
            String[] parts = line.split("\\s");
            if (parts.length > 0){
                String cmd = parts[0];
                String[] cmdArgs = new String[parts.length - 1];
                System.arraycopy(parts, 1, cmdArgs, 0, cmdArgs.length);
                handleCommand(cmd, cmdArgs, System.out);
            }
        }
    }


    public static void main(String[] args) throws Exception {
        if (args.length != 1){
            System.out.println("usage: <url>");
            System.exit(1);
        }

        String remoteLocation = args[0];
        if (!remoteLocation.startsWith("http://") && !remoteLocation.startsWith("https://")) {
            remoteLocation = "http://" + remoteLocation;
        }
        if (!remoteLocation.equals("/")) {
            remoteLocation = remoteLocation + "/";
        }

        URL url = new URL(remoteLocation);
        ClueCommandClient client = new ClueCommandClient(url);
        client.run();
    }
}