/**
 * Tool Policy Engine — facade class (M8 — see
 * `.cursor/plans/m8-design.md` § 2).
 *
 * Wraps `composeDecision(...)` with engine-instance state:
 *   - bound inputs (`tools`, `assessment`, `currentItemRef`, ...)
 *   - registered custom `PolicySource`s
 *   - `onPolicyChange` subscriber bus
 *   - `dispose()` clean-up
 *
 * The class has no DOM dependency and no Svelte runes. It is safe to
 * instantiate inside a worker, in a Node test, or inside a CE
 * connectedCallback. The Svelte context glue (`TOOL_POLICY_ENGINE_KEY`)
 * lives in `engine-context.ts`.
 *
 * PR 1 ships the engine *without callers*. PR 2 wires it into
 * `ToolkitCoordinator`. PR 3 switches `<pie-item-toolbar>` over.
 */
import { normalizeToolsConfig } from "../../services/tools-config-normalizer.js";
import { composeDecision } from "./compose-decision.js";
import { resolveDefaultPnpEnforcement } from "./pnp-policy-inputs.js";
import { PnpPolicySource } from "../sources/PnpPolicySource.js";
const DEFAULT_TOOLS = normalizeToolsConfig({
    policy: { allowed: [], blocked: [] },
    placement: { section: [], item: [], passage: [] },
    providers: {},
});
export class ToolPolicyEngine {
    toolRegistry;
    contextId;
    pnpPolicySource;
    customSources;
    listeners = new Set();
    tools;
    assessment;
    currentItemRef;
    pnpEnforcement;
    disposed = false;
    constructor(args) {
        this.toolRegistry = args.toolRegistry;
        this.contextId = args.contextId ?? "tool-policy";
        this.pnpPolicySource = new PnpPolicySource(this.toolRegistry);
        this.customSources = args.customSources ? [...args.customSources] : [];
        const inputs = args.inputs ?? {};
        this.tools = inputs.tools ?? DEFAULT_TOOLS;
        this.assessment = inputs.assessment ?? null;
        this.currentItemRef = inputs.currentItemRef ?? null;
        this.pnpEnforcement =
            inputs.pnpEnforcement ??
                resolveDefaultPnpEnforcement({
                    assessment: this.assessment,
                    currentItemRef: this.currentItemRef,
                });
    }
    /**
     * Resolve the visible tool set for a given placement level and
     * scope.
     *
     * `visibleTools` and `diagnostics` are deterministic with respect
     * to the engine's currently-bound inputs — calling `decide(...)`
     * twice with identical inputs returns structurally equal
     * `visibleTools` and `diagnostics`. The returned `provenance`
     * carries call-time `evaluatedAt` timestamps generated by
     * `ToolPolicyProvenanceBuilder` (one per `decide(...)`); two
     * back-to-back calls therefore produce different `evaluatedAt`
     * values even though the policy decision itself is identical.
     * Hosts that want timestamp-stable provenance should reuse a
     * cached `ToolPolicyDecision` rather than re-call `decide(...)`.
     */
    decide(request) {
        this.assertNotDisposed();
        const requestContextId = request.scope.scopeId
            ? `${this.contextId}:${request.level}:${request.scope.scopeId}`
            : `${this.contextId}:${request.level}`;
        return composeDecision({
            request,
            tools: this.tools,
            pnpPolicy: {
                source: this.pnpPolicySource,
                assessment: this.assessment ?? undefined,
                currentItemRef: this.currentItemRef ?? undefined,
                enforcement: this.pnpEnforcement,
            },
            customSources: this.customSources,
            contextId: requestContextId,
        });
    }
    /**
     * Convenience wrapper for hosts that just want the visible tool
     * IDs. Equivalent to `decide(...).visibleTools.map(e => e.toolId)`.
     */
    getVisibleToolIds(level, scopeId) {
        return this.decide({
            level,
            scope: { level, scopeId },
        }).visibleTools.map((entry) => entry.toolId);
    }
    /**
     * Apply a partial input update.
     *
     * Each key is value-diffed via `Object.is` before storing, so callers
     * may safely re-push the same reference (e.g. on Svelte effect
     * re-runs caused by unrelated tracked reads): no-op patches do not
     * fire `onPolicyChange` listeners. This contract is relied on by
     * `PieAssessmentToolkit.svelte`'s prop-forwarding effect and by the
     * coordinator's `decideToolPolicy()` tests.
     *
     * `pnpEnforcement` is stored on its own field and always emits with
     * a distinct `"pnp-enforcement"` reason when no other key changed,
     * so PR 4's auto-on flip can target that reason without a separate
     * event channel.
     */
    updateInputs(patch) {
        this.assertNotDisposed();
        let changed = false;
        let pnpChanged = false;
        if ("tools" in patch) {
            const next = patch.tools ?? DEFAULT_TOOLS;
            if (!Object.is(this.tools, next)) {
                this.tools = next;
                changed = true;
            }
        }
        if ("assessment" in patch) {
            const next = patch.assessment ?? null;
            if (!Object.is(this.assessment, next)) {
                this.assessment = next;
                changed = true;
            }
        }
        if ("currentItemRef" in patch) {
            const next = patch.currentItemRef ?? null;
            if (!Object.is(this.currentItemRef, next)) {
                this.currentItemRef = next;
                changed = true;
            }
        }
        if ("pnpEnforcement" in patch && patch.pnpEnforcement) {
            if (this.pnpEnforcement !== patch.pnpEnforcement) {
                this.pnpEnforcement = patch.pnpEnforcement;
                pnpChanged = true;
            }
        }
        if (changed || pnpChanged) {
            this.emit({
                reason: pnpChanged && !changed ? "pnp-enforcement" : "inputs",
                inputs: this.snapshotInputs(),
            });
        }
    }
    registerPolicySource(source) {
        this.assertNotDisposed();
        this.customSources.push(source);
        this.emit({
            reason: "policy-source-added",
            inputs: this.snapshotInputs(),
        });
        return () => {
            const idx = this.customSources.indexOf(source);
            if (idx !== -1) {
                this.customSources.splice(idx, 1);
                if (!this.disposed) {
                    this.emit({
                        reason: "policy-source-removed",
                        inputs: this.snapshotInputs(),
                    });
                }
            }
        };
    }
    onPolicyChange(listener) {
        this.listeners.add(listener);
        return () => {
            this.listeners.delete(listener);
        };
    }
    getInputs() {
        return this.snapshotInputs();
    }
    dispose() {
        if (this.disposed)
            return;
        this.disposed = true;
        this.emit({
            reason: "disposed",
            inputs: this.snapshotInputs(),
        });
        this.listeners.clear();
        this.customSources.length = 0;
    }
    snapshotInputs() {
        return Object.freeze({
            tools: this.tools,
            assessment: this.assessment,
            currentItemRef: this.currentItemRef,
            pnpEnforcement: this.pnpEnforcement,
        });
    }
    emit(event) {
        for (const listener of Array.from(this.listeners)) {
            try {
                listener(event);
            }
            catch {
                // Subscriber errors must not break the engine. Hosts that
                // want error telemetry should wrap their listener.
            }
        }
    }
    assertNotDisposed() {
        if (this.disposed) {
            throw new Error("ToolPolicyEngine has been disposed.");
        }
    }
}
//# sourceMappingURL=ToolPolicyEngine.js.map