
// moderatorControversy provides functions for calculating a moderator's controversy rating
// Controversy ratings are used by moderators to find controversial moderators
// These are moderators who might be worthy of being banned or debated with

package moderatorControversy

// Controversy calculation can be tuned in several ways:
//
//	1. Weight controversy by moderator scores
//		-If we turn off, we use number of moderators.
//		-If we turn on, we use combined score of moderators
//		-When toggled on, controversy is lower if disagreement is among low-score moderators
//	2. Exclude banned moderators in calculation
//		-When enabled, banned moderator verdicts will not be included
//	3. TODO: Only include from a specific time range (Time min- Time Max)
//		-This is useful to see moderators who only recently became controversial
//	4. Omit agree with consensus in calculation
//		-When enabled, moderator controversy will only count when moderators disagree with the majority
//		-This prevents moderators from making their controversy scores seem lower by creating many agreement reviews
//		-When enabled, the controversy score is a measure of how much the moderator disagrees, only when they dissent from the majority

//TODO: Reduce the calculation to a random sampling to make it less expensive

import "seekia/internal/badgerDatabase"
import "seekia/internal/contentMetadata"
import "seekia/internal/encoding"
import "seekia/internal/helpers"
import "seekia/internal/identity"
import "seekia/internal/moderation/bannedModeratorConsensus"
import "seekia/internal/moderation/enabledModerators"
import "seekia/internal/moderation/moderatorScores"
import "seekia/internal/moderation/readReviews"
import "seekia/internal/moderation/reviewStorage"

import "bytes"
import "errors"

//Outputs:
//	-bool: Score is known (parameters exist, downloading any reviews)
//	-int64: Moderator controversy score
//	-error
func GetModeratorControversyRating(moderatorIdentityHash [16]byte, networkType byte)(bool, int64, error){

	isValid, err := identity.VerifyIdentityHash(moderatorIdentityHash, true, "Moderator")
	if (err != nil) { return false, 0, err }
	if (isValid == false){
		moderatorIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityHash[:])
		return false, 0, errors.New("GetModeratorControversyRating called with invalid moderatorIdentityHash: " + moderatorIdentityHashHex)
	}

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

	//TODO: Make sure we are actually downloading any reviews (moderator mode/host mode is enabled)
	// We may only be hosting a small range of total reviews. This is fine.
	// Controversy will be calculated for reviews of content within the range

	// There is no controversy for identity reviews
	// This is because there can be no identity approve reviews, only ban reviews.
	reviewTypesList := []string{"Profile", "Attribute", "Message"}

	numberOfRatingsIncluded := 0
	controversyRatingsSum := float64(0)

	for _, reviewType := range reviewTypesList{

		// These are all reviews created by this moderator
		exists, reviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(moderatorIdentityHash, reviewType)
		if (err != nil) { return false, 0, err }
		if (exists == false){
			// No reviews exist by this moderator
			// No controversy exists
			return true, 0, nil
		}

		reviewsList := make([]readReviews.ReviewWithHash, 0)

		for _, reviewHash := range reviewHashesList{

			exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash)
			if (err != nil) { return false, 0, err }
			if (exists == false){
				// Review must have been deleted, backgroundJobs will prune reviewHashesList			
				continue
			}

			reviewObject := readReviews.ReviewWithHash{
				ReviewHash: reviewHash,
				ReviewBytes: reviewBytes,
			}

			reviewsList = append(reviewsList, reviewObject)
		}

		// This returns a map of the newest reviews for each reviewedHash in the list
		newestReviewsMap, err := readReviews.GetNewestModeratorReviewsMapFromReviewsList(reviewsList, moderatorIdentityHash, networkType, true, reviewType)
		if (err != nil) { return false, 0, err }

		for reviewedHashString, reviewBytes := range newestReviewsMap{

			reviewedHash := []byte(reviewedHashString)

			ableToRead, _, reviewNetworkType, reviewerIdentityHash, _, currentReviewType, currentReviewedHash, reviewVerdict, reviewMap, err := readReviews.ReadReview(false, reviewBytes)
			if (err != nil) { return false, 0, err }
			if (ableToRead == false){
				return false, 0, errors.New("Database corrupt: Contains invalid review.")
			}
			if (reviewNetworkType != networkType){
				return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning different networkType review.")
			}
			if (reviewerIdentityHash != moderatorIdentityHash){
				return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning review by different reviewer.")
			}
			areEqual := bytes.Equal(reviewedHash, currentReviewedHash)
			if (areEqual == false){
				return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning different reviewedHash review")
			}
			if (currentReviewType != reviewType){
				return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning review of different reviewType.")
			}
			if (reviewVerdict == "None"){
				return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning review with None verdict.")
			}

			//Outputs:
			//	-bool: Required data exists
			//	-map[[16]byte]int64: Content Approve advocates map map (identity hash -> Time of approval)
			//	-map[[16]byte]int64: Content Ban advocates map (identity hash -> Time of ban)
			//	-error
			getApproveAndBanAdvocateMaps := func()(bool, map[[16]byte]int64, map[[16]byte]int64, error){

				if (reviewType == "Profile"){

					if (len(reviewedHash) != 28){
						reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash)
						return false, nil, nil, errors.New("ReadReview returning invalid reviewed profileHash: " + reviewedHashHex)
					}

					reviewedProfileHash := [28]byte(reviewedHash)

					metadataExists, _, profileNetworkType, _, _, profileIsDisabled, _, profileAttributeHashesMap, err := contentMetadata.GetProfileMetadata(reviewedProfileHash)
					if (err != nil) { return false, nil, nil, err }
					if (metadataExists == false){
						// We can't verify attribute reviews

						return false, nil, nil, nil
					}
					if (profileNetworkType != reviewNetworkType){
						// This moderator has reviewed a profile from a different network as the review network
						// This moderator must be malicious
						// The Seekia app should automatically ban these moderators
						// We will not count this profile in the calculation of this moderator's controversy

						return false, nil, nil, nil
					}
					if (profileIsDisabled == true){
						// Disabled profiles can't be reviewed.
						// The review is invalid. This moderator must be malicious.
						// The moderator should automatically ban them in the background.					

						return false, nil, nil, nil
					}

					// We will find all moderators who have approved/banned the profile
					// This requires finding users who have banned any attribute within the profile
					// This could override their full profile approval, if the attribute ban was submitted after the full profile approval

					// profileAttributeHashesMap is a map whose values are the attribute hashes of this profile
					attributeHashesList := helpers.GetListOfMapValues(profileAttributeHashesMap)

					approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetProfileVerdictMaps(reviewedProfileHash, profileNetworkType, true, attributeHashesList)
					if (err != nil) { return false, nil, nil, err }

					return true, approveAdvocatesMap, banAdvocatesMap, nil
				}
				if (reviewType == "Attribute"){

					// We want to find all moderators who have approved/banned this particular attribute
					// This requires checking for full profile approvals for all profiles which contain this attribute

					if (len(reviewedHash) != 27){
						reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash)
						return false, nil, nil, errors.New("ReadReview returning invalid reviewed attributeHash: " + reviewedHashHex)
					}

					reviewedAttributeHash := [27]byte(reviewedHash)

					getAttributeProfileHashesList := func()([][28]byte, error){		

						anyExist, attributeProfilesList, err := badgerDatabase.GetAttributeProfilesList(reviewedAttributeHash)
						if (err != nil) { return nil, err }
						if (anyExist == false){
							emptyList := make([][28]byte, 0)
							return emptyList, nil
						}
						return attributeProfilesList, nil
					}

					attributeProfileHashesList, err := getAttributeProfileHashesList()
					if (err != nil) { return false, nil, nil, err }

					approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetProfileAttributeVerdictMaps(reviewedAttributeHash, networkType, true, attributeProfileHashesList)
					if (err != nil) { return false, nil, nil, err }

					return true, approveAdvocatesMap, banAdvocatesMap, nil
				}
				if (reviewType == "Message"){

					if (len(reviewedHash) != 26){
						reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash)
						return false, nil, nil, errors.New("ReadReview returning invalid reviewed messageHash: " + reviewedHashHex)
					}

					reviewedMessageHash := [26]byte(reviewedHash)

					metadataExists, _, messageNetworkType, _, _, messageCipherKeyHash, err := contentMetadata.GetMessageMetadata(reviewedMessageHash)
					if (err != nil){ return false, nil, nil, err }
					if (metadataExists == false){
						// We cannot verify reviews, so no approve or ban advocates can be determined

						return false, nil, nil, nil
					}
					if (messageNetworkType != reviewNetworkType){

						// Review author must be malicious
						// They created a review for a message on a different network
						// The app should ban them automatically in the background

						return false, nil, nil, nil
					}

					reviewIsValid, err := readReviews.VerifyMessageReviewCipherKey(reviewMap, messageCipherKeyHash)
					if (err != nil){ return false, nil, nil, err }
					if (reviewIsValid == false){
						// Moderator must be malicious.
						// They should be banned automatically in the background

						return false, nil, nil, nil
					}

					approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetMessageVerdictMaps(reviewedMessageHash, messageNetworkType, messageCipherKeyHash)
					if (err != nil) { return false, nil, nil, err }

					return true, approveAdvocatesMap, banAdvocatesMap, nil
				}

				return false, nil, nil, errors.New("reviewTypesList contains invalid reviewType: " + reviewType)
			}

			requiredDataExists, approveAdvocatesMap, banAdvocatesMap, err := getApproveAndBanAdvocateMaps()
			if (err != nil) { return false, 0, err }
			if (requiredDataExists == false){
				continue
			}
			if (len(approveAdvocatesMap) == 0 && len(banAdvocatesMap) == 0){
				continue
			}

			//TODO: Fix below to retrieve from settings
			includeBannedModerators := true
			multiplyIdentityScores := true
			excludeConsensusAgreement := true

			// Outputs:
			//	-bool: Required data exists
			//	-float64: Moderators value
			//	-error
			getModeratorsValue := func(inputModeratorsMap map[[16]byte]int64)(bool, float64, error){

				moderatorsValue := float64(0)

				for identityHash, _ := range inputModeratorsMap{

					if (identityHash == reviewerIdentityHash){
						// Exclude current reviewer
						continue
					}

					moderatorIsEnabled, err := enabledModerators.CheckIfModeratorIsEnabled(true, identityHash, networkType)
					if (err != nil) { return false, 0, err }
					if (moderatorIsEnabled == false){
						// Skip this moderator
						continue
					}

					if (includeBannedModerators == false){

						requiredDataBeingDownloaded, parametersExist, isBanned, err := bannedModeratorConsensus.GetModeratorIsBannedStatus(true, identityHash, networkType)
						if (err != nil){ return false, 0, err }
						if (requiredDataBeingDownloaded == false || parametersExist == false){
							return false, 0, nil
						}
						if (isBanned == true){
							continue
						}
					}

					if (multiplyIdentityScores == false){
						moderatorsValue += 1
						continue
					}

					statusIsKnown, moderatorScore, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(identityHash)
					if (err != nil) { return false, 0, err }
					if (statusIsKnown == false){
						// Client has not downloaded moderator score. It will do so automatically.
						continue
					}
					if (scoreIsSufficient == false){
						// Moderator cannot participate in consensus				
						continue
					}
					moderatorsValue += moderatorScore
				}

				return true, moderatorsValue, nil
			}

			requiredDataExists, approveModeratorsValue, err := getModeratorsValue(approveAdvocatesMap)
			if (requiredDataExists == false){
				// We need the parameters and required data to calculate if moderators are banned
				// This is required for all moderators, and is required to calculate controversy
				// Thus, we cannot calculate controversy for this moderator

				return false, 0, nil
			}

			requiredDataExists, banModeratorsValue, err := getModeratorsValue(banAdvocatesMap)
			if (requiredDataExists == false){
				return false, 0, nil
			}

			if (approveModeratorsValue == 0 && banModeratorsValue == 0){
				continue
			}
		
			getAgreeDisagreeValues := func()(float64, float64, error){

				if (reviewVerdict == "Approve"){
					return approveModeratorsValue, banModeratorsValue, nil
				}

				return banModeratorsValue, approveModeratorsValue, nil
			}

			agreeValue, disagreeValue, err := getAgreeDisagreeValues()
			if (err != nil) { return false, 0, err }

			if (excludeConsensusAgreement == true){

				if (agreeValue > disagreeValue){
					continue
				}
			}

			getCurrentControversyRating := func()float64{
				if (disagreeValue == 0){
					return 0
				}
				currentControversyRating := (agreeValue/disagreeValue) * disagreeValue

				return currentControversyRating
			}

			currentControversyRating := getCurrentControversyRating()

			controversyRatingsSum += currentControversyRating
			numberOfRatingsIncluded += 1
		}
	}

	if (numberOfRatingsIncluded == 0){
		// There is no controversy
		return true, 0, nil
	}

	moderatorControversyRating := controversyRatingsSum/float64(numberOfRatingsIncluded)

	scoreInt, err := helpers.FloorFloat64ToInt64(moderatorControversyRating)
	if (err != nil) { return false, 0, err }

	return true, scoreInt, nil
}



