/**
 * Test-connection probe for Anthropic API keys.
 *
 * Implements the staged-advisory contract: each call walks an ordered
 * sequence of stages (reachable → auth → listing → model_in_list), and
 * returns the per-stage outcomes plus an overall ok flag. The overall
 * flag is true IFF every non-advisory stage passed; the `model_in_list`
 * stage is advisory, so a missing model.id only surfaces as a hint —
 * the user might be using a preview-tagged or beta model that isn't in
 * the public listing yet.
 *
 * No upstream error bodies are echoed. Stage details are short,
 * human-authored strings, and never carry the API key, header values,
 * or the upstream response payload beyond what's explicitly modeled
 * here.
 *
 * Other providers (r2, brave) still use the legacy stub shape — they
 * return `{ ok, message, durationMs }` only, no stages. The shared
 * TestResult type below treats the new fields as optional so both
 * shapes coexist behind one type until later waves promote the rest
 * to staged diagnostics.
 */
const ANTHROPIC_API_BASE = 'https://api.anthropic.com';
const REACHABLE_TIMEOUT_MS = 5_000;
const AUTH_TIMEOUT_MS = 10_000;
/**
 * Levenshtein distance, capped — we only need to rank suggestions for
 * a short user-supplied model.id against a short list. The cap keeps
 * the function from running away on long strings (model IDs are
 * typically 10–40 chars).
 */
function levenshtein(a, b) {
    if (a === b)
        return 0;
    if (a.length === 0)
        return b.length;
    if (b.length === 0)
        return a.length;
    const v0 = new Array(b.length + 1);
    const v1 = new Array(b.length + 1);
    for (let i = 0; i <= b.length; i++)
        v0[i] = i;
    for (let i = 0; i < a.length; i++) {
        v1[0] = i + 1;
        for (let j = 0; j < b.length; j++) {
            const cost = a[i] === b[j] ? 0 : 1;
            v1[j + 1] = Math.min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost);
        }
        for (let j = 0; j <= b.length; j++)
            v0[j] = v1[j];
    }
    return v1[b.length];
}
/**
 * Pick the top-N closest model IDs to `needle` from `haystack`. Prefix
 * matches win first, then sort the rest by Levenshtein distance.
 * Returns at most `limit` entries.
 */
export function suggestModelIds(needle, haystack, limit = 3) {
    if (!needle || haystack.length === 0)
        return haystack.slice(0, limit);
    const prefixes = [];
    const rest = [];
    for (const id of haystack) {
        if (id === needle)
            continue;
        if (id.startsWith(needle) || needle.startsWith(id)) {
            prefixes.push(id);
        }
        else {
            rest.push({ id, d: levenshtein(id, needle) });
        }
    }
    rest.sort((a, b) => a.d - b.d);
    return [...prefixes, ...rest.map((r) => r.id)].slice(0, limit);
}
/**
 * Fetch wrapper that honours a per-call timeout via AbortController.
 * Returns the Response on success; throws the original error on
 * network failure / abort. Callers convert the throw into a stage
 * with an error_code rather than propagating raw error text.
 *
 * Accepts the fetch impl as an explicit dep so unit tests can swap
 * in a mock without patching the global.
 */
async function fetchWithTimeout(fetchImpl, url, init) {
    const { timeoutMs, ...rest } = init;
    const ctl = new AbortController();
    const handle = setTimeout(() => ctl.abort(), timeoutMs);
    try {
        return await fetchImpl(url, { ...rest, signal: ctl.signal });
    }
    finally {
        clearTimeout(handle);
    }
}
/**
 * Anthropic probe entry point.
 *
 * Signature is loose so the legacy `(apiKey) => Promise<TestResult>`
 * call sites in `registry.ts` and the `/api/secrets/:key/test`
 * dispatcher keep working. When the API layer has a configured
 * `model.id` it forwards it via `ctx.model_id`; otherwise stage 4
 * runs without a needle and reports `model_in_list: undefined`.
 *
 * The injected `fetchImpl` lets unit tests swap in a mock without
 * patching the global. Default = the platform `fetch`.
 */
export async function probeAnthropic(apiKey, ctx, fetchImpl = fetch) {
    const started = Date.now();
    const stages = [];
    const advisory = {};
    if (!apiKey || typeof apiKey !== 'string') {
        return {
            ok: false,
            message: 'No API key provided',
            durationMs: Date.now() - started,
            stages: [{ name: 'auth', ok: false, error_code: 'missing_value', detail: 'No API key provided' }],
        };
    }
    // ── Stage 1: reachable ────────────────────────────────────────────────
    // Cheap network round-trip to verify DNS + TCP + TLS to the API host.
    // A 404 here is fine — we just need a response. The intent is to
    // separate "network broken" from "API key bad" so the UI can show
    // a useful error.
    try {
        await fetchWithTimeout(fetchImpl, `${ANTHROPIC_API_BASE}/`, {
            method: 'GET',
            timeoutMs: REACHABLE_TIMEOUT_MS,
            // The bare hostname returns 404 with a small JSON body — we don't
            // care about status here, only that we reached the server.
        });
        stages.push({ name: 'reachable', ok: true });
    }
    catch (err) {
        stages.push({
            name: 'reachable',
            ok: false,
            error_code: 'network',
            detail: err instanceof Error && err.name === 'AbortError'
                ? 'Timed out connecting to api.anthropic.com'
                : 'Could not reach api.anthropic.com',
        });
        return {
            ok: false,
            message: 'Network unreachable',
            durationMs: Date.now() - started,
            stages,
        };
    }
    // ── Stages 2 + 3: auth + listing ─────────────────────────────────────
    // One request covers both — `GET /v1/models` returns 401/403 on bad
    // auth and a `{ data: [...] }` envelope on success. We score the
    // stages separately so the UI can show "auth ok, but listing shape
    // unexpected" if Anthropic ever changes the response.
    let listing;
    try {
        const resp = await fetchWithTimeout(fetchImpl, `${ANTHROPIC_API_BASE}/v1/models`, {
            method: 'GET',
            headers: {
                'x-api-key': apiKey,
                'anthropic-version': '2023-06-01',
            },
            timeoutMs: AUTH_TIMEOUT_MS,
        });
        if (resp.status === 401 || resp.status === 403) {
            stages.push({
                name: 'auth',
                ok: false,
                error_code: 'unauthorized',
                detail: 'API key rejected (HTTP ' + resp.status + ')',
            });
            return {
                ok: false,
                message: 'API key rejected',
                durationMs: Date.now() - started,
                stages,
            };
        }
        if (resp.status >= 500) {
            stages.push({
                name: 'auth',
                ok: false,
                error_code: 'upstream_5xx',
                detail: 'Anthropic returned HTTP ' + resp.status,
            });
            return {
                ok: false,
                message: 'Upstream error',
                durationMs: Date.now() - started,
                stages,
            };
        }
        if (resp.status < 200 || resp.status >= 300) {
            stages.push({
                name: 'auth',
                ok: false,
                error_code: 'unexpected_status',
                detail: 'Unexpected HTTP ' + resp.status,
            });
            return {
                ok: false,
                message: 'Unexpected status',
                durationMs: Date.now() - started,
                stages,
            };
        }
        stages.push({ name: 'auth', ok: true });
        // Listing — parse the JSON. A network success with a malformed body
        // gets its own error_code so the UI can distinguish from outright
        // auth failure.
        try {
            listing = await resp.json();
        }
        catch {
            stages.push({
                name: 'listing',
                ok: false,
                error_code: 'invalid_json',
                detail: 'Could not parse /v1/models response',
            });
            return {
                ok: false,
                message: 'Invalid response shape',
                durationMs: Date.now() - started,
                stages,
            };
        }
        if (!listing || !Array.isArray(listing.data)) {
            stages.push({
                name: 'listing',
                ok: false,
                error_code: 'invalid_shape',
                detail: 'Listing response missing `data` array',
            });
            return {
                ok: false,
                message: 'Invalid response shape',
                durationMs: Date.now() - started,
                stages,
            };
        }
        stages.push({ name: 'listing', ok: true });
    }
    catch (err) {
        stages.push({
            name: 'auth',
            ok: false,
            error_code: 'network',
            detail: err instanceof Error && err.name === 'AbortError'
                ? 'Timed out fetching /v1/models'
                : 'Network error fetching /v1/models',
        });
        return {
            ok: false,
            message: 'Network error',
            durationMs: Date.now() - started,
            stages,
        };
    }
    // ── Stage 4: model_in_list (ADVISORY) ────────────────────────────────
    // Only runs when the API caller forwarded a configured model.id.
    // The stage's ok flag is reported per-stage but does NOT flip the
    // overall ok: preview / unlisted models are intentionally allowed.
    const ids = (listing.data ?? [])
        .map((m) => (m && typeof m.id === 'string') ? m.id : undefined)
        .filter((s) => !!s);
    if (ctx?.model_id) {
        const present = ids.includes(ctx.model_id);
        if (present) {
            stages.push({ name: 'model_in_list', ok: true });
            advisory.model_in_list = true;
        }
        else {
            const suggestions = suggestModelIds(ctx.model_id, ids, 3);
            stages.push({
                name: 'model_in_list',
                ok: false,
                error_code: 'not_listed',
                detail: 'Configured model.id not found in /v1/models (advisory — preview models are allowed)',
            });
            advisory.model_in_list = false;
            if (suggestions.length > 0)
                advisory.suggestions = suggestions;
        }
    }
    // Overall ok = every non-advisory stage passed. By construction we
    // returned early on any non-advisory failure, so reaching here means
    // reachable + auth + listing are all green.
    return {
        ok: true,
        message: ctx?.model_id && advisory.model_in_list === false
            ? 'Key valid; configured model.id not in listing (advisory)'
            : 'Key valid',
        durationMs: Date.now() - started,
        stages,
        advisory: Object.keys(advisory).length > 0 ? advisory : undefined,
    };
}
//# sourceMappingURL=anthropic.js.map