
// myMatches provides functions to generate and retrieve a user's mate matches
// Matches are the users whom fulfill a user's desires.

package myMatches

import "seekia/internal/appMemory"
import "seekia/internal/badgerDatabase"
import "seekia/internal/desires/myMateDesires"
import "seekia/internal/helpers"
import "seekia/internal/identity"
import "seekia/internal/myBlockedUsers"
import "seekia/internal/myDatastores/myList"
import "seekia/internal/myIdentity"
import "seekia/internal/mySettings"
import "seekia/internal/profiles/viewableProfiles"

import "slices"
import "sync"
import "errors"

// This mutex will be locked whenever we are updating matches.
var updatingMatchesMutex sync.Mutex

var myMatchesListDatastore *myList.MyList

// This function must be called whenever an app user signs in
func InitializeMyMatchesDatastores()error{

	updatingMatchesMutex.Lock()
	defer updatingMatchesMutex.Unlock()
	
	newMyMatchesListDatastore, err := myList.CreateNewList("MyMatches")
	if (err != nil) { return err }

	myMatchesListDatastore = newMyMatchesListDatastore

	return nil
}

func GetMatchesSortByAttribute()(string, error){

	exists, currentAttribute, err := mySettings.GetSetting("MatchesSortByAttribute")
	if (err != nil) { return "", err }
	if (exists == false){
		return "MatchScore", nil
	}

	return currentAttribute, nil
}


func GetMatchesSortDirection()(string, error){

	exists, currentDirection, err := mySettings.GetSetting("MatchesSortDirection")
	if (err != nil) { return "", err }
	if (exists == false){
		return "Descending", nil
	}
	if (currentDirection != "Ascending" && currentDirection != "Descending"){
		return "", errors.New("MySettings malformed: Invalid MatchesSortDirection: " + currentDirection)
	}

	return currentDirection, nil
}


func GetMatchesReadyStatus(networkType byte)(bool, error) {

	isValid := helpers.VerifyNetworkType(networkType)
	if (isValid == false){
		networkTypeString := helpers.ConvertByteToString(networkType)
		return false, errors.New("GetMatchesReadyStatus called with invalid networkType: " + networkTypeString)
	}

	exists, matchesGeneratedStatus, err := mySettings.GetSetting("MatchesGeneratedStatus")
	if (err != nil) { return false, err }
	if (exists == false || matchesGeneratedStatus != "Yes") { 
		return false, nil
	}

	exists, matchesSortedStatus, err := mySettings.GetSetting("MatchesSortedStatus")
	if (err != nil) { return false, err }
	if (exists == false || matchesSortedStatus != "Yes") {
		return false, nil
	}

	exists, matchesNetworkTypeString, err := mySettings.GetSetting("MatchesNetworkType")
	if (err != nil) { return false, err }
	if (exists == false){
		// This should not happen, because Matches network type is created whenever matches are generated
		return false, errors.New("mySettings missing MatchesNetworkType when MatchesGeneratedStatus exists.")
	}

	matchesNetworkType, err := helpers.ConvertNetworkTypeStringToByte(matchesNetworkTypeString)
	if (err != nil) {
		return false, errors.New("mySettings contains invalid MatchesNetworkType: " + matchesNetworkTypeString)
	}
	if (matchesNetworkType != networkType){
		// Matches were generated for a different networkType
		// This should never happen, because we will always set MatchesGeneratedStatus to No when we switch network types,
		//		and we will always call the GetMatchesReadyStatus function with the current appNetworkType

		//TODO: Log this.

		err := mySettings.SetSetting("MatchesGeneratedStatus", "No")
		if (err != nil) { return false, err }

		return false, nil
	}

	return true, nil
}

// Will need a refresh any time a new mate profile is downloaded
func CheckIfMyMatchesNeedRefresh()(bool, error){

	exists, needsRefresh, err := mySettings.GetSetting("MatchesNeedRefreshYesNo")
	if (err != nil) { return true, err }
	if (exists == true && needsRefresh == "No"){
		return false, nil
	}

	return true, nil
}

// This function should only be called once matches are ready (generated and sorted)
//Outputs:
//	-bool: Matches are ready
//	-[][16]byte: List of sorted matches
//	-error
func GetMyMatchesList(networkType byte)(bool, [][16]byte, error) {

	isValid := helpers.VerifyNetworkType(networkType)
	if (isValid == false){
		networkTypeString := helpers.ConvertByteToString(networkType)
		return false, nil, errors.New("GetMyMatchesList called with invalid networkType: " + networkTypeString)
	}

	matchesReady, err := GetMatchesReadyStatus(networkType)
	if (err != nil) { return false, nil, err }
	if (matchesReady == false){
		return false, nil, nil
	}

	myMatchIdentityHashesList, err := myMatchesListDatastore.GetList()
	if (err != nil) { return false, nil, err }

	myMatchesList := make([][16]byte, 0, len(myMatchIdentityHashesList))

	for _, matchIdentityHashString := range myMatchIdentityHashesList{

		matchIdentityHash, identityType, err := identity.ReadIdentityHashString(matchIdentityHashString)
		if (err != nil){
			return false, nil, errors.New("myMatchesListDatastore contains invalid match identity hash: " + matchIdentityHashString)
		}
		if (identityType != "Mate"){
			return false, nil, errors.New("myMatchesListDatastore contains non-Mate match identity hash.")
		}

		myMatchesList = append(myMatchesList, matchIdentityHash)
	}

	return true, myMatchesList, nil
}

// This function returns the number of matches
// It can be called before the matches are ready (after being generated, but before being sorted)
func GetNumberOfGeneratedMatches()(int, error){

	myMatchesList, err := myMatchesListDatastore.GetList()
	if (err != nil) { return 0, err } 

	numberOfGeneratedMatches := len(myMatchesList)

	return numberOfGeneratedMatches, nil
}

//Outputs:
//	-bool: Build encountered error
//	-string: Error encountered
//	-bool: Build is stopped (will be stopped if user went to different page)
//	-bool: Matches are ready
//	-float64: Progress status (0 - 1)
//	-error
func GetMyMatchesBuildStatus(networkType byte)(bool, string, bool, bool, float64, error){

	isValid := helpers.VerifyNetworkType(networkType)
	if (isValid == false){
		networkTypeString := helpers.ConvertByteToString(networkType)
		return false, "", false, false, 0, errors.New("GetMyMatchesBuildStatus called with invalid networkType: " + networkTypeString)
	}

	exists, encounteredError := appMemory.GetMemoryEntry("MatchesBuildEncounteredError")
	if (exists == false){
		// No build exists. A build has not been started since Seekia was started
		return false, "", false, false, 0, nil
	}
	if (encounteredError == "Yes"){
		exists, errorEncountered := appMemory.GetMemoryEntry("MatchesBuildError")
		if (exists == false){
			return false, "", false, false, 0, errors.New("Matches build encountered error is yes, but no error exists.")
		}

		return true, errorEncountered, false, false, 0, nil
	}

	isStopped := CheckIfBuildMatchesIsStopped()
	if (isStopped == true){
		return false, "", true, false, 0, nil
	}

	matchesReadyBool, err := GetMatchesReadyStatus(networkType)
	if (err != nil) { return false, "", false, false, 0, err }
	if (matchesReadyBool == true){
		return false, "", false, true, 1, nil
	}

	exists, currentPercentageString := appMemory.GetMemoryEntry("MatchesReadyProgressPercentage")
	if (exists == false){
		// No build exists. A build has not been started since Seekia was started
		return false, "", false, false, 0, nil
	}

	currentPercentageFloat, err := helpers.ConvertStringToFloat64(currentPercentageString)
	if (err != nil){
		return false, "", false, false, 0, errors.New("MatchesReadyProgressPercentage is not a float: " + currentPercentageString)
	}

	return false, "", false, false, currentPercentageFloat, nil
}


// True == Stop building matches
// False = Don't stop building matches
func CheckIfBuildMatchesIsStopped()bool{

	exists, stopBuildMatchesYesNo := appMemory.GetMemoryEntry("StopBuildMatchesYesNo")
	if (exists == false || stopBuildMatchesYesNo != "No"){
		return true
	}

	return false
}

// This function will cancel the current build (if one is running)
// It will then start updating our matches
func StartUpdatingMyMatches(networkType byte)error{

	isValid := helpers.VerifyNetworkType(networkType)
	if (isValid == false){
		networkTypeString := helpers.ConvertByteToString(networkType)
		return errors.New("StartUpdatingMyMatches called with invalid networkType: " + networkTypeString)
	}

	appMemory.SetMemoryEntry("StopBuildMatchesYesNo", "Yes")

	// We wait for any existing builds to stop.
	updatingMatchesMutex.Lock()

	appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", "0")
	appMemory.SetMemoryEntry("MatchesBuildEncounteredError", "No")
	appMemory.SetMemoryEntry("MatchesBuildError", "")

	appMemory.SetMemoryEntry("StopBuildMatchesYesNo", "No")

	updateMatches := func()error{

		appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", "0")

		getMatchesNeedToBeGeneratedStatus := func()(bool, error){

			exists, matchesGeneratedStatus, err := mySettings.GetSetting("MatchesGeneratedStatus")
			if (err != nil) { return false, err }
			if (exists == false || matchesGeneratedStatus != "Yes"){
				return true, nil
			}

			exists, matchesNetworkTypeString, err := mySettings.GetSetting("MatchesNetworkType")
			if (err != nil) { return false, err }
			if (exists == false){
				// This should not happen, because MatchesNetworkType is set to Yes whenever matches are generated
				return false, errors.New("MatchesNetworkType missing when MatchesGeneratedStatus exists.")
			}

			matchesNetworkType, err := helpers.ConvertNetworkTypeStringToByte(matchesNetworkTypeString)
			if (err != nil) {
				return false, errors.New("mySettings contains invalid MatchesNetworkType: " + matchesNetworkTypeString)
			}
			if (matchesNetworkType != networkType){
				// This should not happen, because MatchesGeneratedStatus should be set to No whenever app network type is changed,
				//		and StartUpdatingMyMatches should only be called with the current app network type.
				return true, nil
			}

			return false, nil
		}

		matchesNeedToBeGenerated, err := getMatchesNeedToBeGeneratedStatus()	
		if (err != nil) { return err }
		if (matchesNeedToBeGenerated == true){

			err := mySettings.SetSetting("MatchesSortedStatus", "No")
			if (err != nil) { return err }

			// We use below to make sure we do not add ourselves as a match
			myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Mate")
			if (err != nil) { return err }

			mateIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Mate")
			if (err != nil) { return err }

			maximumIndex := len(mateIdentityHashesList) - 1

			// This is a list of match identity hashes
			matchesList := make([]string, 0)

			for index, peerIdentityHash := range mateIdentityHashesList{

				stopBuildMatchesYesNo := CheckIfBuildMatchesIsStopped()
				if (stopBuildMatchesYesNo == true){
					// User has gone to a different page, match generation was stopped
					return nil
				}

				progressPercentage, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 0, 50)
				if (err != nil) { return err }

				progressFloat := float64(progressPercentage)/100
				matchesReadyProgressPercentage := helpers.ConvertFloat64ToString(progressFloat)

				appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", matchesReadyProgressPercentage)

				if (myIdentityExists == true && peerIdentityHash == myIdentityHash){
					// We never show ourselves as a match
					continue
				}

				userIsBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(peerIdentityHash)
				if (err != nil) { return err }
				if (userIsBlocked == true){
					continue
				}

				profileExists, _, getAnyUserProfileAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(peerIdentityHash, networkType, true, false, true)
				if (err != nil) { return err }
				if (profileExists == false) {
					// Profile must have been deleted or it is not viewable
					// User cannot be a match, because they have no viewable profile.
					continue
				}

				exists, _, userIsDisabled, err := getAnyUserProfileAttributeFunction("Disabled")
				if (err != nil) { return err }
				if (exists == true && userIsDisabled == "Yes"){
					// User's newest viewable profile is disabled.
					continue
				}

				profilePassesDesires, err := myMateDesires.CheckIfMateProfilePassesAllMyDesires(false, "", getAnyUserProfileAttributeFunction)
				if (err != nil) { return err }
				if (profilePassesDesires == true){

					peerIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash)
					if (err != nil) { return err }

					matchesList = append(matchesList, peerIdentityHashString)	
				}
			}

			err = myMatchesListDatastore.OverwriteList(matchesList)
			if (err != nil) { return err }

			err = mySettings.SetSetting("MatchesGeneratedStatus", "Yes")
			if (err != nil) { return err }

			networkTypeString := helpers.ConvertByteToString(networkType)

			err = mySettings.SetSetting("MatchesNetworkType", networkTypeString)
			if (err != nil) { return err }

			err = mySettings.SetSetting("MatchesViewIndex", "0")
			if (err != nil) { return err }
		}

		stopBuildMatchesBool := CheckIfBuildMatchesIsStopped()
		if (stopBuildMatchesBool == true){
			return nil
		}

		appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", "0.50")

		exists, matchesSortedStatus, err := mySettings.GetSetting("MatchesSortedStatus")
		if (err != nil) { return err }
		if (exists == false || matchesSortedStatus != "Yes"){

			// Now we sort matches.

			myMatchesList, err := myMatchesListDatastore.GetList()
			if (err != nil) { return err }

			currentSortBy, err := GetMatchesSortByAttribute()
			if (err != nil) { return err }
			currentSortDirection, err := GetMatchesSortDirection()
			if (err != nil) { return err }

			// We use this map to make sure there are no duplicate matches
			// This should never happen, unless the user's stored list was edited or there is a bug
			allMatchesMap := make(map[[16]byte]struct{})

			// Map structure: Match Identity Hash -> Sort By Attribute Value
			matchAttributeValuesMap := make(map[string]float64)

			maximumIndex := len(myMatchesList) - 1

			for index, matchIdentityHashString := range myMatchesList{

				matchIdentityHash, _, err := identity.ReadIdentityHashString(matchIdentityHashString)
				if (err != nil){
					return errors.New("myMatchesList contains invalid identity hash during sort: " + matchIdentityHashString)
				}

				_, exists := allMatchesMap[matchIdentityHash]
				if (exists == true){
					return errors.New("myMatchesList contains duplicate match.")
				}
				allMatchesMap[matchIdentityHash] = struct{}{}

				profileExists, _, attributeExists, attributeValue, err := viewableProfiles.GetAnyAttributeFromNewestViewableUserProfile(matchIdentityHash, networkType, currentSortBy, true, false, false)
				if (err != nil) { return err }
				if (profileExists == false){
					// Profile must have been deleted, or it became unviewable
					// The gui will let the user know this when they navigate to the profile
					// The user will be placed towards the end of the sort, with all of the other No Response users
					continue
				}
				if (attributeExists == true){
					attributeValueFloat, err := helpers.ConvertStringToFloat64(attributeValue)
					if (err != nil) {
						return errors.New("Mate profile attribute cannot be converted to float during myMatches sort: " + attributeValue)
					}

					matchAttributeValuesMap[matchIdentityHashString] = attributeValueFloat
				}

				isStopped := CheckIfBuildMatchesIsStopped()
				if (isStopped == true){
					// User has gone to a different page, match generation was stopped
					return nil
				}

				newScaledPercentageInt, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 50, 95)
				if (err != nil) { return err }

				newProgressFloat := float64(newScaledPercentageInt)/100

				newProgressString := helpers.ConvertFloat64ToString(newProgressFloat)

				appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", newProgressString)
			}

			compareMatchesFunction := func(identityHashA string, identityHashB string) int {

				if (identityHashA == identityHashB){
					panic("Duplicate match identity hashes called during sort.")
				}

				attributeValueA, attributeValueAExists := matchAttributeValuesMap[identityHashA]

				attributeValueB, attributeValueBExists := matchAttributeValuesMap[identityHashB]

				if (attributeValueAExists == false && attributeValueBExists == false){

					// We don't know the attribute value for either match
					// We sort matches in unicode order
					if (identityHashA < identityHashB){
						return -1
					}
					return 1

				} else if (attributeValueAExists == true && attributeValueBExists == false){

					// We sort unknown attribute matches to the back of the list

					return -1

				} else if (attributeValueAExists == false && attributeValueBExists == true){

					return 1
				}

				// Both attribute values exist

				if (attributeValueA == attributeValueB){
					// We sort identity hashes in unicode order
					if (identityHashA < identityHashB){
						return -1
					}
					return 1
				}

				if (attributeValueA < attributeValueB){

					if (currentSortDirection == "Ascending"){
						return -1
					}
					return 1
				}
				if (currentSortDirection == "Ascending"){
					return 1
				}
				return -1
			}

			slices.SortFunc(myMatchesList, compareMatchesFunction)

			err = myMatchesListDatastore.OverwriteList(myMatchesList)
			if (err != nil) { return err }

			err = mySettings.SetSetting("MatchesSortedStatus", "Yes")
			if (err != nil) { return err }
		}

		appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", "1")

		err = mySettings.SetSetting("MatchesNeedRefreshYesNo", "No")
		if (err != nil) { return err }

		return nil
	}

	updateFunction := func(){

		err := updateMatches()
		if (err != nil) {
			appMemory.SetMemoryEntry("MatchesBuildEncounteredError", "Yes")
			appMemory.SetMemoryEntry("MatchesBuildError", err.Error())
		}

		updatingMatchesMutex.Unlock()
	}

	go updateFunction()

	return nil
}





