/**
 * Function/Tools/archive-info.js
 * ---------------------------------------------------------------------------
 * ARCHIVE INFORMATION
 *
 * Inspects a ZIP archive by reading its central directory - no extraction, no
 * external tool and no npm package. Other archive formats are reported at file
 * level only.
 *
 * Nothing is ever extracted: an unknown archive is never unpacked.
 * ---------------------------------------------------------------------------
 */

'use strict';

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

const EOCD_SIGNATURE = 0x06054b50; // End of central directory
const CENTRAL_SIGNATURE = 0x02014b50; // Central directory file header

const COMPRESSION = {
  0: 'Stored', 1: 'Shrunk', 6: 'Imploded', 8: 'Deflate', 9: 'Deflate64',
  12: 'BZIP2', 14: 'LZMA', 93: 'Zstandard', 95: 'XZ', 98: 'PPMd',
};

/** DOS date/time as stored in a ZIP entry. */
function dosDateTime(date, time) {
  const day = date & 0x1f;
  const month = (date >> 5) & 0x0f;
  const year = ((date >> 9) & 0x7f) + 1980;
  const seconds = (time & 0x1f) * 2;
  const minutes = (time >> 5) & 0x3f;
  const hours = (time >> 11) & 0x1f;
  if (!month || !day) return null;
  const pad = (n) => String(n).padStart(2, '0');
  return `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
}

/**
 * Read a ZIP central directory.
 * @returns {{ok:boolean, entries?:Array, comment?:string, error?:string}}
 */
function readZip(file) {
  let handle;
  try {
    handle = fs.openSync(file, 'r');
    const size = fs.fstatSync(handle).size;

    // The EOCD sits at the end, after an optional comment of up to 64 KB.
    const tailSize = Math.min(size, 66 * 1024);
    const tail = Buffer.alloc(tailSize);
    fs.readSync(handle, tail, 0, tailSize, size - tailSize);

    let eocd = -1;
    for (let i = tail.length - 22; i >= 0; i -= 1) {
      if (tail.readUInt32LE(i) === EOCD_SIGNATURE) {
        eocd = i;
        break;
      }
    }
    if (eocd === -1) {
      fs.closeSync(handle);
      return { ok: false, error: 'no ZIP central directory found (damaged, or not a ZIP file)' };
    }

    const entryCount = tail.readUInt16LE(eocd + 10);
    const centralSize = tail.readUInt32LE(eocd + 12);
    const centralOffset = tail.readUInt32LE(eocd + 16);
    const commentLength = tail.readUInt16LE(eocd + 20);
    const comment = commentLength ? tail.slice(eocd + 22, eocd + 22 + commentLength).toString('utf8') : '';

    if (centralOffset === 0xffffffff || entryCount === 0xffff) {
      fs.closeSync(handle);
      return { ok: false, error: 'ZIP64 archives are not supported by this reader' };
    }

    const central = Buffer.alloc(centralSize);
    fs.readSync(handle, central, 0, centralSize, centralOffset);
    fs.closeSync(handle);

    const entries = [];
    let offset = 0;
    while (offset + 46 <= central.length && entries.length < 100000) {
      if (central.readUInt32LE(offset) !== CENTRAL_SIGNATURE) break;

      const method = central.readUInt16LE(offset + 10);
      const time = central.readUInt16LE(offset + 12);
      const date = central.readUInt16LE(offset + 14);
      const compressedSize = central.readUInt32LE(offset + 20);
      const uncompressedSize = central.readUInt32LE(offset + 24);
      const nameLength = central.readUInt16LE(offset + 28);
      const extraLength = central.readUInt16LE(offset + 30);
      const commentLen = central.readUInt16LE(offset + 32);
      const name = central.slice(offset + 46, offset + 46 + nameLength).toString('utf8');

      entries.push({
        name,
        isFolder: name.endsWith('/'),
        compressedSize,
        uncompressedSize,
        method: COMPRESSION[method] || `Method ${method}`,
        modified: dosDateTime(date, time),
      });

      offset += 46 + nameLength + extraLength + commentLen;
    }

    return { ok: true, entries, comment, declared: entryCount };
  } catch (err) {
    try {
      if (handle !== undefined) fs.closeSync(handle);
    } catch {
      /* already closed */
    }
    return { ok: false, error: err.message };
  }
}

const KNOWN_EXTENSIONS = /\.(zip|jar|apk|docx|xlsx|pptx|epub|whl|nupkg|vsix|crx)$/i;

module.exports = {
  name: 'Archive Information',
  menuName: 'Archive Information',
  description: 'Inspects a ZIP archive: entry count, sizes, compression and the largest entries.',
  version: '1.0.0',
  author: 'InfinitySniper HFX',
  category: 'Tools',
  order: 30,
  requiredPermissions: 'normal',
  destructive: false,
  dependencies: [],
  minimumAppVersion: '1.0.0',
  permissions: ['filesystem:read', 'filesystem:write(reports)'],
  tags: ['archive', 'zip', 'compression', 'inspect'],
  documentation:
    'Reads the central directory of a ZIP archive directly, so it lists the contents without ' +
    'extracting anything and without needing 7-Zip or any package: number of files and folders, ' +
    'compressed and uncompressed size, compression ratio, method and the largest entries. ' +
    'ZIP-based formats such as .docx, .xlsx, .jar and .apk work too. Other formats (RAR, 7z, TAR) ' +
    'are reported at file level only - an unknown archive is never unpacked.',

  async selfTest() {
    return { ok: true, message: 'ZIP reader ready' };
  },

  async execute(ctx) {
    const { ui, prompt } = ctx;
    kit.header('Archive Information', ['Inspects a ZIP archive without extracting it.']);

    for (;;) {
      const input = (await prompt.text('Archive 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, not an archive.');
        continue;
      }

      const full = path.resolve(input);
      ui.blank();
      ui.section('ARCHIVE');
      ui.blank();
      kit.kv('Archive Name', path.basename(full));
      kit.kv('Archive Size', system.formatBytes(stat.size));
      kit.kv('Location', path.dirname(full));
      kit.kv('Last Modified', system.isoLocal(stat.mtime));

      const spin = ui.spinner('Reading the archive directory...');
      const result = readZip(full);
      spin.stop();

      if (!result.ok) {
        ui.blank();
        if (KNOWN_EXTENSIONS.test(full)) {
          ui.error(`This ZIP archive could not be read: ${result.error}`);
        } else {
          ui.warning('This is not a ZIP archive - only file-level information is available.');
          ui.println('  ' + ui.c.gray('RAR, 7z and TAR archives need their own tool to list contents.'));
          ui.println('  ' + ui.c.gray('Nothing was extracted.'));
        }
        ui.blank();
        const again = await prompt.confirm('Inspect another archive?');
        if (again !== true) break;
        continue;
      }

      const files = result.entries.filter((entry) => !entry.isFolder);
      const folders = result.entries.filter((entry) => entry.isFolder);
      const uncompressed = files.reduce((sum, entry) => sum + entry.uncompressedSize, 0);
      const compressed = files.reduce((sum, entry) => sum + entry.compressedSize, 0);
      const ratio = uncompressed ? Math.round((1 - compressed / uncompressed) * 100) : 0;
      const largest = [...files].sort((a, b) => b.uncompressedSize - a.uncompressedSize);

      ui.blank();
      ui.section('CONTENTS');
      ui.blank();
      kit.kv('File Count', system.formatNumber(files.length));
      kit.kv('Folder Count', system.formatNumber(folders.length));
      kit.kv('Uncompressed Size', system.formatBytes(uncompressed));
      kit.kv('Compressed Size', system.formatBytes(compressed));
      kit.kv('Compression Ratio', `${ratio} % saved`, 'brightGreen');
      kit.kv('Archive Comment', result.comment ? ui.truncate(result.comment, 40) : null);

      ui.blank();
      ui.section('LARGEST ENTRIES');
      ui.blank();
      kit.table(
        ['ENTRY', 'SIZE', 'PACKED', 'METHOD'],
        largest.map((entry) => [
          ui.c.brightWhite(ui.truncate(entry.name, 34)),
          ui.c.brightCyan(system.formatBytes(entry.uncompressedSize)),
          ui.c.gray(system.formatBytes(entry.compressedSize)),
          ui.c.gray(entry.method),
        ]),
        { align: ['left', 'right', 'right', 'left'], limit: 15 }
      );

      await kit.offerExport('Archive Information Report', 'ArchiveInfo', (report) => {
        const section = report.section('ARCHIVE');
        section.add('Archive Name', path.basename(full));
        section.add('Full Path', full);
        section.add('Archive Size', system.formatBytes(stat.size));
        section.add('File Count', system.formatNumber(files.length));
        section.add('Folder Count', system.formatNumber(folders.length));
        section.add('Uncompressed Size', system.formatBytes(uncompressed));
        section.add('Compression Ratio', `${ratio} %`);
        section.add('Archive Comment', result.comment || null);

        const list = report.section('ENTRIES');
        largest.slice(0, 200).forEach((entry, index) => {
          list.add(
            `${index + 1}. ${entry.name}`,
            `${system.formatBytes(entry.uncompressedSize)} -> ${system.formatBytes(entry.compressedSize)} (${entry.method})${entry.modified ? ' - ' + entry.modified : ''}`
          );
        });
      });

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

    return kit.done('Archive inspection closed.');
  },
};
