// B5 — plans-client: HTTP client to plan-agent-do's /v1/plans/* relays.
//
// Cloud URL discovery:
//   1. env var $OLAM_CLOUD_URL (preferred for ops scripts)
//   2. ~/.olam/cloud-url file (operator-set; chmod 644)
//
// Bearer auth:
//   The relay paths on plan-agent-do gate via showcase Basic auth
//   (operator:$SHOWCASE_PASSWORD). v1 of this CLI reads
//   $OLAM_SHOWCASE_PASSWORD or ~/.olam/showcase-password. A future
//   pass replaces this with the operator-id-keyed bearer once the
//   cloud-side auth flow lands.
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
export class PlansClientError extends Error {
    status;
    body;
    constructor(status, message, body) {
        super(message);
        this.status = status;
        this.body = body;
        this.name = 'PlansClientError';
    }
}
const OLAM_HOME = path.join(os.homedir(), '.olam');
function readFileOrNull(p) {
    try {
        return fs.readFileSync(p, 'utf8').trim();
    }
    catch {
        return null;
    }
}
/**
 * Resolve cloud config from env + dotfiles. Returns null when no
 * cloud config is present (callers surface that as
 * "olam cloud not configured; run olam auth setup-cloud or set
 * OLAM_CLOUD_URL").
 */
export function resolveCloudConfig() {
    const cloudUrl = process.env.OLAM_CLOUD_URL
        ?? readFileOrNull(path.join(OLAM_HOME, 'cloud-url'));
    if (!cloudUrl)
        return null;
    const bearer = process.env.OLAM_SHOWCASE_PASSWORD
        ?? readFileOrNull(path.join(OLAM_HOME, 'showcase-password'));
    if (!bearer)
        return null;
    return { cloudUrl: cloudUrl.replace(/\/+$/, ''), bearer };
}
export class PlansClient {
    cfg;
    constructor(cfg) {
        this.cfg = cfg;
    }
    /** Build the showcase Basic-auth header. */
    authHeader() {
        const credentials = Buffer.from(`operator:${this.cfg.bearer}`).toString('base64');
        return `Basic ${credentials}`;
    }
    async request(path, init) {
        const headers = {
            Authorization: this.authHeader(),
        };
        if (init.body)
            headers['content-type'] = 'application/json';
        if (init.adminSecret)
            headers['X-Admin-Secret'] = init.adminSecret;
        return fetch(`${this.cfg.cloudUrl}${path}`, {
            method: init.method,
            headers,
            body: init.body ? JSON.stringify(init.body) : undefined,
        });
    }
    async list(operator_id) {
        const res = await this.request(`/v1/plans/list?operator_id=${encodeURIComponent(operator_id)}`, {
            method: 'GET',
        });
        if (!res.ok)
            throw new PlansClientError(res.status, `list failed: ${res.status}`, await res.text());
        const json = (await res.json());
        return json.plans;
    }
    async show(plan_id, operator_id) {
        const res = await this.request(`/v1/plans/${encodeURIComponent(plan_id)}?operator_id=${encodeURIComponent(operator_id)}`, { method: 'GET' });
        if (!res.ok)
            throw new PlansClientError(res.status, `show failed: ${res.status}`, await res.text());
        return (await res.json());
    }
    async remove(plan_id, operator_id) {
        const res = await this.request(`/v1/plans/${encodeURIComponent(plan_id)}?operator_id=${encodeURIComponent(operator_id)}`, { method: 'DELETE' });
        if (!res.ok && res.status !== 404) {
            throw new PlansClientError(res.status, `delete failed: ${res.status}`, await res.text());
        }
    }
    /**
     * Pin a plan to skip the TTL pruner until `pinned_until_ms`. Calls
     * /v1/plans/update-urls with a synthetic body — but plans-index
     * doesn't expose a pin path yet. v1 wires this via update-urls
     * with a deliberate empty URL update + a sibling POST that we'll
     * stub here pending B5+ follow-up. For now: throws not_implemented
     * so the operator sees a clear error.
     */
    async pin(_plan_id, _operator_id, _pinned_until_ms) {
        throw new PlansClientError(501, 'pin requires /v1/plans/update-pin endpoint (queued as B5 follow-up). ' +
            'Pin manually via DO console for now.');
    }
    async unpin(_plan_id, _operator_id) {
        throw new PlansClientError(501, 'unpin requires /v1/plans/update-pin endpoint (queued as B5 follow-up).');
    }
    async reregister(plan_id, confirmation_token) {
        const res = await this.request('/v1/plans/re-register', {
            method: 'POST',
            body: { plan_id, confirmation_token },
        });
        if (!res.ok) {
            throw new PlansClientError(res.status, `re-register failed: ${res.status}`, await res.text());
        }
    }
    /**
     * C6 — Admin aggregate by operator. Requires admin_secret in
     * X-Admin-Secret header (in addition to the operator's showcase
     * Basic auth at the relay layer).
     */
    async adminAggregate(admin_secret) {
        const res = await this.request('/v1/admin/aggregate-by-operator', {
            method: 'GET',
            adminSecret: admin_secret,
        });
        if (!res.ok) {
            throw new PlansClientError(res.status, `aggregate failed: ${res.status}`, await res.text());
        }
        return (await res.json());
    }
}
//# sourceMappingURL=plans-client.js.map