#!/usr/bin/env node
/**
 * NAV V2 — Worker enrichissement IA
 * Lancé via systemd timer toutes les 5 minutes
 * Pipeline : fetch pending → scrape crawl4ai → Mistral Nemo → update NocoDB → log stats
 */

import { spawnSync, execSync } from 'child_process';
import { existsSync, writeFileSync, unlinkSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';

// ─── CONFIG DEPUIS .env ───────────────────────────────────────────────────────
const NOCODB_URL = process.env.NOCODB_URL || 'http://localhost:8070';
const NOCODB_TOKEN = process.env.NOCODB_TOKEN;
const NOCODB_BASE = process.env.NOCODB_BASE;
const NOCODB_TABLE_ORGAS = process.env.NOCODB_TABLE_ORGAS;
const NOCODB_TABLE_STATS = process.env.NOCODB_TABLE_STATS;
const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY;
const RESEND_API_KEY = process.env.RESEND_API_KEY;
const RESEND_FROM = process.env.RESEND_FROM || 'contact@trans-former.fr';
const EMAIL_JULES = process.env.EMAIL_JULES || 'jules@trans-former.fr';
const BUDGET_MAX_EUR = parseFloat(process.env.BUDGET_MAX_EUR || '20');
const WORKER_LIMIT = parseInt(process.env.WORKER_LIMIT || '5');
const LOCK_FILE = '/tmp/nav-worker.lock';

// ─── PRIX MISTRAL NEMO (USD → EUR) ───────────────────────────────────────────
const NEMO_PRICE_IN = 0.02 / 1_000_000;   // $0.02 / 1M tokens input
const NEMO_PRICE_OUT = 0.04 / 1_000_000;  // $0.04 / 1M tokens output
const USD_TO_EUR = 0.93;

// ─── TAXONOMIE VALIDE (apostrophe typographique U+2019 comme NocoDB) ─────────
const VALID_FONCTIONS = [
  'Juridique', 'Technique', 'Économique', 'Administratif', 'Chantier',
  'Comptabilité', 'Développement', 'Formation', 'Gestion d\u2019agence', 'Santé mentale'
];

// ─── MAPPING NORMALISATION TAGS ───────────────────────────────────────────────
const TAG_MAP = [
  [['juridique', 'droit', 'litige', 'contrat', 'déontologie', 'décennale', 'médiation', 'pi ', 'propriété intellectuelle', 'ccag', 'marchés publics droit'], 'Juridique'],
  [['technique', 're2020', 'thermique', 'structure', 'bim', 'dtu', 'acoustique', 'matériaux', 'simulation', 'opr', 'réserves', 'acv', 'pcd', 'eurocodes', 'pmc'], 'Technique'],
  [['économique', 'prix', 'tarif', 'honoraire', 'devis', 'roi', 'financement', 'subvention', 'cee', 'maprimerenov', 'anah', 'business plan', 'pricing'], 'Économique'],
  [['administratif', 'permis', 'plu', 'plui', 'erp', 'autorisation travaux', 'abf', 'patrimoine', 'marchés publics procédure', 'cctp', 'dpgf', 'concours', 'urbanisme'], 'Administratif'],
  [['chantier', 'coordination chantier', 'det', 'suivi travaux', 'sps', 'sécurité chantier', 'planning chantier', 'entreprise', 'sous-traitance', 'réception travaux'], 'Chantier'],
  [['comptabilité', 'fiscal', 'tva', 'bnc', 'bic', 'expert-comptable', 'bilan', 'trésorerie', 'transmission agence', 'création agence', 'micro'], 'Comptabilité'],
  [['développement', 'prospection', 'commercial', 'client', 'réseau', 'candidature', 'consultation', 'acquisition', 'marketing', 'notoriété', 'ao '], 'Développement'],
  [['formation', 'école', 'mooc', 'organisme', 'formation continue', 'cpf', 'dpc', 'cfaa'], 'Formation'],
  [['gestion d\u2019agence', 'gestion d\'agence', 'rh', 'recrutement', 'emploi', 'salaire', 'ccn', 'convention collective', 'idcc', 'temps de travail', 'management'], 'Gestion d\u2019agence'],
  [['santé mentale', 'burn-out', 'épuisement', 'souffrance', 'bien-être', 'harcèlement', 'stress', 'psychologique', 'équilibre'], 'Santé mentale'],
];

function normalizeTag(raw) {
  const t = raw.toLowerCase().trim();
  // D'abord chercher correspondance exacte dans les valeurs valides
  const exact = VALID_FONCTIONS.find(v => v.toLowerCase() === t);
  if (exact) return exact;
  // Sinon chercher par mots-clés
  for (const [patterns, normalized] of TAG_MAP) {
    if (patterns.some(p => t.includes(p))) return normalized;
  }
  return null;
}

// ─── UTILITAIRES LOG ─────────────────────────────────────────────────────────
function log(...args) {
  const ts = new Date().toISOString();
  console.log(`[${ts}]`, ...args);
}

// ─── LOCK ANTI-OVERLAP ────────────────────────────────────────────────────────
function acquireLock() {
  if (existsSync(LOCK_FILE)) {
    const content = execSync(`cat ${LOCK_FILE}`).toString().trim();
    const pid = parseInt(content);
    try {
      execSync(`kill -0 ${pid} 2>/dev/null`);
      return false; // Process encore vivant
    } catch {
      log('Lock orphelin détecté, suppression');
      unlinkSync(LOCK_FILE);
    }
  }
  writeFileSync(LOCK_FILE, process.pid.toString());
  return true;
}

function releaseLock() {
  try { unlinkSync(LOCK_FILE); } catch {}
}

// ─── NOCODB API ──────────────────────────────────────────────────────────────
async function nocodbGet(path) {
  const res = await fetch(`${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE}/${path}`, {
    headers: { 'xc-token': NOCODB_TOKEN }
  });
  if (!res.ok) throw new Error(`NocoDB GET ${path} → ${res.status}: ${await res.text()}`);
  return res.json();
}

async function nocodbPatch(tableId, rowId, data) {
  const res = await fetch(`${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE}/${tableId}/${rowId}`, {
    method: 'PATCH',
    headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
  if (!res.ok) throw new Error(`NocoDB PATCH ${tableId}/${rowId} → ${res.status}: ${await res.text()}`);
  return res.json();
}

async function nocodbPost(tableId, data) {
  const res = await fetch(`${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE}/${tableId}`, {
    method: 'POST',
    headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
  if (!res.ok) throw new Error(`NocoDB POST ${tableId} → ${res.status}: ${await res.text()}`);
  return res.json();
}

// ─── BUDGET CIRCUIT BREAKER ──────────────────────────────────────────────────
async function getBudgetMoisCourant() {
  const now = new Date();
  const year = now.getFullYear();
  const month = now.getMonth(); // 0-indexed
  // NocoDB ne supporte pas bien le filtre datetime — on récupère tout et filtre en JS
  try {
    const data = await nocodbGet(`${NOCODB_TABLE_STATS}?limit=1000&sort=-timestamp`);
    const total = (data.list || []).reduce((sum, row) => {
      const ts = new Date(row.timestamp || row.CreatedAt || 0);
      if (ts.getFullYear() === year && ts.getMonth() === month) {
        return sum + (parseFloat(row.cout_eur) || 0);
      }
      return sum;
    }, 0);
    return total;
  } catch (e) {
    log('Erreur lecture budget:', e.message);
    return 0;
  }
}

async function logUsage(usage, model, endpoint, orgaId) {
  const tokensIn = usage?.prompt_tokens || 0;
  const tokensOut = usage?.completion_tokens || 0;
  const coutEur = ((tokensIn * NEMO_PRICE_IN) + (tokensOut * NEMO_PRICE_OUT)) * USD_TO_EUR;

  await nocodbPost(NOCODB_TABLE_STATS, {
    model,
    endpoint,
    tokens_in: tokensIn,
    tokens_out: tokensOut,
    cout_eur: parseFloat(coutEur.toFixed(6)),
    timestamp: new Date().toISOString(),
    orga_id: orgaId || null
  });

  log(`Usage log: ${tokensIn}in + ${tokensOut}out = €${coutEur.toFixed(6)} (${model})`);
  return coutEur;
}

// ─── FETCH FICHES PENDING ────────────────────────────────────────────────────
async function fetchPendingRows() {
  const data = await nocodbGet(
    `${NOCODB_TABLE_ORGAS}?where=(moderation_status,eq,pending)~and(ai_processed,eq,false)&limit=${WORKER_LIMIT}&sort=submitted_at`
  );
  return data.list || [];
}

// ─── SCRAPING CRAWL4AI (mode HTTP statique, sans Playwright) ─────────────────
async function scrapeWithCrawl4ai(url) {
  log(`Scraping: ${url}`);

  // Script Python temporaire pour crawl4ai
  const urlSafe = url.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
  const script = `
import asyncio, sys
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
from crawl4ai.async_crawler_strategy import AsyncHTTPCrawlerStrategy

async def scrape():
    strategy = AsyncHTTPCrawlerStrategy()
    run_cfg = CrawlerRunConfig(
        word_count_threshold=20,
        excluded_tags=['nav', 'footer', 'script', 'style', 'head'],
        remove_overlay_elements=True
    )
    async with AsyncWebCrawler(crawler_strategy=strategy, verbose=False) as crawler:
        result = await crawler.arun(url='${urlSafe}', config=run_cfg)
        if result.success and result.markdown:
            content = result.markdown[:16000]
            sys.stdout.buffer.write(content.encode('utf-8'))
        else:
            sys.stderr.write(f"Scrape failed: success={result.success}\\n")
            sys.exit(1)

asyncio.run(scrape())
`;

  const scriptPath = join(tmpdir(), `nav-scrape-${Date.now()}.py`);
  writeFileSync(scriptPath, script, 'utf-8');

  try {
    const result = spawnSync('python3', [scriptPath], {
      timeout: 180_000,
      maxBuffer: 20 * 1024 * 1024,
      env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1', PYTHONIOENCODING: 'utf-8' }
    });

    if (result.error) throw result.error;

    const stdout = result.stdout?.toString('utf-8') || '';
    const stderr = result.stderr?.toString('utf-8') || '';

    if (result.status === 0 && stdout.length > 50) {
      log(`Scrape OK: ${stdout.length} chars`);
      return stdout.trim();
    } else {
      throw new Error(`Scrape failed (code ${result.status}): ${stderr.slice(0, 300)}`);
    }
  } finally {
    try { unlinkSync(scriptPath); } catch {}
  }
}

// ─── APPEL MISTRAL NEMO ───────────────────────────────────────────────────────
const SYSTEM_PROMPT = `Tu es un assistant spécialisé dans l'écosystème professionnel de l'architecture en France. Tu reçois des informations sur une organisation ou ressource liée au secteur de l'architecture, et tu dois les enrichir pour alimenter une cartographie collaborative.

RÈGLES ABSOLUES :
1. Tu ne dois JAMAIS inventer d'informations non présentes dans les sources fournies.
2. Si une information est absente ou incertaine, retourne \`null\` pour ce champ.
3. Tu dois retourner UNIQUEMENT un objet JSON valide, sans texte avant ou après.
4. La description_enrichie doit être neutre, factuelle, en français, sans jugement de valeur.
5. Les points_cles sont des phrases courtes (max 12 mots chacune), actionnables pour un architecte.
6. Pour les tags_fonction, ne propose que des valeurs parmi la liste autorisée.

TAXONOMIE AUTORISÉE :
- Échelle (une seule valeur) : "National" | "Régional" | "Départemental" | "Local"
- Territoire (une seule valeur) : "Métropole" | "Guadeloupe" | "Martinique" | "Guyane" | "Réunion" | "Mayotte" | null
- Tags fonction (1 à 5 valeurs) : "Juridique" | "Technique" | "Économique" | "Administratif" | "Chantier" | "Comptabilité" | "Développement" | "Formation" | "Gestion d\u2019agence" | "Santé mentale"

FORMAT DE SORTIE JSON :
{
  "description_enrichie": "string (max 300 chars, français, neutre, factuel)",
  "points_cles": ["string", "string", "string"],
  "tags_fonction": ["Valeur1", "Valeur2"],
  "echelle": "National" | "Régional" | "Départemental" | "Local" | null,
  "territoire": "Métropole" | ... | null,
  "localisation_ville": "string" | null,
  "confiance": "haute" | "moyenne" | "faible"
}

Le champ "confiance" reflète ta certitude globale sur l'enrichissement :
- "haute" : URL scrapée avec contenu riche, informations claires
- "moyenne" : URL scrapée mais contenu partiel, ou description_user seule suffisante
- "faible" : URL non disponible et description_user vague, inférences importantes`;

function buildUserPrompt(row, scrapeContent) {
  return `ORGANISATION À ENRICHIR :

Nom : ${row.nom}
URL : ${row.url || 'non fournie'}
Description soumise par l'utilisateur : ${row.description_user || row.description || 'non fournie'}

CONTENU DU SITE WEB (extrait par scraping) :
${scrapeContent || 'Site non accessible ou URL non fournie.'}

---

Enrichis cette fiche selon les règles du system prompt. Retourne uniquement le JSON.`;
}

async function callMistralWithRetry(row, scrapeContent, maxRetries = 2) {
  const userPrompt = buildUserPrompt(row, scrapeContent);

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    if (attempt > 0) {
      log(`Retry ${attempt}/${maxRetries} pour fiche ${row.Id}`);
      await new Promise(r => setTimeout(r, 2000 * attempt));
    }

    try {
      const res = await fetch('https://api.mistral.ai/v1/chat/completions', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${MISTRAL_API_KEY}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          model: 'open-mistral-nemo',
          temperature: 0.2,
          max_tokens: 800,
          response_format: { type: 'json_object' },
          messages: [
            { role: 'system', content: SYSTEM_PROMPT },
            { role: 'user', content: userPrompt }
          ]
        }),
        signal: AbortSignal.timeout(60_000)
      });

      if (!res.ok) {
        const err = await res.text();
        throw new Error(`Mistral API ${res.status}: ${err}`);
      }

      const data = await res.json();
      const content = data.choices?.[0]?.message?.content;
      if (!content) throw new Error('Réponse Mistral vide');

      const parsed = JSON.parse(content);
      // Attacher usage pour logging
      parsed._usage = data.usage;
      parsed._raw = content;
      return parsed;

    } catch (e) {
      log(`Erreur Mistral (tentative ${attempt + 1}): ${e.message}`);
      if (attempt === maxRetries) return null;
    }
  }
  return null;
}

// ─── EMAIL JULES VIA RESEND ───────────────────────────────────────────────────
async function sendEmailJules(subject, body) {
  if (!RESEND_API_KEY) {
    log('RESEND_API_KEY absent, email skippé');
    return;
  }
  try {
    const res = await fetch('https://api.resend.com/emails', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${RESEND_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        from: RESEND_FROM,
        to: [EMAIL_JULES],
        subject,
        text: body
      })
    });
    if (!res.ok) log('Email error:', await res.text());
    else log('Email envoyé à Jules:', subject);
  } catch (e) {
    log('Email exception:', e.message);
  }
}

// ─── VÉRIFICATION SEUIL 5 FICHES PENDING MODÉRATION ─────────────────────────
async function checkModerationQueue() {
  const data = await nocodbGet(
    `${NOCODB_TABLE_ORGAS}?where=(moderation_status,eq,ai_processed)&limit=100`
  );
  const count = data.pageInfo?.totalRows || 0;
  if (count >= 5) {
    log(`Seuil modération atteint: ${count} fiches en attente`);
    await sendEmailJules(
      `NAV — ${count} fiches à modérer`,
      `Bonjour Jules,\n\n${count} fiches ont été enrichies par l'IA et attendent ta validation dans NocoDB.\n\nLien NocoDB : http://localhost:8070\nFiltre : moderation_status = ai_processed\n\nBonne modération !`
    );
  }
}

// ─── MAIN ─────────────────────────────────────────────────────────────────────
async function run() {
  if (!acquireLock()) {
    log('Worker déjà en cours, skip');
    process.exit(0);
  }

  const startTime = Date.now();
  log('=== Worker NAV enrichissement démarré ===');

  try {
    // Vérification clés obligatoires
    if (!NOCODB_TOKEN || !NOCODB_BASE || !NOCODB_TABLE_ORGAS || !MISTRAL_API_KEY) {
      throw new Error('Variables .env manquantes (NOCODB_TOKEN, NOCODB_BASE, NOCODB_TABLE_ORGAS, MISTRAL_API_KEY)');
    }

    // Check budget global
    const budgetMois = await getBudgetMoisCourant();
    log(`Budget mois courant: €${budgetMois.toFixed(4)} / €${BUDGET_MAX_EUR}`);

    if (budgetMois >= BUDGET_MAX_EUR) {
      log('Budget épuisé pour ce mois. Worker en pause.');
      await sendEmailJules(
        'NAV — Budget IA épuisé ce mois',
        `Le budget IA de ${BUDGET_MAX_EUR}€ a été atteint. Le worker est en pause jusqu'au 1er du mois prochain.\n\nConsommation actuelle : €${budgetMois.toFixed(4)}`
      );
      return;
    }

    // Fetch fiches pending
    const rows = await fetchPendingRows();
    log(`${rows.length} fiche(s) à traiter`);

    if (rows.length === 0) {
      log('Rien à traiter.');
      return;
    }

    let processedCount = 0;

    for (const row of rows) {
      const rowStart = Date.now();
      log(`--- Traitement fiche ${row.Id}: ${row.nom} ---`);

      // Re-check budget avant chaque fiche
      const budgetCheck = await getBudgetMoisCourant();
      if (budgetCheck >= BUDGET_MAX_EUR) {
        log('Budget atteint mid-pipeline, arrêt.');
        break;
      }

      // Scraping
      let scrapeContent = null;
      const hasUrl = row.url && row.url.trim().length > 0;
      const shouldScrape = hasUrl && (row.scrape_status === 'pending' || !row.scrape_status);

      if (shouldScrape) {
        try {
          scrapeContent = await scrapeWithCrawl4ai(row.url);
          await nocodbPatch(NOCODB_TABLE_ORGAS, row.Id, {
            scrape_status: 'scraped',
            scrape_content: scrapeContent
          });
        } catch (e) {
          log(`Scrape échoué: ${e.message}`);
          await nocodbPatch(NOCODB_TABLE_ORGAS, row.Id, { scrape_status: 'failed' });
          // Continue avec l'IA sans contenu scrape
        }
      } else if (!hasUrl) {
        await nocodbPatch(NOCODB_TABLE_ORGAS, row.Id, { scrape_status: 'no_link' });
      }

      // Appel Mistral Nemo
      const enriched = await callMistralWithRetry(row, scrapeContent);

      if (!enriched) {
        log(`Échec Mistral sur fiche ${row.Id}, flag ai_error`);
        await nocodbPatch(NOCODB_TABLE_ORGAS, row.Id, {
          moderation_status: 'ai_error',
          ai_processed: true
        });
        continue;
      }

      // Normalisation tags
      const rawTags = enriched.tags_fonction || [];
      const normalizedTags = [...new Set(rawTags.map(normalizeTag).filter(Boolean))];

      // Update NocoDB
      const updateData = {
        description_enrichie: enriched.description_enrichie || null,
        points_cles: enriched.points_cles ? JSON.stringify(enriched.points_cles) : null,
        tags_fonction: normalizedTags.join(','),
        moderation_status: 'ai_processed',
        ai_processed: true,
        ai_raw_output: JSON.stringify({ output: enriched, confiance: enriched.confiance })
      };

      // Conserver echelle/territoire/localisation si l'IA les a enrichis
      if (enriched.echelle && !row.echelle) updateData.echelle = enriched.echelle;
      if (enriched.territoire && !row.territoire) updateData.territoire = enriched.territoire;
      if (enriched.localisation_ville && !row.localisation_ville) {
        updateData.localisation_ville = enriched.localisation_ville;
      }

      await nocodbPatch(NOCODB_TABLE_ORGAS, row.Id, updateData);

      // Log usage tokens
      await logUsage(enriched._usage, 'open-mistral-nemo', 'enrichissement', row.Id);

      const elapsed = ((Date.now() - rowStart) / 1000).toFixed(1);
      log(`Fiche ${row.Id} traitée en ${elapsed}s — confiance: ${enriched.confiance || 'nc'}`);
      processedCount++;
    }

    log(`=== Run terminé: ${processedCount}/${rows.length} fiches traitées en ${((Date.now() - startTime) / 1000).toFixed(1)}s ===`);

    // Check seuil modération
    await checkModerationQueue();

  } catch (e) {
    log('ERREUR WORKER:', e.message);
    console.error(e.stack);
    process.exit(1);
  } finally {
    releaseLock();
  }
}

run();
