"use server";

import { z } from "genkit";
import { ai } from "./genkit.config";

const ExtractDataFromInvoiceInputSchema = z.object({
  imageParts: z
    .array(
      z.object({
        data: z.string().describe("Datos de la imagen en base64"),
        mimeType: z.string().describe("Tipo MIME de la imagen"),
      })
    )
    .describe("Array de partes de imagen con datos base64 y mimeType"),
  customFields: z
    .string()
    .optional()
    .describe("Campos personalizados a extraer, separados por comas"),
});

export type ExtractDataFromInvoiceInput = z.infer<
  typeof ExtractDataFromInvoiceInputSchema
>;

const ExtractDataFromInvoiceOutputSchema = z.object({
  extractedData: z.any().describe("Datos extraídos en formato JSON dinámico"),
});

export type ExtractDataFromInvoiceOutput = z.infer<
  typeof ExtractDataFromInvoiceOutputSchema
>;

export async function extractDataFromInvoice(
  input: ExtractDataFromInvoiceInput
): Promise<ExtractDataFromInvoiceOutput> {
  return extractDataFromInvoiceFlow(input);
}

const extractDataFromInvoiceFlow = ai.defineFlow(
  {
    name: "extractDataFromInvoiceFlow",
    inputSchema: ExtractDataFromInvoiceInputSchema,
    outputSchema: ExtractDataFromInvoiceOutputSchema,
  },
  async (input) => {
    const maxAttempts = 3;
    let lastError: Error | null = null;

    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        const fieldsToExtract =
          input.customFields?.trim();

        const prompt = `
Eres un especialista experto en extracción de datos OCR multilingüe. Tu tarea es extraer ÚNICAMENTE la información solicitada, incluso si el documento está en otro idioma o formato.

### OBJETIVO
Extrae SOLO los campos especificados en la siguiente lista y NINGÚN OTRO:
${fieldsToExtract}

No devuelvas texto adicional, explicaciones ni campos no solicitados.

---

### INSTRUCCIONES
1. El documento puede estar en cualquier idioma. Traduce los encabezados o sinónimos si es necesario para encontrar los campos solicitados (por ejemplo, "Invoice No", "Factura N°", "Fattura" → invoice_no).
2. Para cada campo solicitado:
   - Si el campo es **simple** (texto, número, fecha), devuelve su valor exacto como aparece en el documento (sin adornos ni explicaciones).
   - Si el campo es **una tabla o lista** (por ejemplo: ITEMS, SUMMARY), devuelve un **array de objetos JSON válidos**, donde cada objeto tenga claves claras y valores de texto o numéricos válidos.
3. Si un campo no existe en el documento, devuélvelo con valor **null**.
4. Las claves deben estar en **snake_case**.
5. NO incluyas claves adicionales como "_original", "_normalized", "_confidence", "_bbox", etc.
6. No inventes datos. Si no se encuentra información, deja el campo en null o vacío.
7. Responde **EXCLUSIVAMENTE con un objeto JSON válido**, sin texto adicional ni formato markdown.

---

### EJEMPLO DE FORMATO DE RESPUESTA

{
  "invoice_no": "51109338",
  "date_of_issue": "2013-04-13",
  "seller": "Andrews, Kirby and Valdez",
  "client": "Becker Ltd",
  "items": [
    { "description": "Table", "quantity": 2, "unit_price": 120.00, "total": 240.00 },
    { "description": "Chair", "quantity": 4, "unit_price": 60.00, "total": 240.00 }
  ],
  "summary": {
    "subtotal": 480.00,
    "tax": 86.40,
    "total": 566.40
  }
}

---

Recuerda: devuelve **solo los campos de ${fieldsToExtract}** y nada más.
`;

        const mediaParts = input.imageParts.map((part) => ({
          media: {
            url: `data:${part.mimeType};base64,${part.data}`,
            contentType: part.mimeType,
          },
        }));

        const llmResponse = await ai.generate({
          prompt: [{ text: prompt }, ...mediaParts],
          output: {
            format: "json",
          },
        });

        const output = llmResponse.text;

        if (!output) {
          throw new Error("No se pudo generar la respuesta del modelo");
        }

        let extractedData;
        try {
          extractedData = typeof output === "string" ? JSON.parse(output) : output;
        } catch (e) {
          console.error(e)
          throw new Error("La respuesta del modelo no es un JSON válido");
        }

        const hasData = Object.values(extractedData).some(
          (value) => value !== null && value !== ""
        );

        if (!hasData) {
          throw new Error(
            "El documento no parece contener ninguno de los campos solicitados."
          );
        }
        
        // Si todo es exitoso, retornamos el resultado y salimos del bucle.
        return { extractedData };

      } catch (error) {
        lastError = error as Error;
        console.error(`Intento ${attempt} de ${maxAttempts} falló:`, lastError.message);

        if (attempt === maxAttempts) {
          break; // Salimos del bucle si este fue el último intento
        }

        // Esperamos antes del siguiente reintento
        const delay = Math.pow(2, attempt - 1) * 1000; // 1s, 2s
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }

    // Si todos los intentos fallaron, lanzamos el último error capturado.
    throw new Error(`Todos los ${maxAttempts} intentos fallaron. Último error: ${lastError?.message}`);
  }
);