import { eq } from "drizzle-orm";
import { createInvitationRequestSchema, joinRequestSchema } from "@familyalbum/shared";
import { ApiError, badRequest } from "../../../core/errors.js";
import { addDays, now } from "../../../core/db/context.js";
import { db, isUniqueViolation } from "../../../core/db/client.js";
import { buildAuthHooks } from "../../../core/plugins/auth.js";
import { generateInvitationCode, normalizeInvitationCode } from "../../../core/lib/codes.js";
import { jsonSchema } from "../../../core/lib/json-schema.js";
import { buildFamilyAuthHooks } from "../plugins/auth.js";
import { issueTokens, loadMe } from "../../../core/routes/auth.js";
import { invitations, families, familyMembers } from "../db/schema.js";
import { users } from "../../../core/db/schema.js";
function serializeInvitation(inv) {
    return {
        id: inv.id,
        familyId: inv.familyId,
        role: inv.role,
        code: inv.code,
        expiresAt: inv.expiresAt.toISOString(),
        maxUses: inv.maxUses,
        usedCount: inv.usedCount,
        revokedAt: inv.revokedAt?.toISOString() ?? null,
    };
}
export function registerInvitationRoutes(app, opts) {
    const { authenticate } = buildAuthHooks(app);
    const { requireFamilyMember } = buildFamilyAuthHooks(app);
    app.post("/families/:familyId/invitations", {
        schema: { body: jsonSchema(createInvitationRequestSchema) },
        preHandler: [
            authenticate,
            requireFamilyMember({ familyParam: "familyId", minRole: "coadmin" }),
        ],
    }, async (request, reply) => {
        const ctx = request.familyContext;
        const { role, maxUses, expiresInDays } = request.body;
        let inserted = null;
        for (let attempt = 0; attempt < 5 && !inserted; attempt++) {
            try {
                inserted = db
                    .insert(invitations)
                    .values({
                    familyId: ctx.familyId,
                    role,
                    code: generateInvitationCode(),
                    createdBy: ctx.userId,
                    expiresAt: addDays(now(), expiresInDays),
                    maxUses,
                })
                    .returning()
                    .all()[0] ?? null;
            }
            catch (err) {
                if (!isUniqueViolation(err))
                    throw err;
                // collision de code — on retente avec un nouveau code
            }
        }
        if (!inserted) {
            throw new ApiError(500, "code_generation_failed", "Échec de génération du code");
        }
        reply.status(201).send(serializeInvitation(inserted));
    });
    app.get("/families/:familyId/invitations", {
        preHandler: [
            authenticate,
            requireFamilyMember({ familyParam: "familyId", minRole: "coadmin" }),
        ],
    }, async (request, reply) => {
        const ctx = request.familyContext;
        const rows = db
            .select()
            .from(invitations)
            .where(eq(invitations.familyId, ctx.familyId))
            .orderBy(invitations.createdAt)
            .all();
        reply.send(rows.map(serializeInvitation));
    });
    app.post("/families/:familyId/invitations/:invitationId/revoke", {
        preHandler: [
            authenticate,
            requireFamilyMember({ familyParam: "familyId", minRole: "coadmin" }),
        ],
    }, async (request, reply) => {
        const ctx = request.familyContext;
        const params = request.params;
        const [row] = db
            .update(invitations)
            .set({ revokedAt: now() })
            .where(eq(invitations.id, params.invitationId))
            .returning()
            .all();
        if (!row)
            throw new ApiError(404, "not_found", "Invitation introuvable");
        reply.send(serializeInvitation(row));
    });
    /**
     * Jointure par code — publique (aucun token requis).
     * Remplace fa_join_family : vérifie code/expiration/usages et crée
     * l'utilisateur viewer + l'appartenance, dans une transaction. Codes
     * d'erreur conservés à l'identique (INV-01..04) pour le client.
     */
    app.post("/join", { schema: { body: jsonSchema(joinRequestSchema) } }, async (request, reply) => {
        const code = normalizeInvitationCode(request.body.code);
        if (!code)
            throw badRequest("Code d'invitation invalide");
        const row = db.transaction((tx) => {
            const [inv] = tx.select().from(invitations).where(eq(invitations.code, code)).limit(1).all();
            if (!inv)
                throw badRequest("Code d'invitation invalide", "invitation_unknown");
            if (inv.revokedAt)
                throw badRequest("Code d'invitation révoqué", "invitation_revoked");
            if (inv.expiresAt < now())
                throw badRequest("Code d'invitation expiré", "invitation_expired");
            if (inv.usedCount >= inv.maxUses) {
                throw badRequest("Code d'invitation épuisé", "invitation_used_max");
            }
            const [family] = tx
                .select({ name: families.name })
                .from(families)
                .where(eq(families.id, inv.familyId))
                .limit(1)
                .all();
            const [user] = tx
                .insert(users)
                .values({ displayName: request.body.displayName })
                .returning()
                .all();
            if (!user)
                throw new ApiError(500, "join_failed", "Échec de la jointure");
            tx.insert(familyMembers)
                .values({ familyId: inv.familyId, userId: user.id, role: inv.role })
                .run();
            tx.update(invitations)
                .set({ usedCount: inv.usedCount + 1 })
                .where(eq(invitations.id, inv.id))
                .run();
            return {
                p_user_id: user.id,
                p_family_id: inv.familyId,
                p_role: inv.role,
                p_family_name: family?.name ?? "",
            };
        });
        const tokens = issueTokens(app, row.p_user_id, row.p_family_id);
        // Même forme que /auth/register et /auth/login (`families` en tableau) —
        // les clients s'appuient dessus pour aiguiller la navigation. `family`
        // reste renvoyé en plus : additif, aucun client déjà installé ne casse.
        const me = loadMe(row.p_user_id, opts.familyProvider);
        reply.status(201).send({
            ...me,
            family: me.families.find((f) => f.id === row.p_family_id) ?? {
                id: row.p_family_id,
                name: row.p_family_name,
                createdAt: "",
                role: row.p_role,
                permission: "full",
                reactionsEnabled: true,
                commentsEnabled: true,
                createdBy: null,
            },
            tokens,
        });
    });
}
//# sourceMappingURL=invitations.js.map