/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.apache.cxf.rs.security.oauth.client;

import java.net.URI;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.UriBuilder;

import net.oauth.OAuth;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthMessage;
import net.oauth.signature.RSA_SHA1;
import org.apache.cxf.jaxrs.client.WebClient;
import org.apache.cxf.rs.security.oauth.provider.OAuthServiceException;

/**
 * The utility class for simplifying making OAuth request and access token
 * requests as well as for creating Authorization OAuth headers
 */
public final class OAuthClientUtils {
    private OAuthClientUtils() {

    }

    /**
     * Returns URI of the authorization service with the query parameter containing
     * the request token key
     * @param authorizationServiceURI the service URI
     * @param requestToken the request token key
     * @return
     */
    public static URI getAuthorizationURI(String authorizationServiceURI, String requestToken) {
        return UriBuilder.fromUri(authorizationServiceURI).
            queryParam("oauth_token", requestToken).build();

    }

    /**
     * Returns a simple representation of the Request token
     * @param requestTokenService initialized RequestToken service client
     * @param consumer Consumer bean containing the consumer key and secret
     * @param callback the callback URI where the request token verifier will
     *        be returned
     * @param extraParams additional parameters such as state, scope, etc
     * @return the token
     */
    public static Token getRequestToken(WebClient requestTokenService,
                             Consumer consumer,
                             URI callback,
                             Map<String, String> extraParams) throws OAuthServiceException {
        return getRequestToken(requestTokenService, consumer, callback, extraParams, null);
    }

    public static Map<String, Object> prepareOAuthRsaProperties(PrivateKey pk) {
        Map<String, Object> props = new HashMap<>();
        props.put(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.RSA_SHA1);
        props.put(RSA_SHA1.PRIVATE_KEY, pk);
        return props;
    }

    public static Token getRequestToken(WebClient requestTokenService,
                                        Consumer consumer,
                                        URI callback,
                                        Map<String, String> extraParams,
                                        Map<String, Object> oauthConsumerProps) throws OAuthServiceException {
        Map<String, String> parameters = new HashMap<>();
        if (extraParams != null) {
            parameters.putAll(extraParams);
        }
        parameters.put(OAuth.OAUTH_CALLBACK, callback.toString());

        if (oauthConsumerProps == null || !oauthConsumerProps.containsKey(OAuth.OAUTH_SIGNATURE_METHOD)) {
            parameters.put(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.HMAC_SHA1);
        }
        parameters.put(OAuth.OAUTH_NONCE, UUID.randomUUID().toString());
        parameters.put(OAuth.OAUTH_TIMESTAMP, String.valueOf(System.currentTimeMillis() / 1000));
        parameters.put(OAuth.OAUTH_CONSUMER_KEY, consumer.getKey());

        OAuthAccessor accessor = createAccessor(consumer, oauthConsumerProps);
        return getToken(requestTokenService, accessor, parameters);
    }
    private static OAuthAccessor createAccessor(Consumer consumer, Map<String, Object> props) {
        OAuthConsumer oAuthConsumer = new OAuthConsumer(null, consumer.getKey(), consumer.getSecret(),
                                                        null);
        if (props != null) {
            for (Map.Entry<String, Object> entry : props.entrySet()) {
                oAuthConsumer.setProperty(entry.getKey(), entry.getValue());
            }
        }
        return new OAuthAccessor(oAuthConsumer);
    }

    /**
     * Returns a simple representation of the Access token
     * @param accessTokenService initialized AccessToken service client
     * @param consumer Consumer bean containing the consumer key and secret
     * @param verifier the verifier/authorization key
     * @return the token
     */
    public static Token getAccessToken(WebClient accessTokenService,
                                       Consumer consumer,
                                       Token requestToken,
                                       String verifier) throws OAuthServiceException {
        return getAccessToken(accessTokenService, consumer, requestToken, verifier, null);
    }

    public static Token getAccessToken(WebClient accessTokenService,
                                       Consumer consumer,
                                       Token requestToken,
                                       String verifier,
                                       Map<String, Object> oauthConsumerProps) throws OAuthServiceException {
        Map<String, String> parameters = new HashMap<>();
        parameters.put(OAuth.OAUTH_CONSUMER_KEY, consumer.getKey());
        parameters.put(OAuth.OAUTH_TOKEN, requestToken.getToken());
        parameters.put(OAuth.OAUTH_VERIFIER, verifier);
        if (oauthConsumerProps == null || !oauthConsumerProps.containsKey(OAuth.OAUTH_SIGNATURE_METHOD)) {
            parameters.put(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.HMAC_SHA1);
        }

        OAuthAccessor accessor = createAccessor(consumer, oauthConsumerProps);
        accessor.requestToken = requestToken.getToken();
        accessor.tokenSecret = requestToken.getSecret();
        return getToken(accessTokenService, accessor, parameters);
    }

    /**
     * Creates OAuth Authorization header
     * @param consumer Consumer bean containing the consumer key and secret
     * @param token Access token representation
     * @param method HTTP method
     * @param requestURI request URI
     * @return the header value
     */
    public static String createAuthorizationHeader(Consumer consumer,
                                            Token accessToken,
                                            String method,
                                            String requestURI) {
        return createAuthorizationHeader(consumer, accessToken, method, requestURI, null);
    }

    public static String createAuthorizationHeader(Consumer consumer,
                                                   Token accessToken,
                                                   String method,
                                                   String requestURI,
                                                   Map<String, Object> oauthConsumerProps) {
        Map<String, String> parameters = new HashMap<>();
        parameters.put(OAuth.OAUTH_CONSUMER_KEY, consumer.getKey());
        if (accessToken != null) {
            parameters.put(OAuth.OAUTH_TOKEN, accessToken.getToken());
        }
        if (oauthConsumerProps == null || !oauthConsumerProps.containsKey(OAuth.OAUTH_SIGNATURE_METHOD)) {
            parameters.put(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.HMAC_SHA1);
        }
        parameters.put(OAuth.OAUTH_NONCE, UUID.randomUUID().toString());
        parameters.put(OAuth.OAUTH_TIMESTAMP, String.valueOf(System.currentTimeMillis() / 1000));

        OAuthAccessor accessor = createAccessor(consumer, oauthConsumerProps);
        if (accessToken != null) {
            accessor.accessToken = accessToken.getToken();
            accessor.tokenSecret = accessToken.getSecret();
        }
        return doGetAuthorizationHeader(accessor, method, requestURI, parameters);
    }


    /**
     * Creates OAuth Authorization header containing consumer key and secret values only
     * @param consumer Consumer bean containing the consumer key and secret
     * @return the header value
     */
    public static String createAuthorizationHeader(Consumer consumer) {
        StringBuilder sb = new StringBuilder(64);
        sb.append("OAuth ").append("oauth_consumer_key=").append(consumer.getKey())
          .append("oauth_consumer_secret=").append(consumer.getSecret());
        return sb.toString();

    }

    private static String doGetAuthorizationHeader(OAuthAccessor accessor,
            String method, String requestURI, Map<String, String> parameters) {
        try {
            OAuthMessage msg = accessor.newRequestMessage(method, requestURI, parameters.entrySet());
            StringBuilder sb = new StringBuilder();
            sb.append(msg.getAuthorizationHeader(null));
            for (Map.Entry<String, String> entry : parameters.entrySet()) {
                if (!entry.getKey().startsWith("oauth_")) {
                    sb.append(", ");
                    sb.append(OAuth.percentEncode(entry.getKey())).append("=\"");
                    sb.append(OAuth.percentEncode(entry.getValue())).append('"');
                }
            }
            return sb.toString();
        } catch (Exception ex) {
            throw new ProcessingException(ex);
        }
    }

    private static Token getToken(WebClient tokenService, OAuthAccessor accessor,
        Map<String, String> parameters) throws OAuthServiceException {
        String header = doGetAuthorizationHeader(accessor,
                                                 "POST",
                                                 tokenService.getBaseURI().toString(),
                                                 parameters);
        try {
            tokenService.replaceHeader("Authorization", header);
            Form form = tokenService.post(null, Form.class);
            return new Token(form.asMap().getFirst("oauth_token"),
                    form.asMap().getFirst("oauth_token_secret"));
        } catch (WebApplicationException ex) {
            throw new OAuthServiceException(ex);
        }
    }

    /**
     * Simple token representation
     */
    public static class Token {
        private String token;
        private String secret;

        public Token(String token, String secret) {
            this.token = token;
            this.secret = secret;
        }
        public String getToken() {
            return token;
        }

        public String getSecret() {
            return secret;
        }


    }
    /**
     * Simple consumer representation
     */
    public static class Consumer {

        private String key;
        private String secret;

        public Consumer(String key, String secret) {
            this.key = key;
            this.secret = secret;
        }
        public String getKey() {
            return key;
        }

        public String getSecret() {
            return secret;
        }


    }
}
