import { ApiError } from "../commands/memory/_lib/errors.js";
import { findContextByUrl, getActiveOrg, resolveContext, validateOrgSlug as validateOrgSlugShared, } from "./context.js";
import { getToken, loadCredentials } from "./credentials.js";
import { extractApiError, fetchWithRetry, parseJsonResponse } from "./http.js";
export class ApiClient {
    apiBaseUrl;
    token;
    fetchImpl;
    constructor(apiBaseUrl, token, fetchImpl = fetch) {
        this.apiBaseUrl = apiBaseUrl;
        this.token = token;
        this.fetchImpl = fetchImpl;
    }
    /**
     * Single HTTP chokepoint: fetch (with retry) → parse JSON → throw `ApiError`
     * on a non-ok / non-allowed status. Returns the response status alongside the
     * parsed body so callers that branch on status (e.g. `lobu apply`'s 404/409
     * handling) don't reimplement the fetch/parse/error pipeline.
     */
    async requestWithStatus(method, path, body, options = {}) {
        const url = path.startsWith("http") ? path : `${this.apiBaseUrl}${path}`;
        const headers = {
            Authorization: `Bearer ${this.token}`,
        };
        if (options.sendAccept !== false)
            headers.Accept = "application/json";
        const init = { method, headers };
        if (body !== undefined) {
            headers["Content-Type"] = "application/json";
            init.body = JSON.stringify(body);
        }
        else if (options.alwaysJsonContentType) {
            headers["Content-Type"] = "application/json";
        }
        const response = await fetchWithRetry(url, init, {
            fetchImpl: this.fetchImpl,
        });
        const parsed = await parseJsonResponse(response, url, (message) => {
            throw new ApiError(message, response.status);
        });
        const okStatuses = options.okStatuses ?? [200, 201, 204];
        if (!response.ok || !okStatuses.includes(response.status)) {
            const { message, code } = extractApiError(parsed, response.status, response.statusText);
            throw new ApiError(`${method} ${path} failed: ${message}`, response.status, code);
        }
        return { status: response.status, body: parsed };
    }
    async request(method, path, body, options = {}) {
        const { body: parsed } = await this.requestWithStatus(method, path, body, options);
        return parsed;
    }
    get(path) {
        return this.request("GET", path);
    }
    post(path, body) {
        return this.request("POST", path, body);
    }
    patch(path, body) {
        return this.request("PATCH", path, body);
    }
    delete(path) {
        return this.request("DELETE", path, undefined, {
            okStatuses: [200, 204],
        });
    }
}
export async function resolveApiClient(options = {}) {
    const target = await resolveApiTarget(options);
    const apiBaseUrl = apiBaseFromContextUrl(target.url);
    const token = process.env.LOBU_API_TOKEN || (await getToken(target.name));
    if (!token) {
        throw new ApiError(`Not logged in to context "${target.name}". Run \`lobu login${options.context ? ` --context ${target.name}` : ""}\` first.`, 401);
    }
    const orgSlug = await resolveOrgSlug({
        ...options,
        contextName: target.name,
        token,
        apiBaseUrl,
        useStoredUserInfoEndpoint: target.useStoredUserInfoEndpoint,
    });
    return {
        client: new ApiClient(apiBaseUrl, token, options.fetchImpl),
        contextName: target.name,
        apiBaseUrl,
        orgSlug,
        token,
    };
}
export async function listOrganizations(options = {}) {
    const target = await resolveApiTarget(options);
    const token = process.env.LOBU_API_TOKEN || (await getToken(target.name));
    if (!token) {
        throw new ApiError(`Not logged in to context "${target.name}". Run \`lobu login${options.context ? ` --context ${target.name}` : ""}\` first.`, 401);
    }
    return getOrganizationsFromUserInfo(target.name, token, apiBaseFromContextUrl(target.url), options.fetchImpl, { useStoredUserInfoEndpoint: target.useStoredUserInfoEndpoint });
}
async function resolveApiTarget(options) {
    const requested = await resolveContext(options.context);
    if (!options.apiUrl) {
        return { ...requested, useStoredUserInfoEndpoint: true };
    }
    const matched = await findContextByUrl(options.apiUrl);
    if (matched) {
        return { ...matched, useStoredUserInfoEndpoint: true };
    }
    const apiBaseUrl = apiBaseFromContextUrl(options.apiUrl);
    const contextApiBaseUrl = apiBaseFromContextUrl(requested.url);
    if (!process.env.LOBU_API_TOKEN && apiBaseUrl !== contextApiBaseUrl) {
        throw new ApiError(`Refusing to send stored context credentials for "${requested.name}" to ${apiBaseUrl}. Add a context for that URL or set LOBU_API_TOKEN explicitly.`);
    }
    return {
        ...requested,
        url: options.apiUrl,
        useStoredUserInfoEndpoint: apiBaseUrl === contextApiBaseUrl,
    };
}
export function apiBaseFromContextUrl(apiUrl) {
    const url = new URL(apiUrl);
    url.pathname = "";
    url.search = "";
    url.hash = "";
    return url.toString().replace(/\/+$/, "");
}
async function resolveOrgSlug(options) {
    const explicit = options.org?.trim() || process.env.LOBU_ORG?.trim();
    if (explicit)
        return validateOrgSlug(explicit);
    const active = await getActiveOrg(options.contextName);
    if (active)
        return validateOrgSlug(active);
    const organizations = await getOrganizationsFromUserInfo(options.contextName, options.token, options.apiBaseUrl, options.fetchImpl, { useStoredUserInfoEndpoint: options.useStoredUserInfoEndpoint }).catch(() => []);
    if (organizations.length === 1) {
        return validateOrgSlug(organizations[0].slug);
    }
    if (organizations.length > 1) {
        throw new ApiError(`Multiple organizations are available (${organizations.map((org) => org.slug).join(", ")}). Run \`lobu org set <slug>\` or pass \`--org <slug>\`.`);
    }
    throw new ApiError("No organization selected. Run `lobu org set <slug>` or pass `--org <slug>`.");
}
async function getOrganizationsFromUserInfo(contextName, token, apiBaseUrl, fetchImpl = fetch, options = {}) {
    const creds = options.useStoredUserInfoEndpoint === false
        ? null
        : await loadCredentials(contextName);
    const endpoint = creds?.oauth?.userinfoEndpoint ?? `${apiBaseUrl}/oauth/userinfo`;
    const response = await fetchWithRetry(endpoint, {
        headers: {
            Accept: "application/json",
            Authorization: `Bearer ${token}`,
        },
    }, { fetchImpl });
    if (!response.ok)
        return [];
    const data = (await response.json().catch(() => null));
    if (!data)
        return [];
    const orgs = Array.isArray(data.organizations) ? data.organizations : [];
    const result = [];
    for (const entry of orgs) {
        if (!entry || typeof entry !== "object")
            continue;
        const value = entry;
        const slug = typeof value.slug === "string" ? value.slug : "";
        if (!slug)
            continue;
        result.push({
            slug,
            ...(typeof value.name === "string" ? { name: value.name } : {}),
        });
    }
    return result;
}
/**
 * Wrap the shared {@link validateOrgSlugShared} so the typed `ApiError`
 * (carrying the same human message) propagates instead of a bare `Error`.
 */
function validateOrgSlug(slug) {
    try {
        return validateOrgSlugShared(slug);
    }
    catch (err) {
        throw new ApiError(err instanceof Error ? err.message : String(err));
    }
}
//# sourceMappingURL=api-client.js.map