/*
 *     Copyright (C) 2015 ASIP Santé
 */
package fr.asipsante.mss.connectkit.paos;

import java.io.IOException;
import java.net.CookieManager;
import java.net.HttpCookie;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import javax.net.ssl.HttpsURLConnection;
import javax.xml.parsers.ParserConfigurationException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import fr.asipsante.mss.connectkit.connection.ConnectionUtil;
import fr.asipsante.mss.connectkit.connection.HttpsConnectionFactory;
import fr.asipsante.mss.connectkit.connection.JsessionIdSearchCookiePolicy;
import fr.asipsante.mss.connectkit.connection.SSLSecurityException;
import fr.asipsante.mss.connectkit.xml.DOMBasedXmlProtocol;

/**
 * Tronc commun des différentes variantes du protocole d'authentification SAML2
 * / PAOS.
 * 
 * @author ASIP Santé
 * 
 */
public abstract class AbstractSaml2Driver extends DOMBasedXmlProtocol {

    /**
	 * 
	 */
    private static final String DEFAULT_ENCODING = "utf-8";

    private static final String SAML2P_URI = "urn:oasis:names:tc:SAML:2.0:protocol";
    public static final String SOAP_NS_URI = "http://schemas.xmlsoap.org/soap/envelope/";
    private static final Logger LOGGER = LoggerFactory
            .getLogger(AbstractSaml2Driver.class);

    private String authHostname;
    private int authPort;
    private String authPath;
    private String defaultEncoding;
    private HttpsConnectionFactory connFactory;

    private ConnectionUtil connUtil = new ConnectionUtil();

    protected ConnectionUtil getConnUtil() {
        return connUtil;
    }

    public AbstractSaml2Driver(String defaultEncoding,
            HttpsConnectionFactory connectionFactory, String authHostname,
            int authPort, String authPath) {
        this.defaultEncoding = defaultEncoding == null ? DEFAULT_ENCODING
                : defaultEncoding;
        if (authHostname == null) {
            throw new IllegalArgumentException("authHostname null interdit");
        }
        this.authHostname = authHostname;
        this.authPort = authPort == -1 ? 443 : authPort;
        if (authPath == null) {
            throw new IllegalArgumentException("authPath null interdit");
        }
        this.authPath = authPath;
        if (connectionFactory == null) {
            throw new IllegalArgumentException(
                    "connectionFactory null interdite");
        }
        this.connFactory = connectionFactory;
    }

    /**
     * Méthode appelée lors de l'authentification SAML2 pour récupérer le jeton
     * SAML2 du service d'authentification. Spécifiée par chaque variante
     * concrète de SAML2.
     * 
     * @param sslProtocolSpec
     * @param xmlContent
     * @return
     * @throws IOException
     * @throws Saml2ProtocolException
     * @throws SSLSecurityException
     */
    public abstract byte[] getSamlAssertionFromAuthService(
            String sslProtocolSpec, byte[] xmlContent) throws IOException,
            Saml2ProtocolException, SSLSecurityException;

    /**
     * Méthode pour configurer la connexion au service d'authentification.
     * 
     * @param sslProtocolSpec
     * @return
     * @throws IOException
     * @throws SSLSecurityException
     */
    protected HttpsURLConnection createConnection(String sslProtocolSpec)
            throws IOException, SSLSecurityException {
        HttpsURLConnection authConnection = getConnFactory()
                .createHttpsConnection(authHostname, authPort, authPath,
                        sslProtocolSpec);
        return authConnection;
    }

    public String getDefaultEncoding() {
        return defaultEncoding;
    }

    protected HttpsConnectionFactory getConnFactory() {
        return connFactory;
    }

    /**
     * Teste la présence d'une requête d'authentification SAML2 dans la réponse
     * du serveur métier, et l'extrait si elle existe.
     * 
     * @param reponseData
     * @return
     * @throws Saml2ProtocolException
     */
    public Node lookForAuthnRequest(byte[] reponseData)
            throws Saml2ProtocolException {

        Document responseDOM;
        try {
            responseDOM = parseRaw(reponseData);
        } catch (ParserConfigurationException e) {
            throw new Saml2ProtocolException(
                    "Echec de l'extraction de l'AuthnRequest", e);
        } catch (SAXException e) {
            throw new Saml2ProtocolException(
                    "Echec de l'extraction de l'AuthnRequest", e);
        } catch (IOException e) {
            throw new Saml2ProtocolException(
                    "Echec de l'extraction de l'AuthnRequest", e);
        }
        NodeList authnRequestList = responseDOM.getElementsByTagNameNS(
                SAML2P_URI, "AuthnRequest");
        Node requAuthn;
        if (authnRequestList.getLength() == 0) {
            requAuthn = null;
        } else if (authnRequestList.getLength() == 1) {
            requAuthn = authnRequestList.item(0);
        } else {
            throw new Saml2ProtocolException("Plusieurs AuthnRequest?");
        }

        return requAuthn;
    }

    public HttpCookie extractJSessionId(Map<String, List<String>> headers,
            URI refUri) throws IOException {
        List<HttpCookie> cookies = getCookies(new String[] { "JSESSIONID" },
                refUri, headers);
        return extractCookiesFromHeaderData(cookies);
    }

    /**
     * Récupère le cookie de session J2EE par la connexion s'il existe.
     * 
     * @param connection
     * @return
     * @throws URISyntaxException
     * @throws IOException
     */
    public HttpCookie extractJsessionId(HttpsURLConnection connection)
            throws URISyntaxException, IOException {
        List<HttpCookie> cookies = getCookies(connection,
                new String[] { "JSESSIONID" });
        return extractCookiesFromHeaderData(cookies);
    }

    private HttpCookie extractCookiesFromHeaderData(List<HttpCookie> cookies) {
        HttpCookie sessionId = null;
        for (HttpCookie cookie : cookies) {
            if ("JSESSIONID".equals(cookie.getName())) {
                sessionId = cookie;

            }
        }
        return sessionId;
    }

    /**
     * Logique de récupération de cookies par nom.
     * 
     * @param connection
     *            la connection potant les cookies.
     * @param targets
     *            les noms de cookies � rechercher.
     * @return
     * @throws URISyntaxException
     * @throws IOException
     */
    protected List<HttpCookie> getCookies(HttpsURLConnection connection,
            String[] targets) throws URISyntaxException, IOException {
        Map<String, List<String>> headerFields = connection.getHeaderFields();
        URI uri = connection.getURL().toURI();
        return getCookies(targets, uri, headerFields);
    }

    private List<HttpCookie> getCookies(String[] targets, URI uri,
            Map<String, List<String>> headerFields) throws IOException {
        CookieManager manager = new CookieManager();
        manager.setCookiePolicy(new JsessionIdSearchCookiePolicy(Arrays
                .asList(targets)));
        manager.put(uri, headerFields);
        List<HttpCookie> cookies = manager.getCookieStore().getCookies();
        return cookies;
    }

    /**
     * Extrait l'URL du service consommateur du jeton SAML2, qui est portée par
     * la requête d'authentification SAML2 (AuthnRequest).
     * 
     * @param requAuthn
     * @return
     * @throws Saml2ProtocolException
     * @throws MalformedURLException
     */
    public URL getAssertionConsumerURL(Node requAuthn)
            throws Saml2ProtocolException, MalformedURLException {
        URL assertionConsumer = null;
        NamedNodeMap attributeMap = requAuthn.getAttributes();
        Node consumerServiceAttribute = attributeMap
                .getNamedItem("AssertionConsumerServiceURL");
        if (consumerServiceAttribute == null) {
            throw new Saml2ProtocolException("Missing assertion consumer URL");
        } else {
            assertionConsumer = new URL(consumerServiceAttribute.getNodeValue());
        }
        return assertionConsumer;
    }

    /**
     * Méthode qui produit l'enveloppe de requête transmise au service
     * d'authentification à partir de la réponse du service métier contenant
     * l'AuthnRequest.
     * 
     * @param reponseData
     * @return
     * @throws Saml2ProtocolException
     */
    public byte[] createAuthMessage(byte[] reponseData)
            throws Saml2ProtocolException {
        try {
            Document message = parseRaw(reponseData);
            String encoding = message.getXmlEncoding();
            if (encoding == null) {
                encoding = this.defaultEncoding;
            }
            LOGGER.debug("Encoding: " + encoding);
            NodeList headerList = message.getElementsByTagNameNS(
                    AbstractSaml2Driver.SOAP_NS_URI, "Header");

            Node candidat = headerList.item(0);
            LOGGER.debug("Candidat pour SOAP Header: " + candidat.getNodeName());

            String prefix = candidat.getPrefix();
            String nomComplet = prefix == null ? "Header" : prefix + ":"
                    + "Header";
            String contentString = new String(reponseData, encoding);
            int headerbegin = contentString.indexOf("<" + nomComplet);
            String closetag = "</" + nomComplet + ">";
            int headerEnd = contentString.indexOf(closetag) + closetag.length();
            String messageContent = contentString.substring(0, headerbegin)
                    + "<" + nomComplet + "></" + nomComplet + ">"
                    + contentString.substring(headerEnd);
            return messageContent.getBytes(encoding);
        } catch (ParserConfigurationException e) {
            throw new Saml2ProtocolException(
                    "Echec de la cr�ation d'enveloppe de requ�te de jeton SAML pour l'IDP",
                    e);
        } catch (SAXException e) {
            throw new Saml2ProtocolException(
                    "Echec de la cr�ation d'enveloppe de requ�te de jeton SAML pour l'IDP",
                    e);
        } catch (IOException e) {
            throw new Saml2ProtocolException(
                    "Echec de la cr�ation d'enveloppe de requ�te de jeton SAML pour l'IDP",
                    e);
        }
    }

    /**
     * Point d'appel pour ajout des headers de connexion, spécifié par chaque
     * variante de protocole SAML2.
     * 
     * @param authConnection
     */
    protected abstract void addConnectionHeaders(
            HttpsURLConnection authConnection);
}