/* MemorizingTrustManager - a TrustManager which asks the user about invalid
 *  certificates and memorizes their decision.
 *
 * Copyright (c) 2010 Georg Lukas <[email protected]>
 *
 * MemorizingTrustManager.java contains the actual trust manager and interface
 * code to create a MemorizingActivity and obtain the results.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.tapchatapp.android.network.ssl;

import android.app.Activity;
import android.app.Application;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;

import com.google.common.collect.Maps;

import java.io.File;
import java.io.FileOutputStream;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Map;

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

public class MemorizingTrustManager implements X509TrustManager {
    public static final String DECISION_INTENT             = "de.duenndns.ssl.DECISION";
    public static final String DECISION_INTENT_APP         = DECISION_INTENT + ".app";
    public static final String DECISION_INTENT_ID          = DECISION_INTENT + ".decisionId";
    public static final String DECISION_INTENT_FINGERPRINT = DECISION_INTENT + ".fingerprint";
    public static final String DECISION_INTENT_CHOICE      = DECISION_INTENT + ".decisionChoice";

    private static final String TAG           = "MemorizingTrustManager";
    private static final String KEYSTORE_DIR  = "KeyStore";
    private static final String KEYSTORE_FILE = "KeyStore.bks";

    private static final Map<Integer, MTMDecision> sDecisions = Maps.newHashMap();
    private static int sLastDecisionId = 0;

    private Context mContext;
    private Handler mHandler;
    private X509TrustManager mDefaultTrustManager;
    private X509TrustManager mAppTrustManager;

    public static X509TrustManager[] getInstanceList(Context c) {
   		return new X509TrustManager[] { new MemorizingTrustManager(c) };
   	}

	public MemorizingTrustManager(Context context) {
		mContext = context;
		mHandler = new Handler();

        mDefaultTrustManager = getTrustManager(null);
        mAppTrustManager     = getTrustManager(loadAppKeyStore());
	}

	private X509TrustManager getTrustManager(KeyStore ks) {
		try {
			TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
			tmf.init(ks);
			for (TrustManager t : tmf.getTrustManagers()) {
				if (t instanceof X509TrustManager) {
					return (X509TrustManager)t;
				}
			}
            return null;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

    private File getKeyStoreFile() {
        Application app = getApplication(mContext);
        File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
        return new File(dir, KEYSTORE_FILE);
    }

    private KeyStore loadAppKeyStore() {
        File file = getKeyStoreFile();
        try {
		    KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
			ks.load(null, null);

            if (file.exists()) {
			    ks.load(new java.io.FileInputStream(file), "MTM".toCharArray());
            }

            return ks;

        } catch (Exception ex) {
            if (file.exists()) {
                file.delete();
            }
            throw new RuntimeException(ex);
        }
	}

	private void storeCert(X509Certificate[] chain) {
        try {
            KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
            store.load(null, null);

		    // Add all certs from chain to key store
			for (X509Certificate c : chain) {
                store.setCertificateEntry(c.getSubjectDN().toString(), c);
            }

		    // Overwrite existing keystore
            FileOutputStream stream = null;
            try {
                stream = new java.io.FileOutputStream(getKeyStoreFile());
                store.store(stream, "MTM".toCharArray());
            } finally {
                if (stream != null) {
                    stream.close();
                }
            }

		    // reload trust manager with new store
		    mAppTrustManager = getTrustManager(store);

		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

    @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
   		checkCertTrusted(chain, authType, false);
   	}

    @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
   		checkCertTrusted(chain, authType, true);
   	}

    @Override public X509Certificate[] getAcceptedIssuers() {
        return mDefaultTrustManager.getAcceptedIssuers();
   	}

	private void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer) throws CertificateException {
        if (checkCertificate(mDefaultTrustManager, chain, authType, isServer)) {
            return;
        }

        if (checkCertificate(mAppTrustManager, chain, authType, isServer)) {
            return;
        }

        interact(chain);
    }

    private boolean checkCertificate(X509TrustManager manager, X509Certificate[] chain, String authType, boolean isServer) {
		try {
			if (isServer) {
				manager.checkServerTrusted(chain, authType);
            } else {
                manager.checkClientTrusted(chain, authType);
            }
            return true;
		} catch (CertificateException ae) {
            return false;
		}
	}

    private void interact(final X509Certificate[] chain) throws CertificateException {
   		final MTMDecision decision = createDecision();

        IntentFilter filter = new IntentFilter(DECISION_INTENT + "/" + mContext.getPackageName());
        BroadcastReceiver receiver = new BroadcastReceiver() {
            @Override public void onReceive(Context ctx, Intent intent) {
                int decisionId = intent.getIntExtra(DECISION_INTENT_ID, MTMDecision.DECISION_INVALID);
                int choice = intent.getIntExtra(DECISION_INTENT_CHOICE, MTMDecision.DECISION_INVALID);

                MTMDecision decision = getDecision(decisionId);
                if (decision == null) {
                    Log.e(TAG, "interactResult: aborting due to stale decision reference!");
                    return;
                }

                //noinspection SynchronizationOnLocalVariableOrMethodParameter
                synchronized (decision) {
                    decision.state = choice;
                    decision.notify();
                }
            }
        };

        mContext.registerReceiver(receiver, filter);

   		mHandler.post(new Runnable() {
            @Override public void run() {
                Intent intent = new Intent(mContext, MemorizingActivity.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   				intent.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + decision.id));
                intent.putExtra(DECISION_INTENT_APP, mContext.getPackageName());
                intent.putExtra(DECISION_INTENT_ID, decision.id);
                intent.putExtra(DECISION_INTENT_FINGERPRINT, CertUtil.certHash(chain[0], CertUtil.SHA1));
                mContext.startActivity(intent);
   			}
   		});

        //noinspection EmptyCatchBlock
        try {
            //noinspection SynchronizationOnLocalVariableOrMethodParameter
            synchronized (decision) {
                decision.wait();
            }
   		} catch (InterruptedException e) {
   		}

        mContext.unregisterReceiver(receiver);

   		switch (decision.state) {
   		    case MTMDecision.DECISION_ALWAYS:
                storeCert(chain);
   		    case MTMDecision.DECISION_ONCE:
   			    break;
   		    default:
   			    throw new CertificateException();
   		}
   	}

    private static MTMDecision createDecision() {
        synchronized (sDecisions) {
            sLastDecisionId++;
            MTMDecision decision = new MTMDecision(sLastDecisionId);
            sDecisions.put(sLastDecisionId, decision);
            return decision;
        }
    }

    private static MTMDecision getDecision(int decisionId) {
        synchronized (sDecisions) {
            return sDecisions.remove(decisionId);
        }
    }

    private static Application getApplication(Context context) {
        if (context instanceof Application) {
            return (Application)context;
        } else if (context instanceof Service) {
            return ((Service)context).getApplication();
        } else if (context instanceof Activity) {
            return ((Activity)context).getApplication();
        } else {
            throw new IllegalArgumentException("context must be either Activity or Service!");
        }
    }
}