// Aggregation execution: the parallel grouped fold (Run) and the chained
// n-hop walk count (the Hop path).

package chickpeas

import (
	
	
	

	
)

// groupKey is the composite group key: up to four inline dimensions (the
// common case, no per-row allocation), the rest spilled to a string of the
// raw i64 bytes. Comparable, so it keys the group map directly.
type groupKey struct {
	n    uint8
	dims [4]int64
	rest string
}

type keyBuilder struct {
	n    int
	dims [4]int64
	rest []int64
}

func ( *keyBuilder) ( int64) {
	if .n < 4 {
		.dims[.n] = 
	} else {
		.rest = append(.rest, )
	}
	.n++
}

func ( *keyBuilder) () groupKey {
	 := groupKey{n: uint8(min(.n, 255)), dims: .dims}
	if len(.rest) > 0 {
		 := make([]byte, 0, len(.rest)*8)
		for ,  := range .rest {
			for  := 0;  < 64;  += 8 {
				 = append(, byte(uint64()>>))
			}
		}
		.rest = string()
	}
	return 
}

func ( groupKey) () []int64 {
	 := make([]int64, 0, int(.n))
	for  := 0;  < int(.n) &&  < 4; ++ {
		 = append(, .dims[])
	}
	for  := 0; +8 <= len(.rest);  += 8 {
		var  uint64
		for  := 7;  >= 0; -- {
			 = <<8 | uint64(.rest[+])
		}
		 = append(, int64())
	}
	return 
}

type resolvedGroup struct {
	kind   groupDimKind
	col    I64Col
	bounds []int64
	unit   TemporalUnit
	label  *nodeset.Set
}

type resolvedFilter struct {
	col   I64Col
	op    AggOp
	value int64
}

// colI64 resolves key to an i64 column reader (dense or sparse); errors
// when the column is absent or not i64 dtype.
func ( *Snapshot) ( string) (I64Col, error) {
	,  := .Col()
	if ! {
		return I64Col{}, fmt.Errorf("%w: column %q not found", ErrSchema, )
	}
	if .Dtype() != DtypeI64 {
		return I64Col{}, fmt.Errorf("%w: column %q is not an i64 column", ErrSchema, )
	}
	return .I64(), nil
}

func ( *Aggregation) ( []aggFilter) ([]resolvedFilter, error) {
	 := make([]resolvedFilter, 0, len())
	for ,  := range  {
		,  := .g.colI64(.column)
		if  != nil {
			return nil, 
		}
		 = append(, resolvedFilter{col: , op: .op, value: .value})
	}
	return , nil
}

type resolvedProjFilter struct {
	projection []NodeID
	column     Column
	allowed    map[Value]struct{}
}

func ( *Aggregation) () ([]resolvedProjFilter, error) {
	 := make([]resolvedProjFilter, 0, len(.projFilters))
	for ,  := range .projFilters {
		,  := .g.PropertyKey(.column)
		if ! {
			return nil, fmt.Errorf("%w: column %q not found", ErrSchema, .column)
		}
		,  := .g.columns[]
		if ! {
			return nil, fmt.Errorf("%w: column %q not found", ErrSchema, .column)
		}
		 = append(, resolvedProjFilter{projection: .projection, column: , allowed: .allowed})
	}
	return , nil
}

func passes( NodeID,  []resolvedFilter) bool {
	for ,  := range  {
		,  := .col.Get()
		if ! || !.op.Test(, .value) {
			return false
		}
	}
	return true
}

// Run executes the reduction in parallel and collects the groups.
func ( *Aggregation) () (*AggResult, error) {
	if len(.hops) > 0 {
		if  := .checkHopsSupported();  != nil {
			return nil, 
		}
		return .runHops()
	}
	 := .g
	,  := .resolveFilters(.filters)
	if  != nil {
		return nil, 
	}
	,  := .resolveFilters(.having)
	if  != nil {
		return nil, 
	}
	,  := .resolveProjFilters()
	if  != nil {
		return nil, 
	}
	 := make([]resolvedGroup, 0, len(.group))
	for ,  := range .group {
		switch .kind {
		case dimLabel:
			,  := .NodesWithLabel(.column)
			if ! {
				return nil, fmt.Errorf("%w: label %q not found", ErrSchema, .column)
			}
			 = append(, resolvedGroup{kind: dimLabel, label: })
		default:
			,  := .colI64(.column)
			if  != nil {
				return nil, 
			}
			 = append(, resolvedGroup{kind: .kind, col: , bounds: .bounds, unit: .unit})
		}
	}
	 := make([]Column, 0, len(.requirePresent))
	for ,  := range .requirePresent {
		,  := .PropertyKey()
		if ! {
			return nil, fmt.Errorf("%w: column %q not found", ErrSchema, )
		}
		,  := .columns[]
		if ! {
			return nil, fmt.Errorf("%w: column %q not found", ErrSchema, )
		}
		 = append(, )
	}
	var  I64Col
	if .hasSum {
		if ,  = .colI64(.sumCol);  != nil {
			return nil, 
		}
	}
	 := make([]*nodeset.Set, 0, len(.labels))
	for ,  := range .labels {
		,  := .NodesWithLabel()
		if ! {
			return nil, fmt.Errorf("%w: label %q not found", ErrSchema, )
		}
		 = append(, )
	}
	var  RelMatch
	if .hasThrough {
		 = .Match(.through)
	}

	type  struct {
		 uint64
		   acc128
	}
	type  struct {
		 map[groupKey]*
		  uint64
		// nbScratch collects one source's matched neighbors for the
		// through+sum dedup (sorted + compacted per node, reused).
		 []NodeID
	}
	var  atomic.Bool
	 := map[groupKey]*{}
	var  uint64

	for ,  := range  {
		 := nodeset.ParFold(,
			func() * { return &{: map[groupKey]*{}} },
			func( *,  NodeID) * {
				// A filter value that is absent excludes the node -- a Null
				// never satisfies a comparison.
				if !passes(, ) {
					return 
				}
				for ,  := range  {
					if ,  := .Get(); ! {
						return 
					}
				}
				for ,  := range  {
					if int() >= len(.projection) {
						return 
					}
					,  := .column.Get(.projection[])
					if ! {
						return 
					}
					if ,  := .allowed[]; ! {
						return 
					}
				}
				.++
				if !passes(, ) {
					return 
				}
				var  keyBuilder
				if .byLabel {
					.push(int64())
				}
				for ,  := range  {
					switch .kind {
					case dimCol:
						,  := .col.Get()
						if ! {
							// An absent plain group value can't be keyed as
							// i64 -- surface it as an error after the fold.
							.Store(true)
							return 
						}
						.push()
					case dimBin:
						// A bin over an absent value is the ELSE bucket.
						 := int64(len(.bounds))
						if ,  := .col.Get();  {
							 = 0
							for ,  := range .bounds {
								if  >=  {
									++
								}
							}
						}
						.push()
					case dimComponent:
						,  := .col.Get()
						if ! {
							.Store(true)
							return 
						}
						.push(.unit.of())
					case dimLabel:
						if .label.Contains() {
							.push(1)
						} else {
							.push(0)
						}
					}
				}
				 := func( groupKey) {
					,  := .[]
					if ! {
						 = &{}
						.[] = 
					}
					.++
					if .hasSum {
						if ,  := .Get();  {
							..add()
						}
					}
				}
				if .hasThrough {
					// Count and sum answer different questions on the
					// through path (rustychickpeas 240): count stays per
					// RELATIONSHIP, while a source's sum value contributes
					// once per DISTINCT neighbor -- deduped across parallel
					// same-type rels, so the Both-direction / undirected-
					// stored-both-ways case no longer inflates sums by the
					// source's degree. RelsMatch keeps per-rel multiplicity
					// independent of the neighbor-id surfaces' set contract.
					 := func( NodeID) groupKey {
						 := 
						.rest = append([]int64(nil), .rest...)
						.push(int64())
						return .key()
					}
					. = .[:0]
					for  := range .RelsMatch(, .throughDir, ) {
						 := .Neighbor
						if .neighborFilter != nil {
							if ,  := .neighborFilter[]; ! {
								continue
							}
						}
						 := ()
						,  := .[]
						if ! {
							 = &{}
							.[] = 
						}
						.++
						. = append(., )
					}
					if .hasSum && len(.) > 0 {
						if ,  := .Get();  {
							slices.Sort(.)
							. = slices.Compact(.)
							for ,  := range . {
								.[()]..add()
							}
						}
					}
				} else {
					(.key())
				}
				return 
			},
			func(,  *) * {
				for ,  := range . {
					if ,  := .[];  {
						. += .
						..merge(.)
					} else {
						.[] = 
					}
				}
				. += .
				return 
			})
		for ,  := range . {
			if ,  := [];  {
				. += .
				..merge(.)
			} else {
				[] = 
			}
		}
		 += .
	}
	if .Load() {
		return nil, fmt.Errorf("%w: group column has absent values; use a generic aggregation path", ErrSchema)
	}

	 := make([]AggRow, 0, len())
	for ,  := range  {
		 := AggRow{Key: .toSlice(), Count: .}
		if ,  := ..int64();  {
			.Sum = &
		}
		 = append(, )
	}
	 := make([]string, 0, len(.group)+2)
	if .byLabel {
		 = append(, "label")
	}
	for ,  := range .group {
		switch .kind {
		case dimCol:
			 = append(, .column)
		case dimBin:
			 = append(, .column+"_bin")
		case dimComponent:
			 = append(, .column+"_"+.unit.suffix())
		case dimLabel:
			 = append(, .column)
		}
	}
	if .hasThrough {
		 = append(, "neighbor")
	}
	return &AggResult{Total: , Rows: , Fields: }, nil
}