
// myMateCriteria provides functions to retrieve a user's downloads criteria and check if users fulfill their criteria

package myMateCriteria

// Criteria is a way of formatting mate desires for hosts
// This is used when downloading mate profiles from hosts
// The user can choose which desires to share with hosts
// The less information they share, the more profiles they will have to download
// It is a tradeoff between speed and privacy

// Most users should share desires which they would not mind being leaked to the public
// These include things like desired distance, age, and sex
// Malicious nodes could track each requestor's criteria, link the requestor to their user identity, and leak the information online

// TODO: Desires could be altered to make them less specific
// For example, Age desires can be rounded to multiples of 5. Example: 18 -> 20, 26 -> 25
// They should be altered in a different way each time to make it harder to fingerprint the requestor
// I'm unsure if this would increase the requestor anonymity set by much
// Most fingerprints will be very unique, unless the user has few desires and/or shares few desires in their criteria

import "seekia/internal/convertCurrencies"
import "seekia/internal/desires/mateDesires"
import "seekia/internal/desires/myLocalDesires"
import "seekia/internal/desires/myMateDesires"
import "seekia/internal/encoding"
import "seekia/internal/genetics/geneticReferences/monogenicDiseases"
import "seekia/internal/genetics/readGeneticAnalysis"
import "seekia/internal/genetics/myChosenAnalysis"
import "seekia/internal/helpers"
import "seekia/internal/myDatastores/myMap"
import "seekia/internal/network/appNetworkType/getAppNetworkType"
import "seekia/internal/profiles/myLocalProfiles"

import "strings"
import "errors"


// This function returns a list of desires that are used in criteria
// This is used when a user is choosing their download desires
// Each desire will have a ShareDesire value associated with it, stored in the myCriteriaMapDatastore
func GetAllMyMateDownloadDesiresList()[]string{

	allMyDesiresList := myMateDesires.GetAllMyDesiresList(false)

	downloadDesiresList := make([]string, 0)

	for _, desireName := range allMyDesiresList{

		if (desireName == "HasMessagedMe" || desireName == "IHaveMessaged" || desireName == "HasRejectedMe" || desireName == "IsLiked" || desireName == "IsIgnored" || desireName == "IsMyContact"){
			// These desires are not used in criteria
			continue
		}

		downloadDesiresList = append(downloadDesiresList, desireName)
	}

	return downloadDesiresList
}

// Outputs:
//	-bool: Any criteria exists
//	-[]byte: My mate downloads criteria
//	-error
func GetMyMateDownloadsCriteria()(bool, []byte, error){

	allDesiresList := mateDesires.GetAllDesiresList(false)

	myCriteriaMap := make(map[string]string)

	for _, desireName := range allDesiresList{

		if (desireName == "HasMessagedMe" || desireName == "IHaveMessaged" || desireName == "HasRejectedMe" || desireName == "IsLiked" || desireName == "IsIgnored" || desireName == "IsMyContact"){
			// These desires are not used in criteria
			// We cannot share these desires with hosts
			continue
		}
		if (desireName == "WealthInGold" || desireName == "DistanceFrom"){
			// These are desires that we will create using different desires
			// Example: Wealth -> WealthInGold, Distance -> DistanceFrom
			continue
		}

		if (desireName == "23andMe_AncestryComposition_Restrictive"){
			// This desire exists along with the non-restrictive version
			// We will only check either desire once, depending on if restrictive mode is enabled
			// Thus, we don't check it twice
			continue
		}

		desireIsMonogenicDisease := strings.HasPrefix(desireName, "MonogenicDisease_")
		if (desireIsMonogenicDisease == true){
			// These are desires that we create from our OffspringHasAnyDiseaseProbability desire
			continue
		}

		shareDesireBool, err := GetShareMyDesireStatus(desireName)
		if (err != nil) { return false, nil, err }
		if (shareDesireBool == false){
			// We will not share this desire with hosts.
			continue
		}

		// We use this function to add the RequireResponse option to the criteria
		// Inputs:
		//	-string: The name of the desire as it appears in myLocalDesires
		//	-string: The name of the desire as it appears in the newly created Criteria
		// Outputs:
		//	-error
		addDesireRequireResponseOptionToCriteriaMap := func(inputDesireName string, criteriaDesireName string)error{

			desireAllowsResponseRequired, _ := mateDesires.CheckIfDesireAllowsRequireResponse(inputDesireName)
			if (desireAllowsResponseRequired == false){
				// RequireResponse is not allowed for this attribute
				return nil
			}

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

				exists, currentResponseRequired, err := myLocalDesires.GetDesire(inputDesireName + "_RequireResponse")
				if (err != nil) { return false, err }
				if (exists == true && currentResponseRequired == "Yes"){

					return true, nil
				}
				return false, nil
			}

			requireResponseBool, err := getRequireResponseBool()
			if (err != nil) { return err }
			if (requireResponseBool == true){
				myCriteriaMap[criteriaDesireName + "_RequireResponse"] = "Yes"
			}

			return nil			
		}
	
		// We use this function to add the FilterAll option to the criteriaMap
		// Inputs:
		//	-string: The name of the desire as it appears in myLocalDesires
		//	-string: The name of the desire as it appears in the newly created Criteria
		// Outputs:
		//	-bool: FilterAll is enabled (if true, we need to add the desire value to the criteria map)
		//	-error
		addDesireFilterAllOptionToCriteriaMap := func(inputDesireName string, criteriaDesireName string)(bool, error){

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

				exists, filterAllStatus, err := myLocalDesires.GetDesire(inputDesireName + "_FilterAll")
				if (err != nil) { return false, err }
				if (exists == true && filterAllStatus == "Yes"){
					return true, nil
				}
				return false, nil
			}

			filterAllBool, err := getFilterAllBool()
			if (err != nil) { return false, err }
			if (filterAllBool == false){
				// We do not have filterAll enabled
				// All users will pass the desire (excluding those without a response, if RequireResponse == true)
				return false, nil
			}
			// FilterAll is enabled
			myCriteriaMap[criteriaDesireName + "_FilterAll"] = "Yes"

			return true, nil
		}

		// For some of the desires, we need to convert them to a different form
		// This is because some of them are calculated from data within our profile

		if (desireName == "Wealth"){

			err := addDesireRequireResponseOptionToCriteriaMap("Wealth", "Wealth")
			if (err != nil) { return false, nil, err }

			// We must convert our currency to gold
			// This way we will not leak our currency to any hosts
			// Hosts will user a user's WealthInGold attribute for the comparison

			currencyExists, currencyCode, err := myLocalDesires.GetDesire("WealthCurrency")
			if (err != nil) { return false, nil, err }
			if (currencyExists == false){
				// We have not selected our desired wealth currency
				continue
			}

			wealthExists, desiredWealthAmount, err := myLocalDesires.GetDesire("Wealth")
			if (err != nil) { return false, nil, err }
			if (wealthExists == true){
				// We have not selected our desired wealth amount
				continue
			}

			filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap("Wealth", "Wealth")
			if (err != nil) { return false, nil, err }
			if (filterAllIsEnabled == false){
				continue
			}

			desiredWealthAmountFloat64, err := helpers.ConvertStringToFloat64(desiredWealthAmount)
			if (err != nil) { return false, nil, err }

			appNetworkType, err := getAppNetworkType.GetAppNetworkType()
			if (err != nil) { return false, nil, err }

			_, convertedDesiredWealth, err := convertCurrencies.ConvertKilogramsOfGoldToAnyCurrency(appNetworkType, desiredWealthAmountFloat64, currencyCode)
			if (err != nil) { return false, nil, err }

			convertedDesiredWealthString := helpers.ConvertFloat64ToString(convertedDesiredWealth)

			myCriteriaMap["WealthInGold"] = convertedDesiredWealthString

			continue
		}

		if (desireName == "Distance"){

			err := addDesireRequireResponseOptionToCriteriaMap("Distance", "DistanceFrom")
			if (err != nil) { return false, nil, err }

			//TODO: Randomize location
			// We do not want to share our exact location
			// This would make it easier to link requestor to user profile
			// We want to choose a random distance from our true location, and create a radius that includes our true desired distance
			// We must figure out the best way to do this that maximizes privacy
			//
			// We might want to use only a few random locations that are generated using entropy from the device seed
			// This means that, even after making hundreds of requests, hosts would not be able to derive true location
			// Otherwise, they could find the average of all requested locations to find the true location of requestor
			// This strategy could be combined by using an origin location that is offset from the true location, 
			//		using entropy from the device seed
 
			// We convert Distance to DistanceFrom

			minimumExists, desiredDistanceMinimum, err := myLocalDesires.GetDesire("Distance_Minimum")
			if (err != nil) { return false, nil, err }

			maximumExists, desiredDistanceMaximum, err := myLocalDesires.GetDesire("Distance_Maximum")
			if (err != nil) { return false, nil, err }

			if (minimumExists == false && maximumExists == false){
				// We do not have a desired distance
				continue
			}

			getMinimumBound := func()string{

				if (minimumExists == false){
					return "0"
				}
				return desiredDistanceMinimum
			}

			minimumBound := getMinimumBound()

			getMaximumBound := func()string{

				if (maximumExists == false){
					return "20000"
				}
				return desiredDistanceMaximum
			}

			maximumBound := getMaximumBound()

			desiredDistanceMinimumFloat64, err := helpers.ConvertStringToFloat64(minimumBound)
			if (err != nil){
				return false, nil, errors.New("MyLocalDesires contains invalid Distance_Minimum: " + desiredDistanceMinimum)
			}

			desiredDistanceMaximumFloat64, err := helpers.ConvertStringToFloat64(maximumBound)
			if (err != nil){
				return false, nil, errors.New("MyLocalDesires contains invalid Distance_Maximum: " + desiredDistanceMaximum)
			}
			
			if (desiredDistanceMinimumFloat64 < 0){
				return false, nil, errors.New("MyLocalDesires contains invalid Distance_Minimum: " + desiredDistanceMinimum)
			}
			if (desiredDistanceMaximumFloat64 < desiredDistanceMinimumFloat64){
				return false, nil, errors.New("MyLocalDesires contains Distance_Minimum that is larger than Distance_Maximum.")
			}

			exists, myLocationLatitudeString, err := myLocalProfiles.GetProfileData("Mate", "PrimaryLocationLatitude")
			if (err != nil) { return false, nil, err }
			if (exists == false){
				// We have not added a location, we cannot calculate distance.
				continue
			}

			filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap("Distance", "DistanceFrom")
			if (err != nil) { return false, nil, err }
			if (filterAllIsEnabled == false){
				continue
			}

			exists, myLocationLongitudeString, err := myLocalProfiles.GetProfileData("Mate", "PrimaryLocationLongitude")
			if (err != nil) { return false, nil, err }
			if (exists == false){
				return false, nil, errors.New("MyLocalProfiles contains PrimaryLocationLatitude but not PrimaryLocationLongitude")
			}

			myLocationLatitudeFloat64, err := helpers.ConvertStringToFloat64(myLocationLatitudeString)
			if (err != nil) {
				return false, nil, errors.New("MyLocalProfiles contains invalid PrimaryLocationLatitude: " + myLocationLatitudeString)
			}
			myLocationLongitudeFloat64, err := helpers.ConvertStringToFloat64(myLocationLongitudeString)
			if (err != nil) {
				return false, nil, errors.New("MyLocalProfiles contains invalid PrimaryLocationLongitude: " + myLocationLongitudeString)
			}

			isValid := helpers.VerifyLatitude(myLocationLatitudeFloat64)
			if (isValid == false){
				return false, nil, errors.New("MyLocalProfiles contains invalid PrimaryLocationLatitude: " + myLocationLatitudeString)
			} 

			isValid = helpers.VerifyLongitude(myLocationLongitudeFloat64)
			if (isValid == false){
				return false, nil, errors.New("MyLocalProfiles contains invalid PrimaryLocationLongitude: " + myLocationLongitudeString)
			}

			distanceFromValue := minimumBound + "$" + maximumBound + "@" + myLocationLatitudeString + "+" + myLocationLongitudeString

			myCriteriaMap["DistanceFrom"] = distanceFromValue

			continue
		}

		if (desireName == "23andMe_AncestryComposition"){

			getDesireToInclude := func()(string, error){

				settingExists, restrictiveModeEnabled, err := myLocalDesires.GetDesire("23andMe_AncestryComposition_RestrictiveModeEnabled")
				if (err != nil) { return "", err }
				if (settingExists == true && restrictiveModeEnabled == "Yes"){
					return "23andMe_AncestryComposition_Restrictive", nil
				}
				return "23andMe_AncestryComposition", nil
			}

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

			err = addDesireRequireResponseOptionToCriteriaMap(desireToInclude, desireToInclude)
			if (err != nil) { return false, nil, err }

			exists, desireValue, err := myLocalDesires.GetDesire(desireToInclude)
			if (err != nil) { return false, nil, err }
			if (exists == false){
				continue
			}

			filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap(desireToInclude, desireToInclude)
			if (err != nil) { return false, nil, err }
			if (filterAllIsEnabled == false){
				continue
			}

			myCriteriaMap[desireToInclude] = desireValue

			continue
		}

		if (desireName == "OffspringProbabilityOfAnyMonogenicDisease"){

			// We have to determine which mongenic diseases we have a non-zero risk of passing a variant for

			desireExists, desiredMaximumRisk, err := myLocalDesires.GetDesire("OffspringProbabilityOfAnyMonogenicDisease_Maximum")
			if (err != nil) { return false, nil, err }
			if (desireExists == false){
				// We have no OffspringProbabilityOfAnyMonogenicDisease desire.
				continue
			}

			myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisObject, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis()
			if (err != nil) { return false, nil, err }
			if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){
				// We have not linked our genome to our profile
				continue
			}

			monogenicDiseaseObjectsList, err := monogenicDiseases.GetMonogenicDiseaseObjectsList()
			if (err != nil) { return false, nil, err }

			for _, diseaseObject := range monogenicDiseaseObjectsList{

				diseaseName := diseaseObject.DiseaseName
				diseaseIsDominantOrRecessive := diseaseObject.DominantOrRecessive

				myProbabilityIsKnown, _, myProbabilityOfPassingAMonogenicDiseaseVariant, _, _, _, _, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(myAnalysisObject, diseaseName, myGenomeIdentifier)
				if (err != nil) { return false, nil, err }
				if (myProbabilityIsKnown == false){
					continue
				}

				// Outputs:
				//	-bool: Desire is needed
				//	-string: Maximum bound of user we desire
				//	-error
				getDiseaseProbabilityMaximumDesiredBound := func()(bool, string, error){

					if (diseaseIsDominantOrRecessive == "Dominant"){

						if (desiredMaximumRisk == "0"){

							// Because disease is dominant, the only way to have a 0% risk is if both users have a 0% risk
							return true, "0", nil
						}

						// desiredMaximumRisk == 99

						if (myProbabilityOfPassingAMonogenicDiseaseVariant == 100){
							// All users will be filtered, regardless of their probability
							// We tried to warn user about this
							return true, "0", nil
						}

						// We might have some probability of passing the disease, but not a 100% probability
						// We must make sure the other user does not have a 100% probability of passing the disease
						return true, "99", nil
					}

					// diseaseIsDominantOrRecessive == "Recessive"

					if (desiredMaximumRisk == "0"){

						if (myProbabilityOfPassingAMonogenicDiseaseVariant == 0){
							// Risk is always 0%, because we are 0%
							// No desire is needed
							return false, "", nil
						}
						// We have >0% risk
						// We must make sure that the other user has a 0% probability of passing a variant
						return true, "0", nil
					}
					// desiredMaximumRisk == "99"

					if (myProbabilityOfPassingAMonogenicDiseaseVariant == 100){

						// We must make sure other user has a <100% probability of passing a variant
						return true, "99", nil
					}

					// We may have some risk, but not 100% risk
					// Thus, offspring risk will never be 100%
					// No desire is needed
					return false, "", nil
				}

				desireNeeded, maximumBound, err := getDiseaseProbabilityMaximumDesiredBound()
				if (err != nil){ return false, nil, err }
				if (desireNeeded == false){
					continue
				}

				diseaseNameWithUnderscores := strings.ReplaceAll(diseaseName, " ", "_")
				diseaseDesireName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_ProbabilityOfPassingAVariant_Maximum"

				myCriteriaMap[diseaseDesireName] = maximumBound
			}
		}

		err = addDesireRequireResponseOptionToCriteriaMap(desireName, desireName)
		if (err != nil) { return false, nil, err }

		desireIsNumerical := mateDesires.CheckIfDesireIsNumerical(desireName)
		if (desireIsNumerical == true){

			minimumExists, minimumValue, err := myLocalDesires.GetDesire(desireName + "_Minimum")
			if (err != nil) { return false, nil, err }

			maximumExists, maximumValue, err := myLocalDesires.GetDesire(desireName + "_Maximum")
			if (err != nil) { return false, nil, err }

			if (minimumExists == false && maximumExists == false){
				continue
			}

			filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap(desireName, desireName)
			if (err != nil) { return false, nil, err }
			if (filterAllIsEnabled == false){
				continue
			}

			myCriteriaMap[desireName + "_Minimum"] = minimumValue
			myCriteriaMap[desireName + "_Maximum"] = maximumValue

			continue
		}

		// Desire is not numerical

		exists, desireValue, err := myLocalDesires.GetDesire(desireName)
		if (err != nil) { return false, nil, err }
		if (exists == false){
			continue
		}

		filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap(desireName, desireName)
		if (err != nil) { return false, nil, err }
		if (filterAllIsEnabled == false){
			continue
		}

		myCriteriaMap[desireName] = desireValue
	}

	if (len(myCriteriaMap) == 0){
		return false, nil, nil
	}

	myCriteriaBytes, err := encoding.EncodeMessagePackBytes(myCriteriaMap)
	if (err != nil) { return false, nil, err }

	return true, myCriteriaBytes, nil
}


// This will store the share status of each desire
// This status determines if the user wants to share their desire or not
var myCriteriaMapDatastore *myMap.MyMap

// This function must be called whenever we sign in to an app user
func InitializeMyCriteriaDatastore()error{

	newMyCriteriaMapDatastore, err := myMap.CreateNewMap("MyCriteria")
	if (err != nil) { return err }

	myCriteriaMapDatastore = newMyCriteriaMapDatastore

	return nil
}

// This will return whether the user has decided to share this desire with hosts
func GetShareMyDesireStatus(desireName string)(bool, error){

	exists, statusValue, err := myCriteriaMapDatastore.GetMapEntry(desireName)
	if (err != nil) { return false, err }
	if (exists == false){
		return false, nil
	}

	statusValueBool, err := helpers.ConvertYesOrNoStringToBool(statusValue)
	if (err != nil) {
		return false, errors.New("Invalid myCriteria desire share status: " + statusValue)
	}

	return statusValueBool, nil
}

func SetShareMyDesireStatus(desireName string, shareStatus bool)error{

	newStatusString := helpers.ConvertBoolToYesOrNoString(shareStatus)

	err := myCriteriaMapDatastore.SetMapEntry(desireName, newStatusString)
	if (err != nil) { return err }

	return nil
}


