/**
 * Microsoft Graph client wrapper.
 *
 * Builds a `Client` from `@microsoft/microsoft-graph-client` whose auth provider
 * pulls from the per-account TokenStore, with auto-refresh when the access
 * token is within the refresh threshold of expiry.
 *
 * Centralises error classification — every Graph error decision keys off the
 * status code and the structured `error.code` field, never the prose body.
 * Aligns with `feedback_no_stdout_parsing_for_control_flow.md`.
 */
import { Client } from "@microsoft/microsoft-graph-client";
import { refreshAccessToken } from "../auth/pkce-flow.js";
import { log } from "./log.js";
/** Pull (statusCode, code, headers) from whatever shape the SDK threw. */
function normalizeGraphError(err) {
    const e = err;
    const statusCode = e.statusCode ?? e.status ?? 0;
    let code = e.code ?? "";
    if (!code && typeof e.body === "string") {
        try {
            const parsed = JSON.parse(e.body);
            code = parsed.error?.code ?? "";
        }
        catch {
            // body wasn't JSON; leave code empty
        }
    }
    else if (!code && e.body && typeof e.body === "object") {
        code = e.body.error?.code ?? "";
    }
    let retryAfterHeader = null;
    let mailServerHeader = null;
    const fetchHeaders = e.response?.headers;
    if (fetchHeaders && typeof fetchHeaders.get === "function") {
        retryAfterHeader = fetchHeaders.get("retry-after") ?? null;
        mailServerHeader = fetchHeaders.get("x-mailbox-server") ?? null;
    }
    else if (e.headers instanceof Map) {
        retryAfterHeader = e.headers.get("retry-after") ?? e.headers.get("Retry-After") ?? null;
        mailServerHeader = e.headers.get("x-mailbox-server") ?? null;
    }
    else if (e.headers && typeof e.headers === "object") {
        const h = e.headers;
        retryAfterHeader =
            h["retry-after"] ?? h["Retry-After"] ?? h["RETRY-AFTER"] ?? null;
        mailServerHeader = h["x-mailbox-server"] ?? h["X-MailboxServer"] ?? null;
    }
    return { statusCode, code, retryAfterHeader, mailServerHeader };
}
export function classifyGraphError(err) {
    const norm = normalizeGraphError(err);
    const { statusCode, code, retryAfterHeader, mailServerHeader } = norm;
    if (statusCode === 401) {
        return { kind: "auth", statusCode, code: code || "Unauthenticated" };
    }
    if (statusCode === 429) {
        if (retryAfterHeader) {
            const seconds = Number(retryAfterHeader);
            if (Number.isFinite(seconds) && seconds >= 0) {
                return {
                    kind: "rate-limit-recoverable",
                    statusCode,
                    retryAfterMs: seconds * 1000,
                };
            }
        }
        return { kind: "rate-limit-terminal", statusCode };
    }
    if (statusCode === 503 && code === "MailboxNotEnabledForRESTAPI") {
        return { kind: "on-prem", statusCode, mailServer: mailServerHeader ?? undefined };
    }
    if (statusCode >= 500 && statusCode < 600) {
        return { kind: "5xx", statusCode, code: code || "ServerError" };
    }
    return { kind: "other", statusCode, code: code || "Unknown" };
}
/**
 * Factory wrapping `Client.init`. Exported as a named function so tests can
 * `vi.spyOn(graphClient, "createGraphClient")` reliably (avoids the
 * destructured-ESM mock no-op pitfall).
 */
export function createGraphClient(config) {
    return Client.init({
        authProvider: async (done) => {
            try {
                const token = await getOrRefreshAccessToken(config);
                done(null, token);
            }
            catch (err) {
                done(err, null);
            }
        },
    });
}
export async function getOrRefreshAccessToken(config) {
    const { tokenStore, clientId, tenantId, accountId } = config;
    const blob = tokenStore.read();
    if (!blob) {
        throw new Error(`auth-required: Outlook not connected for account=${accountId}; run outlook-account-register.`);
    }
    if (tokenStore.refreshTokenExpired() || !blob.refreshToken) {
        throw new Error(`auth-required: Outlook refresh token expired for account=${accountId}; run outlook-account-register.`);
    }
    if (!tokenStore.needsRefresh()) {
        return blob.accessToken;
    }
    const oldExpSec = Math.floor(blob.accessTokenExpiry / 1000);
    try {
        const refreshed = await refreshAccessToken({
            clientId,
            tenantId,
            refreshToken: blob.refreshToken,
        });
        const updated = tokenStore.store(refreshed.access_token, refreshed.refresh_token ?? blob.refreshToken, refreshed.expires_in, { graphUserId: blob.graphUserId, scopes: blob.scopes });
        log({
            event: "token-refreshed",
            account: accountId,
            oldExpSec,
            newExpSec: Math.floor(updated.accessTokenExpiry / 1000),
        });
        return updated.accessToken;
    }
    catch (err) {
        log({
            event: "token-refresh-failed",
            account: accountId,
            reason: err.message,
        });
        tokenStore.clear();
        throw new Error(`Outlook token refresh failed for account=${accountId}; re-auth required.`);
    }
}
/**
 * Run a Graph call with classification, single retry on `auth` (refresh +
 * retry once), and respect for 429 Retry-After. All terminal classes throw
 * with operator-readable messages.
 */
export async function callGraph(config, invoke, opts = { tool: "graph" }) {
    let attempts = 0;
    // Up to 3 attempts: original, one for auth-refresh, one for 429-recoverable.
    while (true) {
        attempts += 1;
        const client = createGraphClient(config);
        try {
            return await invoke(client);
        }
        catch (err) {
            const cls = classifyGraphError(err);
            log({
                event: "graph-error",
                account: config.accountId,
                status: cls.statusCode,
                code: "code" in cls ? cls.code : "",
                retryAfterMs: cls.kind === "rate-limit-recoverable" ? cls.retryAfterMs : null,
            });
            if (cls.kind === "on-prem") {
                log({
                    event: "on-prem-rejected",
                    account: config.accountId,
                    mailServer: cls.mailServer ?? "unknown",
                });
                throw new Error("Microsoft Graph does not support on-premises Exchange. Use IMAP.");
            }
            if (cls.kind === "rate-limit-terminal") {
                throw new Error(`Outlook rate-limited without Retry-After hint (account=${config.accountId}, tool=${opts.tool}).`);
            }
            if (cls.kind === "auth" && attempts === 1) {
                // Force a refresh on the next iteration by clearing the local cache
                // and explicitly refreshing, then retry.
                await getOrRefreshAccessToken({ ...config });
                continue;
            }
            if (cls.kind === "auth") {
                throw new Error(`Outlook auth expired for account=${config.accountId}; run outlook-account-register.`);
            }
            if (cls.kind === "rate-limit-recoverable" && attempts === 1) {
                await new Promise((resolve) => setTimeout(resolve, cls.retryAfterMs));
                continue;
            }
            if (cls.kind === "5xx" && attempts === 1) {
                await new Promise((resolve) => setTimeout(resolve, 500));
                continue;
            }
            throw err;
        }
    }
}
//# sourceMappingURL=graph-client.js.map