/**
 * Read-only projection of the remote-command lifecycle audit (GAP 3).
 *
 * The command.* events are written to the durable audit ledger but carried no
 * HTTP read surface, so a hosted/local UI could not observe remote-command
 * outcomes (only an out-of-band SQLite read could). This service projects them
 * as a bounded, workspace-scoped, leak-guarded feed.
 *
 * Scoping: command.* metadata carries deviceId/commandId but no workspaceId, so
 * the service resolves the owning workspace authoritatively from the command
 * inbox (commandId -> payload deviceId -> device.workspaceId). An event whose
 * workspace cannot be resolved, or that resolves to a different workspace, is
 * not surfaced — no cross-tenant read.
 */
import { REMOTE_COMMAND_AUDIT_CONTRACT_VERSION, readAuditKind, readAuditMode, readAuditOutcome, readBoundedAuditString, } from "./remoteCommandAuditProjectionContract.js";
const DEFAULT_LIMIT = 50;
const MAX_LIMIT = 200;
// Reads more raw rows than we project so that, after dropping events whose
// workspace cannot be resolved or that belong to another workspace, the limit
// can still be filled from this workspace's events. Bounded to stay finite.
const SCAN_MULTIPLIER = 4;
export class RemoteCommandAuditProjectionService {
    store;
    constructor(store) {
        this.store = store;
    }
    listForWorkspace(workspaceId, limit = DEFAULT_LIMIT) {
        const bounded = clamp(limit);
        const empty = {
            contractVersion: REMOTE_COMMAND_AUDIT_CONTRACT_VERSION,
            generatedAt: new Date().toISOString(),
            entries: [],
        };
        if (!workspaceId)
            return empty;
        const raw = this.store.listCommandAuditEvents(bounded * SCAN_MULTIPLIER);
        const workspaceByCommand = new Map();
        const entries = [];
        for (const event of raw) {
            if (entries.length >= bounded)
                break;
            const entry = this.#project(event, workspaceId, workspaceByCommand);
            if (entry)
                entries.push(entry);
        }
        return {
            contractVersion: REMOTE_COMMAND_AUDIT_CONTRACT_VERSION,
            generatedAt: new Date().toISOString(),
            entries,
        };
    }
    #project(event, workspaceId, cache) {
        const kind = readAuditKind(event.type);
        if (!kind)
            return null;
        const metadata = event.metadata ?? {};
        const commandId = readBoundedAuditString(metadata.commandId);
        const resolvedWorkspace = this.#resolveWorkspace(commandId, cache);
        // Fail-closed scoping: an event whose owning workspace cannot be resolved is
        // never surfaced (it could otherwise leak across tenants).
        if (resolvedWorkspace !== workspaceId)
            return null;
        return {
            kind,
            at: event.at,
            workspaceId,
            commandId,
            deviceId: readBoundedAuditString(metadata.deviceId),
            category: readBoundedAuditString(metadata.category),
            mode: readAuditMode(metadata.mode),
            outcome: readAuditOutcome(metadata.outcome),
            reasonCode: readBoundedAuditString(metadata.reasonCode),
        };
    }
    #resolveWorkspace(commandId, cache) {
        if (!commandId)
            return null;
        const cached = cache.get(commandId);
        if (cached !== undefined)
            return cached;
        let workspaceId = null;
        const command = this.store.findCommand(commandId);
        if (command) {
            const deviceId = readCommandDeviceId(command.payloadJson);
            const device = deviceId ? this.store.findDevice(deviceId) : undefined;
            workspaceId = device?.workspaceId ?? null;
        }
        cache.set(commandId, workspaceId);
        return workspaceId;
    }
}
function readCommandDeviceId(payloadJson) {
    let parsed;
    try {
        parsed = JSON.parse(payloadJson);
    }
    catch {
        return null;
    }
    if (typeof parsed !== "object" || parsed === null)
        return null;
    const payload = parsed.payload;
    if (typeof payload !== "object" || payload === null)
        return null;
    const deviceId = payload.deviceId;
    return typeof deviceId === "string" && deviceId.length > 0 ? deviceId : null;
}
function clamp(value) {
    if (!Number.isFinite(value))
        return DEFAULT_LIMIT;
    return Math.max(1, Math.min(MAX_LIMIT, Math.trunc(value)));
}
