
// myProfileExports provides functions to manage a user's exported profiles
// Exported profiles exist so users can view their profiles before broadcasting them
// They are stored in their own folder, waiting to be broadcasted

package myProfileExports

import "seekia/internal/encoding"
import "seekia/internal/genetics/geneticReferences/monogenicDiseases"
import "seekia/internal/genetics/geneticReferences/polygenicDiseases"
import "seekia/internal/genetics/geneticReferences/traits"
import "seekia/internal/genetics/readGeneticAnalysis"
import "seekia/internal/genetics/myChosenAnalysis"
import "seekia/internal/helpers"
import "seekia/internal/localFilesystem"
import "seekia/internal/messaging/myChatKeys"
import "seekia/internal/myDevice"
import "seekia/internal/myIdentity"
import "seekia/internal/profiles/createProfiles"
import "seekia/internal/profiles/myLocalProfiles"
import "seekia/internal/profiles/profileFormat"
import "seekia/internal/profiles/readProfiles"

import messagepack "github.com/vmihailenco/msgpack/v5"

import "strings"
import "time"
import "errors"
import "path/filepath"
import "sync"
import "slices"

// We lock this when we are writing any exported profile files
var writingExportedProfileFilesMutex sync.Mutex

func UpdateMyExportedProfile(myProfileType string, networkType byte)error{

	if (myProfileType != "Mate" && myProfileType != "Host" && myProfileType != "Moderator"){
		return errors.New("UpdateMyExportedProfile called with invalid myProfileType: " + myProfileType)
	}

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

	networkTypeString := helpers.ConvertByteToString(networkType)

	myIdentityFound, myIdentityHash, err := myIdentity.GetMyIdentityHash(myProfileType)
	if (err != nil) { return err }
	if (myIdentityFound == false) {
		return errors.New("UpdateMyExportedProfile called when my identity is missing.")
	}

	getNewProfileMap := func()(map[string]string, error){

		profileMap := map[string]string{
			"ProfileType": myProfileType,
			"NetworkType": networkTypeString,
		}

		attributeExists, profileIsDisabled, err := myLocalProfiles.GetProfileData(myProfileType, "Disabled")
		if (err != nil) { return nil, err }
		if (attributeExists == true && profileIsDisabled == "Yes"){

			profileMap["Disabled"] = "Yes"
			return profileMap, nil
		}

		// This is a map of all possible profile attributes
		// This map contains some attributes that are calculated, and thus will not be found when querying the user profile
		// An example is a user's genetic analysis, which is added in its own function
		// Map Structure: Attribute name -> Attribute object
		profileAttributeObjectsList, err := profileFormat.GetProfileAttributeObjectsList()
		if (err != nil) { return nil, err }

		for _, attributeObject := range profileAttributeObjectsList{

			attributeName := attributeObject.AttributeName

			if (attributeName == "ProfileType" || attributeName == "CreationTime" || attributeName == "IdentityKey" || attributeName == "ProfileVersion" || attributeName == "Disabled"){
				continue
			}

			profileVersion := 1

			attributeProfileTypesList, err := attributeObject.GetProfileTypes(profileVersion)
			if (err != nil) { return nil, err }

			attributeIsRelevant := slices.Contains(attributeProfileTypesList, myProfileType)
			if (attributeIsRelevant == false){
				continue
			}

			attributeValueExists, attributeValue, err := myLocalProfiles.GetProfileData(myProfileType, attributeName)
			if (err != nil){ return nil, err }
			if (attributeValueExists == false){
				continue
			}

			if (attributeName == "23andMe_AncestryComposition"){

				// The user has the option of hiding this attribute from their profile
				// The user can then view offspring ancestry compositions without sharing that information on their profile
				// This is the only attribute (other than genetic analysis attributes) where hiding the attribute is possible				

				exists, visibilityStatus, err := myLocalProfiles.GetProfileData("Mate", "VisibilityStatus_23andMe_AncestryComposition")
				if (err != nil) { return nil, err }
				if (exists == true && visibilityStatus == "No"){
					// The user has opted to not share this attribute
					continue
				}
			}

			if (attributeName == "PrimaryLocationLatitude" || attributeName == "PrimaryLocationLongitude" || attributeName == "PrimaryLocationCountry" || attributeName == "SecondaryLocationLatitude" || attributeName == "SecondaryLocationLongitude" || attributeName == "SecondaryLocationCountry"){

				exists, visibilityStatus, err := myLocalProfiles.GetProfileData("Mate", "VisibilityStatus_Location")
				if (err != nil){ return nil, err }
				if (exists == true && visibilityStatus == "No"){
					// The user has opted to not share their location
					continue
				} 
			}

			profileMap[attributeName] = attributeValue
		}

		addGeneticAnalysisToProfileMap := func()error{

			if (myProfileType != "Mate"){
				// Only mate profiles can contain a genetic analysis
				return nil
			}

			myGenomePersonChosen, anyGenomesExist, personAnalysisIsReady, myGeneticAnalysisObject, genomeIdentifierToShare, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis()
			if (err != nil) { return err }
			if (myGenomePersonChosen == false){
				// User has not linked a genome person.
				// No genetic analysis needs to be added.
				return nil
			}
			if (anyGenomesExist == false){
				// The profile has a genome person, but the genome person has no genomes
				return nil
			}
			if (personAnalysisIsReady == false){
				return errors.New("UpdateMyExportedProfile called when profile genetic analysis is not ready.")
			}

			_, _, _, _, myGenomesMap, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(myGeneticAnalysisObject)
			if (err != nil) { return err }

			monogenicDiseaseNamesList, err := monogenicDiseases.GetMonogenicDiseaseNamesList()
			if (err != nil) { return err }

			for _, monogenicDiseaseName := range monogenicDiseaseNamesList{

				shareDiseaseInfoAttributeName := "ShareMonogenicDiseaseInfo_" + monogenicDiseaseName

				currentShareDiseaseInfoAttributeExists, currentShareDiseaseInfoAttribute, err := myLocalProfiles.GetProfileData("Mate", shareDiseaseInfoAttributeName)
				if (err != nil) { return err }
				if (currentShareDiseaseInfoAttributeExists == false){
					continue
				}
				if (currentShareDiseaseInfoAttribute != "Yes"){
					continue
				}

				probabilitiesKnown, _, probabilityOfPassingADiseaseVariant, _, numberOfVariantsTested, _, _, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(myGeneticAnalysisObject, monogenicDiseaseName, genomeIdentifierToShare)
				if (err != nil) { return err }
				if (probabilitiesKnown == false){
					continue
				}

				diseaseNameWithUnderscores := strings.ReplaceAll(monogenicDiseaseName, " ", "_")

				probabilityOfPassingVariantAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_ProbabilityOfPassingAVariant"

				probabilityOfPassingADiseaseVariantString := helpers.ConvertIntToString(probabilityOfPassingADiseaseVariant)

				variantsTestedAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_NumberOfVariantsTested"

				numberOfVariantsTestedString := helpers.ConvertIntToString(numberOfVariantsTested)

				profileMap[probabilityOfPassingVariantAttributeName] = probabilityOfPassingADiseaseVariantString

				profileMap[variantsTestedAttributeName] = numberOfVariantsTestedString
			}

			polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList()
			if (err != nil) { return err }

			// This map stores the rsIDs to share in our profile
			// We use a map to avoid duplicates
			myLociToShareMap := make(map[int64]struct{})		

			for _, diseaseObject := range polygenicDiseaseObjectsList{

				diseaseName := diseaseObject.DiseaseName

				shareDiseaseInfoAttributeName := "SharePolygenicDiseaseInfo_" + diseaseName	

				currentShareDiseaseInfoAttributeExists, currentShareDiseaseInfoAttribute, err := myLocalProfiles.GetProfileData("Mate", shareDiseaseInfoAttributeName)
				if (err != nil) { return err }
				if (currentShareDiseaseInfoAttributeExists == false){
					continue
				}
				if (currentShareDiseaseInfoAttribute != "Yes"){
					continue
				}

				lociList := diseaseObject.LociList

				for _, rsID := range lociList{

					myLociToShareMap[rsID] = struct{}{}
				}
			}

			traitObjectsList, err := traits.GetTraitObjectsList()
			if (err != nil) { return err }

			for _, traitObject := range traitObjectsList{

				traitName := traitObject.TraitName

				shareTraitInfoAttributeName := "ShareTraitInfo_" + traitName

				currentShareTraitInfoAttributeExists, currentShareTraitInfoAttribute, err := myLocalProfiles.GetProfileData("Mate", shareTraitInfoAttributeName)
				if (err != nil) { return err }
				if (currentShareTraitInfoAttributeExists == false){
					continue
				}
				if (currentShareTraitInfoAttribute != "Yes"){
					continue
				}

				lociList := traitObject.LociList

				for _, rsID := range lociList{

					myLociToShareMap[rsID] = struct{}{}
				}
			}

			myGenomeLocusValuesMap, exists := myGenomesMap[genomeIdentifierToShare]
			if (exists == false){
				return errors.New("GetMyChosenMateGeneticAnalysis returning genetic analysis which has GenomesMap which is missing my genome identifier.")
			}

			for rsID, _ := range myLociToShareMap{

				locusValueObject, exists := myGenomeLocusValuesMap[rsID]
				if (exists == false){
					continue
				}

				rsIDString := helpers.ConvertInt64ToString(rsID)

				locusBase1 := locusValueObject.Base1Value
				locusBase2 := locusValueObject.Base2Value
				locusIsPhased := locusValueObject.LocusIsPhased

				basePairValue := locusBase1 + ";" + locusBase2
				locisIsPhasedString := helpers.ConvertBoolToYesOrNoString(locusIsPhased)

				locusValueAttributeName := "LocusValue_rs" + rsIDString
				locusIsPhasedAttributeName := "LocusIsPhased_rs" + rsIDString

				profileMap[locusValueAttributeName] = basePairValue
				profileMap[locusIsPhasedAttributeName] = locisIsPhasedString
			}

			return nil
		}

		err = addGeneticAnalysisToProfileMap()
		if (err != nil) { return nil, err }

		if (myProfileType != "Host"){

			// We add the chat keys to the Mate/Moderator profile
			// Host profiles do not have chat keys

			identityExists, myNaclKey, myMlkemKey, myCsidhKey, err := myChatKeys.GetMyNewestPublicChatKeys(myIdentityHash, networkType)
			if (err != nil) { return nil, err }
			if (identityExists == false){
				return nil, errors.New("My identity not found after being found already.")
			}

			myIdentityFound, myDeviceIdentifier, err := myDevice.GetMyDeviceIdentifier(myIdentityHash, networkType)
			if (err != nil) { return nil, err }
			if (myIdentityFound == false){
				return nil, errors.New("My identity not found after being found already.")
			}

			myDeviceIdentifierHex := encoding.EncodeBytesToHexString(myDeviceIdentifier[:])

			myNaclKeyString := encoding.EncodeBytesToBase64String(myNaclKey[:])
			myMlkemKeyString := encoding.EncodeBytesToBase64String(myMlkemKey[:])
			myCsidhKeyString := encoding.EncodeBytesToBase64String(myCsidhKey[:])

			profileMap["DeviceIdentifier"] = myDeviceIdentifierHex
			profileMap["NaclKey"] = myNaclKeyString
			profileMap["MlkemKey"] = myMlkemKeyString
			profileMap["CsidhKey"] = myCsidhKeyString

			// We get the chatKeysLatestUpdateTime, assuming that this profile will be broadcast
			// We will only update the chatKeysLatestUpdateTime we have saved locally if the profile is actually broadcast
			getChatKeysLatestUpdateTime := func()(int64, error){

				myIdentityExists, anyKeysExist, existingNaclKey, existingMlkemKey, existingCsidhKey, err := myChatKeys.GetMyNewestBroadcastPublicChatKeys(myIdentityHash, networkType)
				if (err != nil){ return 0, err }
				if (myIdentityExists == false){
					return 0, errors.New("My identity not found after being found already.")
				}
				if (anyKeysExist == true){
					if (existingNaclKey != myNaclKey || existingMlkemKey != myMlkemKey || existingCsidhKey != myCsidhKey){
						// This profile contains new keys.
						// Our latestChatKeysUpdateTime is now the creation time of the profile we are currently creating
						// We will get the time now, which may be a second older than the creationTime of the profile, which doesn't matter
						currentTime := time.Now().Unix()
						return currentTime, nil
					}
				}
				updateTimeExists, existingUpdateTime, err := myChatKeys.GetMyChatKeysLatestUpdateTime(myIdentityHash, networkType)
				if (err != nil) { return 0, err }
				if (updateTimeExists == false){
					// We have not broadcast a profile yet.
					// Our current exported profile will contain the newest chat keys
					currentTime := time.Now().Unix()
					return currentTime, nil
				}
				// We have broadcasted a profile before, and the current exported profile is not broadcasting any novel chat keys
				return existingUpdateTime, nil
			}

			chatKeysLatestUpdateTime, err := getChatKeysLatestUpdateTime()
			if (err != nil) { return nil, err }

			chatKeysLatestUpdateTimeString := helpers.ConvertInt64ToString(chatKeysLatestUpdateTime)

			profileMap["ChatKeysLatestUpdateTime"] = chatKeysLatestUpdateTimeString
		}

		return profileMap, nil
	}

	profileMap, err := getNewProfileMap()
	if (err != nil){ return err }

	myIdentityExists, myIdentityPublicKey, myIdentityPrivateKey, err := myIdentity.GetMyPublicPrivateIdentityKeys(myProfileType)
	if (err != nil) { return err }
	if (myIdentityExists == false){
		return errors.New("UpdateMyExportedProfile called when my identity is missing.")
	}

	profileBytes, err := createProfiles.CreateProfile(myIdentityPublicKey, myIdentityPrivateKey, profileMap)
	if (err != nil) { return err }

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

	exportedProfilesDirectory := filepath.Join(userDirectory, "MyExportedProfiles")

	networkTypeExportedProfilesDirectory := filepath.Join(exportedProfilesDirectory, "Network" + networkTypeString)

	writingExportedProfileFilesMutex.Lock()
	defer writingExportedProfileFilesMutex.Unlock()

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

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

	profileFileName := myProfileType + "Profile.messagepack"

	err = localFilesystem.CreateOrOverwriteFile(profileBytes, networkTypeExportedProfilesDirectory, profileFileName)
	if (err != nil) { return err }

	return nil
}


//Outputs:
//	-bool: Profile found
//	-[28]byte: Profile hash
//	-[]byte: Profile bytes
//	-map[int]messagepack.RawMessage: Raw profile map
//	-error
func GetMyExportedProfile(myProfileType string, networkType byte)(bool, [28]byte, []byte, map[int]messagepack.RawMessage, error){

	if (myProfileType != "Mate" && myProfileType != "Host" && myProfileType != "Moderator"){
		return false, [28]byte{}, nil, nil, errors.New("GetMyExportedProfile called with invalid profileType: " + myProfileType)
	}

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

	myIdentityFound, myIdentityHash, err := myIdentity.GetMyIdentityHash(myProfileType)
	if (err != nil) { return false, [28]byte{}, nil, nil, err }
	if (myIdentityFound == false) {
		return false, [28]byte{}, nil, nil, errors.New("GetMyExportedProfile called when my identity is missing.")
	}

	userDirectory, err := localFilesystem.GetAppUserFolderPath()
	if (err != nil ) { return false, [28]byte{}, nil, nil, err }

	networkTypeString := helpers.ConvertByteToString(networkType)

	profileFilePath := filepath.Join(userDirectory, "MyExportedProfiles", "Network" + networkTypeString, myProfileType + "Profile.messagepack")

	exists, profileBytes, err := localFilesystem.GetFileContents(profileFilePath)
	if (err != nil) { return false, [28]byte{}, nil, nil, err }
	if (exists == false) {
		return false, [28]byte{}, nil, nil, nil
	}

	ableToRead, profileHash, _, profileNetworkType, profileAuthor, _, _, rawProfileMap, err := readProfiles.ReadProfileAndHash(true, profileBytes)
	if (err != nil) { return false, [28]byte{}, nil, nil, err }
	if (ableToRead == false){
		return false, [28]byte{}, nil, nil, errors.New("MyExportedProfiles folder contains invalid profile.")
	}
	if (profileNetworkType != networkType){
		return false, [28]byte{}, nil, nil, errors.New("MyExportedProfiles folder contains profile for different networkType.")
	}

	if (profileAuthor != myIdentityHash){
		// This profile must be authored by an old identity.
		// We will not return error, instead just return profile missing
		// Once we update the exported profile again, this old profile will be overwritten
		return false, [28]byte{}, nil, nil, nil
	}

	return true, profileHash, profileBytes, rawProfileMap, nil
}





