// SPDX-License-Identifier: GPL-3.0-or-later

package chartengine

import (
	"fmt"
	"maps"
	"math"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/netdata/netdata/go/plugins/pkg/metrix"
	"github.com/netdata/netdata/go/plugins/plugin/framework/chartengine/internal/program"
)

type labelSliceView struct {
	items []metrix.Label
}

func (v labelSliceView) Get(key string) (string, bool) {
	i := sort.Search(len(v.items), func(i int) bool { return v.items[i].Key >= key })
	if i < len(v.items) && v.items[i].Key == key {
		return v.items[i].Value, true
	}
	return "", false
}

func (v labelSliceView) Range(fn func(key, value string) bool) {
	for _, item := range v.items {
		if !fn(item.Key, item.Value) {
			return
		}
	}
}

func (v labelSliceView) Len() int {
	return len(v.items)
}

func (v labelSliceView) CloneMap() map[string]string {
	out := make(map[string]string, len(v.items))
	for _, item := range v.items {
		out[item.Key] = item.Value
	}
	return out
}

// Plan is the deterministic planner output consumed by chartemit. Actions and
// their referenced maps and slices are immutable snapshots; consumers must not
// mutate them.
//
// Current scope:
//   - create/update/remove actions for chart and dimension lifecycle,
//   - inferred dynamic dimension names resolved from flattened metric metadata.
type Plan struct {
	Actions            []EngineAction
	InferredDimensions []InferredDimension
}

// InferredDimension is one resolved dynamic dimension name from planner input.
type InferredDimension struct {
	ChartTemplateID string
	DimensionIndex  int
	Name            string
}

type dimensionAlgorithm uint8

const (
	dimensionAlgorithmAbsolute dimensionAlgorithm = iota
	dimensionAlgorithmIncremental
)

func compactDimensionAlgorithm(algorithm program.Algorithm) dimensionAlgorithm {
	if algorithm == program.AlgorithmIncremental {
		return dimensionAlgorithmIncremental
	}
	return dimensionAlgorithmAbsolute
}

func (a dimensionAlgorithm) programAlgorithm() program.Algorithm {
	if a == dimensionAlgorithmIncremental {
		return program.AlgorithmIncremental
	}
	return program.AlgorithmAbsolute
}

type dimensionState struct {
	sortKey     dimensionSortKey
	order       int
	multiplier  int
	divisor     int
	algorithm   dimensionAlgorithm
	hidden      bool
	float       bool
	static      bool
	aggregation program.Aggregation
}

type dimensionSortKind uint8

const (
	dimensionSortDefault dimensionSortKind = iota
	dimensionSortHistogramBucket
)

type dimensionSortKey struct {
	kind       dimensionSortKind
	upperBound float64
}

type dimBuildEntry struct {
	seenSeq      uint64
	observations uint64
	value        metrix.SampleValue
	dimensionState
}

func (e *dimBuildEntry) aggregate(value metrix.SampleValue) {
	if e.aggregation == program.AggregationSum {
		e.value += value
		return
	}
	e.aggregateNonSum(value)
}

func (e *dimBuildEntry) aggregateNonSum(value metrix.SampleValue) {
	switch e.aggregation {
	case program.AggregationMin:
		if math.IsNaN(e.value) || (!math.IsNaN(value) && value < e.value) {
			e.value = value
		}
	case program.AggregationMax:
		if math.IsNaN(e.value) || (!math.IsNaN(value) && value > e.value) {
			e.value = value
		}
	case program.AggregationAvg:
		e.observations++
		e.value = runningAverage(e.value, value, e.observations)
	}
}

func runningAverage(current, value metrix.SampleValue, observations uint64) metrix.SampleValue {
	if math.IsNaN(current) || math.IsNaN(value) {
		return math.NaN()
	}
	if math.IsInf(current, 0) {
		if math.IsInf(value, 0) && math.Signbit(current) != math.Signbit(value) {
			return math.NaN()
		}
		return current
	}
	if math.IsInf(value, 0) {
		return value
	}

	n := float64(observations)
	delta := value - current
	if !math.IsInf(delta, 0) {
		return math.FMA(delta, 1/n, current)
	}

	return current*((n-1)/n) + value/n
}

type chartState struct {
	templateID      string
	chartID         string
	meta            program.ChartMeta
	lifecycle       program.LifecyclePolicy
	labelTracker    chartLabelTracker
	entries         map[string]*dimBuildEntry
	observedCount   int
	currentBuildSeq uint64
}

type planBuildContext struct {
	out               *Plan
	reader            metrix.Reader
	collectMeta       metrix.CollectMeta
	buildCycle        uint64
	prog              *program.Program
	cache             *routeCache
	routeCacheEnabled bool
	routeObserver     func(PlanRouteDiagnostic)
	index             matchIndex
	flat              metrix.Reader

	seenInfer        map[string]struct{}
	chartsByID       map[string]*chartState
	chartOwners      map[string]string
	dimCapHints      map[string]int
	materialized     *materializedState
	materializedByID map[string]*materializedChartState

	planRouteStats
}

type flattenedReadChecker interface {
	FlattenedRead() bool
}

func isHistogramBucketSeries(meta metrix.SeriesMeta) bool {
	return meta.SourceKind == metrix.MetricKindHistogram &&
		meta.FlattenRole == metrix.FlattenRoleHistogramBucket
}

func (e *Engine) preparePlan(reader metrix.Reader) (Plan, materializedState, uint64, uint64, uint64, bool, error) {
	if e == nil {
		return Plan{}, materializedState{}, 0, 0, 0, false, fmt.Errorf("chartengine: nil engine")
	}
	if reader == nil {
		return Plan{}, materializedState{}, 0, 0, 0, false, fmt.Errorf("chartengine: nil metrics reader")
	}
	out := Plan{
		Actions:            make([]EngineAction, 0),
		InferredDimensions: make([]InferredDimension, 0),
	}
	collectMeta := reader.CollectMeta()

	e.mu.Lock()
	defer e.mu.Unlock()
	if e.state.outstanding != 0 {
		return Plan{}, materializedState{}, 0, 0, 0, false, ErrOutstandingPlanAttempt
	}
	sample := PlanRuntimeSample{startedAt: time.Now()}
	defer func() { e.observeBuildSample(sample) }()
	// Failed attempt must not trigger lifecycle transitions.
	if collectMeta.LastAttemptStatus != metrix.CollectStatusSuccess {
		sample.skippedFailed = true
		e.logDebugf("chartengine build skipped: collect status=%d", collectMeta.LastAttemptStatus)
		return out, materializedState{}, 0, 0, 0, false, nil
	}

	obs := e.observeBuildSuccessSeq(collectMeta.LastSuccessSeq)
	buildCycle := e.nextBuildCycle(collectMeta.LastSuccessSeq)
	sample.buildSeqViolation = e.state.buildSeq.violating
	sample.buildSeqObserved = true
	switch obs.transition {
	case buildSeqTransitionBroken:
		sample.buildSeqBroken = true
		e.logWarningf(
			"chartengine build sequence is non-monotonic: current=%d previous=%d (suppressing repeats until recovery)",
			collectMeta.LastSuccessSeq,
			obs.previous,
		)
	case buildSeqTransitionRecovered:
		sample.buildSeqRecovered = true
		e.logInfof(
			"chartengine build sequence monotonicity recovered: current=%d previous=%d",
			collectMeta.LastSuccessSeq,
			obs.previous,
		)
	}

	phaseStartedAt := time.Now()
	staged := e.state.materialized.clone()
	ctx, err := e.preparePlanBuildContext(reader, &out, collectMeta, buildCycle, &staged)
	sample.phasePrepareSeconds = time.Since(phaseStartedAt).Seconds()
	if err != nil {
		sample.buildErr = true
		e.logWarningf("chartengine build prepare failed: %v", err)
		return Plan{}, materializedState{}, 0, 0, 0, false, err
	}
	phaseStartedAt = time.Now()
	if err := validateBuildReaderForInferredDimensions(ctx.index, reader); err != nil {
		sample.phaseValidateSeconds = time.Since(phaseStartedAt).Seconds()
		sample.buildErr = true
		e.logWarningf("chartengine build reader validation failed: %v", err)
		return Plan{}, materializedState{}, 0, 0, 0, false, err
	}
	sample.phaseValidateSeconds = time.Since(phaseStartedAt).Seconds()
	phaseStartedAt = time.Now()
	if err := e.scanPlanSeries(ctx); err != nil {
		sample.phaseScanSeconds = time.Since(phaseStartedAt).Seconds()
		sample.buildErr = true
		e.logWarningf("chartengine build scan failed: %v", err)
		return Plan{}, materializedState{}, 0, 0, 0, false, err
	}
	sample.phaseScanSeconds = time.Since(phaseStartedAt).Seconds()

	// Route-cache lifecycle follows metrix snapshot membership. Diagnostic plans
	// bypass it completely so repeated attempts retain complete route facts.
	if ctx.routeCacheEnabled {
		phaseStartedAt = time.Now()
		retainStats := ctx.cache.RetainSeen(ctx.collectMeta.LastSuccessSeq)
		sample.phaseRetainSeconds = time.Since(phaseStartedAt).Seconds()
		sample.routeCacheEntries = retainStats.EntriesAfter
		sample.routeCacheRetained = retainStats.EntriesAfter
		sample.routeCachePruned = retainStats.Pruned
		sample.routeCacheFullDrop = retainStats.FullDrop
	}

	phaseStartedAt = time.Now()
	removeByCapDims, removeByCapCharts := enforceLifecycleCapsWithObserver(
		ctx.collectMeta.LastSuccessSeq,
		ctx.chartsByID,
		ctx.materialized,
		ctx.routeObserver,
	)
	sample.phaseLifecycleCapsSec = time.Since(phaseStartedAt).Seconds()
	sample.lifecycleRemovedDimensionByCap = len(removeByCapDims)
	sample.lifecycleRemovedChartByCap = len(removeByCapCharts)
	for _, action := range removeByCapDims {
		out.Actions = append(out.Actions, action)
	}
	for _, action := range removeByCapCharts {
		out.Actions = append(out.Actions, action)
	}
	phaseStartedAt = time.Now()
	if err := e.materializePlanCharts(ctx); err != nil {
		sample.phaseMaterializeSeconds = time.Since(phaseStartedAt).Seconds()
		sample.buildErr = true
		e.logWarningf("chartengine build materialization failed: %v", err)
		return Plan{}, materializedState{}, 0, 0, 0, false, err
	}
	sample.phaseMaterializeSeconds = time.Since(phaseStartedAt).Seconds()
	phaseStartedAt = time.Now()
	removeDims, removeCharts := collectExpiryRemovals(ctx.collectMeta.LastSuccessSeq, ctx.materialized)
	sample.phaseExpirySeconds = time.Since(phaseStartedAt).Seconds()
	sample.lifecycleRemovedDimensionByExpiry = len(removeDims)
	sample.lifecycleRemovedChartByExpiry = len(removeCharts)
	for _, action := range removeDims {
		out.Actions = append(out.Actions, action)
	}
	for _, action := range removeCharts {
		out.Actions = append(out.Actions, action)
	}
	phaseStartedAt = time.Now()
	sortInferredDimensions(out.InferredDimensions)
	sample.phaseSortSeconds = time.Since(phaseStartedAt).Seconds()

	sample.planRouteStats = ctx.planRouteStats
	sample.planChartInstances = len(ctx.chartsByID)
	sample.planInferredDimensions = len(out.InferredDimensions)

	actionCounts := actionKindCounts(out.Actions)
	sample.actionCreateChart = actionCounts.actionCreateChart
	sample.actionCreateDimension = actionCounts.actionCreateDimension
	sample.actionUpdateChartLabels = actionCounts.actionUpdateChartLabels
	sample.actionUpdateChart = actionCounts.actionUpdateChart
	sample.actionRemoveDimension = actionCounts.actionRemoveDimension
	sample.actionRemoveChart = actionCounts.actionRemoveChart
	sample.buildSuccess = true
	e.state.hints.chartsByID = len(ctx.chartsByID)
	e.state.hints.seenInfer = len(ctx.seenInfer)

	attemptID := e.nextAttemptIDLocked()
	e.state.outstanding = attemptID
	return out, staged, e.state.engineEpoch, e.state.commitSeq, attemptID, true, nil
}

func validateBuildReaderForInferredDimensions(index matchIndex, reader metrix.Reader) error {
	templateID, dimIndex, requiresFlatten := firstInferDimension(index)
	if !requiresFlatten {
		return nil
	}
	aware, ok := reader.(flattenedReadChecker)
	if ok && aware.FlattenedRead() {
		return nil
	}
	return fmt.Errorf(
		"chartengine: inferred dimension requires flattened reader metadata (template_id=%q dim_index=%d); use store.Read(metrix.ReadFlatten())",
		templateID,
		dimIndex,
	)
}

func firstInferDimension(index matchIndex) (string, int, bool) {
	if len(index.chartsByID) == 0 {
		return "", 0, false
	}
	templateIDs := make([]string, 0, len(index.chartsByID))
	for templateID := range index.chartsByID {
		templateIDs = append(templateIDs, templateID)
	}
	sort.Strings(templateIDs)
	for _, templateID := range templateIDs {
		chart := index.chartsByID[templateID]
		for i := range chart.Dimensions {
			if chart.Dimensions[i].InferNameFromSeriesMeta {
				return templateID, i, true
			}
		}
	}
	return "", 0, false
}

func (e *Engine) preparePlanBuildContext(
	reader metrix.Reader,
	out *Plan,
	collectMeta metrix.CollectMeta,
	buildCycle uint64,
	materialized *materializedState,
) (*planBuildContext, error) {
	prog := e.state.program
	if prog == nil {
		return nil, fmt.Errorf("chartengine: no compiled program loaded")
	}
	cache := e.state.routeCache
	if cache == nil {
		cache = newRouteCache()
		e.state.routeCache = cache
	}
	if materialized == nil {
		return nil, fmt.Errorf("chartengine: nil materialized state")
	}
	if materialized.charts == nil {
		*materialized = newMaterializedState()
	}
	index := e.state.matchIndex
	if index.chartsByID == nil {
		index = buildMatchIndex(prog.Charts())
		e.state.matchIndex = index
	}
	chartOwners := make(map[string]string, len(materialized.charts))
	dimCapHints := make(map[string]int, len(materialized.charts))
	for chartID, matChart := range materialized.charts {
		chartOwners[chartID] = matChart.templateID
		if n := len(matChart.dimensions); n > 0 {
			dimCapHints[chartID] = n
		}
	}
	chartsCap := max(e.state.hints.chartsByID, len(materialized.charts))
	seenInferCap := e.state.hints.seenInfer
	return &planBuildContext{
		out:               out,
		reader:            reader,
		collectMeta:       collectMeta,
		buildCycle:        buildCycle,
		prog:              prog,
		cache:             cache,
		routeCacheEnabled: e.state.cfg.routeObserver == nil,
		routeObserver:     e.state.cfg.routeObserver,
		index:             index,
		flat:              reader,
		seenInfer:         make(map[string]struct{}, seenInferCap),
		chartsByID:        make(map[string]*chartState, chartsCap),
		chartOwners:       chartOwners,
		dimCapHints:       dimCapHints,
		materialized:      materialized,
		materializedByID:  materialized.charts,
	}, nil
}

func (e *Engine) scanPlanSeries(ctx *planBuildContext) error {
	return e.forEachPlanSeriesRoute(ctx, false)
}

func (e *Engine) forEachPlanSeriesRoute(ctx *planBuildContext, replayLabels bool) error {
	var firstErr error
	buildSeq := ctx.collectMeta.LastSuccessSeq
	trackStats := !replayLabels
	process := func(
		identity metrix.SeriesIdentity,
		meta metrix.SeriesMeta,
		name string,
		labels metrix.LabelView,
		v metrix.SampleValue,
	) {
		if trackStats {
			ctx.seriesScanned++
		}
		if firstErr != nil {
			return
		}
		if e.state.cfg.seriesSelection == seriesSelectionLastSuccessOnly &&
			meta.LastSeenSuccessSeq != ctx.collectMeta.LastSuccessSeq {
			if trackStats {
				ctx.seriesFilteredBySeq++
				if ctx.routeCacheEnabled {
					ctx.cache.MarkSeenIfPresent(identity, buildSeq)
				}
				if ctx.routeObserver != nil {
					ctx.observeRouteDiagnostic(PlanRouteDiagnostic{
						Decision:       PlanRouteSeriesFilteredBySequence,
						SeriesIdentity: identity,
						MetricName:     name,
					})
				}
			}
			return
		}
		if selector := e.state.cfg.selector; selector != nil && !selector.Matches(name, labels) {
			if trackStats {
				ctx.seriesFilteredBySel++
				if ctx.routeCacheEnabled {
					ctx.cache.MarkSeenIfPresent(identity, buildSeq)
				}
				if ctx.routeObserver != nil {
					ctx.observeRouteDiagnostic(PlanRouteDiagnostic{
						Decision:       PlanRouteSeriesFilteredBySelector,
						SeriesIdentity: identity,
						MetricName:     name,
					})
				}
			}
			return
		}

		observer := ctx.routeObserver
		if !trackStats {
			observer = nil
		}
		routes, hit, err := e.resolveSeriesRoutes(
			ctx.cache,
			ctx.routeCacheEnabled,
			observer,
			identity,
			name,
			labels,
			meta,
			ctx.reader,
			ctx.index,
			ctx.prog.Revision(),
			buildSeq,
		)
		if err != nil {
			firstErr = err
			return
		}
		if trackStats && ctx.routeCacheEnabled {
			if hit {
				e.addRouteCacheHit()
				ctx.routeCacheHits++
			} else {
				e.addRouteCacheMiss()
				ctx.routeCacheMisses++
			}
		}
		if len(routes) == 0 {
			autoRoutes, ok, reason, ruleIndex, err := e.resolveAutogenRouteWithReason(ctx.reader, name, labels, meta)
			if err != nil {
				firstErr = err
				return
			}
			if ok {
				routes = autoRoutes
				if trackStats {
					ctx.seriesAutogenMatched++
					ctx.seriesMatched++
				}
			} else {
				if trackStats {
					ctx.seriesUnmatched++
					if ctx.routeObserver != nil {
						ruleScope := ""
						if ruleIndex >= 0 && ruleIndex < len(e.state.cfg.autogen.Rules) {
							ruleScope = e.state.cfg.autogen.Rules[ruleIndex].Scope
						}
						ctx.observeRouteDiagnostic(PlanRouteDiagnostic{
							Decision:         PlanRouteUnmatched,
							Reason:           reason,
							SeriesIdentity:   identity,
							MetricName:       name,
							MetricFamilyName: diagnosticMetricFamilyName(name, labels, meta),
							AutogenRuleIndex: ruleIndex,
							AutogenRuleScope: ruleScope,
						})
					}
				}
				return
			}
		} else if trackStats {
			ctx.seriesMatched++
		}

		for _, route := range routes {
			if replayLabels {
				chart := ctx.chartsByID[route.ChartID]
				if chart == nil || chart.templateID != route.ChartTemplateID || !chart.labelTracker.needsReplay() {
					continue
				}
				if err := chart.labelTracker.observeReplay(labels, route.DimensionKeyLabel); err != nil {
					firstErr = err
					return
				}
				continue
			}
			route = finalizeRouteAlgorithm(route, meta.Kind)
			if err := ctx.accumulateRoute(ctx.index, route, identity, name, meta, labels, v); err != nil {
				firstErr = err
				return
			}
		}
	}

	if rawIter, ok := ctx.flat.(metrix.SeriesIdentityRawIterator); ok {
		view := &labelSliceView{}
		rawIter.ForEachSeriesIdentityRaw(func(identity metrix.SeriesIdentity, meta metrix.SeriesMeta, name string, labels []metrix.Label, v metrix.SampleValue) {
			view.items = labels
			process(identity, meta, name, view, v)
		})
		return firstErr
	}

	ctx.flat.ForEachSeriesIdentity(func(identity metrix.SeriesIdentity, meta metrix.SeriesMeta, name string, labels metrix.LabelView, v metrix.SampleValue) {
		process(identity, meta, name, labels, v)
	})
	return firstErr
}

func (ctx *planBuildContext) accumulateRoute(
	index matchIndex,
	route routeBinding,
	identity metrix.SeriesIdentity,
	metricName string,
	seriesMeta metrix.SeriesMeta,
	labels metrix.LabelView,
	value metrix.SampleValue,
) error {
	cs, exists := ctx.chartsByID[route.ChartID]
	if exists && cs.templateID != route.ChartTemplateID {
		if !route.Autogen && isAutogenTemplateID(cs.templateID) {
			// Template wins over autogen on chart-id collision.
			ctx.observeAutogenDisplacement(route, identity, metricName, cs.templateID)
			ctx.chartOwners[route.ChartID] = route.ChartTemplateID
			delete(ctx.chartsByID, route.ChartID)
			cs = nil
			exists = false
		} else {
			// Cross-template rendered-id collision.
			// Existing owner keeps chart-id ownership.
			if ctx.routeObserver != nil {
				ctx.observeRouteDiagnostic(PlanRouteDiagnostic{
					Decision:                PlanRouteCollisionRejected,
					SeriesIdentity:          identity,
					MetricName:              metricName,
					ChartTemplateID:         route.ChartTemplateID,
					DimensionIndex:          route.DimensionIndex,
					ChartID:                 route.ChartID,
					DimensionName:           route.DimensionName,
					DimensionKeyLabel:       route.DimensionKeyLabel,
					ExistingChartTemplateID: cs.templateID,
					Autogen:                 route.Autogen,
				})
			}
			return nil
		}
	}
	if !exists {
		ownerTemplateID, ownerExists := ctx.chartOwners[route.ChartID]
		if ownerExists && ownerTemplateID != route.ChartTemplateID {
			if !route.Autogen && isAutogenTemplateID(ownerTemplateID) {
				// Template wins over autogen on chart-id collision.
				ctx.observeAutogenDisplacement(route, identity, metricName, ownerTemplateID)
				ctx.chartOwners[route.ChartID] = route.ChartTemplateID
				delete(ctx.chartsByID, route.ChartID)
			} else {
				// Cross-template rendered-id collision.
				// Existing owner keeps chart-id ownership.
				if ctx.routeObserver != nil {
					ctx.observeRouteDiagnostic(PlanRouteDiagnostic{
						Decision:                PlanRouteCollisionRejected,
						SeriesIdentity:          identity,
						MetricName:              metricName,
						ChartTemplateID:         route.ChartTemplateID,
						DimensionIndex:          route.DimensionIndex,
						ChartID:                 route.ChartID,
						DimensionName:           route.DimensionName,
						DimensionKeyLabel:       route.DimensionKeyLabel,
						ExistingChartTemplateID: ownerTemplateID,
						Autogen:                 route.Autogen,
					})
				}
				return nil
			}
		}
		if !ownerExists {
			ctx.chartOwners[route.ChartID] = route.ChartTemplateID
		}

		dimCap := ctx.dimCapHints[route.ChartID]
		var entries map[string]*dimBuildEntry
		matChart := ctx.materializedByID[route.ChartID]
		matchingMaterializedChart := matChart != nil && matChart.templateID == route.ChartTemplateID
		if matchingMaterializedChart {
			entries = matChart.checkoutScratchEntries(dimCap)
		} else {
			entries = make(map[string]*dimBuildEntry, dimCap)
		}

		var previousPresentation *materializedChartPresentation
		if matchingMaterializedChart && matChart.presentation != nil {
			previousPresentation = matChart.presentation
		}
		cs = &chartState{
			templateID:      route.ChartTemplateID,
			chartID:         route.ChartID,
			meta:            route.Meta,
			lifecycle:       route.Lifecycle,
			labelTracker:    newChartLabelTracker(previousPresentation),
			entries:         entries,
			currentBuildSeq: ctx.buildCycle,
		}
		ctx.chartsByID[route.ChartID] = cs
	}
	if isHistogramBucketSeries(seriesMeta) {
		cs.meta.Type = program.ChartTypeHeatmap
	}
	sortKey := histogramBucketDimensionSortKey(route, seriesMeta, labels)

	entry, exists := cs.entries[route.DimensionName]
	if !exists {
		entry = &dimBuildEntry{}
		cs.entries[route.DimensionName] = entry
	}
	if entry.seenSeq != cs.currentBuildSeq {
		entry.seenSeq = cs.currentBuildSeq
		entry.observations = 1
		entry.value = value
		entry.dimensionState = dimensionState{
			hidden:      route.Hidden,
			float:       route.Float,
			static:      route.Static,
			order:       route.DimensionIndex,
			sortKey:     sortKey,
			aggregation: route.Aggregation,
			algorithm:   compactDimensionAlgorithm(route.Algorithm),
			multiplier:  route.Multiplier,
			divisor:     route.Divisor,
		}
		cs.observedCount++
	} else {
		if entry.hidden != route.Hidden {
			// First-observed hidden flag wins within one build; conflicting routes are ignored.
		}
		if entry.float != route.Float {
			// First-observed float flag wins within one build; conflicting routes are ignored.
		}
		entry.aggregate(value)
	}

	if cs.labelTracker.observeMembership(identity, route.DimensionKeyLabel) {
		policy := labelPolicyForTemplate(index, route.ChartTemplateID, route.Autogen)
		if policy == nil {
			return fmt.Errorf("chartengine: route references unknown chart template %q", route.ChartTemplateID)
		}
		if err := cs.labelTracker.observeDuringScan(policy, labels, route.DimensionKeyLabel); err != nil {
			return err
		}
	}

	if route.Inferred {
		key := fmt.Sprintf("%s\xff%d\xff%s", route.ChartTemplateID, route.DimensionIndex, route.DimensionName)
		if _, exists := ctx.seenInfer[key]; !exists {
			ctx.seenInfer[key] = struct{}{}
			ctx.out.InferredDimensions = append(ctx.out.InferredDimensions, InferredDimension{
				ChartTemplateID: route.ChartTemplateID,
				DimensionIndex:  route.DimensionIndex,
				Name:            route.DimensionName,
			})
		}
	}
	if ctx.routeObserver != nil {
		policy := labelPolicyForTemplate(index, route.ChartTemplateID, route.Autogen)
		if policy == nil {
			return fmt.Errorf("chartengine: route references unknown chart template %q", route.ChartTemplateID)
		}
		instanceLabels, ok := diagnosticChartInstanceLabels(policy.instancePlan, labels)
		if !ok {
			return fmt.Errorf("chartengine: accepted route has unresolved instance labels for chart template %q", route.ChartTemplateID)
		}
		promotionMode, promotedLabels := diagnosticLabelPromotion(policy)
		ctx.observeRouteDiagnostic(PlanRouteDiagnostic{
			Decision:           PlanRouteAccepted,
			SeriesIdentity:     identity,
			MetricName:         metricName,
			MetricFamilyName:   diagnosticMetricFamilyName(metricName, labels, seriesMeta),
			ChartTemplateID:    route.ChartTemplateID,
			DimensionIndex:     route.DimensionIndex,
			ChartID:            route.ChartID,
			DimensionName:      route.DimensionName,
			DimensionKeyLabel:  route.DimensionKeyLabel,
			InstanceLabels:     instanceLabels,
			Context:            cs.meta.Context,
			Family:             cs.meta.Family,
			Units:              cs.meta.Units,
			Algorithm:          string(route.Algorithm),
			Aggregation:        diagnosticAggregation(route.Aggregation),
			Presentation:       string(cs.meta.Type),
			SeriesKind:         diagnosticMetricKind(seriesMeta.Kind),
			Multiplier:         route.Multiplier,
			Divisor:            route.Divisor,
			LabelPromotionMode: promotionMode,
			PromotedLabels:     promotedLabels,
			Autogen:            route.Autogen,
		})
	}
	return nil
}

func labelPolicyForTemplate(index matchIndex, templateID string, autogen bool) *chartLabelPolicy {
	if autogen {
		return index.autogenLabels
	}
	return index.labelPolicies[templateID]
}

func (e *Engine) materializePlanCharts(ctx *planBuildContext) error {
	if err := e.reconcileChangedChartLabels(ctx); err != nil {
		return err
	}

	chartIDs := make([]string, 0, len(ctx.chartsByID))
	for chartID, cs := range ctx.chartsByID {
		if cs.observedCount == 0 {
			continue
		}
		chartIDs = append(chartIDs, chartID)
	}
	sort.Strings(chartIDs)

	for _, chartID := range chartIDs {
		cs := ctx.chartsByID[chartID]
		matChart, chartCreated := ctx.materialized.ensureChart(cs.chartID, cs.templateID, cs.meta, cs.lifecycle)
		previousPresentation := matChart.presentation
		membershipChanged := cs.labelTracker.changed()
		if membershipChanged {
			membership := cs.labelTracker.membership()
			accumulator := cs.labelTracker.accumulator()
			labelsChanged := previousPresentation == nil ||
				!accumulator.materializedEquals(previousPresentation.labelValues)
			if labelsChanged {
				chartLabels := accumulator.materialize()
				matChart.replaceLabels(maps.Clone(chartLabels), membership)
				if chartCreated {
					ctx.out.Actions = append(ctx.out.Actions, CreateChartAction{
						ChartTemplateID: cs.templateID,
						ChartID:         cs.chartID,
						Meta:            cs.meta,
						Labels:          chartLabels,
					})
				} else {
					ctx.out.Actions = append(ctx.out.Actions, UpdateChartLabelsAction{
						ChartID: cs.chartID,
						Meta:    cs.meta,
						Labels:  chartLabels,
					})
				}
			} else {
				matChart.replaceLabelMembership(membership)
			}
		} else if chartCreated {
			return fmt.Errorf("chartengine: new chart %q unexpectedly matched prior label membership", cs.chartID)
		}
		matChart.lastSeenSuccessSeq = ctx.collectMeta.LastSuccessSeq

		observedNames := observedDimensionNames(cs, matChart)
		for _, name := range observedNames {
			entry := cs.entries[name]
			if entry == nil || entry.seenSeq != cs.currentBuildSeq {
				continue
			}
			matDim, dimCreated := matChart.ensureDimension(name, entry.dimensionState)
			if dimCreated {
				ctx.out.Actions = append(ctx.out.Actions, CreateDimensionAction{
					ChartID:    cs.chartID,
					ChartMeta:  cs.meta,
					Name:       name,
					Hidden:     entry.hidden,
					Float:      entry.float,
					Algorithm:  entry.algorithm.programAlgorithm(),
					Multiplier: entry.multiplier,
					Divisor:    entry.divisor,
				})
			}
			matDim.lastSeenSuccessSeq = ctx.collectMeta.LastSuccessSeq
		}

		updateNames := matChart.orderedDimensionNames()
		values := make([]UpdateDimensionValue, 0, len(updateNames))
		for _, name := range updateNames {
			entry, ok := cs.entries[name]
			if ok && entry != nil && entry.seenSeq == cs.currentBuildSeq {
				if math.IsNaN(entry.value) || math.IsInf(entry.value, 0) {
					// A non-finite value (e.g. a summary quantile with no observations this
					// cycle) must render as a gap, not 0: emit SETEMPTY rather than carry NaN.
					values = append(values, UpdateDimensionValue{Name: name, IsEmpty: true})
					continue
				}
				values = append(values, UpdateDimensionValue{
					Name:    name,
					IsFloat: entry.float,
					Int64:   int64(entry.value),
					Float64: entry.value,
				})
				continue
			}
			values = append(values, UpdateDimensionValue{
				Name:    name,
				IsEmpty: true,
			})
		}
		ctx.out.Actions = append(ctx.out.Actions, UpdateChartAction{
			ChartID: cs.chartID,
			Values:  values,
		})
		matChart.storeScratchEntries(cs.entries)
		matChart.pruneScratchEntries(cs.currentBuildSeq)
	}
	return nil
}

func (e *Engine) reconcileChangedChartLabels(ctx *planBuildContext) error {
	needsReplay := false
	for _, chart := range ctx.chartsByID {
		if chart == nil || chart.observedCount == 0 || !chart.labelTracker.finishMembership() {
			continue
		}
		if chart.labelTracker.needsReplay() {
			policy := labelPolicyForTemplate(ctx.index, chart.templateID, isAutogenTemplateID(chart.templateID))
			if policy == nil {
				return fmt.Errorf("chartengine: missing label policy for chart template %q", chart.templateID)
			}
			chart.labelTracker.beginReplay(policy)
			needsReplay = true
		}
	}
	if !needsReplay {
		return nil
	}

	// A mismatch may be discovered after a matching prefix whose labels were not
	// retained. Replay the immutable current snapshot only on changed cycles and
	// use the same route walk so selection and autogen semantics cannot diverge.
	return e.forEachPlanSeriesRoute(ctx, true)
}

func observedDimensionNames(cs *chartState, matChart *materializedChartState) []string {
	if cs == nil {
		return nil
	}
	if cs.observedCount == 0 {
		return nil
	}
	if matChart == nil || len(matChart.dimensions) == 0 {
		return orderedObservedDimensionNames(cs.entries, cs.currentBuildSeq)
	}
	prev := matChart.orderedDimensionNames()
	if len(prev) != cs.observedCount {
		return orderedObservedDimensionNames(cs.entries, cs.currentBuildSeq)
	}
	for _, name := range prev {
		entry, ok := cs.entries[name]
		if !ok || entry == nil || entry.seenSeq != cs.currentBuildSeq {
			return orderedObservedDimensionNames(cs.entries, cs.currentBuildSeq)
		}
		existing := matChart.dimensions[name]
		if existing == nil || existing.static != entry.static || existing.order != entry.order || existing.sortKey != entry.sortKey {
			return orderedObservedDimensionNames(cs.entries, cs.currentBuildSeq)
		}
	}
	return prev
}

func histogramBucketDimensionSortKey(route routeBinding, meta metrix.SeriesMeta, labels metrix.LabelView) dimensionSortKey {
	if !isHistogramBucketSeries(meta) || route.DimensionKeyLabel != metrix.HistogramBucketLabel || labels == nil {
		return dimensionSortKey{}
	}
	upperBound, ok := labels.Get(metrix.HistogramBucketLabel)
	if !ok {
		return dimensionSortKey{}
	}
	return histogramBucketSortKey(upperBound)
}

func histogramBucketSortKey(upperBound string) dimensionSortKey {
	value, ok := parseHistogramBucketUpperBound(upperBound)
	if !ok {
		return dimensionSortKey{}
	}
	return dimensionSortKey{
		kind:       dimensionSortHistogramBucket,
		upperBound: value,
	}
}

func parseHistogramBucketUpperBound(upperBound string) (float64, bool) {
	upperBound = strings.TrimSpace(upperBound)
	if upperBound == "" {
		return 0, false
	}
	value, err := strconv.ParseFloat(upperBound, 64)
	if err != nil || math.IsNaN(value) || math.IsInf(value, -1) {
		return 0, false
	}
	return value, true
}

func sortInferredDimensions(in []InferredDimension) {
	histogramGroups := inferredHistogramBucketGroups(in)
	sort.Slice(in, func(i, j int) bool {
		lhs := in[i]
		rhs := in[j]
		if lhs.ChartTemplateID != rhs.ChartTemplateID {
			return lhs.ChartTemplateID < rhs.ChartTemplateID
		}
		if lhs.DimensionIndex != rhs.DimensionIndex {
			return lhs.DimensionIndex < rhs.DimensionIndex
		}
		if histogramGroups[inferredDimensionGroup{chartTemplateID: lhs.ChartTemplateID, dimensionIndex: lhs.DimensionIndex}] {
			lhsBound, lhsOK := parseHistogramBucketUpperBound(lhs.Name)
			rhsBound, rhsOK := parseHistogramBucketUpperBound(rhs.Name)
			if lhsOK && rhsOK && lhsBound != rhsBound {
				return lhsBound < rhsBound
			}
		}
		return lhs.Name < rhs.Name
	})
}

type inferredDimensionGroup struct {
	chartTemplateID string
	dimensionIndex  int
}

type inferredHistogramGroupStats struct {
	total     int
	parseable int
	hasInf    bool
}

func inferredHistogramBucketGroups(in []InferredDimension) map[inferredDimensionGroup]bool {
	stats := make(map[inferredDimensionGroup]inferredHistogramGroupStats)
	for _, dim := range in {
		group := inferredDimensionGroup{
			chartTemplateID: dim.ChartTemplateID,
			dimensionIndex:  dim.DimensionIndex,
		}
		item := stats[group]
		item.total++
		if upperBound, ok := parseHistogramBucketUpperBound(dim.Name); ok {
			item.parseable++
			item.hasInf = item.hasInf || math.IsInf(upperBound, 1)
		}
		stats[group] = item
	}

	out := make(map[inferredDimensionGroup]bool, len(stats))
	for group, item := range stats {
		out[group] = item.total > 0 && item.parseable == item.total && item.hasInf
	}
	return out
}

func isAutogenTemplateID(templateID string) bool {
	return strings.HasPrefix(templateID, autogenTemplatePrefix)
}

func (e *Engine) nextBuildCycle(sourceSuccessSeq uint64) uint64 {
	if !e.state.cfg.runtimePlanner {
		return sourceSuccessSeq
	}
	e.state.plannerBuildSeq++
	// Seen-seq zero value is reserved for "never seen".
	if e.state.plannerBuildSeq == 0 {
		e.state.plannerBuildSeq = 1
	}
	return e.state.plannerBuildSeq
}
