/**
 * SessionsHttpClient — the managed transport for `agents sessions export
 * --to-r2` / `import --from-r2` when the caller is signed in to Phoenix.
 *
 * Same put / get / list / delete surface as `R2Client` (`./r2.ts`), so the
 * export/import command drives EITHER a managed HTTP client or a BYO S3 client
 * through the one shared {@link SessionsBackupClient} interface. The difference
 * is only the wire: this talks to the managed sessions Worker over `fetch` with
 * a `Authorization: Bearer <phoenix-token>` header (the same request shape as
 * `traces/sync.ts`), instead of SigV4 against R2 directly.
 *
 * The userId namespace prefix is prepended INSIDE this client
 * (`${baseUrl}/${userId}/${key}`), so the caller passes the SAME object key it
 * passes to `R2Client` (`sessions/<machine>/<agent>/<sessionId>.jsonl`). That
 * keeps the BYO object layout (SES-27a) byte-identical while the managed Worker
 * gets its required `segments[0] === userId` prefix.
 */
export class SessionsHttpClient {
    kind = 'managed';
    base;
    userId;
    token;
    constructor(opts) {
        this.base = opts.baseUrl.replace(/\/+$/, '');
        this.userId = safeOwnerSegment(opts.userId);
        this.token = opts.token;
    }
    /** `${baseUrl}/<userId>/<key>` with every path segment percent-encoded. */
    objUrl(key) {
        const rel = safeObjectKey(key).map(encodeURIComponent).join('/');
        return `${this.base}/${encodeURIComponent(this.userId)}/${rel}`;
    }
    authHeaders(extra = {}) {
        return { authorization: `Bearer ${this.token}`, ...extra };
    }
    /** Upload an object under this owner's namespace. Overwrites unconditionally. */
    async put(key, body, contentType = 'application/octet-stream') {
        const res = await fetch(this.objUrl(key), {
            method: 'PUT',
            headers: this.authHeaders({ 'content-type': contentType }),
            body,
        });
        if (!res.ok)
            throw new Error(`sessions PUT ${key} failed: ${res.status} ${await safeText(res)}`);
    }
    /** Create an object exactly once. A concurrent winner returns false. */
    async putIfAbsent(key, body, contentType = 'application/octet-stream') {
        const res = await fetch(this.objUrl(key), {
            method: 'PUT',
            headers: this.authHeaders({ 'content-type': contentType, 'if-none-match': '*' }),
            body,
        });
        if (res.status === 409)
            return false;
        if (!res.ok) {
            throw new Error(`sessions PUT-IF-ABSENT ${key} failed: ${res.status} ${await safeText(res)}`);
        }
        return true;
    }
    /** Fetch an object as text, or null if it does not exist (404). */
    async get(key) {
        const res = await fetch(this.objUrl(key), { method: 'GET', headers: this.authHeaders() });
        if (res.status === 404)
            return null;
        if (!res.ok)
            throw new Error(`sessions GET ${key} failed: ${res.status} ${await safeText(res)}`);
        return await res.text();
    }
    /**
     * List this owner's object keys, each relative to the `<userId>/` prefix (so
     * they feed straight back into `get`/`delete`). The `prefix` arg is accepted
     * for interface parity with `R2Client` but ignored — the managed Worker
     * enumerates the whole owner namespace and excludes the reserved `__` keys
     * server-side.
     */
    async list(_prefix) {
        const url = `${this.base}/${encodeURIComponent(this.userId)}/?list`;
        const res = await fetch(url, { method: 'GET', headers: this.authHeaders() });
        if (!res.ok)
            throw new Error(`sessions LIST failed: ${res.status} ${await safeText(res)}`);
        const body = (await res.json());
        if (!body || !Array.isArray(body.keys))
            return [];
        return body.keys.filter((k) => typeof k === 'string');
    }
    /** Delete an object (the Worker refunds its bytes to the quota ledger). */
    async delete(key) {
        const res = await fetch(this.objUrl(key), { method: 'DELETE', headers: this.authHeaders() });
        if (!res.ok && res.status !== 404) {
            throw new Error(`sessions DELETE ${key} failed: ${res.status} ${await safeText(res)}`);
        }
    }
}
function safeOwnerSegment(owner) {
    const value = owner.trim();
    if (!value || value === '.' || value === '..' || value.includes('/')) {
        throw new Error(`Invalid managed sessions user id: ${JSON.stringify(owner)}`);
    }
    return value;
}
/** Prevent URL dot-segment normalization from escaping the verified owner path. */
function safeObjectKey(key) {
    const segments = key.split('/');
    if (segments.length === 0 ||
        segments.some(segment => !segment || segment === '.' || segment === '..')) {
        throw new Error(`Invalid managed sessions object key: ${JSON.stringify(key)}`);
    }
    return segments;
}
async function safeText(res) {
    try {
        return (await res.text()).slice(0, 200);
    }
    catch {
        return '';
    }
}
