import { and, eq, isNull } from "drizzle-orm";
import { plugins, pluginState } from "@zmeel/db";
import { notFound } from "../errors.js";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Default namespace used when the plugin does not specify one. */
const DEFAULT_NAMESPACE = "default";
/**
 * Build the WHERE clause conditions for a scoped state lookup.
 *
 * The five-part composite key is:
 *   `(pluginId, scopeKind, scopeId, namespace, stateKey)`
 *
 * `scopeId` may be null (for `instance` scope) or a non-empty string.
 */
function scopeConditions(pluginId, scopeKind, scopeId, namespace, stateKey) {
    const conditions = [
        eq(pluginState.pluginId, pluginId),
        eq(pluginState.scopeKind, scopeKind),
        eq(pluginState.namespace, namespace),
        eq(pluginState.stateKey, stateKey),
    ];
    if (scopeId != null && scopeId !== "") {
        conditions.push(eq(pluginState.scopeId, scopeId));
    }
    else {
        conditions.push(isNull(pluginState.scopeId));
    }
    return and(...conditions);
}
// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------
/**
 * Plugin State Store — scoped key-value persistence for plugin workers.
 *
 * Provides `get`, `set`, `delete`, and `list` operations over the
 * `plugin_state` table. Each plugin's data is strictly namespaced by
 * `pluginId` so plugins cannot read or write each other's state.
 *
 * This service implements the server-side backing for the `ctx.state` SDK
 * client exposed to plugin workers. The host is responsible for:
 * - enforcing `plugin.state.read` capability before calling `get` / `list`
 * - enforcing `plugin.state.write` capability before calling `set` / `delete`
 *
 * @see PLUGIN_SPEC.md §14 — SDK Surface (`ctx.state`)
 * @see PLUGIN_SPEC.md §15.1 — Capabilities: Plugin State
 * @see PLUGIN_SPEC.md §21.3 — `plugin_state` table
 */
export function pluginStateStore(db) {
    // -----------------------------------------------------------------------
    // Internal helpers
    // -----------------------------------------------------------------------
    async function assertPluginExists(pluginId) {
        const rows = await db
            .select({ id: plugins.id })
            .from(plugins)
            .where(eq(plugins.id, pluginId));
        if (rows.length === 0) {
            throw notFound(`Plugin not found: ${pluginId}`);
        }
    }
    // -----------------------------------------------------------------------
    // Public API
    // -----------------------------------------------------------------------
    return {
        /**
         * Read a state value.
         *
         * Returns the stored JSON value, or `null` if no entry exists for the
         * given scope and key.
         *
         * Requires `plugin.state.read` capability (enforced by the caller).
         *
         * @param pluginId - UUID of the owning plugin
         * @param scopeKind - Granularity of the scope
         * @param scopeId - Identifier for the scoped entity (null for `instance` scope)
         * @param stateKey - The key to read
         * @param namespace - Sub-namespace (defaults to `"default"`)
         */
        get: async (pluginId, scopeKind, stateKey, { scopeId, namespace = DEFAULT_NAMESPACE, } = {}) => {
            const rows = await db
                .select()
                .from(pluginState)
                .where(scopeConditions(pluginId, scopeKind, scopeId, namespace, stateKey));
            return rows[0]?.valueJson ?? null;
        },
        /**
         * Write (create or replace) a state value.
         *
         * Uses an upsert so the caller does not need to check for prior existence.
         * On conflict (same composite key) the existing row's `value_json` and
         * `updated_at` are overwritten.
         *
         * Requires `plugin.state.write` capability (enforced by the caller).
         *
         * @param pluginId - UUID of the owning plugin
         * @param input - Scope key and value to store
         */
        set: async (pluginId, input) => {
            await assertPluginExists(pluginId);
            const namespace = input.namespace ?? DEFAULT_NAMESPACE;
            const scopeId = input.scopeId ?? null;
            await db
                .insert(pluginState)
                .values({
                pluginId,
                scopeKind: input.scopeKind,
                scopeId,
                namespace,
                stateKey: input.stateKey,
                valueJson: input.value,
                updatedAt: new Date(),
            })
                .onConflictDoUpdate({
                target: [
                    pluginState.pluginId,
                    pluginState.scopeKind,
                    pluginState.scopeId,
                    pluginState.namespace,
                    pluginState.stateKey,
                ],
                set: {
                    valueJson: input.value,
                    updatedAt: new Date(),
                },
            });
        },
        /**
         * Delete a state value.
         *
         * No-ops silently if the entry does not exist (idempotent by design).
         *
         * Requires `plugin.state.write` capability (enforced by the caller).
         *
         * @param pluginId - UUID of the owning plugin
         * @param scopeKind - Granularity of the scope
         * @param stateKey - The key to delete
         * @param scopeId - Identifier for the scoped entity (null for `instance` scope)
         * @param namespace - Sub-namespace (defaults to `"default"`)
         */
        delete: async (pluginId, scopeKind, stateKey, { scopeId, namespace = DEFAULT_NAMESPACE, } = {}) => {
            await db
                .delete(pluginState)
                .where(scopeConditions(pluginId, scopeKind, scopeId, namespace, stateKey));
        },
        /**
         * List all state entries for a plugin, optionally filtered by scope.
         *
         * Returns all matching rows as `PluginStateRecord`-shaped objects.
         * The `valueJson` field contains the stored value.
         *
         * Requires `plugin.state.read` capability (enforced by the caller).
         *
         * @param pluginId - UUID of the owning plugin
         * @param filter - Optional scope filters (scopeKind, scopeId, namespace)
         */
        list: async (pluginId, filter = {}) => {
            const conditions = [eq(pluginState.pluginId, pluginId)];
            if (filter.scopeKind !== undefined) {
                conditions.push(eq(pluginState.scopeKind, filter.scopeKind));
            }
            if (filter.scopeId !== undefined) {
                conditions.push(eq(pluginState.scopeId, filter.scopeId));
            }
            if (filter.namespace !== undefined) {
                conditions.push(eq(pluginState.namespace, filter.namespace));
            }
            return db
                .select()
                .from(pluginState)
                .where(and(...conditions));
        },
        /**
         * Delete all state entries owned by a plugin.
         *
         * Called during plugin uninstall when `removeData = true`. Also useful
         * for resetting a plugin's state during testing.
         *
         * @param pluginId - UUID of the owning plugin
         */
        deleteAll: async (pluginId) => {
            await db
                .delete(pluginState)
                .where(eq(pluginState.pluginId, pluginId));
        },
    };
}
//# sourceMappingURL=plugin-state-store.js.map