/**
 * Function/Storage/duplicate-finder.js
 * ---------------------------------------------------------------------------
 * DUPLICATE FILE FINDER
 *
 * Two-stage detection: group by exact size first, then hash only the files
 * that share a size. Nothing is deleted - the result is a report.
 * ---------------------------------------------------------------------------
 */

'use strict';

const fs = require('fs');
const os = require('os');
const path = require('path');
const crypto = require('crypto');
const kit = require('../../core/kit');
const system = require('../../core/system');
const scan = require('../../core/fs-scan');

/** SHA-256 of a file, streamed so large files do not sit in memory. */
function hashFile(file, limitBytes) {
  return new Promise((resolve) => {
    const hash = crypto.createHash('sha256');
    const stream = fs.createReadStream(file, limitBytes ? { start: 0, end: limitBytes - 1 } : undefined);
    stream.on('data', (chunk) => hash.update(chunk));
    stream.on('end', () => resolve(hash.digest('hex')));
    stream.on('error', () => resolve(null));
  });
}

function suggestions() {
  const home = os.homedir();
  return [
    { label: `Downloads  (${path.join(home, 'Downloads')})`, value: path.join(home, 'Downloads') },
    { label: `Pictures   (${path.join(home, 'Pictures')})`, value: path.join(home, 'Pictures') },
    { label: `Documents  (${path.join(home, 'Documents')})`, value: path.join(home, 'Documents') },
    { label: `Videos     (${path.join(home, 'Videos')})`, value: path.join(home, 'Videos') },
    { label: 'Enter another folder...', value: null },
  ];
}

module.exports = {
  name: 'Duplicate File Finder',
  menuName: 'Duplicate File Finder',
  description: 'Finds identical files by size and content hash and groups them for review.',
  version: '1.0.0',
  author: 'InfinitySniper HFX',
  category: 'Storage',
  order: 40,
  requiredPermissions: 'normal',
  destructive: false,
  dependencies: [],
  minimumAppVersion: '1.0.0',
  permissions: ['filesystem:read', 'filesystem:write(reports)'],
  tags: ['duplicate', 'files', 'hash', 'cleanup', 'space'],
  config: { minimumKb: 64, maxFilesToHash: 4000, timeBudgetSeconds: 120 },
  documentation:
    'Files are first grouped by their exact byte size - files with a unique size can never be ' +
    'duplicates, so only the remaining candidates are hashed with SHA-256. That keeps the scan fast ' +
    'and the result exact. Duplicate groups are listed with every copy and the space that could be ' +
    'recovered. This function never deletes anything.',

  async selfTest() {
    const probe = await hashFile(__filename, 4096);
    return probe ? { ok: true, message: 'hashing works' } : { ok: false, message: 'hashing failed' };
  },

  async execute(ctx) {
    const { ui, prompt, config } = ctx;
    kit.header('Duplicate File Finder', ['Detects identical files by size and content hash.', 'Nothing is deleted.']);

    const options = suggestions();
    const choice = await prompt.choose('Which folder should be checked?', options.map((o) => o.label));
    if (choice === null) return kit.done('Scan cancelled.');

    let target = options[choice].value;
    if (!target) {
      target = await prompt.text('Folder path');
      if (!target) return kit.done('Scan cancelled.');
    }
    target = String(target).replace(/^"|"$/g, '').trim();

    if (!scan.isDirectory(target)) {
      ui.blank();
      ui.error(`"${target}" is not a folder that can be read.`);
      return { ok: false, reason: 'invalid-path' };
    }

    const minimumKb = (await prompt.number('Ignore files smaller than (KB)', 1, 1024 * 1024)) ?? config.minimumKb;

    // ---- stage 1: collect ----------------------------------------------
    ui.blank();
    const spin = ui.spinner('Collecting files...');
    const walked = await scan.walk(target, {
      minSize: minimumKb * 1024,
      timeBudgetMs: Math.max(10, Number(config.timeBudgetSeconds) || 120) * 1000,
      onProgress: (state) => spin.update(`Collecting... ${system.formatNumber(state.files)} files`),
    });
    spin.stop('OK', `${system.formatNumber(walked.files.length)} candidate file(s) collected`);

    // ---- stage 2: group by size ----------------------------------------
    const bySize = new Map();
    for (const file of walked.files) {
      if (!bySize.has(file.size)) bySize.set(file.size, []);
      bySize.get(file.size).push(file);
    }
    const candidates = [...bySize.values()].filter((group) => group.length > 1).flat();

    ui.blank();
    kit.kv('Files Examined', system.formatNumber(walked.files.length));
    kit.kv('Same-size Candidates', system.formatNumber(candidates.length));

    if (!candidates.length) {
      ui.blank();
      ui.info('No two files even share a size - there are no duplicates here.');
      return { ok: true, duplicates: 0 };
    }

    const limit = Number(config.maxFilesToHash) || 4000;
    const toHash = candidates.slice(0, limit);
    if (candidates.length > limit) {
      ui.warning(`Only the first ${system.formatNumber(limit)} candidates are hashed (configurable).`);
    }

    // ---- stage 3: hash --------------------------------------------------
    ui.blank();
    const byHash = new Map();
    for (let i = 0; i < toHash.length; i += 1) {
      const file = toHash[i];
      if (i % 10 === 0) ui.progressLine('Hashing files', i, toHash.length);
      // eslint-disable-next-line no-await-in-loop
      const digest = await hashFile(file.path);
      if (!digest) continue;
      const key = `${file.size}:${digest}`;
      if (!byHash.has(key)) byHash.set(key, []);
      byHash.get(key).push(file);
    }
    ui.endProgressLine();

    const groups = [...byHash.values()]
      .filter((group) => group.length > 1)
      .sort((a, b) => b[0].size * (b.length - 1) - a[0].size * (a.length - 1));

    const wasted = groups.reduce((sum, group) => sum + group[0].size * (group.length - 1), 0);

    ui.blank();
    ui.section('RESULT');
    ui.blank();
    kit.kv('Duplicate Groups', system.formatNumber(groups.length));
    kit.kv('Duplicate Files', system.formatNumber(groups.reduce((sum, g) => sum + g.length - 1, 0)));
    kit.kv('Recoverable Space', system.formatBytes(wasted), 'brightGreen');

    if (!groups.length) {
      ui.blank();
      ui.info('No duplicate content was found.');
      return { ok: true, duplicates: 0 };
    }

    ui.blank();
    ui.section('DUPLICATE GROUPS');
    for (const group of groups.slice(0, 12)) {
      ui.blank();
      ui.println(
        '  ' + ui.c.brightYellow(ui.glyphs().arrow + ' ') +
          ui.c.brightWhite(ui.truncate(group[0].name, 32)) +
          ui.c.gray(`  ${system.formatBytes(group[0].size)} x ${group.length} copies`) +
          ui.c.brightGreen(`  (+${system.formatBytes(group[0].size * (group.length - 1))})`)
      );
      for (const file of group) {
        ui.println('      ' + ui.c.gray(ui.truncate(file.path, ui.getWidth() - 6)));
      }
    }
    if (groups.length > 12) {
      ui.blank();
      ui.println('  ' + ui.c.gray(`... and ${groups.length - 12} more group(s) - export to see them all`));
    }

    ui.blank();
    ui.println('  ' + ui.c.gray('Nothing was deleted. Keep one copy of each group if you clean up manually.'));

    await kit.offerExport('Duplicate File Report', 'Duplicates', (report) => {
      const summary = report.section('SUMMARY');
      summary.add('Target Directory', target);
      summary.add('Files Examined', system.formatNumber(walked.files.length));
      summary.add('Duplicate Groups', system.formatNumber(groups.length));
      summary.add('Recoverable Space', system.formatBytes(wasted));

      const section = report.section('DUPLICATE GROUPS');
      groups.forEach((group, index) => {
        const entry = section.group(`${index + 1}. ${group[0].name} (${system.formatBytes(group[0].size)} x ${group.length})`);
        group.forEach((file, position) => entry.add(`Copy ${position + 1}`, file.path));
      });
    });

    return kit.done(`${groups.length} duplicate group(s) found.`);
  },
};
