/* ============================================================
   Mock data — realistic Spanish newsroom backend
   Exports onto window for sibling Babel scripts.
   ============================================================ */

const SOURCES = [
  { name: 'EFE',           weight: 1.55 },
  { name: 'BBC Mundo',     weight: 1.38 },
  { name: 'DW',            weight: 1.25 },
  { name: 'Guardian',      weight: 1.20 },
  { name: 'Marca',         weight: 1.20 },
  { name: 'AS',            weight: 1.08 },
  { name: 'El País',       weight: 0.85 },
  { name: 'Infobae',       weight: 0.75 },
  { name: 'news.google.com', weight: 0.60 },
];

const CATEGORIES = ['política', 'economía', 'deportes', 'sociedad', 'tecnología', 'cultura'];

const TOPICS = ['actores_geopolíticos', 'economía_global', 'política_global', 'conflicto_armado', 'sociedad', 'tecnología', 'deportes', 'cultura'];

/* A curated set of realistic Spanish headlines, paired with a one-line deck,
   a category, plausible source and a writeup snippet for the article body. */
const STORY_BANK = [
  {
    titulo: 'El Banco Central Europeo recorta los tipos de interés al 2,5% y apunta a una pausa en julio',
    deck: 'Christine Lagarde subraya que la inflación subyacente sigue por encima del objetivo del 2%',
    fuente: 'EFE', cat: 'economía', topic: 'economía_global',
    body: [
      'El Consejo de Gobierno del Banco Central Europeo decidió este jueves rebajar los tipos de interés oficiales en 25 puntos básicos, situando la facilidad de depósito en el 2,5%. La decisión, anunciada en Fráncfort tras una reunión de seis horas, mantiene el ritmo de relajación monetaria iniciado en junio de 2024.',
      'En la rueda de prensa posterior, la presidenta Christine Lagarde defendió que la trayectoria desinflacionaria "está bien encaminada", aunque admitió que la inflación subyacente —del 2,7% en abril— continúa por encima del objetivo. Lagarde anticipó que la institución podría hacer una pausa en julio para evaluar el impacto acumulado de los recortes.',
      'Los mercados reaccionaron con caídas moderadas en los rendimientos de la deuda soberana. El bono español a diez años cedió tres puntos básicos, hasta el 3,18%, y el euro retrocedió un 0,4% frente al dólar.',
      'Varios economistas consultados por esta agencia coinciden en que la decisión era esperada. "La inflación de servicios sigue siendo el principal motivo de preocupación, pero el deterioro del mercado laboral justificaba la rebaja", afirmó Silvia Dall\'Angelo, economista jefa de Federated Hermes para Europa.',
    ],
    h2: 'Divergencia con la Reserva Federal',
    body2: [
      'La decisión profundiza la divergencia con la Reserva Federal estadounidense, que mantuvo los tipos en mayo y descartó nuevos recortes a corto plazo. Esta brecha de política monetaria está presionando al euro, que acumula una depreciación del 4% frente al dólar en lo que va de año.',
      'El próximo encuentro del BCE está previsto para el 18 de julio, cuando se actualizarán las proyecciones macroeconómicas trimestrales.',
    ],
    tags: ['BCE', 'tipos de interés', 'Lagarde', 'eurozona', 'inflación'],
  },
  {
    titulo: 'El Real Madrid se proclama campeón de la Champions League tras vencer al Inter por 3-1 en Múnich',
    deck: 'Vinícius firmó un doblete y Bellingham selló el triunfo en el minuto 88 ante 75.000 espectadores',
    fuente: 'Marca', cat: 'deportes', topic: 'deportes',
    body: [
      'El Real Madrid conquistó este sábado su decimosexta Liga de Campeones tras imponerse al Inter de Milán por 3-1 en el Allianz Arena de Múnich. El equipo de Carlo Ancelotti, que partía como ligero favorito, dominó la segunda mitad ante un Inter que se mostró sólido en los primeros 45 minutos.',
      'Vinícius Júnior abrió el marcador en el minuto 52 tras una jugada individual por la banda izquierda, y duplicó la ventaja desde el punto de penalti en el 71. Marcus Thuram acortó distancias para los italianos en el 79 con un cabezazo, pero Jude Bellingham sentenció el partido a dos minutos del final con un disparo desde la frontera.',
      'Es la sexta Champions del club blanco en los últimos doce años. Toni Kroos, que disputó su último partido antes de retirarse, fue ovacionado al ser sustituido en el minuto 84.',
    ],
    h2: 'La consolidación de una era',
    body2: [
      'Con este título, Ancelotti se convierte en el primer entrenador en ganar cinco Champions, ampliando su récord. El Madrid suma además su cuarto título europeo en cinco años, un dato sin precedentes en la historia moderna de la competición.',
      'La final del próximo año se disputará en Wembley el 30 de mayo de 2027.',
    ],
    tags: ['Real Madrid', 'Champions League', 'Vinícius', 'Bellingham', 'Ancelotti'],
  },
  {
    titulo: 'OpenAI presenta GPT-6 con razonamiento extendido y reduce a la mitad el coste por consulta',
    deck: 'La nueva versión llegará a usuarios Plus la próxima semana y a la API en versión beta el 20 de mayo',
    fuente: 'BBC Mundo', cat: 'tecnología', topic: 'tecnología',
    body: [
      'OpenAI anunció este lunes el lanzamiento de GPT-6, su nuevo modelo de lenguaje, que combina capacidades de razonamiento extendido con una reducción del 50% en el coste por consulta respecto a su predecesor. El modelo estará disponible para suscriptores de ChatGPT Plus a partir del próximo martes.',
      'Sam Altman, consejero delegado de la compañía, defendió que GPT-6 representa "una mejora cualitativa, no sólo cuantitativa". Según las métricas presentadas, el modelo supera el 92% en el benchmark MMLU, frente al 87% de GPT-5, y reduce las alucinaciones en un 40%.',
      'La empresa también introdujo una nueva modalidad llamada "agente persistente", que permite delegar tareas multi-paso de varios días con supervisión humana opcional. La función incluye memoria episódica y trazabilidad completa de cada decisión tomada por el modelo.',
    ],
    h2: 'Competencia y regulación',
    body2: [
      'El anuncio se produce días después de que Anthropic presentara Claude 5 Opus y Google liberara Gemini 3 Ultra. Las tres empresas se disputan un mercado que, según Gartner, alcanzará los 1,3 billones de dólares en 2030.',
      'En paralelo, la Comisión Europea evalúa si los modelos de frontera deben someterse a la categoría de "riesgo sistémico" de la Ley de IA, lo que obligaría a OpenAI a notificar incidentes graves en un plazo de 72 horas.',
    ],
    tags: ['OpenAI', 'GPT-6', 'inteligencia artificial', 'Altman', 'Ley IA'],
  },
  {
    titulo: 'Alemania aprueba un paquete de 100.000 millones para infraestructura y energía verde',
    deck: 'El plan, pactado entre socialdemócratas y verdes, financiará trenes, redes eléctricas y vivienda',
    fuente: 'DW', cat: 'política', topic: 'política_global',
    body: [
      'El Bundestag aprobó este miércoles, con 412 votos a favor y 198 en contra, un paquete de inversión de 100.000 millones de euros destinado a modernizar la infraestructura ferroviaria, ampliar la red eléctrica y construir vivienda asequible. El plan se ejecutará a lo largo de los próximos diez años.',
      'El canciller Friedrich Merz defendió el plan como "la mayor apuesta por la modernización desde la reunificación". Los recursos provendrán de un fondo especial autorizado por una reforma constitucional aprobada el pasado marzo, que permitió eludir el llamado freno a la deuda.',
      'El bloque conservador-socialdemócrata-verde superó las objeciones de la oposición liberal y de AfD, que calificó la medida de "irresponsable hipoteca para las próximas generaciones".',
    ],
    h2: 'Reparto de fondos',
    body2: [
      'Del total, 42.000 millones se destinarán a Deutsche Bahn para la renovación de la red, 28.000 a redes de transmisión eléctrica, 18.000 a vivienda social y 12.000 a digitalización de la administración. Los 100 primeros proyectos se licitarán antes de fin de año.',
      'El ministro de Finanzas, Lars Klingbeil, aseguró que la deuda adicional no comprometerá el rating AAA de Alemania, citando dictámenes del Bundesbank.',
    ],
    tags: ['Alemania', 'Merz', 'infraestructura', 'Bundestag', 'energía'],
  },
  {
    titulo: 'La OMS declara el fin de la emergencia sanitaria por el brote de cólera en África Oriental',
    deck: 'Once meses después del primer caso, los contagios cayeron un 94% gracias a una campaña masiva de vacunación',
    fuente: 'Guardian', cat: 'sociedad', topic: 'sociedad',
    body: [
      'La Organización Mundial de la Salud anunció este viernes el fin oficial de la emergencia sanitaria internacional declarada en junio de 2025 por el brote de cólera en África Oriental, que afectó a Kenia, Tanzania, Etiopía y Sudán del Sur y causó la muerte de al menos 14.200 personas.',
      'El director general, Tedros Adhanom Ghebreyesus, atribuyó el éxito de la contención a una campaña de vacunación oral que alcanzó a 41 millones de personas, así como al despliegue de equipos de respuesta rápida en 67 distritos sanitarios. Los nuevos casos semanales han caído de 38.000 en el pico de octubre a menos de 2.300 en abril.',
      'No obstante, la organización advirtió que la vigilancia epidemiológica debe mantenerse al menos seis meses más. "El cólera está controlado, pero no erradicado", subrayó la doctora Matshidiso Moeti, directora regional para África.',
    ],
    h2: 'Lecciones para el futuro',
    body2: [
      'La OMS publicó junto al anuncio un informe técnico de 78 páginas en el que identifica el saneamiento deficiente, los desplazamientos por conflictos y los efectos del cambio climático como los principales factores que prolongaron el brote. El documento recomienda destinar al menos 1.200 millones anuales a infraestructura de agua potable en la región.',
    ],
    tags: ['OMS', 'cólera', 'África', 'vacunación', 'Tedros'],
  },
  {
    titulo: 'La presidenta de México anuncia una reforma fiscal que elevará el impuesto a las grandes fortunas',
    deck: 'La iniciativa busca recaudar 240.000 millones de pesos anuales y financiar el sistema sanitario',
    fuente: 'Infobae', cat: 'política', topic: 'actores_geopolíticos',
    body: [
      'La presidenta Claudia Sheinbaum presentó este martes en el Senado una propuesta de reforma fiscal que introduce un impuesto progresivo a patrimonios superiores a 50 millones de pesos. La iniciativa, que llega tras seis meses de negociación con el sector empresarial, prevé recaudar 240.000 millones de pesos anuales a partir de 2027.',
      'El proyecto contempla tipos del 1%, 2% y 3% según los tramos del patrimonio, con exenciones para vivienda habitual y activos productivos en pequeñas y medianas empresas. La recaudación se destinará íntegramente al Instituto Mexicano del Seguro Social y al programa universal de medicamentos gratuitos.',
      'La presidenta defendió la medida como "un acto de justicia fiscal" y aseguró que afectará a unas 18.000 familias, el 0,02% de los hogares mexicanos. La Confederación Patronal advirtió, sin embargo, que la reforma podría acelerar la fuga de capitales hacia paraísos fiscales.',
    ],
    h2: 'Tramitación parlamentaria',
    body2: [
      'Morena y sus aliados disponen de 256 de los 500 escaños en San Lázaro, pero requerirán el apoyo del PVEM y de algunos diputados del PRD para alcanzar la mayoría calificada necesaria para aprobar las modificaciones constitucionales asociadas.',
      'El debate en comisiones comenzará el próximo 28 de mayo.',
    ],
    tags: ['México', 'Sheinbaum', 'reforma fiscal', 'patrimonio', 'IMSS'],
  },
  {
    titulo: 'La Bienal de Venecia premia con el León de Oro al pabellón uruguayo por su instalación sobre la memoria',
    deck: 'La obra de la artista Margarita Pérez combina archivo histórico, sonido y proyecciones sobre los desaparecidos',
    fuente: 'El País', cat: 'cultura', topic: 'cultura',
    body: [
      'La 60ª Bienal de Venecia concedió este sábado el León de Oro al mejor pabellón nacional a Uruguay, por la instalación "Las cartas que nunca llegaron", de la artista Margarita Pérez. El jurado, presidido por la crítica Adriana Cavarero, destacó "la profundidad poética y la rigurosidad documental" de la pieza.',
      'La obra ocupa los tres pisos del pabellón uruguayo en los Giardini y combina más de 400 cartas escritas por familiares de detenidos desaparecidos durante la dictadura cívico-militar (1973-1985), proyecciones de archivo audiovisual y un ambiente sonoro compuesto por la artista Florencia Beltrán.',
      'Pérez, de 47 años, agradeció el premio dedicándolo a "las mujeres que durante medio siglo han buscado a sus hijos". Es la primera vez que Uruguay obtiene esta distinción.',
    ],
    h2: 'Otros premios',
    body2: [
      'El León de Oro al mejor artista individual recayó en la pintora brasileña Sonia Gomes, mientras que la Mención Especial para artista joven fue para el colectivo nigeriano Otobong Nkanga.',
      'La Bienal, abierta hasta el 24 de noviembre, ha recibido ya más de 700.000 visitantes en sus dos primeros meses.',
    ],
    tags: ['Bienal de Venecia', 'Uruguay', 'León de Oro', 'memoria', 'arte'],
  },
  {
    titulo: 'La Comisión Europea propone que todos los coches nuevos lleven freno automático de emergencia desde 2028',
    deck: 'Bruselas estima que la medida evitaría 1.500 muertes anuales en las carreteras del bloque',
    fuente: 'EFE', cat: 'política', topic: 'política_global',
    body: [
      'La Comisión Europea propuso este lunes que todos los vehículos nuevos vendidos en la UE incorporen, a partir del 1 de enero de 2028, sistemas avanzados de frenado automático de emergencia capaces de detectar peatones, ciclistas y motoristas. La medida amplía la regulación vigente, que actualmente sólo exige la detección de otros vehículos.',
      'La comisaria de Transporte, Apostolos Tzitzikostas, estimó que el sistema —ya disponible como opción en la mayoría de fabricantes— evitaría unas 1.500 muertes anuales y 13.000 heridos graves. El sobrecoste para el consumidor se estima en entre 400 y 700 euros por vehículo.',
      'La industria, representada por ACEA, acogió la propuesta con cautela. "Apoyamos el objetivo, pero pedimos que los plazos sean compatibles con los ciclos de homologación", indicó la patronal en un comunicado.',
    ],
    h2: 'Próximos pasos',
    body2: [
      'La propuesta deberá ser debatida por el Parlamento Europeo y el Consejo. Las primeras lecturas están previstas para el otoño y, en caso de aprobarse en ese plazo, la norma entraría en vigor para los nuevos modelos en 2027 y para todas las matriculaciones en 2028.',
      'España es el segundo mercado del bloque en producción de vehículos, con 2,3 millones de unidades en 2025.',
    ],
    tags: ['UE', 'Bruselas', 'seguridad vial', 'frenado automático', 'ACEA'],
  },
  {
    titulo: 'El precio de la vivienda en alquiler en España sube un 11,4% interanual y marca un nuevo récord',
    deck: 'Madrid y Barcelona lideran las subidas, mientras los salarios apenas crecieron un 3,1% en el mismo periodo',
    fuente: 'El País', cat: 'economía', topic: 'sociedad',
    body: [
      'El precio medio del alquiler en España alcanzó en abril los 13,8 euros por metro cuadrado, un 11,4% más que hace un año, según los datos publicados este jueves por el portal Idealista. Es el cuadragésimo mes consecutivo de subidas y supera ya en un 47% los niveles previos a la pandemia.',
      'Las mayores subidas se registran en Madrid (+15,2%) y Barcelona (+13,1%), donde el precio medio supera los 22 euros por metro cuadrado. En localidades como Valencia, Málaga o Palma de Mallorca el incremento interanual rebasa el 13%, según la misma fuente.',
      'El director de estudios de Idealista, Francisco Iñareta, atribuyó la presión al desajuste entre oferta y demanda: "Faltan al menos 250.000 viviendas en alquiler permanente para frenar las subidas".',
    ],
    h2: 'Reacciones políticas',
    body2: [
      'El Gobierno reiteró su compromiso de declarar "zonas tensionadas" en 270 municipios antes del verano, lo que activaría topes a los incrementos en contratos de grandes tenedores. La medida ha sido contestada por varias comunidades autónomas y por las patronales del sector.',
      'Las organizaciones de inquilinos convocaron una manifestación para el próximo 24 de mayo en doce ciudades.',
    ],
    tags: ['vivienda', 'alquiler', 'Idealista', 'Madrid', 'Barcelona'],
  },
  {
    titulo: 'Las imágenes del telescopio James Webb revelan una galaxia formada 280 millones de años tras el Big Bang',
    deck: 'JADES-GS-z14 es el objeto luminoso más antiguo jamás observado y reabre el debate sobre las primeras estrellas',
    fuente: 'BBC Mundo', cat: 'tecnología', topic: 'tecnología',
    body: [
      'Un equipo internacional de astrónomos liderado por la Universidad de Cambridge anunció este martes el descubrimiento de la galaxia más antigua observada hasta la fecha. La galaxia, catalogada como JADES-GS-z14, existía cuando el universo tenía apenas 280 millones de años, según los datos del telescopio espacial James Webb.',
      'El hallazgo, publicado en Nature, fue confirmado mediante espectroscopía: la luz emitida por la galaxia presenta un corrimiento al rojo de 14,32, lo que permite calcular con precisión su antigüedad. Hasta ahora, el récord lo ostentaba JADES-GS-z13, identificada en 2023.',
      'Lo que más sorprende a la comunidad científica es el tamaño y luminosidad del objeto. "Es demasiado masiva y demasiado brillante para haberse formado tan pronto según los modelos cosmológicos estándar", explicó la astrónoma Brant Robertson, del equipo. "O bien las primeras estrellas eran muy distintas a lo que pensábamos, o algo falta en nuestra teoría".',
    ],
    h2: 'Próximas observaciones',
    body2: [
      'El equipo solicitó tiempo adicional de observación con Webb y con el radiotelescopio ALMA para caracterizar la composición química de la galaxia. Los resultados podrían publicarse a finales de 2026.',
      'El descubrimiento confirma la promesa del James Webb, que en sus cuatro años de funcionamiento ya ha redefinido en varias ocasiones los límites del universo observable.',
    ],
    tags: ['James Webb', 'NASA', 'cosmología', 'galaxia', 'Big Bang'],
  },
];

const PROHIBITED = [
  'cabe destacar', 'es importante señalar', 'sin lugar a dudas',
  'en este sentido', 'a tal efecto', 'no obstante',
];
const EMPTY_ADJ = ['importante', 'crucial', 'fundamental', 'tremendo'];

function renderArticleHtml(story, withFlaws = false) {
  const parts = [];
  parts.push(`<p>${story.body[0]}</p>`);
  if (withFlaws) {
    parts.push(`<p><span class="article-mark">Cabe destacar que</span> el anuncio supone un giro <span class="article-mark">importante</span> en la política reciente. <span class="article-strike">No obstante, el contexto sigue siendo complejo.</span> ${story.body[1]}</p>`);
  } else {
    parts.push(`<p>${story.body[1]}</p>`);
  }
  parts.push(`<p>${story.body[2]}</p>`);
  parts.push(`<h2>${story.h2}</h2>`);
  for (const p of story.body2) parts.push(`<p>${p}</p>`);
  return parts.join('\n');
}

function pickModel(seed) { return seed % 2 === 0 ? 'mistral-nemo:12b' : 'gemma4:e4b'; }

function isoHoursAgo(hours, mins=0) {
  const d = new Date();
  d.setHours(d.getHours() - hours, d.getMinutes() - mins, 0, 0);
  return d.toISOString();
}

function fmtTime(iso) {
  const d = new Date(iso);
  return d.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
}

function makeRun(idx, story, opts) {
  const status = opts.status;
  const writerAttempts = opts.attempts;
  const winnerModel = pickModel(idx);
  const loserModel = winnerModel === 'mistral-nemo:12b' ? 'gemma4:e4b' : 'mistral-nemo:12b';

  // base scores
  const srcW = SOURCES.find(s => s.name === story.fuente)?.weight || 0.85;
  const breakdown = {
    source:    +(srcW * 0.18).toFixed(3),
    freshness: +(Math.random() * 0.16 + 0.12).toFixed(3),
    relevance: +(Math.random() * 0.18 + 0.18).toFixed(3),
    trends:    +(Math.random() * 0.10 + 0.04).toFixed(3),
    gemini:    +(Math.random() * 0.08 + 0.02).toFixed(3),
  };
  const editorial = +(breakdown.source + breakdown.freshness + breakdown.relevance + breakdown.trends + breakdown.gemini).toFixed(3);

  // candidates: include story + 7 others
  const otherStories = STORY_BANK.filter(s => s !== story).slice(0, 9);
  const candidates = [
    {
      titulo: story.titulo, fuente: story.fuente, url: '#',
      editorial_score: editorial,
      source_score: breakdown.source, freshness_score: breakdown.freshness,
      relevance_score: breakdown.relevance, depth_bonus: 0.04,
    },
    ...otherStories.map((s, i) => {
      const w = SOURCES.find(x => x.name === s.fuente)?.weight || 0.85;
      const sc = +(0.32 + Math.random() * 0.30 - i * 0.015).toFixed(3);
      return {
        titulo: s.titulo, fuente: s.fuente, url: '#',
        editorial_score: sc, source_score: +(w * 0.18).toFixed(3),
        freshness_score: +(Math.random() * 0.12).toFixed(3),
        relevance_score: +(Math.random() * 0.15).toFixed(3),
        depth_bonus: +(Math.random() * 0.03).toFixed(3),
      };
    })
  ].sort((a,b) => b.editorial_score - a.editorial_score);

  const totalFound = 80 + Math.floor(Math.random() * 25);
  const afterDedup = totalFound - (12 + Math.floor(Math.random() * 8));
  const afterCool  = afterDedup - (8 + Math.floor(Math.random() * 6));
  const editorSel  = 1;
  const qualityOk  = status === 'error' ? 0 : 1;
  const published  = (status === 'success' || (status === 'partial' && Math.random() < 0.5)) ? 1 : 0;

  // attempts history
  const attemptsHistory = [];
  for (let n = 1; n <= writerAttempts; n++) {
    const isLast = n === writerAttempts;
    const willPass = isLast && status !== 'error';
    const model = (writerAttempts > 1 && n > 1)
      ? (n % 2 === 0 ? 'gemma4:e4b' : 'mistral-nemo:12b')
      : winnerModel;
    const dur = +(8 + Math.random() * 14).toFixed(1);
    const issues = willPass ? [] : [
      `Frase prohibida: '${PROHIBITED[(n + idx) % PROHIBITED.length]}'`,
      `Adjetivo vacío: '${EMPTY_ADJ[n % EMPTY_ADJ.length]} novedad'`,
      ...(n === 2 ? [`Párrafo ${3 + n % 2} sin densidad factual`] : []),
      ...(n === 3 ? [`Estructura no cumple: faltan H2 intermedios`] : []),
    ];
    const removed = willPass ? [] : ['cabe destacar que', 'en este sentido', 'sin lugar a dudas'].slice(0, 1 + (n%2));
    attemptsHistory.push({
      attempt_n: n,
      model,
      duration_seconds: dur,
      passed: willPass,
      reason: willPass ? 'approved' : (n % 3 === 0 ? 'factcheck' : 'quality'),
      quality_issues: issues,
      removed_phrases: removed,
      html: renderArticleHtml(story, !willPass),
      prompt_tokens: 2400 + Math.floor(Math.random() * 600),
      output_tokens: 1100 + Math.floor(Math.random() * 400),
    });
  }

  const duration = writerAttempts === 1
    ? +(95 + Math.random() * 50).toFixed(1)
    : writerAttempts === 2
      ? +(170 + Math.random() * 40).toFixed(1)
      : +(220 + Math.random() * 60).toFixed(1);

  const errors = [];
  if (status === 'error') {
    errors.push({ stage: 'writer', message: 'Quality gate failed after 4 retries: max retries exceeded', timestamp: opts.timestamp });
  }
  if (status === 'partial') {
    errors.push({ stage: 'publisher', message: 'WordPress responded 504 (gateway timeout). Retry succeeded on second attempt.', timestamp: opts.timestamp });
  }

  // cooldown penalized topics
  const penalizedTopics = {};
  for (let i = 0; i < 3 + Math.floor(Math.random()*3); i++) {
    const t = TOPICS[(idx + i*2) % TOPICS.length];
    penalizedTopics[t] = (penalizedTopics[t] || 0) + 1 + Math.floor(Math.random()*7);
  }

  return {
    id: `run-${String(idx).padStart(3, '0')}`,
    timestamp: opts.timestamp,
    status,
    duration_seconds: duration,
    llm_calls: 18 + writerAttempts * 5 + Math.floor(Math.random()*4),
    input_chars: 18000 + Math.floor(Math.random()*8000),
    candidate_attempts: writerAttempts,

    collector: {
      total_found: totalFound,
      candidates_after_dedup: afterDedup,
      candidates,
    },
    cooldown: {
      applied: true, window: 15, factor: 0.65,
      penalized_count: 6 + Math.floor(Math.random()*4),
      candidates_after_cooldown: afterCool,
      penalized_topics: penalizedTopics,
    },
    trends: {
      gemini: {
        hints: ['BCE', 'inflación', 'Champions', 'Real Madrid', 'OpenAI'],
        matches: 2 + Math.floor(Math.random()*2),
        unmatched_hints: ['mercados emergentes'],
        boosted_candidates: 3 + Math.floor(Math.random()*3),
        top_boosts: [{ titulo: story.titulo, score: 0.08, match: story.tags[0] }],
      },
      google: {
        hints: ['tipos interés', 'champions league', 'gpt'],
        matches: 1 + Math.floor(Math.random()*2),
        unmatched_hints: [],
        boosted_candidates: 2 + Math.floor(Math.random()*3),
        top_boosts: [{ titulo: story.titulo, score: 0.05, match: story.tags[1] || story.tags[0] }],
      },
    },
    editor: {
      selected_titulo: story.titulo,
      selected_fuente: story.fuente,
      selected_url: '#',
      editorial_score: editorial,
      breakdown,
      top_3_candidates: candidates.slice(0, 3).map(c => ({ titulo: c.titulo, fuente: c.fuente, editorial_score: c.editorial_score })),
    },
    extractor: {
      success: true,
      source_words: 480 + Math.floor(Math.random()*180),
      enriched: true,
    },
    writer: {
      success: status !== 'error',
      winner_model: winnerModel,
      attempts: writerAttempts,
      quality_rejects: Math.max(0, writerAttempts - 1),
      final_length: 410 + Math.floor(Math.random()*120),
      issues: attemptsHistory.flatMap(a => a.quality_issues).slice(0, 3),
      attempts_history: attemptsHistory,
    },
    selector: {
      enabled: true,
      winner: winnerModel,
      scores: {
        'mistral-nemo:12b': winnerModel === 'mistral-nemo:12b' ? 0.78 : 0.61,
        'gemma4:e4b':       winnerModel === 'gemma4:e4b'       ? 0.81 : 0.58,
      },
      reason: writerAttempts > 1
        ? 'Mayor densidad factual; menos adjetivos vacíos; estructura con dos H2 según rúbrica.'
        : 'Lead más directo; cita concreta a fuente primaria; respeta el contrato de longitud.',
    },
    fact_checker: {
      passed: status !== 'error',
      claim_count: 14 + Math.floor(Math.random()*8),
      uncertain_claims: writerAttempts > 1
        ? ["La cifra '40 millones' no aparece en el texto fuente.", "Atribución '\"está bien encaminada\"' verificada con archivo de prensa BCE."]
        : [],
    },
    quality: {
      passed: qualityOk === 1,
      checks: {
        word_count: true,
        paragraph_count: true,
        prohibited_phrases: status !== 'error',
      },
    },
    seo: {
      titulo: story.titulo,
      slug: story.titulo.toLowerCase().replace(/[^a-záéíóúñ ]/g, '').replace(/\s+/g, '-').slice(0, 80),
      tags: story.tags,
      category: story.cat,
      focus_keyword: story.tags[0],
    },
    publisher: {
      success: published === 1,
      post_id: published ? (24800 + idx) : null,
      post_url: published ? `https://example.com/${story.cat}/${idx}` : null,
    },
    errors,
  };
}

function getMockExecutionHistory() {
  // 7 success / 2 partial / 1 error
  const plan = [
    { idx: 0, story: STORY_BANK[0], status: 'success', attempts: 1, hoursAgo: 0, mins: 14 },     // BCE
    { idx: 1, story: STORY_BANK[1], status: 'success', attempts: 1, hoursAgo: 1, mins: 6 },      // Madrid
    { idx: 2, story: STORY_BANK[2], status: 'success', attempts: 3, hoursAgo: 2, mins: 22 },     // GPT-6 (retry story!)
    { idx: 3, story: STORY_BANK[3], status: 'partial', attempts: 2, hoursAgo: 4, mins: 8 },      // Merz
    { idx: 4, story: STORY_BANK[4], status: 'success', attempts: 1, hoursAgo: 5, mins: 45 },     // OMS
    { idx: 5, story: STORY_BANK[5], status: 'success', attempts: 2, hoursAgo: 8, mins: 12 },     // Sheinbaum
    { idx: 6, story: STORY_BANK[6], status: 'success', attempts: 1, hoursAgo: 10, mins: 30 },    // Bienal
    { idx: 7, story: STORY_BANK[7], status: 'error',   attempts: 4, hoursAgo: 13, mins: 4 },     // UE coches (fail!)
    { idx: 8, story: STORY_BANK[8], status: 'partial', attempts: 1, hoursAgo: 17, mins: 36 },    // alquiler
    { idx: 9, story: STORY_BANK[9], status: 'success', attempts: 1, hoursAgo: 21, mins: 18 },    // Webb
  ];
  return plan.map(p => makeRun(p.idx, p.story, {
    status: p.status, attempts: p.attempts,
    timestamp: isoHoursAgo(p.hoursAgo, p.mins),
  }));
}

/* ============ Logs ============ */
function getMockLogs() {
  const lines = [];
  const baseTime = new Date();
  baseTime.setHours(baseTime.getHours() - 1);
  let t = baseTime.getTime();

  const seq = [
    ['STEP',    'Pipeline', '═══ Iniciando ciclo de ejecución ═══'],
    ['INFO',    'RSSCollector', 'Iniciando recolección desde 9 fuentes RSS'],
    ['INFO',    'RSSCollector', 'EFE: 14 noticias obtenidas en 1.2s'],
    ['INFO',    'RSSCollector', 'BBC Mundo: 11 noticias obtenidas en 0.9s'],
    ['INFO',    'RSSCollector', 'DW: 9 noticias obtenidas en 1.4s'],
    ['INFO',    'RSSCollector', 'Marca: 12 noticias obtenidas en 0.7s'],
    ['INFO',    'RSSCollector', 'AS: 8 noticias obtenidas en 0.8s'],
    ['INFO',    'RSSCollector', 'Guardian: 10 noticias obtenidas en 1.1s'],
    ['INFO',    'RSSCollector', 'El País: 13 noticias obtenidas en 0.6s'],
    ['INFO',    'RSSCollector', 'Infobae: 9 noticias obtenidas en 1.3s'],
    ['INFO',    'RSSCollector', 'news.google.com: 7 noticias obtenidas en 0.5s'],
    ['STEP',    'RSSCollector', '93 noticias recolectadas en total'],
    ['INFO',    'DeduplicationGate', '20 títulos publicados en memoria (ventana 15 ejecuciones)'],
    ['INFO',    'DeduplicationGate', 'Similitud Jaccard ≥ 0.78 → descartado: "Real Madrid roza la decimosexta..."'],
    ['INFO',    'DeduplicationGate', 'Similitud Jaccard ≥ 0.82 → descartado: "BCE mantiene una postura prudente..."'],
    ['INFO',    'DeduplicationGate', '14 duplicados eliminados → 79 candidatos restantes'],
    ['INFO',    'CooldownGate', 'Aplicando factor 0.65 a temas penalizados'],
    ['WARNING', 'CooldownGate', 'Tema "actores_geopolíticos" aparece 8 veces en ventana → penalización máxima'],
    ['INFO',    'CooldownGate', 'Tema "economía_global" penalizado x1'],
    ['INFO',    'CooldownGate', '6 candidatos penalizados, 73 restantes'],
    ['STEP',    'TrendsAgent', 'Solicitando hints a Gemini Pro (qwen2.5:14b local off)'],
    ['INFO',    'TrendsAgent', 'Gemini hints: ["BCE", "inflación", "Champions", "Real Madrid", "OpenAI"]'],
    ['INFO',    'TrendsAgent', 'Google Trends hints: ["tipos interés", "champions league", "gpt"]'],
    ['INFO',    'TrendsAgent', 'Boost aplicado a 5 candidatos (+0.04 a +0.09)'],
    ['STEP',    'EditorAgent', 'Evaluando 73 candidatos con qwen2.5:14b'],
    ['INFO',    'EditorAgent', 'Top-1: "El Banco Central Europeo recorta los tipos..." score=0.713 fuente=EFE'],
    ['INFO',    'EditorAgent', 'Top-2: "El Real Madrid se proclama campeón..." score=0.681 fuente=Marca'],
    ['INFO',    'EditorAgent', 'Top-3: "OpenAI presenta GPT-6..." score=0.652 fuente=BBC Mundo'],
    ['INFO',    'EditorAgent', 'Selección: Top-1 ✓ — desglose: source=0.279 freshness=0.182 relevance=0.218 trends=0.034 gemini=0.000'],
    ['INFO',    'Extractor', 'Descargando contenido completo de la URL fuente'],
    ['INFO',    'Extractor', '562 palabras extraídas — enriquecimiento OK'],
    ['STEP',    'DualWriter', 'Lanzando A/B competition: mistral-nemo:12b vs gemma4:e4b'],
    ['INFO',    'Writer[A]', 'mistral-nemo:12b — generando borrador (prompt: 2.512 tokens)'],
    ['INFO',    'Writer[B]', 'gemma4:e4b — generando borrador (prompt: 2.498 tokens)'],
    ['INFO',    'Writer[A]', 'Borrador completo en 11.4s — 1.342 tokens de salida'],
    ['INFO',    'Writer[B]', 'Borrador completo en 9.8s — 1.188 tokens de salida'],
    ['STEP',    'Selector', 'Comparando borradores con qwen2.5:14b'],
    ['INFO',    'Selector', 'Ganador: mistral-nemo:12b (0.78 vs 0.61) — razón: "Mayor densidad factual"'],
    ['STEP',    'FactChecker', 'Verificando 17 afirmaciones extraídas'],
    ['INFO',    'FactChecker', '17/17 afirmaciones verificadas en el texto fuente ✓'],
    ['STEP',    'QualityGate', 'Aplicando rúbrica de calidad editorial'],
    ['INFO',    'QualityGate', 'Conteo de palabras: 461 ∈ [350,600] ✓'],
    ['INFO',    'QualityGate', 'Conteo de párrafos: 6 ∈ [4,8] ✓'],
    ['INFO',    'QualityGate', 'Frases prohibidas: 0/12 detectadas ✓'],
    ['INFO',    'QualityGate', 'Adjetivos vacíos: 1/8 detectados — toleración OK ✓'],
    ['INFO',    'QualityGate', 'Quality PASSED — no se requieren reintentos'],
    ['STEP',    'SEOAgent', 'Generando metadatos con gemma4:e4b'],
    ['INFO',    'SEOAgent', 'Slug: "el-banco-central-europeo-recorta-los-tipos-de-interes-al-25"'],
    ['INFO',    'SEOAgent', 'Keyword: "BCE" — Tags: BCE, tipos de interés, Lagarde, eurozona, inflación'],
    ['STEP',    'WordPressPublisher', 'POST /wp-json/wp/v2/posts'],
    ['INFO',    'WordPressPublisher', 'HTTP 201 → post_id=24812 → publicado en categoría "economía"'],
    ['STEP',    'Pipeline', '═══ Ciclo completado en 2m 28s ═══'],
    ['INFO',    'Pipeline', '18 llamadas a LLM — 24.812 chars de entrada — 0 errores'],
    // intermixed warnings / errors
    ['WARNING', 'OllamaClient', 'Latencia P95 > 4.5s en gemma4:e4b — posible throttling GPU'],
    ['INFO',    'OllamaClient', 'GPU vRAM 71% — 3 modelos cargados'],
    ['INFO',    'HealthMonitor', 'Heartbeat: bridge ok | ollama ok | wordpress ok'],
    ['STEP',    'Pipeline', '═══ Iniciando próximo ciclo ═══'],
    ['INFO',    'RSSCollector', 'Iniciando recolección desde 9 fuentes RSS'],
    ['ERROR',   'RSSCollector', 'Timeout en news.google.com tras 5.0s — reintentando 1/3'],
    ['INFO',    'RSSCollector', 'news.google.com: reintento exitoso, 8 noticias obtenidas'],
  ];

  // generate ~200 lines by cycling
  for (let i = 0; i < 200; i++) {
    const tpl = seq[i % seq.length];
    t += 800 + Math.floor(Math.random()*1400);
    const d = new Date(t);
    const ts = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')},${String(d.getMilliseconds()).padStart(3,'0')}`;
    lines.push({
      id: i,
      timestamp: ts,
      ts_compact: `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}.${String(d.getMilliseconds()).padStart(3,'0')}`,
      level: tpl[0],
      module: tpl[1],
      message: tpl[2],
    });
  }
  return lines;
}

/* ============ Config ============ */
function getMockConfig() {
  return {
    models: {
      collector: 'qwen2.5:14b',
      editor: 'qwen2.5:14b',
      trends_agent: 'qwen2.5:14b',
      writer_a: 'mistral-nemo:12b',
      writer_b: 'gemma4:e4b',
      selector: 'qwen2.5:14b',
      fact_checker: 'qwen2.5:14b',
      seo_agent: 'gemma4:e4b',
    },
    source_weights: Object.fromEntries(SOURCES.map(s => [s.name, s.weight])),
    pipeline: {
      MAX_WRITER_RETRIES: 4,
      DUAL_WRITER_ENABLED: true,
      TOPIC_COOLDOWN_WINDOW: 15,
      TOPIC_COOLDOWN_FACTOR: 0.65,
      JACCARD_THRESHOLD: 0.78,
      QUALITY_MIN_WORDS: 350,
      QUALITY_MAX_WORDS: 600,
      QUALITY_PROHIBITED_PHRASES: 12,
      POLL_INTERVAL_SECONDS: 4,
    },
  };
}

Object.assign(window, {
  STORY_BANK, SOURCES, CATEGORIES, TOPICS,
  getMockExecutionHistory, getMockLogs, getMockConfig,
  fmtTime,
});
