import type { OpenAI } from 'openai';
import type { FastifyBaseLogger } from 'fastify';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { Readable } from 'node:stream';
import { uploadFile, getFileUrl } from '../utils/storage';
import { buildImagePrompt } from './prompts';
import { computeImageCost, type CallLog } from './cost';

const ENABLE_IMAGE_GENERATION = process.env.ENABLE_IMAGE_GENERATION !== 'false';
const PRIMARY_MODEL = process.env.OPENAI_IMAGE_MODEL || 'gpt-image-1';
const FALLBACK_MODEL = process.env.OPENAI_IMAGE_FALLBACK_MODEL || 'dall-e-3';
const IMAGE_QUALITY = process.env.OPENAI_IMAGE_QUALITY || 'medium';
const IMAGE_SIZE = process.env.OPENAI_IMAGE_SIZE || '1024x1024';
const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL || 'http://localhost:3000';

export interface GenerateImageOptions {
  title: string;
  description?: string;
  userId?: string;
}

export interface GenerateImageResult {
  url: string | null;
  log: CallLog | null;
}

/**
 * Slugifie un titre en préservant les caractères latins étendus.
 * "Côte de bœuf" → "cote-de-boeuf"
 */
function slugify(input: string): string {
  return input
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[œŒ]/g, 'oe')
    .replace(/[æÆ]/g, 'ae')
    .replace(/[^a-zA-Z0-9]+/g, '-')
    .replace(/^-|-$/g, '')
    .toLowerCase()
    .slice(0, 80);
}

/**
 * Génère une image pour la recette. Best-effort : retourne `null` au lieu
 * de jeter en cas d'échec, pour ne pas casser la création de recette.
 */
export async function generateRecipeImage(
  openai: OpenAI,
  logger: FastifyBaseLogger,
  options: GenerateImageOptions
): Promise<GenerateImageResult> {
  if (!ENABLE_IMAGE_GENERATION) {
    return { url: null, log: null };
  }

  const start = Date.now();
  const prompt = buildImagePrompt(options.title, options.description);

  // 1. Tenter le modèle principal, puis le fallback
  let imageBuffer: Buffer | null = null;
  let modelUsed = PRIMARY_MODEL;

  try {
    imageBuffer = await callImageModel(openai, PRIMARY_MODEL, prompt);
  } catch (err) {
    logger.warn(
      `Génération image (${PRIMARY_MODEL}) échouée: ${(err as Error).message}`
    );

    if (FALLBACK_MODEL && FALLBACK_MODEL !== PRIMARY_MODEL) {
      logger.info(`Tentative avec le modèle de fallback: ${FALLBACK_MODEL}`);
      try {
        imageBuffer = await callImageModel(openai, FALLBACK_MODEL, prompt);
        modelUsed = FALLBACK_MODEL;
      } catch (fallbackErr) {
        logger.warn(
          `Génération image (${FALLBACK_MODEL}) échouée aussi: ${(fallbackErr as Error).message}`
        );
        return { url: null, log: null };
      }
    } else {
      return { url: null, log: null };
    }
  }

  if (!imageBuffer) return { url: null, log: null };

  // 2. Stocker : MinIO en priorité, fallback local (servi via /uploads)
  const fileName = `${slugify(options.title)}-${Date.now()}.png`;
  let publicUrl: string | null = null;

  try {
    const filePath = await uploadFile(
      { filename: fileName, file: Readable.from(imageBuffer) },
      'recipes'
    );
    publicUrl = await getFileUrl(filePath);
  } catch (storageErr) {
    logger.warn(
      `Upload MinIO image échoué, fallback local: ${(storageErr as Error).message}`
    );

    try {
      const recipesDir = path.join('./uploads', 'recipes');
      if (!fs.existsSync(recipesDir)) {
        fs.mkdirSync(recipesDir, { recursive: true });
      }
      const localPath = path.join(recipesDir, fileName);
      fs.writeFileSync(localPath, imageBuffer);
      // Le serveur expose ./uploads via une route statique /uploads
      publicUrl = `${PUBLIC_BASE_URL}/uploads/recipes/${fileName}`;
    } catch (localErr) {
      logger.warn(
        `Sauvegarde locale image échouée: ${(localErr as Error).message}`
      );
      publicUrl = null;
    }
  }

  const cost = computeImageCost(modelUsed, IMAGE_SIZE, IMAGE_QUALITY);
  const log: CallLog = {
    userId: options.userId,
    operation: 'image_generation',
    model: modelUsed,
    durationMs: Date.now() - start,
    costUsd: cost,
  };

  logger.info(log, 'openai_image_generated');
  return { url: publicUrl, log };
}

/**
 * Appelle l'API Images d'OpenAI pour un modèle donné et retourne le buffer
 * de l'image générée. Unifie le traitement des deux modèles (gpt-image-1
 * renvoie du base64, dall-e-3 renvoie une URL).
 */
async function callImageModel(
  openai: OpenAI,
  model: string,
  prompt: string
): Promise<Buffer> {
  const isGptImage = model === 'gpt-image-1';

  // dall-e-3 n'accepte pas 'low'/'medium'/'high' — on mappe vers 'standard'
  const qualityForDallE =
    IMAGE_QUALITY === 'hd' || IMAGE_QUALITY === 'standard'
      ? IMAGE_QUALITY
      : 'standard';

  const params = {
    model,
    prompt,
    n: 1,
    size: IMAGE_SIZE,
    ...(isGptImage
      ? { quality: IMAGE_QUALITY }
      : { quality: qualityForDallE, response_format: 'b64_json' as const }),
  };

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const response = await openai.images.generate(params as any);

  const first = response.data?.[0];
  if (!first) throw new Error('Réponse images vide');

  if (first.b64_json) {
    return Buffer.from(first.b64_json, 'base64');
  }
  if (first.url) {
    const fetched = await fetch(first.url);
    if (!fetched.ok) throw new Error(`Téléchargement image: ${fetched.statusText}`);
    return Buffer.from(await fetched.arrayBuffer());
  }
  throw new Error('Ni base64 ni URL dans la réponse images');
}
