import drive from '@adonisjs/drive/services/main'
import env from '#start/env'
import type { OcrProvider, OcrResult } from '#services/ocr/ocr_provider'

const MISTRAL_API = 'https://api.mistral.ai/v1'
// Modèle OCR dédié de Mistral — extrait le texte structuré d'un doc.
const OCR_MODEL = 'mistral-ocr-latest'
// Modèle chat pour la 2e étape (markdown → JSON typé via json_schema strict).
const EXTRACTION_MODEL = 'mistral-large-latest'

const MIME_BY_EXT: Record<string, string> = {
  pdf: 'application/pdf',
  png: 'image/png',
  jpg: 'image/jpeg',
  jpeg: 'image/jpeg',
}

const SYSTEM_PROMPT = `Tu es un extracteur de factures françaises B2B.
Tu reçois le markdown d'une facture (issu d'une OCR) et tu retournes un
JSON strict avec les champs demandés.

Règles :
- amountTtcCents : montant TTC en centimes (entier). Pas le HT.
- issueDate / dueDate : ISO 8601 datetime UTC à 09:00 (ex. "2026-04-15T09:00:00.000Z").
- clientEmail : null si absent ou illisible (pas d'invention).
- numero : tel qu'imprimé sur la facture.
- Si un champ est ambigu, mets une confiance basse (0.3–0.6).`

/**
 * Provider OCR Mistral. Pipeline en 2 étapes :
 *  1. POST /v1/ocr avec le PDF en data URI base64 → markdown structuré
 *  2. POST /v1/chat/completions avec le markdown + json_schema strict →
 *     extraction typée des champs
 *
 * Nécessite un PDF réel (storageKey non null). Pour le dev sans PDF,
 * utiliser OCR_PROVIDER=mock.
 */
export class MistralOcrProvider implements OcrProvider {
  private apiKey: string

  constructor() {
    const key = env.get('MISTRAL_API_KEY', '')
    if (!key) {
      throw new Error(
        'MISTRAL_API_KEY manquante. Posez la dans .env ou bascule OCR_PROVIDER=mock.'
      )
    }
    this.apiKey = key
  }

  async extract(input: {
    storageKey: string | null
    filename: string
  }): Promise<OcrResult> {
    if (!input.storageKey) {
      throw new Error(
        `MistralOcrProvider exige un PDF stocké (storageKey). Filename "${input.filename}" reçu sans storageKey — utiliser OCR_PROVIDER=mock pour les uploads sans fichier réel.`
      )
    }

    // 1. Télécharge le fichier depuis Drive (MinIO en dev) puis encode en base64.
    const buffer = await this.downloadAsBuffer(input.storageKey)
    const mimeType = this.mimeTypeFromFilename(input.filename)
    const dataUri = `data:${mimeType};base64,${buffer.toString('base64')}`

    // 2. OCR → markdown
    const ocrJson = await this.postJson('/ocr', {
      model: OCR_MODEL,
      document: this.documentPayload(dataUri, mimeType),
    })
    const markdown = (ocrJson?.pages ?? [])
      .map((p: { markdown?: string }) => p.markdown ?? '')
      .join('\n\n')
      .trim()

    if (!markdown) {
      throw new Error("Mistral OCR n'a retourné aucun texte exploitable")
    }

    // 3. Extraction structurée via chat avec json_schema strict.
    const extracted = await this.extractFields(markdown)

    return {
      fields: {
        clientName: { value: extracted.clientName, confidence: extracted._conf.clientName },
        clientEmail: { value: extracted.clientEmail, confidence: extracted._conf.clientEmail },
        numero: { value: extracted.numero, confidence: extracted._conf.numero },
        amountTtcCents: {
          value: extracted.amountTtcCents,
          confidence: extracted._conf.amountTtcCents,
        },
        issueDate: { value: extracted.issueDate, confidence: extracted._conf.issueDate },
        dueDate: { value: extracted.dueDate, confidence: extracted._conf.dueDate },
      },
      rawProviderResponse: { ocr: ocrJson, extracted },
    }
  }

  private async downloadAsBuffer(storageKey: string): Promise<Buffer> {
    const arr = await drive.use().getArrayBuffer(storageKey)
    return Buffer.from(arr)
  }

  private mimeTypeFromFilename(filename: string): string {
    const ext = filename.split('.').pop()?.toLowerCase() ?? ''
    const mimeType = MIME_BY_EXT[ext]
    if (!mimeType) {
      throw new Error(`Format OCR non supporté pour "${filename}"`)
    }
    return mimeType
  }

  private documentPayload(
    dataUri: string,
    mimeType: string
  ):
    | { type: 'document_url'; document_url: string }
    | { type: 'image_url'; image_url: string } {
    if (mimeType === 'application/pdf') {
      return { type: 'document_url', document_url: dataUri }
    }
    return { type: 'image_url', image_url: dataUri }
  }

  private async postJson(path: string, body: unknown): Promise<any> {
    const res = await fetch(`${MISTRAL_API}${path}`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    })
    if (!res.ok) {
      const text = await res.text()
      throw new Error(`Mistral ${path} → HTTP ${res.status}: ${text}`)
    }
    return res.json()
  }

  private async extractFields(markdown: string): Promise<{
    clientName: string
    clientEmail: string | null
    numero: string
    amountTtcCents: number
    issueDate: string
    dueDate: string
    _conf: Record<string, number>
  }> {
    const json = await this.postJson('/chat/completions', {
      model: EXTRACTION_MODEL,
      messages: [
        { role: 'system', content: SYSTEM_PROMPT },
        { role: 'user', content: markdown },
      ],
      response_format: {
        type: 'json_schema',
        json_schema: {
          name: 'invoice_fields',
          strict: true,
          schema: {
            type: 'object',
            additionalProperties: false,
            properties: {
              clientName: { type: 'string' },
              clientEmail: { type: ['string', 'null'] },
              numero: { type: 'string' },
              amountTtcCents: { type: 'integer' },
              issueDate: { type: 'string' },
              dueDate: { type: 'string' },
              _conf: {
                type: 'object',
                additionalProperties: false,
                properties: {
                  clientName: { type: 'number' },
                  clientEmail: { type: 'number' },
                  numero: { type: 'number' },
                  amountTtcCents: { type: 'number' },
                  issueDate: { type: 'number' },
                  dueDate: { type: 'number' },
                },
                required: [
                  'clientName',
                  'clientEmail',
                  'numero',
                  'amountTtcCents',
                  'issueDate',
                  'dueDate',
                ],
              },
            },
            required: [
              'clientName',
              'clientEmail',
              'numero',
              'amountTtcCents',
              'issueDate',
              'dueDate',
              '_conf',
            ],
          },
        },
      },
      temperature: 0,
    })

    const content = json?.choices?.[0]?.message?.content
    if (typeof content !== 'string') {
      throw new Error('Mistral chat: pas de content string dans la réponse')
    }
    return JSON.parse(content)
  }
}
