
// myContacts provides functions to manage a user's contacts

package myContacts

import "seekia/internal/encoding"
import "seekia/internal/helpers"
import "seekia/internal/identity"
import "seekia/internal/myDatastores/myList"
import "seekia/internal/myDatastores/myMapList"

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

// This mutex will be locked whenever we edit contacts and their categories
var updatingMyContactsMutex sync.Mutex

var myContactsMapListDatastore *myMapList.MyMapList

var myMateContactCategoriesListDatastore *myList.MyList
var myHostContactCategoriesListDatastore *myList.MyList
var myModeratorContactCategoriesListDatastore *myList.MyList

func getContactCategoriesListDatastoreFromIdentityType(identityType string)(*myList.MyList, error){

	if (identityType == "Mate"){
		return myMateContactCategoriesListDatastore, nil
	}
	if (identityType == "Host"){
		return myHostContactCategoriesListDatastore, nil
	}
	if (identityType == "Moderator"){
		return myModeratorContactCategoriesListDatastore, nil
	}
	return nil, errors.New("getContactCategoriesListDatastoreFromIdentityType called with invalid IdentityType: " + identityType)
}

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

	updatingMyContactsMutex.Lock()
	defer updatingMyContactsMutex.Unlock()

	newMyContactsMapListDatastore, err := myMapList.CreateNewMapList("MyContacts")
	if (err != nil) { return err }

	myContactsMapListDatastore = newMyContactsMapListDatastore

	newMateContactCategoriesListDatastore, err := myList.CreateNewList("MyMateContactCategories")
	if (err != nil) { return err }

	newHostContactCategoriesListDatastore, err := myList.CreateNewList("MyHostContactCategories")
	if (err != nil) { return err }

	newModeratorContactCategoriesListDatastore, err := myList.CreateNewList("MyModeratorContactCategories")
	if (err != nil) { return err }

	myMateContactCategoriesListDatastore = newMateContactCategoriesListDatastore
	myHostContactCategoriesListDatastore = newHostContactCategoriesListDatastore
	myModeratorContactCategoriesListDatastore = newModeratorContactCategoriesListDatastore

	return nil
}

//Outputs:
//	-bool: Contact already exists
//	-error
func AddContact(userIdentityHash [16]byte, contactName string, categoriesList []string, contactDescription string)(bool, error){
	
	updatingMyContactsMutex.Lock()
	defer updatingMyContactsMutex.Unlock()

	userIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(userIdentityHash)
	if (err != nil) {
		userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:])
		return false, errors.New("AddContact called with invalid identity hash: " + userIdentityHashHex)
	}

	if (contactName == ""){
		return false, errors.New("AddContact called with empty contactName.")
	}

	containsDuplicates, _ := helpers.CheckIfListContainsDuplicates(categoriesList)
	if (containsDuplicates == true){
		return false, errors.New("AddContact called with CategoriesList that contains duplicates.")
	}

	contactExists, _, _, _, _, err := GetMyContactDetails(userIdentityHash)
	if (err != nil) { return false, err }
	if (contactExists == true){
		// GUI should prevent this from happening.
		return false, errors.New("AddContact called with pre-existing contact.")
	}

	currentTime := time.Now().Unix()
	currentTimeString := helpers.ConvertInt64ToString(currentTime)

	newMapItem := map[string]string{
		"IdentityHash": userIdentityHashString,
		"Name": contactName,
		"AddedTime": currentTimeString,
		"Description": contactDescription,
	}

	if (len(categoriesList) != 0){

		categoriesListBase64 := make([]string, 0, len(categoriesList))

		for _, categoryName := range categoriesList{

			if (categoryName == ""){
				return false, errors.New("AddContact called with categoriesList containing empty category.")
			}

			categoryBase64 := encoding.EncodeBytesToBase64String([]byte(categoryName))

			categoriesListBase64 = append(categoriesListBase64, categoryBase64)
		}

		categoriesListJoined := strings.Join(categoriesListBase64, "+")
		newMapItem["Categories"] = categoriesListJoined
	}
	
	err = myContactsMapListDatastore.AddMapListItem(newMapItem)
	if (err != nil) { return false, err }

	return false, nil
}

func DeleteContact(userIdentityHash [16]byte)error{

	updatingMyContactsMutex.Lock()
	defer updatingMyContactsMutex.Unlock()

	userIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(userIdentityHash)
	if (err != nil) {
		userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:])
		return errors.New("DeleteContact called with invalid identity hash: " + userIdentityHashHex)
	}

	mapToDelete := map[string]string{
		"IdentityHash": userIdentityHashString,
	}

	err = myContactsMapListDatastore.DeleteMapListItems(mapToDelete)
	if (err != nil) { return err }

	return nil
}

// Outputs:
//	-[][16]byte: My contact identity hashes list
//	-error
func GetMyContactsList(identityType string)([][16]byte, error){

	myContactsMapList, err := GetMyContactsMapList(identityType)
	if (err != nil) { return nil, err }

	myContactIdentityHashesList := make([][16]byte, 0, len(myContactsMapList))

	for _, contactMap := range myContactsMapList{

		contactIdentityHashString, exists := contactMap["IdentityHash"]
		if (exists == false) {
			return nil, errors.New("Malformed contact map: Missing IdentityHash")
		}

		contactIdentityHash, contactIdentityType, err := identity.ReadIdentityHashString(contactIdentityHashString)
		if (err != nil){
			return nil, errors.New("Malformed contact map: Contains invalid identityHash: " + contactIdentityHashString)
		}
		if (contactIdentityType != identityType){
			return nil, errors.New("Malformed contact map: Contains identityHash of a different identityType.")
		}

		myContactIdentityHashesList = append(myContactIdentityHashesList, contactIdentityHash)
	}

	return myContactIdentityHashesList, nil
}

//Outputs:
//	-[]map[string]string: My contacts map list
//	-error
func GetMyContactsMapList(identityType string)([]map[string]string, error){

	if (identityType != "Mate" && identityType != "Host" && identityType != "Moderator"){
		return nil, errors.New("GetMyContactsMapList called with invalid identity type: " + identityType)
	}

	myContactsMapList, err := myContactsMapListDatastore.GetMapList()
	if (err != nil) { return nil, err }

	identityTypeContacts := make([]map[string]string, 0)

	for _, contactMap := range myContactsMapList{

		currentIdentityHash, exists := contactMap["IdentityHash"]
		if (exists == false) {
			return nil, errors.New("MyContacts map missing IdentityHash")
		}

		_, currentIdentityType, err := identity.ReadIdentityHashString(currentIdentityHash)
		if (err != nil) {
			return nil, errors.New("MyContacts map contains invalid IdentityHash: " + currentIdentityHash)
		}

		if (currentIdentityType == identityType) {

			identityTypeContacts = append(identityTypeContacts, contactMap)
		}
	}

	return identityTypeContacts, nil
}

func CheckIfUserIsMyContact(userIdentityHash [16]byte) (bool, error) {

	contactExists, _, _, _, _, err := GetMyContactDetails(userIdentityHash)
	if (err != nil) { return false, err }

	return contactExists, nil
}

//Outputs
//	-bool: contact exists
//	-string: Contact name
//	-int64: Added time
//	-[]string: List of categories
//	-string: Description
//	-error
func GetMyContactDetails(userIdentityHash [16]byte)(bool, string, int64, []string, string, error){

	userIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(userIdentityHash)
	if (err != nil) {
		userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:])
		return false, "", 0, nil, "", errors.New("GetMyContactDetails called with invalid identity hash: " + userIdentityHashHex)
	}

	lookupMap := map[string]string{
		"IdentityHash": userIdentityHashString,
	}

	anyItemsFound, contactsMapList, err := myContactsMapListDatastore.GetMapListItems(lookupMap)
	if (err != nil) { return false, "", 0, nil, "", err }
	if (anyItemsFound == false) {
		return false, "", 0, nil, "", nil
	}
	
	if (len(contactsMapList) != 1) {
		return false, "", 0, nil, "", errors.New("Malformed contacts map. Two contacts found for 1 identity hash.")
	}

	contactDetailsMap := contactsMapList[0]

	contactName, exists := contactDetailsMap["Name"]
	if (exists == false) {
		return false, "", 0, nil, "", errors.New("Malformed MyContacts map list: Item missing Name")
	}

	addedTime, exists := contactDetailsMap["AddedTime"]
	if (exists == false){
		return false, "", 0, nil, "", errors.New("Malformed MyContacts map list: Item missing AddedTime")
	}

	addedTimeInt64, err := helpers.ConvertStringToInt64(addedTime)
	if (err != nil) {
		return false, "", 0, nil, "", errors.New("Malformed MyContacts map list: AddedTime is invalid: " + addedTime)
	}

	contactDescription, exists := contactDetailsMap["Description"]
	if (exists == false){
		return false, "", 0, nil, "", errors.New("Malformed MyContacts map list: Item missing Description")
	}

	contactCategoriesListString, exists := contactDetailsMap["Categories"]
	if (exists == false) {
		emptyList := make([]string, 0)
		return true, contactName, addedTimeInt64, emptyList, contactDescription, nil
	}

	contactCategoriesListBase64 := strings.Split(contactCategoriesListString, "+")

	contactCategoriesList := make([]string, 0, len(contactCategoriesListBase64))

	for _, categoryBase64 := range contactCategoriesListBase64{

		categoryString, err := encoding.DecodeBase64StringToUnicodeString(categoryBase64)
		if (err != nil) {
			return false, "", 0, nil, "", errors.New("MyContacts map list malformed: Category name is not Base64: " + categoryBase64)
		}

		contactCategoriesList = append(contactCategoriesList, categoryString)
	}

	// We prune any deleted categories (there shouldn't be any)

	allContactCategoriesList, err := GetAllMyContactCategories(userIdentityType)
	if (err != nil) { return false, "", 0, nil, "", err }

	contactCategoriesListPruned := helpers.GetSharedItemsOfTwoStringLists(contactCategoriesList, allContactCategoriesList)

	return true, contactName, addedTimeInt64, contactCategoriesListPruned, contactDescription, nil
}

// A user's categories are always selected from the categoriesMapList
// Thus, there will never be a need to add a category to the categoriesMapList from this function (or the addContact function)
// All new categories must be added via AddContactCategory function
func EditContact(userIdentityHash [16]byte, newContactName string, newContactCategoriesList []string, newContactDescription string)error{

	updatingMyContactsMutex.Lock()
	defer updatingMyContactsMutex.Unlock()

	userIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(userIdentityHash)
	if (err != nil) {
		userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:])
		return errors.New("EditContact called with invalid identity hash: " + userIdentityHashHex)
	}

	containsDuplicates, _ := helpers.CheckIfListContainsDuplicates(newContactCategoriesList)
	if (containsDuplicates == true){
		return errors.New("EditContact called with newContactCategoriesList that contains duplicates")
	}

	myContactsMapList, err := GetMyContactsMapList(userIdentityType)
	if (err != nil) { return err }

	newContactsMapList := make([]map[string]string, 0)

	for _, contactMap := range myContactsMapList{

		currentIdentityHash, exists := contactMap["IdentityHash"]
		if (exists == false) {
			return errors.New("MyContacts map list item missing IdentityHash")
		}
		
		if (currentIdentityHash != userIdentityHashString){
			newContactsMapList = append(newContactsMapList, contactMap)
			continue
		}

		contactAddedTime, exists := contactMap["AddedTime"]
		if (exists == false) {
			return errors.New("MyContacts map list item missing AddedTime")
		}

		newContactMap := map[string]string{
			"IdentityHash": userIdentityHashString,
			"AddedTime": contactAddedTime,
			"Name": newContactName,
			"Description": newContactDescription,
		}

		if (len(newContactCategoriesList) != 0){
		
			newCategoriesBase64List := make([]string, 0)
	
			for _, categoryName := range newContactCategoriesList{

				if (categoryName == ""){
					return errors.New("EditContact called with newContactCategoriesList which contains empty string.")
				}

				categoryNameBase64 := encoding.EncodeBytesToBase64String([]byte(categoryName))

				newCategoriesBase64List = append(newCategoriesBase64List, categoryNameBase64)
			}

			newCategoriesListString := strings.Join(newCategoriesBase64List, "+")
	
			newContactMap["Categories"] = newCategoriesListString
		}

		newContactsMapList = append(newContactsMapList, newContactMap)
	}

	err = myContactsMapListDatastore.OverwriteMapList(newContactsMapList)
	if (err != nil) { return err }

	return nil
}


func GetAllMyContactCategories(identityType string)([]string, error){

	contactCategoriesListDatastore, err := getContactCategoriesListDatastoreFromIdentityType(identityType)
	if (err != nil) { return nil, err }

	myContactCategoriesList, err := contactCategoriesListDatastore.GetList()
	if (err != nil) { return nil, err }

	allCategoriesListPruned := helpers.RemoveDuplicatesFromStringList(myContactCategoriesList)

	return allCategoriesListPruned, nil
}


func AddContactCategory(identityType string, categoryName string)error{

	updatingMyContactsMutex.Lock()
	defer updatingMyContactsMutex.Unlock()

	contactCategoriesListDatastore, err := getContactCategoriesListDatastoreFromIdentityType(identityType)
	if (err != nil) { return err }

	myContactCategoriesList, err := contactCategoriesListDatastore.GetList()
	if (err != nil) { return err }

	categoryExists := slices.Contains(myContactCategoriesList, categoryName)
	if (categoryExists == true){
		// GUI should prevent this
		return errors.New("AddContactCategory called with pre-existing category.")
	}

	err = contactCategoriesListDatastore.AddListItem(categoryName)
	if (err != nil) { return err }

	return nil
}


func DeleteContactCategory(identityType string, categoryToDeleteName string)error{

	updatingMyContactsMutex.Lock()
	defer updatingMyContactsMutex.Unlock()

	contactCategoriesListDatastore, err := getContactCategoriesListDatastoreFromIdentityType(identityType)
	if (err != nil) { return err }

	err = contactCategoriesListDatastore.DeleteListItem(categoryToDeleteName)
	if (err != nil) { return err }

	// Now we prune any deleted categories from each contact entry

	categoryToDeleteNameBase64 := encoding.EncodeBytesToBase64String([]byte(categoryToDeleteName))

	myContactsMapList, err := GetMyContactsMapList(identityType)
	if (err != nil) { return err }

	newContactsMapList := make([]map[string]string, 0)

	for _, contactMap := range myContactsMapList{

		currentIdentityHash, exists := contactMap["IdentityHash"]
		if (exists == false) {
			return errors.New("MyContacts map list item missing IdentityHash")
		}

		_, userIdentityType, err := identity.ReadIdentityHashString(currentIdentityHash)
		if (err != nil) {
			return errors.New("MyContacts map list item contains invalid IdentityHash: " + currentIdentityHash)
		}

		if (userIdentityType != identityType){
			newContactsMapList = append(newContactsMapList, contactMap)
			continue
		}

		contactCategoriesListString, exists := contactMap["Categories"]
		if (exists == false) {
			newContactsMapList = append(newContactsMapList, contactMap)
			continue
		}

		contactCategoriesListBase64 := strings.Split(contactCategoriesListString, "+")

		newCategoriesList, deletedAny := helpers.DeleteAllMatchingItemsFromList(contactCategoriesListBase64, categoryToDeleteNameBase64)
		if (deletedAny == false){
			newContactsMapList = append(newContactsMapList, contactMap)
			continue
		}
		
		newCategoriesListJoined := strings.Join(newCategoriesList, "+")

		contactMap["Categories"] = newCategoriesListJoined

		newContactsMapList = append(newContactsMapList, contactMap)
	}

	err = myContactsMapListDatastore.OverwriteMapList(newContactsMapList)
	if (err != nil) { return err }

	return nil
}



