/**
 * Layer 3 Remote Control HTTP routes (local UI, scope 'remote:control',
 * session-gated). All are workspace-scoped and authorize the bound session's
 * workspace; none accept the bridge token as a browser substitute beyond the
 * standard authorizeWorkspace bypass.
 *
 * GET  /workspaces/{ws}/remote-control/options          → policy per category
 * PUT  /workspaces/{ws}/remote-control/options          → upsert one category
 * GET  /workspaces/{ws}/remote-control/confirmations    → parked commands (bounded)
 * POST /workspaces/{ws}/remote-control/confirmations/{commandId}/resolve → approve/reject
 * POST /workspaces/{ws}/remote-control/pairings/claim   → bridge claims a pairing code
 */
import { REMOTE_COMMAND_CATEGORIES, } from "../authority/remoteCommandAuthorization.js";
import { defaultRiskModeFor, REMOTE_CONTROL_HIGH_RISK_CATEGORIES, } from "../permissions/remoteControlPolicy.js";
import { RemoteCommandAuditProjectionService } from "../audit/remoteCommandAuditProjectionService.js";
import { authorizeWorkspace } from "./authGuards.js";
import { writeJson } from "./publicResponse.js";
import { readJsonBody } from "./requestParsing.js";
import { firstCapture, matchPath, secondCapture } from "./routeMatching.js";
import { isRecord } from "../utils/json.js";
const SCAN_OPTIONS = {
    allowedFieldNames: [
        "categories",
        "category",
        "enabled",
        "riskMode",
        "highRisk",
        "defaultRiskMode",
        "confirmations",
        "commandId",
        "deviceId",
        "receivedAt",
        "resolved",
        "paired",
        // Remote-command audit projection (bounded lifecycle outcomes).
        "contractVersion",
        "generatedAt",
        "entries",
        "kind",
        "at",
        "workspaceId",
        "mode",
        "outcome",
        "reasonCode",
    ],
};
const OPTIONS_PATTERN = /^\/workspaces\/([^/]+)\/remote-control\/options$/;
const CONFIRMATIONS_PATTERN = /^\/workspaces\/([^/]+)\/remote-control\/confirmations$/;
const RESOLVE_PATTERN = /^\/workspaces\/([^/]+)\/remote-control\/confirmations\/([^/]+)\/resolve$/;
const PAIRING_CLAIM_PATTERN = /^\/workspaces\/([^/]+)\/remote-control\/pairings\/claim$/;
const COMMAND_AUDIT_PATTERN = /^\/workspaces\/([^/]+)\/remote-control\/command-audit$/;
function isRiskMode(value) {
    return value === "confirm" || value === "silent";
}
function isCategory(value) {
    return REMOTE_COMMAND_CATEGORIES.includes(value);
}
export async function handleRemoteControlOptionsRoutes(context) {
    const { app, auth, request, response, url } = context;
    const match = matchPath(url.pathname, OPTIONS_PATTERN);
    if (!match)
        return false;
    const workspaceId = firstCapture(match);
    if (!workspaceId)
        return false;
    if (!authorizeWorkspace(response, auth, workspaceId))
        return true;
    if (request.method === "GET") {
        // Project every category with its current (or default-deny) policy so the
        // UI always renders the full default-deny surface, not just stored rows.
        const categories = REMOTE_COMMAND_CATEGORIES.map((category) => {
            const row = app.localStore.getRemoteControlPolicy(workspaceId, category);
            return {
                category,
                enabled: row?.enabled ?? false,
                riskMode: row?.riskMode ?? defaultRiskModeFor(category),
                defaultRiskMode: defaultRiskModeFor(category),
                highRisk: REMOTE_CONTROL_HIGH_RISK_CATEGORIES.includes(category),
            };
        });
        writeJson(response, 200, { categories }, {
            surface: "remote-control.options",
            scanOptions: SCAN_OPTIONS,
        });
        return true;
    }
    if (request.method === "PUT") {
        const body = await readJsonBody(request);
        if (!isRecord(body) ||
            !isCategory(body.category) ||
            typeof body.enabled !== "boolean" ||
            !isRiskMode(body.riskMode)) {
            writeJson(response, 400, {
                error: "invalid_request",
                message: "Expected { category, enabled: boolean, riskMode: 'confirm'|'silent' }.",
            });
            return true;
        }
        app.localStore.setRemoteControlPolicy(workspaceId, {
            category: body.category,
            enabled: body.enabled,
            riskMode: body.riskMode,
        });
        writeJson(response, 200, {
            category: body.category,
            enabled: body.enabled,
            riskMode: body.riskMode,
        }, { surface: "remote-control.options", scanOptions: SCAN_OPTIONS });
        return true;
    }
    return false;
}
export async function handleRemoteControlConfirmationsRoutes(context) {
    const { app, auth, request, response, url } = context;
    // GET list of parked confirmations (bounded — no raw params).
    const listMatch = matchPath(url.pathname, CONFIRMATIONS_PATTERN);
    if (listMatch && request.method === "GET") {
        const workspaceId = firstCapture(listMatch);
        if (!workspaceId)
            return false;
        if (!authorizeWorkspace(response, auth, workspaceId))
            return true;
        const confirmations = app.remoteCommandExecutor.listAwaitingConfirmations();
        writeJson(response, 200, { confirmations }, {
            surface: "remote-control.confirmations",
            scanOptions: SCAN_OPTIONS,
        });
        return true;
    }
    // POST resolve (approve/reject) a parked confirmation.
    const resolveMatch = matchPath(url.pathname, RESOLVE_PATTERN);
    if (resolveMatch && request.method === "POST") {
        const workspaceId = firstCapture(resolveMatch);
        const commandId = secondCapture(resolveMatch);
        if (!workspaceId || !commandId)
            return false;
        if (!authorizeWorkspace(response, auth, workspaceId))
            return true;
        const body = await readJsonBody(request);
        if (!isRecord(body) || typeof body.approve !== "boolean") {
            writeJson(response, 400, {
                error: "invalid_request",
                message: "Expected { approve: boolean }.",
            });
            return true;
        }
        const result = await app.remoteCommandExecutor.resolveConfirmation(commandId, body.approve);
        if (result.resolved === "not_awaiting") {
            writeJson(response, 404, {
                error: "not_awaiting_confirmation",
                message: "No parked command with that id is awaiting confirmation.",
            });
            return true;
        }
        writeJson(response, 200, { resolved: result.resolved }, { surface: "remote-control.confirmations", scanOptions: SCAN_OPTIONS });
        return true;
    }
    return false;
}
export async function handleRemoteControlCommandAuditRoute(context) {
    const { app, auth, request, response, url } = context;
    if (request.method !== "GET")
        return false;
    const match = matchPath(url.pathname, COMMAND_AUDIT_PATTERN);
    if (!match)
        return false;
    const workspaceId = firstCapture(match);
    if (!workspaceId)
        return false;
    if (!authorizeWorkspace(response, auth, workspaceId))
        return true;
    // Bounded, workspace-scoped projection of the command.* lifecycle ledger so a
    // local UI can observe remote-command outcomes over HTTP. Only finite metadata
    // (kind, ids, category, mode, outcome, reasonCode, timestamp) is surfaced; raw
    // params/paths/tokens are never projected and the leak scan still runs.
    const service = new RemoteCommandAuditProjectionService(app.localStore);
    writeJson(response, 200, service.listForWorkspace(workspaceId), { surface: "remote-control.command-audit", scanOptions: SCAN_OPTIONS });
    return true;
}
export async function handleRemoteControlPairingClaimRoute(context) {
    const { app, auth, request, response, url } = context;
    if (request.method !== "POST")
        return false;
    const match = matchPath(url.pathname, PAIRING_CLAIM_PATTERN);
    if (!match)
        return false;
    const workspaceId = firstCapture(match);
    if (!workspaceId)
        return false;
    if (!authorizeWorkspace(response, auth, workspaceId))
        return true;
    const body = await readJsonBody(request);
    if (!isRecord(body) ||
        typeof body.pairingCode !== "string" ||
        body.pairingCode.length === 0 ||
        typeof body.deviceId !== "string" ||
        body.deviceId.length === 0 ||
        typeof body.deviceLabel !== "string" ||
        body.deviceLabel.length === 0) {
        writeJson(response, 400, {
            error: "invalid_request",
            message: "Expected { pairingCode, deviceId, deviceLabel }.",
        });
        return true;
    }
    // The bridge is the OUTBOUND party: it claims the pairing against the hosted
    // backend, registering its device public key and recording the trusted issuer
    // key locally. The pairing code never grants the browser any authority here.
    const claim = await app.devicePairingService.claimPairing({
        pairingCode: body.pairingCode,
        deviceId: body.deviceId,
        deviceLabel: body.deviceLabel,
        workspaceId,
    });
    if (!claim.ok) {
        writeJson(response, 502, {
            error: "pairing_failed",
            reasonCode: claim.reason,
            message: "The device pairing claim did not succeed.",
        });
        return true;
    }
    writeJson(response, 200, { paired: true, deviceId: claim.deviceId }, { surface: "remote-control.pairing", scanOptions: SCAN_OPTIONS });
    return true;
}
