package io.mrarm.irc.config;

import android.content.Context;
import android.graphics.Typeface;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import io.mrarm.irc.util.ColoredTextBuilder;

public class ServerCertificateManager {

    private static final String TAG = "CertificateManager";

    private static final Map<String, WeakReference<ServerCertificateManager>> mInstances = new HashMap<>();

    public static ServerCertificateManager get(File file) {
        synchronized (mInstances) {
            WeakReference<ServerCertificateManager> instance = mInstances.get(file.getAbsolutePath());
            if (instance != null) {
                ServerCertificateManager helper = instance.get();
                if (helper != null)
                    return helper;
            }
            ServerCertificateManager ret = new ServerCertificateManager(file);
            mInstances.put(file.getAbsolutePath(), new WeakReference<>(ret));
            return ret;
        }
    }

    public static ServerCertificateManager get(Context context, UUID serverUUID) {
        return get(ServerConfigManager.getInstance(context).getServerSSLCertsFile(serverUUID));
    }

    private File mKeyStoreFile;
    private KeyStore mKeyStore;
    private X509TrustManager mKeyStoreTrustManager;

    private ServerCertificateManager(File keyStoreFile) {
        mKeyStoreFile = keyStoreFile;
        if (keyStoreFile != null && keyStoreFile.exists()) {
            try {
                loadKeyStore(new FileInputStream(mKeyStoreFile));
            } catch (Exception e) {
                Log.w(TAG, "Failed to load keystore");
                mKeyStore = null;
            }
        }
    }

    @Override
    protected void finalize() throws Throwable {
        synchronized (mInstances) {
            String path = mKeyStoreFile.getAbsolutePath();
            if (mInstances.containsKey(path)) {
                if (mInstances.get(path).get() == this)
                    mInstances.remove(path);
            }
        }
        super.finalize();
    }

    public void loadKeyStore(InputStream stream) throws KeyStoreException, IOException,
            CertificateException, NoSuchAlgorithmException {
        synchronized (this) {
            mKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            mKeyStore.load(stream, null);
        }
    }

    private void createKeyStoreIfNull() throws KeyStoreException, IOException, CertificateException,
            NoSuchAlgorithmException {
        synchronized (this) {
            if (mKeyStore == null) {
                mKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
                mKeyStore.load(null, null);
            }
        }
    }

    public void saveKeyStore(OutputStream stream) throws KeyStoreException, IOException,
            CertificateException, NoSuchAlgorithmException {
        synchronized (this) {
            createKeyStoreIfNull();
            mKeyStore.store(stream, null);
        }
    }

    public void saveKeyStore() throws KeyStoreException, IOException, CertificateException,
            NoSuchAlgorithmException {
        saveKeyStore(new FileOutputStream(mKeyStoreFile));
    }

    public void addCertificateException(X509Certificate certificate) {
        synchronized (this) {
            try {
                createKeyStoreIfNull();
                mKeyStore.setCertificateEntry("cert-" + UUID.randomUUID(), certificate);
                if (mKeyStoreFile != null)
                    saveKeyStore();
            } catch (Exception e) {
                Log.e(TAG, "Failed to add certificate exception");
                e.printStackTrace();
            }
        }
    }

    public void removeCertificate(String alias) {
        synchronized (this) {
            if (mKeyStore == null)
                return;
            try {
                mKeyStore.deleteEntry(alias);
                if (mKeyStoreFile != null)
                    saveKeyStore();
            } catch (Exception e) {
                Log.e(TAG, "Failed to remove certificate");
                e.printStackTrace();
            }
        }
    }

    public List<String> getCertificateAliases() {
        synchronized (this) {
            if (mKeyStore == null)
                return null;
            try {
                return Collections.list(mKeyStore.aliases());
            } catch (KeyStoreException e) {
                return null;
            }
        }
    }

    public X509Certificate getCertificate(String alias) {
        synchronized (this) {
            try {
                return (X509Certificate) mKeyStore.getCertificate(alias);
            } catch (KeyStoreException e) {
                return null;
            }
        }
    }

    public boolean hasCertificate(Certificate certificate) throws KeyStoreException {
        synchronized (this) {
            return mKeyStore.getCertificateAlias(certificate) != null;
        }
    }

    public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
        synchronized (this) {
            if (mKeyStoreTrustManager == null && mKeyStore != null)
                mKeyStoreTrustManager = createKeyStoreTrustManager(mKeyStore);
            if (mKeyStoreTrustManager == null)
                    throw new CertificateException("Key store is null");
            mKeyStoreTrustManager.checkServerTrusted(chain, authType);
        }
    }

    public static SpannableString buildCertOverviewString(X509Certificate cert) {
        String sha1Fingerprint;
        try {
            StringBuilder builder = new StringBuilder();
            MessageDigest digest = MessageDigest.getInstance("SHA-1");
            byte[] bytes = digest.digest(cert.getEncoded());
            for (byte b : bytes)
                builder.append(String.format("%02x ", b));
            sha1Fingerprint = builder.toString();
        } catch (NoSuchAlgorithmException | CertificateEncodingException e) {
            throw new RuntimeException(e);
        }

        ColoredTextBuilder builder = new ColoredTextBuilder();
        builder.append("Subject: ", new StyleSpan(Typeface.BOLD));
        builder.append(cert.getSubjectX500Principal().getName().replace(",", ",\u200B"));
        builder.append("\nApplies to: ", new StyleSpan(Typeface.BOLD));
        builder.append(buildCertAppliesToString(cert));
        builder.append("\nIssuer: ", new StyleSpan(Typeface.BOLD));
        builder.append(cert.getIssuerDN().toString().replace(",", ",\u200B"));
        builder.append("\nSHA1 fingerprint:\n", new StyleSpan(Typeface.BOLD));
        builder.append(sha1Fingerprint);
        return SpannableString.valueOf(builder.getSpannable());
    }

    public static String buildCertAppliesToString(X509Certificate cert) {
        List<String> elements = new ArrayList<>();
        try {
            Collection<List<?>> altNames = cert.getSubjectAlternativeNames();
            if (altNames != null) {
                for (List<?> altName : altNames) {
                    Integer altNameType = (Integer) altName.get(0);
                    if (altNameType != 2 && altNameType != 7) // dns or ip
                        continue;
                    elements.add((String) altName.get(1));
                }
            }
        } catch (CertificateParsingException ignored) {
        }

        if (elements.size() == 0)
            return "none";
        return TextUtils.join(",", elements.toArray());
    }

    public static X509TrustManager createKeyStoreTrustManager(KeyStore keyStore) {
        try {
            TrustManagerFactory factory = TrustManagerFactory.getInstance(
                    TrustManagerFactory.getDefaultAlgorithm());
            factory.init(keyStore);
            for (TrustManager manager : factory.getTrustManagers()) {
                if (manager instanceof X509TrustManager)
                    return (X509TrustManager) manager;
            }
        } catch (NoSuchAlgorithmException | KeyStoreException e) {
            throw new RuntimeException(e);
        }
        return null;
    }

}