/**
 * GitHub Copilot OAuth flow
 */

import { GITHUB_COPILOT_MODELS } from "../../providers/github-copilot.models.ts";
import { sleep } from "../../utils/sleep.ts";
import type { OAuthAuth, OAuthCredential, ProviderAuthInteraction } from "../types.ts";
import { pollOAuthDeviceCodeFlow } from "./device-code.ts";

const decode = (s: string) => atob(s);
const CLIENT_ID = decode("SXYxLmI1MDdhMDhjODdlY2ZlOTg=");

const COPILOT_HEADERS = {
	"User-Agent": "GitHubCopilotChat/0.35.0",
	"Editor-Version": "vscode/1.107.0",
	"Editor-Plugin-Version": "copilot-chat/0.35.0",
	"Copilot-Integration-Id": "vscode-chat",
} as const;
const COPILOT_API_VERSION = "2026-06-01";

type DeviceCodeResponse = {
	device_code: string;
	user_code: string;
	verification_uri: string;
	interval?: number;
	expires_in: number;
};

type DeviceTokenSuccessResponse = {
	access_token: string;
	token_type?: string;
	scope?: string;
};

type DeviceTokenErrorResponse = {
	error: string;
	error_description?: string;
	interval?: number;
};

function normalizeDomain(input: string): string | null {
	const trimmed = input.trim();
	if (!trimmed) return null;
	try {
		const url = trimmed.includes("://") ? new URL(trimmed) : new URL(`https://${trimmed}`);
		return url.hostname;
	} catch {
		return null;
	}
}

function getUrls(domain: string): {
	deviceCodeUrl: string;
	accessTokenUrl: string;
	copilotTokenUrl: string;
} {
	return {
		deviceCodeUrl: `https://${domain}/login/device/code`,
		accessTokenUrl: `https://${domain}/login/oauth/access_token`,
		copilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`,
	};
}

/**
 * Parse the proxy-ep from a Copilot token and convert to API base URL.
 * Token format: tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...
 * Returns API URL like https://api.individual.githubcopilot.com
 */
function getBaseUrlFromToken(token: string): string | null {
	const match = token.match(/proxy-ep=([^;]+)/);
	if (!match) return null;
	const proxyHost = match[1];
	// Convert proxy.xxx to api.xxx
	const apiHost = proxyHost.replace(/^proxy\./, "api.");
	return `https://${apiHost}`;
}

function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {
	// If we have a token, extract the base URL from proxy-ep
	if (token) {
		const urlFromToken = getBaseUrlFromToken(token);
		if (urlFromToken) return urlFromToken;
	}
	// Fallback for enterprise or if token parsing fails
	if (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;
	return "https://api.individual.githubcopilot.com";
}

function asRecord(value: unknown): Record<string, unknown> | undefined {
	return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
}

function parseGitHubCopilotModelCatalog(raw: unknown, allowPolicyFallback: boolean) {
	const data = asRecord(raw)?.data;
	if (!Array.isArray(data)) {
		throw new Error("Invalid Copilot models response");
	}

	const accountModels = data.flatMap((rawItem) => {
		const item = asRecord(rawItem);
		const id = item?.id;
		if (!item || typeof id !== "string") return [];

		const capabilities = asRecord(item.capabilities);
		const supports = asRecord(capabilities?.supports);
		if (supports?.tool_calls === false) return [];

		return [
			{
				id,
				pickerEnabled: item.model_picker_enabled === true,
				policyState: asRecord(item.policy)?.state,
			},
		];
	});
	const pickerModelIds = accountModels
		.filter((model) => model.pickerEnabled && model.policyState !== "disabled")
		.map((model) => model.id);
	const usePolicyFallback = allowPolicyFallback && pickerModelIds.length === 0;
	const availableModelIds =
		pickerModelIds.length > 0 || !allowPolicyFallback
			? pickerModelIds
			: accountModels.filter((model) => model.policyState === "enabled").map((model) => model.id);
	const policyModelIds = accountModels
		.filter(
			(model) =>
				model.policyState === "unconfigured" &&
				Object.hasOwn(GITHUB_COPILOT_MODELS, model.id) &&
				(model.pickerEnabled || usePolicyFallback),
		)
		.map((model) => model.id);
	return { availableModelIds, policyModelIds };
}

async function fetchWithRateLimitRetry(
	url: string,
	init: RequestInit,
	signal: AbortSignal,
	retryPolicy: { maxRetries: number; maxElapsedMs: number },
): Promise<Response> {
	const retryBudgetSignal =
		retryPolicy.maxRetries > 0 && retryPolicy.maxElapsedMs > 0
			? AbortSignal.timeout(retryPolicy.maxElapsedMs)
			: undefined;
	const requestSignal = retryBudgetSignal ? AbortSignal.any([signal, retryBudgetSignal]) : signal;
	const retryDeadline = retryBudgetSignal ? Date.now() + retryPolicy.maxElapsedMs : undefined;
	for (let retry = 0; ; retry++) {
		const response = await fetch(url, {
			...init,
			signal: AbortSignal.any([requestSignal, AbortSignal.timeout(5000)]),
		});
		if (response.status !== 429 || retry === retryPolicy.maxRetries) return response;

		const retryAfter = response.headers.get("retry-after");
		let delayMs = 500 * 2 ** retry;
		if (retryAfter) {
			const seconds = Number.parseFloat(retryAfter);
			delayMs = Number.isNaN(seconds) ? Date.parse(retryAfter) - Date.now() : seconds * 1000;
			if (!Number.isFinite(delayMs)) return response;
		}
		delayMs = Math.max(0, delayMs);
		if (retryDeadline !== undefined && delayMs >= retryDeadline - Date.now()) return response;
		await response.body?.cancel();
		await sleep(delayMs, requestSignal);
	}
}

async function fetchGitHubCopilotModels(
	copilotToken: string,
	enterpriseDomain: string | undefined,
	signal: AbortSignal,
	retryPolicy: { maxRetries: number; maxElapsedMs: number },
) {
	const baseUrl = getGitHubCopilotBaseUrl(copilotToken, enterpriseDomain);
	// Some Individual accounts return false for every picker flag despite explicit enabled policies.
	// Limit the fallback to that endpoint so other account types keep strict picker semantics.
	const allowPolicyFallback = baseUrl === "https://api.individual.githubcopilot.com";
	const response = await fetchWithRateLimitRetry(
		`${baseUrl}/models`,
		{
			headers: {
				Accept: "application/json",
				Authorization: `Bearer ${copilotToken}`,
				...COPILOT_HEADERS,
				"X-GitHub-Api-Version": COPILOT_API_VERSION,
			},
		},
		signal,
		retryPolicy,
	);
	if (!response.ok) {
		throw new Error(`${response.status} ${response.statusText}: ${await response.text()}`);
	}
	return parseGitHubCopilotModelCatalog(await response.json(), allowPolicyFallback);
}

async function fetchJson(url: string, init: RequestInit): Promise<unknown> {
	const response = await fetch(url, init);
	if (!response.ok) {
		const text = await response.text();
		throw new Error(`${response.status} ${response.statusText}: ${text}`);
	}
	return response.json();
}

async function startDeviceFlow(domain: string, signal: AbortSignal): Promise<DeviceCodeResponse> {
	const urls = getUrls(domain);
	const data = await fetchJson(urls.deviceCodeUrl, {
		method: "POST",
		headers: {
			Accept: "application/json",
			"Content-Type": "application/x-www-form-urlencoded",
			"User-Agent": "GitHubCopilotChat/0.35.0",
		},
		body: new URLSearchParams({
			client_id: CLIENT_ID,
			scope: "read:user",
		}),
		signal,
	});

	if (!data || typeof data !== "object") {
		throw new Error("Invalid device code response");
	}

	const deviceCode = (data as Record<string, unknown>).device_code;
	const userCode = (data as Record<string, unknown>).user_code;
	const verificationUri = (data as Record<string, unknown>).verification_uri;
	const interval = (data as Record<string, unknown>).interval;
	const expiresIn = (data as Record<string, unknown>).expires_in;

	if (
		typeof deviceCode !== "string" ||
		typeof userCode !== "string" ||
		typeof verificationUri !== "string" ||
		(interval !== undefined && typeof interval !== "number") ||
		typeof expiresIn !== "number"
	) {
		throw new Error("Invalid device code response fields");
	}

	// The verification URI is opened in the user's browser and to prevent `open` from
	// opening an executable or similar, we force it to be a URL.
	let parsedUri: URL;
	try {
		parsedUri = new URL(verificationUri);
	} catch {
		throw new Error("Untrusted verification_uri in device code response");
	}
	if (parsedUri.protocol !== "https:" && parsedUri.protocol !== "http:") {
		throw new Error("Untrusted verification_uri in device code response");
	}

	return {
		device_code: deviceCode,
		user_code: userCode,
		verification_uri: parsedUri.href,
		interval,
		expires_in: expiresIn,
	};
}

async function pollForGitHubAccessToken(
	domain: string,
	device: DeviceCodeResponse,
	signal: AbortSignal,
): Promise<string> {
	const urls = getUrls(domain);
	return pollOAuthDeviceCodeFlow<string>({
		intervalSeconds: device.interval,
		expiresInSeconds: device.expires_in,
		waitBeforeFirstPoll: true,
		signal,
		poll: async () => {
			const raw = await fetchJson(urls.accessTokenUrl, {
				method: "POST",
				headers: {
					Accept: "application/json",
					"Content-Type": "application/x-www-form-urlencoded",
					"User-Agent": "GitHubCopilotChat/0.35.0",
				},
				body: new URLSearchParams({
					client_id: CLIENT_ID,
					device_code: device.device_code,
					grant_type: "urn:ietf:params:oauth:grant-type:device_code",
				}),
				signal,
			});

			if (raw && typeof raw === "object" && typeof (raw as DeviceTokenSuccessResponse).access_token === "string") {
				return { status: "complete", value: (raw as DeviceTokenSuccessResponse).access_token };
			}

			if (raw && typeof raw === "object" && typeof (raw as DeviceTokenErrorResponse).error === "string") {
				const { error, error_description: description, interval } = raw as DeviceTokenErrorResponse;
				if (error === "authorization_pending") {
					return { status: "pending" };
				}

				if (error === "slow_down") {
					return { status: "slow_down", intervalSeconds: typeof interval === "number" ? interval : undefined };
				}

				const descriptionSuffix = description ? `: ${description}` : "";
				return { status: "failed", message: `Device flow failed: ${error}${descriptionSuffix}` };
			}

			return { status: "failed", message: "Invalid device token response" };
		},
	});
}

async function refreshGitHubCopilotAccessToken(
	refreshToken: string,
	enterpriseDomain: string | undefined,
	signal: AbortSignal,
): Promise<OAuthCredential> {
	const domain = enterpriseDomain || "github.com";
	const urls = getUrls(domain);

	const raw = await fetchJson(urls.copilotTokenUrl, {
		headers: {
			Accept: "application/json",
			Authorization: `Bearer ${refreshToken}`,
			...COPILOT_HEADERS,
		},
		signal,
	});

	if (!raw || typeof raw !== "object") {
		throw new Error("Invalid Copilot token response");
	}

	const token = (raw as Record<string, unknown>).token;
	const expiresAt = (raw as Record<string, unknown>).expires_at;

	if (typeof token !== "string" || typeof expiresAt !== "number") {
		throw new Error("Invalid Copilot token response fields");
	}

	return {
		type: "oauth",
		refresh: refreshToken,
		access: token,
		expires: expiresAt * 1000 - 5 * 60 * 1000,
		enterpriseUrl: enterpriseDomain,
	};
}

/**
 * Refresh GitHub Copilot token
 */
async function refreshGitHubCopilotToken(
	refreshToken: string,
	enterpriseDomain: string | undefined,
	signal: AbortSignal,
): Promise<OAuthCredential> {
	const credentials = await refreshGitHubCopilotAccessToken(refreshToken, enterpriseDomain, signal);
	const { availableModelIds } = await fetchGitHubCopilotModels(credentials.access, enterpriseDomain, signal, {
		maxRetries: 0,
		maxElapsedMs: 0,
	});
	return {
		...credentials,
		availableModelIds,
	};
}

/**
 * Enable a model for the user's GitHub Copilot account.
 * This is required for some models (like Claude, Grok) before they can be used.
 */
async function enableGitHubCopilotModel(
	token: string,
	modelId: string,
	enterpriseDomain: string | undefined,
	signal: AbortSignal,
): Promise<boolean> {
	const baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);
	const url = `${baseUrl}/models/${modelId}/policy`;

	let response: Response;
	try {
		response = await fetchWithRateLimitRetry(
			url,
			{
				method: "POST",
				headers: {
					"Content-Type": "application/json",
					Authorization: `Bearer ${token}`,
					...COPILOT_HEADERS,
					"openai-intent": "chat-policy",
					"x-interaction-type": "chat-policy",
				},
				body: JSON.stringify({ state: "enabled" }),
			},
			signal,
			{ maxRetries: 2, maxElapsedMs: 5000 },
		);
	} catch (error) {
		if (signal.aborted) throw error;
		return false;
	}
	if (response.status === 429) {
		throw new Error(`${response.status} ${response.statusText}: ${await response.text()}`);
	}
	return response.ok;
}

/**
 * Enable the requested GitHub Copilot models and return the successful IDs.
 * Policy updates are best effort; exhausted rate limiting stops the batch.
 */
async function enableGitHubCopilotModels(
	token: string,
	modelIds: readonly string[],
	enterpriseDomain: string | undefined,
	signal: AbortSignal,
): Promise<string[]> {
	const enabledModelIds: string[] = [];
	for (const modelId of modelIds) {
		try {
			if (await enableGitHubCopilotModel(token, modelId, enterpriseDomain, signal)) {
				enabledModelIds.push(modelId);
			}
		} catch (error) {
			if (signal.aborted) throw error;
			break;
		}
	}
	return enabledModelIds;
}

async function loginGitHubCopilot(interaction: ProviderAuthInteraction): Promise<OAuthCredential> {
	const input = await interaction.prompt({
		type: "text",
		message: "GitHub Enterprise URL/domain (blank for github.com)",
		placeholder: "company.ghe.com",
	});
	if (interaction.signal.aborted) throw new Error("Login cancelled");

	const trimmed = input.trim();
	const enterpriseDomain = normalizeDomain(input);
	if (trimmed && !enterpriseDomain) throw new Error("Invalid GitHub Enterprise URL/domain");
	const domain = enterpriseDomain || "github.com";

	const device = await startDeviceFlow(domain, interaction.signal);
	interaction.notify({
		type: "device_code",
		userCode: device.user_code,
		verificationUri: device.verification_uri,
		intervalSeconds: device.interval,
		expiresInSeconds: device.expires_in,
	});

	const githubAccessToken = await pollForGitHubAccessToken(domain, device, interaction.signal);
	const credentials = await refreshGitHubCopilotAccessToken(
		githubAccessToken,
		enterpriseDomain ?? undefined,
		interaction.signal,
	);
	const models = await fetchGitHubCopilotModels(
		credentials.access,
		enterpriseDomain ?? undefined,
		interaction.signal,
		{
			maxRetries: 2,
			maxElapsedMs: 5000,
		},
	);
	let enabledModelIds: string[] = [];
	if (models.policyModelIds.length > 0) {
		interaction.notify({ type: "progress", message: "Enabling models..." });
		enabledModelIds = await enableGitHubCopilotModels(
			credentials.access,
			models.policyModelIds,
			enterpriseDomain ?? undefined,
			interaction.signal,
		);
	}
	return {
		...credentials,
		availableModelIds: [...new Set([...models.availableModelIds, ...enabledModelIds])],
	};
}

function copilotEnterpriseDomain(credential: OAuthCredential): string | undefined {
	const enterpriseUrl = credential.enterpriseUrl;
	if (typeof enterpriseUrl !== "string" || !enterpriseUrl) return undefined;
	return normalizeDomain(enterpriseUrl) ?? undefined;
}

export const githubCopilotOAuth: OAuthAuth = {
	name: "GitHub Copilot",
	isSubscription: true,
	login: loginGitHubCopilot,
	refresh: (credential, signal) =>
		refreshGitHubCopilotToken(credential.refresh, copilotEnterpriseDomain(credential), signal),

	/** Derive the credential-specific proxy endpoint for each request. */
	async toAuth(credential) {
		return {
			apiKey: credential.access,
			baseUrl: getGitHubCopilotBaseUrl(credential.access, copilotEnterpriseDomain(credential)),
		};
	},
};
