package itemregistry

import (
	"fmt"

	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/identity"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/entityreconcile"
)

type safeReconciliationGroup struct {
	members   []int
	canonical int
}

func reconciliationCandidates(records []normalizedRecord) []entityreconcile.Candidate {
	candidates := make([]entityreconcile.Candidate, len(records))
	for index, record := range records {
		candidates[index] = entityreconcile.Candidate{Name: record.item.Name, SourceRefs: cloneSourceRefs(record.item.SourceRefs)}
	}
	return candidates
}

func reconciliationGroups(assessment entityreconcile.Assessment, candidateKeys []string) []safeReconciliationGroup {
	positions := make(map[string]int, len(candidateKeys))
	for index, key := range candidateKeys {
		positions[key] = index
	}
	safeGroups := assessment.SafeGroups()
	groups := make([]safeReconciliationGroup, 0, len(safeGroups))
	for _, group := range safeGroups {
		members := group.Members()
		memberPositions := make([]int, len(members))
		valid := true
		for index, key := range members {
			position, ok := positions[key]
			if !ok {
				valid = false
				break
			}
			memberPositions[index] = position
		}
		canonical, ok := positions[group.Canonical()]
		if valid && ok {
			groups = append(groups, safeReconciliationGroup{members: memberPositions, canonical: canonical})
		}
	}
	return groups
}

func reconciliationIssues(assessment entityreconcile.Assessment) []string {
	issues := assessment.Issues()
	details := make([]string, len(issues))
	for index, issue := range issues {
		details[index] = fmt.Sprintf("group %d: %s", issue.GroupIndex, issue.Category)
	}
	return details
}

func applySafeGroups(records []normalizedRecord, groups []safeReconciliationGroup, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning, int) {
	byMember := make(map[int]safeReconciliationGroup, len(groups)*2)
	for _, group := range groups {
		for _, member := range group.members {
			byMember[member] = group
		}
	}
	output := make([]normalizedRecord, 0, len(records)-len(groups))
	warnings := make([]contracts.Warning, 0, len(groups))
	rejectedGroups := 0
	for index, record := range records {
		group, grouped := byMember[index]
		if !grouped {
			output = append(output, cloneRecord(record))
			continue
		}
		if group.members[0] != index {
			continue
		}
		if !canConsolidate(records, group) {
			for _, member := range group.members {
				output = append(output, cloneRecord(records[member]))
			}
			warnings = append(warnings, contracts.Warning{Scope: itemScope(records[group.members[0]].earliest), ReasonCode: ReasonCodeItemSemanticProposalInvalid, Message: "proposal group preserved because currency may only be consolidated with aliases of one denomination"})
			rejectedGroups++
			continue
		}
		consolidated := consolidateSemanticGroup(records, group, order)
		output = append(output, consolidated)
		warnings = append(warnings, semanticDuplicateWarning(consolidated, records[group.canonical]))
	}
	return output, warnings, rejectedGroups
}

func canConsolidate(records []normalizedRecord, group safeReconciliationGroup) bool {
	denomination := ""
	hasCurrency := false
	hasNonCurrency := false
	hasConflictingDenominations := false
	for _, member := range group.members {
		current := currencyDenomination(records[member].item.Name)
		if current == "" {
			hasNonCurrency = true
			continue
		}
		hasCurrency = true
		if denomination != "" && denomination != current {
			hasConflictingDenominations = true
			continue
		}
		denomination = current
	}
	return !hasCurrency || (!hasNonCurrency && !hasConflictingDenominations)
}

func currencyDenomination(name string) string {
	switch identity.ComparisonKey(name) {
	case "copper piece", "copper pieces", "cp":
		return "copper"
	case "silver piece", "silver pieces", "sp":
		return "silver"
	case "electrum piece", "electrum pieces", "ep":
		return "electrum"
	case "gold piece", "gold pieces", "gp":
		return "gold"
	case "platinum piece", "platinum pieces", "pp":
		return "platinum"
	default:
		return ""
	}
}

func consolidateSemanticGroup(records []normalizedRecord, group safeReconciliationGroup, order shared.SourceRefOrder) normalizedRecord {
	output := cloneRecord(records[group.members[0]])
	output.item.Name = records[group.canonical].item.Name
	for _, member := range group.members[1:] {
		output.item.SourceRefs = append(output.item.SourceRefs, records[member].item.SourceRefs...)
		output.inputIndexes = append(output.inputIndexes, records[member].inputIndexes...)
		if records[member].earliest < output.earliest {
			output.earliest = records[member].earliest
		}
	}
	output.inputIndexes = sortedUniqueIndexes(output.inputIndexes)
	output.item.SourceRefs = order.Canonicalize(output.item.SourceRefs)
	output.item.ID = identity.DeriveID(output.item.Name)
	return output
}

func semanticDuplicateWarning(record normalizedRecord, canonical normalizedRecord) contracts.Warning {
	details := make([]string, 0, len(record.inputIndexes)+1)
	for _, inputIndex := range record.inputIndexes {
		details = append(details, fmt.Sprintf("input index %d", inputIndex))
	}
	if canonical.earliest != record.earliest {
		details = append(details, fmt.Sprintf("canonical display name from input index %d", canonical.earliest))
	}
	return contracts.Warning{Scope: itemScope(record.earliest), ReasonCode: ReasonCodeDuplicateItemCollapsed, Message: diagnostics.Aggregate("semantic duplicate consolidation", details)}
}
