import { createToolExecutionBatches } from "./tool-registry-contract.js";
import { decideToolAuthority, mapToolNameToOp, } from "../safety/tool-authority-gate.js";
const ENFORCE_PATTERN = /^(1|true|yes|on)$/i;
/**
 * Resolve the global enforcement opt-in from the environment. Default OFF means
 * the gate runs in shadow mode (record only). Set `OMK_TOOL_AUTHORITY_ENFORCE=1`
 * to enable fail-closed enforcement at the dispatch checkpoint.
 */
export function resolveToolAuthorityEnforcement(env = process.env) {
    return ENFORCE_PATTERN.test((env.OMK_TOOL_AUTHORITY_ENFORCE ?? "").trim());
}
/** Build a redacted reason string from non-secret authority signals only. */
function redactedAuthorityReason(op, decision, wiring) {
    return (`tool-authority ${decision} for ${op} op ` +
        `(write=${wiring.writeAuthority}, shell=${wiring.shellAuthority}, ` +
        `policy=${wiring.approvalPolicy}, sandbox=${wiring.sandboxMode}, tty=${wiring.tty})`);
}
/** Error used to reject a tool call rejected by the authority gate (enforce mode). */
export class ToolAuthorityBlockedError extends Error {
    toolName;
    op;
    decision;
    constructor(record) {
        super(record.reason);
        this.name = "ToolAuthorityBlockedError";
        this.toolName = record.toolName;
        this.op = record.op;
        this.decision = record.decision;
    }
}
/** Compute the gate verdict for a single call. Pure (no IO, no env reads). */
export function evaluateToolAuthority(toolName, wiring) {
    const op = mapToolNameToOp(toolName);
    const decision = decideToolAuthority({
        op,
        writeAuthority: wiring.writeAuthority,
        shellAuthority: wiring.shellAuthority,
        approvalPolicy: wiring.approvalPolicy,
        sandboxMode: wiring.sandboxMode,
        tty: wiring.tty,
    });
    const enforce = wiring.enforce === true;
    // Fail-closed: a non-TTY "ask" is treated as a block (decideToolAuthority
    // already returns "block" for non-TTY interactive; the second clause is a
    // defensive guard in case the caller ever surfaces "ask" without a TTY).
    const wouldBlock = decision === "block" || (decision === "ask" && !wiring.tty);
    const blocked = enforce && wouldBlock;
    return {
        record: {
            toolName,
            op,
            decision,
            mode: enforce ? "enforce" : "shadow",
            enforced: blocked,
            reason: redactedAuthorityReason(op, decision, wiring),
        },
        blocked,
    };
}
/**
 * Wrap a dispatch function with the authority checkpoint. In shadow mode the
 * wrapper records the verdict and always delegates to `dispatchOne`. In enforce
 * mode a blocked verdict rejects the call with a redacted reason.
 */
function buildGatedDispatch(wiring, dispatchOne) {
    return async (call) => {
        const { record, blocked } = evaluateToolAuthority(call.toolName, wiring);
        wiring.onDecision?.(record);
        if (blocked) {
            throw new ToolAuthorityBlockedError(record);
        }
        return dispatchOne(call);
    };
}
export async function dispatchToolCallsByContract(calls, registry, dispatchOne, authority) {
    // When no authority wiring is supplied the dispatch path is byte-identical to
    // the pre-gate behavior (the checkpoint is a no-op).
    const effectiveDispatch = authority ? buildGatedDispatch(authority, dispatchOne) : dispatchOne;
    const batches = createToolExecutionBatches(calls, registry);
    const appended = [];
    for (const batch of batches) {
        if (batch.kind === "parallel") {
            const settled = await Promise.allSettled(batch.calls.map((call) => effectiveDispatch(call)));
            settled.forEach((result, index) => {
                const call = batch.calls[index];
                if (!call)
                    return;
                appended.push(toDispatchResult(call, result));
            });
            continue;
        }
        for (const call of batch.calls) {
            try {
                appended.push({ call, status: "fulfilled", value: await effectiveDispatch(call) });
            }
            catch (reason) {
                appended.push({ call, status: "rejected", reason });
            }
        }
    }
    return appended;
}
function toDispatchResult(call, result) {
    if (result.status === "fulfilled") {
        return { call, status: "fulfilled", value: result.value };
    }
    return { call, status: "rejected", reason: result.reason };
}
