package communities

import (
	"context"
	"errors"
	"fmt"
	"math/big"
	"strconv"
	"strings"

	"go.uber.org/zap"

	maps "golang.org/x/exp/maps"
	slices "golang.org/x/exp/slices"

	gethcommon "github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/status-im/status-go/protocol/ens"
	"github.com/status-im/status-go/protocol/protobuf"
	walletcommon "github.com/status-im/status-go/services/wallet/common"
	"github.com/status-im/status-go/services/wallet/thirdparty"
)

type PermissionChecker interface {
	CheckPermissionToJoin(*Community, []gethcommon.Address) (*CheckPermissionToJoinResponse, error)
	CheckPermissions(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error)
	CheckPermissionsWithPreFetchedData(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool, collectiblesOwners CollectiblesOwners) (*CheckPermissionsResponse, error)
}

type DefaultPermissionChecker struct {
	tokenManager        TokenManager
	collectiblesManager CollectiblesManager
	ensVerifier         *ens.Verifier

	logger *zap.Logger
}

type PreParsedPermissionsData struct {
	Erc721TokenRequirements map[uint64]map[string]*protobuf.TokenCriteria
	Erc20TokenAddresses     []gethcommon.Address
	Erc20ChainIDsMap        map[uint64]bool
	Erc721ChainIDsMap       map[uint64]bool
}

type PreParsedCommunityPermissionsData struct {
	*PreParsedPermissionsData
	Permissions []*CommunityTokenPermission
}

func (p *DefaultPermissionChecker) getOwnedENS(addresses []gethcommon.Address) ([]string, error) {
	ownedENS := make([]string, 0)
	if p.ensVerifier == nil {
		p.logger.Warn("no ensVerifier configured for communities manager")
		return ownedENS, nil
	}
	for _, address := range addresses {
		name, err := p.ensVerifier.ReverseResolve(address)
		if err != nil && err.Error() != "not a resolver" {
			return ownedENS, err
		}
		if name != "" {
			ownedENS = append(ownedENS, name)
		}
	}
	return ownedENS, nil
}

type collectiblesBalancesGetter = func(ctx context.Context, chainID walletcommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error)

func (p *DefaultPermissionChecker) getOwnedERC721Tokens(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64, getCollectiblesBalances collectiblesBalancesGetter) (CollectiblesByChain, error) {
	if p.collectiblesManager == nil {
		return nil, errors.New("no collectibles manager")
	}

	ctx := context.Background()

	ownedERC721Tokens := make(CollectiblesByChain)

	for chainID, erc721Tokens := range tokenRequirements {

		skipChain := true
		for _, cID := range chainIDs {
			if chainID == cID {
				skipChain = false
			}
		}

		if skipChain {
			continue
		}

		contractAddresses := make([]gethcommon.Address, 0)
		for contractAddress := range erc721Tokens {
			contractAddresses = append(contractAddresses, gethcommon.HexToAddress(contractAddress))
		}

		if _, exists := ownedERC721Tokens[chainID]; !exists {
			ownedERC721Tokens[chainID] = make(map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress)
		}

		for _, owner := range walletAddresses {
			balances, err := getCollectiblesBalances(ctx, walletcommon.ChainID(chainID), owner, contractAddresses)
			if err != nil {
				p.logger.Info("couldn't fetch owner assets", zap.Error(err))
				return nil, err
			}
			ownedERC721Tokens[chainID][owner] = balances
		}
	}
	return ownedERC721Tokens, nil
}

func (p *DefaultPermissionChecker) accountChainsCombinationToMap(combinations []*AccountChainIDsCombination) map[gethcommon.Address][]uint64 {
	result := make(map[gethcommon.Address][]uint64)
	for _, combination := range combinations {
		result[combination.Address] = combination.ChainIDs
	}
	return result
}

// merge valid combinations w/o duplicates
func (p *DefaultPermissionChecker) MergeValidCombinations(left, right []*AccountChainIDsCombination) []*AccountChainIDsCombination {

	leftMap := p.accountChainsCombinationToMap(left)
	rightMap := p.accountChainsCombinationToMap(right)

	// merge maps, result in left map
	for k, v := range rightMap {
		if _, exists := leftMap[k]; !exists {
			leftMap[k] = v
			continue
		} else {
			// append chains which are new
			chains := leftMap[k]
			for _, chainID := range v {
				if !slices.Contains(chains, chainID) {
					chains = append(chains, chainID)
				}
			}
			leftMap[k] = chains
		}
	}

	result := []*AccountChainIDsCombination{}
	for k, v := range leftMap {
		result = append(result, &AccountChainIDsCombination{
			Address:  k,
			ChainIDs: v,
		})
	}

	return result
}

func (p *DefaultPermissionChecker) CheckPermissionToJoin(community *Community, addresses []gethcommon.Address) (*CheckPermissionToJoinResponse, error) {
	becomeAdminPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_ADMIN)
	becomeMemberPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_MEMBER)
	becomeTokenMasterPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER)

	adminOrTokenMasterPermissionsToJoin := append(becomeAdminPermissions, becomeTokenMasterPermissions...)

	allChainIDs, err := p.tokenManager.GetAllChainIDs()
	if err != nil {
		return nil, err
	}

	accountsAndChainIDs := combineAddressesAndChainIDs(addresses, allChainIDs)

	// Check becomeMember and (admin & token master) permissions separately.
	becomeMemberPermissionsResponse, err := p.checkPermissionsOrDefault(becomeMemberPermissions, accountsAndChainIDs)
	if err != nil {
		return nil, err
	}

	if len(adminOrTokenMasterPermissionsToJoin) <= 0 {
		return becomeMemberPermissionsResponse, nil
	}
	// If there are any admin or token master permissions, combine result.
	preParsedPermissions := preParsedCommunityPermissionsData(adminOrTokenMasterPermissionsToJoin)
	var adminOrTokenPermissionsResponse *CheckPermissionsResponse

	if community.IsControlNode() {
		adminOrTokenPermissionsResponse, err = p.CheckPermissions(preParsedPermissions, accountsAndChainIDs, false)
	} else {
		adminOrTokenPermissionsResponse, err = p.CheckCachedPermissions(preParsedPermissions, accountsAndChainIDs, false)
	}
	if err != nil {
		return nil, err
	}

	mergedPermissions := make(map[string]*PermissionTokenCriteriaResult)
	maps.Copy(mergedPermissions, becomeMemberPermissionsResponse.Permissions)
	maps.Copy(mergedPermissions, adminOrTokenPermissionsResponse.Permissions)

	mergedCombinations := p.MergeValidCombinations(becomeMemberPermissionsResponse.ValidCombinations, adminOrTokenPermissionsResponse.ValidCombinations)

	combinedResponse := &CheckPermissionsResponse{
		Satisfied:         becomeMemberPermissionsResponse.Satisfied || adminOrTokenPermissionsResponse.Satisfied,
		Permissions:       mergedPermissions,
		ValidCombinations: mergedCombinations,
	}

	return combinedResponse, nil
}

func (p *DefaultPermissionChecker) checkPermissionsOrDefault(permissions []*CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination) (*CheckPermissionsResponse, error) {
	if len(permissions) == 0 {
		// There are no permissions to join on this community at the moment,
		// so we reveal all accounts + all chain IDs
		response := &CheckPermissionsResponse{
			Satisfied:         true,
			Permissions:       make(map[string]*PermissionTokenCriteriaResult),
			ValidCombinations: accountsAndChainIDs,
		}
		return response, nil
	}

	preParsedPermissions := preParsedCommunityPermissionsData(permissions)
	return p.CheckCachedPermissions(preParsedPermissions, accountsAndChainIDs, false)
}

type ownedERC721TokensGetter = func(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64) (CollectiblesByChain, error)
type balancesByChainGetter = func(ctx context.Context, accounts, tokens []gethcommon.Address, chainIDs []uint64) (BalancesByChain, error)

func (p *DefaultPermissionChecker) checkTokenRequirement(
	tokenRequirement *protobuf.TokenCriteria,
	accounts []gethcommon.Address, ownedERC20TokenBalances BalancesByChain, ownedERC721Tokens CollectiblesByChain,
	accountsChainIDsCombinations map[gethcommon.Address]map[uint64]bool,
) (TokenRequirementResponse, error) {
	tokenRequirementResponse := TokenRequirementResponse{TokenCriteria: tokenRequirement}

	switch tokenRequirement.Type {

	case protobuf.CommunityTokenType_ERC721:

		if len(ownedERC721Tokens) == 0 {
			return tokenRequirementResponse, nil
		}

		// Limit NFTs count to uint32
		requiredCount, err := strconv.ParseUint(tokenRequirement.AmountInWei, 10, 32)
		if err != nil {
			return tokenRequirementResponse, fmt.Errorf("invalid ERC721 amount: %s", tokenRequirement.AmountInWei)
		}
		accumulatedCount := uint64(0)

		for chainID, addressStr := range tokenRequirement.ContractAddresses {
			contractAddress := gethcommon.HexToAddress(addressStr)
			if _, exists := ownedERC721Tokens[chainID]; !exists || len(ownedERC721Tokens[chainID]) == 0 {
				continue
			}

			for account := range ownedERC721Tokens[chainID] {
				if _, exists := ownedERC721Tokens[chainID][account]; !exists {
					continue
				}

				tokenBalances := ownedERC721Tokens[chainID][account][contractAddress]
				accumulatedCount += uint64(len(tokenBalances))

				if len(tokenBalances) > 0 {
					// 'account' owns some TokenID owned from contract 'address'
					if _, exists := accountsChainIDsCombinations[account]; !exists {
						accountsChainIDsCombinations[account] = make(map[uint64]bool)
					}

					// account has balance > 0 on this chain for this token, so let's add it the chain IDs
					accountsChainIDsCombinations[account][chainID] = true

					if len(tokenRequirement.TokenIds) == 0 {
						// no specific tokenId of this collection is needed

						if accumulatedCount >= requiredCount {
							tokenRequirementResponse.Satisfied = true
							return tokenRequirementResponse, nil
						}
					}

					for _, tokenID := range tokenRequirement.TokenIds {
						tokenIDBigInt := new(big.Int).SetUint64(tokenID)

						for _, asset := range tokenBalances {
							if asset.TokenID.Cmp(tokenIDBigInt) == 0 && asset.Balance.Sign() > 0 {
								tokenRequirementResponse.Satisfied = true
								return tokenRequirementResponse, nil
							}
						}
					}
				}
			}
		}

	case protobuf.CommunityTokenType_ERC20:

		if len(ownedERC20TokenBalances) == 0 {
			return tokenRequirementResponse, nil
		}

		accumulatedBalance := new(big.Int)

	chainIDLoopERC20:
		for chainID, address := range tokenRequirement.ContractAddresses {
			if _, exists := ownedERC20TokenBalances[chainID]; !exists || len(ownedERC20TokenBalances[chainID]) == 0 {
				continue chainIDLoopERC20
			}
			contractAddress := gethcommon.HexToAddress(address)
			for account := range ownedERC20TokenBalances[chainID] {
				if _, exists := ownedERC20TokenBalances[chainID][account][contractAddress]; !exists {
					continue
				}

				value := ownedERC20TokenBalances[chainID][account][contractAddress]

				if _, exists := accountsChainIDsCombinations[account]; !exists {
					accountsChainIDsCombinations[account] = make(map[uint64]bool)
				}

				if value.ToInt().Cmp(big.NewInt(0)) > 0 {
					// account has balance > 0 on this chain for this token, so let's add it the chain IDs
					accountsChainIDsCombinations[account][chainID] = true
				}

				// check if adding current chain account balance to accumulated balance
				// satisfies required amount
				prevBalance := accumulatedBalance
				accumulatedBalance.Add(prevBalance, value.ToInt())

				requiredAmount, success := new(big.Int).SetString(tokenRequirement.AmountInWei, 10)
				if !success {
					return tokenRequirementResponse, fmt.Errorf("amountInWeis value is incorrect: %s", tokenRequirement.AmountInWei)
				}

				if accumulatedBalance.Cmp(requiredAmount) != -1 {
					tokenRequirementResponse.Satisfied = true
					return tokenRequirementResponse, nil
				}
			}
		}

	case protobuf.CommunityTokenType_ENS:

		for _, account := range accounts {
			ownedENSNames, err := p.getOwnedENS([]gethcommon.Address{account})
			if err != nil {
				return tokenRequirementResponse, err
			}

			if _, exists := accountsChainIDsCombinations[account]; !exists {
				accountsChainIDsCombinations[account] = make(map[uint64]bool)
			}

			if !strings.HasPrefix(tokenRequirement.EnsPattern, "*.") {
				for _, ownedENS := range ownedENSNames {
					if ownedENS == tokenRequirement.EnsPattern {
						accountsChainIDsCombinations[account][walletcommon.EthereumMainnet] = true
						tokenRequirementResponse.Satisfied = true
						return tokenRequirementResponse, nil
					}
				}
			} else {
				parentName := tokenRequirement.EnsPattern[2:]
				for _, ownedENS := range ownedENSNames {
					if strings.HasSuffix(ownedENS, parentName) {
						accountsChainIDsCombinations[account][walletcommon.EthereumMainnet] = true
						tokenRequirementResponse.Satisfied = true
						return tokenRequirementResponse, nil
					}
				}
			}
		}

	}

	return tokenRequirementResponse, nil
}

func (p *DefaultPermissionChecker) checkPermissions(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool,
	getOwnedERC721Tokens ownedERC721TokensGetter, getBalancesByChain balancesByChainGetter) (*CheckPermissionsResponse, error) {

	response := &CheckPermissionsResponse{
		Satisfied:         false,
		Permissions:       make(map[string]*PermissionTokenCriteriaResult),
		ValidCombinations: make([]*AccountChainIDsCombination, 0),
	}

	if permissionsParsedData == nil {
		response.Satisfied = true
		return response, nil
	}

	erc721TokenRequirements := permissionsParsedData.Erc721TokenRequirements

	erc20ChainIDsMap := permissionsParsedData.Erc20ChainIDsMap
	erc721ChainIDsMap := permissionsParsedData.Erc721ChainIDsMap

	erc20TokenAddresses := permissionsParsedData.Erc20TokenAddresses

	accounts := make([]gethcommon.Address, 0)

	// TODO: move outside in order not to convert it
	for _, accountAndChainIDs := range accountsAndChainIDs {
		accounts = append(accounts, accountAndChainIDs.Address)
	}

	chainIDsForERC20 := calculateChainIDsSet(accountsAndChainIDs, erc20ChainIDsMap)
	chainIDsForERC721 := calculateChainIDsSet(accountsAndChainIDs, erc721ChainIDsMap)

	// if there are no chain IDs that match token criteria chain IDs
	// we aren't able to check balances on selected networks
	if len(erc20ChainIDsMap) > 0 && len(chainIDsForERC20) == 0 {
		response.NetworksNotSupported = true
		return response, nil
	}

	ownedERC20TokenBalances := make(map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big, 0)
	if len(chainIDsForERC20) > 0 {
		// this only returns balances for the networks we're actually interested in
		balances, err := getBalancesByChain(context.Background(), accounts, erc20TokenAddresses, chainIDsForERC20)
		if err != nil {
			return nil, err
		}
		ownedERC20TokenBalances = balances
	}

	ownedERC721Tokens := make(CollectiblesByChain)
	if len(chainIDsForERC721) > 0 {
		collectibles, err := getOwnedERC721Tokens(accounts, erc721TokenRequirements, chainIDsForERC721)
		if err != nil {
			return nil, err
		}
		ownedERC721Tokens = collectibles
	}

	accountsChainIDsCombinations := make(map[gethcommon.Address]map[uint64]bool)

	for _, tokenPermission := range permissionsParsedData.Permissions {
		permissionRequirementsMet := true
		response.Permissions[tokenPermission.Id] = &PermissionTokenCriteriaResult{Role: tokenPermission.Type}

		// There can be multiple token requirements per permission.
		// If only one is not met, the entire permission is marked
		// as not fulfilled
		for _, tokenRequirement := range tokenPermission.TokenCriteria {
			tokenRequirementResponse, err := p.checkTokenRequirement(tokenRequirement, accounts, ownedERC20TokenBalances, ownedERC721Tokens, accountsChainIDsCombinations)
			if err != nil {
				p.logger.Error("failed to check token requirement", zap.Error(err))
			}

			if !tokenRequirementResponse.Satisfied {
				permissionRequirementsMet = false
			}

			response.Permissions[tokenPermission.Id].TokenRequirements = append(response.Permissions[tokenPermission.Id].TokenRequirements, tokenRequirementResponse)
			response.Permissions[tokenPermission.Id].Criteria = append(response.Permissions[tokenPermission.Id].Criteria, tokenRequirementResponse.Satisfied)
		}
		response.Permissions[tokenPermission.Id].ID = tokenPermission.Id

		// multiple permissions are treated as logical OR, meaning
		// if only one of them is fulfilled, the user gets permission
		// to join and we can stop early
		if shortcircuit && permissionRequirementsMet {
			break
		}
	}

	// attach valid account and chainID combinations to response
	for account, chainIDs := range accountsChainIDsCombinations {
		combination := &AccountChainIDsCombination{
			Address: account,
		}
		for chainID := range chainIDs {
			combination.ChainIDs = append(combination.ChainIDs, chainID)
		}
		response.ValidCombinations = append(response.ValidCombinations, combination)
	}

	response.calculateSatisfied()

	return response, nil
}

type balancesByOwnerAndContractAddressGetter = func(ctx context.Context, chainID walletcommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (map[gethcommon.Address][]thirdparty.TokenBalance, error)

func (p *DefaultPermissionChecker) handlePermissionsCheck(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool,
	getBalancesByOwnerAndContractAddress balancesByOwnerAndContractAddressGetter,
	getBalancesByChain balancesByChainGetter) (*CheckPermissionsResponse, error) {

	var getOwnedERC721Tokens ownedERC721TokensGetter = func(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64) (CollectiblesByChain, error) {
		return p.getOwnedERC721Tokens(walletAddresses, tokenRequirements, chainIDs, getBalancesByOwnerAndContractAddress)
	}

	return p.checkPermissions(permissionsParsedData, accountsAndChainIDs, shortcircuit, getOwnedERC721Tokens, getBalancesByChain)
}

func (p *DefaultPermissionChecker) CheckCachedPermissions(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error) {
	return p.handlePermissionsCheck(permissionsParsedData, accountsAndChainIDs, shortcircuit, p.collectiblesManager.FetchCachedBalancesByOwnerAndContractAddress, p.tokenManager.GetCachedBalancesByChain)
}

// CheckPermissions will retrieve balances and check whether the user has
// permission to join the community, if shortcircuit is true, it will stop as soon
// as we know the answer
func (p *DefaultPermissionChecker) CheckPermissions(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error) {
	return p.handlePermissionsCheck(permissionsParsedData, accountsAndChainIDs, shortcircuit, p.collectiblesManager.FetchBalancesByOwnerAndContractAddress, p.tokenManager.GetBalancesByChain)
}

type CollectiblesOwners = map[walletcommon.ChainID]map[gethcommon.Address]*thirdparty.CollectibleContractOwnership

// Same as CheckPermissions but relies on already provided collectibles owners
func (p *DefaultPermissionChecker) CheckPermissionsWithPreFetchedData(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool, collectiblesOwners CollectiblesOwners) (*CheckPermissionsResponse, error) {
	var getCollectiblesBalances collectiblesBalancesGetter = func(ctx context.Context, chainID walletcommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error) {
		ret := make(thirdparty.TokenBalancesPerContractAddress)

		collectiblesByChain, ok := collectiblesOwners[chainID]
		if !ok {
			return nil, errors.New("no data available for chainID")
		}

		for _, contractAddress := range contractAddresses {
			ownership, ok := collectiblesByChain[contractAddress]
			if !ok {
				return nil, errors.New("no data available for collectible")
			}

			for _, nftOwner := range ownership.Owners {
				if nftOwner.OwnerAddress == ownerAddress {
					ret[contractAddress] = nftOwner.TokenBalances
					break
				}
			}
		}

		return ret, nil
	}

	var getOwnedERC721Tokens ownedERC721TokensGetter = func(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64) (CollectiblesByChain, error) {
		return p.getOwnedERC721Tokens(walletAddresses, tokenRequirements, chainIDs, getCollectiblesBalances)
	}

	return p.checkPermissions(permissionsParsedData, accountsAndChainIDs, shortcircuit, getOwnedERC721Tokens, p.tokenManager.GetBalancesByChain)
}

func preParsedPermissionsData(permissions []*CommunityTokenPermission) *PreParsedPermissionsData {
	erc20TokenRequirements, erc721TokenRequirements, _ := ExtractTokenCriteria(permissions)

	erc20ChainIDsMap := make(map[uint64]bool)
	erc721ChainIDsMap := make(map[uint64]bool)

	erc20TokenAddresses := make([]gethcommon.Address, 0)

	// figure out chain IDs we're interested in
	for chainID, tokens := range erc20TokenRequirements {
		erc20ChainIDsMap[chainID] = true
		for contractAddress := range tokens {
			erc20TokenAddresses = append(erc20TokenAddresses, gethcommon.HexToAddress(contractAddress))
		}
	}

	for chainID := range erc721TokenRequirements {
		erc721ChainIDsMap[chainID] = true
	}

	return &PreParsedPermissionsData{
		Erc721TokenRequirements: erc721TokenRequirements,
		Erc20TokenAddresses:     erc20TokenAddresses,
		Erc20ChainIDsMap:        erc20ChainIDsMap,
		Erc721ChainIDsMap:       erc721ChainIDsMap,
	}
}

func preParsedCommunityPermissionsData(permissions []*CommunityTokenPermission) *PreParsedCommunityPermissionsData {
	if len(permissions) == 0 {
		return nil
	}

	return &PreParsedCommunityPermissionsData{
		Permissions:              permissions,
		PreParsedPermissionsData: preParsedPermissionsData(permissions),
	}
}

func PreParsePermissionsData(permissions map[string]*CommunityTokenPermission) (map[protobuf.CommunityTokenPermission_Type]*PreParsedCommunityPermissionsData, map[string]*PreParsedCommunityPermissionsData) {
	becomeMemberPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_BECOME_MEMBER)
	becomeAdminPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_BECOME_ADMIN)
	becomeTokenMasterPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER)

	viewOnlyPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL)
	viewAndPostPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL)
	channelPermissions := append(viewAndPostPermissions, viewOnlyPermissions...)

	communityPermissionsPreParsedData := make(map[protobuf.CommunityTokenPermission_Type]*PreParsedCommunityPermissionsData)
	communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_MEMBER] = preParsedCommunityPermissionsData(becomeMemberPermissions)
	communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_ADMIN] = preParsedCommunityPermissionsData(becomeAdminPermissions)
	communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER] = preParsedCommunityPermissionsData(becomeTokenMasterPermissions)

	channelPermissionsPreParsedData := make(map[string]*PreParsedCommunityPermissionsData)
	for _, channelPermission := range channelPermissions {
		channelPermissionsPreParsedData[channelPermission.Id] = preParsedCommunityPermissionsData([]*CommunityTokenPermission{channelPermission})
	}

	return communityPermissionsPreParsedData, channelPermissionsPreParsedData
}

func CollectibleAddressesFromPreParsedPermissionsData(communityPermissions map[protobuf.CommunityTokenPermission_Type]*PreParsedCommunityPermissionsData, channelPermissions map[string]*PreParsedCommunityPermissionsData) map[walletcommon.ChainID]map[gethcommon.Address]struct{} {
	ret := make(map[walletcommon.ChainID]map[gethcommon.Address]struct{})

	allPermissionsData := []*PreParsedCommunityPermissionsData{}
	for _, permissionsData := range communityPermissions {
		if permissionsData != nil {
			allPermissionsData = append(allPermissionsData, permissionsData)
		}
	}
	for _, permissionsData := range channelPermissions {
		if permissionsData != nil {
			allPermissionsData = append(allPermissionsData, permissionsData)
		}
	}

	for _, data := range allPermissionsData {
		for chainID, contractAddresses := range data.Erc721TokenRequirements {
			if ret[walletcommon.ChainID(chainID)] == nil {
				ret[walletcommon.ChainID(chainID)] = make(map[gethcommon.Address]struct{})
			}

			for contractAddress := range contractAddresses {
				ret[walletcommon.ChainID(chainID)][gethcommon.HexToAddress(contractAddress)] = struct{}{}
			}
		}
	}

	return ret
}