import { createPublicKey } from "node:crypto";
import { verifyRemoteCommandAuthorization, } from "../authority/remoteCommandAuthorization.js";
import { resolveRemoteControlDecision } from "../permissions/remoteControlPolicy.js";
export class RemoteCommandExecutor {
    #store;
    #audit;
    #dispatcher;
    #now;
    constructor(options) {
        this.#store = options.store;
        this.#audit = options.audit;
        this.#dispatcher = options.dispatcher;
        this.#now = options.now ?? (() => new Date());
    }
    async drainOnce() {
        const result = {
            processed: 0,
            denied: 0,
            awaitingConfirmation: 0,
            failed: 0,
        };
        for (const command of this.#store.listPendingCommands()) {
            const outcome = await this.#process(command);
            result[outcome] += 1;
        }
        return result;
    }
    async #process(command) {
        this.#audit.append({
            type: "command.received",
            severity: "info",
            message: "Remote command received for processing.",
            metadata: {
                commandId: command.commandId,
                commandType: command.commandType,
            },
        });
        // 1–3) Parse + resolve the addressed paired device + verify the end-to-end
        //    issuer signature (Phase 2a). Fail-closed on every step.
        const verified = this.#verifyCommand(command);
        if (!verified.ok) {
            return this.#deny(command.commandId, verified.reasonCode, verified.deviceId);
        }
        const { device, rca } = verified;
        // Claim the issuer-signed command id single-use for THIS inbox row. The
        // verify step above already rejected this row as a replay if the signed id
        // was claimed by a different transport delivery; claiming here closes the
        // window so a later redelivery under a fresh transport id is rejected
        // (security model §3b exactly-once effect / §4d replay prevention).
        this.#store.claimRemoteCommand(rca.commandId, command.commandId);
        // 4) Default-deny category authorization (bridge-side, authoritative).
        const policy = this.#store.getRemoteControlPolicy(device.workspaceId, rca.category);
        const decision = resolveRemoteControlDecision(rca.category, policy);
        if (decision.outcome === "denied") {
            return this.#deny(command.commandId, decision.reasonCode, device.id, rca.category);
        }
        // 5) Risk tier: high-risk (or user-tightened) categories require LOCAL
        //    confirmation — park the command, never dispatch it here.
        if (decision.outcome === "confirm-required") {
            this.#audit.append({
                type: "command.confirm_required",
                severity: "warning",
                message: "Remote command requires local confirmation before it runs.",
                metadata: {
                    commandId: command.commandId,
                    deviceId: device.id,
                    category: rca.category,
                },
            });
            this.#store.markCommandAwaitingConfirmation(command.commandId);
            return "awaitingConfirmation";
        }
        // 6) Authorized for silent execution → dispatch to an existing secured route.
        this.#audit.append({
            type: "command.authorized",
            severity: "info",
            message: "Remote command authorized for execution.",
            metadata: {
                commandId: command.commandId,
                deviceId: device.id,
                category: rca.category,
                mode: "silent",
            },
        });
        return this.#dispatch(command.commandId, device.id, device.workspaceId, rca);
    }
    // Parse → resolve the addressed paired device + its trusted issuer key →
    // verify the end-to-end issuer signature. Shared by the drain path and the
    // confirmation-resolve path so a parked command is re-verified before it runs
    // (it may have expired while awaiting confirmation).
    #verifyCommand(command) {
        const parsed = parseJson(command.payloadJson);
        if (parsed === undefined) {
            return {
                ok: false,
                reasonCode: "command_malformed",
                deviceId: undefined,
            };
        }
        const deviceId = readDeviceId(parsed);
        const device = deviceId ? this.#store.findDevice(deviceId) : undefined;
        const trustedIssuerPublicKey = device?.pairedIssuerPublicKeySpki
            ? importSpki(device.pairedIssuerPublicKeySpki)
            : null;
        if (!device || !trustedIssuerPublicKey) {
            return { ok: false, reasonCode: "command_device_unpaired", deviceId };
        }
        const verdict = verifyRemoteCommandAuthorization(parsed, {
            expectedDeviceId: device.id,
            trustedIssuerPublicKey,
            // Replay defense keys on the issuer-signed rca.commandId (claimed
            // single-use in #process), NOT the inbox/transport command id — the
            // untrusted relay chooses the transport id freely, so the inbox primary
            // key alone cannot stop a captured RCA re-enqueued under a fresh id.
            // The same inbox row re-verifying its own claim (confirm-resolve) is not
            // a replay.
            isReplay: (rcaCommandId) => this.#store.isRemoteCommandReplay(rcaCommandId, command.commandId),
            now: this.#now,
        });
        if (verdict.kind === "error") {
            return { ok: false, reasonCode: verdict.reasonCode, deviceId: device.id };
        }
        return { ok: true, device, rca: verdict.command };
    }
    /**
     * Lists parked commands awaiting local confirmation, as BOUNDED entries only
     * (commandId, category, deviceId, receivedAt) — never the raw RCA params. Used
     * by the local confirm UI; safe to surface.
     */
    listAwaitingConfirmations() {
        return this.#store.listAwaitingConfirmationCommands().map((command) => {
            const parsed = parseJson(command.payloadJson);
            const category = readCategory(parsed);
            return {
                commandId: command.commandId,
                category,
                deviceId: readDeviceId(parsed) ?? null,
                receivedAt: command.receivedAt,
            };
        });
    }
    /**
     * Resolves a parked (awaiting_confirmation) high-risk command after the local
     * user decides. Approve re-verifies the issuer signature (the command may have
     * expired while parked) then dispatches; reject marks it failed. Both audited.
     * A command not currently awaiting confirmation is a no-op guard that prevents
     * double-resolve.
     */
    async resolveConfirmation(commandId, approve) {
        const command = this.#store.findCommand(commandId);
        if (!command || command.status !== "awaiting_confirmation") {
            return { resolved: "not_awaiting" };
        }
        if (!approve) {
            this.#deny(commandId, "confirmation_rejected", undefined);
            return { resolved: "rejected" };
        }
        const verified = this.#verifyCommand(command);
        if (!verified.ok) {
            this.#deny(commandId, verified.reasonCode, verified.deviceId);
            return { resolved: "denied" };
        }
        this.#audit.append({
            type: "command.authorized",
            severity: "warning",
            message: "Remote command authorized by local confirmation.",
            metadata: {
                commandId,
                deviceId: verified.device.id,
                category: verified.rca.category,
                mode: "confirmed",
            },
        });
        const outcome = await this.#dispatch(commandId, verified.device.id, verified.device.workspaceId, verified.rca);
        return { resolved: outcome === "processed" ? "executed" : "failed" };
    }
    async #dispatch(commandId, deviceId, workspaceId, rca) {
        this.#audit.append({
            type: "command.executed",
            severity: "info",
            message: "Dispatching remote command to its local operation.",
            metadata: { commandId, deviceId, category: rca.category },
        });
        let result;
        try {
            result = await this.#dispatcher.dispatch({
                category: rca.category,
                params: rca.params,
                workspaceId,
                deviceId,
                commandId,
            });
        }
        catch {
            // Never log raw dispatch errors — bounded reason only.
            result = { ok: false, reasonCode: "dispatch_threw" };
        }
        if (result.ok) {
            this.#audit.append({
                type: "command.result",
                severity: "info",
                message: "Remote command executed successfully.",
                metadata: {
                    commandId,
                    deviceId,
                    category: rca.category,
                    outcome: "success",
                },
            });
            this.#store.markCommandProcessed(commandId);
            return "processed";
        }
        this.#audit.append({
            type: "command.result",
            severity: "warning",
            message: "Remote command dispatch failed.",
            metadata: {
                commandId,
                deviceId,
                category: rca.category,
                outcome: "failure",
                reasonCode: result.reasonCode,
            },
        });
        this.#store.markCommandFailed(commandId, result.reasonCode);
        return "failed";
    }
    #deny(commandId, reasonCode, deviceId, category) {
        this.#audit.append({
            type: "command.denied",
            severity: "warning",
            message: "Remote command denied.",
            metadata: {
                commandId,
                ...(deviceId ? { deviceId } : {}),
                ...(category ? { category } : {}),
                reasonCode,
            },
        });
        this.#store.markCommandFailed(commandId, reasonCode);
        return "denied";
    }
}
function parseJson(value) {
    try {
        return JSON.parse(value);
    }
    catch {
        return undefined;
    }
}
function readDeviceId(parsed) {
    if (typeof parsed !== "object" || parsed === null)
        return undefined;
    const payload = parsed.payload;
    if (typeof payload !== "object" || payload === null)
        return undefined;
    const deviceId = payload.deviceId;
    return typeof deviceId === "string" && deviceId.length > 0
        ? deviceId
        : undefined;
}
const KNOWN_CATEGORIES = [
    "read",
    "review-decisions",
    "start-runs",
    "open-prs",
];
function readCategory(parsed) {
    if (typeof parsed !== "object" || parsed === null)
        return null;
    const payload = parsed.payload;
    if (typeof payload !== "object" || payload === null)
        return null;
    const category = payload.category;
    return KNOWN_CATEGORIES.includes(category)
        ? category
        : null;
}
function importSpki(spkiBase64) {
    try {
        return createPublicKey({
            key: Buffer.from(spkiBase64, "base64"),
            format: "der",
            type: "spki",
        });
    }
    catch {
        return null;
    }
}
