/**
 * Traduction automatique du français vers l'anglais.
 *
 * Le service est le point de traduction gratuit de Google, sans clef ni
 * contrat : il tolère quelques appels, puis répond 429 et ferme la porte. La
 * synchronisation LinkedIn en demandait deux par publication, 558 pour les
 * 279 publications de la page, toutes les nuits et pour les mêmes textes.
 * Le quota tombait dès la vingtième publication, et les 500 échecs suivants
 * remplissaient le journal d'autant de piles d'appels.
 *
 * Trois mesures, dans cet ordre d'importance :
 *
 * 1. un cache sur disque, indexé par le texte source : une publication déjà
 *    traduite ne l'est jamais deux fois, et la synchronisation nocturne ne
 *    demande plus que les nouveautés ;
 * 2. une reprise avec attente croissante sur 429, le service acceptant de
 *    nouveau après quelques secondes ;
 * 3. un coupe-circuit : après plusieurs refus consécutifs, la traduction est
 *    abandonnée pour le reste de l'exécution. Insister ne fait que prolonger
 *    la sanction, et le site sait déjà se rabattre sur le français.
 */
import fs from 'fs/promises'
import path from 'path'
import crypto from 'crypto'

export interface TranslationResult {
  translatedText: string
  success: boolean
  error?: string
}

const CACHE_FILE = path.join(process.cwd(), 'data', 'translations-cache.json')

/** Nombre de refus consécutifs au-delà duquel on renonce pour cette exécution. */
const REFUS_TOLERES = 3

/** Attentes successives avant de réessayer un appel refusé, en millisecondes. */
const ATTENTES = [1_000, 3_000, 8_000]

let cache: Record<string, string> | null = null
let cacheModifie = false
let refusConsecutifs = 0
let abandonne = false

const empreinte = (texte: string) =>
  crypto.createHash('sha1').update(texte).digest('hex').slice(0, 16)

async function chargerCache(): Promise<Record<string, string>> {
  if (cache) return cache
  try {
    cache = JSON.parse(await fs.readFile(CACHE_FILE, 'utf-8'))
  } catch {
    cache = {}
  }
  return cache!
}

/**
 * Écrit le cache sur disque. À appeler une fois la série de traductions
 * terminée : écrire à chaque traduction multiplierait les accès disque pour
 * un fichier qui ne sert qu'à la fin.
 */
export async function enregistrerCacheTraductions(): Promise<void> {
  if (!cache || !cacheModifie) return
  await fs.mkdir(path.dirname(CACHE_FILE), { recursive: true })
  await fs.writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), 'utf-8')
  cacheModifie = false
}

const patienter = (ms: number) => new Promise((r) => setTimeout(r, ms))

/** Un appel au service, sans reprise : renvoie null si le service refuse. */
async function appeler(texte: string): Promise<string | null> {
  const url =
    'https://translate.googleapis.com/translate_a/single?' +
    new URLSearchParams({ client: 'gtx', sl: 'fr', tl: 'en', dt: 't', q: texte })

  const reponse = await fetch(url)

  if (reponse.status === 429) return null
  if (!reponse.ok) throw new Error(`Service de traduction : HTTP ${reponse.status}`)

  const data = await reponse.json()
  if (data && data[0] && Array.isArray(data[0])) {
    return data[0].map((item: unknown[]) => item[0]).filter(Boolean).join('')
  }
  throw new Error('Réponse de traduction inattendue')
}

export async function translateToEnglish(text: string): Promise<TranslationResult> {
  if (!text || text.trim().length === 0) {
    return { translatedText: text, success: true }
  }

  const memoire = await chargerCache()
  const cle = empreinte(text)
  if (memoire[cle] !== undefined) {
    return { translatedText: memoire[cle], success: true }
  }

  // Le coupe-circuit vaut pour toute l'exécution : une fois le quota atteint,
  // les appels suivants seraient refusés de la même façon.
  if (abandonne) {
    return { translatedText: text, success: false, error: 'quota de traduction atteint' }
  }

  for (let essai = 0; essai <= ATTENTES.length; essai++) {
    try {
      const traduit = await appeler(text)

      if (traduit !== null) {
        memoire[cle] = traduit
        cacheModifie = true
        refusConsecutifs = 0
        return { translatedText: traduit, success: true }
      }

      // Refus : on patiente avant de réessayer, sauf au dernier tour.
      if (essai < ATTENTES.length) await patienter(ATTENTES[essai])
    } catch (erreur) {
      // Une panne franche ne se répare pas en réessayant : on rend l'original.
      return {
        translatedText: text,
        success: false,
        error: erreur instanceof Error ? erreur.message : 'erreur inconnue',
      }
    }
  }

  refusConsecutifs += 1
  if (refusConsecutifs >= REFUS_TOLERES && !abandonne) {
    abandonne = true
    console.warn(
      `[Traduction] Quota atteint après ${refusConsecutifs} refus consécutifs. ` +
        'Les textes restants gardent leur version française ; ' +
        'les traductions déjà obtenues sont conservées en cache.'
    )
  }

  return { translatedText: text, success: false, error: 'quota de traduction atteint' }
}

/**
 * Traduit le titre et le contenu d'une publication.
 *
 * Le titre est souvent la première ligne du contenu : le cache absorbe alors
 * le second appel sans rien demander au service.
 */
export async function translateLinkedInPost(
  title: string,
  content: string
): Promise<{ titleEn: string; contentEn: string; success: boolean }> {
  const titre = await translateToEnglish(title)
  const corps = await translateToEnglish(content)

  return {
    titleEn: titre.translatedText,
    contentEn: corps.translatedText,
    success: titre.success && corps.success,
  }
}

/** Traduit une série de textes, en s'appuyant sur le cache et le coupe-circuit. */
export async function translateBatch(
  texts: string[],
  delayMs: number = 100
): Promise<TranslationResult[]> {
  const resultats: TranslationResult[] = []

  for (let i = 0; i < texts.length; i++) {
    resultats.push(await translateToEnglish(texts[i]))
    // Inutile d'espacer les appels une fois le quota atteint : plus rien ne
    // part vers le service.
    if (!abandonne && i < texts.length - 1) await patienter(delayMs)
  }

  await enregistrerCacheTraductions()
  return resultats
}
