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

import {
	appviewFallback,
	getDistinctAppviewRecordCount,
	listAllAppviewRecords,
	type AppviewRecordView,
} from './appview';
import {
	findBacklinkRefByDid,
	getBacklinkDids,
} from './backlinks';
import { STAR_COLLECTION } from './constants';
import { getRpc } from './client';
import { createAuthRpc } from './identity';
import {
	parseAtUri,
	type RecordCreationResult,
} from './records';
import type { RepoContext } from './repos';

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

export interface StarRecord {
	uri: ResourceUri;
	cid: string;
	rkey: string;
	value: ShTangledFeedStar.Main;
}

const STAR_BACKLINK_SOURCES = [
	'sh.tangled.feed.star:subject.did',
	'sh.tangled.feed.star:subjectDid',
];

const getRepoStarSummaryFromBacklinks = async (repo: RepoContext, viewerDid?: Did | null): Promise<RepoStarSummary> => {
	const starrers = new Set<Did>();
	const backlinkDidGroups = await Promise.all(STAR_BACKLINK_SOURCES.map((source) => getBacklinkDids(repo.repoDid, source)));

	for (const did of backlinkDidGroups.flat()) {
		starrers.add(did);
	}

	const currentUserStar =
		viewerDid && starrers.has(viewerDid)
			? await findBacklinkRefByDid(repo.repoDid, STAR_BACKLINK_SOURCES, viewerDid)
			: undefined;

	return {
		count: starrers.size,
		isStarred: !!currentUserStar,
		currentUserStarRkey: currentUserStar?.rkey,
	};
};

const getRepoStarCountFromBacklinks = async (repo: RepoContext): Promise<number> => {
	const summary = await getRepoStarSummaryFromBacklinks(repo);
	return summary.count;
};

const appviewStarSubjectDid = (star: ShTangledFeedStar.Main): Did | null => {
	const subject = star.subject as { did?: unknown; subjectDid?: unknown };
	const did = subject.did ?? subject.subjectDid;
	return typeof did === 'string' && did.startsWith('did:') ? (did as Did) : null;
};

const getRepoStarCountFromAppview = async (repo: RepoContext): Promise<number> =>
	getDistinctAppviewRecordCount('sh.tangled.feed.countStars', repo.repoDid);

const listStarRecordsFromAppview = async (actor: { did: Did }): Promise<StarRecord[]> => {
	const stars = await listAllAppviewRecords<ShTangledFeedStar.Main>('sh.tangled.feed.listStarsBy', actor.did);
	return stars.map((star) => ({
		uri: star.uri,
		cid: star.cid ?? '',
		rkey: parseAtUri(star.uri).rkey,
		value: star.value,
	}));
};

const listStarRecordsFromPds = async (actor: { did: Did; pds: string }): Promise<StarRecord[]> => {
	const rpc = getRpc(actor.pds);
	const records: StarRecord[] = [];
	let cursor: string | undefined;

	do {
		const page = await ok(
			rpc.get('com.atproto.repo.listRecords', {
				params: {
					repo: actor.did,
					collection: STAR_COLLECTION,
					limit: 100,
					cursor,
					reverse: true,
				},
			}),
		);
		records.push(...page.records.map((record) => ({
			uri: record.uri,
			cid: record.cid ?? '',
			rkey: parseAtUri(record.uri).rkey,
			value: record.value as ShTangledFeedStar.Main,
		})));
		cursor = page.cursor;
	} while (cursor);

	return records;
};

export const listStarRecordsByActor = async (actor: { did: Did; pds: string }): Promise<StarRecord[]> => {
	const records = await appviewFallback(
		() => listStarRecordsFromAppview(actor),
		() => listStarRecordsFromPds(actor),
	);
	if (records.length > 0) {
		return records;
	}

	// Bobbin/Hydrant can return a successful empty list while the actor's PDS has
	// stars. Use PDS as the authoritative tie-breaker for profile data.
	try {
		const pdsRecords = await listStarRecordsFromPds(actor);
		return pdsRecords.length > 0 ? pdsRecords : records;
	} catch {
		return records;
	}
};

const findCurrentUserStarFromAppview = async (
	repo: RepoContext,
	viewerDid: Did,
): Promise<AppviewRecordView<ShTangledFeedStar.Main> | undefined> => {
	const stars = await listAllAppviewRecords<ShTangledFeedStar.Main>('sh.tangled.feed.listStarsBy', viewerDid);
	return stars.find((star) => appviewStarSubjectDid(star.value) === repo.repoDid);
};

const getRepoStarSummaryFromAppview = async (
	repo: RepoContext,
	viewerDid?: Did | null,
): Promise<RepoStarSummary> => {
	const [count, currentUserStar] = await Promise.all([
		getRepoStarCountFromAppview(repo),
		viewerDid ? findCurrentUserStarFromAppview(repo, viewerDid) : undefined,
	]);

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

export const getRepoStarSummary = async (repo: RepoContext, viewerDid?: Did | null): Promise<RepoStarSummary> =>
	appviewFallback(
		() => getRepoStarSummaryFromAppview(repo, viewerDid),
		() => getRepoStarSummaryFromBacklinks(repo, viewerDid),
	);

export const getRepoStarCount = async (repo: RepoContext): Promise<number> =>
	appviewFallback(
		() => getRepoStarCountFromAppview(repo),
		() => getRepoStarCountFromBacklinks(repo),
	);

export const createRepoStar = async (agent: OAuthUserAgent, repo: RepoContext): Promise<RecordCreationResult> => {
	const rpc = createAuthRpc(agent);
	const record: ShTangledFeedStar.Main = {
		$type: 'sh.tangled.feed.star',
		subject: {
			$type: 'sh.tangled.feed.star#repo',
			did: repo.repoDid,
		},
		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 deleteRepoStar = 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,
			},
		}),
	);
};
