/**
 * A colour per thing, not per position.
 *
 * Every series and slice on Overview is ranked by count, and a rank that changes
 * between two thirty-second polls would recolour the whole panel if colour came
 * from the ordinal. So colour keys on the entry's semantic identity: the qtype
 * value, the client string, or — for routes — the full `(route, source)` pair,
 * because keying on the route kind alone would paint two adjacent upstream
 * slices the same and merge them into one shape.
 *
 * The four source-less route kinds and the "other" bucket are fixed rather than
 * hashed: they mean the same thing on every install, and Blocked and Cache
 * already have colours on the query-volume timeline.
 */

import type { RouteKind } from "@/lib/types";

/**
 * The dynamic hues, validated for CVD separation and 3:1 contrast against both
 * surfaces; the same hex in light and dark, as the timeline's series are. The
 * five fixed colours below are deliberately not in here: a nameless upstream row
 * must not come out the same red as Blocked in the ring beside it.
 */
const PALETTE = ["#3b82f6", "#ec4899", "#14b8a6", "#f97316", "#6366f1", "#84cc16", "#06b6d4", "#a855f7"] as const;

const FIXED: Record<string, string> = {
	"route:blocked": "#ef4444",
	"route:cache": "#059669",
	"route:local": "#8b5cf6",
	"route:rejected": "#f59e0b",
	other: "#71717a",
};

/** The identity of everything outside the top eight clients. */
export const OTHER_KEY = "other";

export function qtypeKey(qtype: number | null): string {
	return qtype === null ? "qtype:none" : `qtype:${qtype}`;
}

export function clientKey(client: string): string {
	return `client:${client}`;
}

/**
 * Upstream and forward-zone rows are identified by their source as well as their
 * kind; the other four kinds have no source and collapse to the kind alone, so
 * they land on their fixed colour.
 */
export function routeKey(route: RouteKind, source: string | null): string {
	return source === null ? `route:${route}` : `route:${route}:${source}`;
}

/** FNV-1a, 32-bit: stable across reloads and across browsers, which is the whole point. */
function hash(key: string): number {
	let value = 0x811c9dc5;
	for (let i = 0; i < key.length; i += 1) {
		value ^= key.charCodeAt(i);
		value = Math.imul(value, 0x01000193);
	}
	return value >>> 0;
}

/**
 * The colour of one key, and of nothing else.
 *
 * This is a pure function of the identity: no panel, no key set, no rank. That
 * is the property the page needs, because the panels churn — a client enters the
 * top eight and another leaves it every few polls — and an assignment that read
 * the whole set would repaint entries that did not change at all.
 *
 * The cost is that a hash is not injective: two entries of one panel can come
 * out the same hue. That is a real cost and it is the smaller one. Resolving it
 * by probing would mean the entries that lost a slot depend on which entries
 * were present, which is the churn this exists to prevent — and eight hues
 * cannot colour nine things distinctly in any case. The failure a shared hue
 * would cause instead, two neighbouring slices merging into one shape, is
 * prevented where it happens: the donut strokes every arc and the client chart
 * strokes every segment in the surface colour, so equal hues still read as two.
 * The legend and the hidden table name every entry either way.
 */
export function seriesColor(key: string): string {
	return FIXED[key] ?? PALETTE[hash(key) % PALETTE.length];
}
