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

import fr.asipsante.mss.connectkit.connection.Fault;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.HttpCookie;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.ssl.HttpsURLConnection;
import javax.swing.JOptionPane;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.asipsante.mss.connectkit.connection.HttpsConnectionFactory;
import fr.asipsante.mss.connectkit.connection.SSLSecurityException;

/**
 * Variante de protocole SAML2 pour authentification double par OTP. (introduit
 * le double appel à l'IDP nécessité par la séquence d'authentification OTP).
 * 
 * @author ASIP Santé
 *
 */
public class OTPAuthSaml2Driver extends AbstractSaml2Driver {

    private static final String TYPECANAL_HEADER_NAME = "TYPECANAL";

    /**
	 * 
	 */
    private static final String AUTHORIZATION_HEADER_NAME = "WWW-Authenticate";

    private static final int ROTTEN_AUTHFAILURE_CODE = 200;

    private static final Logger LOGGER = LoggerFactory
            .getLogger(OTPAuthSaml2Driver.class);

    private static final String NUMHOMOLOCATION_FIXED_VALUE = "0";
    private static final String NUMHOMOLOGATION_HEADER_NAME = "NUMHOMOLOGATION";
    private static final String IDCANAL_HEADER_NAME = "IDCANAL";
    private static final String PASSWORD_HEADER_NAME = "PASSWORD";
    private static final String IDNAT_HEADER_NAME = "IDNAT";
    private String idCanal;
    private String optCanalPassword;
    private String idNatPS;
    private TypeOTPCanal typeCanal;

    public void setIdCanal(String idCanal) {
        this.idCanal = idCanal;
        this.typeCanal = null;
    }

    public void setOptCanalPassword(String optCanalPassword) {
        this.optCanalPassword = optCanalPassword;
    }

    public void setIdNatPS(String idNatPS) {
        this.idNatPS = idNatPS;
    }

    public void setTypeCanal(TypeOTPCanal typeCanal) {
        this.typeCanal = typeCanal;
        this.idCanal = null;
    }

    private static final String NEXT_URL_REGEX = " *OTP nextUrl=(.*)";

    /**
     * Paramétrage de l'authentification OTP avec utilisation de l'ID de canal
     * OTP.
     * 
     * @param defaultEncoding
     * @param connectionFactory
     * @param authHostname
     * @param authPort
     * @param authPath
     * @param idOTPCanal
     * @param OTPCanalPassword
     * @param idNatPs
     * @throws IllegalArgumentException
     *             si idOTPCanal null
     */
    public OTPAuthSaml2Driver(String defaultEncoding,
            HttpsConnectionFactory connectionFactory, String authHostname,
            int authPort, String authPath, String idOTPCanal,
            String OTPCanalPassword, String idNatPs) {
        super(defaultEncoding, connectionFactory, authHostname, authPort,
                authPath);
        if (idOTPCanal == null) {
            throw new IllegalArgumentException("Id canal null");
        }
        idCanal = idOTPCanal;
        optCanalPassword = OTPCanalPassword;
        idNatPS = idNatPs;
    }

    /**
     * Paramétrage de l'authentification OTP avec utilisation du type de canal.
     * 
     * @param defaultEncoding
     * @param connectionFactory
     * @param authHostname
     * @param authPort
     * @param authPath
     * @param OTPCanalPassword
     * @param typeCanal
     * @throws IllegalArgumentException
     *             si id de canal null si typeCanal null
     */
    public OTPAuthSaml2Driver(String defaultEncoding,
            HttpsConnectionFactory connectionFactory, String authHostname,
            int authPort, String authPath, TypeOTPCanal typeCanal,
            String OTPCanalPassword, String idNatPs) {
        super(defaultEncoding, connectionFactory, authHostname, authPort,
                authPath);
        if (typeCanal == null) {
            throw new IllegalArgumentException("Type canal null");
        }
        optCanalPassword = OTPCanalPassword;
        idNatPS = idNatPs;
        this.typeCanal = typeCanal;
    }

/**
     * Préinstanciation pour l'utilisation Spring. Les identifiants de connexion
     * à l'IDP (idOTPCanal ou typeCanal + mot de passe canal et idNat
     * utilisateur) doivent être injectés avant la connexion (voir les setters
     * {@link #setIdCanal(java.lang.String) },
     * {@link #setTypeCanal(fr.asipsante.mss.connectkit.paos.TypeOTPCanal) ,
     * 
     * @link #setOptCanalPassword(java.lang.String) } et
     *       {@link #setIdNatPS(java.lang.String) }.
     * 
     * @param defaultEncoding
     * @param connectionFactory
     * @param authHostname
     * @param authPort
     * @param authPath
     * @throws IllegalArgumentException
     *             si id de canal null, si typeCanal null
     */
    public OTPAuthSaml2Driver(String defaultEncoding,
            HttpsConnectionFactory connectionFactory, String authHostname,
            int authPort, String authPath) {
        super(defaultEncoding, connectionFactory, authHostname, authPort,
                authPath);
    }

    @Override
    public byte[] getSamlAssertionFromAuthService(String sslProtocolSpec,
            byte[] xmlContent) throws Saml2ProtocolException, IOException,
            SSLSecurityException {
        HttpsURLConnection authConnection = createConnection(sslProtocolSpec);
        addConnectionHeaders(authConnection);

        int authResponseCode = getConnUtil().sendPost(authConnection,
                new ByteArrayInputStream(xmlContent));
        LOGGER.debug("Authentication service return code:" + authResponseCode);

        byte[] assertionResponse = null;

        if (ROTTEN_AUTHFAILURE_CODE == authResponseCode) {
            LOGGER.debug(new String(getConnUtil().getResponse(authConnection),
                    getDefaultEncoding()));
            authConnection.disconnect();
            throw new Saml2ProtocolException(
                    "Echec de l'authentification sur le canal OTP");
        } else if (authResponseCode == 401) {
            try {
                String authorizationHeader = authConnection
                        .getHeaderField(AUTHORIZATION_HEADER_NAME);
                if (null == authorizationHeader) {
                    StringBuilder headerData = new StringBuilder(
                            "Header existants:\n");
                    for (Entry<String, List<String>> header : authConnection
                            .getHeaderFields().entrySet()) {
                        headerData.append(header.getKey()).append(": ");
                        for (String value : header.getValue()) {
                            headerData.append(value).append("\n");
                        }
                    }
                    throw new Saml2ProtocolException("Manque le header: "
                            + AUTHORIZATION_HEADER_NAME + "\n"
                            + headerData.toString());
                }
                Set<HttpCookie> otpSessionCookies = extractOTPSessionCookies(authConnection);

                String nextURL = getNextURL(authorizationHeader);
                authConnection.disconnect();
                URL nextURLObject = new URL(authConnection.getURL(), nextURL);
                authConnection = getConnFactory().createHttpsConnection(
                        nextURLObject, sslProtocolSpec);// createConnection(sslProtocolSpec);
                authConnection.addRequestProperty("OTP", getOTPValue());
                getConnUtil().addCookies(authConnection, otpSessionCookies);
                getConnUtil().sendPost(authConnection,
                        new ByteArrayInputStream(xmlContent));
                assertionResponse = getConnUtil().getResponse(authConnection);
            } catch (URISyntaxException | Fault e) {
                throw new Saml2ProtocolException(
                        "Failed to get OTP session cookies", e);
            }
        } else {
            getConnUtil().getResponse(authConnection);
        }
        return assertionResponse;
    }

    // isolé et passé protected pour la testabilité (la regex doit être
    // validée...)
    protected String getNextURL(String authorizationHeader)
            throws Saml2ProtocolException {
        Pattern pattern = Pattern.compile(NEXT_URL_REGEX);
        Matcher nextURLextractor = pattern.matcher(authorizationHeader);
        String nextURL;
        if (nextURLextractor.find()) {
            nextURL = nextURLextractor.group(1);
        } else {
            throw new Saml2ProtocolException(
                    "Le header WWW-AUTHORIZATION n'est pas reconnu: '"
                            + authorizationHeader + "'");
        }
        return nextURL;
    }

    private String getOTPValue() {
        return JOptionPane.showInputDialog("Merci d'entrer l'OTP reçu.");
    }

    protected void addConnectionHeaders(HttpsURLConnection authConnection) {
        authConnection.addRequestProperty(IDNAT_HEADER_NAME, idNatPS);
        authConnection.addRequestProperty(PASSWORD_HEADER_NAME,
                optCanalPassword);
        if (idCanal == null) {
            authConnection.addRequestProperty(TYPECANAL_HEADER_NAME,
                    typeCanal.name());
        } else {
            authConnection.addRequestProperty(IDCANAL_HEADER_NAME, idCanal);
        }
        authConnection.addRequestProperty(NUMHOMOLOGATION_HEADER_NAME,
                NUMHOMOLOCATION_FIXED_VALUE);
    }

    /**
     * Récupération des cookies de load balancing en cours d'authentification.
     * 
     * @param connection
     * @return
     * @throws URISyntaxException
     * @throws IOException
     */
    protected Set<HttpCookie> extractOTPSessionCookies(
            HttpsURLConnection connection) throws URISyntaxException,
            IOException {
        HashSet<HttpCookie> result = new HashSet<HttpCookie>();
        for (HttpCookie cookie : getCookies(connection, new String[] {
                "amlbcookie", "AMAuthCookie" })) {
            if ("AMAuthCookie".equals(cookie.getName())) {
                result.add(cookie);
            } else if ("amlbcookie".equals(cookie.getName())) {
                result.add(cookie);
            }
        }
        return result;
    }

}
