import { ApiClient } from "../../../internal/api-client.js";
import { resolveApiClient } from "../../../internal/index.js";
import { ApiError } from "../../memory/_lib/errors.js";
import { isRecord, } from "./shared.js";
// ── Shape predicates ───────────────────────────────────────────────────────
/**
 * Read the first array-valued key from a response body. Endpoints that may
 * return either a snake_case or camelCase collection key go through this so the
 * `body.snake ?? body.camel ?? []` triple isn't repeated at every call site.
 */
function pickArray(body, ...keys) {
    for (const key of keys) {
        const value = body[key];
        if (Array.isArray(value))
            return value;
    }
    return [];
}
/**
 * The server stores entity-type per-field config as a single `metadata_schema`
 * JSON Schema. The diff compares the desired config's flat `properties`/
 * `required` against the remote snapshot, so hoist them out of the returned
 * `metadata_schema` to the row's top level. Mirrors `upsertEntityType`, which
 * folds the flat fields back into `metadata_schema` when writing.
 */
function hoistEntityTypeSchema(row) {
    const schema = row.metadata_schema;
    const out = {
        slug: row.slug,
        ...(row.name !== undefined ? { name: row.name } : {}),
        ...(row.description !== undefined ? { description: row.description } : {}),
        // Preserve owning org so prune can skip public types from other orgs.
        ...(row.organization_id !== undefined
            ? { organization_id: row.organization_id }
            : {}),
    };
    if (isRecord(schema)) {
        if (isRecord(schema.properties))
            out.properties = schema.properties;
        if (Array.isArray(schema.required)) {
            out.required = schema.required.filter((v) => typeof v === "string");
        }
    }
    // A type is derived iff it has view SQL; stored types carry no backing, so it
    // compares equal to the desired side without churn. `backing_source` (a
    // connection slug) is hoisted to `backing.connection` only when set, so an
    // internal-backed view stays `{ sql }` and never churns.
    if (typeof row.backing_sql === "string") {
        out.backing = {
            sql: row.backing_sql,
            ...(typeof row.backing_source === "string" && row.backing_source
                ? { connection: row.backing_source }
                : {}),
        };
    }
    // Hoist metrics_config → `metrics` only when the column holds a non-empty
    // object, so a type with no declared metrics stays `undefined` on both sides
    // and never churns the diff (mirrors `backing`).
    if (isRecord(row.metrics_config) &&
        Object.keys(row.metrics_config).length > 0) {
        out.metrics = row.metrics_config;
    }
    // Hoist event_kinds only when non-empty, so a type with no declared kinds
    // stays `undefined` on both sides and never churns the diff (mirrors metrics).
    if (isRecord(row.event_kinds) && Object.keys(row.event_kinds).length > 0) {
        out.eventKinds = row.event_kinds;
    }
    return out;
}
/**
 * Typed wrappers for the existing server endpoints `lobu apply` calls.
 *
 * The class is open over an injectable `fetchImpl` so tests can stub the
 * network without monkey-patching globals. Real callers leave `fetchImpl`
 * unset and pick up `globalThis.fetch`.
 */
export class ApplyClient {
    orgSlug;
    http;
    constructor(cfg, fetchImpl = fetch) {
        this.orgSlug = cfg.orgSlug;
        this.http = new ApiClient(cfg.apiBaseUrl, cfg.token, fetchImpl);
    }
    /**
     * Delegates the fetch/parse/non-ok-error pipeline to the shared
     * {@link ApiClient}, then layers on the apply-specific shape:
     *   - the body is coerced to a record (`undefined`→`{}`, non-record→`{value}`)
     *   - a body-level `error` string is treated as a failure even on a 2xx
     *
     * Apply endpoints only ever return the listed `okStatuses` (200/201/204) on
     * success or a 4xx/5xx on failure, so `ApiClient`'s status gate produces the
     * same outcome the local pipeline did. `Content-Type: application/json` is
     * sent on every request (no `Accept`) to mirror the previous wire shape.
     */
    async request(method, path, body, okStatuses = [200, 201, 204]) {
        const { status, body: raw } = await this.http.requestWithStatus(method, path, body, { okStatuses, sendAccept: false, alwaysJsonContentType: true });
        let parsed;
        if (raw === undefined) {
            parsed = {};
        }
        else if (isRecord(raw)) {
            parsed = raw;
        }
        else {
            parsed = { value: raw };
        }
        if (typeof parsed.error === "string" && parsed.error.length > 0) {
            throw new ApiError(`${method} ${path} returned error: ${parsed.error}`, status);
        }
        return { status, body: parsed };
    }
    // ── Organization ──────────────────────────────────────────────────────────
    /**
     * Orgs the authenticated user belongs to, read from the OAuth userinfo
     * endpoint — the same source `lobu org list` uses. Used to check whether the
     * `[memory].org` slug already resolves to one of the operator's orgs. Does
     * not depend on `this.orgSlug`. (`lobu apply` can't create an org headlessly
     * — that needs a logged-in browser session — so there is no `createOrg`.)
     */
    async listOrgs() {
        const { body } = await this.request("GET", `/oauth/userinfo`);
        const orgs = Array.isArray(body.organizations) ? body.organizations : [];
        const out = [];
        for (const entry of orgs) {
            if (!isRecord(entry))
                continue;
            const id = typeof entry.id === "string" ? entry.id : "";
            const slug = typeof entry.slug === "string" ? entry.slug : "";
            if (!id || !slug)
                continue;
            out.push({
                id,
                slug,
                ...(typeof entry.name === "string" ? { name: entry.name } : {}),
            });
        }
        return out;
    }
    // ── Agents ────────────────────────────────────────────────────────────────
    async listAgents() {
        const { body } = await this.request("GET", `/api/${this.orgSlug}/agents`);
        return body.agents ?? [];
    }
    /**
     * Idempotent create: PR-2 makes `POST /` return 200 with the existing
     * payload when an agent of the same ID already exists in the same org.
     * Cross-org collision still surfaces as 409 with a clear `error.code` —
     * we re-throw verbatim so `lobu apply` can show the operator the link
     * to the org-scoped IDs issue.
     */
    async upsertAgent(agent) {
        const { body } = await this.request("POST", 
        // No trailing slash — Hono matches `routes.post('/', ...)` mounted at
        // `/api/:orgSlug/agents` against `/api/dev/agents`, not `/api/dev/agents/`.
        `/api/${this.orgSlug}/agents`, agent, [200, 201]);
        return body;
    }
    async patchAgentMetadata(agentId, agent) {
        await this.request("PATCH", `/api/${this.orgSlug}/agents/${encodeURIComponent(agentId)}`, agent);
    }
    async getAgentSettings(agentId) {
        try {
            const { body } = await this.request("GET", `/api/${this.orgSlug}/agents/${encodeURIComponent(agentId)}/config`);
            return body;
        }
        catch (err) {
            if (err instanceof ApiError && err.status === 404)
                return null;
            throw err;
        }
    }
    async patchAgentSettings(agentId, settings) {
        await this.request("PATCH", `/api/${this.orgSlug}/agents/${encodeURIComponent(agentId)}/config`, settings);
    }
    /**
     * Set (or rotate) the org-shared API key for a provider. Idempotent.
     * Lands in `agent_secrets` under `provider:<id>:apiKey`, scoped to the org.
     */
    async setProviderApiKey(agentId, providerId, value) {
        await this.request("PUT", `/api/${this.orgSlug}/agents/${encodeURIComponent(agentId)}/providers/${encodeURIComponent(providerId)}/api-key`, { value });
    }
    // ── Platforms ─────────────────────────────────────────────────────────────
    async listPlatforms(agentId) {
        const { body } = await this.request("GET", `/api/${this.orgSlug}/agents/${encodeURIComponent(agentId)}/platforms`);
        return body.platforms ?? [];
    }
    /**
     * Stable-ID upsert.
     *
     * Server contract:
     *   PUT /:agentId/platforms/by-stable-id/:stableId
     *   body: { platform, name?, config }
     *   response when unchanged: { noop: true, platform }
     *   response when changed:   { updated: true, willRestart: true, platform }
     *   response on first write: { created: true, platform }
     */
    async upsertPlatform(agentId, stableId, payload) {
        const { body } = await this.request("PUT", `/api/${this.orgSlug}/agents/${encodeURIComponent(agentId)}/platforms/by-stable-id/${encodeURIComponent(stableId)}`, payload);
        return body;
    }
    /**
     * Reconcile a platform's declarative channel bindings.
     *
     * Server contract:
     *   POST /:agentId/platforms/:platformId/sync-channels
     *   body: { channels: string[] }   // each "<teamId>/<channelId>"
     *   response: { bound: string[], removed: string[] }
     */
    async syncPlatformChannels(agentId, platformId, channels) {
        const { body } = await this.request("POST", `/api/${this.orgSlug}/agents/${encodeURIComponent(agentId)}/platforms/${encodeURIComponent(platformId)}/sync-channels`, { channels });
        return { bound: body.bound ?? [], removed: body.removed ?? [] };
    }
    // ── Memory schema ─────────────────────────────────────────────────────────
    async listEntityTypes() {
        const { body } = await this.request("POST", `/api/${this.orgSlug}/manage_entity_schema`, {
            schema_type: "entity_type",
            action: "list",
        });
        // The server returns the type's fields inside a single `metadata_schema`
        // JSON Schema (+ typed backing_* columns). Surface `properties`/`required`
        // and normalize `backing` at top level so the diff compares them against the
        // desired config (which carries them flat).
        return pickArray(body, "entity_types", "entityTypes").map(hoistEntityTypeSchema);
    }
    /**
     * The `manage_entity_schema` admin tool exposes separate `create` / `update`
     * actions and surfaces duplicates as a coded 409 (`[entity_type_exists]` /
     * `[relationship_type_exists]`). Probe with `create`; on that explicit
     * duplicate signal retry with `update`. Any other error (e.g. a 422
     * `[invalid_schema]` validation failure) propagates verbatim — retrying it
     * as an update used to mask the real message behind "Entity type not
     * found" (issue #1177).
     */
    async upsertSchemaResource(schemaType, payload) {
        const url = `/api/${this.orgSlug}/manage_entity_schema`;
        try {
            await this.request("POST", url, {
                schema_type: schemaType,
                action: "create",
                ...payload,
            });
            return { created: true };
        }
        catch (err) {
            if (err instanceof ApiError && isDuplicateError(err)) {
                await this.request("POST", url, {
                    schema_type: schemaType,
                    action: "update",
                    ...payload,
                });
                return { updated: true };
            }
            throw err;
        }
    }
    async upsertEntityType(
    // `metadata` is authoring-only — never sent to the server (see
    // DesiredEntityType); extra properties on the passed value are ignored.
    entity) {
        // The server stores per-type fields as a single `metadata_schema` JSON
        // Schema (`{ type, properties, required }`) — it does NOT read top-level
        // `properties`/`required`. Fold them into `metadata_schema` so the schema
        // actually persists (otherwise every apply re-reports a `properties`
        // update because the stored schema stays empty).
        const { slug, name, description, required, properties, eventKinds, backing, metrics, } = entity;
        const payload = { slug };
        if (name !== undefined)
            payload.name = name;
        if (description !== undefined)
            payload.description = description;
        if (properties !== undefined || required !== undefined) {
            payload.metadata_schema = {
                type: "object",
                properties: properties ?? {},
                ...(required && required.length > 0 ? { required } : {}),
            };
        }
        // Event kinds sent on every upsert so it is deterministic: an object declares
        // the type's kinds; `null` clears them. Stored verbatim in event_kinds.
        payload.event_kinds = eventKinds ?? null;
        // Backing is sent on every upsert so it is deterministic: `{ sql }` makes
        // the type derived; `null` makes it stored (and reverts a previously-derived
        // type). `connection` (a slug) is forwarded so the server can bind the view
        // to an external database; the server resolves slug → connection at read time.
        payload.backing = backing
            ? {
                sql: backing.sql,
                ...(backing.connection ? { connection: backing.connection } : {}),
            }
            : null;
        // Metrics sent on every upsert so it is deterministic: an object declares
        // the type's metrics; `null` clears them. Stored verbatim in metrics_config.
        payload.metrics_config = metrics ?? null;
        return this.upsertSchemaResource("entity_type", payload);
    }
    /**
     * Fetch an entity type's current default view template (`null` if none).
     * Apply uses this to diff ONLY the types it needs (declared templates, plus
     * every config type under prune) — the template is deliberately NOT returned
     * by the entity-type list, which the UI/bootstrap also calls.
     */
    async getEntityTypeViewTemplate(slug) {
        const { body } = await this.request("POST", `/api/${this.orgSlug}/manage_view_templates`, {
            action: "get",
            resource_type: "entity_type",
            resource_id: slug,
        });
        return body.default_tab?.current?.json_template ?? null;
    }
    /**
     * Set the entity type's default view template. A separate, version-appending
     * tool from the schema upsert, so apply calls this ONLY on create or a changed
     * template (see apply-cmd) — never every run, which would churn the history.
     */
    async setEntityTypeViewTemplate(slug, jsonTemplate) {
        await this.request("POST", `/api/${this.orgSlug}/manage_view_templates`, {
            action: "set",
            resource_type: "entity_type",
            resource_id: slug,
            json_template: jsonTemplate,
            change_notes: "lobu apply",
        });
    }
    /**
     * Clear the entity type's default view template (prune-gated removal). Nulls
     * the current-version pointer server-side; history rows stay for rollback.
     */
    async clearEntityTypeViewTemplate(slug) {
        await this.request("POST", `/api/${this.orgSlug}/manage_view_templates`, {
            action: "clear",
            resource_type: "entity_type",
            resource_id: slug,
        });
    }
    async listRelationshipTypes() {
        const { body } = await this.request("POST", `/api/${this.orgSlug}/manage_entity_schema`, {
            schema_type: "relationship_type",
            action: "list",
        });
        return pickArray(body, "relationship_types", "relationshipTypes");
    }
    /**
     * Fetch a relationship type's rules (the `list` action omits them, so the
     * apply diff can't otherwise see remote rules and would churn a perpetual
     * "rules changed" update). Maps the server's `*_entity_type_slug` columns to
     * `{ source, target }`; `id` is carried for reconcile (remove_rule by id).
     */
    async listRelationshipTypeRules(slug) {
        const { body } = await this.request("POST", `/api/${this.orgSlug}/manage_entity_schema`, {
            schema_type: "relationship_type",
            action: "list_rules",
            slug,
        });
        return (body.rules ?? [])
            .filter((r) => r.id != null && r.source_entity_type_slug && r.target_entity_type_slug)
            .map((r) => ({
            id: r.id,
            source: r.source_entity_type_slug,
            target: r.target_entity_type_slug,
        }));
    }
    async upsertRelationshipType(
    // `metadata` is authoring-only — never sent (see DesiredRelationshipType).
    rel) {
        const { rules, ...payload } = rel;
        const result = await this.upsertSchemaResource("relationship_type", payload);
        // Reconcile rules to exactly the desired set so config is the source of
        // truth (declarative). Without removing extras, dropping a rule from config
        // would never take effect AND would churn a perpetual "rules changed"
        // update on every apply. add_rule is idempotent; remove_rule takes a id.
        const desired = rules ?? [];
        const ruleKey = (r) => `${r.source}	${r.target}`;
        const desiredKeys = new Set(desired.map(ruleKey));
        const remote = await this.listRelationshipTypeRules(rel.slug);
        const remoteKeys = new Set(remote.map(ruleKey));
        for (const rule of desired) {
            if (remoteKeys.has(ruleKey(rule)))
                continue;
            try {
                await this.request("POST", `/api/${this.orgSlug}/manage_entity_schema`, {
                    schema_type: "relationship_type",
                    action: "add_rule",
                    slug: rel.slug,
                    source_entity_type_slug: rule.source,
                    target_entity_type_slug: rule.target,
                });
            }
            catch (err) {
                if (err instanceof ApiError && isDuplicateError(err))
                    continue;
                throw err;
            }
        }
        for (const rule of remote) {
            if (desiredKeys.has(ruleKey(rule)))
                continue;
            await this.request("POST", `/api/${this.orgSlug}/manage_entity_schema`, {
                schema_type: "relationship_type",
                action: "remove_rule",
                slug: rel.slug,
                rule_id: rule.id,
            });
        }
        return result;
    }
    /**
     * Delete an entity type (code-managed prune). The server soft-deletes and
     * REFUSES if instances of the type still exist — the data is exempt from
     * prune, so that surfaces as a clear error rather than cascading.
     */
    async deleteEntityType(slug) {
        await this.request("POST", `/api/${this.orgSlug}/manage_entity_schema`, {
            schema_type: "entity_type",
            action: "delete",
            slug,
        });
    }
    /** Delete a relationship type (code-managed prune). */
    async deleteRelationshipType(slug) {
        await this.request("POST", `/api/${this.orgSlug}/manage_entity_schema`, {
            schema_type: "relationship_type",
            action: "delete",
            slug,
        });
    }
    // ── Watchers ──────────────────────────────────────────────────────────────
    /**
     * Fetch a single watcher's full payload — `getWatcher` server-side, which
     * returns reaction_script (not in the list response). Used by
     * `lobu init --from-org` to round-trip reaction scripts back to sibling
     * `.ts` files.
     */
    async getWatcherDetail(watcherId) {
        try {
            const { body } = await this.request("GET", `/api/${this.orgSlug}/watchers?watcher_id=${encodeURIComponent(watcherId)}`);
            return body.watcher ?? null;
        }
        catch (err) {
            if (err instanceof ApiError && err.status === 404)
                return null;
            throw err;
        }
    }
    async listWatchers() {
        // `include_details=true` pulls the version-bound fields (prompt,
        // classifiers, keying_config, reactions_guidance) too.
        // Apply diffs against these to detect drift on prompt / sources / etc.
        const { body } = await this.request("GET", `/api/${this.orgSlug}/watchers?include_details=true`);
        return body.watchers ?? [];
    }
    /**
     * Create a watcher owned by `agentId`. Duplicate-slug surfaces as a
     * structured error the caller swallows for idempotency.
     */
    async createWatcher(payload) {
        const { body } = await this.request("POST", `/api/${this.orgSlug}/manage_watchers`, {
            action: "create",
            slug: payload.slug,
            agent_id: payload.agentId,
            ...(payload.name ? { name: payload.name } : {}),
            ...(payload.description ? { description: payload.description } : {}),
            prompt: payload.prompt,
            ...(payload.schedule ? { schedule: payload.schedule } : {}),
            ...(payload.sources?.length ? { sources: payload.sources } : {}),
            ...(payload.reactions_guidance !== undefined
                ? { reactions_guidance: payload.reactions_guidance }
                : {}),
            ...(payload.device_worker_id !== undefined
                ? { device_worker_id: payload.device_worker_id }
                : {}),
            ...(payload.scheduler_client_id !== undefined
                ? { scheduler_client_id: payload.scheduler_client_id }
                : {}),
            ...(payload.notification_channel !== undefined
                ? { notification_channel: payload.notification_channel }
                : {}),
            ...(payload.notification_priority !== undefined
                ? { notification_priority: payload.notification_priority }
                : {}),
            ...(payload.min_cooldown_seconds !== undefined
                ? { min_cooldown_seconds: payload.min_cooldown_seconds }
                : {}),
            ...(payload.tags?.length ? { tags: payload.tags } : {}),
            ...(payload.agent_kind !== undefined
                ? { agent_kind: payload.agent_kind }
                : {}),
            ...(payload.keying_config !== undefined
                ? { keying_config: payload.keying_config }
                : {}),
            ...(payload.classifiers !== undefined
                ? { classifiers: payload.classifiers }
                : {}),
        });
        return { ...(body.watcher_id ? { watcher_id: body.watcher_id } : {}) };
    }
    /**
     * Update the **scalar** fields on the `watchers` row — these don't require
     * a new version. Version-bound fields (prompt / sources / reactions_guidance /
     * keying_config / classifiers) require `createWatcherVersion`
     * instead.
     *
     * `null` clears nullable fields (device_worker_id, scheduler_client_id,
     * agent_kind) per the server contract.
     */
    async updateWatcher(payload) {
        await this.request("POST", `/api/${this.orgSlug}/manage_watchers`, {
            action: "update",
            watcher_id: payload.watcher_id,
            ...(payload.schedule !== undefined ? { schedule: payload.schedule } : {}),
            ...(payload.agent_id !== undefined ? { agent_id: payload.agent_id } : {}),
            ...(payload.device_worker_id !== undefined
                ? { device_worker_id: payload.device_worker_id }
                : {}),
            ...(payload.scheduler_client_id !== undefined
                ? { scheduler_client_id: payload.scheduler_client_id }
                : {}),
            ...(payload.notification_channel !== undefined
                ? { notification_channel: payload.notification_channel }
                : {}),
            ...(payload.notification_priority !== undefined
                ? { notification_priority: payload.notification_priority }
                : {}),
            ...(payload.min_cooldown_seconds !== undefined
                ? { min_cooldown_seconds: payload.min_cooldown_seconds }
                : {}),
            ...(payload.tags !== undefined ? { tags: payload.tags } : {}),
            ...(payload.agent_kind !== undefined
                ? { agent_kind: payload.agent_kind }
                : {}),
        });
    }
    /**
     * Create a new watcher_versions row carrying the version-bound fields, then
     * upgrade the watcher's `current_version_id` to that new version. Server
     * inherits unset fields from the previous version row.
     */
    async createWatcherVersion(payload) {
        const { body } = await this.request("POST", `/api/${this.orgSlug}/manage_watchers`, {
            action: "create_version",
            watcher_id: payload.watcher_id,
            set_as_current: true,
            ...(payload.prompt !== undefined ? { prompt: payload.prompt } : {}),
            ...(payload.sources !== undefined ? { sources: payload.sources } : {}),
            ...(payload.keying_config !== undefined
                ? { keying_config: payload.keying_config }
                : {}),
            ...(payload.classifiers !== undefined
                ? { classifiers: payload.classifiers }
                : {}),
            ...(payload.reactions_guidance !== undefined
                ? { reactions_guidance: payload.reactions_guidance }
                : {}),
            ...(payload.change_notes
                ? { change_notes: payload.change_notes }
                : { change_notes: "lobu apply" }),
        });
        return body.version !== undefined ? { version: body.version } : {};
    }
    /**
     * Attach (or clear) a reaction script. Pass an empty string to remove it —
     * matches the admin tool contract.
     */
    async setReactionScript(watcherId, reactionScript) {
        await this.request("POST", `/api/${this.orgSlug}/manage_watchers`, {
            action: "set_reaction_script",
            watcher_id: watcherId,
            reaction_script: reactionScript,
        });
    }
    /**
     * Delete a watcher by its numeric `watcher_id` (code-managed prune). The
     * admin tool takes an array; we delete one slug's watcher at a time so a
     * failure is attributable.
     */
    async deleteWatcher(watcherId) {
        await this.request("POST", `/api/${this.orgSlug}/manage_watchers`, {
            action: "delete",
            watcher_ids: [watcherId],
        });
    }
    // ── Connector definitions ─────────────────────────────────────────────────
    async connectionsTool(body) {
        const { body: parsed } = await this.request("POST", `/api/${this.orgSlug}/manage_connections`, body);
        return parsed;
    }
    async feedsTool(body) {
        const { body: parsed } = await this.request("POST", `/api/${this.orgSlug}/manage_feeds`, body);
        return parsed;
    }
    async authProfilesTool(body) {
        const { body: parsed } = await this.request("POST", `/api/${this.orgSlug}/manage_auth_profiles`, body);
        return parsed;
    }
    async catalogTool(body) {
        const { body: parsed } = await this.request("POST", `/api/${this.orgSlug}/manage_catalog`, body);
        return parsed;
    }
    /** Installed org connectors + (with `includeInstallable`) the bundled catalog. */
    async listConnectors(includeInstallable = true) {
        const body = await this.catalogTool({
            action: "list_installed",
            kinds: ["connectors"],
            include_catalog: includeInstallable,
        });
        const items = body.installed?.connectors?.items ?? [];
        return items.map((item) => mapConnectorDefinitionItem(item));
    }
    /**
     * Idempotent connector install. The CLI passes raw TypeScript source
     * (`compiled: false`) or a `source_url`; the server compiles + extracts
     * metadata and returns the resolved `connectorKey` plus `updated` (false
     * when the installed code is byte-identical).
     */
    async installConnector(payload) {
        const body = await this.connectionsTool({
            action: "install_connector",
            ...(payload.sourceCode !== undefined
                ? {
                    source_code: payload.sourceCode,
                    compiled: payload.compiled ?? false,
                }
                : {}),
            ...(payload.sourceUrl ? { source_url: payload.sourceUrl } : {}),
            ...(payload.sourceUri ? { source_uri: payload.sourceUri } : {}),
        });
        return {
            connectorKey: body.connector_key ?? "",
            updated: body.updated ?? false,
            ...(body.version ? { version: body.version } : {}),
        };
    }
    async uninstallConnector(connectorKey) {
        await this.connectionsTool({
            action: "uninstall_connector",
            connector_key: connectorKey,
        });
    }
    // ── Auth profiles ─────────────────────────────────────────────────────────
    async listAuthProfiles() {
        const body = await this.authProfilesTool({ action: "list_auth_profiles" });
        return body.auth_profiles ?? [];
    }
    async getAuthProfileBySlug(slug) {
        try {
            const body = await this.authProfilesTool({ action: "get_auth_profile", auth_profile_slug: slug });
            return body.auth_profile ?? null;
        }
        catch (err) {
            if (err instanceof ApiError && err.status === 404)
                return null;
            throw err;
        }
    }
    async createAuthProfile(payload) {
        const body = await this.authProfilesTool({
            action: "create_auth_profile",
            connector_key: payload.connector,
            profile_kind: payload.kind,
            display_name: payload.name ?? payload.slug,
            slug: payload.slug,
            ...(payload.credentials && Object.keys(payload.credentials).length > 0
                ? { credentials: payload.credentials }
                : {}),
        });
        return {
            created: true,
            updated: false,
            ...(body.auth_profile?.status
                ? { status: body.auth_profile.status }
                : {}),
            ...(body.connect_url ? { connectUrl: body.connect_url } : {}),
        };
    }
    async updateAuthProfile(payload) {
        const body = await this.authProfilesTool({
            action: "update_auth_profile",
            auth_profile_slug: payload.slug,
            ...(payload.name ? { display_name: payload.name } : {}),
            ...(payload.credentials && Object.keys(payload.credentials).length > 0
                ? { credentials: payload.credentials }
                : {}),
        });
        return {
            created: false,
            updated: true,
            ...(body.auth_profile?.status
                ? { status: body.auth_profile.status }
                : {}),
            ...(body.connect_url ? { connectUrl: body.connect_url } : {}),
        };
    }
    /** Re-issue a connect token for an existing interactive-auth profile. */
    async reconnectAuthProfile(slug) {
        const body = await this.authProfilesTool({
            action: "update_auth_profile",
            auth_profile_slug: slug,
            reconnect: true,
        });
        return body.connect_url;
    }
    // ── Connections ───────────────────────────────────────────────────────────
    async listConnections() {
        const body = await this.connectionsTool({ action: "list", limit: 500 });
        return body.connections ?? [];
    }
    async createConnection(payload) {
        const body = await this.connectionsTool({
            action: "create",
            connector_key: payload.connector,
            slug: payload.slug,
            ...(payload.name ? { display_name: payload.name } : {}),
            ...(payload.authProfileSlug
                ? { auth_profile_slug: payload.authProfileSlug }
                : {}),
            ...(payload.appAuthProfileSlug
                ? { app_auth_profile_slug: payload.appAuthProfileSlug }
                : {}),
            ...(payload.config ? { config: payload.config } : {}),
            ...(payload.deviceWorkerId
                ? { device_worker_id: payload.deviceWorkerId }
                : {}),
        });
        if (!body.connection) {
            throw new ApiError(`create connection "${payload.slug}" returned no connection payload`);
        }
        return body.connection;
    }
    async updateConnection(connectionId, payload) {
        const body = await this.connectionsTool({
            action: "update",
            connection_id: connectionId,
            ...(payload.name !== undefined ? { display_name: payload.name } : {}),
            ...(payload.authProfileSlug !== undefined
                ? { auth_profile_slug: payload.authProfileSlug }
                : {}),
            ...(payload.appAuthProfileSlug !== undefined
                ? { app_auth_profile_slug: payload.appAuthProfileSlug }
                : {}),
            // `lobu apply` is declarative — replace, don't merge, so removed
            // manifest keys disappear remotely (server defaults to merge).
            ...(payload.config !== undefined
                ? { config: payload.config, replace_config: true }
                : {}),
            ...(payload.deviceWorkerId !== undefined
                ? { device_worker_id: payload.deviceWorkerId }
                : {}),
        });
        if (!body.connection) {
            throw new ApiError(`update connection #${connectionId} returned no connection payload`);
        }
        return body.connection;
    }
    // ── Feeds (managed per-connection) ────────────────────────────────────────
    async listFeeds(connectionId) {
        const body = await this.feedsTool({
            action: "list_feeds",
            connection_id: connectionId,
            limit: 500,
        });
        return body.feeds ?? [];
    }
    async createFeed(payload) {
        const body = await this.feedsTool({
            action: "create_feed",
            connection_id: payload.connectionId,
            feed_key: payload.feedKey,
            ...(payload.name ? { display_name: payload.name } : {}),
            ...(payload.schedule ? { schedule: payload.schedule } : {}),
            ...(payload.config ? { config: payload.config } : {}),
        });
        if (!body.feed) {
            throw new ApiError(`create feed "${payload.feedKey}" returned no feed payload`);
        }
        return body.feed;
    }
    async updateFeed(feedId, payload) {
        const body = await this.feedsTool({
            action: "update_feed",
            feed_id: feedId,
            ...(payload.name !== undefined ? { display_name: payload.name } : {}),
            ...(payload.schedule !== undefined ? { schedule: payload.schedule } : {}),
            ...(payload.config !== undefined
                ? { config: payload.config, replace_config: true }
                : {}),
        });
        if (!body.feed) {
            throw new ApiError(`update feed #${feedId} returned no feed payload`);
        }
        return body.feed;
    }
}
function mapConnectorDefinitionItem(item) {
    const detail = item.detail ?? {};
    const installed = detail.installed !== false;
    return {
        key: item.id,
        name: item.name,
        version: typeof detail.version === "string" ? detail.version : undefined,
        options_schema: detail.options_schema && typeof detail.options_schema === "object"
            ? detail.options_schema
            : null,
        feeds_schema: detail.feeds_schema && typeof detail.feeds_schema === "object"
            ? detail.feeds_schema
            : null,
        auth_schema: detail.auth_schema && typeof detail.auth_schema === "object"
            ? detail.auth_schema
            : null,
        installed,
        installable: Boolean(detail.installable ?? !installed),
        catalog_origin: detail.catalog_origin === "catalog" || detail.catalog_origin === "org"
            ? detail.catalog_origin
            : installed
                ? "org"
                : "catalog",
        source_uri: typeof detail.source_uri === "string" ? detail.source_uri : null,
    };
}
/**
 * Recognise duplicate-name errors from the admin tools without substring
 * matching the user-facing message. The server stamps a bracketed code into
 * the error message of every duplicate path `apply` upserts through
 * (`manage_entity_schema` create / add_rule → `[entity_type_exists]`,
 * `[relationship_type_exists]`, `[already_exists]`, all httpStatus 409).
 *
 * Anything else is NOT a duplicate. In particular a 422 schema-validation
 * error (`[invalid_schema]`, e.g. ">4 x-table-column fields") must surface
 * verbatim — the old status-only fallback treated any 4xx as "already
 * exists", retried with `action: update`, and buried the real message under
 * a misleading "Entity type not found" (issue #1177). The 409 fallback stays
 * as a belt-and-braces signal for duplicate paths that predate the codes.
 */
export function isDuplicateError(err) {
    if (typeof err.status !== "number")
        return false;
    const message = err.message.toLowerCase();
    if (message.includes("[entity_type_exists]") ||
        message.includes("[relationship_type_exists]") ||
        message.includes("[already_exists]")) {
        return true;
    }
    return err.status === 409;
}
export async function resolveApplyClient(opts) {
    const { token, apiBaseUrl, orgSlug } = await resolveApiClient({
        org: opts.org,
        apiUrl: opts.url,
        fetchImpl: opts.fetchImpl,
    });
    const client = new ApplyClient({ apiBaseUrl, orgSlug, token }, opts.fetchImpl);
    return { client, apiBaseUrl, orgSlug };
}
//# sourceMappingURL=client.js.map