package entityreconcile

import (
	"sort"
	"strings"
)

// ProposalResponse is the private structured response exchanged with the
// reconciliation prompt. It identifies candidates only by opaque keys.
type ProposalResponse struct {
	DuplicateGroups []DuplicateGroup `json:"duplicate_groups"`
}

// DuplicateGroup proposes candidate keys that might denote one entity.
type DuplicateGroup struct {
	Members   []string `json:"members"`
	Canonical string   `json:"canonical"`
}

// Issue identifies one unsafe proposal category without prescribing a warning
// message or retry policy to a consuming normalizer.
type Issue struct {
	GroupIndex int
	Category   string
}

// SafeGroup identifies one validated, non-overlapping duplicate group.
type SafeGroup struct {
	members   []string
	canonical string
}

// Members returns an owned copy of the group's candidate keys.
func (g SafeGroup) Members() []string { return append([]string(nil), g.members...) }

// Canonical returns the selected canonical candidate key.
func (g SafeGroup) Canonical() string { return g.canonical }

// Assessment contains only safe groups. Its accessors return owned copies so
// callers cannot mutate retained assessment data.
type Assessment struct {
	safeGroups      []SafeGroup
	discardedGroups int
	issues          []Issue
}

// SafeGroups returns validated non-overlapping groups in proposal order.
func (a Assessment) SafeGroups() []SafeGroup {
	groups := make([]SafeGroup, len(a.safeGroups))
	for index, group := range a.safeGroups {
		groups[index] = SafeGroup{members: append([]string(nil), group.members...), canonical: group.canonical}
	}
	return groups
}

// DiscardedGroups returns the number of rejected proposal groups.
func (a Assessment) DiscardedGroups() int { return a.discardedGroups }

// Issues returns the deterministic rejection categories in proposal order.
func (a Assessment) Issues() []Issue { return append([]Issue(nil), a.issues...) }

// Assess validates a proposal against the opaque keys created by BuildContext.
func (m Materials) Assess(response ProposalResponse) Assessment {
	all := make(map[string]struct{}, len(m.candidateKeys))
	for _, key := range m.candidateKeys {
		all[key] = struct{}{}
	}
	groups := make([]assessedGroup, len(response.DuplicateGroups))
	issues := make([]Issue, 0)
	for groupIndex, proposal := range response.DuplicateGroups {
		groups[groupIndex] = assessGroup(proposal, all, m.eligible)
		for _, category := range groups[groupIndex].issues {
			issues = append(issues, Issue{GroupIndex: groupIndex, Category: category})
		}
	}

	owners := make(map[string][]int)
	for groupIndex, group := range groups {
		if !group.locallyValid {
			continue
		}
		for _, key := range group.members {
			owners[key] = append(owners[key], groupIndex)
		}
	}
	for groupIndex := range groups {
		if !groups[groupIndex].locallyValid {
			continue
		}
		for _, key := range groups[groupIndex].members {
			if len(owners[key]) > 1 {
				groups[groupIndex].conflicting = true
				issues = append(issues, Issue{GroupIndex: groupIndex, Category: "overlapping_member"})
				break
			}
		}
	}

	assessment := Assessment{issues: issues}
	for _, group := range groups {
		if !group.locallyValid || group.conflicting {
			assessment.discardedGroups++
			continue
		}
		assessment.safeGroups = append(assessment.safeGroups, SafeGroup{members: append([]string(nil), group.members...), canonical: group.canonical})
	}
	return assessment
}

type assessedGroup struct {
	members      []string
	canonical    string
	issues       []string
	locallyValid bool
	conflicting  bool
}

func assessGroup(proposal DuplicateGroup, all, eligible map[string]struct{}) assessedGroup {
	issues := make([]string, 0)
	members := make([]string, 0, len(proposal.Members))
	seen := make(map[string]struct{}, len(proposal.Members))
	for _, key := range proposal.Members {
		if category := keyCategory(key, all, eligible); category != "" {
			issues = append(issues, "member_"+category)
			continue
		}
		if _, exists := seen[key]; exists {
			issues = append(issues, "repeated_member")
			continue
		}
		seen[key] = struct{}{}
		members = append(members, key)
	}
	canonicalCategory := keyCategory(proposal.Canonical, all, eligible)
	if canonicalCategory != "" {
		issues = append(issues, "canonical_"+canonicalCategory)
	}
	if len(members) < 2 {
		issues = append(issues, "fewer_than_two_members")
	}
	if canonicalCategory == "" && !contains(members, proposal.Canonical) {
		issues = append(issues, "canonical_not_member")
	}
	sort.Strings(members)
	return assessedGroup{members: members, canonical: proposal.Canonical, issues: issues, locallyValid: len(issues) == 0}
}

func keyCategory(key string, all, eligible map[string]struct{}) string {
	if strings.TrimSpace(key) == "" {
		return "blank"
	}
	if _, ok := all[key]; !ok {
		return "unknown"
	}
	if _, ok := eligible[key]; !ok {
		return "ineligible"
	}
	return ""
}

func contains(values []string, want string) bool {
	for _, value := range values {
		if value == want {
			return true
		}
	}
	return false
}
