/**
 * Client activity over the same window as the query-volume chart: one stacked
 * series per named client, plus everything outside the top eight as "Other".
 *
 * The x-axis is derived from this response's own `since` and `bucket_seconds`,
 * which the API aligns with the timeseries endpoint's buckets, so the two charts
 * stack directly above one another and a spike in one is at the same horizontal
 * position in the other. Colour keys on the client string, so a client that
 * changes rank between polls keeps its colour.
 */

import * as stylex from "@stylexjs/stylex";
import { Group } from "@visx/group";
import { BarStack } from "@visx/shape";
import { formatTime } from "@/lib/format";
import type { StatsClients } from "@/lib/types";
import { styles as shared } from "@/ui/styles";
import { colors } from "@/ui/tokens.stylex";
import { clientLabel, useClientNames, type ClientNames } from "@/features/clients/clientNames";
import {
	BucketOverlay,
	CHART_HEIGHT,
	ChartFrame,
	ChartRoot,
	ChartTooltip,
	EmptyChart,
	StackSegment,
	bandScale,
	labelTickValues,
	plotArea,
	slotCenter,
	useActiveIndex,
	useMeasuredWidth,
	valueScale,
	valueTicks,
	type TooltipContent,
} from "./chartKit";
import { OTHER_KEY, clientKey, seriesColor } from "./seriesColors";

const styles = stylex.create({
	legend: {
		marginTop: "0.5rem",
		display: "flex",
		flexWrap: "wrap",
		columnGap: "1rem",
		rowGap: "0.25rem",
		listStyleType: "none",
		padding: 0,
		margin: 0,
		fontSize: "0.75rem",
		lineHeight: "1rem",
		color: colors.textSecondary,
	},
	legendItem: {
		display: "flex",
		alignItems: "center",
		gap: "0.375rem",
	},
	swatch: {
		display: "inline-block",
		width: "0.625rem",
		height: "0.625rem",
		borderRadius: "0.125rem",
	},
	/** Dynamic: the swatch takes the colour the bars are drawn in. */
	swatchColor: (color: string) => ({ backgroundColor: color }),
});

interface Series {
	key: string;
	label: string;
	/** Kept beside the label so a renamed client is still identifiable by address. */
	address: string | null;
	color: string;
	buckets: number[];
}

/**
 * "Other" last, so it sits at the top of every column rather than under a client,
 * and dropped entirely when it counted nothing across the window: an aggregation
 * bucket that aggregated nothing is a legend entry, a stack key, a tooltip row
 * and a table column all saying zero. The named clients stay at zero, because a
 * client that went quiet is something the reader wants to see.
 */
function seriesOf(data: StatsClients, names: ClientNames): Series[] {
	const named = data.clients.map((client) => ({
		key: clientKey(client.client),
		// The name if the client is registered under one, the address otherwise —
		// the same precedence and the same lookup the query tables use. The colour
		// keys on the address regardless, so naming a client never repaints it.
		label: clientLabel(client.client, names)?.text ?? client.client,
		address: client.client,
		color: seriesColor(clientKey(client.client)),
		buckets: client.buckets,
	}));
	if (data.other.every((count) => count === 0)) return named;
	return [
		...named,
		{ key: OTHER_KEY, label: "Other", address: null, color: seriesColor(OTHER_KEY), buckets: data.other },
	];
}

/** One column: the timestamp plus one entry per series, keyed by the series key. */
type Column = { ts: number } & Record<string, number>;

export default function ClientChart({ data }: { data: StatsClients }) {
	const [containerRef, width] = useMeasuredWidth();
	// A hover survives a re-render only while it still names the same bucket at
	// the same place: a poll that rolls the window, or a resize, retires it.
	const hovered = useActiveIndex(`${data.since}:${data.bucket_seconds}:${data.other.length}:${width}`);
	const names = useClientNames();

	const series = seriesOf(data, names);
	const bucketCount = data.other.length;
	const columns: Column[] = Array.from({ length: bucketCount }, (_, i) => {
		const column: Column = { ts: data.since + i * data.bucket_seconds };
		for (const one of series) column[one.key] = one.buckets[i] ?? 0;
		return column;
	});

	if (bucketCount === 0 || columns.every((column) => series.every((one) => column[one.key] === 0))) {
		return <EmptyChart containerRef={containerRef} />;
	}

	const timestamps = columns.map((column) => column.ts);
	const totals = columns.map((column) => series.reduce((sum, one) => sum + column[one.key], 0));
	const plot = plotArea(width);
	const xScale = bandScale(timestamps, plot);
	// Every series here is a disjoint part of the whole rather than a highlighted
	// subset of a separately reported total, so the tallest column's own sum is
	// the scale.
	const yScale = valueScale(Math.max(...totals), [plot.bottom, plot.y]);
	const yTicks = valueTicks(yScale);
	const colorOf = new Map(series.map((one) => [one.key, one.color]));

	function tooltipOf(index: number): TooltipContent {
		return {
			title: formatTime(columns[index].ts),
			rows: [
				{ key: "queries", label: "Queries", value: String(totals[index]) },
				...series.map((one) => ({
					key: one.key,
					label: one.label,
					color: one.color,
					value: String(columns[index][one.key]),
				})),
			],
		};
	}

	return (
		<ChartRoot containerRef={containerRef}>
			<svg
				role="img"
				aria-label={`Client activity over time, ${bucketCount} buckets, ${series.length} series`}
				width="100%"
				height={CHART_HEIGHT}
				viewBox={`0 0 ${width} ${CHART_HEIGHT}`}
				onMouseLeave={hovered.clear}
			>
				<ChartFrame
					plot={plot}
					yScale={yScale}
					yTicks={yTicks}
					xScale={xScale}
					xTickValues={labelTickValues(timestamps, plot.width)}
					bucketSeconds={data.bucket_seconds}
				/>
				<BarStack<Column, string>
					data={columns}
					keys={series.map((one) => one.key)}
					x={(column) => column.ts}
					xScale={xScale}
					yScale={yScale}
					color={(key) => colorOf.get(key) ?? seriesColor(key)}
				>
					{(stacks) =>
						columns.map((column, index) => (
							<Group
								key={column.ts}
								opacity={hovered.index === null || hovered.index === index ? 1 : 0.55}
							>
								{stacks.map((stack) => {
									const bar = stack.bars[index];
									return (
										<StackSegment
											key={stack.key}
											x={bar.x}
											y={bar.y}
											width={bar.width}
											height={bar.height}
											fill={bar.color}
										/>
									);
								})}
							</Group>
						))
					}
				</BarStack>
				<BucketOverlay plot={plot} values={timestamps} xScale={xScale} onEnter={hovered.show} />
			</svg>
			{hovered.index !== null && (
				<ChartTooltip
					index={hovered.index}
					content={tooltipOf(hovered.index)}
					left={slotCenter(xScale, timestamps[hovered.index], plot)}
				/>
			)}
			<ul {...stylex.props(styles.legend)}>
				{series.map((one) => (
					<li key={one.key} title={one.address ?? undefined} {...stylex.props(styles.legendItem)}>
						<span aria-hidden="true" {...stylex.props(styles.swatch, styles.swatchColor(one.color))} />
						{one.label}
					</li>
				))}
			</ul>
			<div {...stylex.props(shared.srOnly)}>
				<table>
					<caption>Queries per client per time bucket</caption>
					<thead>
						<tr>
							<th scope="col">Time</th>
							{series.map((one) => (
								<th key={one.key} scope="col">
									{one.label}
								</th>
							))}
						</tr>
					</thead>
					<tbody>
						{columns.map((column) => (
							<tr key={column.ts}>
								<th scope="row">{formatTime(column.ts)}</th>
								{series.map((one) => (
									<td key={one.key}>{column[one.key]}</td>
								))}
							</tr>
						))}
					</tbody>
				</table>
			</div>
		</ChartRoot>
	);
}
