import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import type { Pool } from "mysql2/promise";
import type { Transporter } from "nodemailer";
import type { RouteContext } from "./types.js";
import type { User, UserRecord } from "../server/types.js";
import { REFRESH_COOKIE_NAME, REFRESH_TTL } from "../server/constants.js";
import {
  expiresAtFrom,
  hashPassword,
  isLocked,
  isBanned,
  verifyPassword,
  isLegacyBcrypt,
  randomTokenBase64Url,
} from "../server/crypto-utils.js";
import { ensureUserData } from "../server/db-schema.js";
import { handleBetaTesterRegistration } from "../server/beta-tester.js";
import { fetchProgress, fetchWallet } from "../server/data-fetchers.js";
import {
  toPublicUser,
  getUserByEmail,
  getUserById,
  createUser,
  createGuestUser,
} from "../server/user-service.js";
import {
  cleanupRefreshTokens,
  consumeEmailVerifyToken,
  revokeRefreshTokensForUser,
  storeRefreshToken,
  verifyRefreshToken,
  revokeRefreshToken,
  clearLoginFailures,
  recordFailedLogin,
  storeEmailVerifyToken,
  storeResetToken,
  consumeResetToken,
} from "../server/auth-service.js";
import {
  buildResetEmail,
  buildVerificationEmail,
} from "../server/email-service.js";
import { recordAudit } from "../server/admin-service.js";

export const registerAuthRoutes = (app: FastifyInstance, ctx: RouteContext) => {
  const {
    db,
    config,
    mailer,
    signAccessToken,
    signRefreshToken,
    setRefreshCookie,
    getRefreshCookie,
    clearRefreshCookie,
    requireAuth,
    appEnv,
  } = ctx;
  const normalizeRegisterDisplayName = (
    value: string | undefined,
    fallback: string,
  ): string => {
    const candidate = value?.trim() || fallback;
    return candidate.slice(0, 15);
  };

  const resolveWebAppUrl = (request: FastifyRequest): string => {
    if (config.appUrl?.trim()) {
      return config.appUrl.trim().replace(/\/+$/, "");
    }
    const host = request.headers.host ?? "localhost:5173";
    const proto = appEnv === "production" ? "https" : "http";
    if (host.includes(":3000")) {
      return `${proto}://${host.replace(":3000", ":5173")}`;
    }
    return `${proto}://${host}`;
  };

  const sendVerificationEmail = async (
    request: FastifyRequest,
    user: UserRecord,
    options?: { force?: boolean },
  ): Promise<string | undefined> => {
    if (user.guest || user.emailVerified) {
      return undefined;
    }
    const now = Date.now();
    const tokenExpiryMs = user.emailVerifyTokenExpires
      ? new Date(user.emailVerifyTokenExpires).getTime()
      : 0;
    const hasActiveToken =
      Boolean(user.emailVerifyTokenHash) && tokenExpiryMs > now + 60 * 1000;
    if (hasActiveToken && options?.force !== true) {
      return undefined;
    }
    const token = randomTokenBase64Url(32);
    await storeEmailVerifyToken(db, user.id, token);
    if (mailer) {
      try {
        const appBaseUrl = resolveWebAppUrl(request);
        const verifyUrl = `${appBaseUrl}/api/auth/verify-email?token=${encodeURIComponent(token)}`;
        const message = buildVerificationEmail({
          email: user.email,
          token,
          verifyUrl,
          fromName: config.smtp.fromName,
        });
        const fromEmail = config.smtp.fromEmail ?? "no-reply@rollerlogic.app";
        const fromLabel = config.smtp.fromName
          ? `${config.smtp.fromName} <${fromEmail}>`
          : fromEmail;
        await mailer.sendMail({
          from: fromLabel,
          to: user.email,
          subject: message.subject,
          text: message.text,
          html: message.html,
        });
      } catch (error) {
        request.log.error({ err: error }, "verify_email_failed");
      }
    }
    return appEnv !== "production" ? token : undefined;
  };

  const buildVerificationRedirectUrl = (
    request: FastifyRequest,
    verified: boolean,
    reason?: string,
  ): string => {
    const url = new URL(resolveWebAppUrl(request));
    url.searchParams.set("email_verified", verified ? "1" : "0");
    if (reason) {
      url.searchParams.set("reason", reason);
    }
    return url.toString();
  };

  const isDuplicateEntryError = (error: unknown): boolean => {
    return (
      typeof error === "object" &&
      error !== null &&
      "code" in error &&
      (error as { code?: string }).code === "ER_DUP_ENTRY"
    );
  };

  const getAuthenticatedGuestUser = async (
    request: FastifyRequest,
  ): Promise<UserRecord | null> => {
    if (!request.headers.authorization) {
      return null;
    }
    try {
      await request.jwtVerify();
    } catch {
      return null;
    }
    if (request.user?.typ && request.user.typ !== "access") {
      return null;
    }
    const userId = Number(request.user?.sub ?? 0);
    if (!Number.isInteger(userId) || userId <= 0) {
      return null;
    }
    const user = await getUserById(db, userId);
    if (!user || !user.guest) {
      return null;
    }
    return user;
  };

  const upgradeUserToRegistered = async (
    targetUser: UserRecord,
    payload: { email: string; displayName: string; passwordHash: string },
  ): Promise<UserRecord> => {
    await db.execute(
      "UPDATE users SET email = ?, password_hash = ?, display_name = ?, guest = FALSE, failed_login_attempts = 0, locked_until = NULL, reset_token_hash = NULL, reset_token_expires = NULL, email_verified = FALSE, email_verified_at = NULL, email_verify_token_hash = NULL, email_verify_token_expires = NULL WHERE id = ?",
      [
        payload.email,
        payload.passwordHash,
        payload.displayName,
        targetUser.id,
      ],
    );
    return {
      ...targetUser,
      email: payload.email,
      displayName: payload.displayName,
      guest: false,
      emailVerified: false,
      passwordHash: payload.passwordHash,
      failedAttempts: 0,
      lockedUntil: null,
      resetTokenHash: null,
      resetTokenExpires: null,
      emailVerifiedAt: null,
      emailVerifyTokenHash: null,
      emailVerifyTokenExpires: null,
    };
  };

  // POST /auth/register
  app.post(
    "/auth/register",
    {
      schema: {
        body: {
          type: "object",
          required: ["email", "password"],
          additionalProperties: false,
          properties: {
            email: {
              type: "string",
              minLength: 3,
              maxLength: 255,
              pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
            },
            password: { type: "string", minLength: 8, maxLength: 128 },
            displayName: { type: "string", minLength: 1, maxLength: 100 },
          },
        },
      },
      config: {
        rateLimit: {
          max: 20,
          timeWindow: "1 minute",
        },
      },
    },
    async (request, reply) => {
      const body = request.body as
        | { email?: string; displayName?: string; password?: string }
        | undefined;
      const email = body?.email?.trim();
      const password = body?.password ?? "";
      if (!email || !password.trim()) {
        reply.code(400);
        return { error: "Email et mot de passe requis" };
      }

      const customDisplayName = body?.displayName?.trim();
      const authenticatedGuest = await getAuthenticatedGuestUser(request);
      const existing = await getUserByEmail(db, email);
      const passwordHash = await hashPassword(password);

      let user: UserRecord;
      try {
        if (authenticatedGuest) {
          if (existing && existing.id !== authenticatedGuest.id) {
            reply.code(409);
            return { error: "Compte deja existant" };
          }
          const displayName = normalizeRegisterDisplayName(
            customDisplayName,
            `Joueur${authenticatedGuest.id}`,
          );
          user = await upgradeUserToRegistered(authenticatedGuest, {
            email,
            displayName,
            passwordHash,
          });
        } else if (existing) {
          if (existing.passwordHash) {
            reply.code(409);
            return { error: "Compte deja existant" };
          }
          const displayName = normalizeRegisterDisplayName(
            customDisplayName,
            `Joueur${existing.id}`,
          );
          user = await upgradeUserToRegistered(existing, {
            email,
            displayName,
            passwordHash,
          });
        } else {
          // Création d'un nouveau compte
          // On utilise un placeholder temporaire, puis on met à jour avec l'ID
          const tempDisplayName = normalizeRegisterDisplayName(
            customDisplayName,
            "Joueur",
          );
          user = await createUser(db, {
            email,
            displayName: tempDisplayName,
            passwordHash,
          });

          // Si pas de pseudo personnalisé, générer "JoueurXXXX" avec l'ID
          if (!customDisplayName) {
            const generatedDisplayName = `Joueur${user.id}`;
            await db.execute("UPDATE users SET display_name = ? WHERE id = ?", [
              generatedDisplayName,
              user.id,
            ]);
            user.displayName = generatedDisplayName;
          }
        }
      } catch (error) {
        if (isDuplicateEntryError(error)) {
          reply.code(409);
          return { error: "Compte deja existant" };
        }
        throw error;
      }
      await ensureUserData(db, user.id);

      // Attribution automatique des avatars beta_testeur si inscription pendant période beta
      await handleBetaTesterRegistration(db, user.id);

      const verificationToken = await sendVerificationEmail(request, user, {
        force: true,
      });

      await revokeRefreshTokensForUser(db, user.id);
      await cleanupRefreshTokens(db);
      const token = signAccessToken(user);
      const refreshToken = signRefreshToken(user);
      await storeRefreshToken(
        db,
        user.id,
        refreshToken,
        expiresAtFrom(REFRESH_TTL),
      );
      setRefreshCookie(reply, refreshToken);
      await recordAudit(db, {
        userId: user.id,
        action: "auth.register",
        meta: { email: user.email },
        ip: request.ip,
        userAgent: request.headers["user-agent"]?.toString() ?? null,
        installationId:
          request.headers["x-installation-id"]?.toString() ?? null,
      });
      // Enregistrer aussi comme login pour que le panel admin affiche le statut ACTIF
      await recordAudit(db, {
        userId: user.id,
        action: "auth.login",
        meta: { email: user.email, via: "register" },
        ip: request.ip,
        userAgent: request.headers["user-agent"]?.toString() ?? null,
        installationId:
          request.headers["x-installation-id"]?.toString() ?? null,
      });
      return {
        user: toPublicUser(user),
        wallet: await fetchWallet(db, user.id),
        progress: await fetchProgress(db, user.id),
        token,
        requiresEmailVerification: !user.guest && !user.emailVerified,
        ...(verificationToken ? { verificationToken } : {}),
      };
    },
  );

  // POST /auth/login
  app.post(
    "/auth/login",
    {
      schema: {
        body: {
          type: "object",
          required: ["email", "password"],
          additionalProperties: false,
          properties: {
            email: {
              type: "string",
              minLength: 3,
              maxLength: 255,
              pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
            },
            password: { type: "string", minLength: 8, maxLength: 128 },
          },
        },
      },
      config: {
        rateLimit: {
          max: 20,
          timeWindow: "1 minute",
        },
      },
    },
    async (request, reply) => {
      const body = request.body as
        | { email?: string; password?: string }
        | undefined;
      const email = body?.email?.trim();
      const password = body?.password ?? "";
      if (!email || !password.trim()) {
        reply.code(400);
        return { error: "Email et mot de passe requis" };
      }
      const user = await getUserByEmail(db, email);
      if (!user) {
        reply.code(401);
        return { error: "Identifiants invalides" };
      }
      if (isLocked(user.lockedUntil)) {
        reply.code(423);
        return { error: "Compte bloque temporairement" };
      }
      // Vérifier si l'utilisateur est banni (auto-déban si le ban a expiré)
      if (isBanned(user.bannedAt, user.banUntil)) {
        // Si le ban a une date de fin, indiquer quand il expire
        if (user.banUntil) {
          const banEnd = new Date(user.banUntil);
          const banEndDate = banEnd.toLocaleDateString("fr-FR");
          const banEndTime = banEnd.toLocaleTimeString("fr-FR", {
            hour: "2-digit",
            minute: "2-digit",
          });
          reply.code(403);
          return {
            error: `Compte banni jusqu'au ${banEndDate} à ${banEndTime}\nRaison: ${user.banReason || "Non spécifiée"}`,
          };
        }
        reply.code(403);
        return {
          error: `Compte banni définitivement\nRaison: ${user.banReason || "Non spécifiée"}`,
        };
      }
      // Si le ban a expiré, le lever automatiquement
      if (
        user.bannedAt &&
        user.banUntil &&
        !isBanned(user.bannedAt, user.banUntil)
      ) {
        await db.execute(
          "UPDATE users SET banned_at = NULL, ban_until = NULL, ban_reason = NULL WHERE id = ?",
          [user.id],
        );
        user.bannedAt = null;
        user.banUntil = null;
        user.banReason = null;
      }
      if (!user.passwordHash) {
        const passwordHash = await hashPassword(password);
        await db.execute(
          "UPDATE users SET password_hash = ?, guest = FALSE, failed_login_attempts = 0, locked_until = NULL WHERE id = ?",
          [passwordHash, user.id],
        );
        user.passwordHash = passwordHash;
        user.failedAttempts = 0;
        user.lockedUntil = null;
      } else {
        let ok = false;
        try {
          ok = await verifyPassword(password, user.passwordHash);
        } catch {
          ok = false;
        }
        if (!ok) {
          const result = await recordFailedLogin(
            db,
            user.id,
            user.failedAttempts,
          );
          reply.code(result.lockedUntil ? 423 : 401);
          return { error: "Identifiants invalides" };
        }
        if (isLegacyBcrypt(user.passwordHash)) {
          const nextHash = await hashPassword(password);
          await db.execute(
            "UPDATE users SET password_hash = ?, failed_login_attempts = 0, locked_until = NULL WHERE id = ?",
            [nextHash, user.id],
          );
          user.passwordHash = nextHash;
        } else {
          await clearLoginFailures(db, user.id);
        }
      }
      await ensureUserData(db, user.id);
      const verificationToken = await sendVerificationEmail(request, user);
      await revokeRefreshTokensForUser(db, user.id);
      await cleanupRefreshTokens(db);
      const token = signAccessToken(user);
      const refreshToken = signRefreshToken(user);
      await storeRefreshToken(
        db,
        user.id,
        refreshToken,
        expiresAtFrom(REFRESH_TTL),
      );
      setRefreshCookie(reply, refreshToken);
      await recordAudit(db, {
        userId: user.id,
        action: "auth.login",
        meta: { email: user.email },
        ip: request.ip,
        userAgent: request.headers["user-agent"]?.toString() ?? null,
        installationId:
          request.headers["x-installation-id"]?.toString() ?? null,
      });
      return {
        user: toPublicUser(user),
        wallet: await fetchWallet(db, user.id),
        progress: await fetchProgress(db, user.id),
        token,
        requiresEmailVerification: !user.guest && !user.emailVerified,
        ...(verificationToken ? { verificationToken } : {}),
      };
    },
  );

  // POST /auth/guest
  app.post(
    "/auth/guest",
    {
      config: {
        rateLimit: {
          max: 20,
          timeWindow: "1 minute",
        },
      },
    },
    async (request, reply) => {
      const user = await createGuestUser(db);
      await ensureUserData(db, user.id);
      // Donner aussi les avatars beta aux comptes invités créés pendant la période beta
      await handleBetaTesterRegistration(db, user.id);
      await revokeRefreshTokensForUser(db, user.id);
      await cleanupRefreshTokens(db);
      const token = signAccessToken(user);
      const refreshToken = signRefreshToken(user);
      await storeRefreshToken(
        db,
        user.id,
        refreshToken,
        expiresAtFrom(REFRESH_TTL),
      );
      setRefreshCookie(reply, refreshToken);
      await recordAudit(db, {
        userId: user.id,
        action: "auth.guest",
        meta: { email: user.email },
        ip: request.ip,
        userAgent: request.headers["user-agent"]?.toString() ?? null,
        installationId:
          request.headers["x-installation-id"]?.toString() ?? null,
      });
      return {
        user: toPublicUser(user),
        wallet: await fetchWallet(db, user.id),
        progress: await fetchProgress(db, user.id),
        token,
      };
    },
  );

  // POST /auth/request-reset
  app.post(
    "/auth/request-reset",
    {
      schema: {
        body: {
          type: "object",
          required: ["email"],
          additionalProperties: false,
          properties: {
            email: {
              type: "string",
              minLength: 3,
              maxLength: 255,
              pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
            },
          },
        },
      },
      config: {
        rateLimit: {
          max: 10,
          timeWindow: "1 minute",
        },
      },
    },
    async (request, reply) => {
      const body = request.body as { email?: string } | undefined;
      const email = body?.email?.trim();
      if (!email) {
        reply.code(400);
        return { error: "Email requis" };
      }
      const user = await getUserByEmail(db, email);
      let resetToken: string | undefined;
      if (user) {
        const token = randomTokenBase64Url(32);
        await storeResetToken(db, user.id, token);
        resetToken = token;
        await recordAudit(db, {
          userId: user.id,
          action: "auth.reset.request",
          meta: { email: user.email },
          ip: request.ip,
          userAgent: request.headers["user-agent"]?.toString() ?? null,
          installationId:
            request.headers["x-installation-id"]?.toString() ?? null,
        });
        if (mailer) {
          try {
            const message = buildResetEmail({
              email: user.email,
              token,
              appUrl: config.appUrl,
              fromName: config.smtp.fromName,
            });
            const fromEmail =
              config.smtp.fromEmail ?? "no-reply@rollerlogic.app";
            const fromLabel = config.smtp.fromName
              ? `${config.smtp.fromName} <${fromEmail}>`
              : fromEmail;
            await mailer.sendMail({
              from: fromLabel,
              to: user.email,
              subject: message.subject,
              text: message.text,
              html: message.html,
            });
          } catch (error) {
            request.log.error({ err: error }, "reset_email_failed");
          }
        }
      }
      const response: { ok: boolean; resetToken?: string } = { ok: true };
      if (appEnv !== "production" && resetToken) {
        response.resetToken = resetToken;
      }
      return response;
    },
  );

  // POST /auth/reset-password
  app.post(
    "/auth/reset-password",
    {
      schema: {
        body: {
          type: "object",
          required: ["token", "password"],
          additionalProperties: false,
          properties: {
            token: { type: "string", minLength: 20, maxLength: 256 },
            password: { type: "string", minLength: 8, maxLength: 128 },
          },
        },
      },
      config: {
        rateLimit: {
          max: 10,
          timeWindow: "1 minute",
        },
      },
    },
    async (request, reply) => {
      const body = request.body as
        | { token?: string; password?: string }
        | undefined;
      const token = body?.token?.trim();
      const password = body?.password ?? "";
      if (!token || !password.trim()) {
        reply.code(400);
        return { error: "Token et mot de passe requis" };
      }
      const user = await consumeResetToken(db, token);
      if (!user) {
        reply.code(400);
        return { error: "Token invalide ou expire" };
      }
      const passwordHash = await hashPassword(password);
      await db.execute(
        "UPDATE users SET password_hash = ?, guest = FALSE, failed_login_attempts = 0, locked_until = NULL WHERE id = ?",
        [passwordHash, user.id],
      );
      await revokeRefreshTokensForUser(db, user.id);
      await cleanupRefreshTokens(db);
      await recordAudit(db, {
        userId: user.id,
        action: "auth.reset.complete",
        meta: { email: user.email },
        ip: request.ip,
        userAgent: request.headers["user-agent"]?.toString() ?? null,
        installationId:
          request.headers["x-installation-id"]?.toString() ?? null,
      });
      return { ok: true };
    },
  );

  // GET /auth/verify-email
  app.get(
    "/auth/verify-email",
    {
      schema: {
        querystring: {
          type: "object",
          required: ["token"],
          additionalProperties: false,
          properties: {
            token: { type: "string", minLength: 20, maxLength: 256 },
          },
        },
      },
      config: {
        rateLimit: {
          max: 30,
          timeWindow: "1 minute",
        },
      },
    },
    async (request, reply) => {
      const query = request.query as { token?: string } | undefined;
      const token = query?.token?.trim();
      if (!token) {
        return reply.redirect(
          buildVerificationRedirectUrl(request, false, "missing_token"),
          302,
        );
      }

      const user = await consumeEmailVerifyToken(db, token);
      if (!user) {
        return reply.redirect(
          buildVerificationRedirectUrl(request, false, "invalid_or_expired"),
          302,
        );
      }

      await revokeRefreshTokensForUser(db, user.id);
      await cleanupRefreshTokens(db);
      const refreshToken = signRefreshToken({
        id: user.id,
        email: user.email,
        emailVerified: true,
      });
      await storeRefreshToken(
        db,
        user.id,
        refreshToken,
        expiresAtFrom(REFRESH_TTL),
      );
      setRefreshCookie(reply, refreshToken);
      await recordAudit(db, {
        userId: user.id,
        action: "auth.verify_email",
        meta: { email: user.email },
        ip: request.ip,
        userAgent: request.headers["user-agent"]?.toString() ?? null,
        installationId:
          request.headers["x-installation-id"]?.toString() ?? null,
      });
      return reply.redirect(buildVerificationRedirectUrl(request, true), 302);
    },
  );

  // POST /auth/resend-verification
  app.post(
    "/auth/resend-verification",
    {
      preHandler: requireAuth,
      config: {
        rateLimit: {
          max: 10,
          timeWindow: "1 minute",
        },
      },
    },
    async (request, reply) => {
      const userId = request.user?.sub;
      if (!userId) {
        reply.code(401);
        return { error: "Authentification requise" };
      }
      const user = await getUserById(db, userId);
      if (!user) {
        reply.code(404);
        return { error: "Compte introuvable" };
      }
      if (user.guest || user.emailVerified) {
        return { ok: true, alreadyVerified: true };
      }
      const verificationToken = await sendVerificationEmail(request, user, {
        force: true,
      });
      await recordAudit(db, {
        userId: user.id,
        action: "auth.verify_email.resend",
        meta: { email: user.email },
        ip: request.ip,
        userAgent: request.headers["user-agent"]?.toString() ?? null,
        installationId:
          request.headers["x-installation-id"]?.toString() ?? null,
      });
      return {
        ok: true,
        ...(verificationToken ? { verificationToken } : {}),
      };
    },
  );

  // POST /auth/refresh
  app.post(
    "/auth/refresh",
    {
      schema: {
        body: {
          type: "object",
          additionalProperties: false,
          properties: {
            refreshToken: { type: "string", minLength: 10 },
          },
        },
      },
      config: {
        rateLimit: {
          max: 30,
          timeWindow: "1 minute",
        },
      },
    },
    async (request, reply) => {
      const body = request.body as { refreshToken?: string } | undefined;
      const cookieToken = getRefreshCookie(request);
      const providedToken = body?.refreshToken ?? cookieToken;

      request.log.info(
        {
          hasCookieToken: !!cookieToken,
          hasBodyToken: !!body?.refreshToken,
          cookieKeys: Object.keys(request.cookies ?? {}),
        },
        "refresh_attempt",
      );

      if (!providedToken) {
        request.log.info({ reason: "no_token" }, "refresh_failed");
        reply.code(204);
        return reply.send();
      }
      try {
        const payload = (await app.jwt.verify(providedToken)) as {
          sub: number;
          email: string;
          typ?: string;
          ev?: boolean;
        };
        if (payload.typ !== "refresh") {
          request.log.info(
            { reason: "wrong_type", typ: payload.typ },
            "refresh_failed",
          );
          if (cookieToken && providedToken === cookieToken) {
            clearRefreshCookie(reply);
          }
          reply.code(401);
          return { error: "Token invalide" };
        }
        const valid = await verifyRefreshToken(db, payload.sub, providedToken);
        if (!valid) {
          request.log.info(
            { reason: "not_in_db", userId: payload.sub },
            "refresh_failed",
          );
          if (cookieToken && providedToken === cookieToken) {
            clearRefreshCookie(reply);
          }
          reply.code(401);
          return { error: "Token invalide" };
        }
        await cleanupRefreshTokens(db);
        const user = await getUserById(db, payload.sub);
        if (!user) {
          if (cookieToken && providedToken === cookieToken) {
            clearRefreshCookie(reply);
          }
          reply.code(401);
          return { error: "Token invalide" };
        }
        const effectiveEmail = user.email;
        const effectiveEmailVerified = user.emailVerified;
        const token = signAccessToken({
          id: payload.sub,
          email: effectiveEmail,
          emailVerified: effectiveEmailVerified,
        });
        const newRefreshToken = signRefreshToken({
          id: payload.sub,
          email: effectiveEmail,
          emailVerified: effectiveEmailVerified,
        });
        await storeRefreshToken(
          db,
          payload.sub,
          newRefreshToken,
          expiresAtFrom(REFRESH_TTL),
        );
        setRefreshCookie(reply, newRefreshToken);
        await recordAudit(db, {
          userId: payload.sub,
          action: "auth.refresh",
          ip: request.ip,
          userAgent: request.headers["user-agent"]?.toString() ?? null,
          installationId:
            request.headers["x-installation-id"]?.toString() ?? null,
        });
        return { token };
      } catch (err) {
        request.log.info(
          { reason: "jwt_verify_failed", error: String(err) },
          "refresh_failed",
        );
        if (cookieToken && providedToken === cookieToken) {
          clearRefreshCookie(reply);
        }
        reply.code(401);
        return { error: "Token invalide" };
      }
    },
  );

  // POST /auth/logout
  app.post(
    "/auth/logout",
    {
      preHandler: requireAuth,
      schema: {
        body: {
          type: "object",
          additionalProperties: false,
          properties: {
            refreshToken: { type: "string", minLength: 10 },
          },
        },
      },
    },
    async (request, reply) => {
      const body = request.body as { refreshToken?: string } | undefined;
      const userId = request.user?.sub;
      if (!userId) {
        reply.code(401);
        return { error: "Authentification requise" };
      }
      const refreshToken = body?.refreshToken ?? getRefreshCookie(request);
      if (refreshToken) {
        await revokeRefreshToken(db, refreshToken);
      } else {
        await revokeRefreshTokensForUser(db, userId);
      }
      await cleanupRefreshTokens(db);
      clearRefreshCookie(reply);
      await recordAudit(db, {
        userId,
        action: "auth.logout",
        ip: request.ip,
        userAgent: request.headers["user-agent"]?.toString() ?? null,
        installationId:
          request.headers["x-installation-id"]?.toString() ?? null,
      });
      return { ok: true };
    },
  );

  // GET /auth/me
  app.get(
    "/auth/me",
    {
      preHandler: requireAuth,
    },
    async (request, reply) => {
      const userId = request.user?.sub;
      if (!userId) {
        reply.code(401);
        return { error: "Authentification requise" };
      }
      const user = await getUserById(db, userId);
      if (!user) {
        reply.code(404);
        return { error: "Compte introuvable" };
      }
      await ensureUserData(db, user.id);
      return { user: toPublicUser(user) };
    },
  );

  // DELETE /account
  app.delete(
    "/account",
    {
      preHandler: requireAuth,
    },
    async (request, reply) => {
      const userId = request.user?.sub;
      if (!userId) {
        reply.code(401);
        return { error: "Authentification requise" };
      }
      await recordAudit(db, {
        userId,
        action: "account.delete",
        ip: request.ip,
        userAgent: request.headers["user-agent"]?.toString() ?? null,
        installationId:
          request.headers["x-installation-id"]?.toString() ?? null,
      });
      const [result] = await db.execute(
        "DELETE FROM users WHERE id = ? LIMIT 1",
        [userId],
      );
      const affected = (result as { affectedRows?: number }).affectedRows ?? 0;
      await cleanupRefreshTokens(db);
      clearRefreshCookie(reply);
      if (!affected) {
        reply.code(404);
        return { error: "Compte introuvable" };
      }
      return { ok: true };
    },
  );
};
