/**
 * OpenAI Codex auth helper — reads ~/.codex/auth.json
 *
 * The real `codex` CLI writes its auth state to ~/.codex/auth.json after the
 * user authenticates. We simply read that file and, if the token is stale,
 * refresh it against OpenAI's token endpoint.
 *
 * No PKCE flow, no callback server, no browser dance in our code.
 * Users authenticate with the real `codex` CLI; we just consume its output.
 *
 * File shape (verified against ~/.codex/auth.json):
 * {
 *   "auth_mode": "chatgpt" | "apikey",   // lowercase
 *   "OPENAI_API_KEY": string | null,
 *   "tokens": {
 *     "id_token": string,
 *     "access_token": string,
 *     "refresh_token": string,
 *     "account_id": string
 *   },
 *   "last_refresh": string               // ISO timestamp
 * }
 */

// NEVER convert to top-level imports - breaks browser/Vite builds (web-ui)
let _os: typeof import("node:os") | null = null;
let _fs: typeof import("node:fs") | null = null;
if (
	typeof process !== "undefined" &&
	(process.versions?.node || process.versions?.bun)
) {
	import("node:os").then((m) => {
		_os = m;
	});
	import("node:fs").then((m) => {
		_fs = m;
	});
}

import type { OAuthCredentials, OAuthProviderInterface } from "./types.js";

const TOKEN_URL = "https://auth.openai.com/oauth/token";
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";

// Refresh threshold: 1 hour (conservative; the real codex CLI uses a similar window)
const REFRESH_THRESHOLD_MS = 60 * 60 * 1000;

// ============================================================================
// ~/.codex/auth.json types
// ============================================================================

interface CodexAuthFile {
	auth_mode?: string;
	OPENAI_API_KEY?: string | null;
	tokens?: {
		id_token?: string;
		access_token?: string;
		refresh_token?: string;
		account_id?: string;
	};
	last_refresh?: string;
}

// ============================================================================
// File reader
// ============================================================================

function getCodexAuthPath(): string {
	if (!_os) throw new Error("node:os not available");
	return `${_os.homedir()}/.codex/auth.json`;
}

function readCodexAuthFile(): CodexAuthFile {
	if (!_fs) {
		throw new Error(
			"OpenAI Codex auth is only available in Node.js environments",
		);
	}
	const authPath = getCodexAuthPath();
	if (!_fs.existsSync(authPath)) {
		throw new Error(
			`~/.codex/auth.json not found.\n\n` +
				`Authenticate with the real \`codex\` CLI first:\n` +
				`  codex auth login\n\n` +
				`Then re-run your command.`,
		);
	}
	const raw = _fs.readFileSync(authPath, "utf-8");
	return JSON.parse(raw) as CodexAuthFile;
}

// ============================================================================
// Token refresh
// ============================================================================

async function refreshCodexToken(
	refreshToken: string,
): Promise<{ access_token: string; refresh_token: string }> {
	const response = await fetch(TOKEN_URL, {
		method: "POST",
		headers: { "Content-Type": "application/x-www-form-urlencoded" },
		body: new URLSearchParams({
			grant_type: "refresh_token",
			refresh_token: refreshToken,
			client_id: CLIENT_ID,
		}),
		signal: AbortSignal.timeout(30_000),
	});

	if (!response.ok) {
		const text = await response.text().catch(() => "");
		throw new Error(
			`[openai-codex] Token refresh failed: ${response.status} ${text}`,
		);
	}

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

	if (!json.access_token || !json.refresh_token) {
		throw new Error(
			"[openai-codex] Token refresh response missing access_token or refresh_token",
		);
	}

	return { access_token: json.access_token, refresh_token: json.refresh_token };
}

// ============================================================================
// Public API
// ============================================================================

/**
 * Read ~/.codex/auth.json and return the active access token.
 *
 * - For auth_mode "apikey": returns OPENAI_API_KEY directly.
 * - For auth_mode "chatgpt": returns tokens.access_token, refreshing first
 *   if last_refresh is more than REFRESH_THRESHOLD_MS ago.
 *
 * Throws a clear error if the file is missing or malformed.
 */
export async function getCodexAccessToken(): Promise<string> {
	const auth = readCodexAuthFile();
	const mode = (auth.auth_mode ?? "").toLowerCase();

	if (mode === "apikey") {
		const key = auth.OPENAI_API_KEY;
		if (!key) {
			throw new Error(
				`~/.codex/auth.json has auth_mode "apikey" but OPENAI_API_KEY is empty.\n` +
					`Re-authenticate with: codex auth login`,
			);
		}
		return key;
	}

	// Default: ChatGPT OAuth
	const tokens = auth.tokens;
	if (!tokens?.access_token || !tokens?.refresh_token) {
		throw new Error(
			`~/.codex/auth.json is missing OAuth tokens.\n` +
				`Re-authenticate with: codex auth login`,
		);
	}

	// Refresh if stale
	const lastRefresh = auth.last_refresh
		? new Date(auth.last_refresh).getTime()
		: 0;
	const isStale =
		!lastRefresh || Date.now() - lastRefresh > REFRESH_THRESHOLD_MS;

	if (isStale) {
		try {
			const refreshed = await refreshCodexToken(tokens.refresh_token);
			return refreshed.access_token;
		} catch (err) {
			// If refresh fails, fall back to the stored access_token (it may still work)
			console.warn(
				`[openai-codex] Token refresh failed, using stored token: ${err instanceof Error ? err.message : String(err)}`,
			);
		}
	}

	return tokens.access_token;
}

/**
 * Read account_id from ~/.codex/auth.json (required as a request header).
 * Falls back to extracting from the access_token JWT payload if not stored.
 */
export function getCodexAccountId(): string {
	const auth = readCodexAuthFile();
	const accountId = auth.tokens?.account_id;
	if (accountId) return accountId;
	throw new Error(
		`~/.codex/auth.json is missing tokens.account_id.\n` +
			`Re-authenticate with: codex auth login`,
	);
}

/**
 * OAuthProviderInterface shim — kept so oauth/index.ts registry and
 * auth-storage OAuth refresh flow continue to compile.
 *
 * login() is a no-op: users authenticate with the real `codex` CLI.
 * getApiKey() reads the access token from ~/.codex/auth.json.
 * refreshToken() is a no-op: getCodexAccessToken() refreshes inline.
 */
export const openaiCodexOAuthProvider: OAuthProviderInterface = {
	id: "openai-codex",
	name: "ChatGPT Plus/Pro (Codex Subscription)",
	usesCallbackServer: false,

	async login(): Promise<OAuthCredentials> {
		throw new Error(
			`OpenAI Codex login is handled by the real \`codex\` CLI.\n` +
				`Run: codex auth login\n\n` +
				`Then use pi normally — it reads ~/.codex/auth.json automatically.`,
		);
	},

	async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
		// Inline refresh via getCodexAccessToken(); return same shape
		return credentials;
	},

	getApiKey(_credentials: OAuthCredentials): string {
		// Synchronous fallback — callers that need the real token should await
		// getCodexAccessToken() directly. This path is used for legacy callers.
		const auth = readCodexAuthFile();
		const mode = (auth.auth_mode ?? "").toLowerCase();
		if (mode === "apikey") return auth.OPENAI_API_KEY ?? "";
		return auth.tokens?.access_token ?? "";
	},
};
