package ctbrec.io;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.nio.file.Files;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;

import ctbrec.Config;
import ctbrec.Settings.ProxyType;
import okhttp3.ConnectionPool;
import okhttp3.Cookie;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;
import okhttp3.OkHttpClient.Builder;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;

public abstract class HttpClient {
    private static final Logger LOG = LoggerFactory.getLogger(HttpClient.class);

    private static final ConnectionPool GLOBAL_HTTP_CONN_POOL = new ConnectionPool(10, 2, TimeUnit.MINUTES);
    
    protected OkHttpClient client;
    protected CookieJarImpl cookieJar = new CookieJarImpl();
    protected boolean loggedIn = false;
    protected int loginTries = 0;
    private String name;

    protected HttpClient(String name) {
        this.name = name;
        cookieJar = createCookieJar();
        reconfigure();
    }

    protected CookieJarImpl createCookieJar() {
        return new CookieJarImpl();
    }

    private void loadProxySettings() {
        ProxyType proxyType = Config.getInstance().getSettings().proxyType;
        switch (proxyType) {
        case HTTP:
            System.setProperty("http.proxyHost", Config.getInstance().getSettings().proxyHost);
            System.setProperty("http.proxyPort", Config.getInstance().getSettings().proxyPort);
            System.setProperty("https.proxyHost", Config.getInstance().getSettings().proxyHost);
            System.setProperty("https.proxyPort", Config.getInstance().getSettings().proxyPort);
            if(Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) {
                String username = Config.getInstance().getSettings().proxyUser;
                String password = Config.getInstance().getSettings().proxyPassword;
                System.setProperty("http.proxyUser", username);
                System.setProperty("http.proxyPassword", password);
            }
            break;
        case SOCKS4:
            System.setProperty("socksProxyVersion", "4");
            System.setProperty("socksProxyHost", Config.getInstance().getSettings().proxyHost);
            System.setProperty("socksProxyPort", Config.getInstance().getSettings().proxyPort);
            break;
        case SOCKS5:
            System.setProperty("socksProxyVersion", "5");
            System.setProperty("socksProxyHost", Config.getInstance().getSettings().proxyHost);
            System.setProperty("socksProxyPort", Config.getInstance().getSettings().proxyPort);
            if(Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) {
                String username = Config.getInstance().getSettings().proxyUser;
                String password = Config.getInstance().getSettings().proxyPassword;
                Authenticator.setDefault(new SocksProxyAuth(username, password));
            }
            break;
        case DIRECT:
        default:
            System.clearProperty("http.proxyHost");
            System.clearProperty("http.proxyPort");
            System.clearProperty("https.proxyHost");
            System.clearProperty("https.proxyPort");
            System.clearProperty("socksProxyVersion");
            System.clearProperty("socksProxyHost");
            System.clearProperty("socksProxyPort");
            System.clearProperty("java.net.socks.username");
            System.clearProperty("java.net.socks.password");
            System.clearProperty("http.proxyUser");
            System.clearProperty("http.proxyPassword");
            break;
        }
    }

    public Response execute(Request req) throws IOException {
        Response resp = client.newCall(req).execute();
        return resp;
    }

    public abstract boolean login() throws IOException;

    public void reconfigure() {
        loadProxySettings();
        loadCookies();
        Builder builder = new OkHttpClient.Builder()
                .cookieJar(cookieJar)
                .connectionPool(GLOBAL_HTTP_CONN_POOL)
                .connectTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS)
                .readTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS)
                .connectionPool(new ConnectionPool(50, 10, TimeUnit.MINUTES));
        //.addInterceptor(new LoggingInterceptor());

        ProxyType proxyType = Config.getInstance().getSettings().proxyType;
        if (proxyType == ProxyType.HTTP) {
            String username = Config.getInstance().getSettings().proxyUser;
            String password = Config.getInstance().getSettings().proxyPassword;
            if (username != null && !username.isEmpty()) {
                builder.proxyAuthenticator(createHttpProxyAuthenticator(username, password));
            }
        }

        // if transport layer security (TLS) is switched on, accept the self signed cert from the server
        if (Config.getInstance().getSettings().transportLayerSecurity) {
            acceptAllTlsCerts(builder);
        }

        client = builder.build();
    }

    /**
     * This is a very simple and insecure solution to accept the self-signed cert from
     * the server. The side effect is, that certificates from other servers are neither checked!
     * TODO Delegate to the default trustmanager, if it is not the self-signed cert
     */
    private void acceptAllTlsCerts(Builder builder) {
        X509TrustManager x509TrustManager = new X509TrustManager() {
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                X509Certificate[] x509Certificates = new X509Certificate[0];
                return x509Certificates;
            }
            @Override public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {}
            @Override public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {}
        };

        try {
            final TrustManager[] trustManagers = new TrustManager[] { x509TrustManager };
            final String PROTOCOL = "TLSv1.2";
            SSLContext sslContext = SSLContext.getInstance(PROTOCOL);
            KeyManager[] keyManagers = null;
            SecureRandom secureRandom = new SecureRandom();
            sslContext.init(keyManagers, trustManagers, secureRandom);
            SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
            builder.sslSocketFactory(sslSocketFactory, x509TrustManager);
            builder.hostnameVerifier((name, sslSession) -> true);
        } catch (KeyManagementException | NoSuchAlgorithmException e) {
            LOG.error("Couldn't install trust managers for TLS connections");
        }
    }

    public void shutdown() {
        persistCookies();
        client.connectionPool().evictAll();
        client.dispatcher().executorService().shutdown();
    }

    private void persistCookies() {
        try {
            CookieContainer cookies = new CookieContainer();
            cookies.putAll(cookieJar.getCookies());
            Moshi moshi = new Moshi.Builder()
                    .add(CookieContainer.class, new CookieContainerJsonAdapter())
                    .build();
            JsonAdapter<CookieContainer> adapter = moshi.adapter(CookieContainer.class).indent("  ");
            String json = adapter.toJson(cookies);

            File cookieFile = new File(Config.getInstance().getConfigDir(), "cookies-" + name + ".json");
            try(FileOutputStream fout = new FileOutputStream(cookieFile)) {
                fout.write(json.getBytes("utf-8"));
            }
        } catch (Exception e) {
            LOG.error("Couldn't persist cookies for {}", name, e);
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private void loadCookies() {
        try {
            File cookieFile = new File(Config.getInstance().getConfigDir(), "cookies-" + name + ".json");
            if(!cookieFile.exists()) {
                return;
            }
            byte[] jsonBytes = Files.readAllBytes(cookieFile.toPath());
            String json = new String(jsonBytes, "utf-8");

            Map<String, List<Cookie>> cookies = cookieJar.getCookies();
            Moshi moshi = new Moshi.Builder()
                    .add(CookieContainer.class, new CookieContainerJsonAdapter())
                    .build();
            JsonAdapter<CookieContainer> adapter = moshi.adapter(CookieContainer.class).indent("  ");
            CookieContainer fromJson = adapter.fromJson(json);
            Set entries = fromJson.entrySet();
            for (Object _entry : entries) {
                Entry entry = (Entry) _entry;
                cookies.put((String)entry.getKey(), (List<Cookie>)entry.getValue());
            }

        } catch (Exception e) {
            LOG.error("Couldn't load cookies for {}", name, e);
        }
    }

    public static class CookieContainer extends HashMap<String, List<Cookie>> {

    }

    private okhttp3.Authenticator createHttpProxyAuthenticator(String username, String password) {
        return new okhttp3.Authenticator() {
            @Override
            public Request authenticate(Route route, Response response) throws IOException {
                String credential = Credentials.basic(username, password);
                return response.request().newBuilder().header("Proxy-Authorization", credential).build();
            }
        };
    }

    public static class SocksProxyAuth extends Authenticator {
        private PasswordAuthentication auth;

        private SocksProxyAuth(String user, String password) {
            auth = new PasswordAuthentication(user, password == null ? new char[]{} : password.toCharArray());
        }

        @Override
        protected PasswordAuthentication getPasswordAuthentication() {
            return auth;
        }
    }

    public CookieJarImpl getCookieJar() {
        return cookieJar;
    }

    public void logout() {
        getCookieJar().clear();
        loggedIn = false;
    }

    public WebSocket newWebSocket(Request request, WebSocketListener l) {
        //Request request = new Request.Builder().url(url).build();
        return client.newWebSocket(request, l);
    }
}
