/* eslint-disable no-continue */
/* eslint-disable no-await-in-loop */
import path from 'path';

import { QueryTypes, Op } from 'sequelize';

import logger from '../logger';
import { ArchiveMetadata } from '../../store/models/archive-metadata';
import { listArchiveFiles, openArchiveSequelize } from './store';

type ArchiveQueryParams = {
  table: string;
  id?: string;
  customer_id?: string;
  from: number;
  to?: number;
  page: number;
  limit: number;
};

type ArchiveQueryResult = {
  data: any[];
  total: number;
  archiveFiles: string[];
};

function buildWhereClause(params: ArchiveQueryParams) {
  const conditions: string[] = [];
  const replacements: Record<string, any> = {};

  // Filter by data's original created_at, not archived_at
  // Users query for "2024 invoices", not "invoices archived at some time"
  const fromDate = new Date(params.from * 1000);
  const toDate = new Date((params.to ?? Math.floor(Date.now() / 1000)) * 1000);

  conditions.push('created_at >= :from');
  conditions.push('created_at <= :to');
  replacements.from = fromDate;
  replacements.to = toDate;

  if (params.id) {
    conditions.push('id = :id');
    replacements.id = params.id;
  }

  if (params.customer_id) {
    if (params.table === 'meter_events') {
      conditions.push("json_extract(payload, '$.customer_id') = :customerId");
    } else {
      conditions.push('customer_id = :customerId');
    }
    replacements.customerId = params.customer_id;
  }

  return { clause: conditions.join(' AND '), replacements };
}

async function recordArchiveQuery(fileNames: string[], actorId?: string) {
  const uniqueFiles = Array.from(new Set(fileNames));
  // Find all metadata records that contain any of the queried files
  // archive_file may contain comma-separated file names like "archive-2024.db,archive-2025.db"
  const updatedIds = new Set<string>();

  for (const fileName of uniqueFiles) {
    const metadataList = await ArchiveMetadata.findAll({
      where: { archive_file: { [Op.like]: `%${fileName}%` } },
    });
    for (const metadata of metadataList) {
      // Avoid updating the same metadata multiple times in one query
      if (updatedIds.has(metadata.id)) {
        continue;
      }
      updatedIds.add(metadata.id);
      const currentActors = metadata.query_actor_ids || [];
      const actorIds = actorId ? Array.from(new Set([...currentActors, actorId])) : currentActors;
      await metadata.update({
        query_count: (metadata.query_count || 0) + 1,
        query_actor_ids: actorIds,
        last_queried_at: Math.floor(Date.now() / 1000),
      });
    }
  }
}

const VALID_TABLE_NAME = /^[a-z][a-z0-9_]*$/;

export async function queryArchive(params: ArchiveQueryParams, actorId?: string): Promise<ArchiveQueryResult> {
  if (!VALID_TABLE_NAME.test(params.table)) {
    throw new Error(`Invalid table name: ${params.table}`);
  }
  const archiveFiles = listArchiveFiles();
  const { clause, replacements } = buildWhereClause(params);
  const results: any[] = [];
  const touchedFiles: string[] = [];

  for (const filePath of archiveFiles) {
    const archiveSequelize = openArchiveSequelize(filePath);
    try {
      // Check if table exists using PRAGMA instead of describeTable
      // (describeTable has a Sequelize SQLite bug: "Cannot set properties of undefined (setting 'unique')")
      const [tables] = await archiveSequelize.query(
        "SELECT name FROM sqlite_master WHERE type='table' AND name=:tableName",
        { replacements: { tableName: params.table } }
      );
      if (!Array.isArray(tables) || tables.length === 0) {
        // Table doesn't exist in this archive file, skip to next file
        // Note: don't close here, finally block will handle it
        continue;
      }

      const sql = `SELECT * FROM "${params.table}" WHERE ${clause} ORDER BY created_at DESC`;
      const rows = (await archiveSequelize.query(sql, {
        replacements,
        type: QueryTypes.SELECT,
      })) as any[];
      if (Array.isArray(rows) && rows.length > 0) {
        results.push(...rows);
        touchedFiles.push(path.basename(filePath));
      }
    } catch (error) {
      logger.warn('archive query failed', { filePath, error });
    } finally {
      await archiveSequelize.close();
    }
  }

  results.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
  const total = results.length;
  const start = (params.page - 1) * params.limit;
  const end = start + params.limit;
  const data = results.slice(start, end);

  await recordArchiveQuery(touchedFiles, actorId);

  return { data, total, archiveFiles: touchedFiles };
}
