import type { ComAtprotoRepoGetRecord, ComAtprotoRepoListRecords } from '@atcute/atproto';
import { type Client, ok } from '@atcute/client';
import type { Blob as AtpBlob, Cid, Did, InferInput, Nsid, ResourceUri } from '@atcute/lexicons';
import type { Records } from '@atcute/lexicons/ambient';

/**
 * typed `com.atproto.repo.*` record helpers.
 *
 * re-types generic record values from `unknown` to their concrete shapes via the ambient `Records` map, and
 * centralizes the necessary casts for repo wire shapes.
 */

/** An NSID for which a record type is registered in the ambient `Records` map. */
type RecordType = keyof Records & Nsid;

interface CreateRecordInput<K extends RecordType> {
	repo: Did;
	collection: K;
	record: InferInput<Records[K]>;
	rkey?: string;
	swapCommit?: string;
	validate?: boolean;
}

type CreateRecordOptions<K extends RecordType> = Omit<CreateRecordInput<K>, 'collection' | 'record'> & {
	record: InferInput<Records[K]> & { $type: K };
};

/**
 * creates a repo record via `com.atproto.repo.createRecord`.
 *
 * @param client client to issue the call on (use `pds` client for own-repo writes)
 * @param options record to create
 * @returns create-record response
 */
export const createRecord = async <K extends RecordType>(client: Client, options: CreateRecordOptions<K>) => {
	const input: CreateRecordInput<K> = {
		...options,
		collection: options.record.$type,
	};
	return await ok(client.post('com.atproto.repo.createRecord', { input }));
};

interface PutRecordInput<K extends RecordType> {
	repo: Did;
	collection: K;
	rkey: string;
	record: InferInput<Records[K]>;
	swapCommit?: string;
	swapRecord?: Cid | null;
	validate?: boolean;
}

type PutRecordOptions<K extends RecordType> = Omit<PutRecordInput<K>, 'collection' | 'record'> & {
	record: InferInput<Records[K]> & { $type: K };
};

/**
 * writes a repo record at a known rkey.
 *
 * @param client client to issue the call on
 * @param options record to write
 * @returns put-record response
 */
export const putRecord = async <K extends RecordType>(client: Client, options: PutRecordOptions<K>) => {
	const input: PutRecordInput<K> = {
		...options,
		collection: options.record.$type,
	};
	return await ok(client.post('com.atproto.repo.putRecord', { input }));
};

interface DeleteRecordOptions<K extends RecordType> {
	repo: Did;
	collection: K;
	rkey: string;
	swapCommit?: string;
	swapRecord?: string;
}

/**
 * deletes a repo record via `com.atproto.repo.deleteRecord`.
 *
 * @param client client to issue the call on — the `pds` client for own-repo writes
 * @param options record to delete
 */
export const deleteRecord = async <K extends RecordType>(client: Client, options: DeleteRecordOptions<K>) => {
	await ok(client.post('com.atproto.repo.deleteRecord', { input: options }));
};

interface GetRecordOptions<K extends RecordType> {
	repo: Did;
	collection: K;
	rkey: string;
	cid?: string;
	signal?: AbortSignal;
}

type GetRecordOutput<T> = ComAtprotoRepoGetRecord.$output & { value: T };

/**
 * reads a single repo record via `com.atproto.repo.getRecord`, typed by its `collection`.
 *
 * @param client client to issue the call on — `appview` for another user's record, `pds` for own.
 * @param options record to read.
 * @returns record output, with `value` typed from the `collection`.
 */
export const getRecord = async <K extends RecordType>(
	client: Client,
	options: GetRecordOptions<K>,
): Promise<GetRecordOutput<InferInput<Records[K]>>> => {
	const data = await ok(
		client.get('com.atproto.repo.getRecord', {
			signal: options.signal,
			params: {
				repo: options.repo,
				collection: options.collection,
				rkey: options.rkey,
				cid: options.cid,
			},
		}),
	);

	// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the requested collection pins `value`, which the wire type leaves `unknown`
	return data as GetRecordOutput<InferInput<Records[K]>>;
};

interface ListRecordsOptions<K extends RecordType> {
	repo: Did;
	collection: K;
	cursor?: string;
	limit?: number;
	signal?: AbortSignal;
}

type ListRecordsOutput<T> = Omit<ComAtprotoRepoListRecords.$output, 'records'> & {
	cursor?: string;
	records: { cid: Cid; uri: ResourceUri; value: T }[];
};

/**
 * lists repo records via `com.atproto.repo.listRecords`, typed by their `collection`.
 *
 * @param client client to issue the call on — `appview` for another user's records, `pds` for own
 * @param options records to list
 * @returns list output, with each record's `value` typed from the `collection`
 */
export const listRecords = async <K extends RecordType>(
	client: Client,
	options: ListRecordsOptions<K>,
): Promise<ListRecordsOutput<InferInput<Records[K]>>> => {
	const data = await ok(
		client.get('com.atproto.repo.listRecords', {
			signal: options.signal,
			params: {
				repo: options.repo,
				collection: options.collection,
				cursor: options.cursor,
				limit: options.limit,
			},
		}),
	);

	// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the requested collection pins each `value`, which the wire type leaves `unknown`
	return data as ListRecordsOutput<InferInput<Records[K]>>;
};

/**
 * uploads a blob to the user's repo.
 *
 * @param client the PDS client.
 * @param blob the blob to upload.
 * @param encoding the blob's mime type, sent as a content-type override; defaults to the blob's own type.
 * @returns the resulting blob ref.
 */
export async function uploadBlob(client: Client, blob: Blob, encoding?: string): Promise<AtpBlob> {
	const data = await ok(
		client.post('com.atproto.repo.uploadBlob', {
			headers: encoding ? { 'content-type': encoding } : undefined,
			input: blob,
		}),
	);
	return data.blob;
}
