/**
 * Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud).
 * Copied from oh-my-pi's google-antigravity + google-oauth-shared modules.
 */
import type { Server } from "node:http";
import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "@earendil-works/pi-ai";
import { getAntigravityUserAgent } from "./wire.js";

const CLIENT_ID = atob(
	"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
);
const CLIENT_SECRET = atob("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
const CALLBACK_PORT = 51121;
const CALLBACK_PATH = "/oauth-callback";

const SCOPES = [
	"https://www.googleapis.com/auth/cloud-platform",
	"https://www.googleapis.com/auth/userinfo.email",
	"https://www.googleapis.com/auth/userinfo.profile",
	"https://www.googleapis.com/auth/cclog",
	"https://www.googleapis.com/auth/experimentsandconfigs",
];

const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
const TOKEN_URL = "https://oauth2.googleapis.com/token";
const CLOUD_CODE_ASSIST_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
const LOAD_CODE_ASSIST_URL = `${CLOUD_CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist`;
const ONBOARD_USER_URL = `${CLOUD_CODE_ASSIST_ENDPOINT}/v1internal:onboardUser`;
const OPERATIONS_URL = `${CLOUD_CODE_ASSIST_ENDPOINT}/v1internal`;
const FREE_TIER_ID = "free-tier";
const ONBOARD_TIMEOUT_MS = 30_000;
const ONBOARD_POLL_INTERVAL_MS = 1_000;
const REQUEST_TIMEOUT_MS = 30_000;

export const ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA = Object.freeze({
	ideType: "ANTIGRAVITY",
});

type AntigravityCredentials = OAuthCredentials & { projectId: string };

interface LoadCodeAssistResponse {
	currentTier?: { id?: string } | null;
	paidTier?: { id?: string } | null;
	allowedTiers?: { id?: string }[];
	ineligibleTiers?: { tierId?: string; reasonMessage?: string; validationUrl?: string }[];
	cloudaicompanionProject?: string;
}

interface OnboardOperation {
	name?: string;
	done?: boolean;
	error?: { code?: number; message?: string } | null;
	response?: { "@type"?: string; cloudaicompanionProject?: string } | null;
}

/** Marker for provisioning/flow errors that should surface as-is rather than be wrapped. */
class OAuthError extends Error {
	constructor(message: string) {
		super(message);
		this.name = "OAuthError";
	}
}

/** Extract the Google "verify your account" URL from an error body, matching oh-my-pi. */
function extractGoogleValidationUrl(errorBody: string): string | undefined {
	if (!errorBody.includes("VALIDATION_REQUIRED")) return undefined;
	const start = errorBody.indexOf("{");
	if (start === -1) return undefined;
	try {
		const parsed = JSON.parse(errorBody.slice(start)) as {
			error?: { details?: Array<{ reason?: string; metadata?: { validation_url?: string } }> };
		};
		const detail = parsed.error?.details?.find(
			(d) => d.reason === "VALIDATION_REQUIRED" && typeof d.metadata?.validation_url === "string",
		);
		return detail?.metadata?.validation_url;
	} catch {
		return undefined;
	}
}

function formatGoogleValidationRequiredMessage(validationUrl: string, nextAction: string, email?: string): string {
	const account = email ? ` for ${email}` : "";
	return `Account verification required${account}. Visit ${validationUrl} to continue, then ${nextAction}.`;
}

function throwIfLoginCancelled(signal: AbortSignal | undefined): void {
	if (signal?.aborted) {
		throw new Error("Login cancelled");
	}
}

async function oauthFetch(
	url: string,
	init: RequestInit,
	signal: AbortSignal | undefined,
	timeoutMs = REQUEST_TIMEOUT_MS,
): Promise<Response> {
	const timeoutSignal = AbortSignal.timeout(timeoutMs);
	const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
	try {
		return await fetch(url, { ...init, signal: requestSignal });
	} catch (err) {
		if (signal?.aborted) {
			throw new Error("Login cancelled");
		}
		if (timeoutSignal.aborted) {
			throw new OAuthError(`Timed out after ${timeoutMs}ms waiting for ${url}`);
		}
		throw err;
	}
}

function parseLoadCodeAssistResponse(payload: unknown): LoadCodeAssistResponse {
	if (typeof payload !== "object" || payload === null) {
		throw new OAuthError("failed to unmarshal LoadCodeAssistResponse");
	}
	return payload as LoadCodeAssistResponse;
}

function parseOnboardOperation(payload: unknown): OnboardOperation {
	if (typeof payload !== "object" || payload === null) {
		throw new OAuthError("failed to unmarshal OnboardUser operation");
	}
	return payload as OnboardOperation;
}

function extractProjectId(payload: LoadCodeAssistResponse): string | undefined {
	const projectId = payload.cloudaicompanionProject;
	return projectId && projectId.length > 0 ? projectId : undefined;
}

function hasMessageField(payload: LoadCodeAssistResponse, field: "currentTier" | "paidTier"): boolean {
	return payload[field] !== undefined && payload[field] !== null;
}

function isFreeTierAllowed(payload: LoadCodeAssistResponse): boolean {
	return payload.allowedTiers?.some((tier) => tier.id === FREE_TIER_ID) === true;
}

function getFreeTierIneligibility(
	payload: LoadCodeAssistResponse,
): { reasonMessage: string; validationUrl: string | undefined } | undefined {
	const tier = payload.ineligibleTiers?.find((candidate) => candidate.tierId === FREE_TIER_ID);
	if (!tier?.reasonMessage) return undefined;
	return {
		reasonMessage: tier.reasonMessage,
		validationUrl: tier.validationUrl && tier.validationUrl.length > 0 ? tier.validationUrl : undefined,
	};
}

function assertFreeTierEligible(payload: LoadCodeAssistResponse): void {
	if (isFreeTierAllowed(payload)) return;
	const ineligibility = getFreeTierIneligibility(payload);
	if (!ineligibility) return;
	const validation = ineligibility.validationUrl ? `\n${ineligibility.validationUrl}` : "";
	throw new OAuthError(`${ineligibility.reasonMessage}${validation}`);
}

async function requestCloudCodeAssist({
	label,
	url,
	init,
	signal,
	timeoutMs,
}: {
	label: string;
	url: string;
	init: RequestInit;
	signal?: AbortSignal;
	timeoutMs?: number;
}): Promise<unknown> {
	throwIfLoginCancelled(signal);
	const response = await oauthFetch(url, init, signal, timeoutMs);
	if (response.status !== 200) {
		const errorText = await response.text();
		throw new OAuthError(`${label} failed: ${response.status} ${response.statusText}: ${errorText}`);
	}
	return response.json();
}

function cloudCodeAssistHeaders(accessToken: string): Record<string, string> {
	return {
		Authorization: `Bearer ${accessToken}`,
		"Content-Type": "application/json",
		"User-Agent": getAntigravityUserAgent(),
	};
}

async function postLoadCodeAssist(
	accessToken: string,
	body: Record<string, unknown>,
	signal?: AbortSignal,
): Promise<LoadCodeAssistResponse> {
	const payload = await requestCloudCodeAssist({
		label: "loadCodeAssist",
		url: LOAD_CODE_ASSIST_URL,
		init: {
			method: "POST",
			headers: cloudCodeAssistHeaders(accessToken),
			body: JSON.stringify(body),
		},
		signal,
	});
	return parseLoadCodeAssistResponse(payload);
}

async function loadCodeAssist(accessToken: string, signal?: AbortSignal): Promise<LoadCodeAssistResponse> {
	let payload = await postLoadCodeAssist(accessToken, { metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA }, signal);
	const projectId = extractProjectId(payload);
	if (!hasMessageField(payload, "paidTier") && projectId) {
		payload = await postLoadCodeAssist(
			accessToken,
			{ cloudaicompanionProject: projectId, metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA },
			signal,
		);
	}
	return payload;
}

function remainingOnboardTime(deadline: number): number {
	const remaining = deadline - Date.now();
	if (remaining > 0) return remaining;
	throw new OAuthError(`onboardUser timed out after ${ONBOARD_TIMEOUT_MS}ms`);
}

function describeOperationError(error: OnboardOperation["error"]): string {
	if (error?.message) {
		return typeof error.code === "number" ? `${error.code}: ${error.message}` : error.message;
	}
	return JSON.stringify(error) ?? String(error);
}

async function onboardUser(accessToken: string, signal?: AbortSignal): Promise<void> {
	const deadline = Date.now() + ONBOARD_TIMEOUT_MS;
	let operation = parseOnboardOperation(
		await requestCloudCodeAssist({
			label: "onboardUser",
			url: ONBOARD_USER_URL,
			init: {
				method: "POST",
				headers: cloudCodeAssistHeaders(accessToken),
				body: JSON.stringify({ tierId: FREE_TIER_ID, metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA }),
			},
			signal,
			timeoutMs: remainingOnboardTime(deadline),
		}),
	);

	while (true) {
		if (operation.done === true) {
			if (operation.error !== undefined && operation.error !== null) {
				throw new OAuthError(`OnboardUser operation failed: ${describeOperationError(operation.error)}`);
			}
			if (operation.response === undefined || operation.response === null) {
				throw new OAuthError("failed to unmarshal OnboardUserResponse");
			}
			return;
		}

		const waitMs = Math.min(ONBOARD_POLL_INTERVAL_MS, remainingOnboardTime(deadline));
		await new Promise((resolve) => setTimeout(resolve, waitMs));
		throwIfLoginCancelled(signal);
		const operationName = operation.name ?? "";
		if (operationName.length === 0) {
			throw new OAuthError("onboardUser returned an operation without a name");
		}
		operation = parseOnboardOperation(
			await requestCloudCodeAssist({
				label: "onboardUser operation",
				url: `${OPERATIONS_URL}/${operationName}`,
				init: { method: "GET", headers: cloudCodeAssistHeaders(accessToken) },
				signal,
				timeoutMs: remainingOnboardTime(deadline),
			}),
		);
	}
}

async function discoverProject(
	accessToken: string,
	onProgress?: (message: string) => void,
	signal?: AbortSignal,
): Promise<string> {
	onProgress?.("Checking Cloud Code Assist account status...");
	try {
		const initial = await loadCodeAssist(accessToken, signal);
		assertFreeTierEligible(initial);
		if (!hasMessageField(initial, "currentTier")) {
			onProgress?.("Provisioning the Antigravity free tier...");
			await onboardUser(accessToken, signal);
		}

		onProgress?.("Refreshing Cloud Code Assist project...");
		const refreshed = await loadCodeAssist(accessToken, signal);
		const projectId = extractProjectId(refreshed);
		if (projectId) return projectId;
		throw new OAuthError("loadCodeAssist did not return a cloudaicompanionProject");
	} catch (error) {
		throwIfLoginCancelled(signal);
		if (error instanceof OAuthError || (error instanceof Error && error.message === "Login cancelled")) {
			throw error;
		}
		throw new Error(
			`Could not discover an Antigravity project. ${error instanceof Error ? error.message : String(error)}`,
		);
	}
}

async function getUserEmail(accessToken: string, signal?: AbortSignal): Promise<string | undefined> {
	try {
		const response = await oauthFetch(
			"https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
			{ headers: { Authorization: `Bearer ${accessToken}` } },
			signal,
		);
		if (response.ok) {
			const data = (await response.json()) as { email?: string };
			return data.email;
		}
	} catch {
		// Email is optional.
	}
	return undefined;
}

export async function refreshAntigravityToken(refreshToken: string, projectId: string): Promise<OAuthCredentials> {
	const response = await fetch(TOKEN_URL, {
		method: "POST",
		headers: { "Content-Type": "application/x-www-form-urlencoded" },
		body: new URLSearchParams({
			client_id: CLIENT_ID,
			client_secret: CLIENT_SECRET,
			refresh_token: refreshToken,
			grant_type: "refresh_token",
		}),
	});

	if (!response.ok) {
		const error = await response.text();
		throw new Error(`Antigravity token refresh failed: ${error}`);
	}

	const data = (await response.json()) as {
		access_token: string;
		expires_in: number;
		refresh_token?: string;
	};

	return {
		refresh: data.refresh_token || refreshToken,
		access: data.access_token,
		expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
		projectId,
	};
}

const OAUTH_SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Antigravity</title></head>
<body style="font-family: system-ui, sans-serif; padding: 40px;">
<h2>Authentication complete</h2><p>You can close this window and return to Prime.</p>
</body></html>`;

const OAUTH_ERROR_HTML = (
	message: string,
) => `<!doctype html><html><head><meta charset="utf-8"><title>Antigravity</title></head>
<body style="font-family: system-ui, sans-serif; padding: 40px;">
<h2>Authentication failed</h2><p>${message.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}</p>
</body></html>`;

interface CallbackResult {
	code: string;
	state: string;
}

type CallbackOutcome = CallbackResult | { error: string } | null;

const CALLBACK_WAIT_TIMEOUT_MS = 300_000;

async function startCallbackServer(
	expectedState: string,
	signal?: AbortSignal,
): Promise<{
	redirectUri: string;
	server: Server;
	waitForCallback: Promise<CallbackOutcome>;
	cancelWait: () => void;
}> {
	const { createServer } = await import("node:http");
	const host = "127.0.0.1";

	const bind = (port: number): Promise<Server> =>
		new Promise((resolve, reject) => {
			const server = createServer();
			server.once("error", reject);
			server.listen(port, host, () => {
				server.removeListener("error", reject);
				resolve(server);
			});
		});

	let server: Server;
	let port: number;
	try {
		server = await bind(CALLBACK_PORT);
		port = CALLBACK_PORT;
	} catch (error) {
		const code = (error as { code?: string })?.code;
		if (code !== "EADDRINUSE") throw error;
		server = await bind(0);
		const address = server.address();
		if (address === null || typeof address === "string") throw new Error("Failed to bind OAuth callback server");
		port = address.port;
	}

	let settleWait: ((value: CallbackOutcome) => void) | undefined;
	const waitForCallback = new Promise<CallbackOutcome>((resolve) => {
		let settled = false;
		settleWait = (value) => {
			if (settled) return;
			settled = true;
			resolve(value);
		};
	});

	server.on("request", (req, res) => {
		const url = new URL(req.url || "", `http://${host}:${port}`);
		if (url.pathname !== CALLBACK_PATH) {
			res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
			res.end(OAUTH_ERROR_HTML("Callback route not found."));
			return;
		}

		const code = url.searchParams.get("code");
		const state = url.searchParams.get("state") ?? "";
		const error = url.searchParams.get("error") ?? "";
		const errorDescription = url.searchParams.get("error_description") ?? error;

		if (error) {
			res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
			res.end(OAUTH_ERROR_HTML(`Authorization failed: ${errorDescription}`));
			// The redirect carries our state nonce, so it came from the genuine
			// authorization flow (e.g. the user denied consent). Surface it now
			// instead of leaving the login waiting for the callback timeout.
			if (!expectedState || state === expectedState) {
				settleWait?.({ error: `Authorization failed: ${errorDescription}` });
			}
			return;
		}
		if (!code) {
			res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
			res.end(OAUTH_ERROR_HTML("Missing authorization code"));
			return;
		}

		res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
		res.end(OAUTH_SUCCESS_HTML);
		settleWait?.({ code, state });
	});

	const cancelWait = () => settleWait?.(null);

	const timeout = setTimeout(() => {
		settleWait?.({ error: "OAuth login timed out waiting for browser authentication" });
		server.close();
	}, CALLBACK_WAIT_TIMEOUT_MS);
	timeout.unref();

	if (signal) {
		signal.addEventListener(
			"abort",
			() => {
				clearTimeout(timeout);
				cancelWait();
				server.close();
			},
			{ once: true },
		);
	}

	return { redirectUri: `http://${host}:${port}${CALLBACK_PATH}`, server, waitForCallback, cancelWait };
}

function generateState(): string {
	const bytes = new Uint8Array(16);
	crypto.getRandomValues(bytes);
	return Array.from(bytes)
		.map((value) => value.toString(16).padStart(2, "0"))
		.join("");
}

function parseCallbackInput(input: string): { code?: string; state?: string } {
	const value = input.trim();
	if (!value) return {};
	try {
		const url = new URL(value);
		return { code: url.searchParams.get("code") ?? undefined, state: url.searchParams.get("state") ?? undefined };
	} catch {
		// not a URL
	}
	if (value.includes("code=")) {
		const params = new URLSearchParams(value.replace(/^[?#]/, ""));
		return { code: params.get("code") ?? undefined, state: params.get("state") ?? undefined };
	}
	const [code, state] = value.split("#", 2);
	return { code, state };
}

async function loginAntigravity(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
	const signal = callbacks.signal;
	const state = generateState();
	throwIfLoginCancelled(signal);

	callbacks.onProgress?.("Starting local server for OAuth callback...");
	const { redirectUri, server, waitForCallback, cancelWait } = await startCallbackServer(state, signal);

	try {
		throwIfLoginCancelled(signal);

		const authParams = new URLSearchParams({
			client_id: CLIENT_ID,
			response_type: "code",
			redirect_uri: redirectUri,
			scope: SCOPES.join(" "),
			state,
			access_type: "offline",
			prompt: "consent",
		});
		const authUrl = `${AUTH_URL}?${authParams.toString()}`;

		callbacks.onAuth({ url: authUrl, instructions: "Complete the sign-in in your browser." });
		callbacks.onProgress?.("Waiting for browser authentication...");

		let code: string | undefined;
		const manualInput = callbacks.onManualCodeInput;
		if (manualInput) {
			let manualResult: CallbackResult | undefined;
			void manualInput()
				.then((input) => {
					const parsed = parseCallbackInput(input);
					if (parsed.code) {
						manualResult = { code: parsed.code, state: parsed.state ?? "" };
					}
					cancelWait();
				})
				.catch(() => {
					cancelWait();
				});

			const result = await waitForCallback;
			if (result && "error" in result) {
				throw new OAuthError(result.error);
			}
			if (result?.code) {
				if (result.state !== state) throw new OAuthError("OAuth state mismatch - possible CSRF attack");
				code = result.code;
			} else if (manualResult?.code) {
				if (manualResult.state && manualResult.state !== state) {
					throw new OAuthError("OAuth state mismatch - possible CSRF attack");
				}
				code = manualResult.code;
			}
		} else {
			const result = await waitForCallback;
			if (result && "error" in result) {
				throw new OAuthError(result.error);
			}
			if (result?.code) {
				if (result.state !== state) throw new OAuthError("OAuth state mismatch - possible CSRF attack");
				code = result.code;
			}
		}

		if (!code) {
			throwIfLoginCancelled(signal);
			throw new Error("No authorization code received");
		}

		callbacks.onProgress?.("Exchanging authorization code for tokens...");
		const tokenResponse = await oauthFetch(
			TOKEN_URL,
			{
				method: "POST",
				headers: { "Content-Type": "application/x-www-form-urlencoded" },
				body: new URLSearchParams({
					client_id: CLIENT_ID,
					client_secret: CLIENT_SECRET,
					code,
					grant_type: "authorization_code",
					redirect_uri: redirectUri,
				}),
			},
			signal,
		);

		if (!tokenResponse.ok) {
			const error = await tokenResponse.text();
			throw new Error(`Token exchange failed: ${error}`);
		}

		const tokenData = (await tokenResponse.json()) as {
			access_token: string;
			refresh_token: string;
			expires_in: number;
		};

		if (!tokenData.refresh_token) {
			throw new Error("No refresh token received. Please try again.");
		}

		throwIfLoginCancelled(signal);
		callbacks.onProgress?.("Getting user info...");
		const email = await getUserEmail(tokenData.access_token, signal);

		let projectId: string;
		try {
			projectId = await discoverProject(tokenData.access_token, callbacks.onProgress, signal);
		} catch (error) {
			const validationUrl = extractGoogleValidationUrl(error instanceof Error ? error.message : String(error));
			if (!validationUrl) throw error;
			throw new OAuthError(formatGoogleValidationRequiredMessage(validationUrl, "sign in again", email));
		}

		return {
			refresh: tokenData.refresh_token,
			access: tokenData.access_token,
			expires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,
			projectId,
			email,
		};
	} finally {
		server.close();
	}
}

export const antigravityOAuth: Omit<OAuthProviderInterface, "id"> = {
	name: "Antigravity (Gemini 3, Claude, GPT-OSS)",
	usesCallbackServer: true,

	async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
		return loginAntigravity(callbacks);
	},

	async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
		const creds = credentials as AntigravityCredentials;
		if (!creds.projectId) {
			throw new Error("Antigravity credentials missing projectId");
		}
		return refreshAntigravityToken(creds.refresh, creds.projectId);
	},

	getApiKey(credentials: OAuthCredentials): string {
		const creds = credentials as AntigravityCredentials;
		return JSON.stringify({ token: creds.access, projectId: creds.projectId, email: creds.email });
	},
};
