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

export interface Rect {
	x: number;
	y: number;
	width: number;
	height: number;
}

export interface BarLayout {
	bucket: Bucket;
	/** queries - blocked - cached, clamped at 0. */
	other: number;
	slot: Rect;
	segments: {
		blocked: Rect;
		cached: Rect;
		other: Rect;
	};
}

export interface ChartLayout {
	width: number;
	height: number;
	plot: Rect;
	scaleMax: number;
	bars: BarLayout[];
	yTicks: { value: number; y: number }[];
	xTicks: { ts: number; x: number }[];
}

export const MARGIN = { top: 8, right: 8, bottom: 22, left: 44 } as const;
const BAR_GAP = 2;
const MIN_X_LABEL_PX = 90;

/** Tick values from 0 upward in a 1/2/5 step, extended until the last tick covers `max`. */
export function niceTicks(max: number, targetCount = 4): number[] {
	if (max <= 0) return [0];
	const rawStep = max / targetCount;
	const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)));
	const normalized = rawStep / magnitude;
	const step = (normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10) * magnitude;
	const ticks: number[] = [];
	for (let value = 0; ; value += step) {
		ticks.push(value);
		if (value >= max) break;
	}
	return ticks;
}

export function isEmptyTimeseries(buckets: Bucket[]): boolean {
	return buckets.every((bucket) => bucket.queries === 0);
}

function plotRect(width: number, height: number): Rect {
	return {
		x: MARGIN.left,
		y: MARGIN.top,
		width: Math.max(0, width - MARGIN.left - MARGIN.right),
		height: Math.max(0, height - MARGIN.top - MARGIN.bottom),
	};
}

/**
 * How many columns share one x-axis label. A 30-day window is 30 columns and a
 * 1-hour window is 60, so at narrow widths the labels have to thin out rather
 * than overprint each other.
 */
function labelStepFor(count: number, plotWidth: number): number {
	if (count === 0 || plotWidth <= 0) return 1;
	return Math.max(1, Math.ceil((count * MIN_X_LABEL_PX) / plotWidth));
}

/** One stacked column: the series values in the order the caller stacks them. */
export interface StackedColumn {
	ts: number;
	total: number;
	slot: Rect;
	segments: Rect[];
}

export interface StackedLayout {
	width: number;
	height: number;
	plot: Rect;
	scaleMax: number;
	columns: StackedColumn[];
	yTicks: { value: number; y: number }[];
	xTicks: { ts: number; x: number }[];
}

/**
 * The same geometry as the query-volume chart, for an arbitrary number of
 * series. The scale comes from the tallest column's own total, because every
 * series here is a disjoint part of the whole rather than a highlighted subset
 * of a separately reported total.
 */
export function layoutStacked(
	columns: { ts: number; values: number[] }[],
	width: number,
	height: number,
): StackedLayout {
	const plot = plotRect(width, height);
	const totals = columns.map((column) => column.values.reduce((sum, value) => sum + value, 0));
	const tickValues = niceTicks(Math.max(0, ...totals));
	const scaleMax = Math.max(tickValues[tickValues.length - 1], 1);
	const baseline = plot.y + plot.height;
	const toHeight = (value: number) => (value / scaleMax) * plot.height;

	const slotWidth = columns.length > 0 ? plot.width / columns.length : 0;
	const barWidth = Math.max(1, slotWidth - BAR_GAP);

	const laidOut: StackedColumn[] = columns.map((column, i) => {
		const slotX = plot.x + i * slotWidth;
		const barX = slotX + (slotWidth - barWidth) / 2;
		let top = baseline;
		const segments = column.values.map((value) => {
			const segmentHeight = toHeight(value);
			top -= segmentHeight;
			return { x: barX, y: top, width: barWidth, height: segmentHeight };
		});
		return {
			ts: column.ts,
			total: totals[i],
			slot: { x: slotX, y: plot.y, width: slotWidth, height: plot.height },
			segments,
		};
	});

	const step = labelStepFor(columns.length, plot.width);
	return {
		width,
		height,
		plot,
		scaleMax,
		columns: laidOut,
		yTicks: tickValues.map((value) => ({ value, y: baseline - toHeight(value) })),
		xTicks: laidOut
			.filter((_, i) => i % step === 0)
			.map((column) => ({ ts: column.ts, x: column.slot.x + column.slot.width / 2 })),
	};
}

export function layoutTimeseries(buckets: Bucket[], width: number, height: number): ChartLayout {
	const plot = plotRect(width, height);
	const maxQueries = buckets.reduce((max, bucket) => Math.max(max, bucket.queries), 0);
	const tickValues = niceTicks(maxQueries);
	const scaleMax = Math.max(tickValues[tickValues.length - 1], 1);
	const baseline = plot.y + plot.height;
	const toHeight = (value: number) => (value / scaleMax) * plot.height;

	const slotWidth = buckets.length > 0 ? plot.width / buckets.length : 0;
	const barWidth = Math.max(1, slotWidth - BAR_GAP);

	const bars: BarLayout[] = buckets.map((bucket, i) => {
		const slotX = plot.x + i * slotWidth;
		const barX = slotX + (slotWidth - barWidth) / 2;
		const other = Math.max(0, bucket.queries - bucket.blocked - bucket.cached);
		const blockedH = toHeight(bucket.blocked);
		const cachedH = toHeight(bucket.cached);
		const otherH = toHeight(other);
		return {
			bucket,
			other,
			slot: { x: slotX, y: plot.y, width: slotWidth, height: plot.height },
			segments: {
				blocked: { x: barX, y: baseline - blockedH, width: barWidth, height: blockedH },
				cached: { x: barX, y: baseline - blockedH - cachedH, width: barWidth, height: cachedH },
				other: { x: barX, y: baseline - blockedH - cachedH - otherH, width: barWidth, height: otherH },
			},
		};
	});

	const yTicks = tickValues.map((value) => ({ value, y: baseline - toHeight(value) }));

	const labelStep = labelStepFor(buckets.length, plot.width);
	const xTicks = bars
		.filter((_, i) => i % labelStep === 0)
		.map((bar) => ({ ts: bar.bucket.ts, x: bar.slot.x + bar.slot.width / 2 }));

	return { width, height, plot, scaleMax, bars, yTicks, xTicks };
}
