import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import crypto from 'crypto';
import fs from 'fs';
import fsPromises from 'fs/promises';
import path from 'path';
import { Worker } from 'worker_threads';
import { DatasetsService } from './datasets.service';
import { DbService } from './db.service';

@Injectable()
export class MaintenanceService {
  private logger = new Logger('MaintenanceService');

  constructor(
    private readonly db: DbService,
    private readonly datasetsService: DatasetsService,
  ) {}

  cleanup(file: string, dirs: string[]) {
    for (let i = 0, l = dirs.length; i < l; i++) {
      const dir = dirs[i];
      if (file && dir && file.indexOf(dir) > -1) {
        const dataset = dir.replace(/.*\/(.*)/, '$1');
        const exists = fs.existsSync(file);
        if (!exists) this.db.removeFile(dataset, file, true);
      }
    }
  }

  purge(
    dirs: string[],
    dayMs = 24 * 60 * 60 * 1000,
    cleanerMs = 60 * 60 * 1000,
  ) {
    const ago = new Date(Date.now() - dayMs);
    this.logger.log(
      `Checking for "deleted" records older than ${ago.toISOString()}`,
    );
    for (let i = 0, l = dirs.length; i < l; i++) {
      const dir = dirs[i];
      const dataset = dir.replace(/.*\/(.*)/, '$1');
      const files = this.db.getDeletedOlderThan(dataset, ago.toISOString());
      for (const file of files as { input: string }[]) {
        this.logger.log(
          `Purging "${file.input}" (${new Date().toISOString()})`,
        );
        if (file && file.input) this.db.removeFile(dataset, file.input, false);
      }
    }
    setTimeout(() => {
      this.purge(dirs, dayMs, cleanerMs);
    }, cleanerMs);
  }

  prune(dirs: string[]) {
    this.logger.log('Checking for any files that no longer exist on disk.');
    for (let i = 0, l = dirs.length; i < l; i++) {
      const dir = dirs[i];
      const dataset = dir.replace(/.*\/(.*)/, '$1');

      // Get all files for this dataset (not just successful ones)
      const allFiles = this.db.getAllFiles(dataset);
      for (const file of allFiles as { input: string; status: string }[]) {
        const exists = fs.existsSync(file.input);
        if (!exists) {
          // Only soft delete if not already marked as deleted
          // Note: Database schema changes must be done via migrations in data/migrations/
          if (file.status !== 'deleted') {
            this.logger.log(
              `Marking missing file as deleted: "${file.input}" (${new Date().toISOString()})`,
            );
            this.db.removeFile(dataset, file.input, true); // soft delete
          }
        }
      }
    }
  }

  // Scheduled task cleanup - runs periodically to prevent task table from growing too large
  @Cron(CronExpression.EVERY_DAY_AT_2AM) // Run daily at 2 AM
  scheduledTaskCleanup() {
    this.logger.log('Running scheduled task cleanup');

    try {
      // Archive completed tasks older than 30 days
      const archiveResult = this.db.archiveOldTasks(30);
      this.logger.log(`Archived ${archiveResult?.changes || 0} old tasks`);

      // Delete failed tasks older than 7 days
      const failedResult = this.db.deleteTasksByStatus('failed', 7);
      this.logger.log(
        `Deleted ${failedResult.changes} failed tasks older than 7 days`,
      );

      // Delete skipped tasks older than 7 days
      const skippedResult = this.db.deleteTasksByStatus('skipped', 7);
      this.logger.log(
        `Deleted ${skippedResult.changes} skipped tasks older than 7 days`,
      );

      // Keep completed tasks for 90 days, then archive them
      const completedResult = this.db.deleteTasksByStatus('completed', 90);
      this.logger.log(
        `Deleted ${completedResult.changes} completed tasks older than 90 days`,
      );
    } catch (error) {
      this.logger.error(
        `Error during scheduled task cleanup: ${error.message}`,
      );
    }
  }

  /**
   * Scan destination folders (as defined in settings.datasets) for duplicate files.
   * Duplicates are recorded in the duplicate_files table for manual review.
   */
  async findDuplicateFiles(options: { resetExisting?: boolean } = {}) {
    const { resetExisting = false } = options;

    this.logger.log('Starting duplicate file scan');

    if (resetExisting) {
      this.db.clearDuplicateGroups();
      this.logger.log('Cleared existing duplicate groups');
    }

    const existing = this.db.listDuplicateGroups();
    const existingMap = new Map<
      string,
      { id: number; status: string; files: string[] }
    >();

    for (const row of existing) {
      const key = `${row.dataset}|${row.destination}|${row.hash}|${row.size}`;
      existingMap.set(key, {
        id: row.id,
        status: row.status,
        files: row.files,
      });
    }

    const datasetConfig = this.datasetsService.getDatasetConfig();
    const scanPromises: Promise<void>[] = [];

    for (const [datasetName, datasetObj] of Object.entries(datasetConfig)) {
      if (
        !datasetObj ||
        datasetObj.enabled === false ||
        datasetObj.enabled === 'false'
      ) {
        this.logger.log(`Skipping disabled dataset: ${datasetName}`);
        continue;
      }

      this.logger.log(`Scanning dataset: ${datasetName}`);
      const destinations = this.collectDestinations(datasetObj);
      for (const destination of destinations) {
        if (!destination || !fs.existsSync(destination)) {
          this.logger.warn(
            `Destination not found for dataset ${datasetName}: ${destination}`,
          );
          continue;
        }

        this.logger.log(`Scanning destination: ${destination}`);
        scanPromises.push(
          this.scanDestinationWithWorker(datasetName, destination, existingMap),
        );
      }
    }

    await Promise.all(scanPromises);

    this.logger.log('Duplicate scan completed');
  }

  private collectDestinations(datasetObj: Record<string, any>): Set<string> {
    const destinations = new Set<string>();

    if (datasetObj.destination && typeof datasetObj.destination === 'string') {
      destinations.add(datasetObj.destination);
    }

    for (const [pathKey, cfg] of Object.entries(datasetObj)) {
      if (pathKey === 'enabled') continue;
      if (cfg && typeof cfg === 'object' && cfg.destination) {
        destinations.add(cfg.destination);
      }
    }

    return destinations;
  }

  private async scanDestinationWithWorker(
    dataset: string,
    destination: string,
    existingMap: Map<string, { id: number; status: string; files: string[] }>,
    useDatabase = true, // Use database-based scanning by default
  ): Promise<void> {
    return new Promise((resolve, reject) => {
      const worker = new Worker(path.join(__dirname, 'duplicate-worker.js'));

      // Get database path
      let projectRoot = process.cwd();
      while (projectRoot !== path.dirname(projectRoot)) {
        if (fs.existsSync(path.join(projectRoot, 'package.json'))) {
          try {
            const pkg = JSON.parse(
              fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf-8'),
            );
            if (pkg.name === 'watch-finished-turbo') {
              break;
            }
          } catch (e) {
            // ignore
          }
        }
        projectRoot = path.dirname(projectRoot);
      }
      const dbPath = path.resolve(projectRoot, 'data/database.db');

      worker.on('message', (message) => {
        if (message.type === 'scan_result') {
          // Save duplicates
          for (const dup of message.duplicates) {
            const entry = {
              dataset: message.dataset,
              destination: message.destination,
              hash: dup.hash,
              size: dup.size,
              files: dup.files,
            };

            const key = `${entry.dataset}|${entry.destination}|${entry.hash}|${entry.size}`;
            const existingEntry = existingMap.get(key);

            // Skip groups that were marked reviewed/ignored previously
            if (existingEntry && existingEntry.status === 'reviewed') {
              continue;
            }

            if (existingEntry) {
              this.db.updateDuplicateGroupFiles(
                existingEntry.id,
                entry.files,
                'pending',
              );
            } else {
              this.db.saveDuplicateGroup(entry);
            }
          }

          // Log similars
          if (message.similars.length) {
            this.logger.log(
              `Found ${message.similars.length} groups of files with similar names in ${message.destination}`,
            );
            for (const group of message.similars) {
              this.logger.log(
                `Similar: ${group.baseName} - ${group.files.join(', ')}`,
              );
            }
          }

          if (message.duplicates.length) {
            this.logger.warn(
              `Found ${message.duplicates.length} duplicate group(s) in destination ${message.destination} (dataset: ${message.dataset})`,
            );
          }
        } else if (message.type === 'error') {
          this.logger.error(`Worker error: ${message.error}`);
        }
        worker.terminate();
        resolve();
      });

      worker.on('error', (error) => {
        this.logger.error(`Worker error: ${error}`);
        reject(error);
      });

      worker.on('exit', (code) => {
        if (code !== 0) {
          this.logger.error(`Worker exited with code ${code}`);
          reject(new Error(`Worker exited with code ${code}`));
        }
      });

      worker.postMessage({
        type: 'scan_duplicates',
        dataset,
        destination,
        useDatabase,
        dbPath,
      });
    });
  }

  private walkFiles(root: 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;
  }

  purgeDuplicateFiles(id: number, files: string[], note?: string) {
    const group = this.db.getDuplicateGroup(id);
    if (!group) {
      throw new Error('Duplicate group not found');
    }

    const toDelete = files && files.length > 0 ? files : [];
    const deleted: string[] = [];
    const errors: Array<{ file: string; error: string }> = [];

    for (const filePath of toDelete) {
      try {
        if (fs.existsSync(filePath)) {
          fs.unlinkSync(filePath);
          deleted.push(filePath);
        } else {
          errors.push({ file: filePath, error: 'File not found' });
        }
      } catch (error) {
        errors.push({ file: filePath, error: (error as Error).message });
      }
    }

    const remaining = group.files.filter((f) => !toDelete.includes(f));
    const nextStatus = remaining.length > 1 ? 'pending' : 'purged';
    this.db.updateDuplicateGroupFiles(id, remaining, nextStatus, note);

    return { deleted, errors, remaining, status: nextStatus };
  }

  /**
   * Hash a file asynchronously
   */
  private async hashFile(filePath: string): Promise<string | null> {
    try {
      const data = await fsPromises.readFile(filePath);
      const hash = crypto.createHash('sha1');
      hash.update(data);
      return hash.digest('hex');
    } catch (error) {
      this.logger.warn(`Hashing failed for ${filePath}: ${error}`);
      return null;
    }
  }

  /**
   * Index all files in a destination directory with their hashes
   * This populates the files table with destination_path, hash, and file_size
   * for fast duplicate detection. Runs in a worker thread to avoid blocking.
   */
  async indexDestinationFiles(
    dataset: string,
    destinationPath: string,
    options: {
      reindex?: boolean; // Clear existing entries and re-index
      batchSize?: number; // Number of files to process at a time
    } = {},
  ): Promise<{
    indexed: number;
    skipped: number;
    errors: number;
  }> {
    const { reindex = false, batchSize = 100 } = options;

    this.logger.log(
      `Starting indexing worker for ${dataset} at ${destinationPath}`,
    );
    this.logger.log(
      `Options: reindex=${reindex}, batchSize=${batchSize}`,
    );

    // Get database path
    let projectRoot = process.cwd();
    while (projectRoot !== path.dirname(projectRoot)) {
      if (fs.existsSync(path.join(projectRoot, 'package.json'))) {
        try {
          const pkg = JSON.parse(
            fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf-8'),
          );
          if (pkg.name === 'watch-finished-turbo') {
            break;
          }
        } catch (e) {
          // ignore
        }
      }
      projectRoot = path.dirname(projectRoot);
    }
    const dbPath = path.resolve(projectRoot, 'data/database.db');

    return new Promise((resolve, reject) => {
      const worker = new Worker(path.join(__dirname, 'indexing-worker.js'));
      let lastProgress = 0;

      worker.on('message', (message) => {
        if (message.type === 'progress') {
          // Log progress updates
          if (message.progress > lastProgress + 5 || message.progress === 100) {
            this.logger.log(
              `Indexing progress: ${message.progress}% (${message.indexed} indexed, ${message.skipped} skipped, ${message.errors} errors)`,
            );
            lastProgress = message.progress;
          }
        } else if (message.type === 'index_result') {
          this.logger.log(
            `Indexing complete: ${message.indexed} indexed, ${message.skipped} skipped, ${message.errors} errors`,
          );
          worker.terminate();
          resolve({
            indexed: message.indexed,
            skipped: message.skipped,
            errors: message.errors,
          });
        } else if (message.type === 'error') {
          this.logger.error(`Indexing worker error: ${message.error}`);
          worker.terminate();
          reject(new Error(message.error));
        }
      });

      worker.on('error', (error) => {
        this.logger.error(`Indexing worker error: ${error}`);
        reject(error);
      });

      worker.on('exit', (code) => {
        if (code !== 0) {
          this.logger.error(`Indexing worker exited with code ${code}`);
          reject(new Error(`Indexing worker exited with code ${code}`));
        }
      });

      worker.postMessage({
        type: 'index_destination',
        dataset,
        destination: destinationPath,
        dbPath,
        reindex,
        batchSize,
      });
    });
  }

  /**
   * Get duplicate file statistics from indexed files
   */
  async getIndexedDuplicateStats(dataset?: string): Promise<{
    totalDuplicates: number;
    duplicatesByDataset: Array<{
      dataset: string;
      hash: string;
      file_size: number;
      file_count: number;
      files: string[];
    }>;
  }> {
    const duplicates = this.db.getAllDuplicates(dataset);

    const duplicatesByDataset = duplicates.map((dup) => ({
      dataset: dup.dataset,
      hash: dup.hash,
      file_size: dup.file_size,
      file_count: dup.file_count,
      files: dup.file_paths.split('|||'),
    }));

    return {
      totalDuplicates: duplicates.length,
      duplicatesByDataset,
    };
  }
}
