package nl.finalist.liferay.oidc;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.oltu.oauth2.client.OAuthClient;
import org.apache.oltu.oauth2.client.URLConnectionClient;
import org.apache.oltu.oauth2.client.request.OAuthBearerClientRequest;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest;
import org.apache.oltu.oauth2.client.response.OAuthResourceResponse;
import org.apache.oltu.oauth2.common.OAuth;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.types.GrantType;

/**
 * Servlet filter that initiates OpenID Connect logins, and handles the resulting flow, until and including the
 * UserInfo request. It saves the UserInfo in a session attribute, to be examined by an AutoLogin.
 */
public class LibFilter  {

    public static final String REQ_PARAM_CODE = "code";
    public static final String REQ_PARAM_STATE = "state";

    /**
     * Property that is used to configure whether to enable OpenID Connect auth
     */
    public static final String PROPKEY_ENABLE_OPEN_IDCONNECT = "openidconnect.enableOpenIDConnect";
    
    public enum FilterResult {
    	CONTINUE_CHAIN, 
    	BREAK_CHAIN;
    }
    
    
    /**
     * Session attribute name containing the UserInfo
     */
    public static final String OPENID_CONNECT_SESSION_ATTR = "OpenIDConnectUserInfo";

  
    private final LiferayAdapter liferay;

    public LibFilter(LiferayAdapter liferay) {
        this.liferay = liferay;
    }


    /**
     * Filter the request. 
     * <br><br>LOGIN:<br> 
     * The first time this filter gets hit, it will redirect to the OP.
     * Second time it will expect a code and state param to be set, and will exchange the code for an access token.
     * Then it will request the UserInfo given the access token.
     * <br>--
     * Result: the OpenID Connect 1.0 flow.
     * <br><br>LOGOUT:<br>
     * When the filter is hit and according values for SSO logout are set, it will redirect to the OP logout resource.
     * From there the request should be redirected "back" to a public portal page or the public portal home page. 
     *
     * @param request the http request
     * @param response the http response
     * @param filterChain the filterchain
     * @throws Exception according to interface.
     * @return FilterResult, to be able to distinct between continuing the chain or breaking it.
     */
    protected FilterResult processFilter(HttpServletRequest request, HttpServletResponse response, FilterChain 
            filterChain) throws Exception {

        OIDCConfiguration oidcConfiguration = liferay.getOIDCConfiguration(liferay.getCompanyId(request));

        // If the plugin is not enabled, short circuit immediately
        if (!oidcConfiguration.isEnabled()) {
            liferay.trace("OpenIDConnectFilter not enabled for this virtual instance. Will skip it.");
            return FilterResult.CONTINUE_CHAIN;
        }

        liferay.trace("In processFilter()...");

		String pathInfo = request.getPathInfo();

		if (null != pathInfo) {
			if (pathInfo.contains("/portal/login")) {
		        if (!StringUtils.isBlank(request.getParameter(REQ_PARAM_CODE))
		                && !StringUtils.isBlank(request.getParameter(REQ_PARAM_STATE))) {

		            if (!isUserLoggedIn(request)) {
		                // LOGIN: Second time it will expect a code and state param to be set, and will exchange the code for an access token.
		                liferay.trace("About to exchange code for access token");
		                exchangeCodeForAccessToken(request);
		            } else {
		                liferay.trace("subsequent run into filter during openid conversation, but already logged in." +
		                        "Will not exchange code for token twice.");
		            }
		        } else {
		        	// LOGIN: The first time this filter gets hit, it will redirect to the OP.
		            liferay.trace("About to redirect to OpenID Provider");
		            redirectToLogin(request, response, oidcConfiguration.clientId());
		            // no continuation of the filter chain; we expect the redirect to commence.
		            return FilterResult.BREAK_CHAIN;
		        }
			} 
			else
			if (pathInfo.contains("/portal/logout")) {
                final String ssoLogoutUri = oidcConfiguration.ssoLogoutUri();
                final String ssoLogoutParam = oidcConfiguration.ssoLogoutParam();
                final String ssoLogoutValue = oidcConfiguration.ssoLogoutValue();
                if (null != ssoLogoutUri && ssoLogoutUri.length
                    () > 0 && isUserLoggedIn(request)) {
					
					liferay.trace("About to logout from SSO by redirect to " + ssoLogoutUri);
			        // LOGOUT: If Portal Logout URL is requested, redirect to OIDC Logout resource afterwards to globally logout.
			        // From there, the request should be redirected back to the Liferay portal home page.
					request.getSession().invalidate();
					redirectToLogout(request, response, ssoLogoutUri, ssoLogoutParam, ssoLogoutValue);
		            // no continuation of the filter chain; we expect the redirect to commence.
		            return FilterResult.BREAK_CHAIN;
				}
			}
		}
        // continue chain
		return FilterResult.CONTINUE_CHAIN;

    }

    protected void exchangeCodeForAccessToken(HttpServletRequest request) throws IOException {
        OIDCConfiguration oidcConfiguration = liferay.getOIDCConfiguration(liferay.getCompanyId(request));

        try {
            String codeParam = request.getParameter(REQ_PARAM_CODE);
            String stateParam = request.getParameter(REQ_PARAM_STATE);

            String expectedState = generateStateParam(request);
            if (!expectedState.equals(stateParam)) {
                liferay.info("Provided state parameter '" + stateParam + "' does not equal expected '"
                        + expectedState + "', cannot continue.");
                throw new IOException("Invalid state parameter");
            }

            OAuthClientRequest tokenRequest = OAuthClientRequest.tokenLocation(oidcConfiguration.tokenLocation())
                    .setGrantType(GrantType.AUTHORIZATION_CODE)
                    .setClientId(oidcConfiguration.clientId())
                    .setClientSecret(oidcConfiguration.secret())
                    .setCode(codeParam)
                    .setRedirectURI(getRedirectUri(request))
                    .buildBodyMessage();
            liferay.debug("Token request to uri: " + tokenRequest.getLocationUri());

            OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
            OpenIdConnectResponse oAuthResponse = oAuthClient.accessToken(tokenRequest, OpenIdConnectResponse.class);
            liferay.trace("Access/id token response: " + oAuthResponse);
            String accessToken = oAuthResponse.getAccessToken();

            if (!oAuthResponse.checkId(oidcConfiguration.issuer(), oidcConfiguration.clientId())) {
                liferay.warn("The token was not valid: " + oAuthResponse.toString());
                return;
            }

            // The only API to be enabled (in case of Google) is Google+.
            OAuthClientRequest userInfoRequest = new OAuthBearerClientRequest(oidcConfiguration.profileUri())
                    .setAccessToken(accessToken).buildHeaderMessage();
            liferay.trace("UserInfo request to uri: " + userInfoRequest.getLocationUri());
            OAuthResourceResponse userInfoResponse =
                    oAuthClient.resource(userInfoRequest, OAuth.HttpMethod.GET, OAuthResourceResponse.class);

            liferay.debug("Response from UserInfo request: " + userInfoResponse.getBody());
            Map openIDUserInfo = new ObjectMapper().readValue(userInfoResponse.getBody(), HashMap.class);

            liferay.debug("Setting OpenIDUserInfo object in session: " + openIDUserInfo);
            request.getSession().setAttribute(OPENID_CONNECT_SESSION_ATTR, openIDUserInfo);

        } catch (OAuthSystemException | OAuthProblemException e) {
            throw new IOException("While exchanging code for access token and retrieving user info", e);
        }
    }

    protected void redirectToLogin(HttpServletRequest request, HttpServletResponse response, String clientId) throws
            IOException {
        OIDCConfiguration oidcConfiguration = liferay.getOIDCConfiguration(liferay.getCompanyId(request));

        try {
            OAuthClientRequest oAuthRequest = OAuthClientRequest
                    .authorizationLocation(oidcConfiguration.authorizationLocation())
                    .setClientId(clientId)
                    .setRedirectURI(getRedirectUri(request))
                    .setResponseType("code")
                    .setScope(oidcConfiguration.scope())
                    .setState(generateStateParam(request))
                    .buildQueryMessage();
            liferay.debug("Redirecting to URL: " + oAuthRequest.getLocationUri());
            response.sendRedirect(oAuthRequest.getLocationUri());
        } catch (OAuthSystemException e) {
            throw new IOException("While redirecting to OP for SSO login", e);
        }
    }
    
    protected void redirectToLogout(HttpServletRequest request, HttpServletResponse response, 
    		String logoutUrl, String logoutUrlParamName, String logoutUrlParamValue) throws
    		IOException {
			// build logout URL and append params if present
			if (StringUtils.isNotEmpty(logoutUrlParamName) && StringUtils.isNotEmpty(logoutUrlParamValue)) {
				logoutUrl = addParameter(logoutUrl, logoutUrlParamName, logoutUrlParamValue);
			}
			liferay.debug("On " + request.getRequestURL() + " redirect to OP for SSO logout: " + logoutUrl);
			response.sendRedirect(logoutUrl);
    }

    protected String getRedirectUri(HttpServletRequest request) {
        String completeURL = liferay.getCurrentCompleteURL(request);
        // remove parameters
        return completeURL.replaceAll("\\?.*", "");
    }

    protected String generateStateParam(HttpServletRequest request) {
        return DigestUtils.md5Hex(request.getSession().getId());
    }
    
    protected boolean isUserLoggedIn(HttpServletRequest request) {
        return liferay.isUserLoggedIn(request);
    }

    protected String addParameter(String url, String param, String value) {
    	String anchor = "";
    	int posOfAnchor = url.indexOf('#');
    	if (posOfAnchor > -1) {
    		anchor = url.substring(posOfAnchor);
    		url = url.substring(0, posOfAnchor);
    	}
    	
		StringBuffer sb = new StringBuffer();
		sb.append(url);
		if (url.indexOf('?') < 0) {
			sb.append('?');
		} else 
		if (!url.endsWith("?") && !url.endsWith("&")) {
			sb.append('&');
		}
		sb.append(param);
		sb.append('=');
		try {
			sb.append(URLEncoder.encode(value, StandardCharsets.UTF_8.toString()));
		} catch (UnsupportedEncodingException e) {
			sb.append(value);
		}
		sb.append(anchor);

    	return sb.toString() + anchor;
    }

}