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

KSOAuth2SessionImp.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2011 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.

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

This is the backing event processor for KSOAuth2Session. This API is considered
private and should only be accessed via the KSOAuth2Session class.

One event processor is created per session ID.

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

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

	-- WF.core.lua.threading
local WFRequestDispatcher = require 'WFRequestDispatcher'
local WFTimer = require 'WFTimer'

	-- WF.core.lua
local KSData = require 'KSData'

	-- WF.core.os
local KSKeychainService = require 'KSKeychainService'
local KSPrefs = require 'KSPrefs'
local WFLocalizationUtils = require 'WFLocalizationUtils'

	-- WF.core.reactive
local KSPromises = require 'KSPromises'
local KSReactive = require 'KSReactive'

	-- WF.core.string
local WFZString = require 'WFZString'

	-- WF.core.utils
local WFArguments = require 'WFArguments'

	-- WF.oz_client.ims
	-- TODO [arahn 2014-05-13]: This is an inverted dependency!
local WFImsAppIdentity = require 'WFImsAppIdentity'

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

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

local WFPlatformInfo = require 'WFPlatformInfo'


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

local ZSTR = assert( WFZString.fromZString )

local kUnchanged = { the_unchanged_sentinel = true }

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

local IMS_TOKEN_SERVICE = "/token"
local IMS_LOGOUT_SERVICE = "/logout"

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

local oAuth2ImpLog = WFLogger 'OAuth2SessionImp'
--	:setLevel 'debug'

WFRequestDispatcher.enableDiagnosticLogForRequestStats()

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

local hasValidLoginB = KSReactive.variableB( true )

local isWaitingForTokensB = KSReactive.variableB( false )

local waitingTokenResolveFn = nil

local languagePrefString = nil

local KSData_isData = assert( KSData.isData )


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

local function acceptLanguagePreferencesString()

	if languagePrefString then
		return languagePrefString
	end
	
	local localeId = WFLocalizationUtils.getLocaleId() -- e.g. "en-US"
	local langCode = WFLocalizationUtils.getLanguageCode() -- e.g. "en"
	
	-- This fallback shouldn't be necessary, but better this
	-- than sending some badly-formatted string to Oz...
	-- Purposely don't cache this bad answer, maybe it will work next time.
	
	if not localeId then
		oAuth2ImpLog:error( "No locale id available in KSOAuth2SessionImp" )
		return langCode or ""
	end
	
	-- e.g. "Accept-Language: en-US, en;q=0.8"
	-- Indicates that the region-specific language identifier is highest preference,
	-- (default q=1), but fallback to any other regional variant of the same language.
	-- q=0.8 is a totally arbitrary value less than 1. Doesn't really matter the value we
	-- choose if there is not a third or fourth preference.
	
	languagePrefString = string.format( "%s, %s;q=0.8", localeId, langCode )
	
	return languagePrefString
	
end

--------------------------------------------------------------------------------
-- On startup, read the previous state (if any) from the keychain.
-- Populate the initial token value from this info.

local initMessageProcessor = function( sessionId )
	
	oAuth2ImpLog:infof( "initMessageProcessor for session ID %s", sessionId )

	assert( type( sessionId ) == 'string', "sessionId must be a string" )
	assert( #sessionId == 32, "sessionId must be a compact UUID (32 characters" )
	
	local accessAndRefreshTokensP
	local accessAndRefreshTokensP_original
	local sClientId, sClientSecret, sImsBaseUrl
	
	local keys = { sessionId, sessionId .. '.device_token', 'ims_device_token', }
	accessAndRefreshTokensP = KSPromises.mapArrayP( function( key )
				return KSKeychainService.readP( key ) end,
			keys )
		:mapP {
	
			data = function( status, arr )
				
				local rawJson, newDeviceToken, legacyDeviceToken = unpack( arr, 1, #keys )
				if rawJson then
				
					local decodedJson = KSJsonDecoder.decodeWebSafeJson_stripNull( rawJson )
					
					if decodedJson and decodedJson.access_token and decodedJson.refresh_token then
						
						local device_token = newDeviceToken or legacyDeviceToken
	
						oAuth2ImpLog:infof( "found refresh+access tokens in keychain for session ID %s", sessionId )
	
						return 'data', decodedJson.access_token, decodedJson.refresh_token, device_token
					
					end
				
				end
				
				-- No data or incomplete data in keychain. Make this into an error.
	
				oAuth2ImpLog:infof( "no refresh/access token found in keychain result: %s", rawJson )
							
				-- FIXME [scouten 2011-11-29]: We're threading a very tricky balance between
				-- two bugs (#3057613 and #3056308). In the former case we just turn the
				-- connection light red when the tokens are missing and leave it that
				-- way when we should force a logout, but in the latter case we are
				-- forcing a logout immediately after logging in (when we should be
				-- expecting a storeTokensP message in just a moment). Relying on a timer
				-- to "fix" this now, but we should probably look for a more solid way
				-- to pass in the initial tokens in the first-login case in a future
				-- version.
				
				WFTimer.afterDelayDo( 10, function()
				
					-- This if clause answers the question: Did someone else provide newer
					-- tokens since we were here originally? If so, don't force a logout.
					-- If not, that probably means we'll never get new tokens.
				
					if accessAndRefreshTokensP_original == accessAndRefreshTokensP then
						oAuth2ImpLog:info( "forcing logout because no tokens were provided" )
						hasValidLoginB:set( false )
					end
				
				end )
				
				return 'error', "No refresh/access token stored in keychain - sessionId = " .. sessionId
				
			end,
			
			error = function( status, message )
			
				oAuth2ImpLog:error( "forcing logout because error occurred reading keychain" )
				hasValidLoginB:set( false )
	
				return status, message
			
			end,
		
		}
	
	accessAndRefreshTokensP_original = accessAndRefreshTokensP
	
	accessAndRefreshTokensP:runUntilDone()
	
	--------------------------------------------------------------------------------
	-- Internal code to store new user info.
	
	local function storeNewUserInfoP( accessToken, refreshToken, deviceToken )
		
		WFArguments.check( '+string', '+string', '?+string|table' )
		
		hasValidLoginB:set( true )
		isWaitingForTokensB:set( false )

		
		local userInfo = {
			access_token = accessToken,
			refresh_token = refreshToken,
		}
		
		local paramArray = {
			{ sessionId, KSJsonEncoder.encodeOzUploadJson( userInfo ) },
			{ sessionId .. '.device_token', deviceToken },
			{ 'ims_device_token', nil },
				-- legacy support; ims_device_token was previously stored separately
		}
		
		local writeDeleteIgnoreFn = function( params )
			local key, value = unpack( params )
			if value == nil then
				return KSKeychainService.deleteP( key )
			elseif value == kUnchanged then
				return KSPromises.resolvedPromiseP( 'data', true )
			else
				return KSKeychainService.writeP( key, value )
			end
		end
		
		return KSPromises.mapArrayP( writeDeleteIgnoreFn, paramArray )
		:mapDataP( function()
			oAuth2ImpLog:infof( "FINISHED storking tokens for session ID %s", sessionId )
			return 'data', true
		end )
		:runUntilDoneP()
		
	end
	
	--------------------------------------------------------------------------------
	
	WFRequestDispatcher.onMessage( 'storeTokensP',
	function( accessToken, refreshToken, deviceToken )
	
		oAuth2ImpLog:infof( "Store tokens for session ID %s", sessionId )

		-- Hack up a small user info packet for now.
	
		if not refreshToken then
			error "KSOAuth2SessionImp:storeTokensP: missing refreshToken"
		end
	
		if not accessToken then
			error "KSOAuth2SessionImp:storeTokensP: missing accessToken"
		end
	
		oAuth2ImpLog:info( "received new refresh and access tokens" )
		
		-- resolve existing accessAndRefreshTokensP created by refreshRequestP
		if waitingTokenResolveFn then
			waitingTokenResolveFn( 'data', accessToken, refreshToken, deviceToken )
			waitingTokenResolveFn = nil
			isWaitingForTokensB:set(false)
		else
			accessAndRefreshTokensP = KSPromises.resolvedPromiseP( 'data',
																   accessToken,
																   refreshToken,
																   deviceToken )
		end
	
		return storeNewUserInfoP( accessToken, refreshToken, deviceToken )
	
	end )
	
	--------------------------------------------------------------------------------
	
	WFRequestDispatcher.onMessage( 'hasValidLoginB', function()
	
		return hasValidLoginB:relayB()
	
	end )

	WFRequestDispatcher.onMessage( 'isWaitingForTokensB', function()
	
		return isWaitingForTokensB:relayB()
	
	end )	
	
	--------------------------------------------------------------------------------
	
	local function deleteAuthKeys()
	
		KSKeychainService.deleteP( sessionId )
		KSKeychainService.deleteP( 'ims_device_token' )
			-- legacy support; ims_device_token was previously stored separately
		
		accessAndRefreshTokensP = KSPromises.resolvedPromiseP( 'error', "Logged out" )
	
	end
	
	--------------------------------------------------------------------------------
	
	
	local function getRefreshTokenUrlAndPostBody( imsBaseUrl, refreshToken )
	
		--	On entry: ( imsBaseUrl refreshToken )
		--	On exit:  ( url )
				
		local url = string.format( "%s%s",
								 imsBaseUrl,
								 IMS_TOKEN_SERVICE )
				
		
		local clientId, clientSecret = sClientId, sClientSecret
		if not clientId then
			clientId, clientSecret = WFImsAppIdentity.getClientIdAndSecret( )
		end

		assert( clientId ~= nil, "clientId is nil in getRefreshTokenUrlAndPostBody" )
		assert( clientSecret ~= nil, "clientSecret is nil in getRefreshTokenUrlAndPostBody" )
		
		local analyticsAppId = WFImsAppIdentity.getAnalyticsAppId( )
		local appVersion = WFPlatformInfo.getAppVersion()
		
		if analyticsAppId == '' or analyticsAppId == nil then
			analyticsAppId = clientId
		end
		
		local imsStateParam = {
			ac = analyticsAppId,
			av = appVersion
		}
		local postBody = string.format( "grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s&state=%s",
				refreshToken, clientId, clientSecret, KSJsonEncoder.encodeOzUploadJson( imsStateParam ) )
			
		return url, postBody
	
	end
	
	--------------------------------------------------------------------------------
	
	local function refreshRequestP( accessToken, refreshToken, deviceToken, authHeader )
		local promise, resolver = KSPromises.promiseP()

		-- will be resolved by storeTokensP message handler
		isWaitingForTokensB:set(true)
		waitingTokenResolveFn = resolver

		return promise
	end

	local function __UNUSED__refreshRequestP( accessToken, refreshToken, deviceToken, authHeader )
	
		-- FIXME [scouten 2011-11-28]: Don't like the hard-coded reference to
		-- Oz's Www-Authenticate header, but not worth fixing right now.
	
		if not authHeader then
			error( "refreshRequestP: missing authHeader parameter", 2 )
		end
	
		local authUrl = authHeader:match( [[auth_url="([^"]+)"]] )
									--" (fix up Halide's syntax coloring)
		
		if not authUrl then
			error( "refreshRequestP: authHeader did not contain a valid URL", 2 )
		end
		
		authUrl = authUrl:match( [[(.*)/authorize]] )
	
		local refreshUrl, postBody = getRefreshTokenUrlAndPostBody( authUrl, refreshToken )
		
		oAuth2ImpLog:debugf( "refreshing using url = %s\n\npostBody = %s",
							refreshUrl, postBody )
	
		oAuth2ImpLog:debugf( "Access Token = %s", accessToken )
		oAuth2ImpLog:debugf( "Refresh Token = %s", refreshToken )
		
		local clientId, _ = WFImsAppIdentity.getClientIdAndSecret( )
		
		local refreshPostArgs = {
			requestMethod = 'POST',
			requestHeaders = {
				{ header = 'Content-Type', value = 'application/x-www-form-urlencoded' },
				{ header = 'X-IMS-ClientID', value = clientId },
			},
		}
		
		local userDataP = WFHttpClient.putP( refreshUrl, postBody, refreshPostArgs )
		:mapP( KSJsonDecoder.decodeWebSafeJson_stripNullP )
		:mapDataP( function( _, userInfo )
			oAuth2ImpLog:debugf( "refreshRequestP returned %s", userInfo )
			
			if not userInfo.access_token then
				oAuth2ImpLog:errorf( "refreshRequestP missing access_token :: returned %s", userInfo )
				return 'error', "missing access_token"
			end

			return 'data', userInfo.access_token, userInfo.refresh_token or refreshToken, deviceToken
		
		end )
		:waitForP( function( status, accessToken, refreshToken )

			oAuth2ImpLog:debugf( "refreshing yielded accessToken = %s, refreshToken = %s, ",
								accessToken, refreshToken )

			if status == 'error' then
				return KSPromises.resolvedPromiseP( 'error', 'refresh token also expired' )
			end
			
			return storeNewUserInfoP( accessToken, refreshToken, kUnchanged )
			
		end ):runUntilDoneP()
	
		local resultP = userDataP:mapP {
	
			error = function( _, data, headers )
			
				local statusCode = headers and headers.status_code
				
				oAuth2ImpLog:errorf( "refreshToken call failed with\n%s\n%s", headers, data )
				oAuth2ImpLog:errorf( "Refresh Failed. Previous Access Token = %s", accessToken )
				oAuth2ImpLog:errorf( "Refresh Failed. Previous Refresh Token = %s", refreshToken )
				
				local function failRefreshP()
					hasValidLoginB:set( false )
					deleteAuthKeys()
					return KSPromises.resolvedPromiseP( 'error', data, headers )
				end
				
				if not statusCode then
				
					oAuth2ImpLog:errorf( "refreshToken call failed with transient error:\n\ndata = %s\n\nheaders = %s",
										data, headers )
				
				elseif statusCode >= '400' and statusCode <= '499' then
					
					-- Check to see if we have a device token stored. If so, use it to
					-- get a new access/refresh token pair. If not, or if this grant fails,
					-- force-logout as usual.
					
					if deviceToken then
					
						local KSOAuth2Impl = require 'KSOAuth2Impl'
						
						return KSOAuth2Impl.fetchImsTokensWithDeviceTokenGrantTypeP( deviceToken, authUrl )
							:mapP( function( status, tokenData, tokenHeaders )
								
								if status == 'data'
									and type( tokenData ) == 'table'
									and tokenData.access_token
									and tokenData.refresh_token then
									
										storeNewUserInfoP( tokenData.access_token, tokenData.refresh_token, kUnchanged )
										
										return 'data', tokenData.access_token, tokenData.refresh_token, deviceToken
										
								else
									
									oAuth2ImpLog:errorf( [[refreshToken with device token call failed
										with permanent error: forcing new login\n%s\n%s]], tokenData, tokenHeaders )
										
									oAuth2ImpLog:error( "forcing logout because token grant with stored device token failed" )
									return failRefreshP()
									
								end
								
							end )
						
					else
					
						oAuth2ImpLog:errorf( [[refreshToken call failed with permanent error:
											forcing new login\n%s\n%s]], headers, data )
											
						oAuth2ImpLog:error( "forcing logout because refreshToken failed" )
						
						return failRefreshP()
						
					end
	
				end
			
				return 'error', data, headers
			
			end,
		
		}
	
		resultP:runUntilDone()
		
		return resultP
	
	end
	
	--------------------------------------------------------------------------------
	
	local function getPutImpP( url, args, getPutInnerFunc )
	
		oAuth2ImpLog:infof( "getPutImp for URL %s", url )
	
		if not accessAndRefreshTokensP then
			error "Not logged in"
		end
		
		local accessAndRefreshTokensP_atStart = accessAndRefreshTokensP
		
		return accessAndRefreshTokensP:mapDataP( function( _, accessToken, refreshToken, deviceToken )
	
			-- [scouten 2011-06-28]: If this happens, please let me know. If possible,
			-- please capture a stack trace and Xcode debug log. Thanks!
			-- [scouten 2011-07-02]: I think this is fixed now, but leaving this
			-- in place for now.
	
			if not accessToken then
				debug.print( "KSOAuth2SessionImp: missing access token" )
				return 'error', "missing access token"
			end
	
			if not refreshToken then
				debug.print( "KSOAuth2SessionImp: missing refresh token" )
				return 'error', "missing refresh token"
			end
	
			-- If we have to get a new access token ("refresh"), this is where it happens.
		
			local function reauthAndRetryP( authorizeHeader )
			
				-- [scouten 2011-11-21]: Here's the fun and tricky new part: If the
				-- access+tokens promise object that we used when we started this request
				-- is still in place, then it's time to trigger a token refresh and
				-- suspend all future requests until we have the new token.
				
				-- If the promise object has already been changed, then we simply
				-- reissue this request with the new token set once we have it.
	
				if accessAndRefreshTokensP_atStart == accessAndRefreshTokensP then
					accessAndRefreshTokensP = refreshRequestP( accessToken, refreshToken, deviceToken, authorizeHeader )
				end
				
				return getPutImpP( url, args, getPutInnerFunc )
			
			end
			
			-- Configure progress rerouting if needed.
	
			-- FIXME [scouten 2011-11-18]: The wantDownloadProgress feature is currently
			-- broken. We need to fix this post-1.1. (Nobody is using it currently...)
			
			local currentProgressBB
	
	--		if args.wantDownloadProgress then
	--
	--			currentProgressBB = KSReactive.variableB( nil )
	--			local resultProgressB = currentProgressBB:mapSwitchB( function( progressB )
	--
	--				if progressB then
	--					return progressB
	--				else
	--					return KSReactive.variableB( 0, 1 )
	--				end
	--
	--			end )
	--
	--		end
		
			-- In order to implement the 401 trapping, we need to suppress the
			-- conversion of HTTP errors to reactive errors.
	
			args = args or {}
		
			local originalReturnHttpErrorsAsData = args.returnHttpErrorsAsData
			args.returnHttpErrorsAsData = true
			args.retainDataAfterRequest = true
		
			-- Strip any previous Authorization and x-token-ttl headers.
			
			local headers = args.requestHeaders or {}
			args.requestHeaders = headers
			
			for i = #headers, 1, -1 do
			
				if headers[ i ].header == 'Authorization'
					or headers[ i ].header == 'x-token-ttl'
				then
					table.remove( headers, i )
				end
			
			end
	
			-- Create the request based on current access token.
	
			headers[ #headers + 1 ] =
				{ header = 'Authorization', value = 'Bearer ' .. accessToken }
	
--			headers[ #headers + 1 ] =
--				{ header = 'x-token-ttl', value = '30' }
					-- Should normally be commented out.
					
			-- Add an Accept-Language header.
	
			headers[ #headers + 1 ] =
				{ header = 'Accept-Language', value = acceptLanguagePreferencesString() }
	
			oAuth2ImpLog:debugf( "sending request to getPutInnerFunc with args = %s", args )
	
			local rawResponseP, rawProgressB = getPutInnerFunc( args )
			
			if currentProgressBB then
				currentProgressBB:set( rawProgressB )
			end
	
			return rawResponseP:mapDataP( function( _, data, headers )
				
				-- Close data after processing this request.
				
				if KSData_isData( data ) then
					WFRequestDispatcher.deferredClose( data )
				end
				
				-- Debug output.
				
				if headers and headers.status_code == '401' then
					oAuth2ImpLog:info( "OAuth2 token invalid; refreshing" )
				elseif headers and headers.status_code >= '400' then
					oAuth2ImpLog:errorf(
							"\error on URL %s\n\ndata = %s\n\nheaders = %s",
							url, data, headers )
				else
					oAuth2ImpLog:debugf(
							"\nnormal response on URL %s\n\nheaders = %s",
							url, headers )
				end
				
				-- 401 errors mean we have an invalid access token. This means we should
				-- use the refresh token and try again.
				
				if headers and headers.status_code == '401' then
					
					-- FIXME [scouten 2011-11-28]: Don't like the hard-coded reference to
					-- Oz's Www-Authenticate header, but not worth fixing right now.
	
					oAuth2ImpLog:infof( "Detected invalid access token %s", headers )
					local authHeader = headers[ 'Www-Authenticate' ] or headers[ 'WWW-Authenticate' ]
					return reauthAndRetryP( authHeader )
				
				end
				
				-- 403 errors (forbidden) may mean that Oz is forcing a client upgrade
				-- or that the catalog has been unshared from this user.
				
				-- FIXME [scouten 2011-11-23]: Consider generalizing this beyond 403
				-- errors in case some code looks for other error conditions.
	
				if headers and headers.status_code == '403' and args.compositeErrorData then
			
					-- Try parsing the JSON data response.
	
					local result, eData = pcall( KSJsonDecoder.decodeWebSafeJson, data, true )
	
					-- If the error code returned by Oz is 4999 we need to force an update
					-- and need to capture the iTMS link that is returned with it to direct the
					-- user to the download location for the upgrade.
	
					local link = result
								and eData
								and eData.code == 4999
								and eData.links
								and eData.links[ '/rels/client_app' ]
								and eData.links[ '/rels/client_app' ].href
								or ''
	
					if eData and eData.code then
						return 'error', string.format( "%s,%s,%s", headers.status_code, eData.code, link ), headers
					end
					
					-- Otherwise, just let it pass through as normal error handling.
					
				end
	
				-- If client requested that we return HTTP errors as errors, we restore
				-- that behavior here.
		
				if headers and headers.status_code >= '400'
					and not originalReturnHttpErrorsAsData
				then
				
					if args.returnHttpErrorsAsStatusCode then
						return 'error', headers.status_code
					else
						return 'error', data, headers
					end
	
				-- Happy path.
		
				else
	
					return 'data', data, headers
		
				end
			
			end )
			
		end )
	
	end
	
	--------------------------------------------------------------------------------
	
	WFRequestDispatcher.onMessage( 'getP', function( url, optArgs, optSequencing )
		
		return getPutImpP( url, optArgs, function( newArgs )
		
			return WFHttpClient.getP( url, newArgs, optSequencing )
		
		end )
	
	end )
	
	--------------------------------------------------------------------------------
	
	WFRequestDispatcher.onMessage( 'putP', function( url, data, optArgs )
		
		local resultP = getPutImpP( url, optArgs, function( newArgs )
	
			return WFHttpClient.putP( url, data, newArgs )
	
		end )
		
		if KSData_isData( data ) then
			resultP = resultP:observeP( function() data:close() end )
		end
		
		return resultP
	
	end )
	
	--------------------------------------------------------------------------------
	--[[
		Should only be used in debug builds! Simulate a failed token refresh.
	]]--
	
	WFRequestDispatcher.onMessage( 'simulateForcedLogoutP', function()
		
		oAuth2ImpLog:errorf( "simulate forced logout" )
		hasValidLoginB:set( false )
	
		deleteAuthKeys()
	
	end )
	
	--------------------------------------------------------------------------------
	
	
	local function getImsLogoutUrl( imsBaseUrl, accessToken, clientId )
	
		local analyticsPlatform
		
		if WFPlatformInfo.isiOS then 
			analyticsPlatform = "ios"
		elseif WFPlatformInfo.isiOS then 
			analyticsPlatform = "android"
		elseif WFPlatformInfo.isMac then 
			analyticsPlatform = "osx"
		elseif WFPlatformInfo.isWindows then 
			analyticsPlatform = "win"
		end
		
		local analyticsAppId = WFImsAppIdentity.getAnalyticsAppId( )
		
		if analyticsAppId == '' or analyticsAppId == nil then
			analyticsAppId = clientId
		end
	
		return string.format( "%s%s?access_token=%s&client_id=%s&ac=%s&pl=%s&redirect_uri=https://fake.domain.adobe.com",
					   imsBaseUrl,
					   IMS_LOGOUT_SERVICE,
					   accessToken,
					   clientId,
					   analyticsAppId,
					   analyticsPlatform)
	
	end
	
	--------------------------------------------------------------------------------
	
	local function logoutP( imsBaseUrl, accessToken )
		--	From https://wiki.corp.adobe.com/display/ims/IMS+API+-+logout :
		--	Front-Channel logout
		--
		--	Request:
		--	  GET /ims/logout/v1?access_token=ACCESS_TOKEN&client_id=CLIENT_ID HTTP/1.1
		--	  Host: ims-na1-stg1.adobelogin.com
		--
		--	Response:
		--	  HTTP/1.1 200
		--	  Cache-Control: no-store
	
		local clientId, _ = sClientId, sClientSecret
		if not clientId then
			clientId, _ = WFImsAppIdentity.getClientIdAndSecret( )
		end
		local logoutUrl = getImsLogoutUrl( imsBaseUrl, accessToken, clientId )
	
		debug.print( "Requesting: %@", logoutUrl.absoluteString )
	
		return WFHttpClient.putP( logoutUrl, nil, {
			requestMethod = "GET",
		} ):mapDataP( function( _, data, headers )
			
			local responseStatusCode = headers.status_code

			if responseStatusCode ~= "302" then
				
				local responseString = tostring( data )
				
				local message = ZSTR( "$$$/IdentityManagement/LogoutError=Received unexpected response code: ^1^nResponse:^2",
									  responseStatusCode,
									  responseString )

				-- debug.print( message )
				
				return 'error', message
				
			else
			
				-- Done? really?
				return 'data', 'done'
				
			end
				
			
		end )
			
	end
	
	--------------------------------------------------------------------------------
	--[[
		Log the user out. Deletes all store tokens and pretend this session never
		existed.
	
		The promise may safely be ignored.
	]]--
	
	WFRequestDispatcher.onMessage( 'logoutP', function()
	
		-- Setting hasValidLoginB to true seems odd, but it's an essential part
		-- of getting things spun up correctly when we try to log this user in
		-- again. If we don't, there's a race condition (which usually blows up)
		-- in which the client UI reads hasValidLoginB as false and forces another
		-- logout before we can propagate the news that we have a new valid
		-- login again. [scouten 2011-11-30, PR# 3050626]
	
		hasValidLoginB:set( true )
	
		if not accessAndRefreshTokensP then
			return 'data', true		-- we're already set!
		end

		-- handle logout during token refresh
		if waitingTokenResolveFn then
			deleteAuthKeys()
			waitingTokenResolveFn = nil
			isWaitingForTokensB:set(false)
			return 'data', true
		end
		
		return accessAndRefreshTokensP:mapP( function( status, accessToken )
		
			deleteAuthKeys()
	
			if status == 'data' then
			
				-- FIXME [scouten 2011-11-21]: This assumes we're using IMS. Won't hold
				-- up if we ever need to do the OAuth2 dance with anyone else.
	
				return logoutP( sImsBaseUrl or KSPrefs.imsBaseUrl, accessToken )
	
			else
	
				return 'error', accessToken
	
			end
			
		end )
	
	end )
	
	--------------------------------------------------------------------------------
	
	WFRequestDispatcher.onMessage( 'DEBUG_dumpCoverageP', function( path )
	
		require 'SetupCoverage'.dumpCoverage( path )
	
		return require 'KSPromises'.resolvedPromiseP( 'data', true )
	
	end )

end

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

return {
	initMessageProcessor = initMessageProcessor,
}
