import type {
	Blocklist,
	BlocklistEcho,
	BlocklistInput,
	CertsReload,
	Client,
	ClientEdit,
	ClientPrefix,
	ClientPrefixInput,
	ConfigStatus,
	DiagnosticEvent,
	DiagnosticsFilter,
	DiagnosticsPage,
	DiagnosticsPurge,
	ForwardZone,
	ForwardZoneInput,
	Group,
	GroupInput,
	Health,
	LocalRecord,
	LocalRecordInput,
	LoginRequest,
	LoginResponse,
	LogoutResponse,
	LookupResult,
	Overview,
	PausePost,
	PauseState,
	Period,
	QueriesFilter,
	QueriesPage,
	QueryDetail,
	Rule,
	RuleEcho,
	RuleInput,
	SettingsEnvelope,
	SettingsPatch,
	SourceStatus,
	Upstream,
	UpstreamEcho,
	UpstreamInput,
	Version,
} from "@/lib/types";

export class ApiError extends Error {
	readonly status: number;
	readonly retryAfter?: number;

	constructor(status: number, message: string, retryAfter?: number) {
		super(message);
		this.name = "ApiError";
		this.status = status;
		this.retryAfter = retryAfter;
	}
}

async function toApiError(res: Response): Promise<ApiError> {
	let message = `HTTP ${res.status}`;
	try {
		const body: unknown = await res.json();
		if (typeof body === "object" && body !== null && "error" in body && typeof body.error === "string") {
			message = body.error;
		}
	} catch {
		// Non-JSON error body; keep the status fallback.
	}
	let retryAfter: number | undefined;
	if (res.status === 429) {
		const header = res.headers.get("Retry-After");
		const seconds = header === null ? NaN : Number(header);
		if (Number.isFinite(seconds) && seconds >= 0) retryAfter = seconds;
	}
	return new ApiError(res.status, message, retryAfter);
}

async function request<T>(path: string, init?: { method?: string; body?: unknown }): Promise<T> {
	const body = init?.body;
	const res = await fetch(path, {
		method: init?.method ?? "GET",
		credentials: "same-origin",
		headers: body !== undefined ? { "content-type": "application/json" } : undefined,
		body: body !== undefined ? JSON.stringify(body) : undefined,
	});
	if (!res.ok) throw await toApiError(res);
	if (res.status === 204) return undefined as T;
	return (await res.json()) as T;
}

type QueryValue = string | number | boolean | undefined;

function qs(params: Record<string, QueryValue>): string {
	const search = new URLSearchParams();
	for (const [key, value] of Object.entries(params)) {
		if (value !== undefined) search.set(key, String(value));
	}
	const encoded = search.toString();
	return encoded === "" ? "" : `?${encoded}`;
}

// Monitoring + meta

export const getHealth = (): Promise<Health> => request("/api/health");
export const getVersion = (): Promise<Version> => request("/api/version");

// Auth

export const login = (body: LoginRequest): Promise<LoginResponse> =>
	request("/api/auth/login", { method: "POST", body });
export const logout = (): Promise<LogoutResponse> => request("/api/auth/logout", { method: "POST", body: {} });

// Query log + overview

export const getQueries = (filter: QueriesFilter = {}): Promise<QueriesPage> =>
	request(`/api/queries${qs({ ...filter })}`);

/** Full provenance of one logged row. 404 when retention has removed it, 503 with no query log. */
export const getQueryDetail = (id: number): Promise<QueryDetail> => request(`/api/queries/${id}`);

/** `EventSource` URL for the live stream; not a fetch route. */
export const liveQueriesUrl = "/api/queries/live";

/** Every Overview panel for one window, from one read transaction. */
export const getOverview = (period?: Period): Promise<Overview> => request(`/api/overview${qs({ period })}`);

export const getLookup = (domain: string, groupId?: number): Promise<LookupResult> =>
	request(`/api/lookup${qs({ domain, group_id: groupId })}`);

// Diagnostics

export const getDiagnostics = (filter: DiagnosticsFilter = {}): Promise<DiagnosticsPage> =>
	request(`/api/diagnostics${qs({ ...filter })}`);

export const getDiagnostic = (id: number): Promise<DiagnosticEvent> => request(`/api/diagnostics/${id}`);

/** Purges one resolved event. An event still active answers 409, an unknown id 404. */
export const purgeDiagnostic = (id: number): Promise<void> => request(`/api/diagnostics/${id}`, { method: "DELETE" });

/** Purges the whole resolved history; active events are never touched. */
export const purgeResolvedDiagnostics = (): Promise<DiagnosticsPurge> =>
	request("/api/diagnostics", { method: "DELETE" });

// Groups

export const listGroups = async (): Promise<Group[]> => (await request<{ groups: Group[] }>("/api/groups")).groups;
export const createGroup = (input: GroupInput): Promise<Group> =>
	request("/api/groups", { method: "POST", body: input });
export const updateGroup = (id: number, input: GroupInput): Promise<Group> =>
	request(`/api/groups/${id}`, { method: "PUT", body: input });
export const deleteGroup = (id: number): Promise<void> => request(`/api/groups/${id}`, { method: "DELETE" });

export const getGroupSources = async (id: number): Promise<number[]> =>
	(await request<{ source_ids: number[] }>(`/api/groups/${id}/sources`)).source_ids;
export const putGroupSources = async (id: number, sourceIds: number[]): Promise<number[]> =>
	(
		await request<{ source_ids: number[] }>(`/api/groups/${id}/sources`, {
			method: "PUT",
			body: { source_ids: sourceIds },
		})
	).source_ids;

// Blocklists

export const listBlocklists = async (): Promise<Blocklist[]> =>
	(await request<{ blocklists: Blocklist[] }>("/api/blocklists")).blocklists;
export const createBlocklist = (input: BlocklistInput): Promise<BlocklistEcho> =>
	request("/api/blocklists", { method: "POST", body: input });
export const updateBlocklistsNow = async (): Promise<SourceStatus[]> =>
	(await request<{ sources: SourceStatus[] }>("/api/blocklists/update", { method: "POST", body: {} })).sources;
export const updateBlocklist = (id: number, input: BlocklistInput): Promise<BlocklistEcho> =>
	request(`/api/blocklists/${id}`, { method: "PUT", body: input });
export const deleteBlocklist = (id: number): Promise<void> => request(`/api/blocklists/${id}`, { method: "DELETE" });

// Rules

export const listRules = async (): Promise<Rule[]> => (await request<{ rules: Rule[] }>("/api/rules")).rules;
export const createRule = (input: RuleInput): Promise<RuleEcho> =>
	request("/api/rules", { method: "POST", body: input });
export const deleteRule = (id: number): Promise<void> => request(`/api/rules/${id}`, { method: "DELETE" });

// Local records

export const listLocalRecords = async (): Promise<LocalRecord[]> =>
	(await request<{ local_records: LocalRecord[] }>("/api/local-records")).local_records;
export const createLocalRecord = (input: LocalRecordInput): Promise<LocalRecord> =>
	request("/api/local-records", { method: "POST", body: input });
export const updateLocalRecord = (id: number, input: LocalRecordInput): Promise<LocalRecord> =>
	request(`/api/local-records/${id}`, { method: "PUT", body: input });
export const deleteLocalRecord = (id: number): Promise<void> =>
	request(`/api/local-records/${id}`, { method: "DELETE" });

// Forward zones

export const listForwardZones = async (): Promise<ForwardZone[]> =>
	(await request<{ forward_zones: ForwardZone[] }>("/api/forward-zones")).forward_zones;
export const createForwardZone = (input: ForwardZoneInput): Promise<ForwardZone> =>
	request("/api/forward-zones", { method: "POST", body: input });
export const updateForwardZone = (id: number, input: ForwardZoneInput): Promise<ForwardZone> =>
	request(`/api/forward-zones/${id}`, { method: "PUT", body: input });
export const deleteForwardZone = (id: number): Promise<void> =>
	request(`/api/forward-zones/${id}`, { method: "DELETE" });

// Clients + prefixes

export const listClients = async (): Promise<Client[]> =>
	(await request<{ clients: Client[] }>("/api/clients")).clients;
export const updateClient = (id: number, edit: ClientEdit): Promise<Client> =>
	request(`/api/clients/${id}`, { method: "PUT", body: edit });
export const deleteClient = (id: number): Promise<void> => request(`/api/clients/${id}`, { method: "DELETE" });

export const listClientPrefixes = async (): Promise<ClientPrefix[]> =>
	(await request<{ client_prefixes: ClientPrefix[] }>("/api/client-prefixes")).client_prefixes;
export const putClientPrefixes = async (prefixes: ClientPrefixInput[]): Promise<ClientPrefix[]> =>
	(
		await request<{ client_prefixes: ClientPrefix[] }>("/api/client-prefixes", {
			method: "PUT",
			body: { client_prefixes: prefixes },
		})
	).client_prefixes;

// Upstreams

export const listUpstreams = async (): Promise<Upstream[]> =>
	(await request<{ upstreams: Upstream[] }>("/api/upstreams")).upstreams;
export const createUpstream = (input: UpstreamInput): Promise<UpstreamEcho> =>
	request("/api/upstreams", { method: "POST", body: input });
export const updateUpstream = (id: number, input: UpstreamInput): Promise<UpstreamEcho> =>
	request(`/api/upstreams/${id}`, { method: "PUT", body: input });
export const deleteUpstream = (id: number): Promise<void> => request(`/api/upstreams/${id}`, { method: "DELETE" });

// Pause + settings

/**
 * The pause state on its own. Protection is read from `/api/health` everywhere
 * the interface shows it — one source, one story — so this is left for the live
 * stream's session probe, which wants the cheapest authenticated GET there is.
 */
export const getPause = (): Promise<PauseState> => request("/api/pause");
export const postPause = (body: PausePost): Promise<PauseState> => request("/api/pause", { method: "POST", body });

export const getSettings = (): Promise<SettingsEnvelope> => request("/api/settings");
export const putSettings = (patch: SettingsPatch): Promise<SettingsEnvelope> =>
	request("/api/settings", { method: "PUT", body: patch });

// Configuration authority and restart state

/**
 * The one route that answers which source governs the running configuration
 * and whether a restart is owed. Both are per-process facts: no other endpoint
 * carries them, and neither survives a restart.
 */
export const getConfigStatus = (): Promise<ConfigStatus> => request("/api/config/status");

/** Reloads the DoH/DoT certificates from disk. A runtime action: served in both authority modes. */
export const reloadCerts = (): Promise<CertsReload> => request("/api/certs/reload", { method: "POST" });
