/**
 * One period, five requests, one window.
 *
 * Totals, the timeline, the per-client series and the two breakdowns are
 * separate calls, so a refresh that straddles a bucket boundary — or a retention
 * pass that advances the watermark mid-page — can answer them for different
 * windows. Rendering them side by side anyway would put a headline count above
 * charts of a different span, a mixed page that looks exactly like a real one.
 *
 * This is **window** coherence, not data-snapshot coherence: matching bounds
 * cannot prove a common database state, and live inserts between requests may
 * still shift counts slightly between panels. What it does guarantee is that no
 * two panels ever describe different spans.
 *
 * Rendering is per panel. A panel whose request is still in flight shows its own
 * loading state and a panel whose request failed shows its own error, while the
 * panels that match the window keep rendering — a failed donut never blanks the
 * charts.
 */

import { useCallback, useEffect, useRef, useState } from "react";
import { keepPreviousData, useQuery, type UseQueryResult } from "@tanstack/react-query";
import { statsClientsQuery, statsQuery, statsRoutesQuery, statsTypesQuery, timeseriesQuery } from "@/lib/queries";
import type {
	Coverage,
	Period,
	StatsClients,
	StatsRoutes,
	StatsTimeseries,
	StatsTotals,
	StatsTypes,
} from "@/lib/types";

export const OVERVIEW_ENDPOINTS = ["totals", "timeseries", "clients", "types", "routes"] as const;
export type OverviewEndpoint = (typeof OVERVIEW_ENDPOINTS)[number];

interface EndpointBodies {
	totals: StatsTotals;
	timeseries: StatsTimeseries;
	clients: StatsClients;
	types: StatsTypes;
	routes: StatsRoutes;
}

/**
 * What makes two responses the same window. `available_since` joins the bounds
 * because retention advancing between requests changes what the same `[since,
 * until)` can answer for, and mixing a pre-prune answer with a post-prune one is
 * the failure the bounds alone would not catch.
 */
export interface WindowId {
	period: Period;
	since: number;
	until: number;
	availableSince: number;
}

/** The four fields every window-bounded stats body carries. */
interface Bounded {
	period: Period;
	since: number;
	until: number;
	coverage: Coverage;
}

export function windowIdOf(body: Bounded): WindowId {
	return {
		period: body.period,
		since: body.since,
		until: body.until,
		availableSince: body.coverage.available_since,
	};
}

export function sameWindow(a: WindowId, b: WindowId): boolean {
	return a.period === b.period && a.since === b.since && a.until === b.until && a.availableSince === b.availableSince;
}

/**
 * Which of two candidate windows the page adopts: the one that reaches further
 * forward in time, and for identical bounds the one that admits the later
 * watermark. Both rules pick the answer a laggard has to catch up to.
 */
export function newerWindow(a: WindowId, b: WindowId): WindowId {
	if (b.until !== a.until) return b.until > a.until ? b : a;
	return b.availableSince > a.availableSince ? b : a;
}

function keyOf(id: WindowId): string {
	return `${id.period}|${id.since}|${id.until}|${id.availableSince}`;
}

export type Panel<T> =
	{ status: "loading" } | { status: "error"; error: unknown; retry: () => void } | { status: "ready"; data: T };

export interface OverviewWindow {
	/** Null until one response for the selected period has arrived. */
	window: WindowId | null;
	/** The adopted window's watermark, for the page's single coverage notice. */
	coverage: Coverage | null;
	totals: Panel<StatsTotals>;
	timeseries: Panel<StatsTimeseries>;
	clients: Panel<StatsClients>;
	types: Panel<StatsTypes>;
	routes: Panel<StatsRoutes>;
}

/**
 * A laggard that stayed behind after its one retry. Not an `ApiError`: nothing
 * failed, the endpoint simply never caught up, and `InlineError` renders the
 * message verbatim.
 */
export const MISMATCH = new Error("This panel is for a different window than the rest of the page. Try again.");

export function useOverviewWindow(period: Period): OverviewWindow {
	const queries: { [K in OverviewEndpoint]: UseQueryResult<EndpointBodies[K]> } = {
		totals: useQuery({ ...statsQuery(period), placeholderData: keepPreviousData }),
		timeseries: useQuery({ ...timeseriesQuery(period), placeholderData: keepPreviousData }),
		clients: useQuery({ ...statsClientsQuery(period), placeholderData: keepPreviousData }),
		types: useQuery({ ...statsTypesQuery(period), placeholderData: keepPreviousData }),
		routes: useQuery({ ...statsRoutesQuery(period), placeholderData: keepPreviousData }),
	};

	// A `keepPreviousData` placeholder for the period just left is a complete,
	// self-consistent body — and still the wrong one to show under the new label,
	// so it is neither a candidate for the window nor a member of it.
	const answers = new Map<OverviewEndpoint, WindowId>();
	for (const endpoint of OVERVIEW_ENDPOINTS) {
		const data = queries[endpoint].data;
		if (data !== undefined && data.period === period) answers.set(endpoint, windowIdOf(data));
	}

	let window: WindowId | null = null;
	for (const id of answers.values()) window = window === null ? id : newerWindow(window, id);

	// The effect below runs on what the responses say, not on how many times they
	// arrived: a poll that returns byte-identical data must not restart the retry
	// bookkeeping. The refetchers ride a ref for the same reason — TanStack hands
	// back a fresh function identity on some renders, and depending on it would
	// re-enter the effect with nothing changed.
	const answersKey = OVERVIEW_ENDPOINTS.map((endpoint) => {
		const id = answers.get(endpoint);
		return id === undefined ? "" : keyOf(id);
	}).join("~");
	const latest = useRef({ answers, refetch: queries });
	latest.current = { answers, refetch: queries };

	// Which mismatch episode each endpoint has already spent its retry on, keyed
	// by endpoint and window identity so a new window buys a new attempt.
	const retriedFor = useRef(new Map<OverviewEndpoint, string>());
	// Which retry each endpoint is waiting on. Per endpoint, because one shared
	// counter would let a second endpoint's retry silence the first's completion;
	// bumped on every retry issued, so a completion from a window or a period the
	// page has left can neither clear an error the current one reached nor spend
	// the current window's one retry.
	const tokens = useRef(new Map<OverviewEndpoint, number>());
	// State, not a ref: a retry that returns byte-identical data changes nothing
	// else a render could see, and the panel still has to reach its error.
	const [landedFor, setLandedFor] = useState(new Map<OverviewEndpoint, string>());

	// Leaving a period ends every episode it opened. A retry issued for the old
	// period can still be in flight, and without this its completion would land
	// under the new one holding a token the map still honours: it would record an
	// episode as spent, so a return to that window would reach the terminal error
	// without the retry that error is supposed to follow. Bumping the tokens
	// orphans those answers, and the cleared maps let the new window start clean.
	const [lastPeriod, setLastPeriod] = useState(period);
	if (lastPeriod !== period) {
		setLastPeriod(period);
		for (const endpoint of OVERVIEW_ENDPOINTS) {
			tokens.current.set(endpoint, (tokens.current.get(endpoint) ?? 0) + 1);
		}
		retriedFor.current.clear();
		setLandedFor(new Map());
	}

	const windowKey = window === null ? null : keyOf(window);

	useEffect(() => {
		if (windowKey === null) return;
		for (const [endpoint, identity] of latest.current.answers) {
			if (keyOf(identity) === windowKey) {
				retriedFor.current.delete(endpoint);
				continue;
			}
			const episode = `${endpoint}|${windowKey}`;
			if (retriedFor.current.get(endpoint) === episode) continue;
			retriedFor.current.set(endpoint, episode);
			const token = (tokens.current.get(endpoint) ?? 0) + 1;
			tokens.current.set(endpoint, token);
			const landed = () => {
				if (tokens.current.get(endpoint) !== token) return;
				setLandedFor((previous) => new Map(previous).set(endpoint, episode));
			};
			void latest.current.refetch[endpoint].refetch().then(landed, landed);
		}
	}, [answersKey, windowKey]);

	const retry = useCallback((endpoint: OverviewEndpoint) => {
		retriedFor.current.delete(endpoint);
		tokens.current.set(endpoint, (tokens.current.get(endpoint) ?? 0) + 1);
		setLandedFor((previous) => {
			const next = new Map(previous);
			next.delete(endpoint);
			return next;
		});
		void latest.current.refetch[endpoint].refetch();
	}, []);

	function panelOf<K extends OverviewEndpoint>(endpoint: K): Panel<EndpointBodies[K]> {
		const query = queries[endpoint];
		const onRetry = () => retry(endpoint);
		if (query.isError) return { status: "error", error: query.error, retry: onRetry };
		const data = query.data;
		if (
			data !== undefined &&
			windowKey !== null &&
			data.period === period &&
			keyOf(windowIdOf(data)) === windowKey
		) {
			return { status: "ready", data };
		}
		if (windowKey !== null && landedFor.get(endpoint) === `${endpoint}|${windowKey}`) {
			return { status: "error", error: MISMATCH, retry: onRetry };
		}
		return { status: "loading" };
	}

	const panels = {
		totals: panelOf("totals"),
		timeseries: panelOf("timeseries"),
		clients: panelOf("clients"),
		types: panelOf("types"),
		routes: panelOf("routes"),
	};

	// The notice describes the window, so any member of it can supply the
	// watermark: whichever panel arrived says the same thing about coverage.
	let coverage: Coverage | null = null;
	for (const endpoint of OVERVIEW_ENDPOINTS) {
		const panel = panels[endpoint];
		if (panel.status === "ready") {
			coverage = panel.data.coverage;
			break;
		}
	}

	return { window, coverage, ...panels };
}
