import { createHash, randomBytes, randomUUID } from "node:crypto";
import { and, eq, lt } from "drizzle-orm";
import { db as prodDb } from "../../db/index.ts";
import { oauthClients, oauthCodes, oauthTokens } from "../../db/schema.ts";

type Db = typeof prodDb;

// ── Client Registration ──────────────────────────────────────────────

export async function registerClient(
	db: Db = prodDb,
	clientName: string,
	redirectUris: string[],
): Promise<{ clientId: string }> {
	const clientId = randomUUID();
	const redirectUrisJson = JSON.stringify(redirectUris);

	await db
		.insert(oauthClients)
		.values({ clientId, clientName, redirectUris: redirectUrisJson });

	return { clientId };
}

export async function getClient(db: Db = prodDb, clientId: string) {
	const [record] = await db
		.select()
		.from(oauthClients)
		.where(eq(oauthClients.clientId, clientId));

	return record ?? null;
}

// ── Authorization Code ───────────────────────────────────────────────

export async function createAuthorizationCode(
	db: Db = prodDb,
	clientId: string,
	userId: number,
	codeChallenge: string,
	codeChallengeMethod: string,
	redirectUri: string,
): Promise<{ code: string }> {
	const code = randomBytes(32).toString("hex");
	const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes

	await db.insert(oauthCodes).values({
		code,
		clientId,
		userId,
		codeChallenge,
		codeChallengeMethod,
		redirectUri,
		expiresAt,
	});

	return { code };
}

export async function exchangeCode(
	db: Db = prodDb,
	code: string,
	codeVerifier: string,
	clientId: string,
	redirectUri: string,
): Promise<{
	accessToken: string;
	refreshToken: string;
	expiresIn: number;
} | null> {
	const [record] = await db
		.select()
		.from(oauthCodes)
		.where(eq(oauthCodes.code, code));

	if (!record) return null;
	if (record.used !== 0) return null;
	if (record.clientId !== clientId) return null;
	if (record.redirectUri !== redirectUri) return null;
	if (record.expiresAt < new Date()) return null;

	// Verify PKCE: SHA-256(verifier) encoded as base64url must match stored challenge
	const computedChallenge = createHash("sha256")
		.update(codeVerifier)
		.digest("base64url");

	if (computedChallenge !== record.codeChallenge) return null;

	// Mark code as used
	await db.update(oauthCodes).set({ used: 1 }).where(eq(oauthCodes.code, code));

	return generateTokens(db, clientId, record.userId);
}

// ── Token Management ─────────────────────────────────────────────────

async function generateTokens(
	db: Db,
	clientId: string,
	userId: number,
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
	const accessToken = randomBytes(32).toString("hex");
	const refreshToken = randomBytes(32).toString("hex");

	const accessTokenHash = createHash("sha256")
		.update(accessToken)
		.digest("hex");
	const refreshTokenHash = createHash("sha256")
		.update(refreshToken)
		.digest("hex");

	const expiresAt = new Date(Date.now() + 3600 * 1000); // 1 hour
	const refreshExpiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days

	await db.insert(oauthTokens).values({
		accessTokenHash,
		refreshTokenHash,
		clientId,
		userId,
		expiresAt,
		refreshExpiresAt,
	});

	return { accessToken, refreshToken, expiresIn: 3600 };
}

export async function verifyAccessToken(
	db: Db = prodDb,
	token: string,
): Promise<{ userId: number } | null> {
	const tokenHash = createHash("sha256").update(token).digest("hex");

	const [record] = await db
		.select()
		.from(oauthTokens)
		.where(eq(oauthTokens.accessTokenHash, tokenHash));

	if (!record) return null;
	if (record.expiresAt < new Date()) return null;

	return { userId: record.userId };
}

export async function refreshAccessToken(
	db: Db = prodDb,
	refreshToken: string,
	clientId: string,
): Promise<{
	accessToken: string;
	refreshToken: string;
	expiresIn: number;
} | null> {
	const tokenHash = createHash("sha256").update(refreshToken).digest("hex");

	const [record] = await db
		.select()
		.from(oauthTokens)
		.where(
			and(
				eq(oauthTokens.refreshTokenHash, tokenHash),
				eq(oauthTokens.clientId, clientId),
			),
		);

	if (!record) return null;
	if (record.refreshExpiresAt < new Date()) return null;

	// Delete old token pair
	await db.delete(oauthTokens).where(eq(oauthTokens.id, record.id));

	return generateTokens(db, clientId, record.userId);
}

// ── Cleanup ──────────────────────────────────────────────────────────

export async function cleanExpiredOAuthData(db: Db = prodDb): Promise<void> {
	const now = new Date();
	await db.delete(oauthCodes).where(lt(oauthCodes.expiresAt, now));
	await db.delete(oauthTokens).where(lt(oauthTokens.expiresAt, now));
}
