import { infiniteQueryOptions, keepPreviousData, queryOptions, type QueryClient } from "@tanstack/react-query";
import * as api from "@/lib/api";
import type {
	BlocklistInput,
	ClientEdit,
	ClientPrefixInput,
	DiagnosticsFilter,
	DiagnosticsPage,
	ForwardZoneInput,
	GroupInput,
	LocalRecordInput,
	PausePost,
	Period,
	QueriesFilter,
	QueriesPage,
	RuleInput,
	SettingsPatch,
	UpstreamInput,
} from "@/lib/types";

export const queryKeys = {
	health: ["health"] as const,
	version: ["version"] as const,
	overview: (period: Period) => ["overview", period] as const,
	queriesInfinite: (filter: QueriesFilter) => ["queries", "infinite", filter] as const,
	queryDetail: (id: number) => ["queries", "detail", id] as const,
	diagnosticsInfinite: (filter: DiagnosticsFilter) => ["diagnostics", "infinite", filter] as const,
	diagnostic: (id: number) => ["diagnostics", "event", id] as const,
	/** Prefix of every diagnostics entry, page and detail alike; the purge target. */
	diagnosticsAll: ["diagnostics"] as const,
	lookup: (domain: string, groupId?: number) => ["lookup", domain, groupId ?? null] as const,
	/** Prefix of every `lookup` entry; the invalidation target after any verdict input changes. */
	lookupAll: ["lookup"] as const,
	groups: ["groups"] as const,
	groupSources: (id: number) => ["groups", id, "sources"] as const,
	blocklists: ["blocklists"] as const,
	rules: ["rules"] as const,
	localRecords: ["local-records"] as const,
	forwardZones: ["forward-zones"] as const,
	clients: ["clients"] as const,
	clientPrefixes: ["client-prefixes"] as const,
	upstreams: ["upstreams"] as const,
	settings: ["settings"] as const,
	configStatus: ["configStatus"] as const,
};

export const healthQuery = () =>
	queryOptions({ queryKey: queryKeys.health, queryFn: api.getHealth, refetchInterval: 10_000 });

export const versionQuery = () =>
	queryOptions({ queryKey: queryKeys.version, queryFn: api.getVersion, staleTime: Infinity });

export const overviewQuery = (period: Period = "24h") =>
	queryOptions({
		queryKey: queryKeys.overview(period),
		queryFn: () => api.getOverview(period),
		refetchInterval: 30_000,
	});

// Keyset pagination on `next_before` (handlers/queries.zig). A background
// refetch replays every page in cursor order, so newly logged rows shift the
// whole window instead of opening a gap between page 1 and page 2.
export const queriesInfiniteQuery = (filter: QueriesFilter = {}) =>
	infiniteQueryOptions({
		queryKey: queryKeys.queriesInfinite(filter),
		queryFn: ({ pageParam }: { pageParam: number | undefined }) =>
			api.getQueries(pageParam === undefined ? filter : { ...filter, before: pageParam }),
		initialPageParam: undefined as number | undefined,
		getNextPageParam: (last: QueriesPage) => last.next_before ?? undefined,
		placeholderData: keepPreviousData,
	});

export const queryDetailQuery = (id: number) =>
	queryOptions({ queryKey: queryKeys.queryDetail(id), queryFn: () => api.getQueryDetail(id) });

// Keyset pagination on `next_before`, exactly as the query log pages
// (handlers/diagnostics.zig copies the /api/queries contract). The active view
// polls on healthQuery's cadence because an episode opening is the same news a
// health banner carries; a resolved-history page is settled and does not poll.
// `enabled` belongs to the factory rather than to a spread at the call site:
// spreading the options object loses the page-param type, and the Diagnostics
// page turns one of its two sections off whenever a filter excludes it.
export const diagnosticsInfiniteQuery = (filter: DiagnosticsFilter = {}, enabled = true) =>
	infiniteQueryOptions({
		enabled,
		queryKey: queryKeys.diagnosticsInfinite(filter),
		queryFn: ({ pageParam }: { pageParam: number | undefined }) =>
			api.getDiagnostics(pageParam === undefined ? filter : { ...filter, before: pageParam }),
		initialPageParam: undefined as number | undefined,
		getNextPageParam: (last: DiagnosticsPage) => last.next_before ?? undefined,
		placeholderData: keepPreviousData,
		refetchInterval: filter.state === "active" ? 10_000 : undefined,
	});

export const diagnosticQuery = (id: number) =>
	queryOptions({ queryKey: queryKeys.diagnostic(id), queryFn: () => api.getDiagnostic(id) });

export const lookupQuery = (domain: string, groupId?: number) =>
	queryOptions({ queryKey: queryKeys.lookup(domain, groupId), queryFn: () => api.getLookup(domain, groupId) });

export const groupsQuery = () => queryOptions({ queryKey: queryKeys.groups, queryFn: api.listGroups });

export const groupSourcesQuery = (id: number) =>
	queryOptions({ queryKey: queryKeys.groupSources(id), queryFn: () => api.getGroupSources(id) });

export const blocklistsQuery = () => queryOptions({ queryKey: queryKeys.blocklists, queryFn: api.listBlocklists });

export const rulesQuery = () => queryOptions({ queryKey: queryKeys.rules, queryFn: api.listRules });

export const localRecordsQuery = () =>
	queryOptions({ queryKey: queryKeys.localRecords, queryFn: api.listLocalRecords });

export const forwardZonesQuery = () =>
	queryOptions({ queryKey: queryKeys.forwardZones, queryFn: api.listForwardZones });

export const clientsQuery = () => queryOptions({ queryKey: queryKeys.clients, queryFn: api.listClients });

export const clientPrefixesQuery = () =>
	queryOptions({ queryKey: queryKeys.clientPrefixes, queryFn: api.listClientPrefixes });

export const upstreamsQuery = () => queryOptions({ queryKey: queryKeys.upstreams, queryFn: api.listUpstreams });

export const settingsQuery = () => queryOptions({ queryKey: queryKeys.settings, queryFn: api.getSettings });

// Restart truth must not depend on the tab that caused it: a notice raised by
// another tab, another API client or a mutation elsewhere in this one has to
// surface here too, and a process restart has to clear it. Hence never stale,
// always refetched when the window regains focus, and polled once a minute.
export const configStatusQuery = () =>
	queryOptions({
		queryKey: queryKeys.configStatus,
		queryFn: api.getConfigStatus,
		staleTime: 0,
		refetchOnWindowFocus: "always",
		refetchInterval: 60_000,
	});

// Mutation option factories. Usage: useMutation(groupCreateMutation(useQueryClient())).
// Group membership and names feed lookup verdicts and the group columns on
// clients, prefixes and rules, hence the wide invalidation on group mutations.

// Both purges invalidate the whole `diagnostics` prefix rather than one page
// key: the resolved list, the active list (whose `active` counts ride along) and
// the detail query of the row just deleted all describe the table that changed.
export const diagnosticPurgeMutation = (qc: QueryClient) => ({
	mutationFn: (id: number) => api.purgeDiagnostic(id),
	onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.diagnosticsAll }),
});

export const diagnosticsPurgeResolvedMutation = (qc: QueryClient) => ({
	mutationFn: () => api.purgeResolvedDiagnostics(),
	onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.diagnosticsAll }),
});

function invalidateGroupWorld(qc: QueryClient): Promise<unknown> {
	return Promise.all([
		qc.invalidateQueries({ queryKey: queryKeys.groups }),
		qc.invalidateQueries({ queryKey: queryKeys.lookupAll }),
		qc.invalidateQueries({ queryKey: queryKeys.clients }),
		qc.invalidateQueries({ queryKey: queryKeys.clientPrefixes }),
		qc.invalidateQueries({ queryKey: queryKeys.rules }),
	]);
}

export const groupCreateMutation = (qc: QueryClient) => ({
	mutationFn: (input: GroupInput) => api.createGroup(input),
	onSuccess: () => invalidateGroupWorld(qc),
});

export const groupUpdateMutation = (qc: QueryClient) => ({
	mutationFn: ({ id, input }: { id: number; input: GroupInput }) => api.updateGroup(id, input),
	onSuccess: () => invalidateGroupWorld(qc),
});

export const groupDeleteMutation = (qc: QueryClient) => ({
	mutationFn: (id: number) => api.deleteGroup(id),
	onSuccess: () => invalidateGroupWorld(qc),
});

export const groupSourcesPutMutation = (qc: QueryClient) => ({
	mutationFn: ({ id, sourceIds }: { id: number; sourceIds: number[] }) => api.putGroupSources(id, sourceIds),
	onSuccess: (sourceIds: number[], { id }: { id: number; sourceIds: number[] }) => {
		qc.setQueryData(queryKeys.groupSources(id), sourceIds);
		return qc.invalidateQueries({ queryKey: queryKeys.lookupAll });
	},
});

function invalidateBlocklistWorld(qc: QueryClient): Promise<unknown> {
	return Promise.all([
		qc.invalidateQueries({ queryKey: queryKeys.blocklists }),
		qc.invalidateQueries({ queryKey: queryKeys.lookupAll }),
		qc.invalidateQueries({ queryKey: queryKeys.groups }),
	]);
}

export const blocklistCreateMutation = (qc: QueryClient) => ({
	mutationFn: (input: BlocklistInput) => api.createBlocklist(input),
	onSuccess: () => invalidateBlocklistWorld(qc),
});

export const blocklistUpdateMutation = (qc: QueryClient) => ({
	mutationFn: ({ id, input }: { id: number; input: BlocklistInput }) => api.updateBlocklist(id, input),
	onSuccess: () => invalidateBlocklistWorld(qc),
});

export const blocklistDeleteMutation = (qc: QueryClient) => ({
	mutationFn: (id: number) => api.deleteBlocklist(id),
	onSuccess: () => invalidateBlocklistWorld(qc),
});

// A runtime action, not a configuration write: it re-fetches the sources the
// running process already declares, so it works under file authority too. The
// 202 body's per-source snapshot is deliberately dropped — refresh outcomes are
// durable counters on the source rows and episodes in Diagnostics, not an
// ephemeral readout that survives one navigation.
export const blocklistsUpdateNowMutation = (qc: QueryClient) => ({
	mutationFn: () => api.updateBlocklistsNow(),
	onSuccess: () =>
		Promise.all([
			qc.invalidateQueries({ queryKey: queryKeys.blocklists }),
			qc.invalidateQueries({ queryKey: queryKeys.lookupAll }),
		]),
});

function invalidateRules(qc: QueryClient): Promise<unknown> {
	return Promise.all([
		qc.invalidateQueries({ queryKey: queryKeys.rules }),
		qc.invalidateQueries({ queryKey: queryKeys.lookupAll }),
	]);
}

export const ruleCreateMutation = (qc: QueryClient) => ({
	mutationFn: (input: RuleInput) => api.createRule(input),
	onSuccess: () => invalidateRules(qc),
});

export const ruleDeleteMutation = (qc: QueryClient) => ({
	mutationFn: (id: number) => api.deleteRule(id),
	onSuccess: () => invalidateRules(qc),
});

function invalidateLocalRecords(qc: QueryClient): Promise<unknown> {
	return Promise.all([
		qc.invalidateQueries({ queryKey: queryKeys.localRecords }),
		qc.invalidateQueries({ queryKey: queryKeys.lookupAll }),
	]);
}

export const localRecordCreateMutation = (qc: QueryClient) => ({
	mutationFn: (input: LocalRecordInput) => api.createLocalRecord(input),
	onSuccess: () => invalidateLocalRecords(qc),
});

export const localRecordUpdateMutation = (qc: QueryClient) => ({
	mutationFn: ({ id, input }: { id: number; input: LocalRecordInput }) => api.updateLocalRecord(id, input),
	onSuccess: () => invalidateLocalRecords(qc),
});

export const localRecordDeleteMutation = (qc: QueryClient) => ({
	mutationFn: (id: number) => api.deleteLocalRecord(id),
	onSuccess: () => invalidateLocalRecords(qc),
});

function invalidateForwardZones(qc: QueryClient): Promise<unknown> {
	return Promise.all([
		qc.invalidateQueries({ queryKey: queryKeys.forwardZones }),
		qc.invalidateQueries({ queryKey: queryKeys.lookupAll }),
	]);
}

export const forwardZoneCreateMutation = (qc: QueryClient) => ({
	mutationFn: (input: ForwardZoneInput) => api.createForwardZone(input),
	onSuccess: () => invalidateForwardZones(qc),
});

export const forwardZoneUpdateMutation = (qc: QueryClient) => ({
	mutationFn: ({ id, input }: { id: number; input: ForwardZoneInput }) => api.updateForwardZone(id, input),
	onSuccess: () => invalidateForwardZones(qc),
});

export const forwardZoneDeleteMutation = (qc: QueryClient) => ({
	mutationFn: (id: number) => api.deleteForwardZone(id),
	onSuccess: () => invalidateForwardZones(qc),
});

export const clientUpdateMutation = (qc: QueryClient) => ({
	mutationFn: ({ id, edit }: { id: number; edit: ClientEdit }) => api.updateClient(id, edit),
	onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.clients }),
});

export const clientDeleteMutation = (qc: QueryClient) => ({
	mutationFn: (id: number) => api.deleteClient(id),
	onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.clients }),
});

export const clientPrefixesPutMutation = (qc: QueryClient) => ({
	mutationFn: (prefixes: ClientPrefixInput[]) => api.putClientPrefixes(prefixes),
	onSuccess: (stored: Awaited<ReturnType<typeof api.putClientPrefixes>>) => {
		qc.setQueryData(queryKeys.clientPrefixes, stored);
	},
});

// An upstream write rebuilds the pool in the running process, so the row list
// is the only thing it changes: no restart is owed and /api/config/status says
// the same thing after the write as before it.
function invalidateUpstreams(qc: QueryClient): Promise<unknown> {
	return qc.invalidateQueries({ queryKey: queryKeys.upstreams });
}

export const upstreamCreateMutation = (qc: QueryClient) => ({
	mutationFn: (input: UpstreamInput) => api.createUpstream(input),
	onSuccess: () => invalidateUpstreams(qc),
});

export const upstreamUpdateMutation = (qc: QueryClient) => ({
	mutationFn: ({ id, input }: { id: number; input: UpstreamInput }) => api.updateUpstream(id, input),
	onSuccess: () => invalidateUpstreams(qc),
});

export const upstreamDeleteMutation = (qc: QueryClient) => ({
	mutationFn: (id: number) => api.deleteUpstream(id),
	onSuccess: () => invalidateUpstreams(qc),
});

// Protection is a health condition, and health is the only thing that reads it:
// the sidebar control, the Diagnostics health strip and the related action on a
// blocked query all render `Health.protection`. Without this invalidation they
// would contradict a successful mutation until the next ten-second poll.
export const pauseMutation = (qc: QueryClient) => ({
	mutationFn: (body: PausePost) => api.postPause(body),
	onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.health }),
});

// Whether the patch needs a restart is the server's decision (it owns the
// restart-required key set), so the client re-reads the status rather than
// deciding for itself.
export const settingsPutMutation = (qc: QueryClient) => ({
	mutationFn: (patch: SettingsPatch) => api.putSettings(patch),
	onSuccess: (envelope: Awaited<ReturnType<typeof api.putSettings>>) => {
		qc.setQueryData(queryKeys.settings, envelope);
		return Promise.all([
			qc.invalidateQueries({ queryKey: queryKeys.settings }),
			qc.invalidateQueries({ queryKey: queryKeys.configStatus }),
		]);
	},
});
