package alert

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"strings"
)

// AggKeyVersion is the current aggregation-key recipe version. Bump it whenever
// AggLabels or the canonicalization changes, and rehash open alerts in a
// migration — otherwise old open alerts (hashed with the previous recipe) stop
// matching new arrivals and the same group opens a duplicate alert.
const AggKeyVersion = 3

// AggLabels define an alert aggregation group. They mirror the alertmanager
// group_by set so alertstore collapses the same fingerprints alertmanager would
// group into one notification. classification is prepended separately so page
// and ticket never share an aggregate even when every label matches.
//
// The cluster-identifying keys are spliced in from factLabels[FactClusterID]
// rather than hand-listed, so an alias added to the cluster-id derivation (as
// tidbcloud_cluster once was, present in the fact but missing here) can never
// again land in the fact but not the aggregation key — the drift that would
// split one cluster's alerts across two aggregates. The namespace fallback the
// fact also reads is covered already: namespace is listed below in its own
// right.
//
// Alerts that differ only in labels outside this set (e.g. instance/pod) fold
// into one page/ticket. Keep the rest in sync with the alertmanager group_by and
// bump AggKeyVersion on any change.
var AggLabels = buildAggLabels()

func buildAggLabels() []string {
	labels := []string{"alertname"}
	labels = append(labels, factLabels[FactClusterID]...)
	// job_id: only the job/changefeed routes' group_by carries it; absent, it
	// hashes as "" and changes nothing.
	return append(labels, "k8s_cluster_info", "o11y_cluster_info", "severity", "namespace", "changefeed", "job_id")
}

// AggKeyDesc builds the human-readable aggregation key for a classification and
// label set. It is stored in attrs for debugging; AggKeyHash of it backs the
// unique dedup index. Deterministic: AggLabels is a fixed order (no sort), and a
// missing label is canonicalized to empty so absent == "" hash identically.
func AggKeyDesc(classification string, labels map[string]string) string {
	var b strings.Builder
	b.WriteString("class=")
	b.WriteString(classification)
	for _, k := range AggLabels {
		b.WriteByte('|')
		b.WriteString(k)
		b.WriteByte('=')
		b.WriteString(labels[k]) // nil map / missing key -> ""
	}
	return b.String()
}

// AggKeyHash reduces a description to a fixed 64-char hex key that fits the
// agg_key column and its unique index regardless of label length. The version
// is folded in so keys from different recipes never collide.
func AggKeyHash(desc string) string {
	sum := sha256.Sum256([]byte(fmt.Sprintf("v%d\x00%s", AggKeyVersion, desc)))
	return hex.EncodeToString(sum[:])
}
