
// sendMessages provides functions to create and send messages

package sendMessages

//TODO: Add function to prune mySentMessagesMapDatastore

import "seekia/internal/convertCurrencies"
import "seekia/internal/cryptography/csidh"
import "seekia/internal/cryptography/mlkem"
import "seekia/internal/cryptography/nacl"
import "seekia/internal/encoding"
import "seekia/internal/helpers"
import "seekia/internal/identity"
import "seekia/internal/logger"
import "seekia/internal/messaging/createMessages"
import "seekia/internal/messaging/inbox"
import "seekia/internal/messaging/myChatKeys"
import "seekia/internal/messaging/myChatMessages"
import "seekia/internal/messaging/mySecretInboxes"
import "seekia/internal/messaging/peerChatKeys"
import "seekia/internal/messaging/peerSecretInboxes"
import "seekia/internal/messaging/readMessages"
import "seekia/internal/myDatastores/myMap"
import "seekia/internal/myDevice"
import "seekia/internal/myIdentity"
import "seekia/internal/network/myBroadcasts"
import "seekia/internal/network/myProofFunds"
import "seekia/internal/parameters/getParameters"
import "seekia/internal/profiles/myProfileStatus"

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

// This mutex will be locked whenever we are adding a new message to send
var addingMessageMutex sync.Mutex

// This list host a map of messages which we have already sent
// Map Structure: Message Identifier -> Message Hash
var mySentMessagesMapDatastore *myMap.MyMap

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

	addingMessageMutex.Lock()
	defer addingMessageMutex.Unlock()

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

	mySentMessagesMapDatastore = newMySentMessagesMapDatastore

	return nil
}

func WaitForPendingSendsToComplete(){

	pendingMessagesWaitgroup.Wait()
}

// This waitgroup will be used to wait for all pending messages to be sent
var pendingMessagesWaitgroup sync.WaitGroup

var pendingMessagesMapMutex sync.RWMutex

// This list contains a map of message identifiers which we are currently attempting to send
var pendingMessagesMap map[[20]byte]struct{} = make(map[[20]byte]struct{})

func addMessageToPendingMessagesMap(messageIdentifier [20]byte){
	pendingMessagesMapMutex.Lock()
	pendingMessagesMap[messageIdentifier] = struct{}{}
	pendingMessagesMapMutex.Unlock()
}

func deleteMessageFromPendingMessagesMap(messageIdentifier [20]byte){

	pendingMessagesMapMutex.Lock()
	delete(pendingMessagesMap, messageIdentifier)
	pendingMessagesMapMutex.Unlock()
}

func checkIfMessageIsPending(messageIdentifier [20]byte)bool{

	pendingMessagesMapMutex.RLock()
	_, exists := pendingMessagesMap[messageIdentifier]
	pendingMessagesMapMutex.RUnlock()
	if (exists == false){
		return false
	}

	return true
}

// This function will tell us if a message has been successfully sent
//Outputs:
//	-bool: Message is sent
//	-[26]byte: Message hash
//	-error
func CheckIfMessageIsSent(messageIdentifier [20]byte)(bool, [26]byte, error){

	messageIdentifierHex := encoding.EncodeBytesToHexString(messageIdentifier[:])

	exists, messageHashString, err := mySentMessagesMapDatastore.GetMapEntry(messageIdentifierHex)
	if (err != nil) { return false, [26]byte{}, err }
	if (exists == false){
		return false, [26]byte{}, nil
	}

	messageHash, err := readMessages.ReadMessageHashHex(messageHashString)
	if (err != nil){
		return false, [26]byte{}, errors.New("mySentMessagesMapDatastore contains invalid map value: Not hex: " + messageHashString)
	}

	return true, messageHash, nil
}

//Outputs:
//	-bool: Message is pending or sent (we are already trying to send the message, or we already sent it)
//	-bool: Successfully started send (required information exists)
//	-error
func StartSendingMessage(messageIdentifier [20]byte, myIdentityHash [16]byte, recipientIdentityHash [16]byte, messageNetworkType byte, messageTimeSent int64, messageCommunication string, messageDuration int)(bool, bool, error){

	isValid := helpers.VerifyNetworkType(messageNetworkType)
	if (isValid == false){
		messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType)
		return false, false, errors.New("StartSendingMessage called with invalid messageNetworkType: " + messageNetworkTypeString)
	}

	addingMessageMutex.Lock()
	defer addingMessageMutex.Unlock()

	messageIsPending := checkIfMessageIsPending(messageIdentifier)
	if (messageIsPending == true){
		// We are already trying to send this message.
		return true, false, nil
	}

	messageIsSent, _, err := CheckIfMessageIsSent(messageIdentifier)
	if (err != nil) { return false, false, err }
	if (messageIsSent == true){
		// We already sent the message
		return true, false, nil
	}	

	identityExists, myIdentityPublicKey, myIdentityPrivateKey, err := myIdentity.GetMyPublicPrivateIdentityKeysFromIdentityHash(myIdentityHash)
	if (err != nil) { return false, false, err }
	if (identityExists == false) {
		return false, false, errors.New("StartSendingMessage called when my identity does not exist.")
	}

	myIdentityExists, isActiveStatus, err := myProfileStatus.GetMyProfileIsActiveStatus(myIdentityHash, messageNetworkType)
	if (err != nil) { return false, false, err }
	if (myIdentityExists == false) {
		return false, false, errors.New("My identity not found after being found already.")
	}
	if (isActiveStatus == false){
		return false, false, nil
	}

	exists, myChatKeysLatestUpdateTime, err := myChatKeys.GetMyChatKeysLatestUpdateTime(myIdentityHash, messageNetworkType)
	if (err != nil) { return false, false, err }
	if (exists == false){
		// We have not broadcast a profile yet.
		return false, false, nil
	}

	//Outputs:
	//	-[10]byte: Recipient Inbox
	//	-[32]byte: Recipient inbox sealer key
	//	-error
	getRecipientInbox := func()([10]byte, [32]byte, error){

		secretInboxFound, theirSecretInbox, theirSecretInboxSealerKey, err := peerSecretInboxes.GetPeerConversationCurrentSecretInbox(myIdentityHash, recipientIdentityHash, messageNetworkType)
		if (err != nil) { return [10]byte{}, [32]byte{}, err }
		if (secretInboxFound == true){
			return theirSecretInbox, theirSecretInboxSealerKey, nil
		}

		recipientPublicInbox, err := inbox.GetPublicInboxFromIdentityHash(recipientIdentityHash)
		if (err != nil) { return [10]byte{}, [32]byte{}, err }

		recipientPublicInboxSealerKey, err := inbox.GetPublicInboxSealerKeyFromIdentityHash(recipientIdentityHash)
		if (err != nil) { return [10]byte{}, [32]byte{}, err }

		return recipientPublicInbox, recipientPublicInboxSealerKey, nil
	}
	
	recipientInbox, recipientInboxSealerKey, err := getRecipientInbox()
	if (err != nil) { return false, false, err }

	if (len(recipientInboxSealerKey) != 32){
		return false, false, errors.New("getRecipientInbox returning invalid recipientInboxSealerKey: Invalid length.")
	}

	recipientInboxSealerKeyArray := [32]byte(recipientInboxSealerKey)

	peerIsDisabled, peerChatKeysExist, recipientNaclKey, recipientMlkemKey, recipientCsidhKey, err := peerChatKeys.GetPeerNewestActiveChatKeys(recipientIdentityHash, messageNetworkType)
	if (err != nil) { return false, false, err }
	if (peerIsDisabled == true || peerChatKeysExist == false) { 
		return false, false, nil
	}

	// The secret inboxes returned from the function below may not exist yet in our storage.
	// If they are missing, we will create new secret inbox(es)
	// We will add our new secret inboxes to our mySecretInboxes storage only if the message send is successful

	parametersExist, myCurrentSecretInboxExists, myExistingCurrentSecretInboxSeed, myNextSecretInboxExists, myExistingNextSecretInboxSeed, err := mySecretInboxes.GetMySecretInboxSeedsForMessage(myIdentityHash, recipientIdentityHash, messageNetworkType, messageTimeSent)
	if (err != nil) { return false, false, err }
	if (parametersExist == false){
		// We cannot determine the current epoch without the epoch parameters
		return false, false, nil
	}

	getMyCurrentSecretInboxSeed := func()([22]byte, error){

		if (myCurrentSecretInboxExists == true){
			return myExistingCurrentSecretInboxSeed, nil
		}

		newCurrentSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed()
		if (err != nil) { return [22]byte{}, err }

		return newCurrentSecretInboxSeed, nil
	}

	getMyNextSecretInboxSeed := func()([22]byte, error){

		if (myNextSecretInboxExists == true){
			return myExistingNextSecretInboxSeed, nil
		}

		newNextSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed()
		if (err != nil) { return [22]byte{}, err }

		return newNextSecretInboxSeed, nil
	}

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

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

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

	chatParametersExist, err := getParameters.CheckIfChatParametersExist(messageNetworkType)
	if (err != nil) { return false, false, err }
	if (chatParametersExist == false){
		return false, false, nil
	}

	// We have everything we need to send the message
	// It will only fail in 2 ways:
	//	1. We cannot connect to the payment proof server
	//	2. The cost of sending messages has increased since we froze the funds, and we don't have enough funds to send the message anymore

	finalMessage, messageHash, err := createMessages.CreateChatMessage(messageNetworkType, myIdentityHash, myIdentityPublicKey, myIdentityPrivateKey, messageTimeSent, myCurrentSecretInboxSeed, myNextSecretInboxSeed, myDeviceIdentifier, myChatKeysLatestUpdateTime, messageCommunication, recipientIdentityHash, recipientInbox, recipientNaclKey, recipientMlkemKey, recipientCsidhKey, recipientInboxSealerKeyArray)
	if (err != nil) { return false, false, err }

	sendMessageFunction := func(){

		attemptToSendMessage := func()error{

			messageSize := len(finalMessage)

			sufficientFundsExist, successfullyFunded, err := myProofFunds.FundMyMessageWithServer(messageIdentifier, messageHash, messageNetworkType, messageSize, messageDuration)
			if (err != nil) { return err }
			if (sufficientFundsExist == false){
				// Message send is not successful because we ran out of funds.
				// We will try to send message again in the future, when hopefully user has added more funds
				// This should only happen if the cost of sending the message has gone up since user froze funds
				return nil
			}
			if (successfullyFunded == false){
				// We could not connect to the server
				return nil
			}

			// Message was successfully funded
			// We add it to our broadcasts to be automatically broadcast
			// We also add the secret inbox seeds and the message map to our chat messages

			parametersExist, err := mySecretInboxes.AddMySecretInboxSeeds(myIdentityHash, recipientIdentityHash, messageNetworkType, messageTimeSent, myCurrentSecretInboxSeed, myNextSecretInboxSeed)
			if (err != nil) { return err }
			if (parametersExist == false){
				// This is very unlikely, because we just checked for parameters
				// We cannot determine the secret inbox epoch for the message we have funded
				// We won't be able to download message replies to our sent secret inboxes
				// As long as the user can pass this at least once within the current secret inbox epoch with a message sent
				// 		to this recipient, they should be able to add their secret inbox seeds to the mySecretInboxes datastore
				// TODO: Use a fallback value so this doesn't happen
			}

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

			err = myChatMessages.AddMyNewMessageToMyChatMessages("Sent", messageHash, messageIdentifier, myIdentityHash, recipientIdentityHash, messageNetworkType, messageTimeSent, messageCommunication)
			if (err != nil) { return err }

			messageIdentifierHex := encoding.EncodeBytesToHexString(messageIdentifier[:])
			messageHashHex := encoding.EncodeBytesToHexString(messageHash[:])

			err = mySentMessagesMapDatastore.SetMapEntry(messageIdentifierHex, messageHashHex)
			if (err != nil){ return err }

			return nil
		}

		err := attemptToSendMessage()
		if (err != nil){
			logger.AddLogError("General", err)
		}

		deleteMessageFromPendingMessagesMap(messageIdentifier)
		pendingMessagesWaitgroup.Done()
	}

	addMessageToPendingMessagesMap(messageIdentifier)
	pendingMessagesWaitgroup.Add(1)

	go sendMessageFunction()

	return false, true, nil
}

// This function calculates current network cost for a provided message
// This is used to show the user approximately how much a message will cost
//Outputs:
//	-bool: Parameters exist
//	-float64: Cost in provided currency
//	-error
func GetMessageNetworkCurrencyCostForProvidedDuration(messageNetworkType byte, messageSizeBytes int, messageDurationToFund int, currencyCode string)(bool, float64, error){

	isValid := helpers.VerifyNetworkType(messageNetworkType)
	if (isValid == false){
		messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType)
		return false, 0, errors.New("GetMessageNetworkCurrencyCostForProvidedDuration called with invalid messageNetworkType: " + messageNetworkTypeString)
	}

	messageKilobytes := messageSizeBytes/1000

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

	parametersExist, currentCostPerDay, err := getParameters.GetMessageKilobyteGoldCostPerDay(messageNetworkType, currentTime)
	if (err != nil) { return false, 0, err }
	if (parametersExist == false) {
		return false, 0, nil
	}

	messageDays := messageDurationToFund/86400

	// GetMessageKilobyteGoldCostPerDay returns grams. We convert to kilograms.
	kilogramsCostPerDay := currentCostPerDay/1000

	costInKilogramsOfGold := float64(messageDays) * float64(messageKilobytes) * kilogramsCostPerDay

	exchangeRatesFound, currencyCost, err := convertCurrencies.ConvertKilogramsOfGoldToAnyCurrency(messageNetworkType, costInKilogramsOfGold, currencyCode)
	if (err != nil) { return false, 0, err }
	if (exchangeRatesFound == false){
		return false, 0, nil
	}

	return true, currencyCost, nil
}

// We use this function to estimate the size of a message we have not created yet
// This enables us to show the user how much the message will cost, and for us to freeze the required funds
func GetEstimatedMessageSize(recipientIdentityHash [16]byte, messageCommunication string)(int, error){

	recipientIdentityType, err := identity.GetIdentityTypeFromIdentityHash(recipientIdentityHash)
	if (err != nil) {
		recipientIdentityHashHex := encoding.EncodeBytesToHexString(recipientIdentityHash[:])
		return 0, errors.New("GetEstimatedMessageSize called with invalid recipientIdentityHash: " + recipientIdentityHashHex)
	}

	senderIdentityPublicKey, senderIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys()
	if (err != nil) { return 0, err }

	senderIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(senderIdentityPublicKey, recipientIdentityType)
	if (err != nil) { return 0, err }

	senderCurrentSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed()
	if (err != nil) { return 0, err }

	senderNextSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed()
	if (err != nil) { return 0, err }

	recipientInbox, err := inbox.GetPublicInboxFromIdentityHash(recipientIdentityHash)
	if (err != nil) { return 0, err }

	doubleSealedKeysSealerKey, err := helpers.GetNewRandom32ByteArray()
	if (err != nil) { return 0, err }

	senderLatestChatKeysUpdateTime := time.Now().UnixNano()

	recipientNaclPublicKey, err := nacl.GetNewRandomPublicNaclKey()
	if (err != nil) { return 0, err }

	recipientMlkemPublicKey, err := mlkem.GetNewRandomPublicMlkemKey()
	if (err != nil) { return 0, err }

	recipientCsidhPublicKey, _, err := csidh.GetNewRandomPublicPrivateCsidhKeys()
	if (err != nil) { return 0, err }

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

	senderDeviceIdentifier, err := helpers.GetNewRandomDeviceIdentifier()
	if (err != nil) { return 0, err }

	finalMessage, _, err := createMessages.CreateChatMessage(1, senderIdentityHash, senderIdentityPublicKey, senderIdentityPrivateKey, messageSentTime, senderCurrentSecretInboxSeed, senderNextSecretInboxSeed, senderDeviceIdentifier, senderLatestChatKeysUpdateTime, messageCommunication, recipientIdentityHash, recipientInbox, recipientNaclPublicKey, recipientMlkemPublicKey, recipientCsidhPublicKey, doubleSealedKeysSealerKey)
	if (err != nil) { return 0, err }

	messageSize := len(finalMessage)

	return messageSize, nil
}



