// NeighborGroups: a lazy query over each source node's typed neighbors,
// grouped by a projected attribute and reduced per source. Nothing runs
// until a terminal (Sizes / TopBySize) is called; reductions run in
// parallel over the sources with projection and counting kept native.

package chickpeas

import (
	
	
	

	
)

// NeighborGroups is built by Snapshot.NeighborGroups; chain Project, then
// call a terminal.
type NeighborGroups struct {
	g       *Snapshot
	sources []NodeID
	m       RelMatch
	dir     Direction
	project []Step
}

// SourceSize pairs a source node with its largest-cohort size.
type SourceSize struct {
	Source NodeID
	Size   uint32
}

// NeighborGroups starts a grouped-neighbor reduction over each source's
// m-matched neighbors in dir.
func ( *Snapshot) ( []NodeID,  RelMatch,  Direction) *NeighborGroups {
	return &NeighborGroups{g: , sources: , m: , dir: }
}

// Project maps each neighbor to its group node via a chain of
// first-neighbor steps (like Follow). Without a projection, neighbors
// group by their own id (every cohort is 1).
func ( *NeighborGroups) ( ...Step) *NeighborGroups {
	.project = 
	return 
}

// Sizes is the raw reduction: per source, the size of its largest cohort.
// Sources with no projectable neighbors yield 0; an unknown rel type or
// projection type yields all-zero sizes. Runs in parallel over the
// sources; output order follows the input sources.
func ( *NeighborGroups) () []SourceSize {
	 := make([]SourceSize, len(.sources))
	// Resolve the projection chain once; an unknown type zeroes everything.
	 := make([]struct {
		 Direction
		   RelMatch
	}, len(.project))
	 := true
	for ,  := range .project {
		,  := .g.RelType(.RelType)
		if ! {
			 = false
			break
		}
		[]. = .Dir
		[]. = MatchType()
	}
	if ! {
		for ,  := range .sources {
			[] = SourceSize{Source: }
		}
		return 
	}
	// One contiguous range per WORKER (not per 4x-oversplit chunk): the
	// per-range scratch is heavy -- a counts map and a neighbors buffer --
	// and both reach a high-water bounded by a single source (counts
	// clears per source, the buffer by max degree), so fewer ranges cost
	// strictly fewer allocations with no growth penalty. Neighbors batch
	// into the reused buffer; the iterator form built a closure per
	// source.
	 := len(.sources)
	 := max(min(, parallel.Workers()), 1)
	 := ( +  - 1) / 
	var  sync.WaitGroup
	.Add()
	for  := range  {
		,  := *, min((+1)*, )
		go func() {
			defer .Done()
			 := map[NodeID]uint32{}
			var  []NodeID
			for  := ;  < ; ++ {
				 := .sources[]
				clear()
				 = .g.AppendNeighborsMatch([:0], , .dir, .m)
				for ,  := range  {
					,  := , true
					for ,  := range  {
						,  := .g.FirstNeighborMatch(, ., .)
						if ! {
							 = false
							break
						}
						 = 
					}
					if  {
						[]++
					}
				}
				 := uint32(0)
				for ,  := range  {
					 = max(, )
				}
				[] = SourceSize{Source: , Size: }
			}
		}()
	}
	.Wait()
	return 
}

// TopBySize is the n sources with the largest cohorts, size descending.
// Ties break by the tieKey node property (read as i64, ascending) when
// non-empty, else by source id ascending -- always deterministic.
func ( *NeighborGroups) ( int,  string) []SourceSize {
	 := .Sizes()
	if  != "" {
		// Resolve each source's tie value once, not inside the comparator.
		 := make([]int64, len())
		for ,  := range  {
			[] = .g.Prop(.Source, ).I64Or(0)
		}
		type  struct {
			   SourceSize
			 int64
		}
		 := make([], len())
		for ,  := range  {
			[] = {: , : []}
		}
		// Generic stable sort: sort.SliceStable's reflection-based
		// swapper dominated hot kernels (typedmemmove per swap).
		slices.SortStableFunc(, func(,  ) int {
			if ..Size != ..Size {
				return cmp.Compare(..Size, ..Size)
			}
			return cmp.Compare(., .)
		})
		 := make([]SourceSize, 0, min(, len()))
		for ,  := range [:min(, len())] {
			 = append(, .)
		}
		return 
	}
	slices.SortStableFunc(, func(,  SourceSize) int {
		if .Size != .Size {
			return cmp.Compare(.Size, .Size)
		}
		return cmp.Compare(.Source, .Source)
	})
	return [:min(, len())]
}