// ========================================
// FICHIER: ArticleStorage.js
// Description: Système de sauvegarde articles avec texte compilé uniquement
// ========================================

require('dotenv').config();
const { google } = require('googleapis');
const { logSh } = require('./ErrorReporting');

// Configuration Google Sheets
const SHEET_CONFIG = {
  sheetId: '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c'
};

/**
 * NOUVELLE FONCTION : Compiler le contenu de manière organique
 * Respecte la hiérarchie et les associations naturelles
 */
async function compileGeneratedTextsOrganic(generatedTexts, elements) {
  if (!generatedTexts || Object.keys(generatedTexts).length === 0) {
    return '';
  }
  
  logSh(`🌱 Compilation ORGANIQUE de ${Object.keys(generatedTexts).length} éléments...`, 'DEBUG');
  
  let compiledParts = [];
  
  // 1. DÉTECTER et GROUPER les sections organiques
  const organicSections = buildOrganicSections(generatedTexts, elements);
  
  // 2. COMPILER dans l'ordre naturel
  organicSections.forEach(section => {
    if (section.type === 'header_with_content') {
      // H1, H2, H3 avec leur contenu associé
      if (section.title) {
        compiledParts.push(cleanIndividualContent(section.title));
      }
      if (section.content) {
        compiledParts.push(cleanIndividualContent(section.content));
      }
    } 
    else if (section.type === 'standalone_content') {
      // Contenu sans titre associé
      compiledParts.push(cleanIndividualContent(section.content));
    }
    else if (section.type === 'faq_pair') {
      // Paire question + réponse
      if (section.question && section.answer) {
        compiledParts.push(cleanIndividualContent(section.question));
        compiledParts.push(cleanIndividualContent(section.answer));
      }
    }
  });
  
  // 3. Joindre avec espacement naturel
  const finalText = compiledParts.join('\n\n');
  
  logSh(`✅ Compilation organique terminée: ${finalText.length} caractères`, 'INFO');
  return finalText.trim();
}

/**
 * Construire les sections organiques en analysant les associations
 */
function buildOrganicSections(generatedTexts, elements) {
  const sections = [];
  const usedTags = new Set();
  
  // 🔧 FIX: Gérer le cas où elements est null/undefined
  if (!elements) {
    logSh('⚠️ Elements null, utilisation compilation simple', 'DEBUG');
    // Compilation simple : tout le contenu dans l'ordre des clés
    Object.keys(generatedTexts).forEach(tag => {
      sections.push({
        type: 'standalone_content',
        content: generatedTexts[tag],
        tag: tag
      });
    });
    return sections;
  }
  
  // 1. ANALYSER l'ordre original des éléments
  const originalOrder = elements.map(el => el.originalTag);
  
  logSh(`📋 Analyse de ${originalOrder.length} éléments dans l'ordre original...`, 'DEBUG');
  
  // 2. DÉTECTER les associations naturelles
  for (let i = 0; i < originalOrder.length; i++) {
    const currentTag = originalOrder[i];
    const currentContent = generatedTexts[currentTag];
    
    if (!currentContent || usedTags.has(currentTag)) continue;
    
    const currentType = identifyElementType(currentTag);
    
    if (currentType === 'titre_h1' || currentType === 'titre_h2' || currentType === 'titre_h3') {
      // CHERCHER le contenu associé qui suit
      const associatedContent = findAssociatedContent(originalOrder, i, generatedTexts, usedTags);
      
      sections.push({
        type: 'header_with_content',
        title: currentContent,
        content: associatedContent.content,
        titleTag: currentTag,
        contentTag: associatedContent.tag
      });
      
      usedTags.add(currentTag);
      if (associatedContent.tag) {
        usedTags.add(associatedContent.tag);
      }
      
      logSh(`  ✓ Section: ${currentType} + contenu associé`, 'DEBUG');
    }
    else if (currentType === 'faq_question') {
      // CHERCHER la réponse correspondante
      const matchingAnswer = findMatchingFAQAnswer(currentTag, generatedTexts);
      
      if (matchingAnswer) {
        sections.push({
          type: 'faq_pair',
          question: currentContent,
          answer: matchingAnswer.content,
          questionTag: currentTag,
          answerTag: matchingAnswer.tag
        });
        
        usedTags.add(currentTag);
        usedTags.add(matchingAnswer.tag);
        
        logSh(`  ✓ Paire FAQ: ${currentTag} + ${matchingAnswer.tag}`, 'DEBUG');
      }
    }
    else if (currentType !== 'faq_reponse') {
      // CONTENU STANDALONE (pas une réponse FAQ déjà traitée)
      sections.push({
        type: 'standalone_content',
        content: currentContent,
        contentTag: currentTag
      });
      
      usedTags.add(currentTag);
      logSh(`  ✓ Contenu standalone: ${currentType}`, 'DEBUG');
    }
  }
  
  logSh(`🏗️ ${sections.length} sections organiques construites`, 'INFO');
  return sections;
}

/**
 * Trouver le contenu associé à un titre (paragraphe qui suit)
 */
function findAssociatedContent(originalOrder, titleIndex, generatedTexts, usedTags) {
  // Chercher dans les éléments suivants
  for (let j = titleIndex + 1; j < originalOrder.length; j++) {
    const nextTag = originalOrder[j];
    const nextContent = generatedTexts[nextTag];
    
    if (!nextContent || usedTags.has(nextTag)) continue;
    
    const nextType = identifyElementType(nextTag);
    
    // Si on trouve un autre titre, on s'arrête
    if (nextType === 'titre_h1' || nextType === 'titre_h2' || nextType === 'titre_h3') {
      break;
    }
    
    // Si on trouve du contenu (texte, intro), c'est probablement associé
    if (nextType === 'texte' || nextType === 'intro') {
      return {
        content: nextContent,
        tag: nextTag
      };
    }
  }
  
  return { content: null, tag: null };
}

/**
 * Extraire le numéro d'une FAQ : |Faq_q_1| ou |Faq_a_2| → "1" ou "2"
 */
function extractFAQNumber(tag) {
  const match = tag.match(/(\d+)/);
  return match ? match[1] : null;
}

/**
 * Trouver la réponse FAQ correspondant à une question
 */
function findMatchingFAQAnswer(questionTag, generatedTexts) {
  // Extraire le numéro : |Faq_q_1| → 1
  const questionNumber = extractFAQNumber(questionTag);
  
  if (!questionNumber) return null;
  
  // Chercher la réponse correspondante
  for (const tag in generatedTexts) {
    const tagType = identifyElementType(tag);
    
    if (tagType === 'faq_reponse') {
      const answerNumber = extractFAQNumber(tag);
      
      if (answerNumber === questionNumber) {
        return {
          content: generatedTexts[tag],
          tag: tag
        };
      }
    }
  }
  
  return null;
}

/**
 * Nouvelle fonction de sauvegarde avec compilation organique
 */
async function saveGeneratedArticleOrganic(articleData, csvData, config = {}) {
  try {
    logSh('💾 Sauvegarde article avec compilation organique...', 'INFO');
    
    const sheets = await getSheetsClient();
    
    // 🆕 Choisir la sheet selon le flag useVersionedSheet
    const targetSheetName = config.useVersionedSheet ? 'Generated_Articles_Versioned' : 'Generated_Articles';
    let articlesSheet = await getOrCreateSheet(sheets, targetSheetName);
    
    // ===== COMPILATION ORGANIQUE =====
    const compiledText = await compileGeneratedTextsOrganic(
      articleData.generatedTexts,
      articleData.originalElements // Passer les éléments originaux si disponibles
    );
    
    logSh(`📝 Texte compilé organiquement: ${compiledText.length} caractères`, 'INFO');
    
    // Métadonnées avec format français
    const now = new Date();
    const frenchTimestamp = formatDateToFrench(now);
    
    // UTILISER le slug du CSV (colonne A du Google Sheet source)
    // Le slug doit venir de csvData.slug (récupéré via getBrainConfig)
    const slug = csvData.slug || generateSlugFromContent(csvData.mc0, csvData.t0);
    
    const metadata = {
      timestamp: frenchTimestamp,
      slug: slug,
      mc0: csvData.mc0,
      t0: csvData.t0,
      personality: csvData.personality?.nom || 'Unknown',
      antiDetectionLevel: config.antiDetectionLevel || config.adversarialMode || 'none',
      elementsCount: Object.keys(articleData.generatedTexts || {}).length,
      textLength: compiledText.length,
      wordCount: countWords(compiledText),
      llmUsed: config.llmUsed || 'openai',
      validationStatus: articleData.validationReport?.status || 'unknown',
      // 🆕 Métadonnées de versioning
      version: config.version || '1.0',
      stage: config.stage || 'final_version',
      stageDescription: config.stageDescription || 'Version finale',
      parentArticleId: config.parentArticleId || null,
      versionHistory: config.versionHistory || null
    };
    
    // Préparer la ligne de données selon le format de la sheet
    let row;

    if (config.useVersionedSheet) {
      // Format VERSIONED (21 colonnes) : avec version, stage, stageDescription, parentArticleId
      row = [
        metadata.timestamp,
        metadata.slug,
        metadata.mc0,
        metadata.t0,
        metadata.personality,
        metadata.antiDetectionLevel,
        compiledText,
        metadata.textLength,
        metadata.wordCount,
        metadata.elementsCount,
        metadata.llmUsed,
        metadata.validationStatus,
        metadata.version,           // Colonne M
        metadata.stage,             // Colonne N
        metadata.stageDescription,  // Colonne O
        metadata.parentArticleId || '', // Colonne P
        '', '', '', '',            // Colonnes Q,R,S,T : scores détecteurs (réservées)
        JSON.stringify({           // Colonne U
          csvData: { ...csvData, xmlTemplate: undefined, xmlFileName: csvData.xmlFileName },
          config: config,
          stats: metadata,
          versionHistory: metadata.versionHistory
        })
      ];
    } else {
      // Format LEGACY (17 colonnes) : sans version/stage, scores détecteurs à la place
      row = [
        metadata.timestamp,
        metadata.slug,
        metadata.mc0,
        metadata.t0,
        metadata.personality,
        metadata.antiDetectionLevel,
        compiledText,
        metadata.textLength,
        metadata.wordCount,
        metadata.elementsCount,
        metadata.llmUsed,
        metadata.validationStatus,
        '', '', '', '',            // Colonnes M,N,O,P : scores détecteurs (GPTZero, Originality, CopyLeaks, HumanQuality)
        JSON.stringify({           // Colonne Q
          csvData: { ...csvData, xmlTemplate: undefined, xmlFileName: csvData.xmlFileName },
          config: config,
          stats: metadata
        })
      ];
    }
    
    // DEBUG: Vérifier le slug généré
    logSh(`💾 Sauvegarde avec slug: "${metadata.slug}" (colonne B)`, 'DEBUG');
    
    // Ajouter la ligne aux données dans la bonne sheet
    // Forcer le range à A1 pour éviter le décalage horizontal
    const targetRange = config.useVersionedSheet ? 'Generated_Articles_Versioned!A1' : 'Generated_Articles!A1';

    logSh(`🔍 DEBUG APPEND: sheetId=${SHEET_CONFIG.sheetId}, range=${targetRange}, rowLength=${row.length}`, 'INFO');
    logSh(`🔍 DEBUG ROW PREVIEW: [${row.slice(0, 5).map(c => typeof c === 'string' ? c.substring(0, 50) : c).join(', ')}...]`, 'INFO');

    const appendResult = await sheets.spreadsheets.values.append({
      spreadsheetId: SHEET_CONFIG.sheetId,
      range: targetRange,
      valueInputOption: 'USER_ENTERED',
      insertDataOption: 'INSERT_ROWS',  // Force l'insertion d'une nouvelle ligne
      resource: {
        values: [row]
      }
    });

    logSh(`✅ APPEND SUCCESS: ${appendResult.status} - Updated ${appendResult.data.updates?.updatedCells || 0} cells`, 'INFO');
    
    // Récupérer le numéro de ligne pour l'ID article
    const targetRangeForId = config.useVersionedSheet ? 'Generated_Articles_Versioned!A:A' : 'Generated_Articles!A:A';
    const response = await sheets.spreadsheets.values.get({
      spreadsheetId: SHEET_CONFIG.sheetId,
      range: targetRangeForId
    });
    
    const articleId = response.data.values ? response.data.values.length : 1;
    
    logSh(`✅ Article organique sauvé: ID ${articleId}, ${metadata.wordCount} mots`, 'INFO');
    
    return {
      articleId: articleId,
      textLength: metadata.textLength,
      wordCount: metadata.wordCount,
      sheetRow: response.data.values ? response.data.values.length : 2
    };
    
  } catch (error) {
    logSh(`❌ Erreur sauvegarde organique: ${error.toString()}`, 'ERROR');
    throw error;
  }
}

/**
 * Générer un slug à partir du contenu MC0 et T0
 */
function generateSlugFromContent(mc0, t0) {
  if (!mc0 && !t0) return 'article-generated';
  
  const source = mc0 || t0;
  return source
    .toString()
    .toLowerCase()
    .replace(/[àáâäã]/g, 'a')
    .replace(/[èéêë]/g, 'e')
    .replace(/[ìíîï]/g, 'i')
    .replace(/[òóôöõ]/g, 'o')
    .replace(/[ùúûü]/g, 'u')
    .replace(/[ç]/g, 'c')
    .replace(/[ñ]/g, 'n')
    .replace(/[^a-z0-9\s-]/g, '') // Enlever caractères spéciaux
    .replace(/\s+/g, '-')          // Espaces -> tirets
    .replace(/-+/g, '-')           // Éviter doubles tirets
    .replace(/^-+|-+$/g, '')       // Enlever tirets début/fin
    .substring(0, 50);             // Limiter longueur
}

/**
 * Identifier le type d'élément par son tag
 */
function identifyElementType(tag) {
  const cleanTag = tag.toLowerCase().replace(/[|{}]/g, '');
  
  if (cleanTag.includes('titre_h1') || cleanTag.includes('h1')) return 'titre_h1';
  if (cleanTag.includes('titre_h2') || cleanTag.includes('h2')) return 'titre_h2';
  if (cleanTag.includes('titre_h3') || cleanTag.includes('h3')) return 'titre_h3';
  if (cleanTag.includes('intro')) return 'intro';
  if (cleanTag.includes('faq_q') || cleanTag.includes('faq_question')) return 'faq_question';
  if (cleanTag.includes('faq_a') || cleanTag.includes('faq_reponse')) return 'faq_reponse';
  
  return 'texte'; // Par défaut
}

/**
 * Nettoyer un contenu individuel
 */
function cleanIndividualContent(content) {
  if (!content) return '';
  
  let cleaned = content.toString();
  
  // 1. Supprimer les balises HTML
  cleaned = cleaned.replace(/<[^>]*>/g, '');
  
  // 2. Décoder les entités HTML
  cleaned = cleaned.replace(/&lt;/g, '<');
  cleaned = cleaned.replace(/&gt;/g, '>');
  cleaned = cleaned.replace(/&amp;/g, '&');
  cleaned = cleaned.replace(/&quot;/g, '"');
  cleaned = cleaned.replace(/&#039;/g, "'");
  cleaned = cleaned.replace(/&nbsp;/g, ' ');
  
  // 3. Nettoyer les espaces
  cleaned = cleaned.replace(/\s+/g, ' ');
  cleaned = cleaned.replace(/\n\s+/g, '\n');
  
  // 4. Supprimer les caractères de contrôle étranges
  cleaned = cleaned.replace(/[\x00-\x1F\x7F-\x9F]/g, '');
  
  return cleaned.trim();
}

/**
 * Créer la sheet de stockage avec headers appropriés
 */
async function createArticlesStorageSheet(sheets, sheetName = 'Generated_Articles') {
  logSh(`🗄️ Création sheet ${sheetName}...`, 'INFO');
  
  try {
    // Créer la nouvelle sheet
    await sheets.spreadsheets.batchUpdate({
      spreadsheetId: SHEET_CONFIG.sheetId,
      resource: {
        requests: [{
          addSheet: {
            properties: {
              title: sheetName
            }
          }
        }]
      }
    });
    
    // Headers avec versioning
    const headers = [
      'Timestamp',
      'Slug', 
      'MC0',
      'T0',
      'Personality',
      'AntiDetection_Level',
      'Compiled_Text',           // ← COLONNE PRINCIPALE
      'Text_Length',
      'Word_Count',
      'Elements_Count',
      'LLM_Used',
      'Validation_Status',
      // 🆕 Colonnes de versioning
      'Version',                 // v1.0, v1.1, v1.2, v2.0
      'Stage',                   // initial_generation, selective_enhancement, etc.
      'Stage_Description',       // Description détaillée de l'étape
      'Parent_Article_ID',       // ID de l'article parent (pour linkage)
      'GPTZero_Score',          // Scores détecteurs (à remplir)
      'Originality_Score',
      'CopyLeaks_Score', 
      'Human_Quality_Score',
      'Full_Metadata_JSON'      // Backup complet avec historique
    ];
    
    // Ajouter les headers
    await sheets.spreadsheets.values.update({
      spreadsheetId: SHEET_CONFIG.sheetId,
      range: `${sheetName}!A1:U1`,
      valueInputOption: 'USER_ENTERED',
      resource: {
        values: [headers]
      }
    });
    
    // Formatter les headers
    await sheets.spreadsheets.batchUpdate({
      spreadsheetId: SHEET_CONFIG.sheetId,
      resource: {
        requests: [{
          repeatCell: {
            range: {
              sheetId: await getSheetIdByName(sheets, sheetName),
              startRowIndex: 0,
              endRowIndex: 1,
              startColumnIndex: 0,
              endColumnIndex: headers.length
            },
            cell: {
              userEnteredFormat: {
                textFormat: {
                  bold: true
                },
                backgroundColor: {
                  red: 0.878,
                  green: 0.878,
                  blue: 0.878
                },
                horizontalAlignment: 'CENTER'
              }
            },
            fields: 'userEnteredFormat(textFormat,backgroundColor,horizontalAlignment)'
          }
        }]
      }
    });
    
    logSh(`✅ Sheet ${sheetName} créée avec succès`, 'INFO');
    return true;
    
  } catch (error) {
    logSh(`❌ Erreur création sheet: ${error.toString()}`, 'ERROR');
    throw error;
  }
}

/**
 * Formater date au format français DD/MM/YYYY HH:mm:ss
 */
function formatDateToFrench(date) {
  // Utiliser toLocaleString avec le format français
  return date.toLocaleString('fr-FR', {
    day: '2-digit',
    month: '2-digit', 
    year: 'numeric',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false,
    timeZone: 'Europe/Paris'
  }).replace(',', '');
}

/**
 * Compter les mots dans un texte
 */
function countWords(text) {
  if (!text || text.trim() === '') return 0;
  return text.trim().split(/\s+/).length;
}

/**
 * Récupérer un article sauvé par ID
 */
async function getStoredArticle(articleId) {
  try {
    const sheets = await getSheetsClient();
    
    const rowNumber = articleId + 2; // +2 car header + 0-indexing
    const response = await sheets.spreadsheets.values.get({
      spreadsheetId: SHEET_CONFIG.sheetId,
      range: `Generated_Articles!A${rowNumber}:Q${rowNumber}`
    });
    
    if (!response.data.values || response.data.values.length === 0) {
      throw new Error(`Article ${articleId} non trouvé`);
    }
    
    const data = response.data.values[0];
    
    return {
      articleId: articleId,
      timestamp: data[0],
      slug: data[1],
      mc0: data[2],
      t0: data[3],
      personality: data[4],
      antiDetectionLevel: data[5],
      compiledText: data[6],      // ← TEXTE PUR
      textLength: data[7],
      wordCount: data[8],
      elementsCount: data[9],
      llmUsed: data[10],
      validationStatus: data[11],
      gptZeroScore: data[12],
      originalityScore: data[13],
      copyLeaksScore: data[14],
      humanScore: data[15],
      fullMetadata: data[16] ? JSON.parse(data[16]) : null
    };
    
  } catch (error) {
    logSh(`❌ Erreur récupération article ${articleId}: ${error.toString()}`, 'ERROR');
    throw error;
  }
}

/**
 * Lister les derniers articles générés
 */
async function getRecentArticles(limit = 10) {
  try {
    const sheets = await getSheetsClient();
    
    const response = await sheets.spreadsheets.values.get({
      spreadsheetId: SHEET_CONFIG.sheetId,
      range: 'Generated_Articles!A:L'
    });
    
    if (!response.data.values || response.data.values.length <= 1) {
      return []; // Pas de données ou seulement headers
    }
    
    const data = response.data.values.slice(1); // Exclure headers
    const startIndex = Math.max(0, data.length - limit);
    const recentData = data.slice(startIndex);
    
    return recentData.map((row, index) => ({
      articleId: startIndex + index,
      timestamp: row[0],
      slug: row[1],
      mc0: row[2],
      personality: row[4],
      antiDetectionLevel: row[5],
      wordCount: row[8],
      validationStatus: row[11]
    })).reverse(); // Plus récents en premier
    
  } catch (error) {
    logSh(`❌ Erreur liste articles récents: ${error.toString()}`, 'ERROR');
    return [];
  }
}

/**
 * Mettre à jour les scores de détection d'un article
 */
async function updateDetectionScores(articleId, scores) {
  try {
    const sheets = await getSheetsClient();
    const rowNumber = articleId + 2;
    
    const updates = [];
    
    // Colonnes des scores : M, N, O (GPTZero, Originality, CopyLeaks)
    if (scores.gptzero !== undefined) {
      updates.push({
        range: `Generated_Articles!M${rowNumber}`,
        values: [[scores.gptzero]]
      });
    }
    if (scores.originality !== undefined) {
      updates.push({
        range: `Generated_Articles!N${rowNumber}`,
        values: [[scores.originality]]
      });
    }
    if (scores.copyleaks !== undefined) {
      updates.push({
        range: `Generated_Articles!O${rowNumber}`,
        values: [[scores.copyleaks]]
      });
    }
    
    if (updates.length > 0) {
      await sheets.spreadsheets.values.batchUpdate({
        spreadsheetId: SHEET_CONFIG.sheetId,
        resource: {
          valueInputOption: 'USER_ENTERED',
          data: updates
        }
      });
    }
    
    logSh(`✅ Scores détection mis à jour pour article ${articleId}`, 'INFO');
    
  } catch (error) {
    logSh(`❌ Erreur maj scores article ${articleId}: ${error.toString()}`, 'ERROR');
    throw error;
  }
}

// ============= HELPERS GOOGLE SHEETS =============

/**
 * Obtenir le client Google Sheets authentifié
 */
async function getSheetsClient() {
  const auth = new google.auth.GoogleAuth({
    credentials: {
      client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
      private_key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n')
    },
    scopes: ['https://www.googleapis.com/auth/spreadsheets']
  });
  
  const authClient = await auth.getClient();
  const sheets = google.sheets({ version: 'v4', auth: authClient });
  
  return sheets;
}

/**
 * Obtenir ou créer une sheet
 */
async function getOrCreateSheet(sheets, sheetName) {
  try {
    // Vérifier si la sheet existe
    const response = await sheets.spreadsheets.get({
      spreadsheetId: SHEET_CONFIG.sheetId
    });
    
    const existingSheet = response.data.sheets.find(
      sheet => sheet.properties.title === sheetName
    );
    
    if (existingSheet) {
      return existingSheet;
    } else {
      // Créer la sheet si elle n'existe pas
      if (sheetName === 'Generated_Articles' || sheetName === 'Generated_Articles_Versioned') {
        await createArticlesStorageSheet(sheets, sheetName);
        return await getOrCreateSheet(sheets, sheetName); // Récursif pour récupérer la sheet créée
      }
      throw new Error(`Sheet ${sheetName} non supportée pour création automatique`);
    }
    
  } catch (error) {
    logSh(`❌ Erreur accès/création sheet ${sheetName}: ${error.toString()}`, 'ERROR');
    throw error;
  }
}

/**
 * Obtenir l'ID d'une sheet par son nom
 */
async function getSheetIdByName(sheets, sheetName) {
  const response = await sheets.spreadsheets.get({
    spreadsheetId: SHEET_CONFIG.sheetId
  });
  
  const sheet = response.data.sheets.find(
    s => s.properties.title === sheetName
  );
  
  return sheet ? sheet.properties.sheetId : null;
}

// ============= EXPORTS =============

module.exports = {
  compileGeneratedTextsOrganic,
  buildOrganicSections,
  findAssociatedContent,
  extractFAQNumber,
  findMatchingFAQAnswer,
  saveGeneratedArticleOrganic,
  identifyElementType,
  cleanIndividualContent,
  createArticlesStorageSheet,
  formatDateToFrench,
  countWords,
  getStoredArticle,
  getRecentArticles,
  updateDetectionScores,
  getSheetsClient,
  getOrCreateSheet,
  getSheetIdByName,
  generateSlugFromContent
};