import { createAppAuth } from "@octokit/auth-app";
/** Backend GitHub App helpers: installation discovery, repo creation, and
 * short-lived installation tokens. */
/** Raised when the GITHUB_APP_* environment is missing or incomplete. */
export class GithubAppConfigError extends Error {
    constructor(message) {
        super(message);
        this.name = "GithubAppConfigError";
    }
}
/** Raised when minting an installation token fails because the app is no longer
 * installed (GitHub 401/404). */
export class ReinstallRequiredError extends Error {
    constructor(message) {
        super(message);
        this.name = "ReinstallRequiredError";
    }
}
/** Raised when repo creation fails because the name is already taken (422). */
export class RepoAlreadyExistsError extends Error {
    constructor(message) {
        super(message);
        this.name = "RepoAlreadyExistsError";
    }
}
let cachedAuth = null;
/** Lazily build (and cache) the app auth interface. Un-escapes `\n` so a
 * single-line `GITHUB_APP_PRIVATE_KEY` env value works. */
function appAuth() {
    if (cachedAuth)
        return cachedAuth;
    const appId = process.env.GITHUB_APP_ID;
    const privateKey = process.env.GITHUB_APP_PRIVATE_KEY?.replace(/\\n/g, "\n");
    if (!appId || !privateKey) {
        throw new GithubAppConfigError("Missing GitHub App config: GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY must be set");
    }
    cachedAuth = createAppAuth({ appId, privateKey });
    return cachedAuth;
}
function isUninstalled(error) {
    const status = error?.status;
    return status === 401 || status === 404;
}
/**
 * Mint a ~1h installation access token. When `repoName` is given, the token is
 * scoped to that single repository with Contents access only. Throws
 * {@link ReinstallRequiredError} if the app has been uninstalled.
 */
export async function mintInstallationToken(installationId, repoName) {
    try {
        const auth = appAuth();
        const result = await auth(repoName
            ? {
                type: "installation",
                installationId: Number(installationId),
                repositoryNames: [repoName],
                permissions: { contents: "write" },
            }
            : { type: "installation", installationId: Number(installationId) });
        return { token: result.token, expiresAt: result.expiresAt };
    }
    catch (error) {
        if (isUninstalled(error)) {
            throw new ReinstallRequiredError(`Installation ${installationId} is no longer accessible; the app was likely uninstalled`);
        }
        throw error;
    }
}
const GITHUB_API = "https://api.github.com";
function userHeaders(userAccessToken) {
    return {
        Authorization: `token ${userAccessToken}`,
        Accept: "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
        "User-Agent": "vetra-cloud",
    };
}
/** Create a blank private repo in the user's account with their access token. */
export async function createRepo(userAccessToken, name) {
    const response = await fetch(`${GITHUB_API}/user/repos`, {
        method: "POST",
        headers: { ...userHeaders(userAccessToken), "Content-Type": "application/json" },
        body: JSON.stringify({ name, private: true, auto_init: false }),
    });
    if (response.status === 422) {
        throw new RepoAlreadyExistsError(`A repository named "${name}" already exists`);
    }
    if (!response.ok) {
        throw new Error(`Failed to create repository: ${response.status} ${response.statusText}`);
    }
    const body = (await response.json());
    return { fullName: body.full_name, url: body.html_url };
}
/**
 * The id of this app's installation on `repoFullName` (`owner/repo`), or null if
 * the app is not installed on that repo. Resolved from the app's own JWT, so it
 * needs no user token.
 */
export async function findRepoInstallationId(repoFullName) {
    const auth = appAuth();
    const { token: jwt } = await auth({ type: "app" });
    const response = await fetch(`${GITHUB_API}/repos/${repoFullName}/installation`, {
        headers: {
            Authorization: `Bearer ${jwt}`,
            Accept: "application/vnd.github+json",
            "X-GitHub-Api-Version": "2022-11-28",
            "User-Agent": "vetra-cloud",
        },
    });
    if (response.status === 404)
        return null;
    if (!response.ok) {
        throw new Error(`Failed to look up repo installation: ${response.status} ${response.statusText}`);
    }
    const body = (await response.json());
    return String(body.id);
}
/**
 * Find a repo named `name` in the authenticated user's account, returning its
 * full name and URL, or null if they have no such repo.
 */
export async function findUserRepo(userAccessToken, name) {
    const userResponse = await fetch(`${GITHUB_API}/user`, {
        headers: userHeaders(userAccessToken),
    });
    if (!userResponse.ok) {
        throw new Error(`Failed to resolve the authenticated user: ${userResponse.status} ${userResponse.statusText}`);
    }
    const { login } = (await userResponse.json());
    const repoResponse = await fetch(`${GITHUB_API}/repos/${login}/${name}`, {
        headers: userHeaders(userAccessToken),
    });
    if (repoResponse.status === 404)
        return null;
    if (!repoResponse.ok) {
        throw new Error(`Failed to look up repository: ${repoResponse.status} ${repoResponse.statusText}`);
    }
    const body = (await repoResponse.json());
    return { fullName: body.full_name, url: body.html_url };
}
const GITHUB_OAUTH_HOST = "https://github.com";
function githubClientId() {
    const clientId = process.env.GITHUB_APP_CLIENT_ID;
    if (!clientId) {
        throw new GithubAppConfigError("Missing GitHub App config: GITHUB_APP_CLIENT_ID must be set");
    }
    return clientId;
}
/**
 * Begin GitHub App device authorization (`POST /login/device/code`); returns the
 * `user_code`, verification URL, and `device_code`.
 */
export async function startDeviceFlow() {
    const response = await fetch(`${GITHUB_OAUTH_HOST}/login/device/code`, {
        method: "POST",
        headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
            "User-Agent": "vetra-cloud",
        },
        body: JSON.stringify({ client_id: githubClientId() }),
    });
    if (!response.ok) {
        throw new Error(`Failed to start device flow: ${response.status} ${response.statusText}`);
    }
    const body = (await response.json());
    return {
        deviceCode: body.device_code,
        userCode: body.user_code,
        verificationUri: body.verification_uri,
        expiresIn: body.expires_in,
        interval: body.interval,
    };
}
/**
 * Exchange a device_code for a user access token, mapping GitHub's polling
 * responses to a discriminated status.
 */
export async function exchangeDeviceCode(deviceCode) {
    const response = await fetch(`${GITHUB_OAUTH_HOST}/login/oauth/access_token`, {
        method: "POST",
        headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
            "User-Agent": "vetra-cloud",
        },
        body: JSON.stringify({
            client_id: githubClientId(),
            device_code: deviceCode,
            grant_type: "urn:ietf:params:oauth:grant-type:device_code",
        }),
    });
    if (!response.ok) {
        throw new Error(`Failed to exchange device code: ${response.status} ${response.statusText}`);
    }
    const body = (await response.json());
    if (body.access_token) {
        return { status: "authorized", accessToken: body.access_token };
    }
    switch (body.error) {
        case "authorization_pending":
            return { status: "pending" };
        case "slow_down":
            return { status: "slowDown" };
        case "expired_token":
            return { status: "expired" };
        case "access_denied":
            return { status: "denied" };
        default:
            throw new Error(`Device flow exchange failed: ${body.error ?? "unknown error"}`);
    }
}
