import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import jwt from "jsonwebtoken";
import { loginRequestSchema, logoutRequestSchema, refreshRequestSchema, registerRequestSchema, } from "@familyalbum/shared";
import { env } from "../env.js";
import { refreshSecret } from "../config.js";
import { conflict, unauthorized } from "../errors.js";
import { hashPassword, verifyPassword } from "../auth/passwords.js";
import { sha256 } from "../crypto/envelope.js";
import { addDays, now } from "../db/context.js";
import { db, isUniqueViolation } from "../db/client.js";
import { refreshTokens, users } from "../db/schema.js";
import { buildAuthHooks } from "../plugins/auth.js";
import { jsonSchema } from "../lib/json-schema.js";
/**
 * Émet une paire de tokens (access JWT + refresh opaque) et enregistre le
 * hash du refresh token. `tx` optionnel : passé quand le caller est déjà
 * dans une transaction (register, rotation) pour éviter une transaction
 * imbriquée inutile.
 */
export function issueTokens(app, userId, familyId, tx = null) {
    const refreshToken = jwt.sign({ sub: userId, jti: randomUUID() }, refreshSecret(), {
        expiresIn: `${env.REFRESH_TOKEN_TTL_DAYS}d`,
        algorithm: "HS256",
    });
    (tx ?? db)
        .insert(refreshTokens)
        .values({
        userId,
        tokenHash: sha256(refreshToken),
        familyId,
        expiresAt: addDays(now(), env.REFRESH_TOKEN_TTL_DAYS),
    })
        .run();
    const accessToken = app.jwt.sign({ sub: userId });
    return { accessToken, refreshToken };
}
function verifyRefreshToken(token) {
    const payload = jwt.verify(token, refreshSecret(), {
        algorithms: ["HS256"],
    });
    if (typeof payload.sub !== "string")
        throw new Error("sub manquant");
    return payload;
}
export function loadMe(userId, familyProvider) {
    const [userRow] = db
        .select()
        .from(users)
        .where(eq(users.id, userId))
        .limit(1)
        .all();
    if (!userRow)
        throw unauthorized();
    return {
        user: {
            id: userRow.id,
            email: userRow.email,
            displayName: userRow.displayName,
            createdAt: userRow.createdAt.toISOString(),
        },
        families: familyProvider.listForUser(userId),
    };
}
export function registerAuthRoutes(app, opts) {
    const { authenticate } = buildAuthHooks(app);
    /**
     * Inscription = compte + famille initiale (module) dans une même
     * transaction. Remplace la fonction SQL fa_register_user de l'ancien
     * backend (qui faisait pareil, en Postgres).
     */
    app.post("/auth/register", { schema: { body: jsonSchema(registerRequestSchema) } }, async (request, reply) => {
        const { email, password, displayName, familyName } = request.body;
        let ids;
        try {
            ids = db.transaction((tx) => {
                const [user] = tx
                    .insert(users)
                    .values({
                    email,
                    passwordHash: hashPassword(password),
                    displayName,
                })
                    .returning()
                    .all();
                if (!user)
                    throw new Error("Échec de l'inscription (insert user)");
                const familyId = opts.familyProvider.createInitialFamily(tx, user.id, familyName);
                return { p_user_id: user.id, p_family_id: familyId };
            });
        }
        catch (err) {
            if (isUniqueViolation(err)) {
                throw conflict("Cet email est déjà utilisé", "email_taken");
            }
            throw err;
        }
        const tokens = issueTokens(app, ids.p_user_id, ids.p_family_id);
        const me = loadMe(ids.p_user_id, opts.familyProvider);
        reply.status(201).send({ user: me.user, families: me.families, tokens });
    });
    app.post("/auth/login", { schema: { body: jsonSchema(loginRequestSchema) } }, async (request, reply) => {
        const { email, password } = request.body;
        const [userRow] = db
            .select()
            .from(users)
            .where(eq(users.email, email))
            .limit(1)
            .all();
        if (!userRow?.passwordHash)
            throw unauthorized("Email ou mot de passe invalide");
        if (!verifyPassword(password, userRow.passwordHash)) {
            throw unauthorized("Email ou mot de passe invalide");
        }
        const tokens = issueTokens(app, userRow.id, null);
        const me = loadMe(userRow.id, opts.familyProvider);
        reply.send({ user: me.user, families: me.families, tokens });
    });
    app.post("/auth/refresh", { schema: { body: jsonSchema(refreshRequestSchema) } }, async (request, reply) => {
        const { refreshToken } = request.body;
        let payload;
        try {
            payload = verifyRefreshToken(refreshToken);
        }
        catch {
            throw unauthorized("Refresh token invalide");
        }
        const hash = sha256(refreshToken);
        const [row] = db
            .select()
            .from(refreshTokens)
            .where(eq(refreshTokens.tokenHash, hash))
            .limit(1)
            .all();
        if (!row || row.revokedAt || row.expiresAt < now()) {
            throw unauthorized("Refresh token révoqué ou expiré");
        }
        // Rotation : on révoque l'ancien et on émet le nouveau.
        const tokens = db.transaction((tx) => {
            tx.update(refreshTokens).set({ revokedAt: now() }).where(eq(refreshTokens.id, row.id)).run();
            return issueTokens(app, payload.sub, row.familyId, tx);
        });
        reply.send(tokens);
    });
    app.post("/auth/logout", { schema: { body: jsonSchema(logoutRequestSchema) } }, async (request, reply) => {
        const { refreshToken } = request.body;
        let payload;
        try {
            payload = verifyRefreshToken(refreshToken);
        }
        catch {
            reply.send({ ok: true });
            return;
        }
        db.update(refreshTokens)
            .set({ revokedAt: now() })
            .where(eq(refreshTokens.tokenHash, sha256(refreshToken)))
            .run();
        reply.send({ ok: true });
    });
    app.get("/auth/me", { preHandler: [authenticate] }, async (request, reply) => {
        const user = request.user;
        reply.send(loadMe(user.sub, opts.familyProvider));
    });
}
//# sourceMappingURL=auth.js.map