/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.products.ov.commons.infrastructure.remote.filter;

import java.io.BufferedReader;
import java.io.IOException;
import java.security.PublicKey;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Response;

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

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.products.ov.commons.infrastructure.exception.OvCommonsInfrastructureException;
import com.scytl.products.ov.commons.infrastructure.remote.client.NodeIdentifier;
import com.scytl.products.ov.commons.infrastructure.remote.client.RestClientInterceptor;
import com.scytl.products.ov.commons.sign.beans.SignedRequestContent;
import com.scytl.products.ov.commons.verify.RequestVerifier;

/**
 * Filter to check Signature inside every request.
 */
public final class SignedRequestFilter implements Filter {

    private static final Logger LOG = LoggerFactory.getLogger("std");

    private SignedRequestKeyManager signedRequestKeyManager;

    private String urlRegularExpression;

    /**
     * @see Filter#init(FilterConfig)
     */
    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {

        LOG.info("Initializing filter - name: " + filterConfig.getFilterName() + ", path: "
            + filterConfig.getServletContext().getContextPath() + ", container: "
            + filterConfig.getServletContext().getServerInfo());

        this.urlRegularExpression = filterConfig.getInitParameter("urlRegularExpression");

        try {
            signedRequestKeyManager = SignedRequestKeyManager.getInstance();

        } catch (OvCommonsInfrastructureException e) {
            throw new ServletException("Something went wrong when creating the filter: " + e, e);
        }
    }
    
    @Override
    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain filterChain)
            throws IOException, ServletException {

        LOG.debug("Filtering request to verify Originator signature...");

        String uri;

        if (request instanceof HttpServletRequest) {
            uri = ((HttpServletRequest) request).getRequestURI();
            LOG.debug("URL: " + uri);
        } else {
            LOG.info("The request is not a HttpServletRequest. Aborting");
            return;
        }

        HttpServletResponse httpServletResponse;
        if (response instanceof HttpServletResponse) {
            httpServletResponse = (HttpServletResponse) response;
        } else {
            LOG.info("The response is not the expected type. Expected a HttpServletResponse. Aborting.");
            return;
        }

        if (applySignatureVerificationToUrl(uri)) {

            // Convert original request...
            /* wrap the request in order to read the inputStream multiple times */
            MultiReadHttpServletRequest multiReadRequest =
                new MultiReadHttpServletRequest((HttpServletRequest) request);

            // Check 'Originator' header
            NodeIdentifier nodeIdentifier;
            try {
                String originatorStr = multiReadRequest.getHeader(RestClientInterceptor.HEADER_ORIGINATOR);
                nodeIdentifier = NodeIdentifier.valueOf(originatorStr);

            } catch (IllegalArgumentException e) {
                LOG.warn("The request has not identified Originator or it is invalid. Aborting", e);
                httpServletResponse.setStatus(Response.Status.UNAUTHORIZED.getStatusCode());
                return;
            } catch (NullPointerException e) {
                LOG.warn("The request has not identified Originator or it is invalid. Aborting",e );
                httpServletResponse.setStatus(Response.Status.UNAUTHORIZED.getStatusCode());
                return;
            }

            if (signatureIsValid(multiReadRequest, nodeIdentifier)) {
                LOG.info("Request signature in headers is VALID. Processing...");
                // PROCEED WITH THE REQUEST
                filterChain.doFilter(multiReadRequest, response);
                return;

            } else {
                LOG.warn("Request signature in headers is INVALID. Aborting...");
                httpServletResponse.setStatus(Response.Status.UNAUTHORIZED.getStatusCode());
                return;
            }
        } else {
            LOG.debug("URL not need to be filtered. Processing...");
            // PROCEED WITH THE REQUEST
            filterChain.doFilter(request, response);
            return;
        }
    }

    private boolean signatureIsValid(final MultiReadHttpServletRequest multiReadHttpServletRequest,
            final NodeIdentifier nodeIdentifier) throws IOException {

        LOG.debug("Retrieving Originator public key... ");

        PublicKey publicKey;
        try {
            publicKey = getPublicKey(nodeIdentifier);
        } catch (OvCommonsInfrastructureException e) {
            LOG.warn("Error obtaining the public key.", e);
            return false;
        }

        LOG.debug("Validating signature... ");

        String signatureStr = multiReadHttpServletRequest.getHeader(RestClientInterceptor.HEADER_SIGNATURE);

        if (signatureStr == null) {
            LOG.warn("The Signature is empty in this request.");
            return false;
        }

        byte[] signature;
        try {
            signature = Base64.getDecoder().decode(signatureStr);
        } catch (IllegalArgumentException e) {
            LOG.warn("The Signature is malformed.", e);
            return false;
        }

        RequestVerifier requestVerifier = new RequestVerifier();

        String body = getRequestBodyToString(multiReadHttpServletRequest);

        SignedRequestContent signedRequestContent =
            new SignedRequestContent(multiReadHttpServletRequest.getRequestURI(),
                multiReadHttpServletRequest.getMethod(), body, nodeIdentifier.name());

        boolean result = false;

        try {
            result = requestVerifier.verifySignature(signedRequestContent, signature, publicKey);

        } catch (GeneralCryptoLibException e) {
            LOG.error("Error validating signature: " + e.getMessage(), e);
            return false;
        }
        return result;
    }

    @Override
    public void destroy() {
        LOG.info("destroying SignedRequestFilter");
    }

    private PublicKey getPublicKey(final NodeIdentifier nodeIdentifier) throws OvCommonsInfrastructureException {

        return signedRequestKeyManager.getPublicKeyFromOriginator(nodeIdentifier);
    }

    /**
     * Reads the request body from the request and returns it as a String.
     *
     * @param multiReadHttpServletRequest
     *            HttpServletRequest that contains the request body
     * @return request body as a String or null
     */
    private String getRequestBodyToString(final MultiReadHttpServletRequest multiReadHttpServletRequest) {
        try {
            // Read from request
            StringBuilder buffer = new StringBuilder();
            BufferedReader reader = multiReadHttpServletRequest.getReader();
            String line;
            while ((line = reader.readLine()) != null) {
                buffer.append(line);
            }
            return buffer.toString();
        } catch (Exception e) {
            LOG.error("Failed to read the request body from the request.", e);
        }
        return null;
    }

    /**
     * Checks the URL against the regular expression to apply the filter.
     * 
     * @param url
     * @return
     */
    private boolean applySignatureVerificationToUrl(String url) {

        if (this.urlRegularExpression == null)
            return false;

        Pattern p = Pattern.compile(this.urlRegularExpression);
        Matcher m = p.matcher(url);
        
        return m.matches();
    }
}
