// Fluent parallel grouped reductions over i64 node columns: scan the nodes
// of one or more labels, filter, group by columns / bins / temporal
// components / label membership, and reduce each group to a count and an
// optional sum -- the fast path for analytic rollups. The scan runs in
// parallel via nodeset.ParFold.

package chickpeas

import (
	
	
)

// ErrSchema reports an aggregation over an unknown column/label, a
// mistyped column, or an unsupported option combination.
var ErrSchema = errors.New("schema error")

// AggOp is a comparison operator for Aggregation filters.
type AggOp uint8

const (
	OpLt AggOp = iota
	OpLe
	OpGt
	OpGe
	OpEq
	OpNe
)

// ParseAggOp parses a comparison symbol (<, <=, >, >=, ==, !=).
func ( string) (AggOp, error) {
	switch  {
	case "<":
		return OpLt, nil
	case "<=":
		return OpLe, nil
	case ">":
		return OpGt, nil
	case ">=":
		return OpGe, nil
	case "==":
		return OpEq, nil
	case "!=":
		return OpNe, nil
	}
	return 0, fmt.Errorf("%w: unknown comparison op %q (use <, <=, >, >=, ==, !=)", ErrSchema, )
}

// Test applies the operator.
func ( AggOp) (,  int64) bool {
	switch  {
	case OpLt:
		return  < 
	case OpLe:
		return  <= 
	case OpGt:
		return  > 
	case OpGe:
		return  >= 
	case OpEq:
		return  == 
	}
	return  != 
}

// TemporalUnit is a calendar/clock component of an epoch-millis (UTC) i64,
// for a temporal group dimension -- mirroring openCypher's .year/.month/...
type TemporalUnit uint8

const (
	UnitYear TemporalUnit = iota
	UnitMonth
	UnitDay
	UnitHour
	UnitMinute
	UnitSecond
)

func ( TemporalUnit) () string {
	switch  {
	case UnitYear:
		return "year"
	case UnitMonth:
		return "month"
	case UnitDay:
		return "day"
	case UnitHour:
		return "hour"
	case UnitMinute:
		return "minute"
	}
	return "second"
}

// of extracts the component from an epoch-millis (UTC) value.
func ( TemporalUnit) ( int64) int64 {
	const  = 86_400_000
	 := (( % ) + ) % 
	switch  {
	case UnitHour:
		return  / 3_600_000
	case UnitMinute:
		return ( / 60_000) % 60
	case UnitSecond:
		return ( / 1000) % 60
	}
	 :=  / 
	if % < 0 {
		--
	}
	, ,  := civilFromDays()
	switch  {
	case UnitYear:
		return 
	case UnitMonth:
		return int64()
	}
	return int64()
}

// civilFromDays is civil (year, month, day) from days since 1970-01-01
// (Howard Hinnant's algorithm), matching the Rust aggregate and the query
// engine's temporal path.
func civilFromDays( int64) (int64, uint32, uint32) {
	 += 719_468
	 := 
	if  < 0 {
		 =  - 146_096
	}
	 /= 146_097
	 :=  - *146_097
	 := ( - /1460 + /36524 - /146_096) / 365
	 :=  + *400
	 :=  - (365* + /4 - /100)
	 := (5* + 2) / 153
	 := uint32( - (153*+2)/5 + 1)
	 := uint32( + 3)
	if  >= 10 {
		 = uint32( - 9)
	}
	if  <= 2 {
		++
	}
	return , , 
}

type groupDimKind uint8

const (
	dimCol groupDimKind = iota
	dimBin
	dimComponent
	dimLabel
)

type groupDim struct {
	kind   groupDimKind
	column string
	bounds []int64
	unit   TemporalUnit
}

type projectedFilter struct {
	projection []NodeID
	column     string
	allowed    map[Value]struct{}
}

// Aggregation is a fluent parallel grouped reduction; build with
// Snapshot.Aggregate, chain the steps, then Run.
type Aggregation struct {
	g              *Snapshot
	labels         []string
	filters        []aggFilter
	having         []aggFilter
	byLabel        bool
	group          []groupDim
	sumCol         string
	hasSum         bool
	through        string
	throughDir     Direction
	hasThrough     bool
	hops           []Step
	neighborFilter map[NodeID]struct{}
	projFilters    []projectedFilter
	requirePresent []string
}

type aggFilter struct {
	column string
	op     AggOp
	value  int64
}

// AggRow is one output group: the key values in field order (the source
// label as its index when grouping by label), the row count, and the
// summed value. Sum is nil exactly when the group's true total lies
// outside int64 range (the accumulator is 128-bit, so the verdict depends
// on the total alone, never on how work was partitioned); a query without
// a Sum column reports 0, not nil.
type AggRow struct {
	Key   []int64
	Count uint64
	Sum   *int64
}

// AggResult is the outcome of Run.
type AggResult struct {
	// Total counts rows passing the population (Filter) predicates.
	Total uint64
	// Rows holds one AggRow per group, unordered.
	Rows []AggRow
	// Fields names each key position: "label", a column name, or
	// "{col}_bin" / "{col}_{unit}" / "neighbor" / "endpoint".
	Fields []string
}

// Aggregate starts a parallel grouped reduction over the given labels.
func ( *Snapshot) ( ...string) *Aggregation {
	return &Aggregation{g: , labels: }
}

// Filter adds the population predicate `column op value`; rows passing all
// filters are counted in Total.
func ( *Aggregation) ( string,  AggOp,  int64) *Aggregation {
	.filters = append(.filters, aggFilter{column: , op: , value: })
	return 
}

// FilterVia keeps a source node only when column of its projected node
// (projection[node], e.g. a RootsVia array) is in allowed -- a membership
// test over any value type, applied with the scalar filters.
func ( *Aggregation) ( []NodeID,  string,  ...Value) *Aggregation {
	 := make(map[Value]struct{}, len())
	for ,  := range  {
		[] = struct{}{}
	}
	.projFilters = append(.projFilters, projectedFilter{projection: , column: , allowed: })
	return 
}

// Having adds a predicate applied to grouped rows only (after the
// population filters).
func ( *Aggregation) ( string,  AggOp,  int64) *Aggregation {
	.having = append(.having, aggFilter{column: , op: , value: })
	return 
}

// ByLabel groups by the source node label (key = its index; field "label").
func ( *Aggregation) () *Aggregation {
	.byLabel = true
	return 
}

// By groups by an i64 column's value (field = the column name).
func ( *Aggregation) ( string) *Aggregation {
	.group = append(.group, groupDim{kind: dimCol, column: })
	return 
}

// Bin groups by a column bucketed at ascending bounds (bucket = count of
// bounds <= value; field = "{column}_bin").
func ( *Aggregation) ( string,  ...int64) *Aggregation {
	.group = append(.group, groupDim{kind: dimBin, column: , bounds: })
	return 
}

// TemporalComponent groups by a temporal component of an epoch-millis
// column (field = "{column}_{unit}").
func ( *Aggregation) ( string,  TemporalUnit) *Aggregation {
	.group = append(.group, groupDim{kind: dimComponent, column: , unit: })
	return 
}

// ByLabelMembership groups by membership of one label (key 0/1; field =
// the label name) -- the n:Label predicate as a dimension, distinct from
// ByLabel.
func ( *Aggregation) ( string) *Aggregation {
	.group = append(.group, groupDim{kind: dimLabel, column: })
	return 
}

// RequirePresent keeps only source nodes carrying a value for column (the
// IS NOT NULL predicate), regardless of dtype.
func ( *Aggregation) ( string) *Aggregation {
	.requirePresent = append(.requirePresent, )
	return 
}

// Sum also sums this i64 column per group.
func ( *Aggregation) ( string) *Aggregation {
	.sumCol, .hasSum = , true
	return 
}

// Through counts rels of relType/dir out of each filtered source instead
// of counting nodes, grouping additionally by the neighbor (field
// "neighbor"); Total still counts source nodes.
func ( *Aggregation) ( string,  Direction) *Aggregation {
	.through, .throughDir, .hasThrough = , , true
	return 
}

// OnlyNeighbors restricts Through counting to these neighbors.
func ( *Aggregation) ( ...NodeID) *Aggregation {
	.neighborFilter = make(map[NodeID]struct{}, len())
	for ,  := range  {
		.neighborFilter[] = struct{}{}
	}
	return 
}

// Hop appends a hop to a chained n-hop WALK count: Run then counts the
// walks of the whole hop sequence from each filtered source, one group per
// reachable final endpoint (field "endpoint"). A rel may be reused across
// hops, so this equals a relationship-unique path count only for shapes
// where no rel can repeat -- the caller restricts to such shapes. Mutually
// exclusive with Through.
func ( *Aggregation) ( string,  Direction) *Aggregation {
	.hops = append(.hops, Step{Dir: , RelType: })
	return 
}