import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import type { RouteContext } from "./types.js";
import { recordAudit } from "../server/admin-service.js";
import { toPublicUser, getUserById } from "../server/user-service.js";

const paramsUserIdSchema = {
  type: "object" as const,
  required: ["userId"],
  properties: { userId: { type: "string", pattern: "^[0-9]+$" } },
};

export const registerProfileRoutes = (
  app: FastifyInstance,
  ctx: RouteContext,
) => {
  const { db, requireAuth } = ctx;

  // Bloque les caractères dangereux tout en autorisant les emojis et les accents
  const hasHtmlOrDangerousChars = (value: string): boolean => {
    // Caractères de contrôle (sécurité)
    if (/[\u0000-\u001F\u007F]/.test(value)) return true;
    // Caractères HTML dangereux (protection XSS)
    if (/[<>"'&]/.test(value)) return true;
    return false;
  };

  // POST /profile/check-display-name
  app.post(
    "/profile/check-display-name",
    {
      preHandler: requireAuth,
      config: {
        rateLimit: {
          max: 30,
          timeWindow: "1 minute",
        },
      },
      schema: {
        body: {
          type: "object",
          required: ["displayName"],
          additionalProperties: false,
          properties: {
            displayName: { type: "string", minLength: 1, maxLength: 15 },
          },
        },
      },
    },
    async (request, reply) => {
      const userId = request.user?.sub;
      if (!userId) {
        reply.code(401);
        return { error: "Authentification requise" };
      }
      const body = request.body as { displayName?: string } | undefined;
      const displayName = body?.displayName?.trim();
      if (!displayName) {
        reply.code(400);
        return { error: "Pseudo requis" };
      }
      if (hasHtmlOrDangerousChars(displayName)) {
        reply.code(400);
        return { error: "Pseudo invalide (caractères interdits)" };
      }
      const [rows] = await db.execute(
        "SELECT id FROM users WHERE LOWER(display_name) = LOWER(?) AND id != ? LIMIT 1",
        [displayName, userId],
      );
      const exists = Array.isArray(rows) && rows.length > 0;
      return { available: !exists };
    },
  );

  // PUT /profile/:userId
  app.put(
    "/profile/:userId",
    {
      preHandler: requireAuth,
      config: {
        rateLimit: {
          max: 10,
          timeWindow: "1 minute",
        },
      },
      schema: {
        params: paramsUserIdSchema,
        body: {
          type: "object",
          required: ["displayName", "accent", "motto", "avatar"],
          additionalProperties: false,
          properties: {
            displayName: { type: "string", minLength: 1, maxLength: 15 },
            accent: { type: "string", maxLength: 50 },
            motto: { type: "string", maxLength: 200 },
            avatar: { type: "string", maxLength: 100 },
            title: { type: "string", maxLength: 50 },
            ballSkin: { type: "string", maxLength: 50 },
          },
        },
      },
    },
    async (request, reply) => {
      const userId = Number((request.params as { userId: string }).userId);
      const tokenUserId = Number(request.user?.sub);
      const body = request.body as
        | {
            displayName?: string;
            accent?: string;
            motto?: string;
            avatar?: string;
            title?: string;
            ballSkin?: string;
          }
        | undefined;
      if (!Number.isFinite(userId)) {
        reply.code(400);
        return { error: "userId invalide" };
      }
      if (!Number.isFinite(tokenUserId) || tokenUserId !== userId) {
        reply.code(403);
        return { error: "Acces refuse" };
      }
      if (!body) {
        reply.code(400);
        return { error: "Payload invalide" };
      }
      const displayName = body.displayName?.trim();
      if (!displayName) {
        reply.code(400);
        return { error: "Pseudo requis" };
      }
      if (hasHtmlOrDangerousChars(displayName)) {
        reply.code(400);
        return { error: "Pseudo invalide (caractères interdits)" };
      }
      const [existingRows] = await db.execute(
        "SELECT id FROM users WHERE LOWER(display_name) = LOWER(?) AND id != ? LIMIT 1",
        [displayName, userId],
      );
      if (Array.isArray(existingRows) && existingRows.length > 0) {
        reply.code(409);
        return { error: "Ce pseudo est déjà utilisé" };
      }
      const accent = body.accent?.trim() || null;
      const motto = body.motto?.trim() || null;
      const title = body.title?.trim() || null;
      if (motto && hasHtmlOrDangerousChars(motto)) {
        reply.code(400);
        return { error: "Signature invalide (caractères interdits)" };
      }
      if (title && hasHtmlOrDangerousChars(title)) {
        reply.code(400);
        return { error: "Titre invalide (caractères interdits)" };
      }
      const rawAvatar = body.avatar?.trim() || "";
      const rawBallSkin = body.ballSkin?.trim() || "";
      // Supporter les chemins d'avatars complets (ex: /avatars/380_animal_avatard/224.svg)
      // ou les codes courts d'avatars de base (ex: A, AB, ABC)
      const avatar = rawAvatar
        ? rawAvatar.startsWith("/avatars/")
          ? rawAvatar.slice(0, 100)
          : rawAvatar.length <= 3
            ? rawAvatar.toUpperCase()
            : rawAvatar.slice(0, 100)
        : null;
      const ballSkin = rawBallSkin.length > 0 ? rawBallSkin.slice(0, 50) : null;
      await db.execute(
        "UPDATE users SET display_name = ?, avatar = ?, accent = ?, motto = ?, title = ? WHERE id = ?",
        [displayName, avatar, accent, motto, title, userId],
      );
      await db.execute(
        "UPDATE user_settings SET ball_skin = ? WHERE user_id = ?",
        [ballSkin, userId],
      );
      await recordAudit(db, {
        userId,
        action: "profile.update",
        ip: request.ip,
        userAgent: request.headers["user-agent"]?.toString() ?? null,
        installationId:
          request.headers["x-installation-id"]?.toString() ?? null,
      });
      const updated = await getUserById(db, userId);
      if (!updated) {
        reply.code(404);
        return { error: "Compte introuvable" };
      }
      return { user: toPublicUser(updated) };
    },
  );
};
