import fp from 'fastify-plugin';
import { OpenAI } from 'openai';
import * as fs from 'node:fs';
import { pipeline } from 'node:stream/promises';
import type { FastifyInstance } from 'fastify';
import type { MultipartFile } from '@fastify/multipart';
import { uploadFile, getFileUrl } from '../utils/storage';
import type { AudioInput, AudioSaveResult, RecipeData } from '../types/fastify';
import {
  transcribeAudio as runTranscribe,
  downloadToTemp,
} from '../ai/transcriber';
import { generateRecipe as runGenerateRecipe, type StructuredRecipe } from '../ai/recipe-generator';
import type { UserPreferences } from '../ai/prompts';
import { generateRecipeImage as runGenerateImage } from '../ai/image-generator';

export default fp(async function aiPlugin(fastify: FastifyInstance) {
  // Client OpenAI partagé, avec retries et timeout configurés
  const openai = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY!,
    maxRetries: Number(process.env.OPENAI_MAX_RETRIES ?? 3),
    timeout: Number(process.env.OPENAI_TIMEOUT_MS ?? 60_000),
  });
  fastify.decorate('openai', openai);

  // --- Transcription audio ---
  fastify.decorate('transcribeAudio', async (audioInput: AudioInput): Promise<string> => {
    let tempFile: { path: string; cleanup: () => void } | null = null;
    let audioPath: string;

    if (typeof audioInput === 'string') {
      audioPath = audioInput;
    } else if (audioInput && 'url' in audioInput && audioInput.url) {
      tempFile = await downloadToTemp(audioInput.url, '.mp3');
      audioPath = tempFile.path;
    } else if (audioInput && 'localPath' in audioInput && audioInput.localPath) {
      audioPath = audioInput.localPath;
    } else {
      throw new Error("Format d'entrée audio non valide");
    }

    try {
      const { text } = await runTranscribe(openai, fastify.log, { audioPath });
      return text;
    } finally {
      tempFile?.cleanup();
    }
  });

  // --- Génération de recette ---
  // Cette fonction est maintenue pour compatibilité avec le contrat existant
  // (elle retourne `RecipeData` à plat). En interne, elle utilise désormais
  // le pipeline structuré + parallélisation image/texte.
  fastify.decorate(
    'generateRecipe',
    async (
      ingredients: string,
      hint?: string,
      preferences?: unknown
    ): Promise<RecipeData> => {
      // 1. Génération du texte (séquentiel obligatoire : on a besoin du titre)
      const { recipe } = await runGenerateRecipe(openai, fastify.log, {
        transcription: ingredients,
        hint,
        preferences: preferences as UserPreferences | null | undefined,
      });

      // 2. Génération de l'image en parallèle de la sérialisation
      // (l'image n'a besoin que du titre + description, qu'on a déjà)
      const imagePromise = runGenerateImage(openai, fastify.log, {
        title: recipe.titre,
        description: recipe.description,
      });

      // 3. Aplatir la recette structurée vers RecipeData (compatibilité Prisma)
      const flat = flattenRecipe(recipe);

      // 4. Attendre l'image (best-effort, peut être null)
      const { url: imageUrl } = await imagePromise;
      flat.image_url = imageUrl;

      return flat;
    }
  );

  // --- Sauvegarde fichier audio ---
  fastify.decorate('saveAudioFile', async (file: MultipartFile): Promise<AudioSaveResult> => {
    if (!file || !file.filename) {
      throw new Error('Fichier audio invalide');
    }

    const fileName = `${Date.now()}-${file.filename}`;

    // Tentative MinIO
    try {
      const filePath = await uploadFile({ filename: fileName, file: file.file }, 'audio');
      const url = await getFileUrl(filePath);
      return { success: true, url, path: filePath };
    } catch (err) {
      fastify.log.warn(`Upload MinIO échoué, fallback local: ${(err as Error).message}`);
    }

    // Fallback local
    const uploadDir = './uploads';
    if (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir, { recursive: true });
    }
    const filepath = `${uploadDir}/${fileName}`;

    if (file.file && typeof file.file.pipe === 'function') {
      await pipeline(file.file, fs.createWriteStream(filepath));
    } else {
      throw new Error('Format de fichier non pris en charge');
    }

    return { success: true, localPath: filepath, isLocal: true };
  });
});

/**
 * Convertit la recette structurée en `RecipeData` plat compatible avec
 * l'ancien contrat utilisé par les routes Prisma.
 */
function flattenRecipe(recipe: StructuredRecipe): RecipeData {
  const ingredients = recipe.ingredients.map((i) => {
    const note = i.notes ? ` (${i.notes})` : '';
    const tag = i.complement ? ' [+]' : '';
    return `${i.quantite} ${i.nom}${note}${tag}`;
  });

  const etapes = recipe.etapes
    .sort((a, b) => a.numero - b.numero)
    .map((e) => {
      const duree = e.duree_minutes ? ` (${e.duree_minutes} min)` : '';
      return `${e.numero}. ${e.instruction}${duree}`;
    });

  const conseils = [
    ...recipe.conseils,
    `Inspiration : ${recipe.origine_inspiration}`,
    recipe.accord_boisson ? `Accord boisson : ${recipe.accord_boisson}` : null,
  ]
    .filter(Boolean)
    .join('\n');

  return {
    titre: recipe.titre,
    ingredients,
    etapes,
    temps_preparation: recipe.temps_preparation,
    temps_cuisson: recipe.temps_cuisson,
    portions: recipe.portions,
    difficulte: recipe.difficulte,
    conseils,
    image_url: null, // rempli plus tard
    structured: recipe,
  };
}
