import { browser } from '$app/environment';
import {
	CompositeDidDocumentResolver,
	LocalActorResolver,
	PlcDidDocumentResolver,
	WebDidDocumentResolver,
	XrpcHandleResolver
} from '@atcute/identity-resolver';
import type { ActorIdentifier, Did } from '@atcute/lexicons/syntax';
import {
	OAuthUserAgent,
	configureOAuth,
	createAuthorizationUrl,
	deleteStoredSession,
	finalizeAuthorization,
	getSession,
	listStoredSessions
} from '@atcute/oauth-browser-client';
import { getContext } from 'svelte';
import { SvelteURL, SvelteURLSearchParams } from 'svelte/reactivity';
import oauthMetadata from '../../static/oauth-client-metadata.json';
import {
	type AuthAccount,
	clearActive,
	dropAccount,
	loadAccounts,
	persistActive,
	readActiveDid,
	reconcileAccounts,
	saveAccounts,
	upsertAccount
} from './auth/accounts';

export const AUTH_KEY = Symbol('auth');
const DEFAULT_APPVIEW_SERVICE = 'https://bobbin.klbr.net';
const DEV_REDIRECT_URI = 'http://127.0.0.1:5173/oauth/callback';
const DEV_CLIENT_ID = `http://localhost?redirect_uri=${encodeURIComponent(DEV_REDIRECT_URI)}&scope=${encodeURIComponent(oauthMetadata.scope)}`;

const identityResolver = new LocalActorResolver({
	handleResolver: new XrpcHandleResolver({
		serviceUrl: 'https://public.api.bsky.app'
	}),
	didDocumentResolver: new CompositeDidDocumentResolver({
		methods: {
			plc: new PlcDidDocumentResolver(),
			web: new WebDidDocumentResolver()
		}
	})
});

let configured = false;

export interface AuthProfile {
	did: Did;
	handle: string;
	avatar?: string;
}

export interface CurrentUser {
	did: Did;
	handle: string;
	avatar?: string;
}

export type { AuthAccount } from './auth/accounts';

export interface Auth {
	readonly agent: OAuthUserAgent | null;
	readonly currentDid: Did | null;
	readonly profile: AuthProfile | null;
	readonly error: string | null;
	readonly profileLoading: boolean;
	readonly authenticating: boolean;
	readonly currentUser: CurrentUser | null;
	readonly accounts: AuthAccount[];
	refresh(): Promise<void>;
	signIn(identifier: string, returnTo?: string): Promise<void>;
	addAccount(identifier: string, returnTo?: string): Promise<void>;
	completeSignIn(): Promise<string>;
	switchAccount(did: Did): Promise<void>;
	removeAccount(did: Did): Promise<void>;
	signOut(): Promise<void>;
	signOutAll(): Promise<void>;
}

type MiniDoc = {
	did: Did;
	handle: string;
	pds?: string;
	avatar?: string;
};

type OAuthSession = ConstructorParameters<typeof OAuthUserAgent>[0];

const ENV_OAUTH_REDIRECT_URI = import.meta.env.VITE_OAUTH_REDIRECT_URI as string | undefined;
const HAS_LOCALHOST_REDIRECT = ENV_OAUTH_REDIRECT_URI?.includes('://localhost') ?? false;
const OAUTH_CLIENT_ID = HAS_LOCALHOST_REDIRECT
	? DEV_CLIENT_ID
	: ((import.meta.env.VITE_OAUTH_CLIENT_ID as string | undefined) ?? DEV_CLIENT_ID);
const OAUTH_REDIRECT_URI = HAS_LOCALHOST_REDIRECT
	? DEV_REDIRECT_URI
	: (ENV_OAUTH_REDIRECT_URI ?? DEV_REDIRECT_URI);
const OAUTH_SCOPE = (import.meta.env.VITE_OAUTH_SCOPE as string | undefined) ?? oauthMetadata.scope;
const APPVIEW_SERVICE =
	(import.meta.env.VITE_TANGLED_APPVIEW_SERVICE as string | undefined)?.replace(/\/+$/, '') ??
	DEFAULT_APPVIEW_SERVICE;

const configure = () => {
	if (!browser || configured) return;

	configureOAuth({
		metadata: {
			client_id: OAUTH_CLIENT_ID,
			redirect_uri: OAUTH_REDIRECT_URI
		},
		identityResolver
	});

	configured = true;
};

const errorMessage = (cause: unknown) => {
	const message = cause instanceof Error ? cause.message : String(cause);
	return message.toLowerCase().includes('unknown state')
		? 'Could not resume OAuth state. In local development, start login from http://127.0.0.1:5173 instead of localhost.'
		: message;
};

const resolveProfile = async (identifier: string): Promise<AuthProfile | null> => {
	try {
		const url = new URL('/xrpc/com.bad-example.identity.resolveMiniDoc', APPVIEW_SERVICE);
		url.searchParams.set('identifier', identifier);
		const response = await fetch(url, { headers: { accept: 'application/json' } });
		if (response.ok) {
			const profile = (await response.json()) as MiniDoc;
			return {
				did: profile.did,
				handle: profile.handle,
				avatar: profile.avatar
			};
		}
	} catch {
		// try local resolver next.
	}

	try {
		const identity = await identityResolver.resolve(identifier as ActorIdentifier);
		return {
			did: identity.did as Did,
			handle: identity.handle
		};
	} catch {
		return null;
	}
};

const returnToFromState = (state: object | null): string => {
	if (state && typeof state === 'object' && 'returnTo' in state) {
		const returnTo = state.returnTo;
		if (typeof returnTo === 'string') return returnTo;
	}
	return '/';
};

export const createAuth = (initial?: { did: string; handle: string } | null): Auth => {
	const seed = initial ?? null;
	let agent = $state<OAuthUserAgent | null>(null);
	let currentDid = $state<Did | null>((seed?.did as Did | undefined) ?? null);
	let profile = $state<AuthProfile | null>(
		seed ? { did: seed.did as Did, handle: seed.handle } : null
	);
	let error = $state<string | null>(null);
	let authenticating = $state(false);
	let accounts = $state<AuthAccount[]>([]);

	// merge atcute's stored sessions with persisted account metadata.
	const syncAccounts = () => {
		accounts = reconcileAccounts(browser ? listStoredSessions() : [], loadAccounts());
		saveAccounts(accounts);
	};

	const resetLoggedOut = () => {
		clearActive();
		agent = null;
		currentDid = null;
		profile = null;
		syncAccounts();
	};

	const hydrateProfile = async (did: Did) => {
		const resolved = await resolveProfile(did);
		profile = resolved ?? { did, handle: did };
		const meta = upsertAccount(loadAccounts(), {
			did,
			handle: profile.handle,
			avatar: profile.avatar,
			addedAt: Math.floor(Date.now() / 1000)
		});
		saveAccounts(meta);
		accounts = reconcileAccounts(listStoredSessions(), meta);
		persistActive(did, profile.handle);
	};

	const adoptSession = (session: OAuthSession) => {
		const nextAgent = new OAuthUserAgent(session);
		agent = nextAgent;
		error = null;
		const did = nextAgent.sub as Did;
		currentDid = did;
		const known = loadAccounts().find((account) => account.did === did);
		persistActive(did, known?.handle ?? did);
		void hydrateProfile(did);
	};

	// prune dead sessions when re-adoption fails.
	const activate = async (did: Did): Promise<boolean> => {
		try {
			const session = await getSession(did, { allowStale: true });
			adoptSession(session);
			return true;
		} catch (cause) {
			deleteStoredSession(did);
			saveAccounts(dropAccount(loadAccounts(), did));
			error = errorMessage(cause);
			return false;
		}
	};

	const refresh = async () => {
		if (!browser) return;
		configure();
		error = null;
		syncAccounts();

		const candidates: Did[] = [];
		for (const candidate of [readActiveDid(), currentDid, ...listStoredSessions()]) {
			if (candidate && !candidates.includes(candidate)) candidates.push(candidate);
		}

		for (const candidate of candidates) {
			if (await activate(candidate)) return;
		}

		resetLoggedOut();
	};

	const signIn = async (identifier: string, returnTo = '/') => {
		if (!browser) return;
		configure();
		error = null;

		const trimmed = identifier.trim();
		if (!trimmed) {
			error = 'Handle or DID required';
			return;
		}

		if (location.hostname === 'localhost') {
			const url = new SvelteURL(location.href);
			url.hostname = '127.0.0.1';
			url.searchParams.set('identifier', trimmed);
			url.searchParams.set('return_url', returnTo);
			location.replace(url);
			return;
		}

		try {
			const url = await createAuthorizationUrl({
				target: {
					type: 'account',
					identifier: trimmed as ActorIdentifier
				},
				scope: OAUTH_SCOPE,
				state: { returnTo }
			});

			window.location.assign(url.toString());
		} catch (cause) {
			error = errorMessage(cause);
			throw cause;
		}
	};

	const completeSignIn = async () => {
		if (!browser) return '/';
		configure();
		error = null;
		authenticating = true;

		try {
			const params = new SvelteURLSearchParams(location.hash.slice(1));
			history.replaceState(null, '', location.pathname + location.search);

			const { session, state } = await finalizeAuthorization(params);
			adoptSession(session);

			return returnToFromState(state);
		} catch (cause) {
			error = errorMessage(cause);
			throw cause;
		} finally {
			authenticating = false;
		}
	};

	const switchAccount = async (did: Did) => {
		if (!browser) return;
		configure();
		error = null;
		if (!(await activate(did))) syncAccounts();
	};

	const removeAccount = async (did: Did) => {
		if (!browser) return;
		error = null;
		const wasActive = currentDid === did;
		try {
			if (wasActive && agent) {
				await agent.signOut();
			} else {
				deleteStoredSession(did);
			}
		} catch {
			deleteStoredSession(did);
		}

		saveAccounts(dropAccount(loadAccounts(), did));
		accounts = reconcileAccounts(listStoredSessions(), loadAccounts());

		if (wasActive) {
			const next = accounts[0]?.did ?? null;
			if (next) {
				await activate(next);
			} else {
				resetLoggedOut();
			}
		}
	};

	const signOut = async () => {
		if (currentDid) {
			await removeAccount(currentDid);
		} else {
			resetLoggedOut();
		}
	};

	const signOutAll = async () => {
		error = null;
		try {
			if (agent) await agent.signOut();
		} catch {
			// remove local session state below.
		}
		if (browser) {
			for (const did of listStoredSessions()) deleteStoredSession(did);
		}
		saveAccounts([]);
		resetLoggedOut();
	};

	return {
		get agent() {
			return agent;
		},
		get currentDid() {
			return currentDid;
		},
		get profile() {
			return profile;
		},
		get error() {
			return error;
		},
		get profileLoading() {
			return currentDid !== null && profile === null;
		},
		get authenticating() {
			return authenticating;
		},
		get currentUser() {
			if (!currentDid) return null;
			return {
				did: currentDid,
				handle: profile?.handle ?? currentDid,
				avatar: profile?.avatar
			};
		},
		get accounts() {
			return accounts;
		},
		refresh,
		signIn,
		addAccount: signIn,
		completeSignIn,
		switchAccount,
		removeAccount,
		signOut,
		signOutAll
	};
};

export const getAuth = () => getContext<Auth>(AUTH_KEY);
