import type { OpenAI } from 'openai';
import type { FastifyBaseLogger } from 'fastify';
import {
  SYSTEM_PROMPT,
  RECIPE_JSON_SCHEMA,
  buildUserPrompt,
  type UserPreferences,
} from './prompts';
import { computeTextCost, type CallLog } from './cost';

export interface RecipeIngredient {
  nom: string;
  quantite: string;
  notes: string | null;
  complement: boolean;
}

export interface RecipeStep {
  numero: number;
  instruction: string;
  duree_minutes: number | null;
}

export interface StructuredRecipe {
  titre: string;
  description: string;
  origine_inspiration: string;
  ingredients: RecipeIngredient[];
  etapes: RecipeStep[];
  temps_preparation: number;
  temps_cuisson: number;
  portions: number;
  difficulte: 'facile' | 'moyen' | 'difficile';
  conseils: string[];
  accord_boisson: string | null;
}

export interface GenerateRecipeOptions {
  transcription: string;
  hint?: string;
  userId?: string;
  preferences?: UserPreferences | null;
}

export interface GenerateRecipeResult {
  recipe: StructuredRecipe;
  log: CallLog;
}

const DEFAULT_MODEL = process.env.OPENAI_TEXT_MODEL || 'gpt-4o-mini';

export async function generateRecipe(
  openai: OpenAI,
  logger: FastifyBaseLogger,
  options: GenerateRecipeOptions
): Promise<GenerateRecipeResult> {
  const start = Date.now();
  const model = DEFAULT_MODEL;

  const completion = await openai.chat.completions.create({
    model,
    messages: [
      { role: 'system', content: SYSTEM_PROMPT },
      {
        role: 'user',
        content: buildUserPrompt({
          transcription: options.transcription,
          hint: options.hint,
          preferences: options.preferences,
        }),
      },
    ],
    response_format: {
      type: 'json_schema',
      json_schema: RECIPE_JSON_SCHEMA,
    },
    temperature: 0.85,
    top_p: 0.95,
    max_tokens: 2000,
  });

  const choice = completion.choices[0];
  if (!choice) throw new Error('Réponse OpenAI vide');

  // Avec strict mode + json_schema, refusal possible si le contenu viole les
  // règles de sécurité OpenAI
  if (choice.message.refusal) {
    throw new Error(`Recette refusée par OpenAI : ${choice.message.refusal}`);
  }

  const raw = choice.message.content;
  if (!raw) throw new Error('Contenu de la réponse vide');

  // Strict mode garantit le format, mais on parse défensivement
  let recipe: StructuredRecipe;
  try {
    recipe = JSON.parse(raw) as StructuredRecipe;
  } catch (err) {
    throw new Error(
      `Parse JSON impossible malgré strict mode : ${(err as Error).message}`
    );
  }

  // Validation post-parse minimale (le schema garantit déjà la structure)
  if (!recipe.titre || !recipe.ingredients?.length || !recipe.etapes?.length) {
    throw new Error('Recette générée incomplète');
  }

  const usage = completion.usage;
  const cachedTokens = usage?.prompt_tokens_details?.cached_tokens ?? 0;
  const cost = computeTextCost(model, {
    promptTokens: usage?.prompt_tokens,
    cachedTokens,
    completionTokens: usage?.completion_tokens,
  });

  const log: CallLog = {
    userId: options.userId,
    operation: 'recipe_generation',
    model,
    durationMs: Date.now() - start,
    costUsd: cost,
    usage: {
      promptTokens: usage?.prompt_tokens,
      cachedTokens,
      completionTokens: usage?.completion_tokens,
    },
    cacheHit: cachedTokens > 0,
  };

  logger.info(log, 'openai_recipe_generated');

  return { recipe, log };
}

// ---------------------------------------------------------------------------
// Streaming version
// ---------------------------------------------------------------------------

export type RecipeStreamEvent =
  | { type: 'delta'; content: string }
  | { type: 'title'; title: string }
  | { type: 'description'; description: string }
  | { type: 'complete'; recipe: StructuredRecipe; log: CallLog };

/**
 * Streame la génération de recette token par token.
 *
 * Émet des événements typés au fil de la génération :
 * - `delta` : chaque morceau de texte brut venu d'OpenAI (pour affichage
 *   ou debug ; le frontend peut l'ignorer)
 * - `title` : le titre dès qu'il est extractible (permet de lancer la
 *   génération d'image en parallèle)
 * - `description` : la description courte dès qu'elle est complète
 * - `complete` : la recette parsée au complet + métadonnées de coût
 */
export async function* streamRecipe(
  openai: OpenAI,
  logger: FastifyBaseLogger,
  options: GenerateRecipeOptions
): AsyncGenerator<RecipeStreamEvent, void, void> {
  const start = Date.now();
  const model = DEFAULT_MODEL;

  const stream = await openai.chat.completions.create({
    model,
    messages: [
      { role: 'system', content: SYSTEM_PROMPT },
      {
        role: 'user',
        content: buildUserPrompt({
          transcription: options.transcription,
          hint: options.hint,
          preferences: options.preferences,
        }),
      },
    ],
    response_format: {
      type: 'json_schema',
      json_schema: RECIPE_JSON_SCHEMA,
    },
    temperature: 0.85,
    top_p: 0.95,
    max_tokens: 2000,
    stream: true,
    // Demander l'usage dans la réponse streamée (important : sans ça les
    // tokens.usage est null à la fin du stream)
    stream_options: { include_usage: true },
  });

  let buffer = '';
  let titleEmitted = false;
  let descriptionEmitted = false;
  let finalUsage: { prompt_tokens?: number; completion_tokens?: number; prompt_tokens_details?: { cached_tokens?: number } } | null = null;

  for await (const chunk of stream) {
    const delta = chunk.choices[0]?.delta?.content ?? '';

    // Le dernier chunk contient l'usage mais pas de delta
    if (chunk.usage) {
      finalUsage = chunk.usage;
    }

    if (!delta) continue;

    buffer += delta;
    yield { type: 'delta', content: delta };

    // Détection précoce du titre via regex sur le buffer
    if (!titleEmitted) {
      const m = buffer.match(/"titre"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/);
      if (m) {
        titleEmitted = true;
        yield { type: 'title', title: m[1] };
      }
    }

    // Détection de la description (vient typiquement après le titre)
    if (titleEmitted && !descriptionEmitted) {
      const m = buffer.match(/"description"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/);
      if (m) {
        descriptionEmitted = true;
        yield { type: 'description', description: m[1] };
      }
    }
  }

  // Parsing final de la recette complète
  let recipe: StructuredRecipe;
  try {
    recipe = JSON.parse(buffer) as StructuredRecipe;
  } catch (err) {
    throw new Error(
      `Parse JSON du stream impossible : ${(err as Error).message}`
    );
  }

  if (!recipe.titre || !recipe.ingredients?.length || !recipe.etapes?.length) {
    throw new Error('Recette générée incomplète');
  }

  const cachedTokens = finalUsage?.prompt_tokens_details?.cached_tokens ?? 0;
  const cost = computeTextCost(model, {
    promptTokens: finalUsage?.prompt_tokens,
    cachedTokens,
    completionTokens: finalUsage?.completion_tokens,
  });

  const log: CallLog = {
    userId: options.userId,
    operation: 'recipe_generation',
    model,
    durationMs: Date.now() - start,
    costUsd: cost,
    usage: {
      promptTokens: finalUsage?.prompt_tokens,
      cachedTokens,
      completionTokens: finalUsage?.completion_tokens,
    },
    cacheHit: cachedTokens > 0,
  };

  logger.info(log, 'openai_recipe_streamed');

  yield { type: 'complete', recipe, log };
}
