import { ClientResponseError, ok } from '@atcute/client';
import type { ResolvedActor } from '@atcute/identity-resolver';
import type { Did, Nsid, ResourceUri } from '@atcute/lexicons/syntax';

import { getRpc } from './client';

export interface HydratedRecord<T> {
	author: ResolvedActor;
	collection: Nsid;
	rkey: string;
	uri: ResourceUri;
	cid?: string;
	value: T;
}

export interface PaginatedResult<T> {
	items: T[];
	totalCount?: number;
	hasNext: boolean;
}

export interface RecordCreationResult {
	uri: ResourceUri;
	rkey: string;
}

export interface BacklinkRecordRef {
	did: Did;
	collection: Nsid;
	rkey: string;
}

const SORTABLE_BASE32_ALPHABET = '234567abcdefghijklmnopqrstuvwxyz';
const TID_PATTERN = /^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/;

export const timestampFromDate = (value?: string): number => {
	if (!value) {
		return 0;
	}

	const timestamp = new Date(value).getTime();
	return Number.isFinite(timestamp) ? timestamp : 0;
};

export const timestampFromTid = (rkey?: string): number => {
	if (!rkey || !TID_PATTERN.test(rkey)) {
		return 0;
	}

	let micros = 0;
	for (const char of rkey.slice(0, 11)) {
		const value = SORTABLE_BASE32_ALPHABET.indexOf(char);
		if (value < 0) {
			return 0;
		}
		micros = micros * 32 + value;
	}

	return Math.floor(micros / 1000);
};

export const maxTimestamp = (...values: number[]): number =>
	values.reduce((latest, value) => Math.max(latest, value), 0);

export const parseAtUri = (uri: string): { did: Did; collection: Nsid; rkey: string } => {
	if (!uri.startsWith('at://')) {
		throw new Error(`Invalid at-uri: ${uri}`);
	}

	const parts = uri.slice(5).split('/');
	const [did, collection, ...rkey] = parts;
	return {
		did: asDid(did),
		collection: asNsid(collection),
		rkey: rkey.join('/'),
	};
};

export const parseDirectRecordRef = (ref: string, collection: Nsid): ResourceUri | null => {
	let normalized = ref.trim();
	try {
		normalized = decodeURIComponent(normalized);
	} catch {
		// Keep the original route param when it is not percent-encoded.
	}

	if (!normalized.startsWith('at://')) {
		return null;
	}

	try {
		return parseAtUri(normalized).collection === collection ? (normalized as ResourceUri) : null;
	} catch {
		return null;
	}
};

export const extractBlobCid = (blob: unknown): string | null => {
	if (!blob || typeof blob !== 'object') {
		return null;
	}

	const value = blob as Record<string, unknown>;
	const ref = value.ref;

	if (typeof ref === 'string') {
		return ref;
	}

	if (ref && typeof ref === 'object') {
		const maybeLink = (ref as Record<string, unknown>).$link;
		if (typeof maybeLink === 'string') {
			return maybeLink;
		}
	}

	const maybeCid = value.cid;
	return typeof maybeCid === 'string' ? maybeCid : null;
};

export const findNumberedRecord = <T extends { number: number; rkey: string; uri: string }>(
	records: T[],
	ref: string,
): T | undefined => {
	const trimmed = ref.trim();
	if (/^\d+$/.test(trimmed)) {
		const number = Number(trimmed);
		return records.find((record) => record.number === number);
	}

	return records.find(
		(record) =>
			record.rkey === trimmed ||
			record.uri === trimmed ||
			parseAtUri(record.uri).rkey === trimmed,
	);
};

export const optionalString = (value: string): string | undefined => {
	const trimmed = value.trim();
	return trimmed.length > 0 ? trimmed : undefined;
};

const uniq = <T extends string>(values: T[]): T[] | undefined => {
	if (values.length === 0) {
		return undefined;
	}

	return [...new Set(values)];
};

export const extractDids = (input: string): Did[] | undefined =>
	uniq(
		Array.from(
			input.matchAll(/\bdid:[a-z0-9]+:[A-Za-z0-9._:%-]+\b/gi),
			(match) => match[0] as Did,
		),
	);

export const extractAtUris = (input: string): ResourceUri[] | undefined =>
	uniq(
		Array.from(
			input.matchAll(/\bat:\/\/[^\s<>()]+/gi),
			(match) => match[0] as ResourceUri,
		),
	);

export const sortByCreatedAt = <T extends { value: { createdAt?: string } }>(left: T, right: T): number =>
	new Date(left.value.createdAt ?? 0).getTime() - new Date(right.value.createdAt ?? 0).getTime();

export const toNumberMap = <T extends { uri: string; rkey: string; value: { createdAt?: string } }>(
	items: T[],
): Map<string, number> => {
	const ordered = [...items].sort((left, right) => {
		const leftTime = timestampFromDate(left.value.createdAt);
		const rightTime = timestampFromDate(right.value.createdAt);
		if (leftTime !== rightTime) {
			return leftTime - rightTime;
		}

		return left.rkey.localeCompare(right.rkey);
	});

	return new Map(ordered.map((item, index) => [item.uri, index + 1]));
};

export const getOptionalRecord = async <T>(
	actor: ResolvedActor,
	collection: Nsid,
	rkey: string,
): Promise<T | null> => {
	try {
		const record = await ok(
			getRpc(actor.pds).get('com.atproto.repo.getRecord', {
				params: {
					repo: actor.did,
					collection,
					rkey,
				},
			}),
		);

		return record.value as T;
	} catch (cause) {
		if (
			cause instanceof ClientResponseError &&
			(cause.status === 400 || cause.status === 404)
		) {
			return null;
		}

		throw cause;
	}
};

export const asDid = (value: string): Did => {
	if (!value.startsWith('did:')) {
		throw new Error(`Invalid DID: ${value}`);
	}

	return value as Did;
};

export const asNsid = (value: string): Nsid => {
	if (value.split('.').length < 3) {
		throw new Error(`Invalid NSID: ${value}`);
	}

	return value as Nsid;
};
