package cat.aoc.pci;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.xml.namespace.QName;
import javax.xml.ws.Binding;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.soap.SOAPBinding;

import org.openuri.ProcesaResponse_ConfirmacionPeticion;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.cxf.configuration.jsse.TLSClientParameters;
import org.apache.cxf.configuration.security.CertificateConstraintsType;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.interceptor.AttachmentOutInterceptor;
import org.apache.cxf.interceptor.Interceptor;
import org.apache.cxf.message.Message;

import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.apache.ws.security.handler.WSHandlerConstants;
import org.openuri.Asincron;
import org.openuri.AsincronSoap;
import org.openuri.Procesa;
import org.openuri.ProcesaResponse;
import org.openuri.Sincron;
import org.openuri.SincronPortType;

import cat.aoc.pci.utils.PWCallback;

public class PCIInvoker {

	// fitxer amb les props per a setejar el TLS i el WSS
	private PropertiesConfiguration trustStoreConfig = new PropertiesConfiguration();
	private PropertiesConfiguration keyStoreConfig = new PropertiesConfiguration();
	// configuració del ws
	private PropertiesConfiguration siriSwatConfig = new PropertiesConfiguration();

	/**
	 * @param configPropsPath
	 * @throws ConfigurationException
	 * @throws FileNotFoundException 
	 */
	public PCIInvoker() throws ConfigurationException, FileNotFoundException{
		// carreguem el truststoreconfig
		trustStoreConfig.load(PCIInvoker.class.getClassLoader().getResource("siri-crypto-truststore.properties"));
		// carreguem el keystoreConfig
		keyStoreConfig.load(PCIInvoker.class.getClassLoader().getResource("siri-crypto-keystore.properties"));
		// carreguem les properties del ws
		siriSwatConfig.load(PCIInvoker.class.getClassLoader().getResource("siri-swat.properties"));
		
	}

	/**
	 * Invoca el servei a partir del client
	 * @param client
	 * @return
	 * @throws Exception 
	 */
	public ProcesaResponse invoke(PCIPeticio client) throws Exception{

		// recuperem el WSDL i ¡la URL es recupera d'aquí!
		URL wsdlURL = PCIInvoker.class.getClass().getResource("/wsdl/Sincron.wsdl");

		// recuperem el nom del servei
		QName serviceName = new QName("http://www.openuri.org/", "Sincron");
		// recuperem el servei sobre el que es realitza la petició
		Sincron sincron = new Sincron(wsdlURL,serviceName);
		
		// recuperem el port
		SincronPortType port = sincron.getPort(SincronPortType.class);

		// recuperem el client per a poder setejar interceptors i demés
		Client cxfClient = ClientProxy.getClient(port);

		// setegem l'endpoint del siri (NO FUNCIONA)
		cxfClient.getEndpoint().getEndpointInfo().setAddress(siriSwatConfig.getString("siriswat.endpoint.url"));

		// comprovem si hem de fer-ho o no amb attachments
		if(client.isMtom()){

			// MTOM: Canal SSL + attachments

			// enableMTOM
			Binding binding = ((BindingProvider)port).getBinding(); 
			((SOAPBinding)binding).setMTOMEnabled(true);   

			// attachment interceptor
			AttachmentOutInterceptor attachOut = new AttachmentOutInterceptor();
			cxfClient.getOutInterceptors().add(attachOut);

			// configurem el SSL
			setupTLS(cxfClient);

		}else{
			// Sense MTOM: WSS + SSL

			// configurem el WSS
			configureWSSOut(cxfClient);
			configureWSSIn(cxfClient);
			
			// configurem el SSL
			setupTLS(cxfClient);

		}

		// montem la petició i enviem
		Procesa procesa = new Procesa();
		procesa.setPeticion(client.getPeticio());

		// invoquem i retornem la resposta
		return port.procesa(procesa);

	}

	/**
	 * Invoca el servei asíncron a partir del client
	 * @param client
	 * @return
	 * @throws Exception 
	 */
	public ProcesaResponse_ConfirmacionPeticion invokeAsincron(PCIPeticio client) throws Exception{

		// recuperem el WSDL ¡D'aquí s'agafa la URL!
		URL wsdlURL = PCIInvoker.class.getClass().getResource("/wsdl/Asincron.wsdl");
		
		// recuperem el nom del servei
		QName serviceName = new QName("http://www.openuri.org/", "Asincron");
		// recuperem el servei sobre el que es realitza la petició
		Asincron sincron = new Asincron(wsdlURL,serviceName);
		
		// recuperem el port
		AsincronSoap port = sincron.getPort(AsincronSoap.class);

		// recuperem el client per a poder setejar interceptors i demés
		Client cxfClient = ClientProxy.getClient(port);

		// setegem l'endpoint del siri (NO FUNCIONA)
		cxfClient.getEndpoint().getEndpointInfo().setAddress(siriSwatConfig.getString("siriswat.endpoint.url"));

		// comprovem si hem de fer-ho o no amb attachments
		if(client.isMtom()){

			// MTOM: Canal SSL + attachments

			// enableMTOM
			Binding binding = ((BindingProvider)port).getBinding(); 
			((SOAPBinding)binding).setMTOMEnabled(true);   

			// attachment interceptor
			AttachmentOutInterceptor attachOut = new AttachmentOutInterceptor();
			cxfClient.getOutInterceptors().add(attachOut);

			// configurem el SSL
			setupTLS(cxfClient);

		}else{
			// Sense MTOM: WSS + SSL

			// configurem el WSS
			configureWSSOut(cxfClient);
			configureWSSIn(cxfClient);
			
			// configurem el SSL
			setupTLS(cxfClient);

		}

		// montem la petició i enviem
		Procesa procesa = new Procesa();
		procesa.setPeticion(client.getPeticio());

		// invoquem i retornem la resposta
		return port.procesa(procesa);

	}
	
	
	/**
	 * Configura el tractament d'entrada i sortida del WSSecurity.
	 * 
	 * @param client
	 */
	private void configureWSSIn(Client client){

		Map<String,Object> inProps= new HashMap<String,Object>();

		inProps.put(WSHandlerConstants.ACTION, "Timestamp Signature");
		inProps.put(WSHandlerConstants.PW_CALLBACK_CLASS, PWCallback.class.getName());
		inProps.put(WSHandlerConstants.SIG_PROP_FILE, "siri-crypto-truststore.properties");

		WSS4JInInterceptor wssIn = new WSS4JInInterceptor(inProps);
		client.getInInterceptors().add((Interceptor<? extends Message>) wssIn);

	}

	private void configureWSSOut(Client client){

		Map<String,Object> outProps = new HashMap<String,Object>();

		outProps.put(WSHandlerConstants.ACTION, "Timestamp Signature");
		outProps.put(WSHandlerConstants.USER, keyStoreConfig.getString("org.apache.ws.security.crypto.merlin.keystore.alias"));
		outProps.put(WSHandlerConstants.PW_CALLBACK_CLASS, PWCallback.class.getName());
		outProps.put(WSHandlerConstants.SIG_KEY_ID, "DirectReference");
		outProps.put(WSHandlerConstants.SIG_PROP_FILE, "siri-crypto-keystore.properties");

		WSS4JOutInterceptor wssOut = new WSS4JOutInterceptor(outProps);
		client.getOutInterceptors().add((Interceptor<? extends Message>) wssOut);
	}


	/**
	 * Configura els paràmetres necessaris per a la connexió SSL
	 * 
	 * @throws Exception 
	 */
	private void setupTLS(Client client) throws Exception {

		HTTPConduit conduit = (HTTPConduit) client.getConduit();

		// creem els parametres de configuració del SSL
		TLSClientParameters tlsCP = new TLSClientParameters();

		// creem el trustStore
		String trustPassword = trustStoreConfig.getString("org.apache.ws.security.crypto.merlin.keystore.password");
		KeyStore trustStore = KeyStore.getInstance(trustStoreConfig.getString("org.apache.ws.security.crypto.merlin.keystore.type"));
		InputStream is = PCIInvoker.class.getClassLoader().getResourceAsStream(trustStoreConfig.getString("org.apache.ws.security.crypto.merlin.file"));
		trustStore.load(is, trustPassword.toCharArray());
		is.close();

		TrustManager[] myTrustStoreKeyManagers = null;
		// configurem el truststore en funció de si volem validar o no el cds
		if(siriSwatConfig.getBoolean("siriswat.cds.validate")){
			// creem el trustManager a partir d'aquest i el setejem en els paràmetres
			// del ssl del client
			myTrustStoreKeyManagers = getTrustManagers(trustStore);	
		}else{
			// Creem un fake trustManager per a poder fer les proves contra
			// un pc el qual el seu CDS no es de confiança per al nostre truststore 
			myTrustStoreKeyManagers = new TrustManager[] { new X509TrustManager() {

				public X509Certificate[] getAcceptedIssuers() {
					// TODO Auto-generated method stub
					return null;
				}

				public void checkServerTrusted(X509Certificate[] arg0, String arg1)
				throws CertificateException {
					// TODO Auto-generated method stub

				}

				public void checkClientTrusted(X509Certificate[] arg0, String arg1)
				throws CertificateException {
					// TODO Auto-generated method stub

				}
			}};
		}
		
		tlsCP.setTrustManagers(myTrustStoreKeyManagers);

		// creem el keyStore
		String keyPassword = keyStoreConfig.getString("org.apache.ws.security.crypto.merlin.keystore.password");
		KeyStore keyStore = KeyStore.getInstance(keyStoreConfig.getString("org.apache.ws.security.crypto.merlin.keystore.type"));
		is = PCIInvoker.class.getClassLoader().getResourceAsStream(keyStoreConfig.getString("org.apache.ws.security.crypto.merlin.file"));
		keyStore.load(is, keyPassword.toCharArray());
		is.close();

		// creem el keyManager a partir del keyStore i el setejem en els paràmetres ssl del client
		KeyManager[] myKeyManagers = getKeyManagers(keyStore,keyStoreConfig.getString("org.apache.ws.security.crypto.merlin.alias.password"));
		tlsCP.setKeyManagers(myKeyManagers);
		
		// Aquest linea servei per eliminar la comprovació del domini, quan per exemple
		// la petició no es realitza contra el frontal sinó directament contra un node,
		// en un entorn productiu aquesta linea s'hauria d'eliminar
		tlsCP.setDisableCNCheck(!siriSwatConfig.getBoolean("siriswat.cds.checkCN"));

		// setejem els paràmetres que hem creat
		conduit.setTlsClientParameters(tlsCP);

	}

	/**
	 * Crea el TrustManager
	 * @param trustStore
	 * 		Truststore que es posarà dins del trustManager
	 * @return
	 * 		Retorna el trustManager creat
	 * @throws NoSuchAlgorithmException
	 * @throws KeyStoreException
	 */
	private TrustManager[] getTrustManagers(KeyStore trustStore) throws NoSuchAlgorithmException, KeyStoreException {
		String alg = KeyManagerFactory.getDefaultAlgorithm();
		TrustManagerFactory fac = TrustManagerFactory.getInstance(alg);
		fac.init(trustStore);
		return fac.getTrustManagers();
	}

	/**
	 * Crea el KeyManager
	 * @param keyStore
	 * 		Keystore que es posarà dintre del keyManager
	 * @param keyPassword
	 * 		Password del Keystore
	 * @return
	 * 		Retorna el keyManager creat
	 * @throws GeneralSecurityException
	 * @throws IOException
	 */
	private KeyManager[] getKeyManagers(KeyStore keyStore, String keyPassword) throws GeneralSecurityException, IOException {
		String alg = KeyManagerFactory.getDefaultAlgorithm();
		char[] keyPass = keyPassword != null ? keyPassword.toCharArray() : null;
		KeyManagerFactory fac = KeyManagerFactory.getInstance(alg);
		fac.init(keyStore, keyPass);
		return fac.getKeyManagers();
	}

}
