import { createHash, randomBytes } from "node:crypto";
import { eq, sql } from "drizzle-orm";
import type { FastifyInstance, FastifyReply } from "fastify";
import * as client from "openid-client";
import { db } from "../db/client.js";
import { refreshTokens, users } from "../db/schema.js";
import { env } from "../plugins/env.js";
import { applyOpenApiRouteStandards, genericErrorSchema } from "../utils/openapi-route-standards.js";

// =============================================================================
// OIDC Configuration Cache
// =============================================================================
let oidcConfig: client.Configuration | null = null;

async function getOIDCConfig(): Promise<client.Configuration> {
	if (oidcConfig) return oidcConfig;

	if (!env.OIDC_ISSUER_URL || !env.OIDC_CLIENT_ID || !env.OIDC_CLIENT_SECRET) {
		throw new Error("OIDC not configured");
	}

	oidcConfig = await client.discovery(new URL(env.OIDC_ISSUER_URL), env.OIDC_CLIENT_ID, env.OIDC_CLIENT_SECRET);

	return oidcConfig;
}

// =============================================================================
// PKCE Helpers
// =============================================================================
function generateCodeVerifier(): string {
	return randomBytes(32).toString("base64url");
}

function generateCodeChallenge(verifier: string): string {
	return createHash("sha256").update(verifier).digest("base64url");
}

function generateState(): string {
	return randomBytes(16).toString("hex");
}

// =============================================================================
// Helpers
// =============================================================================
function getFrontendUrl(): string {
	return env.CORS_ORIGINS.split(",")[0] || "http://localhost:5173";
}

// =============================================================================
// OIDC Routes
// =============================================================================
export async function oidcRoutes(app: FastifyInstance) {
	applyOpenApiRouteStandards(app, { tag: "auth", protectedByDefault: false });

	if (!env.OIDC_ENABLED) {
		// Register a disabled route that returns an error
		app.get("/auth/oidc/login", { schema: { response: { 400: genericErrorSchema } } }, async (_request, reply) => {
			return reply.status(400).send({ error: "OIDC authentication is not enabled" });
		});
		app.get("/auth/oidc/callback", { schema: { response: { 400: genericErrorSchema } } }, async (_request, reply) => {
			return reply.status(400).send({ error: "OIDC authentication is not enabled" });
		});
		return;
	}

	// ---------------------------------------------------------------------------
	// GET /auth/oidc/login - Initiates OIDC flow
	// ---------------------------------------------------------------------------
	app.get(
		"/auth/oidc/login",
		{
			schema: {
				response: {
					302: { type: "null", description: "Redirect to OIDC provider" },
					500: genericErrorSchema,
				},
			},
		},
		async (request, reply) => {
			try {
				const config = await getOIDCConfig();

				// Generate PKCE values
				const codeVerifier = generateCodeVerifier();
				const codeChallenge = generateCodeChallenge(codeVerifier);
				const state = generateState();

				// Store PKCE verifier and state in signed cookies (short-lived)
				reply.setCookie("oidc_code_verifier", codeVerifier, {
					httpOnly: true,
					secure: env.NODE_ENV === "production",
					sameSite: "lax",
					path: "/",
					maxAge: 600, // 10 minutes
					signed: true,
				});

				reply.setCookie("oidc_state", state, {
					httpOnly: true,
					secure: env.NODE_ENV === "production",
					sameSite: "lax",
					path: "/",
					maxAge: 600,
					signed: true,
				});

				// Build authorization URL
				const redirectUri = env.OIDC_REDIRECT_URI!;
				const scope = env.OIDC_SCOPES;

				const authUrl = client.buildAuthorizationUrl(config, {
					redirect_uri: redirectUri,
					scope,
					state,
					code_challenge: codeChallenge,
					code_challenge_method: "S256",
				});

				return reply.redirect(authUrl.href);
			} catch (err: unknown) {
				request.log.error({ err }, "[OIDC] Login initialization failed");
				return reply.redirect(getFrontendUrl());
			}
		}
	);

	// ---------------------------------------------------------------------------
	// GET /auth/oidc/callback - Handles callback from OIDC provider
	// ---------------------------------------------------------------------------
	app.get<{ Querystring: { code?: string; state?: string; error?: string; error_description?: string } }>(
		"/auth/oidc/callback",
		{
			schema: {
				querystring: {
					type: "object",
					properties: {
						code: { type: "string" },
						state: { type: "string" },
						error: { type: "string" },
						error_description: { type: "string" },
					},
				},
				response: {
					302: { type: "null", description: "Redirect back to frontend" },
				},
			},
		},
		async (request, reply) => {
			const { code, state, error, error_description } = request.query;

			// Handle OIDC provider errors
			if (error) {
				app.log.warn({ error, errorDescription: error_description }, "[OIDC] Provider returned error");
				return reply.redirect(getFrontendUrl());
			}

			if (!code || !state) {
				return reply.redirect(getFrontendUrl());
			}

			// Verify state
			const storedState = request.unsignCookie(request.cookies.oidc_state || "");
			if (!storedState.valid || storedState.value !== state) {
				request.log.warn("[OIDC] State mismatch during callback validation");
				return reply.redirect(getFrontendUrl());
			}

			// Get code verifier
			const storedVerifier = request.unsignCookie(request.cookies.oidc_code_verifier || "");
			if (!storedVerifier.valid || !storedVerifier.value) {
				request.log.warn("[OIDC] Missing/invalid code verifier cookie");
				return reply.redirect(getFrontendUrl());
			}

			try {
				const config = await getOIDCConfig();
				const redirectUri = env.OIDC_REDIRECT_URI!;

				// Exchange code for tokens
				// Build complete callback URL with query parameters for validation
				const callbackUrl = new URL(redirectUri);
				callbackUrl.search = new URLSearchParams(request.query as Record<string, string>).toString();

				const tokens = await client.authorizationCodeGrant(config, callbackUrl, {
					pkceCodeVerifier: storedVerifier.value,
					expectedState: state,
				});

				// Get user info
				const sub = tokens.claims()?.sub;
				if (!sub) {
					request.log.error("[OIDC] Missing sub claim in token response");
					return reply.redirect(getFrontendUrl());
				}
				const userInfo = await client.fetchUserInfo(config, tokens.access_token, sub);

				// Extract username from configured claim
				const usernameClaim = env.OIDC_USERNAME_CLAIM;
				const username =
					(userInfo as Record<string, string>)[usernameClaim] ||
					userInfo.preferred_username ||
					userInfo.email ||
					userInfo.sub;
				const oidcSubject = userInfo.sub;

				if (!username || !oidcSubject) {
					request.log.error(
						{ hasUsername: Boolean(username), hasOidcSubject: Boolean(oidcSubject) },
						"[OIDC] Missing required user info"
					);
					return reply.redirect(getFrontendUrl());
				}

				// Clean cookies
				reply.clearCookie("oidc_code_verifier", { path: "/" });
				reply.clearCookie("oidc_state", { path: "/" });

				// Find or create user
				const user = await findOrCreateOIDCUser(username, oidcSubject, reply);

				if (!user) {
					return reply.redirect(getFrontendUrl());
				}

				// Update last login
				await db.update(users).set({ lastLoginAt: new Date() }).where(eq(users.id, user.id));

				// Issue JWT tokens (same as local auth)
				const accessToken = await generateAccessToken(app, user.id, user.username);
				const { refreshToken, tokenId, expiresAt } = await generateRefreshToken(app, user.id);

				// Store refresh token
				await db.insert(refreshTokens).values({
					userId: user.id,
					tokenId,
					expiresAt,
				});

				// Set cookies (use app's centralized cookie options)
				request.log.debug(
					`[OIDC] Setting auth cookies for username=${user.username}, userId=${user.id}, NODE_ENV=${env.NODE_ENV}, secure=${app.config.cookieOptions.secure}`
				);
				setAuthCookies(app, reply, accessToken, refreshToken);

				// Redirect to frontend dashboard
				// In dev: CORS_ORIGINS contains the frontend URL
				const frontendUrl = env.CORS_ORIGINS.split(",")[0] || "http://localhost:5173";
				return reply.redirect(`${frontendUrl}/dashboard`);
			} catch (err: unknown) {
				request.log.error({ err }, "[OIDC] Callback processing failed");
				return reply.redirect(getFrontendUrl());
			}
		}
	);
}

// =============================================================================
// User Management
// =============================================================================
async function findOrCreateOIDCUser(
	username: string,
	oidcSubject: string,
	_reply: FastifyReply
): Promise<{ id: number; username: string } | null> {
	// First, try to find user by OIDC subject (most reliable)
	const [existingBySubject] = await db.select().from(users).where(eq(users.oidcSubject, oidcSubject));

	if (existingBySubject) {
		return { id: existingBySubject.id, username: existingBySubject.username };
	}

	// Check if username already exists (potential collision)
	const [existingByUsername] = await db.select().from(users).where(sql`lower(${users.username}) = lower(${username})`);

	if (existingByUsername) {
		// Username collision! Check if it's a local user without OIDC linked
		if (existingByUsername.authProvider === "local" && !existingByUsername.oidcSubject) {
			// Local user exists without SSO - link this OIDC account to existing user
			await db.update(users).set({ oidcSubject: oidcSubject }).where(eq(users.id, existingByUsername.id));
			// Linked OIDC to existing local user
			return { id: existingByUsername.id, username: existingByUsername.username };
		} else if (existingByUsername.oidcSubject && existingByUsername.oidcSubject !== oidcSubject) {
			// User already has a DIFFERENT OIDC subject - create new user with suffix
			username = `${username}_sso`;
			// Username collision (different OIDC subject), use suffixed name
		}
	}

	// Check if auto-create is enabled
	if (!env.OIDC_AUTO_CREATE_USERS) {
		// No logger is available in this helper, route-level logs already capture callback failures.
		return null;
	}

	// Create new OIDC user
	const [newUser] = await db
		.insert(users)
		.values({
			username,
			passwordHash: null,
			authProvider: "oidc",
			oidcSubject: oidcSubject,
			isActive: true,
		})
		.returning({ id: users.id, username: users.username });

	// New OIDC user created
	return newUser;
}

// =============================================================================
// JWT Token Generation (reused from auth.ts logic)
// =============================================================================
async function generateAccessToken(app: FastifyInstance, userId: number, username: string): Promise<string> {
	return await app.jwt.sign({ sub: userId, username }, { expiresIn: `${env.ACCESS_TOKEN_TTL_MINUTES}m` });
}

async function generateRefreshToken(
	app: FastifyInstance,
	userId: number
): Promise<{ refreshToken: string; tokenId: string; expiresAt: Date }> {
	const tokenId = randomBytes(32).toString("hex");
	const expiresAt = new Date(Date.now() + env.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000);

	const refreshToken = await app.jwt.sign(
		{ sub: userId, jti: tokenId, type: "refresh" },
		{ expiresIn: `${env.REFRESH_TOKEN_TTL_DAYS}d` }
	);

	return { refreshToken, tokenId, expiresAt };
}

function setAuthCookies(app: FastifyInstance, reply: FastifyReply, accessToken: string, refreshToken: string) {
	// Use the same cookie options as regular auth for consistency
	reply.setCookie("access_token", accessToken, app.config.cookieOptions);
	reply.setCookie("refresh_token", refreshToken, app.config.refreshCookieOptions);
}
