#!/usr/bin/env node
// Tier 2: pregunta a Gemma 4 vision (LM Studio) sobre una screenshot.
//
// Uso:
//   node ask-vision.js <ruta/screenshot.png> "<prompt en español>"
//   cat report.json | node ask-vision.js --auto out/feadulta/latest
//     → para cada result con severity != OK, pregunta a la vision y añade
//       el resultado a out/feadulta/latest/vision.json + vision.md
//
// Env vars opcionales:
//   LMSTUDIO_URL   default http://172.19.128.1:1234/v1
//   LMSTUDIO_MODEL default google/gemma-4-e4b
//   MAX_TOKENS     default 800
//   TEMPERATURE    default 0.2

import fs from 'node:fs';
import path from 'node:path';

const LMSTUDIO_URL   = process.env.LMSTUDIO_URL   || 'http://172.19.128.1:1234/v1';
const LMSTUDIO_MODEL = process.env.LMSTUDIO_MODEL || 'google/gemma-4-e4b';
const MAX_TOKENS     = Number(process.env.MAX_TOKENS || 800);
const TEMPERATURE    = Number(process.env.TEMPERATURE || 0.2);

const DEFAULT_PROMPT =
  'Eres un revisor visual de páginas web. Describe brevemente en español qué ves en la captura: ' +
  'estructura general (cabecera, contenido, pie), si hay imágenes rotas (placeholders, iconos de imagen rota, espacios vacíos donde debería haber contenido), ' +
  'si el texto se ve bien (no solapado, no recortado), y cualquier elemento claramente fuera de sitio. ' +
  'Sé conciso (máximo 6 frases). Empieza la respuesta con una de estas etiquetas: [OK], [WARN] o [FAIL].';

async function ask(screenshotPath, prompt = DEFAULT_PROMPT) {
  if (!fs.existsSync(screenshotPath)) throw new Error(`No existe ${screenshotPath}`);
  const buf = fs.readFileSync(screenshotPath);
  const b64 = buf.toString('base64');

  const body = {
    model: LMSTUDIO_MODEL,
    messages: [{
      role: 'user',
      content: [
        { type: 'text', text: prompt },
        { type: 'image_url', image_url: { url: `data:image/png;base64,${b64}` } },
      ],
    }],
    max_tokens: MAX_TOKENS,
    temperature: TEMPERATURE,
  };

  const t0 = Date.now();
  const resp = await fetch(`${LMSTUDIO_URL}/chat/completions`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  if (!resp.ok) throw new Error(`LM Studio ${resp.status}: ${await resp.text()}`);
  const out = await resp.json();
  const dt = Date.now() - t0;

  const raw  = out.choices?.[0]?.message?.content ?? '';
  const text = extractFinal(raw);
  return {
    screenshot: screenshotPath,
    text,
    raw,
    model: LMSTUDIO_MODEL,
    elapsedMs: dt,
    usage: out.usage,
  };
}

function extractFinal(s) {
  // Gemma 4 a veces escribe razonamiento antes de la respuesta. Buscamos marcadores.
  const m = s.match(/Final Answer[^:]*:\s*([\s\S]*)$/i);
  if (m) return m[1].trim();
  // O coge desde la primera etiqueta de severidad
  const sev = s.match(/\[(?:OK|WARN|FAIL)\][\s\S]*$/);
  if (sev) return sev[0].trim();
  return s.trim();
}

async function modeAuto(reportDir) {
  const jsonPath = path.join(reportDir, 'report.json');
  const report = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
  const sevs = report.results.filter(r => sevTag(r) !== 'OK');
  console.log(`[vision] ${sevs.length}/${report.results.length} candidatos`);
  const results = [];
  for (const r of sevs) {
    if (!r.screenshot) continue;
    const shot = path.join(reportDir, r.screenshot);
    console.log(`  → ${r.slug}`);
    try {
      const v = await ask(shot);
      results.push({ slug: r.slug, severity: sevTag(r), ...v });
    } catch (e) {
      results.push({ slug: r.slug, severity: sevTag(r), error: String(e?.message ?? e) });
    }
  }
  fs.writeFileSync(path.join(reportDir, 'vision.json'), JSON.stringify(results, null, 2));
  const md = ['# Vision check', ''];
  for (const v of results) {
    md.push(`## ${v.slug} — ${v.severity}`);
    md.push('');
    md.push(v.text ?? `_error: ${v.error}_`);
    md.push('');
  }
  fs.writeFileSync(path.join(reportDir, 'vision.md'), md.join('\n'));
  console.log(`[vision] vision.md escrito en ${reportDir}`);
}

function sevTag(r) {
  if (r.error) return 'FAIL';
  if (r.httpStatus && r.httpStatus >= 400) return 'FAIL';
  if (r.brokenImages?.length > 0) return 'WARN';
  if (r.failedRequests?.some(f => f.resourceType === 'image' || f.resourceType === 'media' || f.resourceType === 'document')) return 'WARN';
  if (r.consoleErrors?.length > 0) return 'WARN';
  return 'OK';
}

const args = process.argv.slice(2);
if (args[0] === '--auto') {
  await modeAuto(args[1]);
} else if (args.length >= 1) {
  const [shot, prompt] = args;
  const v = await ask(shot, prompt);
  console.log(`[${v.elapsedMs} ms · ${v.usage?.total_tokens ?? '?'} tk]\n`);
  console.log(v.text);
} else {
  console.error('Uso:');
  console.error('  node ask-vision.js <screenshot.png> "<prompt>"');
  console.error('  node ask-vision.js --auto <report_dir>');
  process.exit(2);
}
