
// peerDevices provides functions to keep track of peer device identifiers
// When a user changes their device identifier, we discard their old secret inboxes and chat keys
// We use this package to keep track of when a user has moved to a new device

package peerDevices

import "seekia/internal/encoding"
import "seekia/internal/helpers"
import "seekia/internal/identity"
import "seekia/internal/myDatastores/myMap"
import "seekia/internal/profiles/profileStorage"
import "seekia/internal/profiles/readProfiles"
import "seekia/internal/unixTime"

import "time"
import "strings"
import "sync"
import "errors"

// We lock this whenever we are updating the map
var updatingPeerDevicesMapMutex sync.Mutex

// Map Structure: Peer Identity Hash + "@" + Network Type -> Device Identifier + "$" + Device First Seen Time
var peerDevicesMapDatastore *myMap.MyMap

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

	updatingPeerDevicesMapMutex.Lock()
	defer updatingPeerDevicesMapMutex.Unlock()

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

	peerDevicesMapDatastore = newPeerDevicesMapDatastore

	return nil
}

//Outputs:
//	-bool: Device info found
//	-[11]byte: Newest device identifier
//	-int64: Time that this device was first seen
//		-We discard all chat keys/secret inboxes that were sent before this point in time
//	-error
func GetPeerNewestDeviceInfo(peerIdentityHash [16]byte, networkType byte)(bool, [11]byte, int64, error){

	identityType, err := identity.GetIdentityTypeFromIdentityHash(peerIdentityHash)
	if (err != nil){
		peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:])
		return false, [11]byte{}, 0, errors.New("GetPeerNewestDeviceInfo called with invalid identityHash: " + peerIdentityHashHex)
	}
	if (identityType == "Host"){
		return false, [11]byte{}, 0, errors.New("GetPeerNewestDeviceInfo called with Host identity.")
	}

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

	// We use this lock because we are comparing the map time with the newest stored profile creation time
	// If we added a newer time from a message during this process, without a lock, we could overwrite the newer time from the message
	updatingPeerDevicesMapMutex.Lock()
	defer updatingPeerDevicesMapMutex.Unlock()

	//Outputs:
	//	-bool: Peer profile found
	//	-string: Peer profile device identifier (encoded Hex)
	//	-[11]byte: Peer profile device identifier
	//	-int64: Creation time of profile
	//	-error
	getPeerDeviceIdentifierFromNewestProfile := func()(bool, string, [11]byte, int64, error){

		profileExists, _, _, _, profileCreationTime, rawProfileMap, err := profileStorage.GetNewestUserProfile(peerIdentityHash, networkType)
		if (err != nil) { return false, "", [11]byte{}, 0, err }
		if (profileExists == false){
			return false, "", [11]byte{}, 0, nil
		}

		attributeExists, deviceIdentifierHex, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap, "DeviceIdentifier")
		if (err != nil) { return false, "", [11]byte{}, 0, err }
		if (attributeExists == false){
			return false, "", [11]byte{}, 0, errors.New("Database corrupt: Contains Mate/Moderator profile missing DeviceIdentifier.")
		}

		deviceIdentifierBytes, err := encoding.DecodeHexStringToBytes(deviceIdentifierHex)
		if (err != nil){
			return false, "", [11]byte{}, 0, errors.New("Database corrupt: Contains profile with invalid DeviceIdentifier: " + deviceIdentifierHex)
		}

		if (len(deviceIdentifierBytes) != 11){
			return false, "", [11]byte{}, 0, errors.New("Database corrupt: Contains profile with invalid DeviceIdentifier: " + deviceIdentifierHex)
		}
	
		deviceIdentifierArray := [11]byte(deviceIdentifierBytes)

		return true, deviceIdentifierHex, deviceIdentifierArray, profileCreationTime, nil
	}

	peerProfileFound, peerProfileDeviceIdentifierHex, peerProfileDeviceIdentifier, peerProfileCreationTime, err := getPeerDeviceIdentifierFromNewestProfile()
	if (err != nil) { return false, [11]byte{}, 0, err }

	peerIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash)
	if (err != nil) {
		peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:])
		return false, [11]byte{}, 0, errors.New("GetIdentityTypeFromIdentityHash not verifying identity hash: " + peerIdentityHashHex)
	}

	networkTypeString := helpers.ConvertByteToString(networkType)

	peerDevicesMapDatastoreKey := peerIdentityHashString + "@" + networkTypeString

	mapEntryExists, mapValue, err := peerDevicesMapDatastore.GetMapEntry(peerDevicesMapDatastoreKey)
	if (err != nil) { return false, [11]byte{}, 0, err }
	if (mapEntryExists == false){

		if (peerProfileFound == false){
			return false, [11]byte{}, 0, nil
		}

		// peerProfileFound == true

		peerProfileCreationTimeString := helpers.ConvertInt64ToString(peerProfileCreationTime)

		newMapValue := peerProfileDeviceIdentifierHex + "$" + peerProfileCreationTimeString

		err := peerDevicesMapDatastore.SetMapEntry(peerDevicesMapDatastoreKey, newMapValue)
		if (err != nil) { return false, [11]byte{}, 0, err }

		return true, peerProfileDeviceIdentifier, peerProfileCreationTime, nil
	}

	// mapEntryExists == true

	mapDeviceIdentifier, mapDeviceFirstSeenTimeString, delimiterFound := strings.Cut(mapValue, "$")
	if (delimiterFound == false){
		return false, [11]byte{}, 0, errors.New("peerDevicesMapDatastore is malformed: Invalid value: " + mapValue)
	}

	mapDeviceIdentifierBytes, err := encoding.DecodeHexStringToBytes(mapDeviceIdentifier)
	if (err != nil){
		return false, [11]byte{}, 0, errors.New("peerDevicesMapDatastore is malformed: Invalid DeviceIdentifier: " + mapDeviceIdentifier)
	}

	if (len(mapDeviceIdentifierBytes) != 11){
		return false, [11]byte{}, 0, errors.New("peerDevicesMapDatastore is malformed: Invalid DeviceIdentifier: " + mapDeviceIdentifier)
	}

	mapDeviceIdentifierArray := [11]byte(mapDeviceIdentifierBytes)

	mapDeviceFirstSeenTime, err := helpers.ConvertCreationTimeStringToInt64(mapDeviceFirstSeenTimeString)
	if (err != nil){
		return false, [11]byte{}, 0, errors.New("peerDevicesMapDatastore is malformed: Invalid device first seen time: " + mapDeviceFirstSeenTimeString)
	}

	if (peerProfileFound == false){

		return true, mapDeviceIdentifierArray, mapDeviceFirstSeenTime, nil	
	}

	// Both a map entry and a stored profile exist
	// We see if the profile is newer/older than what we have stored in the map
	// If it is, we update the map

	if (peerProfileDeviceIdentifierHex == mapDeviceIdentifier){

		if (mapDeviceFirstSeenTime <= peerProfileCreationTime){
			// Map device first seen time is earlier than or equal to the profile
			// Nothing to update
			return true, mapDeviceIdentifierArray, mapDeviceFirstSeenTime, nil
		}

		// Profile has a device first seen time that is earlier
		// We will update the map with the profile time
	} else {

		if (mapDeviceFirstSeenTime >= peerProfileCreationTime){
			// Map device first seen time is newer than or equal to the profile
			// Nothing to update
			return true, mapDeviceIdentifierArray, mapDeviceFirstSeenTime, nil
		}
	}

	peerProfileCreationTimeString := helpers.ConvertInt64ToString(peerProfileCreationTime)
		
	newMapValue := peerProfileDeviceIdentifierHex + "$" + peerProfileCreationTimeString

	err = peerDevicesMapDatastore.SetMapEntry(peerDevicesMapDatastoreKey, newMapValue)
	if (err != nil) { return false, [11]byte{}, 0, err }

	return true, peerProfileDeviceIdentifier, peerProfileCreationTime, nil
}


func AddPeerDeviceIdentifierFromMessage(peerIdentityHash [16]byte, networkType byte, deviceIdentifierBytes [11]byte, messageCreationTimeUnix int64)error{

	peerIdentityHashString, identityType, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash)
	if (err != nil) {
		peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:])
		return errors.New("AddPeerDeviceIdentifierFromMessage called with invalid identityHash: " + peerIdentityHashHex)
	}
	if (identityType == "Host"){
		return errors.New("AddPeerDeviceIdentifierFromMessage called with Host identity.")
	}

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

	deviceIdentifier := encoding.EncodeBytesToHexString(deviceIdentifierBytes[:])

	currentTime := time.Now().Unix()

	dayUnix := unixTime.GetDayUnix()

	latestAllowedTime := currentTime + dayUnix	

	if (messageCreationTimeUnix > latestAllowedTime){
		// Either sender's or our own computer clock must be inaccurate
		// We don't want to add the device identifier because it could prevent us from ever being able to contact the user
		// For example, if the user's message creation time is accidentally 1 year in the future, we would not be able to contact them for 1 year
		// We would mistakenly reject all of their new profile chat keys as being out of date
		return nil
	}

	networkTypeString := helpers.ConvertByteToString(networkType)

	peerDevicesMapDatastoreKey := peerIdentityHashString + "@" + networkTypeString

	updatingPeerDevicesMapMutex.Lock()
	defer updatingPeerDevicesMapMutex.Unlock()

	mapEntryExists, mapValue, err := peerDevicesMapDatastore.GetMapEntry(peerDevicesMapDatastoreKey)
	if (err != nil) { return err }
	if (mapEntryExists == true){

		// We see if the map entry is newer than the device identifier sent in the message
		// If it is newer, then we will not add the message device identifier

		mapDeviceIdentifier, mapDeviceFirstSeenTimeString, delimiterFound := strings.Cut(mapValue, "$")
		if (delimiterFound == false){
			return errors.New("peerDevicesMapDatastore is malformed: Invalid value: " + mapValue)
		}

		mapDeviceFirstSeenTime, err := helpers.ConvertCreationTimeStringToInt64(mapDeviceFirstSeenTimeString)
		if (err != nil){
			return errors.New("peerDevicesMapDatastore is malformed: Invalid device first seen time: " + mapDeviceFirstSeenTimeString)
		}

		if (mapDeviceIdentifier == deviceIdentifier){

			if (mapDeviceFirstSeenTime <= messageCreationTimeUnix){
				// Our map device first seen time is earlier than or equal to the message
				// Nothing to do
				return nil
			}
			// We will update our map device first seen time to be earlier
		} else {

			if (mapDeviceFirstSeenTime >= messageCreationTimeUnix){
				// Our map device first seen time is newer than or equal to the input message.
				// Nothing to do
				return nil
			}
		}
	}

	messageCreationTimeUnixString := helpers.ConvertInt64ToString(messageCreationTimeUnix)

	newMapValue := deviceIdentifier + "$" + messageCreationTimeUnixString

	err = peerDevicesMapDatastore.SetMapEntry(peerDevicesMapDatastoreKey, newMapValue)
	if (err != nil) { return err }

	return nil
}




