package shared

import (
	"sort"

	"gitea.maximumdirect.net/eric/notarius/internal/core/source"
)

// SourceRefOrder provides a stable snapshot of a source document's unit
// ordering for source-reference comparison and canonicalization.
type SourceRefOrder struct {
	sourceID string
	index    source.DocumentIndex
}

// NewSourceRefOrder captures the source identity and unit positions from doc.
func NewSourceRefOrder(doc *source.SourceDocument) SourceRefOrder {
	return NewSourceRefOrderFromIndex(source.NewDocumentIndex(doc))
}

// NewSourceRefOrderFromIndex captures source identity and unit positions from
// one document index for source-reference comparison and canonicalization.
func NewSourceRefOrderFromIndex(index source.DocumentIndex) SourceRefOrder {
	sourceID, ok := index.DocumentID()
	if !ok {
		return SourceRefOrder{}
	}
	return SourceRefOrder{sourceID: sourceID, index: index}
}

// Less orders references by source identity, then document positions when
// available, and finally literal endpoint IDs.
func (o SourceRefOrder) Less(left, right source.SourceRef) bool {
	if left.SourceID != right.SourceID {
		return left.SourceID < right.SourceID
	}
	if o.lessEndpoint(left.SourceID, left.StartUnitID, right.StartUnitID) {
		return true
	}
	if o.lessEndpoint(left.SourceID, right.StartUnitID, left.StartUnitID) {
		return false
	}
	return o.lessEndpoint(left.SourceID, left.EndUnitID, right.EndUnitID)
}

// EarliestValid returns the earliest document position among valid refs.
func (o SourceRefOrder) EarliestValid(refs []source.SourceRef) (int, bool) {
	if len(refs) == 0 || o.sourceID == "" {
		return 0, false
	}
	found := false
	earliest := 0
	for _, ref := range refs {
		if o.index.ValidateRef(ref) != nil {
			continue
		}
		start, _ := o.index.Position(ref.StartUnitID)
		if !found || start < earliest {
			earliest = start
			found = true
		}
	}
	return earliest, found
}

// OrderEndpoints returns an owned copy of refs with each resolvable range
// ordered by document position. References outside the indexed source, or
// whose endpoints cannot both be resolved, remain unchanged so validators can
// diagnose them.
func (o SourceRefOrder) OrderEndpoints(refs []source.SourceRef) []source.SourceRef {
	if refs == nil {
		return nil
	}
	ordered := append([]source.SourceRef{}, refs...)
	for index := range ordered {
		ref := &ordered[index]
		if ref.SourceID != o.sourceID || ref.StartUnitID <= 0 || ref.EndUnitID <= 0 {
			continue
		}
		start, startOK := o.index.Position(ref.StartUnitID)
		end, endOK := o.index.Position(ref.EndUnitID)
		if startOK && endOK && start > end {
			ref.StartUnitID, ref.EndUnitID = ref.EndUnitID, ref.StartUnitID
		}
	}
	return ordered
}

// Canonicalize returns an owned, stable-sorted, exactly de-duplicated copy of
// refs. It deliberately preserves invalid references for diagnostics.
func (o SourceRefOrder) Canonicalize(refs []source.SourceRef) []source.SourceRef {
	if refs == nil {
		return nil
	}
	canonical := append([]source.SourceRef{}, refs...)
	sort.SliceStable(canonical, func(left, right int) bool {
		return o.Less(canonical[left], canonical[right])
	})
	unique := make([]source.SourceRef, 0, len(canonical))
	for _, ref := range canonical {
		if len(unique) == 0 || unique[len(unique)-1] != ref {
			unique = append(unique, ref)
		}
	}
	return unique
}

func (o SourceRefOrder) lessEndpoint(sourceID string, left, right int) bool {
	leftPosition, leftOK := o.position(sourceID, left)
	rightPosition, rightOK := o.position(sourceID, right)
	if leftOK != rightOK {
		return leftOK
	}
	if leftOK && leftPosition != rightPosition {
		return leftPosition < rightPosition
	}
	return left < right
}

func (o SourceRefOrder) position(sourceID string, unitID int) (int, bool) {
	if o.sourceID == "" || sourceID != o.sourceID {
		return 0, false
	}
	return o.index.Position(unitID)
}
