/**
 * Routes webhook - Enregistrées SANS préfixe /api
 * Stripe envoie les webhooks à /payments/stripe/webhook
 */
import type { FastifyInstance } from "fastify";
import type { Pool } from "mysql2/promise";
import Stripe from "stripe";
import { recordAudit, recordWalletChange } from "../server/admin-service.js";
import type { WalletChange } from "../server/admin-service.js";

const parseSkinIds = (value: unknown): string[] => {
  if (Array.isArray(value)) {
    return value.map((entry) => String(entry)).filter(Boolean);
  }
  if (typeof value === "string" && value.trim().length > 0) {
    try {
      const parsed = JSON.parse(value) as unknown;
      if (Array.isArray(parsed)) {
        return parsed.map((entry) => String(entry)).filter(Boolean);
      }
    } catch {
      return value
        .split(",")
        .map((entry) => entry.trim())
        .filter(Boolean);
    }
  }
  return [];
};

const CASH_PACKS: Record<
  string,
  {
    name: string;
    price: number;
    items: { hints: number; undos: number; replays: number };
    bonusPoints: number;
  }
> = {
  silver: {
    name: "Pack Silver",
    price: 1.99,
    items: { hints: 6, undos: 4, replays: 1 },
    bonusPoints: 40,
  },
  gold: {
    name: "Pack Gold",
    price: 3.99,
    items: { hints: 12, undos: 8, replays: 2 },
    bonusPoints: 90,
  },
  platinum: {
    name: "Pack Platinum",
    price: 6.99,
    items: { hints: 24, undos: 16, replays: 4 },
    bonusPoints: 180,
  },
};

export const registerWebhookRoutes = async (
  app: FastifyInstance,
  db: Pool,
  stripe: Stripe | null,
  stripeWebhookSecret: string | undefined,
): Promise<void> => {
  // Plugin scopé : le content-type parser raw ne s'applique QU'À cette route
  // et ne pollue pas le parser JSON par défaut des autres routes
  await app.register(async (webhookApp) => {
    webhookApp.addContentTypeParser(
      "application/json",
      { parseAs: "buffer" },
      (_req, body, done) => {
        done(null, body);
      },
    );

    webhookApp.post("/payments/stripe/webhook", async (request, reply) => {
      // Log de réception du webhook
      app.log.info(
        {
          headers: request.headers,
          method: request.method,
          url: request.url,
        },
        "🔔 Webhook Stripe reçu",
      );

      if (!stripe || !stripeWebhookSecret) {
        app.log.error("Stripe non configuré (clé ou secret manquant)");
        reply.code(503);
        return { error: "Stripe non configuré" };
      }
      const signature = request.headers["stripe-signature"];
      if (typeof signature !== "string") {
        app.log.warn("Signature Stripe manquante dans les headers");
        reply.code(400);
        return { error: "Signature manquante" };
      }
      const rawBody = request.body as Buffer | undefined;
      if (!rawBody) {
        app.log.warn("Corps de requête vide");
        reply.code(400);
        return { error: "Payload invalide" };
      }
      let event: Stripe.Event;
      try {
        event = stripe.webhooks.constructEvent(
          rawBody,
          signature,
          stripeWebhookSecret,
        );
        app.log.info(
          { eventType: event.type, eventId: event.id },
          "✅ Événement Stripe vérifié",
        );
      } catch (error) {
        app.log.error({ error: String(error) }, "❌ Signature Stripe invalide");
        reply.code(400);
        return { error: "Signature invalide" };
      }

      if (event.type === "checkout.session.completed") {
        const session = event.data.object as Stripe.Checkout.Session;
        const metaUserId = Number(session.metadata?.userId ?? 0);
        const packId = session.metadata?.packId?.toString() ?? "";
        const packType = session.metadata?.packType?.toString() ?? "avatar";
        const sessionId = session.id;
        const paymentIntentId =
          typeof session.payment_intent === "string"
            ? session.payment_intent
            : session.payment_intent?.id;
        const amountTotal =
          typeof session.amount_total === "number" ? session.amount_total : 0;
        const currency = session.currency?.toUpperCase() ?? "EUR";

        if (metaUserId && packId) {
          // ═══════════════════════════════════════════════════════════════
          // Idempotence + transaction SQL
          // Le UNIQUE KEY sur stripe_session_id empêche le double-traitement
          // ═══════════════════════════════════════════════════════════════
          const connection = await db.getConnection();
          try {
            await connection.beginTransaction();

            // Verrou d'idempotence :
            // - Si la row existe avec status 'created' (pré-insérée au checkout) → on la passe à 'processing'
            // - Si aucune row n'existe → on l'insère avec status 'processing'
            // - Si la row existe avec status 'completed' ou 'processing' → doublon, on ignore

            // Étape A : essayer de promouvoir une row 'created' existante
            const [updateResult] = await connection.execute(
              `UPDATE payment_transactions
             SET status = 'processing', pack_type = ?, updated_at = NOW()
             WHERE stripe_session_id = ? AND status = 'created'`,
              [packType, sessionId],
            );
            const updated =
              (updateResult as { affectedRows?: number }).affectedRows ?? 0;

            if (updated === 0) {
              // Pas de row 'created' → soit elle n'existe pas, soit elle est déjà traitée
              const [insertResult] = await connection.execute(
                `INSERT IGNORE INTO payment_transactions
                 (user_id, pack_id, pack_type, provider, status, amount, currency, stripe_session_id, stripe_payment_intent_id)
               VALUES (?, ?, ?, 'stripe', 'processing', ?, ?, ?, ?)`,
                [
                  metaUserId,
                  packId,
                  packType,
                  amountTotal ? amountTotal / 100 : 0,
                  currency,
                  sessionId,
                  paymentIntentId ?? null,
                ],
              );
              const inserted =
                (insertResult as { affectedRows?: number }).affectedRows ?? 0;
              if (inserted === 0) {
                // La row existe déjà avec status 'processing' ou 'completed' → vrai doublon
                app.log.info(
                  { sessionId, packId, userId: metaUserId },
                  "Webhook doublon ignoré (session déjà traitée)",
                );
                await connection.rollback();
                return { received: true };
              }
            }

            // ─── Traitement par type de pack (dans la transaction) ───

            if (packType === "cash") {
              const cashPack = CASH_PACKS[packId];
              if (cashPack) {
                await connection.execute(
                  `UPDATE wallets SET hints = hints + ?, undos = undos + ?, replays = replays + ?, bonus_points = bonus_points + ?, last_updated = NOW() WHERE user_id = ?`,
                  [
                    cashPack.items.hints,
                    cashPack.items.undos,
                    cashPack.items.replays,
                    cashPack.bonusPoints,
                    metaUserId,
                  ],
                );

                await recordWalletChange(
                  connection,
                  metaUserId,
                  (
                    [
                      {
                        resource: "hints" as const,
                        delta: cashPack.items.hints,
                      },
                      {
                        resource: "undos" as const,
                        delta: cashPack.items.undos,
                      },
                      {
                        resource: "replays" as const,
                        delta: cashPack.items.replays,
                      },
                      {
                        resource: "bonus_points" as const,
                        delta: cashPack.bonusPoints,
                      },
                    ] satisfies WalletChange[]
                  ).filter((c) => c.delta !== 0),
                  "stripe.cash",
                  { packId, sessionId },
                );

                await connection.execute(
                  `INSERT INTO user_cash_pack_purchases (user_id, pack_id, pack_name, price_eur, items_hints, items_undos, items_replays, bonus_points, stripe_session_id, stripe_payment_intent_id, status, processed_at)
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
                  [
                    metaUserId,
                    packId,
                    cashPack.name,
                    amountTotal ? amountTotal / 100 : cashPack.price,
                    cashPack.items.hints,
                    cashPack.items.undos,
                    cashPack.items.replays,
                    cashPack.bonusPoints,
                    sessionId,
                    paymentIntentId ?? null,
                    "completed",
                  ],
                );
              }
            } else if (packType === "avatar") {
              const [rows] = await connection.execute(
                `SELECT pack_id FROM avatar_packs WHERE pack_id = ? AND is_active = 1 LIMIT 1`,
                [packId],
              );
	              if (Array.isArray(rows) && rows.length > 0) {
	                await connection.execute(
	                  `INSERT IGNORE INTO user_avatar_packs (user_id, pack_id, purchased_at, transaction_id, price_paid) VALUES (?, ?, NOW(), ?, ?)`,
	                  [metaUserId, packId, sessionId, amountTotal ? amountTotal / 100 : 0],
	                );

                const [avatarRows] = await connection.execute(
                  `SELECT avatar_id FROM pack_avatars WHERE pack_id = ? ORDER BY display_order ASC`,
                  [packId],
                );

                if (Array.isArray(avatarRows)) {
                  for (const avatarRow of avatarRows) {
                    const row = avatarRow as Record<string, unknown>;
                    const avatarId = row.avatar_id;
                    if (avatarId) {
                      await connection.execute(
                        `INSERT IGNORE INTO user_avatars (user_id, avatar_id, unlocked_at) VALUES (?, ?, NOW())`,
                        [metaUserId, avatarId],
                      );
                    }
                  }
                }
              }
            } else if (packType === "ball_skin") {
              const [rows] = await connection.execute(
                `SELECT pack_id, skin_ids FROM ball_skin_packs WHERE pack_id = ? AND is_active = 1 LIMIT 1`,
                [packId],
              );
              if (Array.isArray(rows) && rows.length > 0) {
                const row = rows[0] as Record<string, unknown>;
                const skinIds = parseSkinIds(row.skin_ids);

                await connection.execute(
                  `INSERT IGNORE INTO user_ball_skin_packs (user_id, pack_id, purchased_at, transaction_id, price_paid) VALUES (?, ?, NOW(), ?, ?)`,
                  [
                    metaUserId,
                    packId,
                    paymentIntentId ?? sessionId,
                    amountTotal ? amountTotal / 100 : 0,
                  ],
                );

                for (const skinId of skinIds) {
                  await connection.execute(
                    `INSERT IGNORE INTO user_ball_skins (user_id, skin_id, source_pack, unlocked_at) VALUES (?, ?, ?, NOW())`,
                    [metaUserId, skinId, packId],
                  );
                }
              }
            } else if (packType === "challenge") {
              await connection.execute(
                `INSERT IGNORE INTO user_challenge_pack (user_id, purchased_at, transaction_id, price_paid) VALUES (?, NOW(), ?, ?)`,
                [
                  metaUserId,
                  paymentIntentId ?? sessionId,
                  amountTotal ? amountTotal / 100 : 0,
                ],
              );
            } else if (packType === "vip") {
              const [currentRows] = await connection.execute<
                import("mysql2/promise").RowDataPacket[]
              >(
                "SELECT vip_expires_at FROM users WHERE id = ? LIMIT 1 FOR UPDATE",
                [metaUserId],
              );
              const currentExpires = currentRows[0]?.vip_expires_at
                ? new Date(currentRows[0].vip_expires_at)
                : null;
              const now = new Date();
              const baseDate =
                currentExpires && currentExpires > now ? currentExpires : now;
              const newExpires = new Date(baseDate);
              newExpires.setMonth(newExpires.getMonth() + 1);

              await connection.execute(
                "UPDATE users SET vip_no_ads = 1, vip_expires_at = ? WHERE id = ?",
                [
                  newExpires.toISOString().slice(0, 19).replace("T", " "),
                  metaUserId,
                ],
              );
            }

            // ─── Audit commun à tous les types de packs ───
            await recordAudit(connection, {
              userId: metaUserId,
              action: `payment.${packType}`,
              meta: {
                packId,
                sessionId,
                paymentIntentId: paymentIntentId ?? null,
                amount: amountTotal ? amountTotal / 100 : null,
                currency,
              },
              ip: request.ip,
              userAgent: request.headers["user-agent"]?.toString() ?? null,
              installationId:
                request.headers["x-installation-id"]?.toString() ?? null,
            });

            // ─── Marquer la transaction comme complétée ───
            await connection.execute(
              `UPDATE payment_transactions SET status = 'completed', stripe_payment_intent_id = ?, updated_at = NOW() WHERE stripe_session_id = ?`,
              [paymentIntentId ?? null, sessionId],
            );

            await connection.commit();
            app.log.info(
              { sessionId, packId, packType, userId: metaUserId },
              "Paiement Stripe traité avec succès",
            );
          } catch (error) {
            await connection.rollback();
            app.log.error(
              { err: error, sessionId, packId, userId: metaUserId },
              "Erreur traitement webhook Stripe",
            );
            // Supprimer la row 'processing' pour permettre un vrai retry Stripe
            try {
              await db.execute(
                `DELETE FROM payment_transactions WHERE stripe_session_id = ? AND status = 'processing'`,
                [sessionId],
              );
            } catch {
              // best-effort cleanup
            }
            // ⚠️ IMPORTANT: répondre en 5xx pour que Stripe RETENTE le webhook.
            // Sinon, on risque de "perdre" l'événement (pas de retry) et l'utilisateur
            // ne recevra jamais son contenu.
            reply.code(500);
            return reply.send({ error: "Erreur traitement webhook" });
          } finally {
            connection.release();
          }
        }
      }

      return { received: true };
    });
  });
};
