import { createHash, verify } from "node:crypto";
import { isRecord, stableStringify } from "../utils/json.js";
export const REMOTE_COMMAND_CATEGORIES = [
    "read",
    "review-decisions",
    "start-runs",
    "open-prs",
];
export const REMOTE_COMMAND_CONTRACT_VERSION = "remote-command-authorization.v1";
export const REMOTE_COMMAND_SIGNATURE_PREFIX = "rca_";
const REMOTE_COMMAND_SIGNATURE_BYTES = 64;
const REMOTE_COMMAND_REQUIRED_STRING_KEYS = [
    "contractVersion",
    "commandId",
    "deviceId",
    "category",
    "issuedAt",
    "expiresAt",
    "nonce",
];
/**
 * Canonical signing payload. Params are bound via a stable hash so the exact
 * params the issuer signed must be the ones delivered (any tamper invalidates
 * the signature), while key ordering is irrelevant.
 */
export function remoteCommandSigningPayload(payload) {
    return [
        payload.contractVersion,
        payload.commandId,
        payload.deviceId,
        payload.category,
        paramsHash(payload.params),
        payload.issuedAt,
        payload.expiresAt,
        payload.nonce,
    ].join("\n");
}
export function verifyRemoteCommandAuthorization(value, options) {
    const payload = readPayload(value);
    if (!payload) {
        return error("command_malformed");
    }
    if (payload.contractVersion !== REMOTE_COMMAND_CONTRACT_VERSION ||
        !REMOTE_COMMAND_CATEGORIES.includes(payload.category)) {
        return error("command_unsupported");
    }
    const issuedAtMs = parseTimestamp(payload.issuedAt);
    const expiresAtMs = parseTimestamp(payload.expiresAt);
    if (issuedAtMs === null || expiresAtMs === null) {
        return error("command_malformed");
    }
    const signature = value.signature;
    if (!verifyIssuerSignature(payload, signature, options.trustedIssuerPublicKey)) {
        return error("command_signature_invalid");
    }
    // Per-device scope: a command signed for another device must not run here,
    // even though its signature is valid.
    if (payload.deviceId !== options.expectedDeviceId) {
        return error("command_wrong_device");
    }
    const nowMs = readNowMs(options.now);
    if (expiresAtMs <= nowMs) {
        return error("command_expired");
    }
    if (options.isReplay(payload.commandId)) {
        return error("command_replayed");
    }
    return { kind: "ok", command: payload };
}
function readPayload(value) {
    if (!isRecord(value))
        return null;
    const { payload, signature } = value;
    if (typeof signature !== "string" || signature.length === 0)
        return null;
    if (!isRecord(payload))
        return null;
    for (const key of REMOTE_COMMAND_REQUIRED_STRING_KEYS) {
        const field = payload[key];
        if (typeof field !== "string" || field.length === 0) {
            return null;
        }
    }
    if (!isRecord(payload.params))
        return null;
    return {
        contractVersion: payload.contractVersion,
        commandId: payload.commandId,
        deviceId: payload.deviceId,
        category: payload.category,
        params: payload.params,
        issuedAt: payload.issuedAt,
        expiresAt: payload.expiresAt,
        nonce: payload.nonce,
    };
}
function verifyIssuerSignature(payload, signature, trustedIssuerPublicKey) {
    if (!signature.startsWith(REMOTE_COMMAND_SIGNATURE_PREFIX)) {
        return false;
    }
    const bytes = base64UrlDecode(signature.slice(REMOTE_COMMAND_SIGNATURE_PREFIX.length));
    if (bytes.length !== REMOTE_COMMAND_SIGNATURE_BYTES) {
        return false;
    }
    try {
        return verify("sha256", Buffer.from(remoteCommandSigningPayload(payload), "utf8"), { key: trustedIssuerPublicKey, dsaEncoding: "ieee-p1363" }, bytes);
    }
    catch {
        return false;
    }
}
function paramsHash(params) {
    return createHash("sha256")
        .update(stableStringify(params))
        .digest("base64url");
}
function parseTimestamp(value) {
    const parsed = Date.parse(value);
    return Number.isFinite(parsed) ? parsed : null;
}
function readNowMs(now) {
    if (!now)
        return Date.now();
    const value = now().getTime();
    return Number.isFinite(value) ? value : Date.now();
}
function base64UrlDecode(value) {
    if (value.length === 0 ||
        value.length % 4 === 1 ||
        !/^[A-Za-z0-9_-]+$/.test(value)) {
        return Buffer.alloc(0);
    }
    try {
        return Buffer.from(value, "base64url");
    }
    catch {
        return Buffer.alloc(0);
    }
}
function error(reasonCode) {
    return { kind: "error", reasonCode };
}
