import type { ResourceUri } from '@atcute/lexicons/syntax';

import { normalizeServiceUrl } from './client';
import {
	getRouteKnotRequestsThroughAppview,
	getTangledAppviewService,
	getForcePds,
} from '../settings';

export interface AppviewRecordView<T> {
	uri: ResourceUri;
	cid?: string | null;
	value: T;
}

export interface AppviewListResponse<T> {
	items: Array<AppviewRecordView<T>>;
	cursor?: string | null;
}

export interface AppviewBulkResponse<T> {
	items: Array<AppviewRecordView<T>>;
}

export interface AppviewStatefulRecordView<T> extends AppviewRecordView<T> {
	state?: string;
	stateUpdatedAt?: string;
	commentCount?: number;
}

export interface AppviewStatefulListResponse<T> {
	items: Array<AppviewStatefulRecordView<T>>;
	cursor?: string | null;
}

export interface AppviewSearchHit<T> extends AppviewRecordView<T> {
	nsid: string;
	score: number;
}

export interface AppviewSearchResponse<T> {
	hits: Array<AppviewSearchHit<T>>;
	cursor?: string | null;
}

interface AppviewCountResponse {
	count: number;
	distinctAuthors?: number;
	distinct_authors?: number;
}

export type AppviewParamValue = string | number | boolean | null | undefined;

export class AppviewResponseError extends Error {
	readonly status: number;

	constructor(status: number, message: string) {
		super(message);
		this.name = 'AppviewResponseError';
		this.status = status;
	}
}

export class AppviewUnavailableError extends Error {
	readonly cause: unknown;

	constructor(cause: unknown) {
		super(cause instanceof Error ? cause.message : 'Appview unavailable');
		this.name = 'AppviewUnavailableError';
		this.cause = cause;
	}
}

export const appviewJson = async <T>(
	nsid: string,
	params: Record<string, AppviewParamValue> = {},
	signal?: AbortSignal,
): Promise<T> => {
	const url = new URL(`/xrpc/${nsid}`, normalizeServiceUrl(getTangledAppviewService()));
	for (const [key, value] of Object.entries(params)) {
		if (value !== undefined && value !== null) {
			url.searchParams.set(key, String(value));
		}
	}

	let response: Response;
	try {
		response = await fetch(url, {
			headers: {
				accept: 'application/json',
			},
			signal,
		});
	} catch (cause) {
		throw new AppviewUnavailableError(cause);
	}

	if (!response.ok) {
		let message = `${response.status} ${response.statusText}`.trim();
		try {
			const body = (await response.json()) as { message?: unknown; error?: unknown };
			const bodyMessage = typeof body.message === 'string' ? body.message : body.error;
			if (typeof bodyMessage === 'string') {
				message = bodyMessage;
			}
		} catch {
			// Keep the HTTP status message when the appview does not return JSON.
		}

		throw new AppviewResponseError(response.status, `Appview ${nsid} failed (${response.status}): ${message}`);
	}

	return (await response.json()) as T;
};

export const appviewJsonUrl = async <T>(url: URL, signal?: AbortSignal): Promise<T> => {
	let response: Response;
	try {
		response = await fetch(url, {
			headers: {
				accept: 'application/json',
			},
			signal,
		});
	} catch (cause) {
		throw new AppviewUnavailableError(cause);
	}

	if (!response.ok) {
		let message = `${response.status} ${response.statusText}`.trim();
		try {
			const body = (await response.json()) as { message?: unknown; error?: unknown };
			const bodyMessage = typeof body.message === 'string' ? body.message : body.error;
			if (typeof bodyMessage === 'string') {
				message = bodyMessage;
			}
		} catch {
			// Keep the HTTP status message when the appview does not return JSON.
		}

		throw new AppviewResponseError(response.status, `Appview ${url.pathname.slice('/xrpc/'.length)} failed (${response.status}): ${message}`);
	}

	return (await response.json()) as T;
};

export const appviewFallback = async <T>(primary: () => Promise<T>, fallback: () => Promise<T>): Promise<T> => {
	if (getForcePds()) {
		return fallback();
	}

	try {
		return await primary();
	} catch (cause) {
		if (!(cause instanceof AppviewUnavailableError)) {
			throw cause;
		}

		return fallback();
	}
};

export const listAllAppviewRecords = async <T>(
	nsid: string,
	subject: string,
	extraParams: Record<string, AppviewParamValue> = {},
): Promise<Array<AppviewRecordView<T>>> => {
	const records: Array<AppviewRecordView<T>> = [];
	let cursor: string | undefined;

	do {
		const page = await appviewJson<AppviewListResponse<T>>(nsid, {
			subject,
			limit: 100,
			cursor,
			...extraParams,
		});
		records.push(...page.items);
		cursor = page.cursor ?? undefined;
	} while (cursor);

	return records;
};

export const appviewBulkRecords = async <T>(
	nsid: string,
	paramName: string,
	uris: ResourceUri[],
): Promise<Array<AppviewRecordView<T>>> => {
	if (uris.length === 0) {
		return [];
	}

	const records: Array<AppviewRecordView<T>> = [];
	for (let index = 0; index < uris.length; index += 50) {
		const url = new URL(`/xrpc/${nsid}`, normalizeServiceUrl(getTangledAppviewService()));
		for (const uri of uris.slice(index, index + 50)) {
			url.searchParams.append(paramName, uri);
		}

		const page = await appviewJsonUrl<AppviewBulkResponse<T>>(url);
		records.push(...page.items);
	}

	return records;
};

export const getAppviewRecordCount = async (nsid: string, subject: string): Promise<number> => {
	const count = await appviewJson<AppviewCountResponse>(nsid, { subject });
	return count.count;
};

export const getDistinctAppviewRecordCount = async (nsid: string, subject: string): Promise<number> => {
	const count = await appviewJson<AppviewCountResponse>(nsid, { subject });
	return count.distinctAuthors ?? count.distinct_authors ?? count.count;
};

export const getRepoViaAppview = async <T>(
	nsid: string,
	repo: { record: { uri: string } },
	params: Record<string, AppviewParamValue> = {},
): Promise<T> =>
	appviewJson<T>(nsid, {
		repo: repo.record.uri,
		...params,
	});

export const appviewOptionalKnotRequest = async <T>(
	appview: () => Promise<T>,
	direct: () => Promise<T>,
): Promise<T> => {
	if (!getRouteKnotRequestsThroughAppview()) {
		return direct();
	}

	return appviewFallback(appview, direct);
};
