const fs = require('fs');
const path = require('path');
const { createClient, readCsv, writeJson } = require('./benchmark-lib');
const { explainSummary, pageFields, query02Dir, query02Sql } = require('./query02-lib');

const rootDir = path.resolve(__dirname, '..', '..');
const reportFile = path.join(rootDir, 'benchmarks', 'INFORME_OPTIMIZACION_QUERY02.md');

const readJsonIfExists = (file) => (fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : null);
const fmt = (value, suffix = '') => {
  if (value === null || value === undefined || value === '') return 'N/D';
  const number = Number(value);
  if (!Number.isFinite(number)) return `${value}${suffix}`;
  return `${number.toLocaleString('es-CO', { maximumFractionDigits: 4 })}${suffix}`;
};

const findRow = (rows, queryId, level = null) =>
  rows.find(
    (row) =>
      row.query_id === queryId &&
      (level === null || String(row.concurrency_level) === String(level)) &&
      !String(row.phase || '').endsWith('_warmup')
  );

const improvement = (before, after) => {
  const b = Number(before);
  const a = Number(after);
  if (!Number.isFinite(b) || !Number.isFinite(a)) return 'N/D';
  return `${fmt(b - a, ' ms')} (${fmt(((b - a) / b) * 100, ' %')})`;
};

const criteriaLine = (name, value, limit, unit = 'ms') => {
  const ok = Number(value) <= limit;
  return `- ${name}: ${fmt(value, ` ${unit}`)} frente a objetivo ${fmt(limit, ` ${unit}`)} => ${ok ? 'cumple' : 'no cumple'}.`;
};

const indexRows = async (client) => {
  const { rows } = await client.query(`
    SELECT c.relname AS index_name,
           pg_size_pretty(pg_relation_size(c.oid)) AS size,
           i.indisvalid,
           i.indisready,
           pg_get_indexdef(i.indexrelid) AS definition
    FROM pg_index i
    JOIN pg_class c ON c.oid = i.indexrelid
    JOIN pg_class t ON t.oid = i.indrelid
    WHERE t.relname = 'invima_api_catalog'
      AND c.relname IN (
        'idx_query02_catalog_vencimiento_desc_nullslast',
        'idx_invima_api_catalog_vencimiento',
        'idx_invima_api_catalog_producto_prefix',
        'invima_api_catalog_pk'
      )
    ORDER BY c.relname;
  `);
  return rows;
};

const tableStats = async (client) => {
  const { rows } = await client.query(`
    SELECT reltuples::bigint AS estimated_rows,
           pg_size_pretty(pg_relation_size('invima_api_catalog')) AS table_size,
           pg_size_pretty(pg_total_relation_size('invima_api_catalog')) AS total_size
    FROM pg_class
    WHERE oid = 'invima_api_catalog'::regclass;
  `);
  return rows[0];
};

const renderIndexTable = (indexes) =>
  [
    '| Indice | Tamano | Valido | Listo | Definicion |',
    '|---|---:|---|---|---|',
    ...indexes.map(
      (row) =>
        `| \`${row.index_name}\` | ${row.size} | ${row.indisvalid} | ${row.indisready} | \`${row.definition.replace(/\|/g, '\\|')}\` |`
    )
  ].join('\n');

const main = async () => {
  const beforeExplain = readJsonIfExists(path.join(query02Dir, 'explain_before.json'));
  const afterExplain = readJsonIfExists(path.join(query02Dir, 'explain_after.json'));
  const validation = readJsonIfExists(path.join(query02Dir, 'validation.json'));
  const serviceProfile = readJsonIfExists(path.join(query02Dir, 'service_profile.json'));
  const oldOptimized = readCsv(path.join(rootDir, 'benchmarks', 'results', 'optimized_summary.csv'));
  const oldConcurrency = readCsv(path.join(rootDir, 'benchmarks', 'results', 'concurrency_summary.csv'));
  const newOptimized = readCsv(path.join(query02Dir, 'optimized_summary.csv'));
  const newConcurrency = readCsv(path.join(query02Dir, 'concurrency_summary.csv'));

  const before = beforeExplain ? explainSummary(beforeExplain) : {};
  const after = afterExplain ? explainSummary(afterExplain) : {};
  const oldSeq = findRow(oldOptimized, 'query_02');
  const newSeq = findRow(newOptimized, 'query_02');

  const client = await createClient();
  let indexes = [];
  let stats = {};
  try {
    indexes = await indexRows(client);
    stats = await tableStats(client);
  } finally {
    await client.end();
  }

  const concurrencyLines = [1, 5, 10, 25, 50]
    .map((level) => {
      const oldRow = findRow(oldConcurrency, 'query_02', level);
      const newRow = findRow(newConcurrency, 'query_02', level);
      return `- ${level} usuario(s): promedio ${fmt(oldRow?.latency_avg_ms, ' ms')} -> ${fmt(
        newRow?.latency_avg_ms,
        ' ms'
      )}; p95 ${fmt(oldRow?.latency_p95_ms, ' ms')} -> ${fmt(newRow?.latency_p95_ms, ' ms')}; throughput ${fmt(
        oldRow?.throughput_per_second,
        ' req/s'
      )} -> ${fmt(newRow?.throughput_per_second, ' req/s')}.`;
    })
    .join('\n');

  const validationSummary = validation?.comparison
    ? [
        `- Total antes/despues: ${fmt(validation.comparison.totalBefore)} / ${fmt(validation.comparison.totalAfter)}.`,
        `- Resultado global: ${validation.comparison.valid ? 'VALIDO' : 'NO VALIDO'}.`,
        ...validation.comparison.pages.map(
          (page) =>
            `- Offset ${fmt(page.offset)}: valido=${page.valid}; faltantes=${page.missingIds?.length ?? 'N/D'}; adicionales=${page.additionalIds?.length ?? 'N/D'}; cambio orden=${page.orderChanged}; cambio claves ORDER BY=${page.orderKeysChanged}.`
        )
      ].join('\n')
    : 'No disponible.';

  const report = `# Informe de optimizacion query_02

Generado: ${new Date().toISOString()}

## 1. Descripcion de query_02

\`query_02\` corresponde a la primera pagina general del catalogo INVIMA:

\`\`\`sql
${query02Sql};
\`\`\`

El endpoint real asociado es \`GET /api/invima/catalogo?limit=25&offset=0\`. En Angular se invoca desde \`InvimaService.searchCatalog()\` y \`CatalogPageComponent.loadData()\`. En Express pasa por \`backend/src/routes/invima.routes.js\` y termina en \`getInvimaCatalog()\` / \`runCatalogQuery()\` dentro de \`backend/invima/service.js\`.

La solicitud real del endpoint ejecuta un \`COUNT(*)\` exacto y luego la consulta paginada. El benchmark historico de \`query_02\` mide la consulta paginada SQL.

## 2. Causa raiz

Antes de optimizar, PostgreSQL recorria aproximadamente ${fmt(stats.estimated_rows)} filas de una tabla de ${stats.table_size || 'N/D'} y aplicaba \`top-N heapsort\` para devolver solo 25 registros. La razon principal es que el orden requerido \`fecha_vencimiento DESC NULLS LAST, producto ASC NULLS LAST\` no tenia un indice compatible. El indice existente sobre \`fecha_vencimiento\` es ascendente y no satisface directamente \`DESC NULLS LAST\`.

Un indice compuesto completo con \`producto\` no es recomendable en esta base: se observaron valores de \`producto\` extremadamente largos, y btree puede superar el limite de tamano de tupla. Por eso la optimizacion usa un indice estrecho por fecha descendente y deja el orden secundario sobre \`producto\` para un conjunto reducido.

## 3. Plan anterior

- Nodos: ${before.node_types || 'N/D'}.
- Seq Scan: ${before.seq_scans ?? 'N/D'}; Index Scan: ${before.index_scans ?? 'N/D'}; Incremental Sort: ${before.incremental_sorts ?? 'N/D'}; Gather Merge: ${before.gather_merges ?? 'N/D'}.
- Filas estimadas/real raiz: ${fmt(before.estimated_rows)} / ${fmt(before.actual_rows)}.
- Ancho estimado de fila: ${fmt(before.estimated_width)} bytes.
- Bloques hit/read: ${fmt(before.shared_hit_blocks)} / ${fmt(before.shared_read_blocks)}.
- Temporales read/write: ${fmt(before.temp_read_blocks)} / ${fmt(before.temp_written_blocks)}.
- Metodo de ordenamiento: ${(before.sort_methods || []).join(', ') || 'N/D'}.
- Planeacion/ejecucion: ${fmt(before.planning_ms, ' ms')} / ${fmt(before.execution_ms, ' ms')}.

## 4. Cambios implementados

Se agrego una migracion reversible:

\`\`\`sql
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_query02_catalog_vencimiento_desc_nullslast
ON invima_api_catalog (fecha_vencimiento DESC NULLS LAST);
ALTER TABLE invima_api_catalog ALTER COLUMN fecha_vencimiento SET STATISTICS 1000;
ALTER TABLE invima_api_catalog ALTER COLUMN producto SET STATISTICS 1000;
\`\`\`

No se modificaron columnas de respuesta, filtros, universo consultado, conteo exacto ni registros regulatorios. Se agrego un desempate estable al \`ORDER BY\` con \`source_dataset_key, source_uid\`, que son la clave primaria, para evitar duplicados u omisiones entre paginas cuando hay empates en \`fecha_vencimiento\` y \`producto\`.

## 5. Indices relevantes

${renderIndexTable(indexes)}

## 6. Plan posterior

- Nodos: ${after.node_types || 'N/D'}.
- Seq Scan: ${after.seq_scans ?? 'N/D'}; Index Scan: ${after.index_scans ?? 'N/D'}; Incremental Sort: ${after.incremental_sorts ?? 'N/D'}; Gather Merge: ${after.gather_merges ?? 'N/D'}.
- Filas estimadas/real raiz: ${fmt(after.estimated_rows)} / ${fmt(after.actual_rows)}.
- Ancho estimado de fila: ${fmt(after.estimated_width)} bytes.
- Bloques hit/read: ${fmt(after.shared_hit_blocks)} / ${fmt(after.shared_read_blocks)}.
- Temporales read/write: ${fmt(after.temp_read_blocks)} / ${fmt(after.temp_written_blocks)}.
- Metodo de ordenamiento: ${(after.sort_methods || []).join(', ') || 'N/D'}.
- Planeacion/ejecucion: ${fmt(after.planning_ms, ' ms')} / ${fmt(after.execution_ms, ' ms')}.

## 7. Validacion funcional

${validationSummary}

La validacion before se ejecuto con el plan antiguo forzado mediante GUCs de sesion, y la validacion after con el nuevo indice habilitado. En ambos casos se uso la misma consulta determinista. La validacion exige que no cambien IDs, orden, claves \`ORDER BY\`, estructura publica ni valores publicos.

## 8. Resultados secuenciales

- Promedio: ${fmt(oldSeq?.latency_avg_ms, ' ms')} -> ${fmt(newSeq?.latency_avg_ms, ' ms')}; mejora ${improvement(
    oldSeq?.latency_avg_ms,
    newSeq?.latency_avg_ms
  )}.
- Mediana: ${fmt(oldSeq?.latency_median_ms, ' ms')} -> ${fmt(newSeq?.latency_median_ms, ' ms')}.
- p95: ${fmt(oldSeq?.latency_p95_ms, ' ms')} -> ${fmt(newSeq?.latency_p95_ms, ' ms')}; mejora ${improvement(
    oldSeq?.latency_p95_ms,
    newSeq?.latency_p95_ms
  )}.
- p99: ${fmt(oldSeq?.latency_p99_ms, ' ms')} -> ${fmt(newSeq?.latency_p99_ms, ' ms')}; mejora ${improvement(
    oldSeq?.latency_p99_ms,
    newSeq?.latency_p99_ms
  )}.
- Fallos: ${fmt(newSeq?.failures)}; tasa de fallos: ${fmt(newSeq?.failure_rate_percent, ' %')}.
- Throughput: ${fmt(oldSeq?.throughput_per_second, ' ops/s')} -> ${fmt(newSeq?.throughput_per_second, ' ops/s')}.

## 9. Resultados concurrentes

${concurrencyLines}

## 10. Criterios

${criteriaLine('Promedio individual', newSeq?.latency_avg_ms, 300)}
${criteriaLine('p95 individual', newSeq?.latency_p95_ms, 500)}
${criteriaLine('p99 individual', newSeq?.latency_p99_ms, 750)}
${criteriaLine('p95 con 5 usuarios', findRow(newConcurrency, 'query_02', 5)?.latency_p95_ms, 1000)}
${criteriaLine('p95 con 10 usuarios', findRow(newConcurrency, 'query_02', 10)?.latency_p95_ms, 2000)}
${criteriaLine('p95 con 25 usuarios', findRow(newConcurrency, 'query_02', 25)?.latency_p95_ms, 5000)}

## 11. Paginacion, conteo y concurrencia

La consulta usa \`LIMIT 25 OFFSET n\`. Para \`OFFSET 0\`, el costo dominante era encontrar el orden inicial sin indice compatible. Las paginas profundas siguen pagando el costo natural de \`OFFSET\`; una siguiente iteracion compatible seria exponer paginacion keyset con cursor basado en \`fecha_vencimiento\`, \`producto\` y la clave primaria \`source_dataset_key/source_uid\`, manteniendo la ruta actual como compatibilidad.

El endpoint real conserva \`COUNT(*)\` exacto en cada solicitud. No se reemplazo por estimaciones ni cache como unica solucion. Si el conteo se convierte en cuello de botella, se recomienda medir y separar el total exacto en una ruta o tabla de estadisticas invalidada durante sincronizacion.

Perfil del servicio actualizado: \`getInvimaCatalog(pool, { limit: 25, offset: 0 })\` devolvio ${fmt(
    serviceProfile?.items
  )} filas, total exacto ${fmt(serviceProfile?.total)}, tiempo logico ${fmt(
    serviceProfile?.service_total_ms,
    ' ms'
  )}, serializacion ${fmt(serviceProfile?.serialization_ms, ' ms')} y respuesta de ${fmt(
    serviceProfile?.response_bytes,
    ' bytes'
  )}.

El pool actual usa \`PGPOOL_MAX\` desde entorno y PostgreSQL tiene \`max_connections\` estandar. En una maquina de 2 vCPU y 4 GB, aumentar conexiones sin reducir el costo de query empeora la contencion; la mejora principal provino del plan SQL.

## 12. Riesgos

- El nuevo indice aumenta espacio en disco y costo de mantenimiento durante inserciones/sincronizaciones.
- No cubrir \`producto\` evita un indice enorme, pero todavia requiere ordenar grupos con la misma fecha.
- El desempate por \`source_dataset_key, source_uid\` estabiliza la paginacion, pero cambia el orden interno previamente no garantizado de filas empatadas.
- \`ANALYZE\` automatico se omitio porque esta base local tiene bytes con codificacion invalida que hacen fallar el escaneo completo.

## 13. Redaccion academica

Antes de la optimizacion, la consulta general paginada presento una latencia promedio de ${fmt(
    oldSeq?.latency_avg_ms,
    ' ms'
  )} y un p95 de ${fmt(oldSeq?.latency_p95_ms, ' ms')}. Despues de implementar el indice \`idx_query02_catalog_vencimiento_desc_nullslast\`, la latencia promedio se redujo a ${fmt(
    newSeq?.latency_avg_ms,
    ' ms'
  )} y el p95 a ${fmt(newSeq?.latency_p95_ms, ' ms')}, equivalente a una mejora de ${improvement(
    oldSeq?.latency_avg_ms,
    newSeq?.latency_avg_ms
  )}. Bajo 50 usuarios concurrentes, la latencia promedio paso de aproximadamente ${fmt(
    findRow(oldConcurrency, 'query_02', 50)?.latency_avg_ms,
    ' ms'
  )} a ${fmt(findRow(newConcurrency, 'query_02', 50)?.latency_avg_ms, ' ms')}.

## 14. Reversion y repeticion

Revertir:

\`\`\`powershell
npm.cmd run benchmark:query02:rollback
\`\`\`

Repetir:

\`\`\`powershell
npm.cmd run benchmark:query02:validate:before
npm.cmd run benchmark:query02:explain:before
npm.cmd run benchmark:query02:optimize
npm.cmd run benchmark:query02:explain:after
npm.cmd run benchmark:query02:validate
npm.cmd run benchmark:query02:optimized
npm.cmd run benchmark:query02:concurrency
npm.cmd run benchmark:query02:report
\`\`\`
`;

  fs.writeFileSync(reportFile, report, 'utf8');
  writeJson(path.join(query02Dir, 'report_metadata.json'), {
    generatedAt: new Date().toISOString(),
    reportFile
  });
};

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
