import {
	type AccessToken,
	exchangeCode,
	RefreshingAuthProvider,
} from "@twurple/auth";
import {
	createAuthRecord,
	deleteAuthRecord,
	getAuthRecord,
	updateAuthRecord,
} from "db/dbAuth";
import kleur from "kleur";
import logger from "lib/logger";

async function initAuth(
	userId: string,
	clientId: string,
	clientSecret: string,
	requestedIntents: string[],
	streamer: boolean,
): Promise<AccessToken> {
	const port = process.env.REDIRECT_PORT ?? 3456;
	const redirectURL = process.env.REDIRECT_URL ?? `http://localhost:${port}`;
	// Set the default url and port to http://localhost:3456

	const state = Bun.randomUUIDv7().replace(/-/g, "").slice(0, 32).toUpperCase();
	// Generate random state variable to prevent cross-site-scripting attacks

	const instruction = `Visit this URL as ${kleur
		.red()
		.underline()
		.italic(
			streamer ? "the streamer" : "the chatter",
		)} to authenticate the bot.`;
	logger.info(instruction);
	logger.info(
		`https://id.twitch.tv/oauth2/authorize?client_id=${clientId}&redirect_uri=${redirectURL}&response_type=code&scope=${requestedIntents.join("+")}&state=${state}`,
	);

	const createCodePromise = () => {
		let resolver: (code: string) => void;
		const promise = new Promise<string>((resolve) => {
			resolver = resolve;
		});
		return { promise, resolver: resolver! };
	};

	const { promise: codepromise, resolver } = createCodePromise();

	const server = Bun.serve({
		port,
		fetch(request) {
			const { searchParams } = new URL(request.url);
			if (
				searchParams.has("code") &&
				searchParams.has("state") &&
				searchParams.get("state") === state
			) {
				// Check if the required fields exist on the params and validate the state
				resolver(searchParams.get("code")!);
				return new Response("Successfully authenticated!");
			} else {
				return new Response(
					`Authentication attempt unsuccessful, please make sure the redirect url in the twitch developer console is set to ${redirectURL} and that the bot is listening to that url & port.`,
					{ status: 400 },
				);
			}
		},
	});

	await deleteAuthRecord(userId);

	const code = await codepromise;
	server.stop(false);
	logger.info(`Authentication code received.`);
	const tokenData = await exchangeCode(
		clientId,
		clientSecret,
		code,
		redirectURL,
	);
	logger.info(`Successfully authenticated code.`);
	await createAuthRecord(tokenData, userId);
	logger.ok(`Successfully saved auth data in the database.`);
	return tokenData;
}

export type authProviderInstructions = {
	userId: string;
	intents: string[];
	streamer: boolean;
};

export async function createAuthProvider(
	data: authProviderInstructions[],
): Promise<RefreshingAuthProvider> {
	const clientId = process.env.CLIENT_ID;
	if (!clientId) {
		logger.enverr("CLIENT_ID");
		process.exit(1);
	}

	const clientSecret = process.env.CLIENT_SECRET;
	if (!clientSecret) {
		logger.enverr("CLIENT_SECRET");
		process.exit(1);
	}

	const authData = new RefreshingAuthProvider({
		clientId,
		clientSecret,
	});

	authData.onRefresh(async (user, token) => {
		logger.ok(`Successfully refreshed auth for user ${user}`);
		await updateAuthRecord(user, token);
	});

	authData.onRefreshFailure((user, err) => {
		logger.err(
			`Failed to refresh auth for user ${user}: ${err.name} ${err.message}`,
		);
	});

	for (const user of data) {
		const authRecord = await getAuthRecord(user.userId, user.intents);

		const token = authRecord
			? authRecord.accesstoken
			: await initAuth(
					user.userId,
					clientId,
					clientSecret,
					user.intents,
					user.streamer,
				);

		try {
			await authData.addUserForToken(token, user.intents);
		} catch (_err) {
			logger.err(
				`Failed to setup user auth. Please restart the bot and re-authenticate.`,
			);
			await deleteAuthRecord(user.userId);
			process.exit(1);
		}

		try {
			await authData.refreshAccessTokenForUser(user.userId);
		} catch (_err) {
			logger.err(
				`Failed to refresh user ${user.userId}. Please restart the bot and re-authenticate it. Make sure the user that auths the bot and the user that's defined in .env are the same.`,
			);
			await deleteAuthRecord(user.userId);
			process.exit(1);
		}
	}

	return authData;
}
