import { ok } from '@atcute/client';
import type { OAuthUserAgent } from '@atcute/oauth-browser-client';
import {
	type ActorResolver,
	type ResolveActorOptions,
	type ResolvedActor,
} from '@atcute/identity-resolver';
import type {
	ActorIdentifier,
	Did,
	Handle,
} from '@atcute/lexicons/syntax';
import { ShTangledActorProfile } from '@atcute/tangled';

import {
	APP_BSKY_PROFILE_COLLECTION,
	ACTOR_PROFILE_COLLECTION,
	SLINGSHOT_SERVICE,
} from './constants';
import { appviewJson, AppviewUnavailableError, type AppviewRecordView } from './appview';
import { clearAppviewStatefulCursorCache } from './appview-cache';
import {
	createAuthRpc as createClientAuthRpc,
	getRpc,
	normalizeServiceUrl,
} from './client';
import {
	extractBlobCid,
	getOptionalRecord,
} from './records';

interface MiniDocResponse {
	did: Did;
	handle: Handle;
	pds: string;
}

interface ProfileWithAvatar {
	avatar?: unknown;
}

const actorCache = new Map<string, Promise<ResolvedActor>>();

class AppviewFirstActorResolver implements ActorResolver {
	async resolve(actor: ActorIdentifier, options?: ResolveActorOptions): Promise<ResolvedActor> {
		try {
			return normalizeResolvedActor(
				await appviewJson<MiniDocResponse>(
					'com.bad-example.identity.resolveMiniDoc',
					{ identifier: actor },
					options?.signal,
				),
			);
		} catch (cause) {
			if (!(cause instanceof AppviewUnavailableError)) {
				throw cause;
			}

			const resolved = await ok(
				getRpc(SLINGSHOT_SERVICE).get('blue.microcosm.identity.resolveMiniDoc', {
					params: {
						identifier: actor,
					},
					signal: options?.signal,
				}),
			);

			return normalizeResolvedActor(resolved);
		}
	}
}

export const identityResolver = new AppviewFirstActorResolver();

export const clearApiCaches = () => {
	actorCache.clear();
	clearAppviewStatefulCursorCache();
};

export const createAuthRpc = createClientAuthRpc;

export const resolveActor = async (identifier: string): Promise<ResolvedActor> => {
	const key = identifier.toLowerCase();
	const cached = actorCache.get(key);
	if (cached) {
		return cached;
	}

	const promise = identityResolver.resolve(asActorIdentifier(identifier));
	actorCache.set(key, promise);
	return promise;
};

export const resolveActorForDisplay = async (did: Did): Promise<ResolvedActor> => {
	try {
		return await resolveActor(did);
	} catch {
		return unresolvedActorFromDid(did);
	}
};

const normalizeResolvedActor = (resolved: MiniDocResponse): ResolvedActor => ({
	did: resolved.did,
	handle: resolved.handle,
	pds: normalizeServiceUrl(resolved.pds),
});

const unresolvedActorFromDid = (did: Did): ResolvedActor => ({
	did,
	handle: did as unknown as Handle,
	pds: '',
});

const getActorProfileFromAppview = async (actor: ResolvedActor): Promise<ShTangledActorProfile.Main | null> => {
	const profile = await appviewJson<AppviewRecordView<ShTangledActorProfile.Main>>(
		'sh.tangled.actor.getProfile',
		{
			actor: `at://${actor.did}/${ACTOR_PROFILE_COLLECTION}/self`,
		},
	);
	return profile.value;
};

export const getActorProfile = async (actor: ResolvedActor): Promise<ShTangledActorProfile.Main | null> => {
	try {
		return await getActorProfileFromAppview(actor);
	} catch (cause) {
		if (!(cause instanceof AppviewUnavailableError)) {
			throw cause;
		}

		return await getOptionalRecord<ShTangledActorProfile.Main>(
			actor,
			ACTOR_PROFILE_COLLECTION,
			'self',
		);
	}
};


export const resolveAvatarUrl = async (identifier: string): Promise<string | null> => {
	const actor = await resolveActor(identifier);
	let tangledProfile: ShTangledActorProfile.Main | null;
	try {
		tangledProfile = await getActorProfileFromAppview(actor);
	} catch (cause) {
		if (!(cause instanceof AppviewUnavailableError)) {
			throw cause;
		}

		tangledProfile = await getOptionalRecord<ShTangledActorProfile.Main>(
			actor,
			ACTOR_PROFILE_COLLECTION,
			'self',
		);
	}
	const tangledCid = extractBlobCid(tangledProfile?.avatar);
	if (tangledCid) {
		return buildPdsBlobUrl(actor.pds, actor.did, tangledCid);
	}

	const bskyProfile = await getOptionalRecord<ProfileWithAvatar>(
		actor,
		APP_BSKY_PROFILE_COLLECTION,
		'self',
	);
	const bskyCid = extractBlobCid(bskyProfile?.avatar);
	if (bskyCid) {
		return buildPdsBlobUrl(actor.pds, actor.did, bskyCid);
	}

	return null;
};

const asActorIdentifier = (value: string): ActorIdentifier => value as ActorIdentifier;

const buildPdsBlobUrl = (pds: string, did: Did, cid: string): string => {
	const url = new URL('/xrpc/com.atproto.sync.getBlob', normalizeServiceUrl(pds));
	url.searchParams.set('did', did);
	url.searchParams.set('cid', cid);
	return url.toString();
};

export const putActorProfile = async (
	agent: OAuthUserAgent,
	profile: ShTangledActorProfile.Main
): Promise<void> => {
	const rpc = createAuthRpc(agent);
	await ok(
		rpc.post('com.atproto.repo.putRecord', {
			input: {
				repo: agent.sub,
				collection: ACTOR_PROFILE_COLLECTION,
				rkey: 'self',
				record: profile,
			},
		})
	);
};
