
// appUsers provides functions to sign Seekia users in and out, and to manage Seekia users
// These are the users that are displayed in the gui upon starting Seekia
// Each user has its own data folder, allowing each user to operate different identities.

package appUsers

// The functions in this package are not safe for concurrency
// The GUI should prevent any concurrent use of these functions

import "seekia/internal/appMemory"
import "seekia/internal/backgroundJobs"
import "seekia/internal/desires/myLocalDesires"
import "seekia/internal/encoding"
import "seekia/internal/genetics/geneticReferences/locusMetadata"
import "seekia/internal/genetics/geneticReferences/monogenicDiseases"
import "seekia/internal/genetics/geneticReferences/polygenicDiseases"
import "seekia/internal/genetics/geneticReferences/traits"
import "seekia/internal/genetics/myAnalyses"
import "seekia/internal/genetics/myCouples"
import "seekia/internal/genetics/myGenomes"
import "seekia/internal/genetics/myPeople"
import "seekia/internal/genetics/trainedPredictionModels"
import "seekia/internal/localFilesystem"
import "seekia/internal/logger"
import "seekia/internal/messaging/myChatConversations"
import "seekia/internal/messaging/myChatFilters"
import "seekia/internal/messaging/myChatKeys"
import "seekia/internal/messaging/myChatMessages"
import "seekia/internal/messaging/myCipherKeys"
import "seekia/internal/messaging/myConversationIndexes"
import "seekia/internal/messaging/myMessageQueue"
import "seekia/internal/messaging/myReadStatus"
import "seekia/internal/messaging/mySecretInboxes"
import "seekia/internal/messaging/peerChatKeys"
import "seekia/internal/messaging/peerDevices"
import "seekia/internal/messaging/peerSecretInboxes"
import "seekia/internal/messaging/sendMessages"
import "seekia/internal/moderation/myHiddenContent"
import "seekia/internal/moderation/mySkippedContent"
import "seekia/internal/moderation/viewedContent"
import "seekia/internal/moderation/viewedModerators"
import "seekia/internal/myBlockedUsers"
import "seekia/internal/myContacts"
import "seekia/internal/myDatastores/myList"
import "seekia/internal/myDatastores/myMap"
import "seekia/internal/myDatastores/myMapList"
import "seekia/internal/myIgnoredUsers"
import "seekia/internal/myLikedUsers"
import "seekia/internal/myMatches"
import "seekia/internal/myMatchScore"
import "seekia/internal/mySeedPhrases"
import "seekia/internal/mySettings"
import "seekia/internal/network/myBroadcasts"
import "seekia/internal/network/myMateCriteria"
import "seekia/internal/network/peerServer"
import "seekia/internal/network/viewedHosts"
import "seekia/internal/profiles/myLocalProfiles"
import "seekia/internal/profiles/profileFormat"
import "seekia/internal/worldLanguages"
import "seekia/internal/worldLocations"

import "errors"
import "os"
import "strings"
import "sync"
import "time"
import "unicode"

import goFilepath "path/filepath"

// This bool is set to true if we have initialized application variables
// These only need to be initialized once for all users each time the application is started
var applicationVariablesInitialized bool = false

func SignInToAppUser(userName string, startBackgroundJobs bool)error{

	appMemory.SetMemoryEntry("AppUser", userName)

	err := createAppUserDataFolders()
	if (err != nil) { return err }

	err = initializeAppUserDatastores()
	if (err != nil) { return err }

	err = myAnalyses.PruneOldAnalyses()
	if (err != nil) { return err }

	if (applicationVariablesInitialized == false){

		// This only needs to be done once per application startup

		err := initializeApplicationVariables()
		if (err != nil) { return err }

		applicationVariablesInitialized = true
	}

	if (startBackgroundJobs == true){

		err = backgroundJobs.StartBackgroundJobs()
		if (err != nil) { return err }
	}

	return nil
}


func SignOutOfAppUser()error{

	var signOutWaitgroup sync.WaitGroup

	stopBackgroundJobs := func(){

		err := backgroundJobs.StopBackgroundJobs()
		if (err != nil) {
			//TODO: Log and show to user
			return
		}

		signOutWaitgroup.Done()
	}

	waitForSendsToComplete := func(){

		sendMessages.WaitForPendingSendsToComplete()

		signOutWaitgroup.Done()
	}

	stopPeerServer := func(){

		err := peerServer.StopPeerServer()
		if (err != nil){
			//TODO: Log and show to user
		}

		signOutWaitgroup.Done()
	}

	//TODO: Wait for manual downloads and manual broadcasts to finish

	signOutWaitgroup.Add(3)

	go stopBackgroundJobs()
	go waitForSendsToComplete()
	go stopPeerServer()

	//TODO: Wait for other tasks that use userData folders

	signOutWaitgroup.Wait()

	// We simulate some time. This will be removed once the above functions are implemented
	time.Sleep(time.Second)

	appMemory.ClearAppMemory()

	return nil
}

func VerifyAppUserNameCharactersAreAllowed(userName string)bool{

	// The user name becomes the name of a folder on the filesystem
	// Thus, we only allow letters and numbers

	const digitsList = "0123456789"

	for _, character := range userName {

		isLetter := unicode.IsLetter(character)
		if (isLetter == true){
			continue
		}

		isDigit := strings.Contains(digitsList, string(character))
		if (isDigit == true){
			continue
		}

		return false
	}

	return true
}


func GetAppUsersList()([]string, error){

	appUsersDirectory, err := localFilesystem.GetAppUsersDataFolderPath()
	if (err != nil) { return nil, err }

	filesystemList, err := os.ReadDir(appUsersDirectory)
	if (err != nil) { return nil, err }

	allUsersList := make([]string, 0, len(filesystemList))

	for _, filesystemObject := range filesystemList{

		filepathIsDirectory := filesystemObject.IsDir()
		if (filepathIsDirectory == false){
			return nil, errors.New("UserData folder is corrupt: Contains non-folder: " + filesystemObject.Name())
		}

		folderName := filesystemObject.Name()

		isValid := VerifyAppUserNameCharactersAreAllowed(folderName)
		if (isValid == false){
			return nil, errors.New("UserData folder is corrupt: Contains invalid folder name: " + folderName)
		}

		allUsersList = append(allUsersList, folderName)
	}

	return allUsersList, nil
}


//Outputs:
//	-bool: App user exists
//	-string: User name
func GetCurrentAppUserName()(bool, string){

	exists, appUserName := appMemory.GetMemoryEntry("AppUser")
	if (exists == false){
		return false, ""
	}

	return true, appUserName
}


//Outputs:
//	-bool: Name is a duplicate
//	-error
func CreateAppUser(newUserName string)(bool, error){

	if (newUserName == ""){
		return false, errors.New("CreateAppUser called with empty user name.")
	}
	if (len(newUserName) > 30){
		return false, errors.New("CreateAppUser called with user name that is too long: " + newUserName)
	}

	isAllowed := VerifyAppUserNameCharactersAreAllowed(newUserName)
	if (isAllowed == false){
		return false, errors.New("CreateAppUser called with user name contains unallowed characters: " + newUserName)	
	}

	appUsersList, err := GetAppUsersList()
	if (err != nil){ return false, err }

	for _, existingUser := range appUsersList{
		if (existingUser == newUserName){
			// User name already exists
			return true, nil
		}
	}

	appUsersDirectory, err := localFilesystem.GetAppUsersDataFolderPath()
	if (err != nil) { return false, err }

	userFolderpath := goFilepath.Join(appUsersDirectory, newUserName)

	_, err = localFilesystem.CreateFolder(userFolderpath)
	if (err != nil) { return false, err }


	// We set the IsIgnored desire
	// This desire is initialized upon user creation, so ignored users will be hidden
	// The user can disable this desire
	// We have to sign in to do this

	err = SignInToAppUser(newUserName, false)
	if (err != nil) { return false, err }

	err = myLocalDesires.SetDesire("IsIgnored_FilterAll", "Yes")
	if (err != nil) { return false, err }

	// Desire value is a "+" delimited list of base64 choices which we desire
	// "No" == We desire users who are not ignored
	noBase64 := encoding.EncodeBytesToBase64String([]byte("No"))

	err = myLocalDesires.SetDesire("IsIgnored", noBase64)
	if (err != nil) { return false, err }

	// We clear app memory to "sign out" of app user
	appMemory.ClearAppMemory()


	return false, nil
}

//Outputs:
//	-bool: User is found
//	-error
func RenameAppUser(userName string, newUserName string)(bool, error){

	err := SignOutOfAppUser()
	if (err != nil) { return false, err }

	appUsersDirectory, err := localFilesystem.GetAppUsersDataFolderPath()
	if (err != nil) { return false, err }

	currentUserFolderpath := goFilepath.Join(appUsersDirectory, userName)
	newUserFolderpath := goFilepath.Join(appUsersDirectory, newUserName)

	err = os.Rename(currentUserFolderpath, newUserFolderpath)
	if (err != nil){
		folderDoesNotExist := os.IsNotExist(err)
		if (folderDoesNotExist == true){
			return false, nil
		}
		return false, err
	}

	return true, nil
}


//Outputs:
//	-bool: User found
//	-error
func DeleteAppUser(userName string)(bool, error){

	err := SignOutOfAppUser()
	if (err != nil) { return false, err }

	appUsersDirectory, err := localFilesystem.GetAppUsersDataFolderPath()
	if (err != nil) { return false, err }

	userFolderPath := goFilepath.Join(appUsersDirectory, userName)

	folderExists, err := localFilesystem.DeleteAllFolderContents(userFolderPath)
	if (err != nil){ return false, err }
	if (folderExists == false){
		return false, nil
	}

	err = localFilesystem.DeleteFileOrFolder(userFolderPath)
	if (err != nil) { return false, err }

	return true, nil
}


// We use this to partially sign in to the first app user when testing packages that need it
// An example is myMap, which requires an app user to be signed in to use it
// We don't need to sign the user out after calling this function, because we only use this for testing
func InitializeAppUserForTests(initializeAppDatastores bool, initializeAppVariables bool)error{

	appUsersList, err := GetAppUsersList()
	if (err != nil){ return err }

	if (len(appUsersList) == 0){
		return errors.New("SignInToAppUserForTests called when no app users exist.")
	}

	userName := appUsersList[0]

	appMemory.SetMemoryEntry("AppUser", userName)

	err = createAppUserDataFolders()
	if (err != nil) { return err }

	if (initializeAppDatastores == true){

		err := initializeAppUserDatastores()
		if (err != nil) { return err }
	}

	if (initializeAppVariables == true){

		err := initializeApplicationVariables()
		if (err != nil) { return err }
	}

	return nil
}

func initializeApplicationVariables()error{

	// This only needs to be done once per application startup

	err := worldLocations.InitializeWorldLocationsVariables()
	if (err != nil) { return err }

	err = worldLanguages.InitializeWorldLanguageVariables()
	if (err != nil) { return err }

	err = locusMetadata.InitializeLocusMetadataVariables()
	if (err != nil) { return err }

	monogenicDiseases.InitializeMonogenicDiseaseVariables()

	err = polygenicDiseases.InitializePolygenicDiseaseVariables()
	if (err != nil) { return err }

	err = traits.InitializeTraitVariables()
	if (err != nil) { return err }

	err = trainedPredictionModels.InitializeTrainedPredictionModels()
	if (err != nil) { return err }

	err = profileFormat.InitializeProfileFormatVariables()
	if (err != nil) { return err }

	return nil
}

func createAppUserDataFolders()error{

	userDirectoryPath, err := localFilesystem.GetAppUserFolderPath()
	if (err != nil) { return err }

	_, err = localFilesystem.CreateFolder(userDirectoryPath)
	if (err != nil) { return err }

	err = myMap.InitializeMyMapsFolder()
	if (err != nil) { return err }

	err = myList.InitializeMyListsFolder()
	if (err != nil) { return err }

	err = myMapList.InitializeMyMapListsFolder()
	if (err != nil) { return err }

	err = myBroadcasts.InitializeMyBroadcastsFolders()
	if (err != nil) { return err }

	err = myGenomes.CreateUserGenomesFolder()
	if (err != nil) { return err }

	err = myAnalyses.CreateMyAnalysesFolder()
	if (err != nil) { return err }

	return nil
}

func initializeAppUserDatastores()error{

	err := mySeedPhrases.InitializeMySeedPhrasesDatastore()
	if (err != nil) { return err }

	err = myLocalDesires.InitializeMyDesiresDatastore()
	if (err != nil) { return err }

	err = myMateCriteria.InitializeMyCriteriaDatastore()
	if (err != nil) { return err }

	err = myMatchScore.InitializeMyMatchScorePointsDatastore()
	if (err != nil) { return err }

	err = mySettings.InitializeMySettingsDatastore()
	if (err != nil) { return err }

	err = myLocalProfiles.InitializeMyLocalProfileDatastores()
	if (err != nil) { return err }

	err = myMatches.InitializeMyMatchesDatastores()
	if (err != nil) { return err }

	err = myContacts.InitializeMyContactDatastores()
	if (err != nil) { return err }

	err = myIgnoredUsers.InitializeMyIgnoredUsersDatastore()
	if (err != nil) { return err }

	err = myBlockedUsers.InitializeMyBlockedUsersDatastore()
	if (err != nil) { return err }

	err = mySecretInboxes.InitializeMySecretInboxesDatastore()
	if (err != nil) { return err }

	err = myChatKeys.InitializeMyChatKeysDatastores()
	if (err != nil) { return err }

	err = myCipherKeys.InitializeMyMessageCipherKeysDatastore()
	if (err != nil) { return err }

	err = myReadStatus.InitializeMyReadStatusDatastore()
	if (err != nil) { return err }

	err = myConversationIndexes.InitializeMyConversationIndexesDatastore()
	if (err != nil) { return err }

	err = myChatFilters.InitializeMyChatFiltersDatastores()
	if (err != nil) { return err }

	err = myLikedUsers.InitializeMyLikedUsersDatastore()
	if (err != nil) { return err }

	err = myChatConversations.InitializeMyChatConversationsDatastores()
	if (err != nil) { return err }

	err = myChatMessages.InitializeMyChatMessageDatastores()
	if (err != nil) { return err }

	err = myMessageQueue.InitializeMyMessageQueueDatastore()
	if (err != nil) { return err }

	err = peerChatKeys.InitializePeerChatKeysDatastores()
	if (err != nil) { return err }

	err = peerSecretInboxes.InitializePeerSecretInboxesDatastore()
	if (err != nil) { return err }

	err = peerDevices.InitializePeerDevicesDatastore()
	if (err != nil) { return err }	

	err = sendMessages.InitializeSentMessagesDatastore()
	if (err != nil) { return err }

	err = myGenomes.InitializeMyGenomeDatastore()
	if (err != nil) { return err }

	err = myPeople.InitializeMyGenomePeopleDatastore()
	if (err != nil) { return err }

	err = myCouples.InitializeMyGenomeCouplesDatastore()
	if (err != nil) { return err }

	err = myAnalyses.InitializeMyAnalysesDatastores()
	if (err != nil) { return err }

	err = viewedContent.InitializeViewedContentDatastores()
	if (err != nil) { return err }

	err = logger.InitializeMyLogDatastores()
	if (err != nil) { return err }

	err = viewedModerators.InitializeViewedModeratorsDatastores()
	if (err != nil) { return err }

	err = viewedHosts.InitializeViewedHostsDatastores()
	if (err != nil) { return err }

	err = mySkippedContent.InitializeMySkippedContentDatastores()
	if (err != nil) { return err }

	err = myHiddenContent.InitializeMyHiddenContentDatastores()
	if (err != nil) { return err }

	return nil
}



