/**
 * Layer 3 device pairing (bridge side). The bridge — always the OUTBOUND party —
 * claims a pairing code (handed to it over loopback from the co-located
 * frontend), registering its device public key with the backend and recording
 * the trusted browser-held issuer public key it will later verify remote
 * commands against. The private key never leaves the DeviceKeyManager; only the
 * SPKI public key is sent. No inbound listener is involved.
 */
const CLAIM_PATH = "/api/v1/sync/devices/pairings/claim";
const DEFAULT_TIMEOUT_MS = 10_000;
export class DevicePairingService {
    #keyManager;
    #bindingStore;
    #endpointOrigin;
    #requester;
    #now;
    constructor(options) {
        this.#keyManager = options.keyManager;
        this.#bindingStore = options.bindingStore;
        this.#endpointOrigin = options.endpointOrigin.replace(/\/+$/u, "");
        this.#requester = options.requester ?? defaultRequester;
        this.#now = options.now ?? (() => new Date());
    }
    async claimPairing(input) {
        const key = await this.#keyManager.ensureDeviceKey(input.deviceId);
        let response;
        try {
            response = await this.#requester(`${this.#endpointOrigin}${CLAIM_PATH}`, {
                method: "POST",
                headers: { "content-type": "application/json" },
                body: JSON.stringify({
                    pairingCode: input.pairingCode,
                    deviceId: input.deviceId,
                    deviceLabel: input.deviceLabel,
                    devicePublicKeySpkiBase64: key.publicKeySpkiBase64,
                }),
            });
        }
        catch {
            // Never log raw transport errors — bounded reason only.
            return { ok: false, reason: "pairing_request_failed" };
        }
        if (!response.ok) {
            return { ok: false, reason: "pairing_rejected" };
        }
        const issuerPublicKeySpki = readIssuerPublicKey(response.json);
        if (issuerPublicKeySpki === null) {
            return { ok: false, reason: "pairing_response_invalid" };
        }
        // Ensure the device row exists (keyed on the browser-supplied id) BEFORE
        // recording the pairing — otherwise recordDevicePairing's UPDATE matches 0
        // rows and silently persists nothing (GAP 2).
        try {
            this.#bindingStore.ensureDeviceForPairing({
                deviceId: input.deviceId,
                workspaceId: input.workspaceId,
                label: input.deviceLabel,
            });
            this.#bindingStore.recordDevicePairing({
                deviceId: input.deviceId,
                devicePublicKeySpki: key.publicKeySpkiBase64,
                issuerPublicKeySpki,
                pairedAt: this.#now().toISOString(),
            });
        }
        catch {
            // A storage failure must not surface as a successful pairing.
            return { ok: false, reason: "pairing_not_persisted" };
        }
        // Fail closed: confirm the device key actually landed. A claim never
        // reports paired:true when nothing was recorded.
        const persisted = this.#bindingStore.findDevice(input.deviceId);
        if (!persisted || !persisted.devicePublicKeySpki) {
            return { ok: false, reason: "pairing_not_persisted" };
        }
        return { ok: true, deviceId: input.deviceId, issuerPublicKeySpki };
    }
}
function readIssuerPublicKey(json) {
    if (typeof json !== "object" || json === null) {
        return null;
    }
    const value = json.issuerPublicKeySpkiBase64;
    return typeof value === "string" && value.length > 0 ? value : null;
}
const defaultRequester = async (url, init) => {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
    try {
        const response = await fetch(url, {
            method: init.method,
            headers: init.headers,
            body: init.body,
            signal: controller.signal,
        });
        let json = null;
        try {
            json = await response.json();
        }
        catch {
            json = null;
        }
        return { ok: response.ok, status: response.status, json };
    }
    finally {
        clearTimeout(timeout);
    }
};
