/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { RuntimeRuleApiError } from './types.js';
/**
 * Typed wrapper for the eight runtime-rule REST endpoints v1 binds to.
 *
 * Each method speaks the canonical route — the `/runtime/mal/otel/...`,
 * `/runtime/mal/log/...`, `/runtime/lal/...` shortcut variants are
 * sugar for scripted ops and aren't surfaced here.
 */
export class RuntimeRuleClient {
    fetchImpl;
    base;
    defaultHeaders;
    timeoutMs;
    constructor(options) {
        this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
        this.base = options.adminUrl.replace(/\/$/, '');
        this.defaultHeaders = options.headers ?? {};
        this.timeoutMs = options.timeoutMs ?? 0;
    }
    /** `GET /runtime/rule/list[?catalog=]` */
    async list(catalog) {
        const url = this.url('/runtime/rule/list', catalog ? { catalog } : undefined);
        const res = await this.send(url, { method: 'GET' });
        if (!res.ok)
            throw await this.toError(res, url);
        return (await res.json());
    }
    /** `GET /runtime/rule/bundled?catalog=[&withContent=]` */
    async listBundled(catalog, withContent = true) {
        const url = this.url('/runtime/rule/bundled', {
            catalog,
            withContent: String(withContent),
        });
        const res = await this.send(url, { method: 'GET' });
        if (!res.ok)
            throw await this.toError(res, url);
        return (await res.json());
    }
    /** `GET /runtime/rule?catalog=&name=[&source=]`. Returns {@link
     *  NotModified} when the server replies 304 to a conditional fetch. */
    async get(args) {
        const params = {
            catalog: args.catalog,
            name: args.name,
        };
        if (args.source)
            params.source = args.source;
        const url = this.url('/runtime/rule', params);
        const headers = { Accept: 'application/x-yaml' };
        if (args.ifNoneMatch)
            headers['If-None-Match'] = args.ifNoneMatch;
        const res = await this.send(url, { method: 'GET', headers });
        if (res.status === 304) {
            return {
                notModified: true,
                etag: res.headers.get('ETag') ?? '',
                contentHash: res.headers.get('X-Sw-Content-Hash') ?? '',
                status: res.headers.get('X-Sw-Status') ?? 'n/a',
            };
        }
        if (!res.ok)
            throw await this.toError(res, url);
        const body = await res.text();
        return {
            status: res.headers.get('X-Sw-Status') ?? 'n/a',
            source: res.headers.get('X-Sw-Source') ?? 'runtime',
            contentHash: res.headers.get('X-Sw-Content-Hash') ?? '',
            updateTime: Number(res.headers.get('X-Sw-Update-Time') ?? '0'),
            etag: res.headers.get('ETag') ?? '',
            content: body,
        };
    }
    /** `POST /runtime/rule/addOrUpdate?catalog=&name=[&allowStorageChange=][&force=]` */
    async addOrUpdate(args) {
        const params = {
            catalog: args.catalog,
            name: args.name,
        };
        if (args.allowStorageChange)
            params.allowStorageChange = 'true';
        if (args.force)
            params.force = 'true';
        const url = this.url('/runtime/rule/addOrUpdate', params);
        const res = await this.send(url, {
            method: 'POST',
            headers: { 'Content-Type': 'text/plain' },
            body: args.body,
        });
        return this.expectApplyResult(res, url);
    }
    /** `POST /runtime/rule/inactivate?catalog=&name=` (Design A — see
     *  `reference_runtime_rule_api.md`). */
    async inactivate(catalog, name) {
        const url = this.url('/runtime/rule/inactivate', { catalog, name });
        const res = await this.send(url, { method: 'POST' });
        return this.expectApplyResult(res, url);
    }
    /** `POST /runtime/rule/delete?catalog=&name=[&mode=revertToBundled]`.
     *  The two-step gate is server-side: a 409 `requires_inactivate_first`
     *  is surfaced via {@link RuntimeRuleApiError}. */
    async delete(catalog, name, mode = '') {
        const params = { catalog, name };
        if (mode)
            params.mode = mode;
        const url = this.url('/runtime/rule/delete', params);
        const res = await this.send(url, { method: 'POST' });
        return this.expectApplyResult(res, url);
    }
    /** `GET /runtime/rule/dump` or `/dump/{catalog}` — streams `tar.gz`. */
    async dump(catalog) {
        const path = catalog
            ? `/runtime/rule/dump/${encodeURIComponent(catalog)}`
            : '/runtime/rule/dump';
        const url = this.url(path);
        const res = await this.send(url, { method: 'GET' });
        if (!res.ok)
            throw await this.toError(res, url);
        return res;
    }
    // ─── private helpers ─────────────────────────────────────────────
    url(path, params) {
        const u = new URL(this.base + path);
        if (params) {
            for (const [k, v] of Object.entries(params))
                u.searchParams.set(k, v);
        }
        return u.toString();
    }
    async send(url, init) {
        const headers = {
            ...this.defaultHeaders,
            ...(init.headers ?? {}),
        };
        const finalInit = { ...init, headers };
        if (this.timeoutMs > 0) {
            const controller = new AbortController();
            const timer = setTimeout(() => controller.abort(), this.timeoutMs);
            try {
                return await this.fetchImpl(url, { ...finalInit, signal: controller.signal });
            }
            finally {
                clearTimeout(timer);
            }
        }
        return this.fetchImpl(url, finalInit);
    }
    /** Handle the apply-result-or-error response shape that addOrUpdate /
     *  inactivate / delete share. 2xx returns the parsed JSON; everything
     *  else throws a {@link RuntimeRuleApiError} with the parsed body so
     *  callers can switch on `applyStatus` (e.g. the 409
     *  `storage_change_requires_explicit_approval` path). */
    async expectApplyResult(res, url) {
        if (res.status >= 200 && res.status < 300) {
            return (await res.json());
        }
        throw await this.toError(res, url);
    }
    async toError(res, url) {
        const text = await res.text();
        let parsed = text;
        try {
            const json = JSON.parse(text);
            if (typeof json.applyStatus === 'string' && typeof json.message === 'string') {
                parsed = json;
            }
        }
        catch {
            // not JSON; keep the raw text.
        }
        return new RuntimeRuleApiError(res.status, parsed, url);
    }
}
//# sourceMappingURL=runtime-rule.js.map