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 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)}`;

// local dev (localinfra) points these at the local pds/plc; defaults are the public network.
const HANDLE_RESOLVER_URL =
	(import.meta.env.VITE_HANDLE_RESOLVER_URL as string | undefined)?.replace(/\/+$/, "") ??
	"https://public.api.bsky.app";
const PLC_DIRECTORY_URL = (import.meta.env.VITE_PLC_DIRECTORY_URL as string | undefined)?.replace(
	/\/+$/,
	""
);

const identityResolver = new LocalActorResolver({
	handleResolver: new XrpcHandleResolver({ serviceUrl: HANDLE_RESOLVER_URL }),
	didDocumentResolver: new CompositeDidDocumentResolver({
		methods: {
			plc: new PlcDidDocumentResolver(PLC_DIRECTORY_URL ? { apiUrl: PLC_DIRECTORY_URL } : {}),
			web: new WebDidDocumentResolver()
		}
	})
});

let configured = false;

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

export interface CurrentUser {
	did: Did;
	handle: 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[];
	bobbinUrl: string;
	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;
};

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 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,
	bobbinUrl: string
): Promise<AuthProfile | null> => {
	try {
		const url = new URL("/xrpc/com.bad-example.identity.resolveMiniDoc", bobbinUrl);
		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
			};
		}
	} 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 = (
	bobbinUrl: string,
	initial?: { did: string; handle: string } | null
): Auth => {
	const seed = initial ?? null;
	let agent = $state<OAuthUserAgent | null>(null);
	const bobbinUrlValue = bobbinUrl;
	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, bobbinUrl);
		profile = resolved ?? { did, handle: did };
		const meta = upsertAccount(loadAccounts(), {
			did,
			handle: profile.handle,
			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 bobbinUrl() {
			return bobbinUrlValue;
		},
		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
			};
		},
		get accounts() {
			return accounts;
		},
		refresh,
		signIn,
		addAccount: signIn,
		completeSignIn,
		switchAccount,
		removeAccount,
		signOut,
		signOutAll
	};
};

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