/*
 * Copyright (C) 2015 Twitter, Inc.
 *
 * 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 com.twitter.sdk.android.core.internal.oauth;

import com.twitter.sdk.android.core.Twitter;
import com.twitter.sdk.android.core.TwitterAuthConfig;
import com.twitter.sdk.android.core.TwitterAuthToken;
import com.twitter.sdk.android.core.TwitterCore;
import com.twitter.sdk.android.core.internal.network.UrlUtils;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import okio.ByteString;

class OAuth1aParameters {
    private static final String VERSION = "1.0";
    private static final String SIGNATURE_METHOD = "HMAC-SHA1";

    /**
     * Secure random number generator to sign requests.
     */
    private static final SecureRandom RAND = new SecureRandom();

    private final TwitterAuthConfig authConfig;
    private final TwitterAuthToken authToken;
    private final String callback;
    private final String method;
    private final String url;
    private final Map<String, String> postParams;

    OAuth1aParameters(TwitterAuthConfig authConfig, TwitterAuthToken authToken,
            String callback, String method, String url, Map<String, String> postParams) {
        this.authConfig = authConfig;
        this.authToken = authToken;
        this.callback = callback;
        this.method = method;
        this.url = url;
        this.postParams = postParams;
    }

    /**
     * @return the authorization header for inclusion in HTTP request headers for a request token.
     */
    public String getAuthorizationHeader() {
        final String nonce = getNonce();
        final String timestamp = getTimestamp();
        final String signatureBase = constructSignatureBase(nonce, timestamp);
        final String signature = calculateSignature(signatureBase);
        return constructAuthorizationHeader(nonce, timestamp, signature);
    }

    private String getNonce() {
        return String.valueOf(System.nanoTime()) + String.valueOf(Math.abs(RAND.nextLong()));
    }

    private String getTimestamp() {
        final long secondsFromEpoch = System.currentTimeMillis() / 1000;
        return Long.toString(secondsFromEpoch);
    }

    String constructSignatureBase(String nonce, String timestamp) {
        // Get query parameters from request.
        final URI uri = URI.create(url);
        final TreeMap<String, String> params = UrlUtils.getQueryParams(uri, true);
        if (postParams != null) {
            params.putAll(postParams);
        }

        // Add OAuth parameters.
        if (callback != null) {
            params.put(OAuthConstants.PARAM_CALLBACK, callback);
        }
        params.put(OAuthConstants.PARAM_CONSUMER_KEY, authConfig.getConsumerKey());
        params.put(OAuthConstants.PARAM_NONCE, nonce);
        params.put(OAuthConstants.PARAM_SIGNATURE_METHOD, SIGNATURE_METHOD);
        params.put(OAuthConstants.PARAM_TIMESTAMP, timestamp);
        if (authToken != null && authToken.token != null) {
            params.put(OAuthConstants.PARAM_TOKEN, authToken.token);
        }
        params.put(OAuthConstants.PARAM_VERSION, VERSION);

        // Construct the signature base.
        final String baseUrl = uri.getScheme() + "://" + uri.getHost() + uri.getPath();
        final StringBuilder sb = new StringBuilder()
                .append(method.toUpperCase(Locale.ENGLISH))
                .append('&')
                .append(UrlUtils.percentEncode(baseUrl))
                .append('&')
                .append(getEncodedQueryParams(params));
        return sb.toString();
    }

    private String getEncodedQueryParams(TreeMap<String, String> params) {
        final StringBuilder paramsBuf = new StringBuilder();
        final int numParams = params.size();
        int current = 0;
        for (Map.Entry<String, String> entry : params.entrySet()) {
            paramsBuf.append(UrlUtils.percentEncode(UrlUtils.percentEncode(entry.getKey())))
                    .append("%3D")
                    .append(UrlUtils.percentEncode(UrlUtils.percentEncode(entry.getValue())));
            current += 1;
            if (current < numParams) {
                paramsBuf.append("%26");
            }
        }
        return paramsBuf.toString();
    }

    String calculateSignature(String signatureBase) {
        try {
            final String key = getSigningKey();
            // Calculate the signature by passing both the signature base and signing key to the
            // HMAC-SHA1 hashing algorithm
            final byte[] signatureBaseBytes = signatureBase.getBytes(UrlUtils.UTF8);
            final byte[] keyBytes = key.getBytes(UrlUtils.UTF8);
            final SecretKey secretKey = new SecretKeySpec(keyBytes, "HmacSHA1");
            final Mac mac = Mac.getInstance("HmacSHA1");
            mac.init(secretKey);
            final byte[] signatureBytes = mac.doFinal(signatureBaseBytes);
            return ByteString.of(signatureBytes, 0, signatureBytes.length).base64();
        } catch (InvalidKeyException e) {
            Twitter.getLogger().e(TwitterCore.TAG, "Failed to calculate signature", e);
            return "";
        } catch (NoSuchAlgorithmException e) {
            Twitter.getLogger().e(TwitterCore.TAG, "Failed to calculate signature", e);
            return "";
        } catch (UnsupportedEncodingException e) {
            Twitter.getLogger().e(TwitterCore.TAG, "Failed to calculate signature", e);
            return "";
        }
    }

    private String getSigningKey() {
        final String tokenSecret = authToken != null ? authToken.secret : null;
        return new StringBuilder()
                .append(UrlUtils.urlEncode(authConfig.getConsumerSecret()))
                .append('&')
                .append(UrlUtils.urlEncode(tokenSecret))
                .toString();
    }

    String constructAuthorizationHeader(String nonce, String timestamp, String signature) {
        final StringBuilder sb = new StringBuilder("OAuth");
        appendParameter(sb, OAuthConstants.PARAM_CALLBACK, callback);
        appendParameter(sb, OAuthConstants.PARAM_CONSUMER_KEY, authConfig.getConsumerKey());
        appendParameter(sb, OAuthConstants.PARAM_NONCE, nonce);
        appendParameter(sb, OAuthConstants.PARAM_SIGNATURE, signature);
        appendParameter(sb, OAuthConstants.PARAM_SIGNATURE_METHOD, SIGNATURE_METHOD);
        appendParameter(sb, OAuthConstants.PARAM_TIMESTAMP, timestamp);
        final String token = authToken != null ? authToken.token : null;
        appendParameter(sb, OAuthConstants.PARAM_TOKEN, token);
        appendParameter(sb, OAuthConstants.PARAM_VERSION, VERSION);
        // Remove the extra ',' at the end.
        return sb.substring(0, sb.length() - 1);
    }

    private void appendParameter(StringBuilder sb, String name, String value) {
        if (value != null) {
            sb.append(' ')
                    .append(UrlUtils.percentEncode(name)).append("=\"")
                    .append(UrlUtils.percentEncode(value)).append("\",");
        }
    }
}