/**
 * Machine registration with the Aexol backend.
 *
 * Contract (Batch 1 backend):
 *   POST <backend>/api/machines/register
 *     Headers: Authorization: Bearer <teamApiKey|userJwt>
 *     Body:    { name?: string, hostname: string, version: string, pid: number }
 *     200:     { machineId: string, jwt: string, name: string, ownerId?: string }
 *
 * Auth priority:
 *   - If `userJwt` is set → used as Bearer token (machine gets ownerId)
 *   - Else → `apiKey` (team API key, machine gets no owner)
 *
 * Why this lives in its own module:
 *  - It's the ONLY consumer of the team API key / user JWT for registration.
 *    Once registration succeeds, the auth token is dropped on the floor —
 *    only the `machineJwt` is persisted (in `machine.json`) and threaded
 *    into `RelayClient`. Keeps the blast radius of an in-memory leak minimal.
 *  - It owns JWT expiry decoding so `serve.ts` doesn't have to know about
 *    JWT internals; a single boundary for "is the saved record still good".
 *
 * Failure mode: registration is fail-fast (no retry). The user is sitting
 * at a CLI prompt; if their team key is wrong or the backend is down, the
 * right answer is a clear error message, not a hang. WS reconnect (which
 * IS retried) is the long-running path; that's `RelayClient`'s job.
 */
import { hostname } from "node:os";
import { loadMachine, saveMachine } from "./machine-store.js";
/**
 * Decode the `exp` claim of a JWT without verifying the signature.
 * We're not using this for AuthN — only for "should we re-register
 * proactively". The backend is the source of truth on validity; if our
 * cheap pre-check is wrong, the WS handshake will reject and we'll fall
 * through to a full re-register on the next `spectral serve`.
 *
 * Returns `null` if the token isn't a parseable JWT or has no `exp`.
 */
export function decodeJwtExp(jwt) {
    const parts = jwt.split(".");
    if (parts.length < 2)
        return null;
    try {
        // base64url -> base64. Node 20's Buffer accepts 'base64url' directly.
        const payloadJson = Buffer.from(parts[1], "base64url").toString("utf8");
        const payload = JSON.parse(payloadJson);
        return typeof payload.exp === "number" ? payload.exp : null;
    }
    catch {
        return null;
    }
}
/** True when the JWT is past expiry (with skew). Treats undecodable as expired. */
export function isJwtExpired(jwt, skewSec = 60) {
    const exp = decodeJwtExp(jwt);
    if (exp === null)
        return true;
    return Date.now() / 1000 + skewSec >= exp;
}
/**
 * Ensure a machine is registered. Returns the record either from the cached
 * `machine.json` (when the JWT is still fresh) or from a freshly issued one.
 *
 * Re-registration triggers:
 *   - No `machine.json` on disk (first run).
 *   - The cached JWT is expired or near-expired.
 *
 * Why we don't re-register on `--machine-name` change: the CLI flag controls
 * the *next* registration's display name, but switching it shouldn't burn a
 * fresh `machineId` on every restart. If users want a clean break they can
 * delete `machine.json`.
 */
export async function ensureMachineRegistered(deps) {
    const skew = deps.expirySkewSec ?? 60;
    const existing = await loadMachine();
    if (existing && !isJwtExpired(existing.machineJwt, skew)) {
        return { reused: true, record: existing };
    }
    const fetchImpl = deps.fetchImpl ?? fetch;
    const url = `${deps.backendUrl.replace(/\/$/, "")}/api/machines/register`;
    const body = {
        // Reuse the cached display name on JWT-expiry refresh so the backend's
        // machine list stays stable; only on a clean install do we fall back to
        // hostname or the CLI override.
        name: deps.machineNameOverride ?? existing?.machineName ?? hostname(),
        hostname: hostname(),
        version: deps.version,
        pid: deps.pid ?? process.pid,
    };
    let res;
    try {
        res = await fetchImpl(url, {
            method: "POST",
            headers: {
                Authorization: `Bearer ${deps.userJwt ?? deps.apiKey}`,
                "Content-Type": "application/json",
            },
            body: JSON.stringify(body),
        });
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        throw new Error(`Failed to reach backend at ${url}: ${msg}`);
    }
    if (!res.ok) {
        let detail = "";
        try {
            detail = await res.text();
        }
        catch {
            // ignore
        }
        throw new Error(`Machine registration failed (${res.status} ${res.statusText})` +
            (detail ? `: ${detail.slice(0, 500)}` : ""));
    }
    let payload;
    try {
        payload = await res.json();
    }
    catch (err) {
        throw new Error(`Machine registration: backend returned non-JSON (${err instanceof Error ? err.message : String(err)})`);
    }
    const obj = payload;
    if (typeof obj.machineId !== "string" ||
        typeof obj.jwt !== "string" ||
        typeof obj.name !== "string") {
        throw new Error(`Machine registration: backend response missing required fields (machineId, jwt, name)`);
    }
    const record = {
        machineId: obj.machineId,
        machineName: obj.name,
        machineJwt: obj.jwt,
        teamId: typeof obj.teamId === "string" ? obj.teamId : undefined,
        ownerId: typeof obj.ownerId === "string" ? obj.ownerId : undefined,
        visibility: typeof obj.visibility === "string" ? obj.visibility : undefined,
        registeredAt: Date.now(),
        hostname: hostname(),
        version: deps.version,
    };
    await saveMachine(record);
    return { reused: false, record };
}
