import * as fs from 'node:fs';
import type { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';
import multipart from '@fastify/multipart';
import { deleteFile } from '../utils/storage';
import { streamRecipe, type StructuredRecipe } from '../ai/recipe-generator';
import { generateRecipeImage } from '../ai/image-generator';
import type { UserPreferences } from '../ai/prompts';

type SSESender = (event: string, data: unknown) => void;

/**
 * Prépare la réponse pour du Server-Sent Events.
 *
 * - Appelle `reply.hijack()` pour dire à Fastify "je m'occupe de la réponse
 *   moi-même, ne tente pas de faire reply.send()"
 * - Écrit les headers SSE + les headers CORS manuellement (les hooks onSend
 *   de @fastify/cors ne s'exécutent pas sur une réponse hijackée)
 */
function setupSSE(request: FastifyRequest, reply: FastifyReply): SSESender {
  reply.hijack();

  const origin = request.headers.origin;
  const corsHeaders: Record<string, string> = {};
  if (origin) {
    corsHeaders['Access-Control-Allow-Origin'] = origin;
    corsHeaders['Access-Control-Allow-Credentials'] = 'true';
    corsHeaders['Vary'] = 'Origin';
  }

  reply.raw.writeHead(200, {
    'Content-Type': 'text/event-stream; charset=utf-8',
    'Cache-Control': 'no-cache, no-transform',
    Connection: 'keep-alive',
    'X-Accel-Buffering': 'no', // désactive le buffering nginx si en prod
    ...corsHeaders,
  });

  // Flush immédiat pour que le client reçoive les headers tout de suite
  // (sinon certains proxies attendent le premier byte)
  reply.raw.write(': ok\n\n');

  return (event, data) => {
    reply.raw.write(`event: ${event}\n`);
    reply.raw.write(`data: ${JSON.stringify(data)}\n\n`);
  };
}

function parseUserPreferences(user: {
  dietaryPreference: string | null;
  allergies: string | null;
  maxCookingTime: number | null;
  equipment: string | null;
  cuisinePreference: string | null;
  servingsDefault: number | null;
}): UserPreferences {
  let equipmentArr: string[] | null = null;
  if (user.equipment) {
    try {
      const parsed = JSON.parse(user.equipment);
      if (Array.isArray(parsed)) equipmentArr = parsed;
    } catch {
      /* ignore malformed */
    }
  }
  return {
    dietaryPreference: user.dietaryPreference,
    allergies: user.allergies,
    maxCookingTime: user.maxCookingTime,
    equipment: equipmentArr,
    cuisinePreference: user.cuisinePreference,
    servingsDefault: user.servingsDefault,
  };
}

const FREE_PLAN_LIMIT = 5;

const recipesRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => {
  await fastify.register(multipart, {
    limits: {
      fileSize: 15 * 1024 * 1024, // 15 MB
      files: 1,
    },
  });

  fastify.addHook('preHandler', fastify.authenticate);

  // --- POST /create ---
  fastify.post('/create', async (request, reply) => {
    try {
      const data = await request.file();
      if (!data) {
        return reply.code(400).send({ error: 'Fichier audio requis' });
      }

      const [user, recipeCount] = await Promise.all([
        fastify.prisma.user.findUnique({ where: { id: request.user.id } }),
        fastify.prisma.recipe.count({ where: { userId: request.user.id } }),
      ]);

      if (!user) {
        return reply.code(404).send({ error: 'Utilisateur non trouvé' });
      }

      const isFree = !user.subscription || user.subscription === 'free';
      if (isFree && recipeCount >= FREE_PLAN_LIMIT) {
        return reply.code(403).send({
          error: `Limite de ${FREE_PLAN_LIMIT} recettes atteinte pour le plan gratuit. Passez au plan premium pour en créer davantage.`,
        });
      }

      const audioResult = await fastify.saveAudioFile(data);
      const audioUrl = audioResult.url || audioResult.localPath || null;
      const audioStoragePath = audioResult.path ?? null;

      const transcription = await fastify.transcribeAudio(audioResult);

      const recipeData = await fastify.generateRecipe(
        transcription,
        undefined,
        parseUserPreferences(user)
      );

      const ingredientsString = Array.isArray(recipeData.ingredients)
        ? recipeData.ingredients.join('\n')
        : recipeData.ingredients || '';

      const stepsString = Array.isArray(recipeData.etapes)
        ? recipeData.etapes.join('\n')
        : recipeData.etapes || '';

      const recipe = await fastify.prisma.recipe.create({
        data: {
          title: recipeData.titre || 'Recette sans titre',
          ingredients: ingredientsString,
          userPrompt: transcription,
          // On stocke la version structurée riche (avec notes, accord boisson,
          // origine, etc.) plutôt que la forme aplatie.
          generatedRecipe: JSON.stringify(recipeData.structured ?? recipeData),
          imageUrl: recipeData.image_url ?? null,
          preparationTime: recipeData.temps_preparation ?? null,
          cookingTime: recipeData.temps_cuisson ?? null,
          servings: recipeData.portions ?? null,
          difficulty: recipeData.difficulte ?? null,
          steps: stepsString,
          tips: recipeData.conseils ?? null,
          audioUrl: audioStoragePath || audioUrl,
          userId: request.user.id,
        },
      });

      return {
        recipe: {
          id: recipe.id,
          title: recipe.title,
          ingredients: recipe.ingredients,
          preparationTime: recipe.preparationTime,
          cookingTime: recipe.cookingTime,
          servings: recipe.servings,
          difficulty: recipe.difficulty,
          steps: recipe.steps,
          tips: recipe.tips,
          imageUrl: recipe.imageUrl,
          audioUrl,
          createdAt: recipe.createdAt,
        },
      };
    } catch (error) {
      fastify.log.error(error);
      return reply.code(500).send({ error: 'Erreur lors de la création de la recette' });
    }
  });

  // --- GET /list ---
  fastify.get('/list', async (request, reply) => {
    try {
      const recipes = await fastify.prisma.recipe.findMany({
        where: { userId: request.user.id },
        orderBy: { createdAt: 'desc' },
        select: {
          id: true,
          title: true,
          ingredients: true,
          preparationTime: true,
          cookingTime: true,
          servings: true,
          difficulty: true,
          imageUrl: true,
          createdAt: true,
        },
      });

      return { recipes };
    } catch (error) {
      fastify.log.error(error);
      return reply.code(500).send({ error: 'Erreur lors de la récupération des recettes' });
    }
  });

  // --- GET /:id ---
  fastify.get<{ Params: { id: string } }>('/:id', async (request, reply) => {
    try {
      const recipe = await fastify.prisma.recipe.findFirst({
        where: {
          id: request.params.id,
          userId: request.user.id,
        },
      });

      if (!recipe) {
        return reply.code(404).send({ error: 'Recette non trouvée' });
      }

      let parsed: unknown = null;
      if (recipe.generatedRecipe) {
        try {
          parsed = JSON.parse(recipe.generatedRecipe);
        } catch (err) {
          fastify.log.warn(
            `generatedRecipe corrompu pour ${recipe.id}: ${(err as Error).message}`
          );
        }
      }

      return { recipe: { ...recipe, generatedRecipe: parsed } };
    } catch (error) {
      fastify.log.error(error);
      return reply.code(500).send({ error: 'Erreur lors de la récupération de la recette' });
    }
  });

  // --- DELETE /:id ---
  fastify.delete<{ Params: { id: string } }>('/:id', async (request, reply) => {
    try {
      const recipe = await fastify.prisma.recipe.findUnique({
        where: { id: request.params.id },
      });

      if (!recipe) {
        return reply.code(404).send({ error: 'Recette non trouvée' });
      }

      if (recipe.userId !== request.user.id) {
        return reply.code(403).send({ error: 'Non autorisé' });
      }

      await fastify.prisma.recipe.delete({ where: { id: request.params.id } });

      // Nettoyage best-effort du fichier audio
      if (recipe.audioUrl) {
        try {
          if (recipe.audioUrl.startsWith('http')) {
            // URL presignée — ne contient pas d'info de clé fiable, skip
          } else if (recipe.audioUrl.includes('/') && !recipe.audioUrl.startsWith('./')) {
            await deleteFile(recipe.audioUrl).catch(() => {});
          } else if (fs.existsSync(recipe.audioUrl)) {
            fs.unlinkSync(recipe.audioUrl);
          }
        } catch (cleanupErr) {
          fastify.log.warn(`Nettoyage audio échoué: ${(cleanupErr as Error).message}`);
        }
      }

      return { success: true };
    } catch (error) {
      fastify.log.error(error);
      return reply.code(500).send({ error: 'Erreur lors de la suppression de la recette' });
    }
  });

  // -------------------------------------------------------------------------
  // POST /create-stream — version streaming (SSE)
  // -------------------------------------------------------------------------
  // Flux d'événements émis :
  //   progress  { step }          — étape courante du pipeline
  //   transcription { text }      — texte transcrit
  //   title { title }             — titre extrait, image lancée en parallèle
  //   description { description } — description courte
  //   delta { content }           — delta brut (optionnel, pour debug)
  //   recipe { recipe }           — recette structurée complète
  //   image { url }               — URL finale de l'image
  //   saved { id }                — id de la recette persistée
  //   error { message }           — erreur terminale
  //   done                        — fin du flux
  //
  // La route reste un POST multipart (audio) mais la réponse est un stream SSE.
  fastify.post('/create-stream', async (request, reply) => {
    const data = await request.file();
    if (!data) {
      return reply.code(400).send({ error: 'Fichier audio requis' });
    }

    const [user, recipeCount] = await Promise.all([
      fastify.prisma.user.findUnique({ where: { id: request.user.id } }),
      fastify.prisma.recipe.count({ where: { userId: request.user.id } }),
    ]);

    if (!user) {
      return reply.code(404).send({ error: 'Utilisateur non trouvé' });
    }

    const isFree = !user.subscription || user.subscription === 'free';
    if (isFree && recipeCount >= FREE_PLAN_LIMIT) {
      return reply.code(403).send({
        error: `Limite de ${FREE_PLAN_LIMIT} recettes atteinte pour le plan gratuit.`,
      });
    }

    const preferences = parseUserPreferences(user);
    const send = setupSSE(request, reply);

    // Heartbeat toutes les 15s pour que les proxies ne ferment pas la connexion
    const heartbeat = setInterval(() => {
      reply.raw.write(`: heartbeat\n\n`);
    }, 15000);

    const cleanup = () => clearInterval(heartbeat);
    request.raw.on('close', cleanup);

    try {
      // --- 1. Sauvegarde audio ---
      send('progress', { step: 'saving_audio' });
      const audioResult = await fastify.saveAudioFile(data);
      const audioUrl = audioResult.url || audioResult.localPath || null;
      const audioStoragePath = audioResult.path ?? null;

      // --- 2. Transcription ---
      send('progress', { step: 'transcribing' });
      const transcription = await fastify.transcribeAudio(audioResult);
      send('transcription', { text: transcription });

      // --- 3. Génération recette en streaming ---
      send('progress', { step: 'generating_recipe' });

      let imagePromise: Promise<{ url: string | null }> | null = null;
      let imageStartedAt: number | null = null;
      let finalRecipe: StructuredRecipe | null = null;

      const iterator = streamRecipe(fastify.openai, fastify.log, {
        transcription,
        preferences,
        userId: user.id,
      });

      for await (const event of iterator) {
        if (event.type === 'title') {
          send('title', { title: event.title });
          // Lance la génération d'image en parallèle dès qu'on a le titre
          if (!imagePromise) {
            imageStartedAt = Date.now();
            send('progress', { step: 'generating_image' });
            imagePromise = generateRecipeImage(fastify.openai, fastify.log, {
              title: event.title,
              userId: user.id,
            });
          }
        } else if (event.type === 'description') {
          send('description', { description: event.description });
        } else if (event.type === 'delta') {
          // Envoyé seulement pour ceux qui veulent l'afficher en brut
          // On peut commenter pour réduire la bande passante
          send('delta', { content: event.content });
        } else if (event.type === 'complete') {
          finalRecipe = event.recipe;
          send('recipe', { recipe: event.recipe });
        }
      }

      if (!finalRecipe) {
        throw new Error('Recette non générée');
      }

      // --- 4. Attente image (peut être déjà prête si génération plus rapide que le stream) ---
      send('progress', { step: 'finalizing' });
      const imageResult = imagePromise
        ? await imagePromise
        : { url: null };
      const imageUrl = imageResult.url;
      if (imageStartedAt !== null) {
        fastify.log.info(
          { parallelImageMs: Date.now() - imageStartedAt },
          'parallel_image_duration'
        );
      }
      send('image', { url: imageUrl });

      // --- 5. Persistance ---
      const ingredientsString = finalRecipe.ingredients
        .map((i) => `${i.quantite} ${i.nom}${i.notes ? ` (${i.notes})` : ''}`)
        .join('\n');
      const stepsString = finalRecipe.etapes
        .sort((a, b) => a.numero - b.numero)
        .map((e) => `${e.numero}. ${e.instruction}`)
        .join('\n');
      const tipsString = [
        ...finalRecipe.conseils,
        `Inspiration : ${finalRecipe.origine_inspiration}`,
        finalRecipe.accord_boisson ? `Accord boisson : ${finalRecipe.accord_boisson}` : null,
      ]
        .filter(Boolean)
        .join('\n');

      const saved = await fastify.prisma.recipe.create({
        data: {
          title: finalRecipe.titre,
          ingredients: ingredientsString,
          userPrompt: transcription,
          generatedRecipe: JSON.stringify(finalRecipe),
          imageUrl,
          preparationTime: finalRecipe.temps_preparation,
          cookingTime: finalRecipe.temps_cuisson,
          servings: finalRecipe.portions,
          difficulty: finalRecipe.difficulte,
          steps: stepsString,
          tips: tipsString,
          audioUrl: audioStoragePath || audioUrl,
          userId: user.id,
        },
      });

      send('saved', { id: saved.id });
      send('done', {});
    } catch (err) {
      fastify.log.error(err, 'create-stream failed');
      try {
        send('error', { message: (err as Error).message });
      } catch {
        /* ignore si le flux est déjà fermé */
      }
    } finally {
      cleanup();
      reply.raw.end();
    }
    // Pas de return : on a appelé reply.hijack() donc Fastify nous laisse
    // gérer la réponse nous-mêmes.
  });
};

export default recipesRoutes;
