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

WFCatCollaboration.lua

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

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

----------------------------------------------------------------------------]]--
local Rx = require 'Rx'
local Observable = Rx.Observable

local KSIdGenerator = require 'KSIdGenerator'
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFLogger = require 'WFLogger'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFTableMerge = require 'WFTableMerge'
local WFRxScheduler = require 'WFRxScheduler'
local WFRxThrottledBatchedBulkApplyOverlay = require 'WFRxThrottledBatchedBulkApplyOverlay'
local WFRxThrottledBatchedBulkDocValue = require 'WFRxThrottledBatchedBulkDocValue'
local WFURI = require 'WFURI'

local WFCatCollaborationConstants = require 'WFCatCollaborationConstants'
local WFCatUtils = require 'WFCatUtils'

local KSJsonEncoder = require "KSJsonEncoder"
local KSJsonDecoder = require "KSJsonDecoder"
local KSDate = require "KSDate"
--------------------------------------------------------------------------------

local check = assert( WFArguments.check )
local KSTableUtils_derefOpt = assert( KSTableUtils.derefOpt )
local logger = WFLogger( 'WFCatCollaboration' )

local kMaxmiumDate = '9999-12-31T23:59:59Z'
local kOwner = 'Owner'
local kAccepted = 'accepted'
	-- http://apidocs.adbecrsl.com/v2/data-model/space_user/index.html

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

-- @mark WFCatCollaboration namespace

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

local WFCatCollaboration = {}

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

local function intToBool( zeroOrOne )
	return zeroOrOne ~= 0
end

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

local function boolToInt( isTrue )
	return isTrue and 1 or 0
end

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

function WFCatCollaboration.generateIndexSchema( indexStoreSchema )

	check( 'WFRxIndexStoreSchema' )

	-- table space

	indexStoreSchema:addTable {

		sql = [[ CREATE TABLE space( 
					created, 
					updateTimeStamp, 
					private, 
					download, 
					allowInviteRequests,
					showMetadata,
					showLocation,
					allowComments,
					allowFavorites,
					ownerId,
					outOfQuota,
					entitlementExpired,
					spaceUserId,
					thisFeedbackUserId,
					isDeleted,
					startingLink,
					currentUpdateLink,
					hide_url,
					isUnderMaintenance
				) ]],
				
		relevantTypeSubtypeTuples = {
			{ 'space', 'shared' },
		},

		hasOneRowPerDocument = true,
		
		map = function( docValue, emit )

			if docValue 
					and docValue.type == 'space' 
					and docValue.subtype == 'shared' then
					
				local thisSpaceUserId = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'this_space_user', 'id' )
				local thisFeedbackUserId = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'this_space_user', 'user', 'id' )	
					-- This is only set if it's a shared-with-you space.
					
				local isFilteredSpace = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'filteredSpace' )
				
				if not isFilteredSpace or not thisSpaceUserId then
						-- When an older client creates a filtered share (which are now deprecated in 
						-- the newer clients), Oz is tagging those shares with this specific flag. Newer
						-- clients completely ignore these shares. The exception here is for filtered
						-- shares that were created by the user and are *not* shared-with-you spaces.

					local localOnly = docValue._localOnly or {}
					
					local created = docValue.created or localOnly.created or
									KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'created' )
					
					local updateTimeStamp = docValue.userUpdated or docValue.updated
					
					local isDeleted = boolToInt( not not docValue._deleted )
					local startingLink = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', 'start', 'href' )
					local currentUpdateLink = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', 'current', 'href' )
					
					-- Owner Info
					
					local ownerId = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'owner', 'id' )
					local outOfQuota = boolToInt( KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'owner', 'outOfQuota' ) )
					local entitlementExpired = boolToInt( KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'owner', 'entitlementExpired' ) )
					local isUnderMaintenance = boolToInt( KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'owner', 'maintenanceLock' ) )
					
					-- Space Settings
					
					local isPrivate = boolToInt( docValue.private )
					local allowInviteRequests = boolToInt( docValue.allowInviteRequests )
					local allowDownloads = boolToInt( docValue.download )
					local showMetadata = boolToInt( docValue.metadata )
					local showLocation = boolToInt( docValue.location )
					local allowComments = boolToInt( not docValue.comments_disabled )
					local allowFavorites = boolToInt( not docValue.favorites_disabled )
					local hideUrl = boolToInt( docValue.hide_url )

					emit( created, updateTimeStamp, isPrivate, allowDownloads, allowInviteRequests,
							showMetadata, showLocation, allowComments, allowFavorites, ownerId,
							outOfQuota, entitlementExpired, thisSpaceUserId, thisFeedbackUserId, isDeleted, 
							startingLink, currentUpdateLink, hideUrl, isUnderMaintenance )
						
				end
					
			end
								
		end

	}

	indexStoreSchema:addIndex
			[[CREATE INDEX space_private_index ON space( docId, private )]]

	indexStoreSchema:addIndex
			[[CREATE INDEX space_entitlement_maintenance_index ON space( docId, entitlementExpired, isUnderMaintenance )]]

	-- table space_album

	indexStoreSchema:addTable {

		sql = [[CREATE TABLE space_album( spaceId, albumId, created, updateTimeStamp )]],
		
		relevantTypeSubtypeTuples = {
			{ 'space_album', 'NULL' },
		},

		map = function( docValue, emit )

			if docValue and not docValue._deleted
			  and docValue.type == 'space_album'
			then

				local localOnly = docValue._localOnly or {}
				
				local albumId = docValue.album and docValue.album.id
				local spaceId = docValue.space and docValue.space.id
				local updateTimeStamp = docValue.userUpdated or docValue.updated
				local created = docValue.created or localOnly.created or
								KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'created' )
				
				if spaceId and albumId then
					
					emit( spaceId, albumId, created, updateTimeStamp )
					
				end
					
			end
						
		end,

		hasOneRowPerDocument = true,

	}

	indexStoreSchema:addIndex [[CREATE INDEX space_album_index ON space_album( spaceId, albumId )]]
	indexStoreSchema:addIndex [[CREATE INDEX space_album_album ON space_album( albumId )]]
	
	
	-- table space_invite

	indexStoreSchema:addTable {

		sql = [[CREATE TABLE space_invite( subtype, spaceId, userId, email, status, role, created, updated, space_user )]],
		
		relevantTypeSubtypeTuples = {
			{ 'spaceinvite', 'request' },
			{ 'spaceinvite', 'single_use' },
		},

		map = function( docValue, emit )

			if docValue and not docValue._deleted then

				local subtype = KSTableUtils_derefOpt( docValue, 'subtype' )
				local spaceId = KSTableUtils_derefOpt( docValue, 'space', 'id' )
				local userId = KSTableUtils_derefOpt( docValue, 'user', 'id' )
				local created = KSTableUtils_derefOpt( docValue, 'userCreated' ) or KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'created' )
				local updated = KSTableUtils_derefOpt( docValue, 'userUpdated' ) or KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'updated' )
				local email = KSTableUtils_derefOpt( docValue, 'email' )
				local space_user = KSTableUtils_derefOpt( docValue, 'space_user', 'id' )
				local status = KSTableUtils_derefOpt( docValue, 'status' )
				local role = KSTableUtils_derefOpt( docValue, 'role' )
				
				if spaceId and subtype and created then
					
					emit( subtype, spaceId, userId, email, status, role, created, updated, space_user )
					
				end
					
			end
			
		end,

		useLocalId = true,
		hasOneRowPerDocument = true,
	}

	indexStoreSchema:addIndex [[CREATE INDEX space_invite_index ON space_invite( spaceId, subtype )]]
	indexStoreSchema:addIndex [[CREATE INDEX space_invite_user ON space_invite( userId )]]
	

	-- table space_user

	indexStoreSchema:addTable {

		sql = [[CREATE TABLE space_user( fullDocId, spaceId, userId, status, role, created, updated )]],
		
		relevantTypeSubtypeTuples = {
			{ 'space_user', 'NULL' },
		},
		
		map = function( docValue, emit )

			if docValue and not docValue._deleted then

				local spaceId = KSTableUtils_derefOpt( docValue, 'space', 'id' )
				local userId = KSTableUtils_derefOpt( docValue, 'user', 'id' )
				local status = KSTableUtils_derefOpt( docValue, 'status' )
				local created = KSTableUtils_derefOpt( docValue, 'userCreated' ) or KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'created' )
				local updated = KSTableUtils_derefOpt( docValue, 'userUpdated' ) or KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'updated' )
				local role = KSTableUtils_derefOpt( docValue, 'role' )
				
				if spaceId and userId then
					
					emit( docValue._id, spaceId, userId, status, role, created, updated )
					
				end
					
			end
						
		end,

		useLocalId = true,
		hasOneRowPerDocument = true,
	}

	indexStoreSchema:addIndex [[CREATE INDEX space_user_index ON space_user( spaceId )]]
	indexStoreSchema:addIndex [[CREATE INDEX space_user_user ON space_user( userId )]]
	indexStoreSchema:addIndex [[CREATE INDEX space_user_fullDocId ON space_user( fullDocId )]]
	
end

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

local function getSyncTerms()

	return {
		fetchSql = [[ SELECT
						space_album.docId, space_album.spaceId
					FROM
						space_album
					INNER JOIN
						space
					ON
						space_album.spaceId == space.docId
					WHERE
						albumId == ?]],
	}
end

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

function WFCatCollaboration.downgradeRoleIfNotSupported( role )

	-- Below are the current roles supported with the Lr ecosystem. If newer
	-- roles are introduced in the future that an older Lr app does not understand,
	-- the role is locally downgraded to 'viewer'.
	
	if not role or not WFCatCollaborationConstants.kRoles[ role ] then
		role = 'viewer'
	end
	
	return role

end

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

--[[!

	\brief Reports if the user as any shared albums that contain display filters.

	\param wfCatalog (WFCatalog)

	\return <code>Observable</code> Reports true if the user has shared albums
		that contain display filters. Otherwise, reports false.
	
--]]

function WFCatCollaboration.hasSharedAlbumsWithDisplayFilters( wfCatalog )

	check( "WFCatalog" )

	local columnName = 'albumId'
			
	if WFPlatformInfo.isMobile() then
		columnName = 'docId'
	end
			
	return wfCatalog:indexStore():queryOnce {
			sql = [[ SELECT space_album.albumId
						FROM space_album
						INNER JOIN albums ON albums.]] .. columnName .. [[ == space_album.albumId
						WHERE albums.hasDisplayFilters == 1 ]],
			quiesceUntilUpToDate = true,
		}
		
		:map( function( rows )
			
			local hasSharedAlbumsWithDisplayFilters = #rows > 0
			return hasSharedAlbumsWithDisplayFilters
			
		end )
	
end

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

--[[!

	\brief Get the ID of the unfiltered album ID that the album was migrated to.

	\param wfCatalog (WFCatalog)
	\param albumId (string|integer) album ID of the shared album.
	
	\return <code>Observable</code>  If the given album was migrated to an unfiltered 
		album, this observable resolves to the ID of the unfiltered album. Otherwise,
		this observable resolves to nil.
	
--]]

function WFCatCollaboration.getMigratedToUnfilteredAlbumId( wfCatalog, albumId )

	check( "WFCatalog", "string|number" )

	return wfCatalog:docStore():docValue( albumId )
	
		:map( function( docValue )
		
			local unfilteredAlbumId = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'migratedToUnfilteredAlbum', 'id' )

			return unfilteredAlbumId
			
		end )
	
end

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

local function maybeGetFullDocId( wfCatalog, maybeLocalId )

	if type( maybeLocalId ) == 'string' then
		return Observable.just( maybeLocalId )
	end
	
	return wfCatalog:docStore():localIdToDocId( maybeLocalId )
end

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

local function isEmailDomainAllowed( allowDomains, email )

	local emailDomain = string.match( email, "@(.*)" )
	
	if not emailDomain or emailDomain == "" then
		return false
	end
	
	emailDomain = string.lower( emailDomain )
	
	local function domainIsInAllowDomains( domain )
		for _, allowDomain in ipairs( allowDomains ) do
			if allowDomain == domain then
				return true
			end
		end
		
		return false
	end
	
	-- If initial check did not work, might be a subdomain so check until we get
	-- to a xxx.xxx domain, then stop checking for subdomains
	local subdomain = emailDomain
	local _, dotCount = string.gsub( subdomain, "%.", "%.")
	while dotCount > 0 do
		if domainIsInAllowDomains( subdomain ) then
			return true
		end

		subdomain = string.match( subdomain, "%.(.*)" )
		dotCount = dotCount - 1
	end
	
	return false
	
end

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

local function canInviteEmailAddress( sharingPolicies, email )

	local canInviteEmail = true
			
	if sharingPolicies.invitationSharingDisallowed then
	
		canInviteEmail = false
		
	elseif sharingPolicies.invitationSharingAllowDomains then

		canInviteEmail = isEmailDomainAllowed( sharingPolicies.allowDomains, email )
	
	end
	
	return canInviteEmail
end

WFCatCollaboration.DEBUG_canInviteEmailAddress = canInviteEmailAddress

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

function WFCatCollaboration.inviteEmailAllowed( wfCatalog, email )

	check( 'WFCatalog', '+string' )
	
	return wfCatalog:ozUser():sharingPolicies()
		:map( function( sharingPolicies )
		
			return canInviteEmailAddress( sharingPolicies, email )
			
		end )

end

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

function WFCatCollaboration.inviteByEmail( wfCatalog, maybeLocalAlbumId, email, optAccessLevel )

	local accessLevel = WFCatCollaboration.downgradeRoleIfNotSupported( optAccessLevel )

	return maybeGetFullDocId( wfCatalog, maybeLocalAlbumId )
		:flatMap( function( fullAlbumId )

			local syncTerms = getSyncTerms()

			return wfCatalog:indexStore():queryOnce( {
					sql = syncTerms.fetchSql,
					quiesceUntilUpToDate = true,
					fullAlbumId,
				} )
				:flatMap( function( idRows )

					local spaceId = KSTableUtils_derefOpt( idRows, 1, 2 )

					if spaceId then
					
						local id = KSIdGenerator.uuidInLowercaseHex()
					
						local baseDocValue = WFCatUtils.createBaseOverlayWithUserCreatedFields( id )

						local spaceinviteDocument = {
							type = 'spaceinvite',
							subtype = 'single_use',
							email = email,
							status = WFCatCollaborationConstants.kSingleUseInviteStatuses.open,
							role = accessLevel,
							space = {
								id = spaceId,
							},
						}
	
						WFTableMerge.applyOverlay( spaceinviteDocument, baseDocValue )
	
						return wfCatalog:docStore():setDocValue( spaceinviteDocument )
					end
					
					logger:warnf( "inviteByEmail without spaceId for album %s", fullAlbumId )
					return Observable.just( true )
					
				end )
		end )	
end

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

local function getNestedContributionCountQuery( isYou )

	local importedByUserCompare = '== space_user.userId'
	
	if isYou then
		importedByUserCompare = 'IS NULL'
			-- Assets imported by the current user have an empty importedByUser field.
	end
	
	local nestedCountQuery
	
	if WFPlatformInfo.isDesktop then
	
		nestedCountQuery = string.format( 
				[[ SELECT COUNT( DISTINCT album_asset_v2.docId ) 
						FROM album_asset_v2
							INNER JOIN assets USING( assetId )
						WHERE album_asset_v2.albumId == ?
							AND assets.importedByUser %s ]], importedByUserCompare )

	else
	
		nestedCountQuery = string.format( 
				[[ SELECT COUNT( DISTINCT album_asset.docId ) 
						FROM album_asset
							INNER JOIN coreInfo ON album_asset.assetId == coreInfo.docId
						WHERE album_asset.albumId == ?
							AND coreInfo.importedByUser %s ]], importedByUserCompare )
	
	end

	return nestedCountQuery

end

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

-- Fetch the contribution count for the syncing (current) user.

local function getContributionCountForYou( indexStore, fullAlbumId )
	
	local sql = getNestedContributionCountQuery( true )
	
	return indexStore:query( {
			sql = sql,
			quiesceUntilUpToDate = true,
			calm = 3,
			fullAlbumId
		} )
		
		:pluck( 1, 1 )
			
end

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

-- Fetch information about all of the members of a particular album. 
-- 
-- NOTE: This function does *not* fetch information about the owner of the album: 
-- the query in the function below only works for users that have space_user 
-- documents which owners do not have. See `getOwnerInforForAlbumStream`.
--
-- NOTE: This function does *not* fetch the correct contribution count for the
-- current user. For the syncing (current) user, assets that were contributed
-- by them will have `importedByUser` set as nil. For assets contributed by
-- everyone else, `importedByUser` will be the feedbackuser ID of the
-- contributor. Though this logic could be encapsulated into a single SQLite
-- query, it would be very complex. Instead, please use `getContributionCountForYou`
-- to fetch the syncing user's contribution count when needed.

local function getCollaboratorsStreamForAlbum( indexStore, fullAlbumId )
	
	local nestedCountQuery = getNestedContributionCountQuery()

	local sql = string.format(
			[[ SELECT space_user.docId, space_user.fullDocId, space_user.userId, space_user.status, 
						space_user.role, space_user.created, space_user.updated, feedback_user.fullName, 
						feedback_user.email, ( %s )
					FROM space_user
						INNER JOIN space_album ON space_user.spaceId == space_album.spaceId
						INNER JOIN feedback_user ON space_user.userId == feedback_user.userId
					WHERE space_album.albumId == ?
						AND space_user.status != 'abandoned' ]], nestedCountQuery )

	return indexStore:query( {
			sql = sql,
			quiesceUntilUpToDate = true,
			calm = 3,
			fullAlbumId,
			fullAlbumId
		} )
			
end

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

-- Fetch information about the owner of a shared album. Though this function
-- is similar to `getCollaboratorsStreamForAlbum`, it must be different
-- because owners do not have space_user documents.

local function getOwnerInfoForAlbumStream( indexStore, fullAlbumId )
	
	local nestedCountQuery
	
	if WFPlatformInfo.isDesktop then
	
		nestedCountQuery = [[ ( SELECT COUNT( DISTINCT album_asset_v2.docId ) 
									FROM album_asset_v2
										INNER JOIN assets USING( assetId )
									WHERE album_asset_v2.albumId == ?
										AND assets.importedByUser == space.ownerId ) ]]

	else
	
		nestedCountQuery = [[ ( SELECT COUNT( DISTINCT album_asset.docId ) 
									FROM album_asset
										INNER JOIN coreInfo ON album_asset.assetId == coreInfo.docId
									WHERE album_asset.albumId == ?
										AND coreInfo.importedByUser == space.ownerId ) ]]
	
	end

	local sql = string.format(
			[[ SELECT space.ownerId, feedback_user.fullName, feedback_user.email, %s
					FROM space
						INNER JOIN feedback_user ON space.ownerId == feedback_user.userId
						INNER JOIN space_album ON space.docId == space_album.spaceId
					WHERE space_album.albumId == ? ]], nestedCountQuery )

	return indexStore:query( {
			sql = sql,
			quiesceUntilUpToDate = true,
			calm = 3,
			fullAlbumId,
			fullAlbumId
		} )
			
end

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

local function getSpaceDocForAlbum( docStore, fullAlbumId )

	return docStore:docValue( fullAlbumId )
	
		:flatMap( function( albumDoc )
		
			local spaceId = albumDoc._spaceId
	
			if not spaceId then
				return Observable.just( false )
			end
			
			return docStore:docValue( spaceId )
		
		end )

end

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

function WFCatCollaboration.getCollaborators( wfCatalog, maybeLocalAlbumId )

	local indexStore = wfCatalog:indexStore()
	local docStore = wfCatalog:docStore()
	
	return maybeGetFullDocId( wfCatalog, maybeLocalAlbumId )

		:flatMapLatest( function( fullAlbumId )
		
			return Observable.combineLatest( 
					getCollaboratorsStreamForAlbum( indexStore, fullAlbumId ),
					getOwnerInfoForAlbumStream( indexStore, fullAlbumId ),
					getContributionCountForYou( indexStore, fullAlbumId ),
					getSpaceDocForAlbum( docStore, fullAlbumId ) )
		
		end )

		:map( function( collaboratorRows, ownerInfoRows, yourContributionCount, maybeSpaceDoc )
		
			local thisSpaceUserId
			
			if maybeSpaceDoc then
			
				thisSpaceUserId = KSTableUtils.derefOpt( maybeSpaceDoc, '_serverProvidedMetadata', 'this_space_user', 'id' )
			
			end
			
			local collaborators = {}
			
			for _, row in ipairs( collaboratorRows ) do

				local role = row[ 5 ]
				-- Hide Unknown roles from Invitee/Member lists
				if role and WFCatCollaborationConstants.kRoles[ role ] then

					local spaceUserId = row[ 2 ]
				
					local isYou = spaceUserId == thisSpaceUserId

					collaborators[ #collaborators + 1 ] = {
					
						id = row[ 1 ], -- space_user local ID
						userId = row[ 3 ], -- feedbackuser UUID
						status = row[ 4 ],
						role = role,
						created = row[ 6 ],
						updated = row[ 7 ],
						fullName = row[ 8 ],
						email = row[ 9 ],
						contributionCount = isYou and yourContributionCount or row[ 10 ],
						isYou = isYou,
						isSpaceOwner = false,
					}

				end

			end
			
			local ownerRow = KSTableUtils.derefOpt( ownerInfoRows, 1 )
			
			if ownerRow then
			
				collaborators[ #collaborators + 1 ] = {
				
					id = nil, -- No space_user doc for owners
					userId = ownerRow[ 1 ], -- feedbackuser UUID
					status = kAccepted,
					role = kOwner,
					created = nil,
					updated = nil,
					fullName = ownerRow[ 2 ],
					email = ownerRow[ 3 ],
					contributionCount = ownerRow[ 4 ],
					isYou = false,
					isSpaceOwner = true,
				}
			
			end
		
			return collaborators
		
		end )
		
end

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

function WFCatCollaboration.getPendingRequests( wfCatalog, maybeLocalAlbumId )

	return maybeGetFullDocId( wfCatalog, maybeLocalAlbumId )
		:flatMap( function( fullAlbumId )
			return wfCatalog:indexStore():query( {
				sql = [[ SELECT
							space_invite.docId, space_invite.userId, space_invite.created, space_invite.updated, space_invite.status, space_invite.role, space_invite.space_user, feedback_user.fullName, feedback_user.email
						FROM
							space_invite
						INNER JOIN
							space_album
						ON
							space_invite.spaceId == space_album.spaceId
						INNER JOIN
							feedback_user
						ON
							space_invite.userId == feedback_user.userId
						WHERE
							space_invite.subtype == 'request' AND space_album.albumId == ? ]],
				quiesceUntilUpToDate = true,
				calm = 2,

				fullAlbumId,
			} )
				:map( function( rows )
					local requests = {}
					
					for _, row in ipairs( rows ) do

						-- For pending requests, there is no role. For this particular
						-- scenario, defaulting to viewer _is_ the right course of action.

						local role = row[ 6 ]
						role = WFCatCollaboration.downgradeRoleIfNotSupported( role )

						requests[ #requests + 1 ] = {
							id = row[ 1 ],
							userId = row[ 2 ],
							created = row[ 3 ],
							updated = row[ 4 ],
							status = row[ 5 ],
							role = role,
							pendingApproval = row[ 7 ] == nil,
							fullName = row[ 8 ],
							email = row[ 9 ],
						}

					end
					
					return requests
				end )
		end )
end

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

function WFCatCollaboration.getInvitations( wfCatalog, maybeLocalAlbumId )

	return maybeGetFullDocId( wfCatalog, maybeLocalAlbumId )
		:flatMap( function( fullAlbumId )
			return wfCatalog:indexStore():query( {
				sql = [[ SELECT
							space_invite.docId, space_invite.email, space_invite.created, space_invite.updated, space_invite.status, space_invite.role, space_invite.userId
						FROM
							space_invite
						INNER JOIN
							space_album
						ON
							space_invite.spaceId == space_album.spaceId
						WHERE
							space_invite.subtype == 'single_use' AND space_album.albumId == ? ]],
				quiesceUntilUpToDate = true,
				calm = 2,

				fullAlbumId,
			} )
				:map( function( rows )
					local invitations = {}
					
					for _, row in ipairs( rows ) do
					
						local role = row[ 6 ]
						-- Hide Observers (New Space User Role) from the Invitee / Member Lists
						if role and WFCatCollaborationConstants.kRoles[ role ] then

							invitations[ #invitations + 1 ] = {
								id = row[ 1 ],
								email = row[ 2 ],
								created = row[ 3 ],
								updated = row[ 4 ],
								status = row[ 5 ],
								role = role,
								userId = row[ 7 ],
							}
						end

					end
					
					return invitations
				end )
		end )
end

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

function WFCatCollaboration.updateCollaborators( wfCatalog, updates )
	
	local docStore = wfCatalog:docStore()
	
	local docIds = {}
	for _, update in ipairs( updates ) do
		docIds[ #docIds + 1 ] = update.id
	end
	
	return WFRxThrottledBatchedBulkDocValue( "WFCatCollaboration_updateCollaborators", docStore, docIds, 100, 10 )
	
		:map( function( batchOfDocValues )
		
			local updateOverlays = {}
			
			for i, update in ipairs( updates ) do
			
				local docValue = batchOfDocValues[ i ]
				local docType = docValue.type
				local subType = docValue.subtype
				
				local optDelete = update.delete
				local optStatus = update.status
				local optRole = update.role
			
				local overlay
				
				if optDelete then
					overlay = {
						_id = docValue._id,
						_deleted = true,
					}
				else
					if docType ~= 'space_user' and docType ~= 'spaceinvite' then
						return Observable.throw( 'Not a collaborator document' )
					end
					
					if optRole and not WFCatCollaborationConstants.kRoles[ optRole ] then
						return Observable.throw( 'Unknown role' )
					end
					
					if optStatus ~= nil and (
						( docType == 'space_user' and not WFCatCollaborationConstants.kSpaceUserStatuses[ optStatus ] ) or
						( docType == 'spaceinvite' and subType == 'request' and not WFCatCollaborationConstants.kRequestInviteStatuses[ optStatus ] ) or
						( docType == 'spaceinvite' and subType == 'single_use' and not WFCatCollaborationConstants.kSingleUseInviteStatuses[ optStatus ] )
					) then
						return Observable.throw( 'Unknown status' )
					end
						
					overlay = WFCatUtils.createBaseOverlayWithUserUpdatedField( docValue._id )
					
					overlay.status = optStatus
					overlay.role = optRole
				end
				
				updateOverlays[ i ] = overlay
			
			end
			
			return docStore:bulkApplyOverlay( updateOverlays )
		
		end )
		
		:flatMerge( 1 )
		
		:takeLast( 1 ) -- Make sure there is only one data event and not multiple.
		
end

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

local function removeAllCollaborators( wfCatalog, albumId )
	
	local allCollaboratorsObservable = WFCatCollaboration.getCollaborators( wfCatalog, albumId )
		:take( 1 )

	local allPendingRequestsObservable = WFCatCollaboration.getPendingRequests( wfCatalog, albumId )
		:take( 1 )

	local allInvitationsObservable = WFCatCollaboration.getInvitations( wfCatalog, albumId )
		:take( 1 )
	
	return Observable.combineLatest( allCollaboratorsObservable, allPendingRequestsObservable, allInvitationsObservable )
		:flatMap( function( allCollaborators, allPendingRequests, allInvitations )
			
			local allOverlays = {}
			
			for _, collaborator in ipairs( allCollaborators ) do
				allOverlays[ #allOverlays + 1 ] = {
					_id = collaborator.id,
					_deleted = true,
				}
			end
			
			for _, request in ipairs( allPendingRequests ) do
				allOverlays[ #allOverlays + 1 ] =  {
					_id = request.id,
					_deleted = true,
				}
			end
			
			for _, invitation in ipairs( allInvitations ) do
				allOverlays[ #allOverlays + 1 ] =  {
					_id = invitation.id,
					_deleted = true,
				}
			end
			
			if #allOverlays == 0 then
				return Observable.just( true )
			end
			
			local docStore = wfCatalog:docStore()
			
			return WFRxThrottledBatchedBulkApplyOverlay( "removeAllCollaborators", docStore, allOverlays, 100, 10 )
			
		end )
end

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

function WFCatCollaboration.getAllSharedAlbums( wfCatalog ) 
	
	local columnName = 'albumId'
			
	if WFPlatformInfo.isMobile() then
		columnName = 'docId'
	end
	
	return wfCatalog:indexStore():query {
			sql = [[ SELECT space_album.albumId, space_album.spaceId, private, allowInviteRequests, hide_url
						FROM space_album 
						JOIN space ON space_album.spaceId == space.docId
						LEFT JOIN albums ON albums.]] .. columnName .. [[ == space_album.albumId
						WHERE albums.spaceId IS NULL ]],
						-- The WHERE clause filters out Shared-With-You albums.
			calm = 2,
		}
		:map( function( sharedAlbums )
			local results = {}
			for _, sharedAlbum in ipairs( sharedAlbums ) do  
				local result = {
					albumId = sharedAlbum[ 1 ],
					spaceId = sharedAlbum[ 2 ],
					isPrivate = intToBool( sharedAlbum[ 3 ] ),
					allowInviteRequests = intToBool( sharedAlbum[ 4 ] ),
					hide_url = intToBool( sharedAlbum[ 5 ] ),
				}
				table.insert( results, result )
			end
			return results
		end )
end

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

function WFCatCollaboration.getInfoOfAllAlbumOwnersByAlbumId( wfCatalog ) 
	
	return wfCatalog:indexStore():query {
			sql = [[ SELECT space_album.albumId, space.ownerId, feedback_user.fullName,
							space.outOfQuota, space.entitlementExpired
						FROM space_album
						INNER JOIN space ON space_album.spaceId == space.docId
						INNER JOIN feedback_user ON feedback_user.userId == space.ownerId ]],
			calm = 5,
		}
		:map( function( rows )
		
			local ownerInfoByAlbumId = {}
			
			for _, row in ipairs( rows ) do
			
				local albumId = row[ 1 ]
				local ownerId = row[ 2 ]
				local name = row[ 3 ]
				local outOfQuota = intToBool( row[ 4 ] )
				local subscriptionLapsed = intToBool( row[ 5 ] )
				
				ownerInfoByAlbumId[ albumId ] = {
					id = ownerId,
					name = name,
					outOfQuota = outOfQuota,
					subscriptionLapsed = subscriptionLapsed
				}
				
			end
			
			return ownerInfoByAlbumId
			
		end )
end

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

function WFCatCollaboration.getSharingStatusByAlbumId( wfCatalog )

	-- Note: getSharingStatusByAlbumId does not return valid sharing status
	-- attributes for SWY albums. It only works for user-owned, catalog albums.

	local indexStore = wfCatalog:indexStore()

	local function getCollaborationEntries( tableName, subType )
	
		local sqlStr = string.format( "SELECT spaceId, status FROM %s WHERE role IN ('%s', '%s', '%s')", tableName,
		WFCatCollaborationConstants.kRoles.viewer, WFCatCollaborationConstants.kRoles.contributor, WFCatCollaborationConstants.kRoles.editor )

		if subType then
			sqlStr = sqlStr .. string.format( " AND subType == '%s'", subType )
		end
	
		return indexStore:query( {
			sql = sqlStr,
			quiesceUntilUpToDate = true,
			calm = 2,
		} )
			:map( function( rows )
				
				local ret = {}
				for _, row in ipairs( rows ) do
					ret[ #ret + 1 ] = {
						space = row[ 1 ],
						status = row[ 2 ],
					}
				end
				
				return ret
				
			end )
			
	end
	
	local function groupBySpace( entriesObservable, filterFn )
		
		return entriesObservable
			:map( function( entries )
			
				local entriesBySpace = {}
				
				for _, entry in ipairs( entries ) do
					if filterFn( entry ) then
						local entriesForSpace = entriesBySpace[ entry.space ] or {}
						entriesForSpace[ #entriesForSpace + 1 ] = entry
						entriesBySpace[ entry.space ] = entriesForSpace
					end
				end
				
				return entriesBySpace
			
			end )
	end
	
	
	local collaboratorsBySpaceObservable = groupBySpace( getCollaborationEntries( 'space_user' ), function( entry )
		return entry.status == WFCatCollaborationConstants.kSpaceUserStatuses.accepted
	end )
	
	local requestsBySpaceObservable = groupBySpace( getCollaborationEntries( 'space_invite', 'request' ), function( entry )
		return entry.status == WFCatCollaborationConstants.kRequestInviteStatuses.open
	end )
	
	local invitationsBySpaceObservable = groupBySpace( getCollaborationEntries( 'space_invite', 'single_use' ), function( entry )
		return entry.status == WFCatCollaborationConstants.kSingleUseInviteStatuses.open
	end )
	
	return Observable.combineLatest(
		WFCatCollaboration.getAllSharedAlbums( wfCatalog ),
		collaboratorsBySpaceObservable,
		requestsBySpaceObservable,
		invitationsBySpaceObservable
	)
		:map( function( sharedAlbums, collaboratorsBySpace, requestsBySpace, invitationsBySpace )
			
			local retSharesByAlbumId = {}
			
			for _, sharedAlbum in ipairs( sharedAlbums ) do
			
				local albumId = sharedAlbum.albumId
				local spaceId = sharedAlbum.spaceId
				
				local hasCollaborators = collaboratorsBySpace[ spaceId ] and next( collaboratorsBySpace[ spaceId ] ) ~= nil
				local hasPendingRequests = requestsBySpace[ spaceId ] and next( requestsBySpace[ spaceId ] ) ~= nil
				local hasInvitations = invitationsBySpace[ spaceId ] and next( invitationsBySpace[ spaceId ] ) ~= nil

				local isSharedFromSettings = ( not sharedAlbum.isPrivate ) or ( not sharedAlbum.hide_url and sharedAlbum.allowInviteRequests )
				local isShared = ( isSharedFromSettings or hasCollaborators or hasPendingRequests or hasInvitations ) and true or false
				
				local nCollaborators, isPrivate
				
				if isShared then
					nCollaborators = collaboratorsBySpace[ spaceId ] and #collaboratorsBySpace[ spaceId ]
					isPrivate = sharedAlbum.isPrivate
				end
				
				retSharesByAlbumId[ albumId ] = {
					isShared = isShared,
					isPrivate = isPrivate,
					nCollaborators = nCollaborators,
				}
			end
			
			return retSharesByAlbumId
			
		end )
end

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

local function setAttribute( dst, src, attribute, fallbackToDefault, default )
	if src[ attribute ] ~= nil then
		dst[ attribute ] = src[ attribute ]
	elseif fallbackToDefault then
		dst[ attribute ] = default
	end
end

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

local function setSpaceAttributes( space, optAttributes, optApplyDefaults )
	local fallbackToDefault = optApplyDefaults
	if fallbackToDefault == nil then
		fallbackToDefault = true
	end
	
	local attr = optAttributes or {}
	setAttribute( space, attr, 'download', fallbackToDefault, false )
	setAttribute( space, attr, 'metadata', fallbackToDefault, false )
	setAttribute( space, attr, 'location', fallbackToDefault, false )
	setAttribute( space, attr, 'private', fallbackToDefault, true )
	setAttribute( space, attr, 'comments_disabled', fallbackToDefault, false )
	setAttribute( space, attr, 'favorites_disabled', fallbackToDefault, false )
	setAttribute( space, attr, 'allowInviteRequests', fallbackToDefault, true )
	setAttribute( space, attr, 'hide_url', fallbackToDefault, false )
	setAttribute( space, attr, 'open_upload_until', fallbackToDefault, nil )

	local isAllowAnyoneCanUpload = KSTableUtils_derefOpt( attr, 'allow_anyone_can_upload' )

	--set `open_upload_until` when clients set 'allow_anyone_can_upload' to true otherwise just remove `open_upload_until`
	if isAllowAnyoneCanUpload then
		space[ 'open_upload_until' ] = kMaxmiumDate
	elseif isAllowAnyoneCanUpload == false then
		space[ 'open_upload_until' ] = { _delete = true }
	end
	
	space.presentationFilters = true
	return space
end

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

local function setAlbumAttributes( albumOverlay, optAttributes, optApplyDefaults )

	local fallbackToDefault = optApplyDefaults
	
	if fallbackToDefault == nil then
		fallbackToDefault = true
	end
	
	local attr = optAttributes or {}

    local story = albumOverlay.story or {}
	setAttribute( story, attr, 'author', false )
	setAttribute( story, attr, 'title', false )
	setAttribute( story, attr, 'description', false )
	albumOverlay.story = story
	
	local presentation = albumOverlay.presentation or {}
	
	local options = presentation.options or {}
	setAttribute( options, attr, 'showTitle', fallbackToDefault, true )
	setAttribute( options, attr, 'showAuthor', fallbackToDefault, true )
	setAttribute( options, attr, 'showCaptions', fallbackToDefault, false )
	setAttribute( options, attr, 'showCover', fallbackToDefault, false )
	setAttribute( options, attr, 'cover', false )
	presentation.options = options
	
	local explore = presentation.explore or {}
	setAttribute( explore, explore, 'version', fallbackToDefault, 1 )
	setAttribute( explore, attr, 'themeId', fallbackToDefault, 'Gallery' )
	setAttribute( explore, attr, 'styleId', fallbackToDefault, 'Dark' )
	presentation.explore = explore
	
	local filters = presentation.filters or {}
	setAttribute( filters, attr, 'pickedOnly', false, false )
	setAttribute( filters, attr, 'ratingAtLeast', false, 0 )
		-- pickedOnly and ratingAtLeast do not use fallbackToDefault because
		-- these fields are essentially deprecated as of the August release.
	presentation.filters = filters
	
	albumOverlay.presentation = presentation
	return albumOverlay
	
end

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

local function getAlbumAttributes( attributes, optAlbumDoc )

	local doc = optAlbumDoc
	if attributes and doc then
		local story = KSTableUtils_derefOpt( doc, 'story'  ) or {}
		setAttribute( attributes, story, 'author', false )
		setAttribute( attributes, story, 'title', false )
		setAttribute( attributes, story, 'description', false )
	
		local options = KSTableUtils_derefOpt( doc, 'presentation', 'options' ) or {}
		setAttribute( attributes, options, 'showTitle', true, true )
		setAttribute( attributes, options, 'showAuthor', true, true )
		setAttribute( attributes, options, 'showCaptions', true, false )
		setAttribute( attributes, options, 'showCover', true, false )
		setAttribute( attributes, options, 'cover', false )
		
		local explore = KSTableUtils_derefOpt( doc, 'presentation', 'explore' ) or {}
		setAttribute( attributes, explore, 'themeId', true, 'Gallery' )
		setAttribute( attributes, explore, 'styleId', true, 'Dark' )
		
		local filters = KSTableUtils_derefOpt( doc, 'presentation', 'filters' ) or {}
		setAttribute( attributes, filters, 'pickedOnly', true, false )
		setAttribute( attributes, filters, 'ratingAtLeast', true, 0 )
    end 
	return attributes
end

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

local function getSharingPolicyAttributes( wfCatalog , attributes )

	local fallbackToDefault = true
	
	return wfCatalog:ozUser():sharingPolicies()
		:map( function( sharingPolicies )
			
			setAttribute( attributes, sharingPolicies, 'invitationSharingDisallowed', fallbackToDefault, false )
			setAttribute( attributes, sharingPolicies, 'invitationSharingAllowDomains', fallbackToDefault, false )
			setAttribute( attributes, sharingPolicies, 'publicSharingDisallowed', fallbackToDefault, false )
			setAttribute( attributes, sharingPolicies, 'requestAccessSharingDisallowed', fallbackToDefault, false )
			setAttribute( attributes, sharingPolicies, 'anyoneCanUploadDisallowed', fallbackToDefault, false )
			setAttribute( attributes, sharingPolicies, 'allowDomains', false )
			
			setAttribute( attributes, sharingPolicies, 'feedbackDisallowed', fallbackToDefault, false )
			setAttribute( attributes, sharingPolicies, 'communityPostingDisallowed', fallbackToDefault, false )
			
			return attributes
			
		end )

end

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

local function createSpaceDocument( spaceId, optAttributes )

	local baseDocValue = WFCatUtils.createBaseOverlayWithUserCreatedFields( spaceId )

	local spaceDocument = {
		type = 'space',
		subtype = "shared",
	}
	
	WFTableMerge.applyOverlay( spaceDocument, baseDocValue )
	
	return setSpaceAttributes ( spaceDocument, optAttributes )
end

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

local function createSpaceAlbumDocument( spaceAlbumId, spaceId, fullDocId )
	
	local baseDocValue = WFCatUtils.createBaseOverlayWithUserCreatedFields( spaceAlbumId )
	
	local spaceAlbumDoc = {
		type = 'space_album',
		space = {
			id = spaceId,
		},
		album = {
			id = fullDocId,
		}
	}
	
	WFTableMerge.applyOverlay( spaceAlbumDoc, baseDocValue )
	
	return spaceAlbumDoc

end

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

--  create space locally when online call fails
local function createSpaceLocally( wfCatalog, spaceId, albumId, spaceAlbumId, optAttributes )
	
	-- if we fail at calling oz, fall back to creating locally and syncing when we get back online ( URL will not update )
	logger:warnf( "Sharing album locally %s", albumId )
	
	local spaceDocument = createSpaceDocument( spaceId, optAttributes )

	local spaceAlbumDocument = createSpaceAlbumDocument( spaceAlbumId, spaceId, albumId )

	local newDocsObservable = wfCatalog:docStore():bulkSetDocValue {
		spaceDocument,
		spaceAlbumDocument,
	}
	
	local albumOverlay = setAlbumAttributes( { _id = albumId, }, optAttributes )
	
	local updatedAlbumObservable = wfCatalog:docStore():applyOverlay( albumOverlay )
	
	return Observable.combineLatest( newDocsObservable, updatedAlbumObservable )
		:flatMap( function()
			
			local attributes = setSpaceAttributes ( { id = spaceDocument._id }, spaceDocument )

			getAlbumAttributes( attributes, albumOverlay )

			return getSharingPolicyAttributes( wfCatalog , attributes )
		
		end )
		
end

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

local function getSpaceDocumentFromOz( wfCatalog, catalogSpec, spaceDocumentUrl, spaceId, albumId, spaceAlbumId, optAttributes )
		
	return catalogSpec:get( spaceDocumentUrl, {} )
	:flatMap( function( data, headers )

		if headers and headers.status_code == "200" then
			local decodedSpaceJson = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( data )
			local spaceDocument = require 'WFOzDocUtils'.remapBasicOzDocument( decodedSpaceJson, decodedSpaceJson.base )
			
			local href = KSTableUtils_derefOpt( spaceDocument, '_serverProvidedMetadata', 'links', '/rels/web_space', 'href'  )

			local spaceAlbumDocument = {
				type = "space_album",
				_id = spaceAlbumId,
				space = {
					id = spaceId,
				},
				album = {
					id = albumId,
				}
			}
			
			local newDocsObservable = wfCatalog:docStore():bulkSetDocValue {
				label = 'oz',
				spaceDocument,
				spaceAlbumDocument,
			}
			
			local albumOverlay = setAlbumAttributes( { _id = albumId, }, optAttributes )
			
			local updatedAlbumObservable = wfCatalog:docStore():applyOverlay( albumOverlay )
			
			return Observable.combineLatest( newDocsObservable, updatedAlbumObservable )
				:flatMap( function()
				
					local attributes = setSpaceAttributes( { id = spaceId, url = href }, spaceDocument )
					
					getAlbumAttributes( attributes, albumOverlay )
					
					return getSharingPolicyAttributes( wfCatalog , attributes )
				end )
		else
			return createSpaceLocally( wfCatalog, spaceId, albumId, spaceAlbumId, optAttributes )
		end
	end )

end
	
--------------------------------------------------------------------------------

local function createShareWithOz( wfCatalog, fullDocId, optAttributes, optOutIfNoneMatchHeader, optForceNoOz --[[bool]] )

	-- if we don't already have a URL, request it directly from oz
	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	local spaceAlbumId = KSIdGenerator.uuidInLowercaseHex()
	local payload = setSpaceAttributes( {} , optAttributes )
	local data = KSJsonEncoder.encodeOzUploadJson(
		{
			payload= payload,
			subtype = "shared",
			resources = {
				{
					id = fullDocId,
					type = "album",
					space_album_id = spaceAlbumId,
				}
			}
		}
	)
	
	local catalogSpec = wfCatalog:ozCatalogSpec()
	
	if WFPlatformInfo.isUnitTest() and optForceNoOz then
		-- TODO: Fix unit tests to remove this check from production code
		-- If we are in a test - force creation of local space and do not reach out to oz
		return createSpaceLocally( wfCatalog, spaceId, fullDocId, spaceAlbumId, optAttributes )
	end
	
	local NoneMatchHeader = not optOutIfNoneMatchHeader and { header = 'If-None-Match', value = fullDocId } or nil
	
	local requestUrl = string.format( '%s/spaces/%s', catalogSpec:catalogHref(), spaceId )
	return catalogSpec:put( requestUrl, data,
			{ 	requestMethod = 'PUT',
				requestHeaders = {
					{ header = 'Content-Type', value = 'application/json' },
					NoneMatchHeader
				},
			} )
	:delay( 0.5, WFRxScheduler.sharedScheduler() )
	:flatMap( function( data, headers )

		if headers and headers.status_code == "201" then
			return getSpaceDocumentFromOz( wfCatalog, catalogSpec, requestUrl, 
											spaceId, fullDocId, spaceAlbumId, optAttributes )
		else
			return createSpaceLocally( wfCatalog, spaceId, fullDocId, spaceAlbumId, optAttributes )
		end
		
	end )
	
	:catch( function( response, headers ) 
		
		if headers and headers.status_code == "412" then
		
			local decodedErrorResponse = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( response )
			
			local errorData = KSTableUtils_derefOpt( decodedErrorResponse, 'errors' )
			local ozSpace = errorData.spaces and errorData.spaces[ 1 ]
			local ozSpaceId = KSTableUtils_derefOpt( ozSpace, 'id' )
			local ozSpaceUrl = KSTableUtils_derefOpt( ozSpace, 'href' )
			
			local ozSpaceResources = errorData.space_resources and errorData.space_resources[ 1 ]
			local ozSpaceAlbumId = ( KSTableUtils_derefOpt( ozSpaceResources, 'type' ) == 'album' ) 
										and KSTableUtils_derefOpt( ozSpaceResources, 'id' ) or nil
		
			if ozSpaceId and ozSpaceUrl and ozSpaceAlbumId then
			
				return getSpaceDocumentFromOz( wfCatalog, catalogSpec, ozSpaceUrl, 
												ozSpaceId, fullDocId, ozSpaceAlbumId, optAttributes )
				
			end

		end
		
		return createSpaceLocally( wfCatalog, spaceId, fullDocId, spaceAlbumId, optAttributes )
		
	end )
	
end

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

-- keeping this around because it is useful for testing and debugging
--local function getShareFromOz( catalog, spaceId )
--	-- if we don't already have a URL, request it directly from oz
--	local KSJsonDecoder = require "KSJsonDecoder" 
--	local catalogSpec = catalog:ozCatalogSpec()
--	local requestUrl = string.format( '%sspaces/%s', catalogSpec:baseHref(), spaceId )
--	return catalogSpec:get( requestUrl, { } )
--	:map( KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty )
--	:map( function( docValue )
--		local href = KSTableUtils_derefOpt( docValue, 'links', '/rels/web_space', 'href'  )
--		return setSpaceAttributes ( { id = spaceId, url = href }, docValue )
--	end )
--end

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

local function shareAlbum( wfCatalog, docValue, spaceSubtype, optAttributes, optOutIfNoneMatchHeader, optForceNoOz )
		
	-- The current sharing model for albums calls for a 1-1 mapping from an 'album' document to
	-- a 'space'. In order to share the document, a 'space' needs to be created and the document
	-- added to it.

	-- This function creates a space and adds the provided album to it by creating a
	-- 'space_album' document as appropriate. If a space already exists the existing space is used
	-- if optional attributes are given, they will be used in the new space or updated in the existing one
	-- This returns the attributes of the space
	
	local syncTerms = getSyncTerms()
	
	local fullDocId = docValue._id
		
	return wfCatalog:indexStore():queryOnce {
			sql = syncTerms.fetchSql,
			quiesceUntilUpToDate = true,
			fullDocId
		}
		:flatMap( function( idRows )

			-- If there is a space for this album, get the ID for that space.
			local spaceId = KSTableUtils_derefOpt( idRows, 1, 2 )

			if spaceId then

				local spaceOverlay = WFCatUtils.createBaseOverlayWithUserCreatedFields( spaceId )
				setSpaceAttributes( spaceOverlay, optAttributes )
				
				local albumOverlay = setAlbumAttributes( { _id = fullDocId, }, optAttributes )
				
				return wfCatalog:docStore():bulkApplyOverlay( { spaceOverlay, albumOverlay } )
					:flatMap( function()
						-- now read the updated space and return it
						return wfCatalog:docStore():docValue( spaceId )
						:flatMap( function( spaceDocValue )
							local href = KSTableUtils_derefOpt( spaceDocValue, '_serverProvidedMetadata', 'links', '/rels/web_space', 'href'  )
							
							local attributes = setSpaceAttributes( { id = spaceId, url = href }, spaceDocValue )
							
							getAlbumAttributes( attributes, albumOverlay )
							
							return getSharingPolicyAttributes( wfCatalog , attributes )
						end )
					end )
			else  -- no space exists, so create one
				return createShareWithOz( wfCatalog, fullDocId, optAttributes, optOutIfNoneMatchHeader, optForceNoOz )
			end
		end )
end

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

local function unshareAlbum( wfCatalog, fullDocId )

	local syncTerms = getSyncTerms()
		
	return wfCatalog:indexStore():queryOnce {
			sql = syncTerms.fetchSql,
			quiesceUntilUpToDate = true,
			fullDocId,
		}
		:flatMap( function( idRows )
		
			local spaceId = KSTableUtils_derefOpt( idRows, 1, 2 )
			
			if not spaceId then
				return 'error', "album is not shared"
			end
			
			local overlay = WFCatUtils.createBaseOverlayWithUserCreatedFields( spaceId )
			overlay.private = true
			overlay.allowInviteRequests = false
				
			return wfCatalog:docStore():applyOverlay( overlay )
				:flatMap( function()
					return removeAllCollaborators( wfCatalog, fullDocId )
				end )
					
		end )
end

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

local function deleteAlbumShare( wfCatalog, fullDocId )

	local syncTerms = getSyncTerms()
	
	return removeAllCollaborators( wfCatalog, fullDocId )
		:flatMap( function()
	
			return wfCatalog:indexStore():queryOnce {
					sql = syncTerms.fetchSql,
					quiesceUntilUpToDate = true,
					fullDocId,
				}
				:flatMap( function( idRows )
					
					-- SPAM [matthewj 2017-06-02]: Currently the sharing model is a
					-- 1-1 space to album relationship. Because of this when an 
					-- album is deleted, we know the space can be. As can all (only 1)
					-- space_album records. If this changes or additional
					-- space types are included in these shares this deletion needs
					-- to be rethought.
					local spaceResourceId = KSTableUtils_derefOpt( idRows, 1, 1 )
					local spaceId = KSTableUtils_derefOpt( idRows, 1, 2 )
					
					local deleteSpaceResource
					if spaceResourceId then
						deleteSpaceResource = wfCatalog:docStore():bulkApplyOverlay( 
						{
							label = 'oz', -- don't send space resource deletes to Oz
							{ 
								_id = spaceResourceId, 
								_deleted = true,
							}
						} )
					else
						deleteSpaceResource = Observable.empty()
					end
					
					local deleteSpace
					if spaceId then
						deleteSpace = wfCatalog:docStore():deleteDoc( spaceId )
					else
						deleteSpace = Observable.empty()
					end
					
					return Observable.combineLatest( deleteSpace, deleteSpaceResource )
					
				end )
				:takeLast( 0 ) 
				
		end )
end

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

local function sharedAlbumUrl( wfCatalog, fullDocId )

	local syncTerms = getSyncTerms()
	
	return wfCatalog:indexStore():query {
			sql = syncTerms.fetchSql,
			fullDocId
		}
		:pluck( 1, 2 )
		:flatMapLatest( function( spaceId )
			
			if spaceId then
				return wfCatalog:docStore():ongoingDocValue( spaceId )
					:pluck( '_serverProvidedMetadata', 'links', 
							'/rels/web_space', 'href' )
			end
			
			return Observable.just( nil )
		
		end )
		:distinctUntilChanged()
end

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

local function distinctTableCompare( _, tbl1, _, tbl2 )

	return KSTableUtils.deepEquals( tbl1, tbl2 )

end

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

local function getShareAttributes( wfCatalog, fullDocId )

	local syncTerms = getSyncTerms()

	return wfCatalog:indexStore():query {
			sql = syncTerms.fetchSql,
			fullDocId,
		}
		:pluck( 1, 2 ) -- Extract spaceId
		:filter( function( spaceId )
			return spaceId -- Only continue the Rx Chain if spaceId is not null.
		end )
		:take( 1 ) -- this take( 1 ) effectively closes down the index store query.
		:flatMap( function( spaceId )
		
			local spaceDocObservable = wfCatalog:docStore():ongoingDocValue( spaceId )
			local albumDocObservable = wfCatalog:docStore():ongoingDocValue( fullDocId )
			
			return Observable.combineLatest( spaceDocObservable, albumDocObservable )
				:flatMap( function( spaceDoc, maybeAlbumDoc )
				
					local href = KSTableUtils_derefOpt( spaceDoc, '_serverProvidedMetadata', 'links', '/rels/web_space', 'href'  )
					
					local attributes = setSpaceAttributes ( { id = spaceId, url = href }, spaceDoc )

					local currentDate = KSDate.timeInIso8601FormatNearestSecond()

					local openUntilDate = KSTableUtils_derefOpt( attributes, 'open_upload_until' )
					
					if openUntilDate == nil then
						attributes [ 'allow_anyone_can_upload' ] = false
					elseif KSDate.differenceInSeconds( currentDate, openUntilDate ) > 0 then
						attributes [ 'allow_anyone_can_upload' ] = true
					else
						attributes [ 'allow_anyone_can_upload' ] = false
					end
					
					getAlbumAttributes( attributes, maybeAlbumDoc )
					
					return getSharingPolicyAttributes( wfCatalog , attributes )
					
				end )
				
		end )
		:distinctUntilChanged( nil, distinctTableCompare )
		
end

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

local function setShareAttributes( wfCatalog, docValue, attributes )

	local syncTerms = getSyncTerms()
	
	local fullDocId = docValue._id

	return wfCatalog:indexStore():queryOnce {
			sql = syncTerms.fetchSql,
			quiesceUntilUpToDate = true,
			fullDocId,
		}
		:flatMap( function( idRows )
			local spaceId = KSTableUtils_derefOpt( idRows, 1, 2 )
			if not spaceId then
				return Observable.throw( "album is not shared" )
			end
			
			local spaceOverlay = setSpaceAttributes( { _id = spaceId, }, attributes, false )
			local albumOverlay = setAlbumAttributes( { _id = fullDocId, }, attributes, false )
			
			return wfCatalog:docStore():bulkApplyOverlay( { spaceOverlay, albumOverlay } )
				:map( function()
					return true
				end )
		end )
end

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

function WFCatCollaboration.sharedAlbumUrl( wfCatalog, maybeLocalAlbumId )

	return maybeGetFullDocId( wfCatalog, maybeLocalAlbumId )
		:flatMap( function( fullAlbumId )
			return sharedAlbumUrl( wfCatalog, fullAlbumId )
		end )
end				

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

function WFCatCollaboration.waitForAlbumSync( wfCatalog, albumId )

	-- entrypoint for a end of sync test...
	
	return wfCatalog:docStore():ongoingDocValue( albumId )
		
		:pluck( '_serverProvidedMetadata' )
		
		:filter( function( spm )
   			return not not spm
		end )
		
		:take( 1 )
		
		:flatMap( function()
			return Observable.just( albumId )
		end	)
end

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

--[[!
	\brief deletes an album share. Generally only use this when the album itself is being 
		deleted. In most cases, this is not what you want as the share is
		permanently deleted. Unsharing an album removes the share from public view but
		keeps it's metadata (particularly it's URL) which allows share/unshare/share to
		be a non-destructive workflow. 
		
	\param albumDocId (string) The unique id of the album to remove shares for.
	
	\return <code>Observable< ></code> An Observable object that completes when 
		the operation is completed; or resolves to an error if an error occurs.
]]--
function WFCatCollaboration.deleteSharedAlbum( wfCatalog, albumDocId )
		
	return deleteAlbumShare( wfCatalog, albumDocId )
	
end

--------------------------------------------------------------------------------
--[[!
	\brief shares an album, creating the associated space and space_album documents.

	\param albumId (string|int) The unique or local ID of the album.
	\param optAttributes ( table ) optional attributes for creating the shared space
	\param optOutIfNoneMatchHeader ( boolean ) optional boolean to opt out If-None-Match header 
												with RESTful create space API

	\return <code>Observable< ></code> An Observable object completes when operation
		is completed; or resolves to an error if an error occurs.
]]--
function WFCatCollaboration.shareAlbum( wfCatalog, albumId, optAttributes, optOutIfNoneMatchHeader, optForceNoOz )

	return wfCatalog:docStore():docValue( albumId )
		:flatMap( function( docValue )
			return shareAlbum( wfCatalog, docValue, 'shared', optAttributes, optOutIfNoneMatchHeader, optForceNoOz )
		end )
		
end

--------------------------------------------------------------------------------
--[[!
	\brief Unshares an album, deleting the associated space and space_album documents.

	\param albumId (string|int) The unique or local ID of the album.

	\return <code>Observable< ></code> An Observable object completes when operation
		is completed; or resolves to an error if an error occurs.
]]--
function WFCatCollaboration.unshareAlbum( wfCatalog, maybeLocalAlbumId )
	
	return maybeGetFullDocId( wfCatalog, maybeLocalAlbumId )
		:flatMap( function( fullAlbumId )
			return unshareAlbum( wfCatalog, fullAlbumId )
		end )

end

-------------------------------------------------------------------------------
--[[!
	\brief get shared album attributes.

	\param albumId (string|int) The unique or local ID of the album.

	\return <code>Observable< ></code> An Observable object completes when operation
		is completed; or resolves to an error if an error occurs.
]]--
function WFCatCollaboration.getSharedAlbumAttributes( wfCatalog, maybeLocalAlbumId )

	return maybeGetFullDocId( wfCatalog, maybeLocalAlbumId )
		:flatMap( function( fullAlbumId )
			return getShareAttributes( wfCatalog, fullAlbumId )
		end )
		
end

-------------------------------------------------------------------------------
--[[!
	\brief set shared album attributes.

	\param albumId (string|int) The unique or local ID of the album.
	\param attributes ( table ) updated attributes for the shared space

	\return <code>Observable< ></code> An Observable object completes when operation
		is completed; or resolves to an error if an error occurs.
]]--
function WFCatCollaboration.setSharedAlbumAttributes( wfCatalog, albumId, attributes )

	return wfCatalog:docStore():docValue( albumId )
		:flatMap( function( docValue )
			return setShareAttributes( wfCatalog, docValue, attributes )
		end )
		
end

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

--[[!
	\brief Deletes display filter settings from the specified album.
	
	\param catalog (WFCatalog)
	\param albumId (string|number) The unique or local ID of the album.

	\return <code>Observable< ></code> An Observable object completes when operation
		is completed; or resolves to an error if an error occurs.
]]--

function WFCatCollaboration.deleteDisplayFiltersFromAlbum( wfCatalog, albumId )

	local overlay = {
		_id = albumId,
		presentation = {
			filters = {
				_delete = true
			}
		}
	}

	return wfCatalog:docStore():applyOverlay( overlay )

end

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

--[[!
	\brief Get the URL for a shared album or asset within a shared album.
	
	\param catalog (WFCatalog)
	\param albumId (string|number) The full doc ID or local ID of the album.
	\param optAssetId (optional string|number) An optional asset id.
	
	\return <code>Observable< ></code> An Observable that fires with a URL. If
		no valid URL can be built, false is returned.
]]--

function WFCatCollaboration.getLrWebShareUrl( wfCatalog, albumId, optAssetId )

	check( 'WFCatalog', '+string|number', '?string|number' )
	
	-- Make sure we're working with full doc IDs.
	
	-- SPAM [jandrew 2019-08-19]: Note that we may need to return the true
	-- catalog doc IDs here instead of the space-feed doc IDs.
	
	local docStore = wfCatalog:docStore()
	
	local albumUuidStream = maybeGetFullDocId( wfCatalog, albumId )
	
	local assetDocValueStream = optAssetId and docStore:docValue( optAssetId )
			or Observable.just( nil )
			
	return Observable.combineLatest( albumUuidStream, assetDocValueStream )
		
		:flatMap( function( albumUuid, maybeAssetDocValue )
		
			-- Get the space_album associated with the album.
	
			return wfCatalog:indexStore():queryOnce {
					sql = [[ SELECT space_album.spaceId
								FROM space_album
								WHERE space_album.albumId == ?]],
					quiesceUntilUpToDate = true,
					albumUuid
				}
				
				:flatMap( function( rows )
				
					-- Use the spaceId from the space_album to build the URL.
		
					local spaceId = KSTableUtils_derefOpt( rows, 1, 1 )

					if spaceId then
					
						return docStore:docValue( spaceId )
					
					else
					
						return Observable.just( false )
					
					end

				end )

				:map( function( spaceDocValue )

					if not spaceDocValue then
						return false
					end
					
					local spaceId = spaceDocValue._id
					
					local activityUrl
				
					if maybeAssetDocValue then
						
						local reviews = KSTableUtils_derefOpt( maybeAssetDocValue, 'reviews' ) or {}
						
						local ownerId = KSTableUtils_derefOpt( spaceDocValue, '_serverProvidedMetadata', 'owner', 'id' )
								or wfCatalog:ozUserId()
						
						local ownerReview = reviews[ ownerId ]
						
						local flaggedAsReject = not not ( KSTableUtils_derefOpt( ownerReview, 'flag' ) == 'reject' )
						
						if not flaggedAsReject then
						
							local assetUuid = maybeAssetDocValue._id
							
							activityUrl = string.format( "/shares/%s/albums/%s/assets/%s/activity",
									spaceId, albumUuid, assetUuid )
					
						end
					
					end
					
					if not activityUrl then

						activityUrl = string.format( "/shares/%s", spaceId )
					
					end

					local lrwProdUrl = "https://lightroom.adobe.com"
					local lrwStageUrl = "https://stage.adobelr.com"
					
					local catSpec = wfCatalog:ozCatalogSpec()
					
					local baseUrl = catSpec:isStageEnv() and lrwStageUrl or lrwProdUrl

					local lrwUrl = WFURI.resolve( baseUrl, activityUrl )

					return lrwUrl
					
				end )
		
		end )

end

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

--[[!
	\brief Create Space and get the URL for a shared album or asset within a shared album.
			Doesn't check if space already exists with the album or asset
	
	\param catalog (WFCatalog)
	\param fullDocId (string) The full doc ID
	\param optAttributes (optional table) optional attributes for creating the shared space
	\param optOutIfNoneMatchHeader ( boolean ) optional boolean to opt out If-None-Match header 
												with RESTful create space API
	
	\return <code>Observable< ></code> An Observable object completes when operation
		is completed; or resolves to an error if an error occurs.
]]--

function WFCatCollaboration.DEBUG_createShareWithOz( wfCatalog, fullDocId, optAttributes, optOutIfNoneMatchHeader, optForceNoOz )

	check( 'WFCatalog', '+string', '?table', '?boolean' )
	
	return createShareWithOz( wfCatalog, fullDocId, optAttributes, optOutIfNoneMatchHeader, optForceNoOz )

end

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

return WFCatCollaboration
