import * as arctic from "arctic";
import type { Env, OAuthProvider } from "./env.js";

export type OAuthProfile = {
	provider: OAuthProvider;
	subject: string;
	email: string;
};

type ProviderClient = {
	createAuthorizationURL: (state: string, codeVerifier: string) => URL;
	validateAuthorizationCode: (
		code: string,
		codeVerifier: string,
	) => Promise<{ accessToken: () => string }>;
	profile: (accessToken: string) => Promise<{ subject: string; email: string }>;
};

function redirectUri(env: Env, provider: OAuthProvider): string {
	return `${env.appOrigin}/v1/auth/${provider}/callback`;
}

function googleClient(env: Env): ProviderClient | undefined {
	const creds = env.oauth.google;
	if (!creds) return undefined;
	const google = new arctic.Google(
		creds.clientId,
		creds.clientSecret,
		redirectUri(env, "google"),
	);
	return {
		createAuthorizationURL: (state, codeVerifier) =>
			google.createAuthorizationURL(state, codeVerifier, [
				"openid",
				"profile",
				"email",
			]),
		validateAuthorizationCode: (code, codeVerifier) =>
			google.validateAuthorizationCode(code, codeVerifier),
		profile: async (accessToken) => {
			const response = await fetch(
				"https://openidconnect.googleapis.com/v1/userinfo",
				{ headers: { authorization: `Bearer ${accessToken}` } },
			);
			if (!response.ok) throw new Error("google userinfo failed");
			const data = (await response.json()) as {
				sub?: string;
				email?: string;
			};
			if (!data.sub || !data.email)
				throw new Error("google profile incomplete");
			return { subject: data.sub, email: data.email };
		},
	};
}

function microsoftClient(env: Env): ProviderClient | undefined {
	const creds = env.oauth.microsoft;
	if (!creds) return undefined;
	const entra = new arctic.MicrosoftEntraId(
		"common",
		creds.clientId,
		creds.clientSecret,
		redirectUri(env, "microsoft"),
	);
	return {
		createAuthorizationURL: (state, codeVerifier) => {
			const url = entra.createAuthorizationURL(state, codeVerifier, [
				"openid",
				"profile",
				"email",
			]);
			url.searchParams.set("nonce", "_");
			return url;
		},
		validateAuthorizationCode: (code, codeVerifier) =>
			entra.validateAuthorizationCode(code, codeVerifier),
		profile: async (accessToken) => {
			const response = await fetch("https://graph.microsoft.com/v1.0/me", {
				headers: { authorization: `Bearer ${accessToken}` },
			});
			if (!response.ok) throw new Error("microsoft profile failed");
			const data = (await response.json()) as {
				id?: string;
				mail?: string;
				userPrincipalName?: string;
			};
			const email = data.mail ?? data.userPrincipalName;
			if (!data.id || !email) throw new Error("microsoft profile incomplete");
			return { subject: data.id, email };
		},
	};
}

function githubClient(env: Env): ProviderClient | undefined {
	const creds = env.oauth.github;
	if (!creds) return undefined;
	const github = new arctic.GitHub(
		creds.clientId,
		creds.clientSecret,
		redirectUri(env, "github"),
	);
	return {
		createAuthorizationURL: (state) =>
			github.createAuthorizationURL(state, ["user:email"]),
		validateAuthorizationCode: (code) => github.validateAuthorizationCode(code),
		profile: async (accessToken) => {
			const headers = {
				authorization: `Bearer ${accessToken}`,
				accept: "application/vnd.github+json",
				"user-agent": "circuy",
			};
			const userResponse = await fetch("https://api.github.com/user", {
				headers,
			});
			if (!userResponse.ok) throw new Error("github profile failed");
			const user = (await userResponse.json()) as {
				id?: number;
				email?: string | null;
			};
			if (!user.id) throw new Error("github profile incomplete");

			let email = user.email ?? undefined;
			if (!email) {
				const emailsResponse = await fetch(
					"https://api.github.com/user/emails",
					{ headers },
				);
				if (!emailsResponse.ok) throw new Error("github emails failed");
				const emails = (await emailsResponse.json()) as Array<{
					email: string;
					primary: boolean;
					verified: boolean;
				}>;
				email = emails.find((item) => item.primary && item.verified)?.email;
			}
			if (!email) throw new Error("github profile incomplete");
			return { subject: String(user.id), email };
		},
	};
}

export function oauthClient(
	env: Env,
	provider: OAuthProvider,
): ProviderClient | undefined {
	if (provider === "google") return googleClient(env);
	if (provider === "microsoft") return microsoftClient(env);
	return githubClient(env);
}

export function generateOAuthState(): string {
	return arctic.generateState();
}

export function generateOAuthVerifier(): string {
	return arctic.generateCodeVerifier();
}

export const oauthProviders = ["google", "microsoft", "github"] as const;

export function parseOAuthProvider(value: string): OAuthProvider | undefined {
	return oauthProviders.find((provider) => provider === value);
}
