/*
 * Copyright (c) 2018, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.wso2.identity.scenarios.commons.util;

import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.wso2.carbon.identity.application.common.model.xsd.Claim;
import org.wso2.carbon.identity.application.common.model.xsd.ClaimMapping;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;

import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.apache.http.HttpHeaders.CONTENT_TYPE;
import static org.wso2.identity.scenarios.commons.util.Constants.APPROVE_ALWAYS;
import static org.wso2.identity.scenarios.commons.util.Constants.APPROVE_ONCE;
import static org.wso2.identity.scenarios.commons.util.Constants.CONTENT_TYPE_APPLICATION_FORM;
import static org.wso2.identity.scenarios.commons.util.Constants.COOKIE;
import static org.wso2.identity.scenarios.commons.util.Constants.ClaimURIs.EMAIL_CLAIM_URI;
import static org.wso2.identity.scenarios.commons.util.Constants.ClaimURIs.FIRST_NAME_CLAIM_URI;
import static org.wso2.identity.scenarios.commons.util.Constants.ClaimURIs.LAST_NAME_CLAIM_URI;
import static org.wso2.identity.scenarios.commons.util.Constants.DENY;
import static org.wso2.identity.scenarios.commons.util.Constants.GRANT_TYPE_AUTHORIZATION_CODE;
import static org.wso2.identity.scenarios.commons.util.Constants.HTTP_RESPONSE_HEADER_LOCATION;
import static org.wso2.identity.scenarios.commons.util.Constants.PARAM_CLIENT_ID;
import static org.wso2.identity.scenarios.commons.util.Constants.PARAM_CODE;
import static org.wso2.identity.scenarios.commons.util.Constants.PARAM_CONSENT;
import static org.wso2.identity.scenarios.commons.util.Constants.PARAM_GRANT_TYPE;
import static org.wso2.identity.scenarios.commons.util.Constants.PARAM_MANDATORY_CLAIMS;
import static org.wso2.identity.scenarios.commons.util.Constants.PARAM_PASSWORD;
import static org.wso2.identity.scenarios.commons.util.Constants.PARAM_REDIRECT_URI;
import static org.wso2.identity.scenarios.commons.util.Constants.PARAM_REQUESTED_CLAIMS;
import static org.wso2.identity.scenarios.commons.util.Constants.PARAM_RESPONSE_TYPE;
import static org.wso2.identity.scenarios.commons.util.Constants.PARAM_SCOPE;
import static org.wso2.identity.scenarios.commons.util.Constants.PARAM_SESSION_DATA_KEY;
import static org.wso2.identity.scenarios.commons.util.Constants.PARAM_SESSION_DATA_KEY_CONSENT;
import static org.wso2.identity.scenarios.commons.util.Constants.PARAM_USERNAME;
import static org.wso2.identity.scenarios.commons.util.DataExtractUtil.getQueryParams;
import static org.wso2.identity.scenarios.commons.util.DataExtractUtil.getRedirectUrlFromResponse;
import static org.wso2.identity.scenarios.commons.util.IdentityScenarioUtil.sendGetRequest;
import static org.wso2.identity.scenarios.commons.util.IdentityScenarioUtil.sendPostRequestWithParameters;

/**
 * Utility class for SSO related functions.
 */
public class SSOUtil {

    /**
     * Sent OAuth authorization request.
     *
     * @param client        HttpClient to be used for request sending.
     * @param authzEndpoint Authorization endpoint URL.
     * @param clientId      Client ID of the application.
     * @param redirectUri   Redirect URI of the application.
     * @param scope         Authorization request scopes.
     * @param params        Additional request parameters.
     * @return HttpResponse with the authorization response.
     * @throws Exception If error occurs while sending the authorize request.
     */
    public static HttpResponse sendAuthorizeGet(HttpClient client, String authzEndpoint, String clientId,
            String redirectUri, String scope, Map<String, String> params) throws Exception {

        Map<String, String> requestParams = new HashMap<>();
        requestParams.put(PARAM_RESPONSE_TYPE, PARAM_CODE);
        requestParams.put(PARAM_CLIENT_ID, clientId);
        requestParams.put(PARAM_REDIRECT_URI, redirectUri);
        if (isNotBlank(scope)) {
            requestParams.put(PARAM_SCOPE, redirectUri);
        }
        if (params != null) {
            requestParams.putAll(params);
        }
        return sendGetRequest(client, authzEndpoint, requestParams);
    }

    /**
     * Retrieves SessionDataConsentKey from OAuth consent page.
     *
     * @param response HttpResponse with OAuth consent page.
     * @return SessionDataConsentKey of the corresponding consent request
     * @throws IOException If error occurs while extracting SessionDataConsentKey.
     */
    public static String getSessionDataConsentKeyFromConsentPage(HttpResponse response) throws IOException {

        Map<String, Integer> keyPositionMap = new HashMap<>(1);
        keyPositionMap.put("name=\"" + PARAM_SESSION_DATA_KEY_CONSENT + "\"", 1);
        List<DataExtractUtil.KeyValue> keyValues = DataExtractUtil
                .extractSessionConsentDataFromResponse(response, keyPositionMap);

        if (keyValues.get(0) != null) {
            return keyValues.get(0).getValue();
        } else {
            return null;
        }
    }

    /**
     * Submits user credentials at SSO basic auth login page.
     *
     * @param client                HttpClient to be used for request sending.
     * @param sessionDataKey        Session key related to the authentication request.
     * @param commonauthEndpointUrl IS commonauth endpoint URL.
     * @param username              Authenticating username.
     * @param password              Authenticating user password.
     * @return @return HttpResponse with the credential submit response.
     * @throws IOException If error occurs while credential submit.
     */
    public static HttpResponse sendLoginPost(HttpClient client, String sessionDataKey, String commonauthEndpointUrl,
            String username, String password) throws IOException {

        List<NameValuePair> urlParameters = getBasicLoginParams(sessionDataKey, username, password);
        return sendPostRequestWithParameters(client, urlParameters, commonauthEndpointUrl, null);
    }

    /**
     * Submits user credentials at SSO basic auth login page.
     *
     * @param client         HttpClient to be used for request sending.
     * @param sessionDataKey Session key related to the authentication request.
     * @param endpointUrl    IS login endpoint URL.
     * @param username       Authenticating username.
     * @param password       Authenticating user password.
     * @return @return HttpResponse with the credential submit response.
     * @throws IOException If error occurs while credential submit.
     */
    public static HttpResponse sendLoginPostWithParamsAndHeaders(HttpClient client, String sessionDataKey,
            String endpointUrl, String username, String password, Map<String, String> params, Header[] headers)
            throws IOException {

        List<NameValuePair> urlParameters = getBasicLoginParams(sessionDataKey, username, password);
        if (!params.isEmpty()) {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                urlParameters.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
            }
        }
        return sendPostRequestWithParameters(client, urlParameters, endpointUrl, headers);
    }

    private static List<NameValuePair> getBasicLoginParams(String sessionDataKey, String username, String password) {
        List<NameValuePair> urlParameters = new ArrayList<>();
        urlParameters.add(new BasicNameValuePair(PARAM_USERNAME, username));
        urlParameters.add(new BasicNameValuePair(PARAM_PASSWORD, password));
        urlParameters.add(new BasicNameValuePair(PARAM_SESSION_DATA_KEY, sessionDataKey));
        return urlParameters;
    }

    /**
     * Sends OAuth consent submit request. This consent will be an 'Approve Once' submit and only for OAuth and not
     * for user attribute sharing related consent.
     *
     * @param client                HttpClient to be used for request sending.
     * @param sessionDataKeyConsent Session key related to the consent request.
     * @param authzEndpointUrl      Authorization endpoint URL.
     * @return @return HttpResponse with the consent submit response.
     * @throws IOException If error occurs while sending the consent submit.
     */
    public static HttpResponse sendOAuthConsentApproveOncePost(HttpClient client, String sessionDataKeyConsent,
            String authzEndpointUrl) throws IOException {

        return sendOAuthConsentPost(client, sessionDataKeyConsent, authzEndpointUrl, APPROVE_ONCE);
    }

    /**
     * Sends OAuth consent submit request. This consent will be an 'Approve Always' submit and only for OAuth and not
     * for user attribute sharing related consent.
     *
     * @param client                HttpClient to be used for request sending.
     * @param sessionDataKeyConsent Session key related to the consent request.
     * @param authzEndpointUrl      Authorization endpoint URL.
     * @return @return HttpResponse with the consent submit response.
     * @throws IOException If error occurs while sending the consent submit.
     */
    public static HttpResponse sendOAuthConsentApproveAlwaysPost(HttpClient client, String sessionDataKeyConsent,
            String authzEndpointUrl) throws IOException {

        return sendOAuthConsentPost(client, sessionDataKeyConsent, authzEndpointUrl, APPROVE_ALWAYS);
    }

    /**
     * Sends OAuth consent submit request. This consent will be a 'Deny' submit and only for OAuth and not
     * for user attribute sharing related consent.
     *
     * @param client                HttpClient to be used for request sending.
     * @param sessionDataKeyConsent Session key related to the consent request.
     * @param authzEndpointUrl      Authorization endpoint URL.
     * @return @return HttpResponse with the consent submit response.
     * @throws IOException If error occurs while sending the consent submit.
     */
    public static HttpResponse sendOAuthConsentDenyPost(HttpClient client, String sessionDataKeyConsent,
            String authzEndpointUrl) throws IOException {

        return sendOAuthConsentPost(client, sessionDataKeyConsent, authzEndpointUrl, DENY);
    }

    /**
     * Sends OAuth consent submit request. This consent will only for OAuth and not for user attribute sharing
     * related consent.
     *
     * @param client                HttpClient to be used for request sending.
     * @param sessionDataKeyConsent Session key related to the consent request.
     * @param authzEndpointUrl      Authorization endpoint URL.
     * @param consentInput          OAuth consent input. Applicable values: approve, approveAlways, deny.
     * @return @return HttpResponse with the consent submit response.
     * @throws IOException If error occurs while sending the consent submit.
     */
    public static HttpResponse sendOAuthConsentPost(HttpClient client, String sessionDataKeyConsent,
            String authzEndpointUrl, String consentInput) throws IOException {

        List<NameValuePair> urlParameters = new ArrayList<>();
        urlParameters.add(new BasicNameValuePair(PARAM_CONSENT, consentInput));
        urlParameters.add(new BasicNameValuePair(PARAM_SESSION_DATA_KEY_CONSENT, sessionDataKeyConsent));

        return sendPostRequestWithParameters(client, urlParameters, authzEndpointUrl, null);
    }

    /**
     * Sends OAuth token request.
     *
     * @param client        HttpClient to be used for request sending.
     * @param authzCode     Authorization code.
     * @param tokenEndpoint Token endpoint URL.
     * @param clientId      Client ID of the application.
     * @param clientSecret  Client secret of the application.
     * @param redirectUri   Redirect URI of the application.
     * @param scope         Token request scopes.
     * @param params        Additional request parameters.
     * @return HttpResponse with the token response.
     * @throws IOException If error occurs while sending the token request.
     */
    public static HttpResponse sendTokenRequest(HttpClient client, String authzCode, String tokenEndpoint,
            String clientId, String clientSecret, String redirectUri, String scope, Map<String, String> params)
            throws IOException {

        Header authzHeader = new BasicHeader(HttpHeaders.AUTHORIZATION,
                IdentityScenarioUtil.constructBasicAuthzHeader(clientId, clientSecret));
        Header contentTypeHeader = new BasicHeader(CONTENT_TYPE, CONTENT_TYPE_APPLICATION_FORM);
        List<NameValuePair> requestParams = new ArrayList<>();
        requestParams.add(new BasicNameValuePair(PARAM_GRANT_TYPE, GRANT_TYPE_AUTHORIZATION_CODE));
        requestParams.add(new BasicNameValuePair(PARAM_CODE, authzCode));
        requestParams.add(new BasicNameValuePair(PARAM_REDIRECT_URI, redirectUri));

        if (isNotBlank(scope)) {
            requestParams.add(new BasicNameValuePair(PARAM_SCOPE, scope));
        }
        if (params != null) {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                requestParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
            }
        }

        return sendPostRequestWithParameters(client, requestParams, tokenEndpoint, new Header[] {
                authzHeader, contentTypeHeader
        });
    }

    /**
     * Send redirect request
     *
     * @param response   HttpResponse to be referred for request sending.
     * @param userAgent  User-Agent
     * @param referrer   Referer Url
     * @param httpClient HttpClient to be used for request sending.
     * @return HttpResponse after redirect
     * @throws IOException
     */
    public static HttpResponse sendRedirectRequest(HttpResponse response, String userAgent, String referrer,
                                                   HttpClient httpClient) throws IOException, URISyntaxException {
        String url = getRedirectUrlFromResponse(response);
        Header[] headers = new Header[2];
        headers[0] = new BasicHeader(HttpHeaders.USER_AGENT, userAgent);
        headers[1] = new BasicHeader(HttpHeaders.REFERER, referrer);
        return sendGetRequest(httpClient, url, null, headers);
    }

    /**
     * Post user consent to Consent page
     *
     * @param response      HttpResponse to be referred for request sending.
     * @param commonAuthUrl commonauth URL
     * @param userAgent     user-Agent
     * @param referer       referer URL
     * @param httpClient    HttpClient to be used for request sending.
     * @param pastreCookie  'pastre' Cookie
     * @return response after submitting consent
     * @throws Exception
     */
    public static HttpResponse sendPOSTConsentMessage(HttpResponse response, String commonAuthUrl, String userAgent,
            String referer, HttpClient httpClient, String pastreCookie) throws Exception {
        String redirectUrl = getRedirectUrlFromResponse(response);
        Map<String, String> queryParams = getQueryParams(redirectUrl);

        String sessionKey = queryParams.get(PARAM_SESSION_DATA_KEY);
        String mandatoryClaims = queryParams.get(PARAM_MANDATORY_CLAIMS);
        String requestedClaims = queryParams.get(PARAM_REQUESTED_CLAIMS);
        String consentRequiredClaims;

        if (isNotBlank(mandatoryClaims) && isNotBlank(requestedClaims)) {
            StringJoiner joiner = new StringJoiner(",");
            joiner.add(mandatoryClaims);
            joiner.add(requestedClaims);
            consentRequiredClaims = joiner.toString();
        } else if (isNotBlank(mandatoryClaims)) {
            consentRequiredClaims = mandatoryClaims;
        } else {
            consentRequiredClaims = requestedClaims;
        }

        String[] claims;
        if (isNotBlank(consentRequiredClaims)) {
            claims = consentRequiredClaims.split(",");
        } else {
            claims = new String[0];
        }

        HttpPost post = new HttpPost(commonAuthUrl);
        post.setHeader(HttpHeaders.USER_AGENT, userAgent);
        post.addHeader(HttpHeaders.REFERER, referer);
        post.addHeader(COOKIE, pastreCookie);
        List<NameValuePair> urlParameters = new ArrayList<>();

        for (int i = 0; i < claims.length; i++) {

            if (isNotBlank(claims[i])) {
                String[] claimMeta = claims[i].split("_", 2);
                if (claimMeta.length == 2) {
                    urlParameters.add(new BasicNameValuePair("consent_" + claimMeta[0], "on"));
                }
            }
        }
        urlParameters.add(new BasicNameValuePair(PARAM_SESSION_DATA_KEY, sessionKey));
        urlParameters.add(new BasicNameValuePair("consent", "approve"));
        post.setEntity(new UrlEncodedFormEntity(urlParameters));
        return httpClient.execute(post);
    }

    public static ClaimMapping[] getClaimMappings() {

        List<ClaimMapping> claimMappingList = new ArrayList<ClaimMapping>();
        addClaimMapping(claimMappingList, FIRST_NAME_CLAIM_URI);
        addClaimMapping(claimMappingList, LAST_NAME_CLAIM_URI);
        addClaimMapping(claimMappingList, EMAIL_CLAIM_URI);

        return claimMappingList.toArray(new ClaimMapping[claimMappingList.size()]);
    }

    public static String getLocationHeader(HttpResponse response) {

        if (response.getFirstHeader(HTTP_RESPONSE_HEADER_LOCATION) != null) {
            return response.getFirstHeader(HTTP_RESPONSE_HEADER_LOCATION).getValue();
        }
        return null;
    }

    private static void addClaimMapping(List<ClaimMapping> claimMappingList, String emailClaimUri) {
        Claim emailClaim = new Claim();
        emailClaim.setClaimUri(emailClaimUri);
        ClaimMapping emailClaimMapping = new ClaimMapping();
        emailClaimMapping.setRequested(true);
        emailClaimMapping.setLocalClaim(emailClaim);
        emailClaimMapping.setRemoteClaim(emailClaim);
        claimMappingList.add(emailClaimMapping);
    }
}
