/**
 * GET /auth/[provider]/callback
 *
 * Handles the OAuth2 authorization code callback.
 *
 * Flow:
 * 1. Validate state cookie (CSRF check).
 * 2. Exchange code for access token with the provider.
 * 3. Fetch the user's profile (email, name, avatar) from the provider.
 * 4. Look up app_users by (oauth_provider, oauth_id).
 *    - If found: log in.
 *    - If not found but email matches an existing user: link the account.
 *    - If not found at all: auto-create a new account.
 * 5. Set auth cookie, redirect to `next` (default: '/').
 */

import { redirect, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { env } from '$env/dynamic/private';
import { randomBytes } from 'node:crypto';
import {
	getUserByOAuth,
	getUserByEmail,
	createOAuthUser,
	linkOAuthToUser
} from '$lib/server/pocketbase';
import { createAuthToken } from '../../../../hooks.server';
import { upsertUserSession, mergeSessionProgress } from '$lib/server/pocketbase';
import { log } from '$lib/server/logger';

type Provider = 'google' | 'github';

const AUTH_COOKIE = 'libnovel_auth';
const ONE_YEAR = 60 * 60 * 24 * 365;

// ─── Token exchange ───────────────────────────────────────────────────────────

interface TokenResponse {
	access_token: string;
	token_type: string;
	error?: string;
}

async function exchangeCode(
	provider: Provider,
	code: string,
	redirectUri: string
): Promise<string> {
	const clientId = provider === 'google' ? env.GOOGLE_CLIENT_ID : env.GITHUB_CLIENT_ID;
	const clientSecret =
		provider === 'google' ? env.GOOGLE_CLIENT_SECRET : env.GITHUB_CLIENT_SECRET;

	const tokenUrl =
		provider === 'google'
			? 'https://oauth2.googleapis.com/token'
			: 'https://github.com/login/oauth/access_token';

	const res = await fetch(tokenUrl, {
		method: 'POST',
		headers: {
			'Content-Type': 'application/x-www-form-urlencoded',
			Accept: 'application/json'
		},
		body: new URLSearchParams({
			code,
			client_id: clientId ?? '',
			client_secret: clientSecret ?? '',
			redirect_uri: redirectUri,
			grant_type: 'authorization_code'
		}).toString()
	});

	if (!res.ok) {
		const body = await res.text().catch(() => '');
		log.error('oauth', 'token exchange failed', { provider, status: res.status, body });
		throw new Error(`Token exchange failed: ${res.status}`);
	}

	const data = (await res.json()) as TokenResponse;
	if (data.error || !data.access_token) {
		log.error('oauth', 'token response error', { provider, error: data.error });
		throw new Error(data.error ?? 'No access_token in response');
	}
	return data.access_token;
}

// ─── Profile fetching ─────────────────────────────────────────────────────────

interface OAuthProfile {
	id: string; // provider's user ID (as string)
	email: string;
	name: string;
	avatarUrl?: string;
}

async function fetchGoogleProfile(accessToken: string): Promise<OAuthProfile> {
	const res = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
		headers: { Authorization: `Bearer ${accessToken}` }
	});
	if (!res.ok) throw new Error(`Google userinfo failed: ${res.status}`);
	const d = await res.json();
	return {
		id: String(d.id),
		email: d.email ?? '',
		name: d.name ?? d.email ?? '',
		avatarUrl: d.picture
	};
}

async function fetchGitHubProfile(accessToken: string): Promise<OAuthProfile> {
	const [userRes, emailRes] = await Promise.all([
		fetch('https://api.github.com/user', {
			headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/vnd.github+json' }
		}),
		fetch('https://api.github.com/user/emails', {
			headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/vnd.github+json' }
		})
	]);

	if (!userRes.ok) throw new Error(`GitHub user API failed: ${userRes.status}`);
	const user = await userRes.json();

	// Primary verified email — required for account linking
	let email = user.email ?? '';
	if (emailRes.ok) {
		const emails = (await emailRes.json()) as Array<{
			email: string;
			primary: boolean;
			verified: boolean;
		}>;
		const primary = emails.find((e) => e.primary && e.verified);
		if (primary) email = primary.email;
	}

	if (!email) throw new Error('GitHub account has no verified primary email');

	return {
		id: String(user.id),
		email,
		name: user.name ?? user.login ?? email,
		avatarUrl: user.avatar_url
	};
}

// ─── Username derivation ──────────────────────────────────────────────────────

/** Derive a valid username from name/email. Sanitises to [a-zA-Z0-9_-], max 32 chars. */
function deriveUsername(name: string, email: string): string {
	// Prefer the part before @ in the email for predictability
	const base = (email.split('@')[0] ?? name)
		.toLowerCase()
		.replace(/[^a-z0-9_-]/g, '_')
		.replace(/^_+|_+$/g, '')
		.slice(0, 28);
	// Append 4 random hex chars to avoid collisions without needing a DB round-trip
	const suffix = randomBytes(2).toString('hex');
	return `${base || 'user'}_${suffix}`;
}

// ─── Handler ──────────────────────────────────────────────────────────────────

export const GET: RequestHandler = async ({ params, url, cookies, locals, request }) => {
	const provider = params.provider as Provider;
	if (provider !== 'google' && provider !== 'github') {
		error(404, 'Unknown OAuth provider');
	}

	const code = url.searchParams.get('code');
	const state = url.searchParams.get('state');
	const storedState = cookies.get(`oauth_state_${provider}`);
	const next = cookies.get(`oauth_next_${provider}`) ?? '/';

	// Clear short-lived cookies
	cookies.delete(`oauth_state_${provider}`, { path: '/' });
	cookies.delete(`oauth_next_${provider}`, { path: '/' });

	if (!code || !state || state !== storedState) {
		log.warn('oauth', 'state mismatch or missing code', { provider });
		redirect(302, '/login?error=oauth_state');
	}

	const redirectUri = `${url.origin}/auth/${provider}/callback`;

	let profile: OAuthProfile;
	try {
		const accessToken = await exchangeCode(provider, code, redirectUri);
		profile =
			provider === 'google'
				? await fetchGoogleProfile(accessToken)
				: await fetchGitHubProfile(accessToken);
	} catch (err) {
		log.error('oauth', 'profile fetch failed', { provider, err: String(err) });
		redirect(302, '/login?error=oauth_failed');
	}

	if (!profile.email) {
		log.warn('oauth', 'no email in profile', { provider, id: profile.id });
		redirect(302, '/login?error=oauth_no_email');
	}

	// ── Find or create user ────────────────────────────────────────────────────

	let user = await getUserByOAuth(provider, profile.id);

	if (!user) {
		// Try to link by email (user may have registered via the other provider)
		const existing = await getUserByEmail(profile.email);
		if (existing) {
			// Link this provider to the existing account
			await linkOAuthToUser(existing.id, provider, profile.id);
			user = existing;
			log.info('oauth', 'linked provider to existing account', {
				provider,
				userId: existing.id
			});
		} else {
			// Auto-create a new account
			const username = deriveUsername(profile.name, profile.email);
			user = await createOAuthUser(username, profile.email, provider, profile.id, profile.avatarUrl);
			log.info('oauth', 'created new account via oauth', { provider, username });
		}
	}

	// ── Merge anonymous session progress ───────────────────────────────────────
	mergeSessionProgress(locals.sessionId, user.id).catch((err) =>
		log.warn('oauth', 'mergeSessionProgress failed (non-fatal)', { err: String(err) })
	);

	// ── Create / reuse session + auth cookie ─────────────────────────────────
	const userAgent = request.headers.get('user-agent') ?? '';
	const ip =
		request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??
		request.headers.get('x-real-ip') ??
		'';
	const candidateSessionId = randomBytes(16).toString('hex');
	let authSessionId: string;
	try {
		({ authSessionId } = await upsertUserSession(user.id, candidateSessionId, userAgent, ip));
	} catch (err) {
		log.warn('oauth', 'upsertUserSession failed (non-fatal)', { err: String(err) });
		authSessionId = candidateSessionId;
	}

	const token = createAuthToken(user.id, user.username, user.role ?? 'user', authSessionId);
	cookies.set(AUTH_COOKIE, token, {
		path: '/',
		httpOnly: true,
		sameSite: 'lax',
		maxAge: ONE_YEAR
	});

	redirect(302, next);
};
