import { ok } from '@atcute/client';
import type { OAuthUserAgent } from '@atcute/oauth-browser-client';
import type { ResolvedActor } from '@atcute/identity-resolver';
import type { Did, Nsid, ResourceUri } from '@atcute/lexicons/syntax';
import { ShTangledString, ShTangledFeedStar } from '@atcute/tangled';

import {
	appviewFallback,
	listAllAppviewRecords,
	getDistinctAppviewRecordCount,
	type AppviewRecordView,
} from './appview';
import { getRpc } from './client';
import { STRING_COLLECTION, STAR_COLLECTION, FEED_COMMENT_COLLECTION } from './constants';
import { resolveActor, createAuthRpc } from './identity';
import { parseAtUri, type RecordCreationResult } from './records';
import { appviewRecordToHydrated } from './hydration';

export interface StringRecord {
	uri: ResourceUri;
	cid: string;
	rkey: string;
	value: ShTangledString.Main;
}

export interface StringComment {
	uri: ResourceUri;
	cid?: string;
	rkey: string;
	author: ResolvedActor;
	value: {
		$type: string;
		body: string;
		createdAt: string;
		subject: { uri: string; cid: string };
		replyTo?: { uri: string; cid: string };
	};
}

export interface StringStarSummary {
	count: number;
	isStarred: boolean;
	currentUserStarRkey?: string;
}

const fetchAllStringRecords = async (
	actor: ResolvedActor,
	collection: Nsid,
): Promise<Array<{ uri: ResourceUri; cid: string; value: unknown }>> => {
	const rpc = getRpc(actor.pds);
	const records: Array<{ uri: ResourceUri; cid: string; value: unknown }> = [];
	let cursor: string | undefined;

	do {
		const page = await ok(
			rpc.get('com.atproto.repo.listRecords', {
				params: {
					repo: actor.did,
					collection,
					limit: 100,
					cursor,
					reverse: true,
				},
			}),
		);

		records.push(...page.records);
		cursor = page.cursor;
	} while (cursor);

	return records;
};

const listStringRecordsFromAppview = async (owner: string | ResolvedActor): Promise<StringRecord[]> => {
	const actor = typeof owner === 'string' ? await resolveActor(owner) : owner;
	const records = await listAllAppviewRecords<ShTangledString.Main>('sh.tangled.string.listStrings', actor.did);

	return records.map((record) => ({
		uri: record.uri,
		cid: record.cid ?? '',
		rkey: parseAtUri(record.uri).rkey,
		value: record.value,
	}));
};

const listStringRecordsFromPds = async (owner: string | ResolvedActor): Promise<StringRecord[]> => {
	const actor = typeof owner === 'string' ? await resolveActor(owner) : owner;
	const records = await fetchAllStringRecords(actor, STRING_COLLECTION);

	return records.map((record) => ({
		uri: record.uri,
		cid: record.cid,
		rkey: parseAtUri(record.uri).rkey,
		value: record.value as ShTangledString.Main,
	}));
};

export const listStringRecords = async (owner: string | ResolvedActor): Promise<StringRecord[]> => {
	const records = await appviewFallback(
		() => listStringRecordsFromAppview(owner),
		() => listStringRecordsFromPds(owner),
	);
	if (records.length > 0) {
		return records;
	}

	// Bobbin/Hydrant can return a successful empty list while the user's PDS has
	// string records. Treat an empty appview list as inconclusive and ask the PDS
	// before showing an empty profile.
	try {
		const pdsRecords = await listStringRecordsFromPds(owner);
		return pdsRecords.length > 0 ? pdsRecords : records;
	} catch {
		return records;
	}
};

export const getString = async (
	ownerIdentifier: string,
	rkey: string,
): Promise<{ owner: ResolvedActor; record: StringRecord }> => {
	const owner = await resolveActor(ownerIdentifier);
	const response = await ok(
		getRpc(owner.pds).get('com.atproto.repo.getRecord', {
			params: {
				repo: owner.did,
				collection: STRING_COLLECTION,
				rkey,
			},
		}),
	);

	const record: StringRecord = {
		uri: response.uri,
		cid: response.cid ?? '',
		rkey,
		value: response.value as ShTangledString.Main,
	};
	return { owner, record };
};

export const getStringRecord = async (uri: ResourceUri): Promise<{ owner: ResolvedActor; record: StringRecord }> => {
	const parsed = parseAtUri(uri);
	const owner = await resolveActor(parsed.did);
	const response = await ok(
		getRpc(owner.pds).get('com.atproto.repo.getRecord', {
			params: {
				repo: owner.did,
				collection: STRING_COLLECTION,
				rkey: parsed.rkey,
			},
		}),
	);

	const record: StringRecord = {
		uri: response.uri,
		cid: response.cid ?? '',
		rkey: parsed.rkey,
		value: response.value as ShTangledString.Main,
	};
	return { owner, record };
};


export const deleteString = async (agent: OAuthUserAgent, rkey: string): Promise<void> => {
	const rpc = createAuthRpc(agent);
	await ok(
		rpc.post('com.atproto.repo.deleteRecord', {
			input: {
				repo: agent.sub,
				collection: STRING_COLLECTION,
				rkey,
			},
		}),
	);
};

export const updateString = async (
	agent: OAuthUserAgent,
	rkey: string,
	input: { filename: string; description?: string; contents: string },
	swapRecord?: string,
): Promise<RecordCreationResult> => {
	const rpc = createAuthRpc(agent);
	const record: ShTangledString.Main = {
		$type: 'sh.tangled.string',
		filename: input.filename.trim(),
		description: input.description?.trim() || '',
		contents: input.contents,
		createdAt: new Date().toISOString(),
	};

	const result = await ok(
		rpc.post('com.atproto.repo.putRecord', {
			input: {
				repo: agent.sub,
				collection: STRING_COLLECTION,
				rkey,
				record,
				swapRecord: swapRecord || undefined,
			},
		}),
	);

	return {
		uri: result.uri,
		rkey: parseAtUri(result.uri).rkey,
	};
};

export const createString = async (
	agent: OAuthUserAgent,
	input: { filename: string; description?: string; contents: string },
): Promise<RecordCreationResult> => {
	const rpc = createAuthRpc(agent);
	const record: ShTangledString.Main = {
		$type: 'sh.tangled.string',
		filename: input.filename.trim(),
		description: input.description?.trim() || '',
		contents: input.contents,
		createdAt: new Date().toISOString(),
	};

	const result = await ok(
		rpc.post('com.atproto.repo.createRecord', {
			input: {
				repo: agent.sub,
				collection: STRING_COLLECTION,
				record,
			},
		}),
	);

	return {
		uri: result.uri,
		rkey: parseAtUri(result.uri).rkey,
	};
};


export const listStringComments = async (stringUri: ResourceUri): Promise<StringComment[]> => {
	const records = await listAllAppviewRecords<any>(
		'sh.tangled.feed.listComments',
		stringUri,
	);
	const comments = await Promise.all(records.map(async (record) => {
		const hydrated = await appviewRecordToHydrated<any>(record);
		const val = hydrated.value;
		const bodyText = (val && typeof val === 'object' && val.body && typeof val.body === 'object' && 'text' in val.body)
			? val.body.text
			: (val && typeof val === 'object' && typeof val.body === 'string' ? val.body : '');
		return {
			...hydrated,
			value: {
				$type: 'sh.tangled.feed.comment',
				body: bodyText,
				createdAt: (val && typeof val === 'object' && val.createdAt) || new Date().toISOString(),
				subject: (val && typeof val === 'object' && val.subject) || { uri: stringUri, cid: '' },
				replyTo: val && typeof val === 'object' ? val.replyTo : undefined,
			}
		};
	}));
	comments.sort((a, b) => new Date(a.value.createdAt).getTime() - new Date(b.value.createdAt).getTime());
	return comments;
};

export const createStringComment = async (
	agent: OAuthUserAgent,
	stringUri: ResourceUri,
	stringCid: string,
	body: string,
	replyTo?: { uri: ResourceUri; cid: string },
): Promise<RecordCreationResult> => {
	const rpc = createAuthRpc(agent);
	const record = {
		$type: 'sh.tangled.feed.comment',
		subject: {
			uri: stringUri,
			cid: stringCid,
		},
		body: {
			$type: 'sh.tangled.markup.markdown',
			text: body.trim(),
		},
		createdAt: new Date().toISOString(),
		...(replyTo ? {
			replyTo: {
				uri: replyTo.uri,
				cid: replyTo.cid,
			}
		} : {}),
	};

	const result = await ok(
		rpc.post('com.atproto.repo.createRecord', {
			input: {
				repo: agent.sub,
				collection: FEED_COMMENT_COLLECTION,
				record,
			},
		}),
	);

	return {
		uri: result.uri,
		rkey: parseAtUri(result.uri).rkey,
	};
};

export const getStringStarSummary = async (
	stringUri: ResourceUri,
	viewerDid?: Did | null,
): Promise<StringStarSummary> => {
	const count = await getDistinctAppviewRecordCount('sh.tangled.feed.countStars', stringUri);
	let currentUserStar: AppviewRecordView<ShTangledFeedStar.Main> | undefined;

	if (viewerDid) {
		const stars = await listAllAppviewRecords<ShTangledFeedStar.Main>('sh.tangled.feed.listStarsBy', viewerDid);
		currentUserStar = stars.find((star) => {
			const subject = star.value.subject as { uri?: unknown };
			return subject?.uri === stringUri;
		});
	}

	return {
		count,
		isStarred: !!currentUserStar,
		currentUserStarRkey: currentUserStar ? parseAtUri(currentUserStar.uri).rkey : undefined,
	};
};

export const createStringStar = async (
	agent: OAuthUserAgent,
	stringUri: ResourceUri,
): Promise<RecordCreationResult> => {
	const rpc = createAuthRpc(agent);
	const record: ShTangledFeedStar.Main = {
		$type: 'sh.tangled.feed.star',
		subject: {
			$type: 'sh.tangled.feed.star#string',
			uri: stringUri,
		},
		createdAt: new Date().toISOString(),
	};
	const result = await ok(
		rpc.post('com.atproto.repo.createRecord', {
			input: {
				repo: agent.sub,
				collection: STAR_COLLECTION,
				record,
			},
		}),
	);

	return {
		uri: result.uri,
		rkey: parseAtUri(result.uri).rkey,
	};
};

export const deleteStringStar = async (agent: OAuthUserAgent, rkey: string): Promise<void> => {
	const rpc = createAuthRpc(agent);
	await ok(
		rpc.post('com.atproto.repo.deleteRecord', {
			input: {
				repo: agent.sub,
				collection: STAR_COLLECTION,
				rkey,
			},
		}),
	);
};
