import Database from 'better-sqlite3';
import crypto from 'crypto';
import fs, { createReadStream } from 'fs';
import fsPromises from 'fs/promises';
import path from 'path';
import { parentPort } from 'worker_threads';

interface WorkerMessage {
  type: string;
  dataset: string;
  destination: string;
  dbPath: string;
  reindex?: boolean;
  batchSize?: number;
}

interface IndexResult {
  indexed: number;
  skipped: number;
  errors: number;
}

function walkFiles(root: string): string[] {
  const pending = [root];
  const files: string[] = [];

  while (pending.length) {
    const current = pending.pop();
    if (!current) continue;

    let stat: fs.Stats;
    try {
      stat = fs.statSync(current);
    } catch {
      continue;
    }

    if (stat.isDirectory()) {
      const children = fs.readdirSync(current);
      for (const child of children) {
        pending.push(path.join(current, child));
      }
    } else if (stat.isFile()) {
      files.push(current);
    }
  }

  return files;
}

async function hashFileAsync(filePath: string): Promise<string | null> {
  return new Promise((resolve) => {
    const hash = crypto.createHash('sha1');
    const stream = createReadStream(filePath, { highWaterMark: 64 * 1024 }); // 64KB chunks

    stream.on('data', (chunk) => hash.update(chunk));
    stream.on('end', () => resolve(hash.digest('hex')));
    stream.on('error', (error) => {
      console.warn(`Worker: Hashing failed for ${filePath}: ${error}`);
      resolve(null);
    });
  });
}

async function indexDestination(
  dataset: string,
  destination: string,
  dbPath: string,
  reindex = false,
  batchSize = 100,
): Promise<IndexResult> {
  console.log(`Worker: Starting indexing for ${dataset} at ${destination}`);
  console.log(`Worker: Database path: ${dbPath}`);
  console.log(`Worker: Reindex: ${reindex}, Batch size: ${batchSize}`);

  let db: Database.Database | null = null;
  try {
    db = new Database(dbPath);
    // Ensure we're using synchronous mode for reliability
    db.pragma('journal_mode = WAL');
    db.pragma('synchronous = NORMAL');

    console.log(`Worker: Database opened successfully at ${dbPath}`);
    if (reindex) {
      const stmt = db.prepare(
        'DELETE FROM files WHERE dataset = ? AND destination_path LIKE ?',
      );
      const result = stmt.run(dataset, `${destination}%`);
      console.log(
        `Worker: Cleared ${result.changes} existing destination file entries for reindexing`,
      );
    }

    // Walk the destination directory
    console.log(`Worker: Scanning directory: ${destination}`);
    const files = walkFiles(destination);
    console.log(`Worker: Found ${files.length} files to index`);

    // Prepare statement to check if file is already indexed
    const checkIndexedStmt = db.prepare(
      'SELECT hash, file_size FROM files WHERE dataset = ? AND input = ? AND hash IS NOT NULL AND file_size IS NOT NULL',
    );

    let indexed = 0;
    let skipped = 0;
    let errors = 0;

    // Prepare statement for inserting/updating files
    const upsertStmt = db.prepare(`
      INSERT INTO files (dataset, input, destination_path, hash, file_size, date)
      VALUES (?, ?, ?, ?, ?, datetime('now'))
      ON CONFLICT(dataset, input) DO UPDATE SET
        destination_path = excluded.destination_path,
        hash = excluded.hash,
        file_size = excluded.file_size,
        date = excluded.date
    `);

    // Also prepare an insert-only statement for files not yet in the database
    const insertStmt = db.prepare(`
      INSERT OR IGNORE INTO files (dataset, input, destination_path, hash, file_size, date)
      VALUES (?, ?, ?, ?, ?, datetime('now'))
    `);

    // Process files in batches
    for (let i = 0; i < files.length; i += batchSize) {
      const batch = files.slice(i, i + batchSize);
      const progressPercent = Math.round((i / files.length) * 100);

      console.log(
        `Worker: Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(files.length / batchSize)} (${progressPercent}% complete, ${indexed} indexed, ${skipped} skipped, ${errors} errors)`,
      );

      // Send progress update
      parentPort?.postMessage({
        type: 'progress',
        indexed,
        skipped,
        errors,
        total: files.length,
        progress: progressPercent,
      });

      await Promise.all(
        batch.map(async (filePath) => {
          try {
            const stat = await fsPromises.stat(filePath);
            if (!stat.isFile()) {
              skipped++;
              return;
            }

            // Skip already-indexed files during batch indexing (unless reindexing)
            if (!reindex) {
              const existing = checkIndexedStmt.get(dataset, filePath) as
                | { hash: string; file_size: number }
                | undefined;
              if (existing && existing.hash && existing.file_size) {
                console.log(
                  `Worker: Skipping already-indexed file: ${filePath}`,
                );
                skipped++;
                return;
              }
            }

            console.log(`Worker: Indexing ${filePath}`);

            // Hash the file (for now still hash the input file)
            // TODO: When output file support is added, hash output instead
            const hash = await hashFileAsync(filePath);
            if (!hash) {
              console.warn(`Worker: Failed to hash ${filePath}`);
              errors++;
              return;
            }

            // Store in database - try upsert first for existing files, then insert-only for new files
            try {
              console.log(
                `Worker: Attempting to insert/update: dataset=${dataset}, input=${filePath}, hash=${hash}, size=${stat.size}`,
              );
              const result = upsertStmt.run(
                dataset,
                filePath,
                filePath,
                hash,
                stat.size,
              );
              console.log(`Worker: Upsert result: changes=${result.changes}`);
              if (result.changes === 0) {
                // File wasn't in database, try insert
                console.log(
                  `Worker: Upsert returned 0 changes, trying insert...`,
                );
                const insertResult = insertStmt.run(
                  dataset,
                  filePath,
                  filePath,
                  hash,
                  stat.size,
                );
                console.log(
                  `Worker: Insert result: changes=${insertResult.changes}`,
                );
              }
            } catch (dbError) {
              console.error(
                `Worker: Database error for ${filePath}: ${dbError}`,
              );
              errors++;
              return;
            }
            indexed++;

            // Log every 50 files
            if (indexed % 50 === 0) {
              console.log(
                `Worker: Indexed ${indexed}/${files.length} files (${Math.round((indexed / files.length) * 100)}%)`,
              );
            }
          } catch (error) {
            console.error(`Worker: Failed to index file ${filePath}: ${error}`);
            errors++;
          }
        }),
      );
    }

    console.log(
      `Worker: Indexing complete - ${indexed} indexed, ${skipped} skipped, ${errors} errors`,
    );

    return { indexed, skipped, errors };
  } catch (error) {
    console.error(`Worker: Fatal error during indexing: ${error}`);
    throw error;
  } finally {
    if (db) {
      db.close();
    }
  }
}

parentPort?.on('message', async (message: WorkerMessage) => {
  if (message.type === 'index_destination') {
    try {
      const result = await indexDestination(
        message.dataset,
        message.destination,
        message.dbPath,
        message.reindex,
        message.batchSize,
      );

      parentPort?.postMessage({
        type: 'index_result',
        dataset: message.dataset,
        destination: message.destination,
        ...result,
      });
    } catch (error) {
      parentPort?.postMessage({
        type: 'error',
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }
});
