const { fetchInvimaDataset, INVIMA_DATASETS } = require('../APIinvima');
const { invimaAdapters, adapterKeys, cleanText } = require('./adapters');

const clampInt = (value, fallback, min, max) => {
  const parsed = Number.parseInt(String(value), 10);
  if (Number.isNaN(parsed)) return fallback;
  return Math.max(min, Math.min(max, parsed));
};

const parseListParam = (value) => {
  if (Array.isArray(value)) {
    return value.map((item) => String(item).trim()).filter(Boolean);
  }
  if (typeof value !== 'string') return [];
  return value
    .split(',')
    .map((item) => item.trim())
    .filter(Boolean);
};

const removeAccents = (value) =>
  String(value || '')
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '');

const slugify = (value) => {
  const normalized = removeAccents(String(value || '').toLowerCase())
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '');
  return normalized || 'sin-categoria';
};

const toTitleCase = (value) =>
  String(value || '')
    .toLowerCase()
    .replace(/\b\w/g, (char) => char.toUpperCase())
    .trim();

const normalizeCategory = (value, fallback = 'Sin categoria') => {
  const base = cleanText(value) || fallback;
  const normalized = removeAccents(base).toUpperCase().replace(/\s+/g, ' ').trim();

  if (normalized.includes('DISPOSIT') || normalized.includes('TECNOLOG')) {
    return 'Dispositivos y otras tecnologias';
  }
  if (normalized.includes('MEDICO') && normalized.includes('QUIR')) return 'Medico quirurgicos';
  if (normalized.includes('MED') && normalized.includes('OFIC')) return 'Medicamentos oficinales';
  if (normalized.includes('MEDIC')) return 'Medicamentos';
  if (normalized.includes('RS') && normalized.includes('NSO')) return 'RS y NSO';
  if (normalized.includes('CUM')) return 'CUM vigentes';
  if (normalized.includes('HOMEOP')) return 'Medicamentos homeopaticos';
  if (normalized.includes('FITOTER')) return 'Productos fitoterapeuticos';
  if (normalized.includes('SUPLEMENT')) return 'Suplementos dietarios';
  if (normalized.includes('VACUN')) return 'Vacunas';
  if (normalized.includes('COSMET')) return 'Cosmeticos';
  if (normalized.includes('REACTIV') && normalized.includes('DIAG')) return 'Reactivos diagnostico';
  if (normalized.includes('BIOLOGIC')) return 'Biologicos';
  if (normalized.includes('ALIMENTO')) return 'Alimentos';
  if (normalized.includes('ASEO') || normalized.includes('LIMPIEZA')) return 'Aseo y limpieza';
  if (normalized.includes('ODONTO')) return 'Odontologicos';
  if (normalized.includes('BEBIDA') && normalized.includes('ALCOH')) return 'Bebidas alcoholicas';
  if (normalized.includes('PLAGUIC')) return 'Plaguicidas';

  return toTitleCase(base);
};

const buildSearchTokens = (value) =>
  removeAccents(String(value || '').toLowerCase())
    .split(/\s+/)
    .map((token) => token.replace(/[^a-z0-9]/g, ''))
    .filter((token) => token.length >= 3)
    .slice(0, 8);

const buildSearchText = (item) =>
  [
    item.source_dataset_name,
    item.categoria_linea,
    item.categoria_canonica,
    item.expediente,
    item.registro_sanitario,
    item.producto,
    item.titular,
    item.estado_registro,
    item.modalidad,
    item.grupo,
    item.marca,
    item.principio_activo,
    item.forma_farmaceutica,
    item.presentacion_comercial,
    item.atc,
    item.rol_nombre,
    item.rol_tipo,
    item.ciudad_titular,
    item.pais_titular,
    item.fabricante,
    item.importador,
    item.vigencia
  ]
    .map((value) => cleanText(value))
    .filter(Boolean)
    .join(' ');

const normalizeCatalogRecord = (item) => {
  const categoriaCanonica = normalizeCategory(
    item.categoria_canonica || item.categoria_linea || item.source_dataset_name || item.source_dataset_key
  );
  return {
    ...item,
    categoria_canonica: categoriaCanonica,
    categoria_slug: slugify(categoriaCanonica),
    search_text: buildSearchText({
      ...item,
      categoria_canonica: categoriaCanonica
    })
  };
};

const ensureInvimaTables = async (pool) => {
  const lockKey = 93477111;
  await pool.query('SELECT pg_advisory_lock($1);', [lockKey]);

  try {
    await pool.query(`
      CREATE TABLE IF NOT EXISTS invima_api_raw (
        id bigserial PRIMARY KEY,
        source_dataset_key text NOT NULL,
        source_dataset_id text NOT NULL,
        source_uid text NOT NULL,
        raw_payload jsonb NOT NULL,
        fetched_at timestamptz NOT NULL DEFAULT NOW(),
        created_at timestamptz NOT NULL DEFAULT NOW(),
        updated_at timestamptz NOT NULL DEFAULT NOW(),
        CONSTRAINT invima_api_raw_unique UNIQUE (source_dataset_key, source_uid)
      );
    `);

    await pool.query(`
      CREATE TABLE IF NOT EXISTS invima_api_catalog (
        source_dataset_key text NOT NULL,
        source_dataset_id text NOT NULL,
        source_uid text NOT NULL,
        source_dataset_name text NOT NULL,
        categoria_linea text NOT NULL,
        categoria_canonica text,
        categoria_slug text,
        expediente text,
        registro_sanitario text,
        producto text,
        titular text,
        estado_registro text,
        fecha_expedicion date,
        fecha_vencimiento date,
        modalidad text,
        grupo text,
        marca text,
        principio_activo text,
        forma_farmaceutica text,
        presentacion_comercial text,
        atc text,
        rol_nombre text,
        rol_tipo text,
        ciudad_titular text,
        pais_titular text,
        fabricante text,
        importador text,
        vigencia text,
        search_text text,
        extra jsonb NOT NULL,
        fetched_at timestamptz NOT NULL DEFAULT NOW(),
        updated_at timestamptz NOT NULL DEFAULT NOW(),
        CONSTRAINT invima_api_catalog_pk PRIMARY KEY (source_dataset_key, source_uid)
      );
    `);

    await pool.query('ALTER TABLE invima_api_catalog ADD COLUMN IF NOT EXISTS categoria_canonica text;');
    await pool.query('ALTER TABLE invima_api_catalog ADD COLUMN IF NOT EXISTS categoria_slug text;');
    await pool.query('ALTER TABLE invima_api_catalog ADD COLUMN IF NOT EXISTS search_text text;');

    let slugSourceExpression = "lower(COALESCE(NULLIF(categoria_canonica, ''), categoria_linea, 'Sin categoria'))";
    try {
      await pool.query('CREATE EXTENSION IF NOT EXISTS unaccent;');
      slugSourceExpression =
        "lower(unaccent(COALESCE(NULLIF(categoria_canonica, ''), categoria_linea, 'Sin categoria')))";
    } catch (error) {
      console.warn('No se pudo crear extension unaccent:', error.message);
    }

    await pool.query(`
      UPDATE invima_api_catalog
      SET
        categoria_canonica = COALESCE(NULLIF(categoria_canonica, ''), categoria_linea, 'Sin categoria'),
        categoria_slug = COALESCE(
          NULLIF(categoria_slug, ''),
          COALESCE(
            NULLIF(
              trim(BOTH '-' FROM regexp_replace(
                ${slugSourceExpression},
                '[^a-z0-9]+',
                '-',
                'g'
              )),
              ''
            ),
            'sin-categoria'
          )
        ),
        search_text = COALESCE(
          NULLIF(search_text, ''),
          trim(concat_ws(' ',
            source_dataset_name,
            categoria_linea,
            categoria_canonica,
            expediente,
            registro_sanitario,
            producto,
            titular,
            estado_registro,
            modalidad,
            grupo,
            marca,
            principio_activo,
            forma_farmaceutica,
            presentacion_comercial,
            atc,
            rol_nombre,
            rol_tipo,
            ciudad_titular,
            pais_titular,
            fabricante,
            importador,
            vigencia
          ))
        )
      WHERE
        categoria_canonica IS NULL OR categoria_canonica = '' OR
        categoria_slug IS NULL OR categoria_slug = '' OR
        search_text IS NULL OR search_text = '';
    `);

    await pool.query('DROP INDEX IF EXISTS idx_invima_api_catalog_producto;');
    await pool.query(
      'CREATE INDEX IF NOT EXISTS idx_invima_api_catalog_dataset ON invima_api_catalog (source_dataset_key);'
    );
    await pool.query(
      'CREATE INDEX IF NOT EXISTS idx_invima_api_catalog_categoria_slug ON invima_api_catalog (categoria_slug);'
    );
    await pool.query(
      'CREATE INDEX IF NOT EXISTS idx_invima_api_catalog_estado ON invima_api_catalog (estado_registro);'
    );
    await pool.query(
      'CREATE INDEX IF NOT EXISTS idx_invima_api_catalog_registro ON invima_api_catalog (registro_sanitario);'
    );
    await pool.query(
      'CREATE INDEX IF NOT EXISTS idx_invima_api_catalog_expediente ON invima_api_catalog (expediente);'
    );
    await pool.query(
      'CREATE INDEX IF NOT EXISTS idx_invima_api_catalog_modalidad ON invima_api_catalog (modalidad);'
    );
    await pool.query(
      'CREATE INDEX IF NOT EXISTS idx_invima_api_catalog_grupo ON invima_api_catalog (grupo);'
    );
    await pool.query(
      'CREATE INDEX IF NOT EXISTS idx_invima_api_catalog_vencimiento ON invima_api_catalog (fecha_vencimiento);'
    );
    await pool.query(
      'CREATE INDEX IF NOT EXISTS idx_invima_api_catalog_expedicion ON invima_api_catalog (fecha_expedicion);'
    );
    await pool.query(
      "CREATE INDEX IF NOT EXISTS idx_invima_api_catalog_search_fts ON invima_api_catalog USING GIN (to_tsvector('spanish', COALESCE(search_text, '')));"
    );

    try {
      await pool.query('CREATE EXTENSION IF NOT EXISTS pg_trgm;');
      await pool.query(`
        CREATE INDEX IF NOT EXISTS idx_invima_api_catalog_search_trgm
        ON invima_api_catalog USING GIN (search_text gin_trgm_ops);
      `);
    } catch (error) {
      console.warn('No se pudo crear indice trigram:', error.message);
    }
  } finally {
    await pool.query('SELECT pg_advisory_unlock($1);', [lockKey]);
  }
};

const buildUpsertQuery = (table, columns, rows, conflictColumns, updateColumns) => {
  if (!rows.length) return null;

  const values = [];
  const placeholders = rows.map((row, rowIndex) => {
    const offset = rowIndex * columns.length;
    row.forEach((value) => values.push(value));
    const rowPlaceholders = columns.map((_, colIndex) => `$${offset + colIndex + 1}`);
    return `(${rowPlaceholders.join(', ')})`;
  });

  const updateSet = updateColumns.map((column) => `${column} = EXCLUDED.${column}`).join(', ');

  return {
    query: `
      INSERT INTO ${table} (${columns.join(', ')})
      VALUES ${placeholders.join(', ')}
      ON CONFLICT (${conflictColumns.join(', ')}) DO UPDATE SET ${updateSet};
    `,
    values
  };
};

const chunkArray = (items, size) => {
  const chunks = [];
  for (let i = 0; i < items.length; i += size) {
    chunks.push(items.slice(i, i + size));
  }
  return chunks;
};

const dedupeBySourceUid = (rows) => {
  const unique = new Map();
  for (const row of rows) {
    if (!row || !row.source_uid) continue;
    unique.set(row.source_uid, row);
  }
  return Array.from(unique.values());
};

const persistBatch = async (pool, datasetKey, datasetId, normalizedRows) => {
  if (!normalizedRows.length) return 0;
  const uniqueRows = dedupeBySourceUid(normalizedRows).map((row) => normalizeCatalogRecord(row));
  if (!uniqueRows.length) return 0;

  const rawColumns = [
    'source_dataset_key',
    'source_dataset_id',
    'source_uid',
    'raw_payload',
    'fetched_at',
    'updated_at'
  ];

  const catalogColumns = [
    'source_dataset_key',
    'source_dataset_id',
    'source_uid',
    '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',
    'search_text',
    'extra',
    'fetched_at',
    'updated_at'
  ];

  const now = new Date();

  const rawRows = uniqueRows.map((item) => [
    datasetKey,
    datasetId,
    item.source_uid,
    item.extra,
    now,
    now
  ]);

  const catalogRows = uniqueRows.map((item) => [
    item.source_dataset_key,
    item.source_dataset_id,
    item.source_uid,
    item.source_dataset_name,
    item.categoria_linea,
    item.categoria_canonica,
    item.categoria_slug,
    item.expediente,
    item.registro_sanitario,
    item.producto,
    item.titular,
    item.estado_registro,
    item.fecha_expedicion,
    item.fecha_vencimiento,
    item.modalidad,
    item.grupo,
    item.marca,
    item.principio_activo,
    item.forma_farmaceutica,
    item.presentacion_comercial,
    item.atc,
    item.rol_nombre,
    item.rol_tipo,
    item.ciudad_titular,
    item.pais_titular,
    item.fabricante,
    item.importador,
    item.vigencia,
    item.search_text,
    item.extra,
    now,
    now
  ]);

  const rawUpsert = buildUpsertQuery(
    'invima_api_raw',
    rawColumns,
    rawRows,
    ['source_dataset_key', 'source_uid'],
    ['raw_payload', 'fetched_at', 'updated_at']
  );

  const catalogUpsert = buildUpsertQuery(
    'invima_api_catalog',
    catalogColumns,
    catalogRows,
    ['source_dataset_key', 'source_uid'],
    [
      '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',
      'search_text',
      'extra',
      'fetched_at',
      'updated_at'
    ]
  );

  await pool.query('BEGIN');
  try {
    if (rawUpsert) {
      await pool.query(rawUpsert.query, rawUpsert.values);
    }
    if (catalogUpsert) {
      await pool.query(catalogUpsert.query, catalogUpsert.values);
    }
    await pool.query('COMMIT');
  } catch (error) {
    await pool.query('ROLLBACK');
    throw error;
  }

  return uniqueRows.length;
};

const isRetryableSocrataError = (error) => {
  const text = String(error?.message || error || '').toLowerCase();
  return (
    text.includes(' 429 ') ||
    text.includes(' 500 ') ||
    text.includes(' 502 ') ||
    text.includes(' 503 ') ||
    text.includes(' 504 ') ||
    text.includes('fetch failed') ||
    text.includes('timeout') ||
    text.includes('internal error') ||
    text.includes('internal-error') ||
    text.includes('socket hang up')
  );
};

const syncOneDataset = async (pool, datasetKey, options = {}) => {
  const adapter = invimaAdapters[datasetKey];
  if (!adapter) {
    throw new Error(`Dataset adapter no soportado: ${datasetKey}`);
  }

  const pageSize = clampInt(options.pageSize, 1000, 100, 5000);
  const maxPages = options.maxPages ? clampInt(options.maxPages, 1, 1, 50000) : null;
  const where = cleanText(options.where);
  const select = cleanText(options.select);
  const requestedOrder = cleanText(options.order) || cleanText(adapter.defaultOrder);
  let currentOrder = requestedOrder;
  let currentPageSize = pageSize;

  let offset = 0;
  let page = 0;
  let totalFetched = 0;
  let totalPersisted = 0;
  const warnings = [];

  const emitProgress = (event) => {
    if (typeof options.onProgress === 'function') {
      options.onProgress({
        datasetKey,
        datasetId: adapter.id,
        ...event
      });
    }
  };

  while (true) {
    if (maxPages && page >= maxPages) break;

    const sodaParams = {
      $limit: currentPageSize,
      $offset: offset
    };

    if (select) sodaParams.$select = select;
    if (where) sodaParams.$where = where;
    if (currentOrder) sodaParams.$order = currentOrder;

    let rawRows = null;
    try {
      rawRows = await fetchInvimaDataset(datasetKey, sodaParams, options.appToken, {
        retries: clampInt(options.fetchRetries, 4, 0, 8),
        timeoutMs: clampInt(options.fetchTimeoutMs, 35000, 4000, 120000)
      });
    } catch (error) {
      const retryable = isRetryableSocrataError(error);
      const message = String(error?.message || error);

      if (retryable && currentOrder && currentOrder !== ':id') {
        currentOrder = ':id';
        warnings.push(
          `[${datasetKey}] Fallo con order "${requestedOrder || 'default'}" en offset ${offset}; reintentando con ":id".`
        );
        continue;
      }

      if (retryable && currentOrder === ':id') {
        currentOrder = '';
        warnings.push(`[${datasetKey}] Fallo con order ":id" en offset ${offset}; reintentando sin order.`);
        continue;
      }

      if (retryable && currentPageSize > 200) {
        const nextSize = Math.max(200, Math.floor(currentPageSize / 2));
        if (nextSize < currentPageSize) {
          warnings.push(
            `[${datasetKey}] Error en offset ${offset}; reduciendo pagina ${currentPageSize} -> ${nextSize}.`
          );
          currentPageSize = nextSize;
          continue;
        }
      }

      const wrapped = new Error(
        `Sincronizacion detenida en dataset ${datasetKey} (offset ${offset}, pagina ${page + 1}): ${message}`
      );
      wrapped.cause = error;
      throw wrapped;
    }

    if (!Array.isArray(rawRows) || rawRows.length === 0) break;

    const normalizedRows = rawRows
      .map((row) => adapter.normalize(row))
      .filter((row) => row && row.source_uid);

    const chunks = chunkArray(normalizedRows, 500);
    let pagePersisted = 0;
    for (const chunk of chunks) {
      const persistedCount = await persistBatch(pool, adapter.key, adapter.id, chunk);
      totalPersisted += persistedCount;
      pagePersisted += persistedCount;
    }

    totalFetched += rawRows.length;
    page += 1;
    offset += rawRows.length;
    emitProgress({
      type: 'page_persisted',
      fetched: totalFetched,
      persisted: totalPersisted,
      pages: page,
      pageFetched: rawRows.length,
      pagePersisted,
      offset,
      pageSizeUsed: currentPageSize,
      warnings: [...warnings]
    });
    if (rawRows.length < currentPageSize) break;
  }

  return {
    datasetKey,
    datasetId: adapter.id,
    fetched: totalFetched,
    persisted: totalPersisted,
    pages: page,
    pageSizeUsed: currentPageSize,
    orderUsed: currentOrder || null,
    warnings
  };
};

const syncInvimaDatasets = async (pool, options = {}) => {
  const keys = getSyncDatasetKeys(options.datasets);
  const concurrency = clampInt(options.concurrency, 3, 1, 8);
  const emitProgress = (event) => {
    if (typeof options.onProgress === 'function') {
      options.onProgress(event);
    }
  };

  if (!keys.length) {
    throw new Error('No hay datasets validos para sincronizar.');
  }

  const queue = [...keys];
  const resultMap = new Map();
  const failed = [];
  const workerCount = Math.min(concurrency, keys.length);

  emitProgress({ type: 'sync_started', keys, concurrency: workerCount });

  const worker = async () => {
    while (queue.length) {
      const key = queue.shift();
      if (!key) return;
      try {
        emitProgress({ type: 'dataset_started', datasetKey: key });
        const result = await syncOneDataset(pool, key, {
          pageSize: options.pageSize,
          maxPages: options.maxPages,
          appToken: options.appToken,
          where: options.where,
          select: options.select,
          order: options.order,
          fetchRetries: options.fetchRetries,
          fetchTimeoutMs: options.fetchTimeoutMs,
          onProgress: options.onProgress
        });
        resultMap.set(key, result);
        emitProgress({ type: 'dataset_completed', ...result });
      } catch (error) {
        const failure = {
          datasetKey: key,
          error: String(error?.message || error)
        };
        failed.push(failure);
        emitProgress({ type: 'dataset_failed', ...failure });
      }
    }
  };

  await Promise.all(Array.from({ length: workerCount }, () => worker()));
  const results = keys.map((key) => resultMap.get(key)).filter(Boolean);

  const summary = results.reduce(
    (acc, item) => {
      acc.datasets += 1;
      acc.fetched += item.fetched;
      acc.persisted += item.persisted;
      acc.pages += item.pages;
      return acc;
    },
    { datasets: 0, fetched: 0, persisted: 0, pages: 0, failed: failed.length, requested: keys.length }
  );

  return {
    summary,
    datasets: results,
    failed
  };
};

const buildCatalogWhere = (filters, params, options = {}) => {
  const where = [];
  let searchQueryParamIndex = null;
  const searchMode = options.searchMode || 'primary';

  if (filters.q) {
    if (searchMode === 'fallback_like') {
      const tokens = filters.searchTokens.length ? filters.searchTokens : [filters.q];
      const tokenClauses = tokens.slice(0, 8).map((token) => {
        params.push(`%${token}%`);
        const likeIdx = params.length;
        return `(
          COALESCE(search_text, '') ILIKE $${likeIdx}
          OR COALESCE(registro_sanitario, '') ILIKE $${likeIdx}
          OR COALESCE(expediente, '') ILIKE $${likeIdx}
          OR COALESCE(producto, '') ILIKE $${likeIdx}
          OR COALESCE(categoria_canonica, '') ILIKE $${likeIdx}
          OR COALESCE(categoria_linea, '') ILIKE $${likeIdx}
        )`;
      });
      where.push(`(${tokenClauses.join(' AND ')})`);
    } else if (filters.useFts) {
      params.push(filters.q);
      searchQueryParamIndex = params.length;
      params.push(`%${filters.q}%`);
      const likeIdx = params.length;
      where.push(
        `(
          to_tsvector('spanish', COALESCE(search_text, '')) @@ websearch_to_tsquery('spanish', $${searchQueryParamIndex})
          OR COALESCE(registro_sanitario, '') ILIKE $${likeIdx}
          OR COALESCE(expediente, '') ILIKE $${likeIdx}
          OR COALESCE(producto, '') ILIKE $${likeIdx}
          OR COALESCE(categoria_canonica, '') ILIKE $${likeIdx}
          OR COALESCE(categoria_linea, '') ILIKE $${likeIdx}
        )`
      );
    } else {
      params.push(`%${filters.q}%`);
      const likeIdx = params.length;
      where.push(`(
        COALESCE(search_text, '') ILIKE $${likeIdx}
        OR COALESCE(registro_sanitario, '') ILIKE $${likeIdx}
        OR COALESCE(expediente, '') ILIKE $${likeIdx}
      )`);
    }
  }

  if (filters.categorias.length) {
    params.push(filters.categorias);
    where.push(`categoria_slug = ANY($${params.length}::text[])`);
  }

  if (filters.datasets.length) {
    params.push(filters.datasets);
    where.push(`source_dataset_key = ANY($${params.length}::text[])`);
  }

  if (filters.estados.length) {
    params.push(filters.estados);
    where.push(`estado_registro = ANY($${params.length}::text[])`);
  }

  if (filters.modalidades.length) {
    params.push(filters.modalidades);
    where.push(`modalidad = ANY($${params.length}::text[])`);
  }

  if (filters.grupos.length) {
    params.push(filters.grupos);
    where.push(`grupo = ANY($${params.length}::text[])`);
  }

  if (filters.fechaVencDesde) {
    params.push(filters.fechaVencDesde);
    where.push(`fecha_vencimiento >= $${params.length}::date`);
  }

  if (filters.fechaVencHasta) {
    params.push(filters.fechaVencHasta);
    where.push(`fecha_vencimiento <= $${params.length}::date`);
  }

  if (filters.fechaExpDesde) {
    params.push(filters.fechaExpDesde);
    where.push(`fecha_expedicion >= $${params.length}::date`);
  }

  if (filters.fechaExpHasta) {
    params.push(filters.fechaExpHasta);
    where.push(`fecha_expedicion <= $${params.length}::date`);
  }

  if (filters.onlyVigentes) {
    where.push(`(
      (COALESCE(estado_registro, '') ILIKE '%vigente%' OR COALESCE(vigencia, '') ILIKE '%vigente%')
      AND COALESCE(estado_registro, '') NOT ILIKE '%no vigente%'
      AND COALESCE(vigencia, '') NOT ILIKE '%no vigente%'
    )`);
  }

  return {
    whereSql: where.length ? `WHERE ${where.join(' AND ')}` : '',
    searchQueryParamIndex
  };
};

const parseCatalogFilters = (query = {}) => {
  const parseDate = (value) => {
    const text = cleanText(value);
    if (!text) return null;
    return /^\d{4}-\d{2}-\d{2}$/.test(text) ? text : null;
  };

  const q = cleanText(query.q) || '';
  const searchTokens = buildSearchTokens(q);
  const useFts = q.length >= 3;
  const queryTokenCount = q ? q.split(/\s+/).filter(Boolean).length : 0;
  const useRank = useFts && queryTokenCount > 1;
  const categorias = parseListParam(query.categorias).map((item) => slugify(item)).slice(0, 30);
  const datasets = parseListParam(query.datasets).filter((key) => invimaAdapters[key]);
  const estados = parseListParam(query.estados).slice(0, 30);
  const modalidades = parseListParam(query.modalidades).slice(0, 30);
  const grupos = parseListParam(query.grupos).slice(0, 30);
  const limit = clampInt(query.limit, 25, 1, 500);
  const offset = clampInt(query.offset, 0, 0, 2_000_000);
  const onlyVigentes = String(query.onlyVigentes || '').toLowerCase() === 'true';
  const fechaVencDesde = parseDate(query.fechaVencDesde);
  const fechaVencHasta = parseDate(query.fechaVencHasta);
  const fechaExpDesde = parseDate(query.fechaExpDesde);
  const fechaExpHasta = parseDate(query.fechaExpHasta);

  return {
    q,
    searchTokens,
    useFts,
    useRank,
    categorias,
    datasets,
    estados,
    modalidades,
    grupos,
    fechaVencDesde,
    fechaVencHasta,
    fechaExpDesde,
    fechaExpHasta,
    limit,
    offset,
    onlyVigentes
  };
};

const catalogSelectSql = `
  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 runCatalogQuery = async (pool, filters, options = {}) => {
  const params = [];
  const searchMode = options.searchMode || 'primary';
  const { whereSql, searchQueryParamIndex } = buildCatalogWhere(filters, params, { searchMode });

  const countSql = `SELECT COUNT(*)::int AS total FROM invima_api_catalog ${whereSql};`;
  const countResult = await pool.query(countSql, params);
  const total = countResult.rows[0]?.total || 0;

  const dataParams = [...params, filters.limit, filters.offset];
  const limitIndex = params.length + 1;
  const offsetIndex = params.length + 2;
  const hasSearch =
    searchMode === 'primary' && Boolean(filters.q) && filters.useRank && searchQueryParamIndex !== null;

  const rankSql = hasSearch
    ? `
      ts_rank(
        to_tsvector('spanish', COALESCE(search_text, '')),
        websearch_to_tsquery('spanish', $${searchQueryParamIndex})
      ) AS score,
    `
    : '';

  const orderSql = hasSearch
    ? 'ORDER BY score DESC NULLS LAST, fecha_vencimiento DESC NULLS LAST, producto ASC NULLS LAST, source_dataset_key ASC, source_uid ASC'
    : 'ORDER BY fecha_vencimiento DESC NULLS LAST, producto ASC NULLS LAST, source_dataset_key ASC, source_uid ASC';

  const dataSql = filters.q
    ? `
      WITH matched AS MATERIALIZED (
        SELECT
          ${rankSql}
          ${catalogSelectSql},
          source_uid
        FROM invima_api_catalog
        ${whereSql}
      )
      SELECT
        ${hasSearch ? 'score,' : ''}
        ${catalogSelectSql}
      FROM matched
      ${orderSql}
      LIMIT $${limitIndex} OFFSET $${offsetIndex};
    `
    : `
      SELECT
        ${catalogSelectSql}
      FROM invima_api_catalog
      ${whereSql}
      ${orderSql}
      LIMIT $${limitIndex} OFFSET $${offsetIndex};
    `;

  const { rows } = await pool.query(dataSql, dataParams);

  return {
    items: rows,
    total,
    limit: filters.limit,
    offset: filters.offset
  };
};

const getInvimaCatalog = async (pool, query = {}) => {
  const filters = parseCatalogFilters(query);
  const primary = await runCatalogQuery(pool, filters, { searchMode: 'primary' });

  if (!filters.q || primary.total > 0 || filters.searchTokens.length <= 1) {
    return primary;
  }

  return runCatalogQuery(pool, filters, { searchMode: 'fallback_like' });
};

const getInvimaCatalogForExport = async (pool, query = {}) => {
  const filters = parseCatalogFilters(query);
  const exportLimit = clampInt(query.exportLimit, 50000, 1, 1_000_000);

  const params = [];
  const { whereSql } = buildCatalogWhere(filters, params);

  params.push(exportLimit);
  const limitIndex = params.length;

  const sql = `
    SELECT
      ${catalogSelectSql}
    FROM invima_api_catalog
    ${whereSql}
    ORDER BY fecha_vencimiento DESC NULLS LAST, producto ASC NULLS LAST, source_dataset_key ASC, source_uid ASC
    LIMIT $${limitIndex};
  `;

  const { rows } = await pool.query(sql, params);
  return rows;
};

const getInvimaCatalogForExportChunk = async (pool, query = {}) => {
  const filters = parseCatalogFilters(query);
  const chunkLimit = clampInt(query.chunkLimit, 5000, 100, 20000);
  const chunkOffset = clampInt(query.chunkOffset, 0, 0, 10_000_000);

  const params = [];
  const { whereSql } = buildCatalogWhere(filters, params);
  params.push(chunkLimit, chunkOffset);
  const limitIndex = params.length - 1;
  const offsetIndex = params.length;

  const sql = `
    SELECT
      ${catalogSelectSql}
    FROM invima_api_catalog
    ${whereSql}
    ORDER BY fecha_vencimiento DESC NULLS LAST, producto ASC NULLS LAST, source_dataset_key ASC, source_uid ASC
    LIMIT $${limitIndex} OFFSET $${offsetIndex};
  `;

  const { rows } = await pool.query(sql, params);
  return rows;
};

const listInvimaCategories = async (pool) => {
  const { rows } = await pool.query(`
    SELECT
      COALESCE(NULLIF(categoria_slug, ''), 'sin-categoria') AS slug,
      MIN(COALESCE(NULLIF(categoria_canonica, ''), 'Sin categoria')) AS categoria,
      COUNT(*)::int AS total
    FROM invima_api_catalog
    GROUP BY 1
    ORDER BY total DESC, categoria ASC;
  `);
  return rows;
};

const listInvimaFilterOptions = async (pool) => {
  const [estadosResult, modalidadesResult, gruposResult] = await Promise.all([
    pool.query(`
      SELECT estado_registro AS value, COUNT(*)::int AS total
      FROM invima_api_catalog
      WHERE estado_registro IS NOT NULL AND estado_registro <> ''
      GROUP BY estado_registro
      ORDER BY total DESC, estado_registro ASC
      LIMIT 200;
    `),
    pool.query(`
      SELECT modalidad AS value, COUNT(*)::int AS total
      FROM invima_api_catalog
      WHERE modalidad IS NOT NULL AND modalidad <> ''
      GROUP BY modalidad
      ORDER BY total DESC, modalidad ASC
      LIMIT 200;
    `),
    pool.query(`
      SELECT grupo AS value, COUNT(*)::int AS total
      FROM invima_api_catalog
      WHERE grupo IS NOT NULL AND grupo <> ''
      GROUP BY grupo
      ORDER BY total DESC, grupo ASC
      LIMIT 200;
    `)
  ]);

  return {
    estados: estadosResult.rows,
    modalidades: modalidadesResult.rows,
    grupos: gruposResult.rows
  };
};

const listInvimaDatasets = () =>
  Object.values(INVIMA_DATASETS).map((dataset) => ({
    key: dataset.key,
    id: dataset.id,
    name: dataset.name,
    defaultCategory: dataset.defaultCategory
  }));

const getSyncDatasetKeys = (datasets) => {
  const requested = parseListParam(datasets);
  return requested.length ? requested.filter((key) => invimaAdapters[key]) : adapterKeys;
};

const getInvimaSyncStats = async (pool, options = {}) => {
  const selectedKeys = parseListParam(options.datasets).filter((key) => invimaAdapters[key]);

  const aggregateSql = `
    SELECT
      COUNT(*)::bigint AS catalog_rows,
      COUNT(DISTINCT categoria_slug)::int AS categories,
      COUNT(DISTINCT source_dataset_key)::int AS datasets_with_data,
      MAX(updated_at) AS max_updated_at,
      COUNT(*) FILTER (
        WHERE
          (COALESCE(estado_registro, '') ILIKE '%vigente%' OR COALESCE(vigencia, '') ILIKE '%vigente%')
          AND COALESCE(estado_registro, '') NOT ILIKE '%no vigente%'
          AND COALESCE(vigencia, '') NOT ILIKE '%no vigente%'
      )::bigint AS vigentes,
      COUNT(*) FILTER (
        WHERE COALESCE(estado_registro, '') ILIKE '%vencido%' OR COALESCE(vigencia, '') ILIKE '%vencido%'
      )::bigint AS vencidos
    FROM invima_api_catalog;
  `;

  const rawSql = `SELECT COUNT(*)::bigint AS raw_rows FROM invima_api_raw;`;

  const byDatasetSql = selectedKeys.length
    ? `
      SELECT source_dataset_key AS key, COUNT(*)::bigint AS total
      FROM invima_api_catalog
      WHERE source_dataset_key = ANY($1::text[])
      GROUP BY 1
      ORDER BY total DESC;
    `
    : `
      SELECT source_dataset_key AS key, COUNT(*)::bigint AS total
      FROM invima_api_catalog
      GROUP BY 1
      ORDER BY total DESC;
    `;

  const byDatasetParams = selectedKeys.length ? [selectedKeys] : [];

  const [aggregateResult, rawResult, byDatasetResult] = await Promise.all([
    pool.query(aggregateSql),
    pool.query(rawSql),
    pool.query(byDatasetSql, byDatasetParams)
  ]);

  const aggregate = aggregateResult.rows[0] || {};
  const datasetMap = new Map(
    byDatasetResult.rows.map((row) => [String(row.key), Number(row.total || 0)])
  );

  const configuredDatasets = listInvimaDatasets();
  const datasetRows = configuredDatasets
    .filter((dataset) => !selectedKeys.length || selectedKeys.includes(dataset.key))
    .map((dataset) => ({
      key: dataset.key,
      id: dataset.id,
      name: dataset.name,
      localRows: datasetMap.get(dataset.key) || 0,
      selected: selectedKeys.length ? selectedKeys.includes(dataset.key) : true
    }));

  return {
    at: new Date().toISOString(),
    totals: {
      catalogRows: Number(aggregate.catalog_rows || 0),
      rawRows: Number(rawResult.rows[0]?.raw_rows || 0),
      categories: Number(aggregate.categories || 0),
      datasetsWithData: Number(aggregate.datasets_with_data || 0),
      vigentes: Number(aggregate.vigentes || 0),
      vencidos: Number(aggregate.vencidos || 0),
      maxUpdatedAt: aggregate.max_updated_at || null
    },
    datasets: datasetRows
  };
};

const toCsv = (rows) => {
  if (!rows.length) return '';

  const headers = Object.keys(rows[0]);
  const escapeCsvValue = (value) => {
    if (value === null || value === undefined) return '';
    const text = String(value);
    if (text.includes('"') || text.includes(',') || text.includes('\n')) {
      return `"${text.replace(/"/g, '""')}"`;
    }
    return text;
  };

  const lines = [headers.join(',')];
  for (const row of rows) {
    lines.push(headers.map((header) => escapeCsvValue(row[header])).join(','));
  }
  return lines.join('\n');
};

module.exports = {
  ensureInvimaTables,
  syncInvimaDatasets,
  getInvimaCatalog,
  getInvimaCatalogForExport,
  getInvimaCatalogForExportChunk,
  listInvimaCategories,
  listInvimaFilterOptions,
  listInvimaDatasets,
  getInvimaSyncStats,
  toCsv,
  parseCatalogFilters,
  parseListParam,
  getSyncDatasetKeys,
  clampInt,
  normalizeCategory,
  slugify
};


