--[[----------------------------------------------------------------------------

KSOAuth2Impl.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2012 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

----------------------------------------------------------------------------]]--

	-- WF.core.logging
local WFLogger = require 'WFLogger'

	-- WF.core.os
local KSDate = require 'KSDate'
local WFPlatformInfo = require 'WFPlatformInfo'

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local WFCrypto = require 'WFCrypto'

	-- WF.oz_client.ims
local WFImsAppIdentity = require 'WFImsAppIdentity'

	-- WF.web.http_client
local WFHttpClient = require 'WFHttpClient'

	-- WF.web.json
local KSJsonDecoder = require 'KSJsonDecoder'
local KSJsonEncoder = require 'KSJsonEncoder'

	-- WF.web.utils
local WFURI = require 'WFURI'

--==============================================================================

local IMS_TOKEN_SERVICE = "/token"
local logger = WFLogger 'KSOauth2Impl'
local WFURI_escape = WFURI.escape
local urlEncode = function( token )
	return WFURI_escape( token, 'http_query_key' )
end

--==============================================================================

local KSOAuth2Impl = {}

--------------------------------------------------------------------------------

local function generateTimestamp()
	return string.format( "%d", KSDate.timeNow() )
end

--------------------------------------------------------------------------------

local function generateNonce()

	return KSIdGenerator.uuid()
end

--------------------------------------------------------------------------------

local function signatureBaseString( args )

	-- OAuth Spec, Section 9.1.1 "Normalize Request Parameters"
	-- build a sorted array of both request parameters and OAuth header parameters
	local parameterPairs = {
	
		{ "oauth_consumer_key", assert( args.consumer_key, "Must provide consumer_key" ) },
		{ "oauth_signature_method", "HMAC-SHA1" },
		{ "oauth_timestamp", assert( args.timestamp , "Must provide timestamp") },
		{ "oauth_nonce", assert( args.nonce, "Must provide nonce" ) },
		{ "oauth_version", "1.0" },

	}
	
	if args.token_key then
		table.insert( parameterPairs, { "oauth_token", args.token_key } )
	end
	
	for k, v in pairs( args.extraOAuthParameters or {} ) do
		table.insert( parameterPairs, { k, v } )
	end
	
	-- Include everything in the query string. So unless
	-- this is multipart/form-data encoded, all the params
	-- for the web service are also part of the base
	if assert( args.contentType ) ~= "multipart/form-data" then
		local params = assert( args.requestParameters, "Missing required argument requestParameters" )
		for k, v in pairs( params ) do
			table.insert( parameterPairs, { k, v } )
		end
	end
	
	-- encode all the values
	for id, kv in ipairs( parameterPairs ) do
		parameterPairs[ id ] = string.format( "%s=%s", urlEncode( kv[ 1 ] ), urlEncode( kv[ 2 ] ) )
	end
	
	-- sort them
	-- SPAM [arahn 2012-05-25]: Do we need a custom compare function?
	table.sort( parameterPairs )

	local normalizedRequestParameters = table.concat( parameterPairs, "&" )
	normalizedRequestParameters = urlEncode( normalizedRequestParameters )
	
	local url = assert( args.URL, "missing required argument URL" )
	
	local parsed = WFURI.parse( url )
	local urlWithoutQuery = string.format( "%s://%s%s", parsed.scheme, parsed.authority, parsed.path )
	
	-- OAuth Spec, Section 9.1.2 "Concatenate Request Elements"
	local ret = string.format( "%s&%s&%s",
					 assert( args.HTTPMethod, "missing required argument HTTPMethod" ),
					 urlWithoutQuery,
					normalizedRequestParameters )
						
	logger:infof( "OAMutableURLRequest parameters %s", normalizedRequestParameters )
	
	return ret

end

--------------------------------------------------------------------------------

local function alphabeticalKeyArray( t )
	
	local keys = {}
	for k in pairs( t ) do
		table.insert( keys, k )
	end
	
	-- SPAM [arahn 2012-05-25]: Do we need a custom compare function?
	table.sort( keys )
	
	return keys
	
end

--------------------------------------------------------------------------------

local function prepareOAuthHeader( args )

	local base = signatureBaseString( args )
	local hashSecret = string.format( "%s&%s", urlEncode( args.consumer_secret ), urlEncode( args.token_secret or "" ) )
	local signature = WFCrypto.HMAC.digest( base, "SHA1", hashSecret )
	-- set OAuth headers
	local oauthToken

	if args.token_key then
		oauthToken = string.format( 'oauth_token="%s", ', urlEncode( args.token_key ) )
	else
		oauthToken = ""
			-- not used on Request Token transactions
	end
	
	local extraParameters = {}
	
	-- Adding the optional parameters in sorted order isn't required by the OAuth spec, but it makes it possible to hard-code expected values in the unit tests.
	for _, k in ipairs( alphabeticalKeyArray( args.extraOAuthParameters or {} ) ) do
		local encK = urlEncode( k )
		local encV = urlEncode( args.extraOAuthParameters[ k ] )
		table.insert( extraParameters, string.format( ', %s="%s"', encK, encV ) )
	end
	
	extraParameters = table.concat( extraParameters )
	
	return string.format( 'OAuth realm="%s", oauth_consumer_key="%s", %soauth_signature_method="%s", oauth_signature="%s", oauth_timestamp="%s", oauth_nonce="%s", oauth_version="1.0"%s',
		urlEncode( assert( args.realm, "Missing required parameter realm" ) ),
		urlEncode( assert( args.consumer_key, "Must provide consumer_key" ) ),
		oauthToken,
		"HMAC-SHA1",
		urlEncode( signature ),
		assert( args.timestamp, "Missing required arg timestamp" ),
		assert( args.nonce, "missing required arg nonce" ),
		extraParameters )
			
end

--------------------------------------------------------------------------------

local function prepareOAuthBody( args )
	
	local encodedParameterPairs = {}
	local params = assert( args.requestParameters, "Missing required argument requestParameters" )
	local body
	
	for _, k in ipairs( alphabeticalKeyArray( params ) ) do
		table.insert( encodedParameterPairs, string.format( "%s=%s",
			urlEncode( k ), urlEncode( params[ k ] ) ) )
	end
	
	encodedParameterPairs = table.concat( encodedParameterPairs, "&" )
	
    if args.HTTPMethod == "GET" or args.HTTPMethod == "DELETE" then
		local parsedUrl = WFURI.parse( assert( args.URL, "Required param URL" ) )
        args.URL = string.format( "%s://%s/%s?%s", parsedUrl.scheme, parsedUrl.authority, parsedUrl.path, encodedParameterPairs )
    else
		body = encodedParameterPairs
		args.contentType = "application/x-www-form-urlencoded"
	end
	
	return body
	
end

--------------------------------------------------------------------------------

local function OAuthP( args )

	local oauthBody = prepareOAuthBody( args )
		-- it's necessary to prepare the body first, as it may adjust our URL
		-- or contentType
	
	local oauthHeader = prepareOAuthHeader( args )
	
	local clientId, _ = WFImsAppIdentity.getClientIdAndSecret( )
	
	return WFHttpClient.putP( args.URL, oauthBody, {
		requestMethod = args.HTTPMethod,
		requestHeaders = {
			{ header = 'Content-Type', value = args.contentType },
			{ header = 'Authorization', value = oauthHeader },
			{ header = 'X-IMS-ClientID', value = clientId },
		},
	} )

end

--------------------------------------------------------------------------------

local function fetchImsTokensImpP( imsBaseUrl, requestParams )

	local clientId, clientSecret = requestParams.client_id, requestParams.client_secret
	
	if clientId == nil or clientSecret == nil then
		clientId, clientSecret = WFImsAppIdentity.getClientIdAndSecret( )
	end
	
	local analyticsAppId = WFImsAppIdentity.getAnalyticsAppId( )
	local appVersion = WFPlatformInfo.getAppVersion()
	
	if analyticsAppId == '' or analyticsAppId == nil then
		analyticsAppId = clientId
	end
	
	local imsStateParam = {
		ac = analyticsAppId,
		av = appVersion
	}
	
	requestParams.state = KSJsonEncoder.encodeOzUploadJson( imsStateParam )
	requestParams.client_id = clientId
	requestParams.client_secret = clientSecret
	
	local args = {
		HTTPMethod = "POST",
		URL = string.format( "%s%s", imsBaseUrl, IMS_TOKEN_SERVICE ),
		consumer_key = clientId,
		consumer_secret = clientSecret,
		nonce = generateNonce(),
		realm = "",
		requestParameters = requestParams,
		timestamp = generateTimestamp(),
	}
	
	return OAuthP( args )
		
	:mapDataP( function( _, json, headers )

		return 'data', KSJsonDecoder.decodeWebSafeJson_stripNull( json ), headers

	end
	)

end

--------------------------------------------------------------------------------

--[[!
	\brief Fetch the OAUTH2 tokens from IMS. Provide your IMS client id and client
	secret here to override the values stored in WFImsAppIdentity (if any).
	
	\param authCode The Auth code received from IMS /ims/authorize call
	\param imsBaseUrl The ims entry point, e.g. "https://ims-na1-qa1.adobelogin.com/"
	\param optClientId Your client id
	\param optClientSecret the ims secret that goes with that client id
--]]
function KSOAuth2Impl.fetchImsTokensP( authCode, imsBaseUrl, optClientId, optClientSecret )
	
	return fetchImsTokensImpP( imsBaseUrl,
		{
			grant_type = "authorization_code",
			code = authCode,
			client_id = optClientId,
			client_secret = optClientSecret,
		}
	)
	
end

--------------------------------------------------------------------------------

function KSOAuth2Impl.fetchImsTokensWithDeviceTokenGrantTypeP( deviceToken, imsBaseUrl, optClientId, optClientSecret )

	local deviceId = WFImsAppIdentity.getImsDeviceId()

	return fetchImsTokensImpP( imsBaseUrl,
		{
			grant_type = "device",
			device_token = deviceToken,
			device_id = deviceId,
			client_id = optClientId,
			client_secret = optClientSecret,
		}
	)

end


return KSOAuth2Impl
