const fs = require('fs');
const path = require('path');
const { performance } = require('perf_hooks');
const {
  config,
  createClient,
  ensureDir,
  readCsv,
  resultsDir,
  summarizeRows,
  writeCsv,
  writeJson
} = require('./benchmark-lib');

const query02Dir = path.join(resultsDir, 'query02');
const query02Sql = fs
  .readFileSync(path.join(__dirname, '..', 'queries', 'query_02.sql'), 'utf8')
  .trim()
  .replace(/;\s*$/, '');

const pageFields = [
  'source_dataset_key',
  'source_dataset_id',
  'source_dataset_name',
  'categoria_linea',
  'categoria_canonica',
  'categoria_slug',
  'expediente',
  'registro_sanitario',
  'producto',
  'titular',
  'estado_registro',
  'fecha_expedicion',
  'fecha_vencimiento',
  'modalidad',
  'grupo',
  'marca',
  'principio_activo',
  'forma_farmaceutica',
  'presentacion_comercial',
  'atc',
  'rol_nombre',
  'rol_tipo',
  'ciudad_titular',
  'pais_titular',
  'fabricante',
  'importador',
  'vigencia'
];

const pageSelectSql = `
SELECT
  source_dataset_key,
  source_uid,
  ${pageFields.filter((field) => field !== 'source_dataset_key').join(',\n  ')}
FROM invima_api_catalog
ORDER BY
  fecha_vencimiento DESC NULLS LAST,
  producto ASC NULLS LAST,
  source_dataset_key ASC,
  source_uid ASC
LIMIT $1 OFFSET $2
`;

const countSql = 'SELECT COUNT(*)::bigint AS total FROM invima_api_catalog';

const explainSql = (sql) =>
  `EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, WAL, FORMAT JSON) ${sql.replace(/;\s*$/, '')}`;

const rowId = (row) => `${row.source_dataset_key}|${row.source_uid}`;

const normalizeValue = (value) => {
  if (value instanceof Date) return value.toISOString();
  if (value === undefined) return null;
  return value;
};

const publicShape = (row) =>
  Object.fromEntries(pageFields.map((field) => [field, normalizeValue(row[field])]));

const orderKey = (row) =>
  `${normalizeValue(row.fecha_vencimiento) || ''}|${row.producto || ''}|${row.source_dataset_key}|${row.source_uid}`;

const duplicates = (items) => {
  const seen = new Set();
  const dupes = new Set();
  for (const item of items) {
    if (seen.has(item)) dupes.add(item);
    seen.add(item);
  }
  return Array.from(dupes);
};

const diffPage = (beforePage, afterPage) => {
  const beforeIds = beforePage.rows.map(rowId);
  const afterIds = afterPage.rows.map(rowId);
  const beforeOrderKeys = beforePage.rows.map(orderKey);
  const afterOrderKeys = afterPage.rows.map(orderKey);
  const beforePublic = beforePage.rows.map(publicShape);
  const afterPublic = afterPage.rows.map(publicShape);
  const beforeSet = new Set(beforeIds);
  const afterSet = new Set(afterIds);
  const missing = beforeIds.filter((id) => !afterSet.has(id));
  const additional = afterIds.filter((id) => !beforeSet.has(id));
  const orderChanged =
    beforeIds.length !== afterIds.length || beforeIds.some((id, index) => id !== afterIds[index]);
  const orderKeysChanged =
    beforeOrderKeys.length !== afterOrderKeys.length ||
    beforeOrderKeys.some((key, index) => key !== afterOrderKeys[index]);
  const structureChanged =
    JSON.stringify(beforePublic.map((row) => Object.keys(row))) !==
    JSON.stringify(afterPublic.map((row) => Object.keys(row)));
  const publicValuesChanged = JSON.stringify(beforePublic) !== JSON.stringify(afterPublic);

  return {
    offset: beforePage.offset,
    limit: beforePage.limit,
    rowCountBefore: beforePage.rows.length,
    rowCountAfter: afterPage.rows.length,
    missingIds: missing,
    additionalIds: additional,
    duplicateIdsBefore: duplicates(beforeIds),
    duplicateIdsAfter: duplicates(afterIds),
    orderChanged,
    orderKeysChanged,
    idOrderChangedWithinTies: orderChanged && !orderKeysChanged,
    structureChanged,
    publicValuesChanged,
    valid:
      beforePage.rows.length === afterPage.rows.length &&
      missing.length === 0 &&
      additional.length === 0 &&
      duplicates(afterIds).length === 0 &&
      !orderKeysChanged &&
      !structureChanged &&
      !publicValuesChanged
  };
};

const capturePageSet = async (client, offsets = [0, 25, 250000, 1000000], limit = 25) => {
  const countResult = await client.query(countSql);
  const total = Number(countResult.rows[0]?.total || 0);
  const pages = [];

  for (const offset of offsets) {
    const result = await client.query(pageSelectSql, [limit, offset]);
    pages.push({
      limit,
      offset,
      ids: result.rows.map(rowId),
      orderKeys: result.rows.map(orderKey),
      fields: pageFields,
      rows: result.rows
    });
  }

  return {
    capturedAt: new Date().toISOString(),
    total,
    pages
  };
};

const compareCaptures = (before, after) => {
  const pageDiffs = before.pages.map((beforePage) => {
    const afterPage = after.pages.find((page) => page.offset === beforePage.offset);
    if (!afterPage) {
      return {
        offset: beforePage.offset,
        valid: false,
        missingAfterPage: true
      };
    }
    return diffPage(beforePage, afterPage);
  });

  return {
    totalBefore: before.total,
    totalAfter: after.total,
    totalMatches: before.total === after.total,
    pages: pageDiffs,
    valid: before.total === after.total && pageDiffs.every((page) => page.valid)
  };
};

const flattenPlans = (plan) => {
  const nodes = [];
  const visit = (node) => {
    if (!node) return;
    nodes.push(node);
    for (const child of node.Plans || []) visit(child);
  };
  visit(plan);
  return nodes;
};

const explainSummary = (doc) => {
  const plan = doc.Plan || {};
  const nodes = flattenPlans(plan);
  const sum = (field) => nodes.reduce((acc, item) => acc + Number(item[field] || 0), 0);
  const nodeTypes = Array.from(new Set(nodes.map((node) => node['Node Type']))).join(', ');
  return {
    planning_ms: Number(doc['Planning Time'] || 0),
    execution_ms: Number(doc['Execution Time'] || 0),
    node_types: nodeTypes,
    seq_scans: nodes.filter((node) => node['Node Type'] === 'Seq Scan').length,
    index_scans: nodes.filter((node) => node['Node Type'] === 'Index Scan').length,
    index_only_scans: nodes.filter((node) => node['Node Type'] === 'Index Only Scan').length,
    bitmap_heap_scans: nodes.filter((node) => node['Node Type'] === 'Bitmap Heap Scan').length,
    sorts: nodes.filter((node) => node['Node Type'] === 'Sort').length,
    incremental_sorts: nodes.filter((node) => node['Node Type'] === 'Incremental Sort').length,
    gathers: nodes.filter((node) => node['Node Type'] === 'Gather').length,
    gather_merges: nodes.filter((node) => node['Node Type'] === 'Gather Merge').length,
    estimated_rows: Number(plan['Plan Rows'] || 0),
    actual_rows: Number(plan['Actual Rows'] || 0),
    estimated_width: Number(plan['Plan Width'] || 0),
    rows_removed_by_filter: sum('Rows Removed by Filter'),
    shared_hit_blocks: sum('Shared Hit Blocks'),
    shared_read_blocks: sum('Shared Read Blocks'),
    temp_read_blocks: sum('Temp Read Blocks'),
    temp_written_blocks: sum('Temp Written Blocks'),
    wal_records: sum('WAL Records'),
    sort_methods: nodes
      .filter((node) => node['Sort Method'])
      .map((node) => `${node['Sort Method']} ${node['Sort Space Used'] || ''}${node['Sort Space Type'] || ''}`)
  };
};

const timedQuery = async (client, sql, params = []) => {
  const started = performance.now();
  const result = await client.query(sql, params);
  return {
    result,
    elapsed_ms: performance.now() - started
  };
};

const query02ResultFile = (name) => path.join(query02Dir, name);

module.exports = {
  capturePageSet,
  compareCaptures,
  config,
  countSql,
  createClient,
  ensureDir,
  explainSql,
  explainSummary,
  pageFields,
  pageSelectSql,
  query02Dir,
  query02ResultFile,
  query02Sql,
  readCsv,
  summarizeRows,
  timedQuery,
  writeCsv,
  writeJson
};
