/**
 * GitHub Copilot OAuth flow
 */
import { getModels } from "../../models.js";
const decode = (s) => 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",
};
export function normalizeDomain(input) {
    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) {
    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) {
    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}`;
}
export function getGitHubCopilotBaseUrl(token, enterpriseDomain) {
    // 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";
}
async function fetchJson(url, init) {
    const response = await fetch(url, {
        ...init,
        signal: init.signal ?? AbortSignal.timeout(30_000),
    });
    if (!response.ok) {
        const text = await response.text();
        throw new Error(`${response.status} ${response.statusText}: ${text}`);
    }
    return response.json();
}
async function startDeviceFlow(domain) {
    const urls = getUrls(domain);
    const data = await fetchJson(urls.deviceCodeUrl, {
        method: "POST",
        headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
            "User-Agent": "GitHubCopilotChat/0.35.0",
        },
        body: JSON.stringify({
            client_id: CLIENT_ID,
            scope: "read:user",
        }),
    });
    if (!data || typeof data !== "object") {
        throw new Error("Invalid device code response");
    }
    const deviceCode = data.device_code;
    const userCode = data.user_code;
    const verificationUri = data.verification_uri;
    const interval = data.interval;
    const expiresIn = data.expires_in;
    if (typeof deviceCode !== "string" ||
        typeof userCode !== "string" ||
        typeof verificationUri !== "string" ||
        typeof interval !== "number" ||
        typeof expiresIn !== "number") {
        throw new Error("Invalid device code response fields");
    }
    return {
        device_code: deviceCode,
        user_code: userCode,
        verification_uri: verificationUri,
        interval,
        expires_in: expiresIn,
    };
}
/**
 * Sleep that can be interrupted by an AbortSignal
 */
function abortableSleep(ms, signal) {
    return new Promise((resolve, reject) => {
        if (signal?.aborted) {
            reject(new Error("Login cancelled"));
            return;
        }
        const timeout = setTimeout(resolve, ms);
        signal?.addEventListener("abort", () => {
            clearTimeout(timeout);
            reject(new Error("Login cancelled"));
        }, { once: true });
    });
}
async function pollForGitHubAccessToken(domain, deviceCode, intervalSeconds, expiresIn, signal) {
    const urls = getUrls(domain);
    const deadline = Date.now() + expiresIn * 1000;
    let intervalMs = Math.max(1000, Math.floor(intervalSeconds * 1000));
    while (Date.now() < deadline) {
        if (signal?.aborted) {
            throw new Error("Login cancelled");
        }
        const raw = await fetchJson(urls.accessTokenUrl, {
            method: "POST",
            headers: {
                Accept: "application/json",
                "Content-Type": "application/json",
                "User-Agent": "GitHubCopilotChat/0.35.0",
            },
            body: JSON.stringify({
                client_id: CLIENT_ID,
                device_code: deviceCode,
                grant_type: "urn:ietf:params:oauth:grant-type:device_code",
            }),
        });
        if (raw && typeof raw === "object" && typeof raw.access_token === "string") {
            return raw.access_token;
        }
        if (raw && typeof raw === "object" && typeof raw.error === "string") {
            const err = raw.error;
            if (err === "authorization_pending") {
                await abortableSleep(intervalMs, signal);
                continue;
            }
            if (err === "slow_down") {
                intervalMs += 5000;
                await abortableSleep(intervalMs, signal);
                continue;
            }
            throw new Error(`Device flow failed: ${err}`);
        }
        await abortableSleep(intervalMs, signal);
    }
    throw new Error("Device flow timed out");
}
/**
 * Refresh GitHub Copilot token
 */
export async function refreshGitHubCopilotToken(refreshToken, enterpriseDomain) {
    const domain = enterpriseDomain || "github.com";
    const urls = getUrls(domain);
    const raw = await fetchJson(urls.copilotTokenUrl, {
        headers: {
            Accept: "application/json",
            Authorization: `Bearer ${refreshToken}`,
            ...COPILOT_HEADERS,
        },
    });
    if (!raw || typeof raw !== "object") {
        throw new Error("Invalid Copilot token response");
    }
    const token = raw.token;
    const expiresAt = raw.expires_at;
    if (typeof token !== "string" || typeof expiresAt !== "number") {
        throw new Error("Invalid Copilot token response fields");
    }
    return {
        refresh: refreshToken,
        access: token,
        expires: expiresAt * 1000 - 5 * 60 * 1000,
        enterpriseUrl: enterpriseDomain,
    };
}
/**
 * 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, modelId, enterpriseDomain) {
    const baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);
    const url = `${baseUrl}/models/${modelId}/policy`;
    try {
        const response = await fetch(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: AbortSignal.timeout(30_000),
        });
        return response.ok;
    }
    catch {
        return false;
    }
}
/**
 * Enable all known GitHub Copilot models that may require policy acceptance.
 * Called after successful login to ensure all models are available.
 */
async function enableAllGitHubCopilotModels(token, enterpriseDomain, onProgress) {
    const models = getModels("github-copilot");
    await Promise.all(models.map(async (model) => {
        const success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);
        onProgress?.(model.id, success);
    }));
}
async function fetchCopilotModelLimits(token, enterpriseDomain) {
    const baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);
    try {
        const response = await fetch(`${baseUrl}/models`, {
            headers: {
                Accept: "application/json",
                Authorization: `Bearer ${token}`,
                "X-GitHub-Api-Version": "2025-05-01",
                ...COPILOT_HEADERS,
            },
            signal: AbortSignal.timeout(30_000),
        });
        if (!response.ok)
            return {};
        const data = (await response.json());
        const limits = {};
        for (const m of data.data || []) {
            const ctx = m.capabilities?.limits?.max_context_window_tokens;
            const out = m.capabilities?.limits?.max_output_tokens;
            if (typeof ctx === "number" && typeof out === "number" && ctx > 0 && out > 0 && Number.isFinite(ctx) && Number.isFinite(out)) {
                limits[m.id] = { contextWindow: ctx, maxTokens: out };
            }
        }
        return limits;
    }
    catch {
        return {};
    }
}
/**
 * Login with GitHub Copilot OAuth (device code flow)
 *
 * @param options.onAuth - Callback with URL and optional instructions (user code)
 * @param options.onPrompt - Callback to prompt user for input
 * @param options.onProgress - Optional progress callback
 * @param options.signal - Optional AbortSignal for cancellation
 */
export async function loginGitHubCopilot(options) {
    const input = await options.onPrompt({
        message: "GitHub Enterprise URL/domain (blank for github.com)",
        placeholder: "company.ghe.com",
        allowEmpty: true,
    });
    if (options.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);
    options.onAuth(device.verification_uri, `Enter code: ${device.user_code}`);
    const githubAccessToken = await pollForGitHubAccessToken(domain, device.device_code, device.interval, device.expires_in, options.signal);
    const credentials = await refreshGitHubCopilotToken(githubAccessToken, enterpriseDomain ?? undefined);
    // Enable all models after successful login
    options.onProgress?.("Enabling models...");
    await enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined);
    // Fetch real model limits from the Copilot API
    options.onProgress?.("Fetching model limits...");
    const modelLimits = await fetchCopilotModelLimits(credentials.access, enterpriseDomain ?? undefined);
    if (Object.keys(modelLimits).length > 0) {
        credentials.modelLimits = modelLimits;
    }
    return credentials;
}
export const githubCopilotOAuthProvider = {
    id: "github-copilot",
    name: "GitHub Copilot",
    async login(callbacks) {
        return loginGitHubCopilot({
            onAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),
            onPrompt: callbacks.onPrompt,
            onProgress: callbacks.onProgress,
            signal: callbacks.signal,
        });
    },
    async refreshToken(credentials) {
        const creds = credentials;
        const refreshed = await refreshGitHubCopilotToken(creds.refresh, creds.enterpriseUrl);
        try {
            const modelLimits = await fetchCopilotModelLimits(refreshed.access, creds.enterpriseUrl);
            if (Object.keys(modelLimits).length > 0) {
                refreshed.modelLimits = modelLimits;
            }
        }
        catch {
            // Model limits fetch is best-effort; don't block token refresh
        }
        return refreshed;
    },
    getApiKey(credentials) {
        return credentials.access;
    },
    modifyModels(models, credentials) {
        const creds = credentials;
        const domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined;
        const baseUrl = getGitHubCopilotBaseUrl(creds.access, domain);
        const limits = creds.modelLimits;
        return models.map((m) => {
            if (m.provider !== "github-copilot")
                return m;
            const modelLimits = limits?.[m.id];
            return {
                ...m,
                baseUrl,
                ...(modelLimits && {
                    contextWindow: modelLimits.contextWindow,
                    maxTokens: modelLimits.maxTokens,
                }),
            };
        });
    },
};
//# sourceMappingURL=github-copilot.js.map