import { randomUUID } from "node:crypto";
import { statfs } from "node:fs/promises";
import { eq } from "drizzle-orm";
import { z } from "zod";
import jwt from "jsonwebtoken";
import { ApiError, forbidden, unauthorized } from "../errors.js";
import { db } from "../db/client.js";
import { maintenanceKeys, users } from "../db/schema.js";
import { sha256 } from "../crypto/envelope.js";
import { jsonSchema } from "../lib/json-schema.js";
import { buildAuthHooks } from "../plugins/auth.js";
import { loadInstallConfig, maintenanceSecret } from "../config.js";
import { env } from "../env.js";
function issueMaintenanceToken(userId) {
    const expiresAt = new Date(Date.now() + maintenanceKeyTtlMs());
    const token = jwt.sign({ sub: userId, scope: "maintenance", jti: randomUUID() }, maintenanceSecret(), { algorithm: "HS256", expiresIn: maintenanceKeyTtlMs() / 1000 });
    return { token, expiresAt };
}
function maintenanceKeyTtlMs() {
    // "30m" | "1h" | "90s" — petite grammaire ISO-8601 sans dépendance.
    const raw = env.MAINTENANCE_KEY_TTL;
    const m = /^(\d+)([smhd])$/.exec(raw);
    if (!m)
        throw new Error(`MAINTENANCE_KEY_TTL invalide : "${raw}" (ex. 30m, 1h)`);
    const n = Number(m[1]);
    const unit = m[2];
    const ms = unit === "s" ? 1_000 : unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : 86_400_000;
    return n * ms;
}
/** Vérifie la clé de maintenance (Bearer, secret dédié + trace en base). */
async function requireMaintenanceKey(request) {
    const header = request.headers.authorization;
    const raw = header?.startsWith("Bearer ") ? header.slice(7) : null;
    if (!raw)
        throw unauthorized("Clé de maintenance manquante");
    let payload;
    try {
        payload = jwt.verify(raw, maintenanceSecret(), { algorithms: ["HS256"] });
    }
    catch {
        throw unauthorized("Clé de maintenance invalide");
    }
    if (payload.scope !== "maintenance" || typeof payload.sub !== "string") {
        throw unauthorized("Clé de maintenance invalide");
    }
    const [row] = db
        .select()
        .from(maintenanceKeys)
        .where(eq(maintenanceKeys.tokenHash, sha256(raw)))
        .limit(1)
        .all();
    if (!row || row.revokedAt || row.expiresAt < new Date()) {
        throw unauthorized("Clé de maintenance révoquée ou expirée");
    }
    request.maintenance = { keyId: row.id, createdBy: row.createdBy };
}
const createKeySchema = z.object({
    label: z.string().max(200).optional(),
});
export function registerMaintenanceRoutes(app) {
    const { authenticate } = buildAuthHooks(app);
    /**
     * Gestionnaire d'installation : le compte admin créé par l'assistant.
     * (Le cœur ne connaît pas les rôles du module — ici, "admin" = le compte
     * du setup, cohérent avec "1 serveur = 1 propriétaire".)
     */
    async function requireInstallAdmin(request, reply) {
        await authenticate(request, reply);
        const sub = request.user.sub;
        const adminEmail = loadInstallConfig().adminEmail;
        if (!adminEmail) {
            throw forbidden("Serveur pas encore configuré (lancez `private-server setup`)");
        }
        const [admin] = db
            .select({ id: users.id })
            .from(users)
            .where(eq(users.email, adminEmail))
            .limit(1)
            .all();
        if (!admin || admin.id !== sub) {
            throw forbidden("Réservé au compte administrateur de l'installation");
        }
    }
    /** Génération d'une clé (une seule affichée : le serveur ne la stocke pas). */
    app.post("/maintenance/keys", { schema: { body: jsonSchema(createKeySchema) }, preHandler: [requireInstallAdmin] }, async (request, reply) => {
        const sub = request.user.sub;
        const { token, expiresAt } = issueMaintenanceToken(sub);
        db.insert(maintenanceKeys)
            .values({
            tokenHash: sha256(token),
            label: request.body.label?.trim() || null,
            createdBy: sub,
            expiresAt,
        })
            .run();
        reply.status(201).send({ key: token, expiresAt: expiresAt.toISOString() });
    });
    /** Liste des clés émises (traçabilité) — hashs jamais renvoyés. */
    app.get("/maintenance/keys", { preHandler: [requireInstallAdmin] }, async (_request, reply) => {
        const rows = db
            .select()
            .from(maintenanceKeys)
            .orderBy(maintenanceKeys.createdAt)
            .all();
        reply.send(rows.map((k) => ({
            id: k.id,
            label: k.label,
            createdBy: k.createdBy,
            createdAt: k.createdAt.toISOString(),
            expiresAt: k.expiresAt.toISOString(),
            revoked: k.revokedAt !== null,
        })));
    });
    /** Révocation immédiate d'une clé. */
    app.delete("/maintenance/keys/:keyId", { preHandler: [requireInstallAdmin] }, async (request, reply) => {
        const { keyId } = request.params;
        const [row] = db
            .select()
            .from(maintenanceKeys)
            .where(eq(maintenanceKeys.id, keyId))
            .limit(1)
            .all();
        if (!row)
            throw new ApiError(404, "not_found", "Clé inconnue");
        db.update(maintenanceKeys)
            .set({ revokedAt: new Date() })
            .where(eq(maintenanceKeys.id, keyId))
            .run();
        reply.send({ ok: true });
    });
    /**
     * Sonde de l'agent de maintenance — LE endpoint protégé par une clé.
     * Donne l'état de la machine sans ouvrir l'API applicative.
     */
    app.get("/maintenance/status", { preHandler: [requireMaintenanceKey] }, async (request, reply) => {
        let disk = null;
        try {
            const fs = await statfs(env.DATA_DIR);
            disk = { free: fs.bavail * fs.bsize, total: fs.blocks * fs.bsize };
        }
        catch {
            /* statfs indisponible (FS exotique) — disk null */
        }
        const version = await appVersion();
        reply.send({
            service: "private-server",
            version,
            db: "up",
            dataDir: env.DATA_DIR,
            disk,
            uptimeSec: Math.round(process.uptime()),
            keyId: request.maintenance?.keyId,
        });
    });
}
let cachedVersion = null;
async function appVersion() {
    if (cachedVersion)
        return cachedVersion;
    try {
        const pkg = JSON.parse(await import("node:fs/promises").then((fs) => fs.readFile(new URL("../../../package.json", import.meta.url), "utf8")));
        cachedVersion = pkg.version ?? null;
    }
    catch {
        cachedVersion = null;
    }
    return cachedVersion;
}
//# sourceMappingURL=maintenance.js.map