package itemregistry

import (
	"fmt"
	"sort"

	"gitea.maximumdirect.net/eric/notarius/internal/framework/semanticreconcile"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
	"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"
)

const incompatibleCurrencyGroup semanticreconcile.RejectionCategory = "incompatible_currency"

func reconciliationInputs(records []normalizedRecord) ([]semanticreconcile.Candidate, []semanticreconcile.Record[dnd.Item], error) {
	candidates := make([]semanticreconcile.Candidate, len(records))
	envelopes := make([]semanticreconcile.Record[dnd.Item], len(records))
	for index, record := range records {
		candidates[index] = semanticreconcile.Candidate{
			Label:      record.item.Name,
			SourceRefs: cloneSourceRefs(record.item.SourceRefs),
		}
		envelope, err := semanticreconcile.NewRecord(record.item, record.inputIndexes, record.earliest, cloneItem)
		if err != nil {
			return nil, nil, fmt.Errorf("record %d: %w", index, err)
		}
		envelopes[index] = envelope
	}
	return candidates, envelopes, nil
}

func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRecord, envelopes []semanticreconcile.Record[dnd.Item], order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding, []diagnostics.Finding, int, error) {
	application, err := semanticreconcile.ApplyPlan(plan, envelopes, semanticreconcile.ApplicationPolicy[dnd.Item]{
		CloneValue: cloneItem,
		RejectGroup: func(members []dnd.Item, _ dnd.Item) semanticreconcile.RejectionCategory {
			if !canConsolidate(members) {
				return incompatibleCurrencyGroup
			}
			return ""
		},
		ConsolidateGroup: func(members []dnd.Item, canonical dnd.Item) (dnd.Item, error) {
			output := cloneItem(canonical)
			output.SourceRefs = nil
			for _, member := range members {
				output.SourceRefs = append(output.SourceRefs, member.SourceRefs...)
			}
			output.SourceRefs = order.Canonicalize(output.SourceRefs)
			output.ID = identity.DeriveID(output.Name)
			return output, nil
		},
	})
	if err != nil {
		return nil, nil, nil, 0, err
	}

	applied := application.Records()
	output := make([]normalizedRecord, len(applied))
	for index, record := range applied {
		output[index] = normalizedRecord{
			item:         record.Value(),
			inputIndexes: record.OriginalInputIndexes(),
			earliest:     record.EarliestInputPosition(),
		}
	}
	type orderedFinding struct {
		position int
		finding  diagnostics.Finding
	}
	observations := make([]orderedFinding, 0, len(application.AppliedGroups()))
	advisories := make([]orderedFinding, 0, len(application.RejectedGroups()))
	for _, event := range application.AppliedGroups() {
		provenance := event.Provenance()
		observations = append(observations, orderedFinding{
			position: provenance.EarliestInputPosition(),
			finding:  semanticDuplicateFinding(provenance, records[provenance.CanonicalPosition()]),
		})
	}
	for _, event := range application.RejectedGroups() {
		provenance := event.Provenance()
		advisories = append(advisories, orderedFinding{
			position: provenance.EarliestInputPosition(),
			finding: diagnostics.Finding{
				Scope:      itemScope(provenance.EarliestInputPosition()),
				ReasonCode: ReasonCodeItemSemanticProposalInvalid,
				Message:    "proposal group preserved because currency may only be consolidated with aliases of one denomination",
			},
		})
	}
	sort.SliceStable(observations, func(left, right int) bool { return observations[left].position < observations[right].position })
	sort.SliceStable(advisories, func(left, right int) bool { return advisories[left].position < advisories[right].position })
	observationFindings := make([]diagnostics.Finding, len(observations))
	for index, entry := range observations {
		observationFindings[index] = entry.finding
	}
	advisoryFindings := make([]diagnostics.Finding, len(advisories))
	for index, entry := range advisories {
		advisoryFindings[index] = entry.finding
	}
	return output, observationFindings, advisoryFindings, len(application.RejectedGroups()), nil
}

func canConsolidate(items []dnd.Item) bool {
	denomination := ""
	hasCurrency := false
	hasNonCurrency := false
	hasConflictingDenominations := false
	for _, item := range items {
		current := currencyDenomination(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 semanticDuplicateFinding(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) diagnostics.Finding {
	inputIndexes := provenance.OriginalInputIndexes()
	details := make([]string, 0, len(inputIndexes)+1)
	for _, inputIndex := range inputIndexes {
		details = append(details, fmt.Sprintf("input index %d", inputIndex))
	}
	if canonical.earliest != provenance.EarliestInputPosition() {
		details = append(details, fmt.Sprintf("canonical display name from input index %d", canonical.earliest))
	}
	return diagnostics.Finding{
		Scope:      itemScope(provenance.EarliestInputPosition()),
		ReasonCode: ReasonCodeDuplicateItemCollapsed,
		Message:    diagnostics.Aggregate("semantic duplicate consolidation", details),
	}
}
