package org.tmatesoft.svn.core.internal.wc;

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

import java.io.File;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;

import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationProvider;
import org.tmatesoft.svn.core.internal.util.SVNBase64;
import org.tmatesoft.svn.core.internal.util.SVNHashMap;
import org.tmatesoft.svn.core.internal.util.SVNSSLUtil;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * @author TMate Software Ltd.
 * @version 1.3
 */
public class DefaultSVNSSLTrustManager implements X509TrustManager {

	private SVNURL myURL;
	private DefaultSVNAuthenticationManager myAuthManager;

	private X509Certificate[] myTrustedCerts;
	private String myRealm;
	private File myAuthDirectory;
	private boolean myIsUseKeyStore;
	private File[] myServerCertFiles;

    private X509TrustManager[] myDefaultTrustManagers;

	public DefaultSVNSSLTrustManager(File authDir, SVNURL url, File[] serverCertFiles, boolean useKeyStore, DefaultSVNAuthenticationManager authManager) {
		myURL = url;
		myAuthDirectory = authDir;
		myRealm = "https://" + url.getHost() + ":" + url.getPort();
		myAuthManager = authManager;
		myIsUseKeyStore = useKeyStore;
		myServerCertFiles = serverCertFiles;
	}

    private X509TrustManager[] getDefaultTrustManagers() {
        if (myDefaultTrustManagers == null && myIsUseKeyStore) {
            myDefaultTrustManagers = initDefaultTrustManagers();
        }
        return myDefaultTrustManagers;
    }

    private X509TrustManager[] initDefaultTrustManagers() {
        try {
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509", "SunJSSE");
            tmf.init((KeyStore) null);
            TrustManager[] trustManagers = tmf.getTrustManagers();
            if (trustManagers == null || trustManagers.length == 0) {
                return null;
            }
            List x509TrustManagers = new ArrayList();
            for (int i = 0; i < trustManagers.length; i++) {
                TrustManager trustManager = trustManagers[i];
                if (trustManager instanceof X509TrustManager) {
                    x509TrustManagers.add(trustManager);
                }
            }
            return (X509TrustManager[]) x509TrustManagers.toArray(new X509TrustManager[x509TrustManagers.size()]);
        } catch (NoSuchAlgorithmException e) {
            SVNDebugLog.getDefaultLog().log(SVNLogType.DEFAULT, e, Level.FINEST);
        } catch (NoSuchProviderException e) {
            SVNDebugLog.getDefaultLog().log(SVNLogType.DEFAULT, e, Level.FINEST);
        } catch (KeyStoreException e) {
            SVNDebugLog.getDefaultLog().log(SVNLogType.DEFAULT, e, Level.FINEST);
        }
        return null;
    }

    private void init() {
		if (myTrustedCerts != null) {
			return;
		}
		Collection trustedCerts = new ArrayList();
		// load trusted certs from files.
		for (int i = 0; i < myServerCertFiles.length; i++) {
			X509Certificate cert = loadCertificate(myServerCertFiles[i]);
			if (cert != null) {
				trustedCerts.add(cert);
			}
		}

        X509TrustManager[] trustManagers = getDefaultTrustManagers();
        for (int i = 0; trustManagers != null && i < trustManagers.length; i++) {
            X509TrustManager trustManager = trustManagers[i];
            X509Certificate[] acceptedCerts = trustManager.getAcceptedIssuers();
            for (int c = 0; acceptedCerts != null && c < acceptedCerts.length; c++) {
                X509Certificate cert = acceptedCerts[c];
                trustedCerts.add(cert);
            }
        }
        myTrustedCerts = (X509Certificate[]) trustedCerts.toArray(new X509Certificate[trustedCerts.size()]);
    }

	public X509Certificate[] getAcceptedIssuers() {
		init();
		return myTrustedCerts;
	}

	public void checkClientTrusted(X509Certificate[] certs, String arg1) throws CertificateException {
	}

	public void checkServerTrusted(X509Certificate[] certs, String algorithm) throws CertificateException {
		if (certs != null && certs.length > 0 && certs[0] != null) {
			String data = SVNBase64.byteArrayToBase64(certs[0].getEncoded());
			String stored = (String) myAuthManager.getRuntimeAuthStorage().getData("svn.ssl.server", myRealm);
			if (data.equals(stored)) {
				return;
			}
			stored = getStoredServerCertificate(myRealm);
			if (data.equals(stored)) {
				return;
			}
			ISVNAuthenticationProvider authProvider = myAuthManager.getAuthenticationProvider();
			int failures = SVNSSLUtil.getServerCertificateFailures(certs[0], myURL.getHost());
			// compose bit mask.
			// 8 is default
			// check dates for 1 and 2
			// check host name for 4
			if (authProvider != null) {
				boolean store = myAuthManager.getHostOptionsProvider().getHostOptions(myURL).isAuthStorageEnabled();
				final CertificateException exception = checkServerTrustedByDefault(certs, algorithm);
                if (exception != null && exception.getMessage().indexOf("Certificates does not conform to algorithm constraints") >= 0) {
                    throw new SVNSSLUtil.CertificateDoesNotConformConstraints("svn: Server SSL certificates chain for '" + myRealm + "' does not conform to algorithm constraints", exception);
                }
                final boolean trustServer = exception == null;
                int result;
                if (trustServer) {
                    result = ISVNAuthenticationProvider.ACCEPTED;
                } else {
                    result = authProvider.acceptServerAuthentication(myURL, myRealm, certs[0], store);
                }
				if (result == ISVNAuthenticationProvider.ACCEPTED && store) {
					try {
						storeServerCertificate(myRealm, data, failures);
					} catch (SVNException e) {
                        // ignore that exception, as we only need to trust now and may save data later.
                        //throw new SVNSSLUtil.CertificateNotTrustedException("svn: Server SSL certificate for '" + myRealm + "' cannot be saved");
					    SVNDebugLog.getDefaultLog().logError(SVNLogType.NETWORK, e);
					}
				}
				if (result != ISVNAuthenticationProvider.REJECTED) {
					myAuthManager.getRuntimeAuthStorage().putData("svn.ssl.server", myRealm, data);
					return;
				}
				throw new SVNSSLUtil.CertificateNotTrustedException("svn: Server SSL certificate for '" + myRealm + "' rejected");
			}
			// like as tmp. accepted.
        }
	}

    private CertificateException checkServerTrustedByDefault(X509Certificate[] certs, String algorithm) {
        final X509TrustManager[] trustManagers = getDefaultTrustManagers();
        if (trustManagers == null) {
            return null;
        }
        for (int i = 0; i < trustManagers.length; i++) {
            final X509TrustManager trustManager = trustManagers[i];
            try {
                trustManager.checkServerTrusted(certs, algorithm);
            } catch (CertificateException e) {
                return e;
            }
        }
        return null;
    }

    private String getStoredServerCertificate(String realm) {
		File file = new File(myAuthDirectory, SVNFileUtil.computeChecksum(realm));
		if (!file.isFile()) {
			return null;
		}
		SVNWCProperties props = new SVNWCProperties(file, "");
		try {
			String storedRealm = props.getPropertyValue("svn:realmstring");
			if (!realm.equals(storedRealm)) {
				return null;
			}
			return props.getPropertyValue("ascii_cert");
		}
		catch (SVNException e) {
		}
		return null;
	}

	private void storeServerCertificate(String realm, String data, int failures) throws SVNException {
		myAuthDirectory.mkdirs();

		File file = new File(myAuthDirectory, SVNFileUtil.computeChecksum(realm));
		SVNHashMap map = new SVNHashMap();
        map.put("ascii_cert", data);
        map.put("svn:realmstring", realm);
        map.put("failures", Integer.toString(failures));
        
		SVNFileUtil.deleteFile(file);
        
        File tmpFile = SVNFileUtil.createUniqueFile(myAuthDirectory, "auth", ".tmp", true);
        try {
            SVNWCProperties.setProperties(SVNProperties.wrap(map), file, tmpFile, SVNWCProperties.SVN_HASH_TERMINATOR);
        } finally {
            SVNFileUtil.deleteFile(tmpFile);
        }
	}

	public static X509Certificate loadCertificate(File pemFile) {
		InputStream is = null;
		try {
			is = SVNFileUtil.openFileForReading(pemFile, SVNLogType.WC);
		}
		catch (SVNException e) {
			return null;
		}
		try {
			CertificateFactory factory = CertificateFactory.getInstance("X509");
			return (X509Certificate)factory.generateCertificate(is);
		}
		catch (CertificateException e) {
			return null;
		}
		finally {
			SVNFileUtil.closeFile(is);
		}
	}
}