/**
 * Backup Cloud Client (P3)
 *
 * OSS-side HTTP client for the Pro-gated cloud backup service
 * (`/api/cloud/backup`). Used by `crewly backup push/pull/list` to park an
 * archive in the cloud and pull it back on another machine.
 *
 * Transport-only: it streams files to/from the cloud and surfaces a friendly
 * upgrade error on 402. Auth (access token) + base URL come from
 * CloudClientService; both are injected so this is testable without the cloud.
 *
 * @module services/backup/backup-cloud.client
 */
import { createReadStream, createWriteStream } from 'node:fs';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
/** Thrown when the account isn't Pro (HTTP 402). */
export class BackupNotProError extends Error {
    code = 'upgrade_required';
    constructor(message = 'Cloud backup requires a Pro plan. Upgrade at https://crewlyai.com/pricing.') {
        super(message);
        this.name = 'BackupNotProError';
    }
}
/** Thrown on any other cloud backup HTTP failure. */
export class BackupCloudError extends Error {
    status;
    constructor(message, status) {
        super(message);
        this.status = status;
        this.name = 'BackupCloudError';
    }
}
/** HTTP client for the cloud backup endpoints. */
export class BackupCloudClient {
    opts;
    fetchImpl;
    constructor(opts) {
        this.opts = opts;
        this.fetchImpl = opts.fetchImpl ?? fetch;
    }
    url(suffix) {
        return `${this.opts.baseUrl.replace(/\/$/, '')}/api/cloud/backup${suffix}`;
    }
    authHeaders(extra = {}) {
        return { Authorization: `Bearer ${this.opts.token}`, ...extra };
    }
    /** List this account's cloud snapshots (newest first). */
    async list() {
        const res = await this.fetchImpl(this.url(''), { headers: this.authHeaders() });
        await this.assertOk(res);
        const json = (await res.json());
        return json.data?.backups ?? [];
    }
    /**
     * Upload an archive to the cloud. Streams the file with its Content-Length so
     * the service can enforce quota before accepting the blob.
     *
     * @param archivePath - Local archive (.tar.gz)
     * @param meta - Optional backupId + device/sha metadata
     * @returns The created snapshot record
     */
    async push(archivePath, meta = {}) {
        const stat = await fs.stat(archivePath);
        const query = meta.backupId ? `?backupId=${encodeURIComponent(meta.backupId)}` : '';
        const headers = this.authHeaders({
            'Content-Type': 'application/gzip',
            'Content-Length': String(stat.size),
            ...(meta.deviceName ? { 'X-Device-Name': meta.deviceName } : {}),
            ...(meta.deviceId ? { 'X-Device-Id': meta.deviceId } : {}),
            ...(meta.sha256 ? { 'X-Backup-Sha256': meta.sha256 } : {}),
        });
        const res = await this.fetchImpl(this.url(query), {
            method: 'POST',
            headers,
            body: createReadStream(archivePath),
            // Node fetch requires this for a streaming request body.
            duplex: 'half',
        });
        await this.assertOk(res);
        const json = (await res.json());
        return json.data;
    }
    /**
     * Download a snapshot to `destPath` (streamed).
     *
     * @param backupId - Snapshot id
     * @param destPath - Local file to write
     */
    async pull(backupId, destPath) {
        const res = await this.fetchImpl(this.url(`/${encodeURIComponent(backupId)}`), { headers: this.authHeaders() });
        await this.assertOk(res);
        if (!res.body)
            throw new BackupCloudError('Empty download response from cloud.');
        await fs.mkdir(path.dirname(destPath), { recursive: true });
        await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath));
    }
    /** Delete a cloud snapshot. */
    async remove(backupId) {
        const res = await this.fetchImpl(this.url(`/${encodeURIComponent(backupId)}`), {
            method: 'DELETE',
            headers: this.authHeaders(),
        });
        await this.assertOk(res);
    }
    /** Map non-2xx responses to typed errors (402 → upgrade). */
    async assertOk(res) {
        if (res.ok)
            return;
        if (res.status === 402) {
            let msg;
            try {
                msg = (await res.json()).error;
            }
            catch {
                /* ignore */
            }
            throw new BackupNotProError(msg);
        }
        let message = `Cloud backup request failed (HTTP ${res.status}).`;
        try {
            const j = (await res.json());
            if (j.error)
                message = j.error;
        }
        catch {
            /* non-JSON body */
        }
        throw new BackupCloudError(message, res.status);
    }
}
//# sourceMappingURL=backup-cloud.client.js.map