/**
 * 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 the timeseries endpoint's own bucket alignment, 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 { useEffect, useRef, useState } from "react";
import * as stylex from "@stylexjs/stylex";
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 { layoutStacked } from "./chartLayout";
import { clientLabel, useClientNames, type ClientNames } from "@/features/clients/clientNames";
import { OTHER_KEY, clientKey, seriesColor } from "./seriesColors";

const CHART_HEIGHT = 240;
const FALLBACK_WIDTH = 640;

const styles = stylex.create({
	empty: {
		display: "flex",
		alignItems: "center",
		justifyContent: "center",
		height: CHART_HEIGHT,
		borderRadius: "0.25rem",
		borderWidth: 1,
		borderStyle: "dashed",
		borderColor: colors.borderStrong,
		fontSize: "0.875rem",
		lineHeight: "1.25rem",
		color: colors.textMuted,
	},
	root: {
		position: "relative",
	},
	gridLine: {
		stroke: colors.border,
	},
	axisLine: {
		stroke: colors.borderStrong,
	},
	axisLabel: {
		fill: colors.textMuted,
		fontSize: "10px",
	},
	/** The hairline separating touching segments is the page ground, not a colour. */
	segment: {
		stroke: colors.surface,
	},
	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 }),
});

function useContainerWidth(): [React.RefObject<HTMLDivElement | null>, number] {
	const ref = useRef<HTMLDivElement>(null);
	const [width, setWidth] = useState(0);
	useEffect(() => {
		const el = ref.current;
		if (el === null) return;
		setWidth(el.clientWidth);
		if (typeof ResizeObserver === "undefined") return;
		const observer = new ResizeObserver(() => setWidth(el.clientWidth));
		observer.observe(el);
		return () => observer.disconnect();
	}, []);
	return [ref, width];
}

const compact = new Intl.NumberFormat(undefined, { notation: "compact" });

function formatTick(ts: number, bucketSeconds: number): string {
	const date = new Date(ts * 1000);
	if (bucketSeconds >= 86_400) {
		return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" }).format(date);
	}
	return new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" }).format(date);
}

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 always present: the response always carries the series, and a
 * legend that dropped it on a quiet period would make the reader think the
 * chart's clients were all of them.
 */
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,
	}));
	return [
		...named,
		{ key: OTHER_KEY, label: "Other", address: null, color: seriesColor(OTHER_KEY), buckets: data.other },
	];
}

export default function ClientChart({ data }: { data: StatsClients }) {
	const [containerRef, measuredWidth] = useContainerWidth();
	const names = useClientNames();
	const width = measuredWidth > 0 ? measuredWidth : FALLBACK_WIDTH;
	const series = seriesOf(data, names);
	const bucketCount = data.other.length;

	if (bucketCount === 0) {
		return (
			<div ref={containerRef} {...stylex.props(styles.empty)}>
				No queries in this period.
			</div>
		);
	}

	const columns = Array.from({ length: bucketCount }, (_, i) => ({
		ts: data.since + i * data.bucket_seconds,
		values: series.map((one) => one.buckets[i] ?? 0),
	}));
	if (columns.every((column) => column.values.every((value) => value === 0))) {
		return (
			<div ref={containerRef} {...stylex.props(styles.empty)}>
				No queries in this period.
			</div>
		);
	}

	const layout = layoutStacked(columns, width, CHART_HEIGHT);
	const baseline = layout.plot.y + layout.plot.height;

	return (
		<div ref={containerRef} {...stylex.props(styles.root)}>
			<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}`}
			>
				{layout.yTicks.map((tick) => (
					<g key={tick.value}>
						<line
							x1={layout.plot.x}
							x2={layout.plot.x + layout.plot.width}
							y1={tick.y}
							y2={tick.y}
							{...stylex.props(styles.gridLine)}
						/>
						<text
							x={layout.plot.x - 6}
							y={tick.y}
							textAnchor="end"
							dominantBaseline="middle"
							{...stylex.props(styles.axisLabel, shared.tabularNums)}
						>
							{compact.format(tick.value)}
						</text>
					</g>
				))}
				<line
					x1={layout.plot.x}
					x2={layout.plot.x + layout.plot.width}
					y1={baseline}
					y2={baseline}
					{...stylex.props(styles.axisLine)}
				/>
				{layout.xTicks.map((tick) => (
					<text
						key={tick.ts}
						x={tick.x}
						y={baseline + 14}
						textAnchor="middle"
						{...stylex.props(styles.axisLabel)}
					>
						{formatTick(tick.ts, data.bucket_seconds)}
					</text>
				))}
				{layout.columns.map((column) => (
					<g key={column.ts}>
						{column.segments.map((rect, index) =>
							rect.height <= 0 ? null : (
								<rect
									key={series[index].key}
									x={rect.x}
									y={rect.y}
									width={rect.width}
									height={rect.height}
									fill={series[index].color}
									strokeWidth={rect.width > 3 ? 1 : 0}
									{...stylex.props(styles.segment)}
								/>
							),
						)}
						<rect
							x={column.slot.x}
							y={column.slot.y}
							width={column.slot.width}
							height={column.slot.height}
							fill="transparent"
						>
							<title>
								{`${formatTime(column.ts)}: ${column.total} ${column.total === 1 ? "query" : "queries"}`}
							</title>
						</rect>
					</g>
				))}
			</svg>
			<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>
								{column.values.map((value, index) => (
									<td key={series[index].key}>{value}</td>
								))}
							</tr>
						))}
					</tbody>
				</table>
			</div>
		</div>
	);
}
