/**
 * Function/Tools/file-hash.js
 * ---------------------------------------------------------------------------
 * FILE HASH CALCULATOR
 *
 * MD5, SHA-1 and SHA-256 of a file, streamed so files of any size work, with
 * an optional comparison against an expected value. Read-only.
 * ---------------------------------------------------------------------------
 */

'use strict';

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

const ALGORITHMS = ['md5', 'sha1', 'sha256'];

/**
 * Hash a file with several algorithms in a single pass.
 * @param {string} file
 * @param {(read:number)=>void} [onProgress]
 */
function hashFile(file, onProgress) {
  return new Promise((resolve) => {
    const hashes = ALGORITHMS.map((name) => ({ name, hash: crypto.createHash(name) }));
    let read = 0;

    const stream = fs.createReadStream(file);
    stream.on('data', (chunk) => {
      read += chunk.length;
      for (const entry of hashes) entry.hash.update(chunk);
      if (onProgress) onProgress(read);
    });
    stream.on('end', () => {
      const result = {};
      for (const entry of hashes) result[entry.name] = entry.hash.digest('hex');
      resolve({ ok: true, hashes: result, bytes: read });
    });
    stream.on('error', (err) => resolve({ ok: false, error: err.message }));
  });
}

module.exports = {
  name: 'File Hash Calculator',
  menuName: 'File Hash Calculator',
  description: 'Calculates MD5, SHA-1 and SHA-256 of a file and can compare against an expected value.',
  version: '1.0.0',
  author: 'InfinitySniper HFX',
  category: 'Tools',
  order: 10,
  requiredPermissions: 'normal',
  destructive: false,
  dependencies: [],
  minimumAppVersion: '1.0.0',
  permissions: ['filesystem:read', 'filesystem:write(reports)'],
  tags: ['hash', 'checksum', 'md5', 'sha256', 'integrity', 'verify'],
  documentation:
    'Reads a file once and produces its MD5, SHA-1 and SHA-256 checksums. Useful to verify that a ' +
    'download matches the checksum published by its author: paste the expected value and the tool ' +
    'compares it against all three, case-insensitively, and tells you whether it matches. ' +
    'Large files are streamed, so memory use stays constant.',

  async selfTest() {
    // A known vector proves the pipeline end to end.
    const digest = crypto.createHash('sha256').update('abc').digest('hex');
    const expected = 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad';
    return digest === expected
      ? { ok: true, message: 'hashing verified against a known vector' }
      : { ok: false, message: 'hash mismatch on the known vector' };
  },

  async execute(ctx) {
    const { ui, prompt } = ctx;
    kit.header('File Hash Calculator', ['MD5, SHA-1 and SHA-256 of any file.']);

    for (;;) {
      const input = (await prompt.text('File path (empty to go back)')).replace(/^"|"$/g, '').trim();
      if (!input) break;

      let stat;
      try {
        stat = fs.statSync(input);
      } catch (err) {
        ui.blank();
        ui.error(`The file could not be read: ${err.code === 'ENOENT' ? 'it does not exist' : err.message}`);
        continue;
      }
      if (stat.isDirectory()) {
        ui.blank();
        ui.error('That is a folder - please give the path of a file.');
        continue;
      }

      ui.blank();
      kit.kv('File', path.basename(input));
      kit.kv('Size', system.formatBytes(stat.size));
      ui.blank();

      const started = Date.now();
      const spin = ui.spinner('Hashing...');
      const result = await hashFile(input, (read) => {
        if (stat.size > 0) {
          spin.update(`Hashing... ${Math.round((read / stat.size) * 100)}% (${system.formatBytes(read)})`);
        }
      });
      spin.stop();

      if (!result.ok) {
        ui.error(`The file could not be hashed: ${result.error}`);
        continue;
      }

      const seconds = (Date.now() - started) / 1000;

      ui.section('CHECKSUMS');
      ui.blank();
      ui.println('  ' + ui.c.gray(ui.padEnd('MD5', 8)) + ui.c.brightWhite(result.hashes.md5));
      ui.println('  ' + ui.c.gray(ui.padEnd('SHA1', 8)) + ui.c.brightWhite(result.hashes.sha1));
      ui.blank();
      ui.println('  ' + ui.c.gray('SHA256'));
      ui.println('  ' + ui.c.brightGreen(result.hashes.sha256));
      ui.blank();
      kit.kv('Bytes Read', system.formatNumber(result.bytes));
      kit.kv('Duration', `${seconds.toFixed(1)}s`);
      if (seconds > 0.2) kit.kv('Throughput', `${system.formatBytes(result.bytes / seconds)}/s`);

      // ---- optional comparison ------------------------------------------
      ui.blank();
      const compare = await prompt.confirm('Compare against an expected checksum?');
      let comparison = null;
      if (compare === true) {
        const expected = (await prompt.text('Expected checksum')).trim().toLowerCase().replace(/\s/g, '');
        if (expected) {
          const match = ALGORITHMS.find((name) => result.hashes[name].toLowerCase() === expected);
          comparison = { expected, match: match || null };
          ui.blank();
          if (match) {
            ui.completed(`MATCH - the file matches the expected ${match.toUpperCase()} checksum.`);
          } else {
            ui.error('NO MATCH - the file does not match that checksum.');
            ui.println('  ' + ui.c.gray('The download may be incomplete, altered or from another version.'));
          }
        }
      }

      await kit.offerExport('File Hash Report', 'FileHash', (report) => {
        const section = report.section('FILE');
        section.add('File Name', path.basename(input));
        section.add('Full Path', path.resolve(input));
        section.add('Size', system.formatBytes(stat.size));
        section.add('Last Modified', system.isoLocal(stat.mtime));

        const hashes = report.section('CHECKSUMS');
        hashes.add('MD5', result.hashes.md5);
        hashes.add('SHA1', result.hashes.sha1);
        hashes.add('SHA256', result.hashes.sha256);

        if (comparison) {
          const check = report.section('VERIFICATION');
          check.add('Expected', comparison.expected);
          check.add('Result', comparison.match ? `MATCH (${comparison.match.toUpperCase()})` : 'NO MATCH');
        }
      });

      ui.blank();
      const again = await prompt.confirm('Hash another file?');
      if (again !== true) break;
    }

    return kit.done('Hash calculator closed.');
  },
};
