/**
 * Function/Windows/installed-software.js
 * ---------------------------------------------------------------------------
 * INSTALLED SOFTWARE LIST
 *
 * Reads the uninstall registry keys (much faster and more complete than
 * Win32_Product, which also triggers an MSI reconfiguration) and offers
 * search, sorting and TXT export. Nothing is ever uninstalled here.
 * ---------------------------------------------------------------------------
 */

'use strict';

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

/**
 * The two sources are queried separately on purpose: Get-AppxPackage can take
 * a minute on a big machine, and a slow Store query must never cost us the
 * (fast, far more important) registry list.
 */
const APPS_QUERY = [
  '$ErrorActionPreference = "SilentlyContinue"',
  '$paths = @(',
  '  "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*",',
  '  "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*",',
  '  "HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*"',
  ')',
  '@(Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |',
  '  Where-Object { $_.DisplayName -and -not $_.SystemComponent -and -not $_.ReleaseType -and -not $_.ParentKeyName } |',
  '  Select-Object DisplayName, DisplayVersion, Publisher, InstallDate, InstallLocation, EstimatedSize, PSPath) |',
  '  ConvertTo-Json -Depth 3 -Compress',
].join('\n');

const STORE_QUERY = [
  '$ErrorActionPreference = "SilentlyContinue"',
  '@(Get-AppxPackage -ErrorAction SilentlyContinue | Select-Object Name, Version, Publisher) | ConvertTo-Json -Depth 3 -Compress',
].join('\n');

async function collect(options = {}) {
  const result = await system.runPowerShellJson(APPS_QUERY, { timeoutMs: 90000 });
  if (!result.ok) return null;

  // Store packages are a bonus - never let them fail the whole function.
  let store = [];
  if (options.includeStore !== false) {
    const storeResult = await system.runPowerShellJson(STORE_QUERY, { timeoutMs: 90000 });
    if (storeResult.ok) store = system.asArray(storeResult.data);
  }

  const apps = system.asArray(result.data)
    .map((app) => ({
      name: system.clean(app.DisplayName),
      version: system.clean(app.DisplayVersion),
      publisher: system.clean(app.Publisher),
      installDate: system.clean(app.InstallDate),
      location: system.clean(app.InstallLocation),
      sizeKb: Number(app.EstimatedSize) || 0,
      scope: String(app.PSPath || '').includes('HKEY_CURRENT_USER') ? 'User' : 'Machine',
    }))
    .filter((app) => app.name);

  // The same product can appear in both the 32-bit and 64-bit hive.
  const unique = [];
  const seen = new Set();
  for (const app of apps) {
    const key = `${app.name.toLowerCase()}|${app.version || ''}`;
    if (seen.has(key)) continue;
    seen.add(key);
    unique.push(app);
  }

  return {
    apps: unique,
    store: store.map((pkg) => ({
      name: system.clean(pkg.Name),
      version: system.clean(pkg.Version),
      publisher: system.clean(pkg.Publisher),
    })),
  };
}

/** "20260824" -> "2026-08-24" */
function formatInstallDate(value) {
  const text = String(value || '');
  const match = text.match(/^(\d{4})(\d{2})(\d{2})$/);
  return match ? `${match[1]}-${match[2]}-${match[3]}` : system.clean(value);
}

const SORTS = [
  { id: 'name', label: 'Name (A-Z)', compare: (a, b) => String(a.name).localeCompare(String(b.name)) },
  { id: 'size', label: 'Size (largest first)', compare: (a, b) => b.sizeKb - a.sizeKb },
  { id: 'date', label: 'Install date (newest first)', compare: (a, b) => String(b.installDate || '').localeCompare(String(a.installDate || '')) },
  { id: 'publisher', label: 'Publisher', compare: (a, b) => String(a.publisher || '').localeCompare(String(b.publisher || '')) },
];

function renderTable(ui, apps, limit) {
  kit.table(
    ['APPLICATION', 'VERSION', 'PUBLISHER', 'SIZE'],
    apps.map((app) => [
      ui.c.brightWhite(ui.truncate(app.name, 32)),
      ui.c.gray(ui.truncate(app.version || '-', 14)),
      ui.c.gray(ui.truncate(app.publisher || '-', 20)),
      app.sizeKb ? ui.c.brightCyan(system.formatBytes(app.sizeKb * 1024)) : ui.c.gray('-'),
    ]),
    { align: ['left', 'left', 'left', 'right'], limit }
  );
}

module.exports = {
  name: 'Installed Software',
  menuName: 'Installed Software List',
  description: 'Lists installed applications with version, publisher, date and size. Search, sort, export.',
  version: '1.0.0',
  author: 'InfinitySniper HFX',
  category: 'Windows',
  order: 10,
  requiredPermissions: 'normal',
  destructive: false,
  dependencies: [],
  minimumAppVersion: '1.0.0',
  permissions: ['registry:read', 'filesystem:write(reports)'],
  tags: ['software', 'programs', 'applications', 'installed', 'apps'],
  documentation:
    'Reads the machine and user uninstall registry keys (both 32-bit and 64-bit) plus the Microsoft ' +
    'Store packages. This is much faster than Win32_Product and, unlike that class, it does not ' +
    'trigger an MSI reconfiguration of every installed product. Results can be searched, sorted by ' +
    'name, size, date or publisher, and exported. This module never uninstalls anything.',

  async selfTest(ctx) {
    if (!ctx.system.IS_WINDOWS) return { ok: false, message: 'Windows only' };
    // Skip the slow Store query in a self-test - the registry list is the point.
    const data = await collect({ includeStore: false });
    return data && data.apps.length
      ? { ok: true, message: `${data.apps.length} application(s) found` }
      : { ok: false, message: 'no software list' };
  },

  reportProvider: {
    id: 'software',
    label: 'Installed Software',
    order: 100,
    async build(report) {
      const data = await collect();
      const section = report.section('INSTALLED SOFTWARE');
      if (!data) {
        section.note('The software list was not available during export.');
        return;
      }
      section.add('Applications', String(data.apps.length));
      section.add('Store Packages', String(data.store.length));
      const list = section.group('Applications');
      [...data.apps].sort(SORTS[0].compare).forEach((app, index) => {
        list.add(
          `${index + 1}. ${app.name}`,
          [app.version, app.publisher, app.sizeKb ? system.formatBytes(app.sizeKb * 1024) : null]
            .filter(Boolean)
            .join(' | ')
        );
      });
    },
  },

  async execute(ctx) {
    const { ui, prompt } = ctx;
    kit.header('Installed Software', ['Applications installed on this computer.', 'Nothing is uninstalled here.']);
    if (!kit.requireWindows('The software list')) return { ok: false, reason: 'unsupported-platform' };

    const spin = ui.spinner('Reading the installed software list...');
    const data = await collect();
    spin.stop();
    if (!data || !data.apps.length) return kit.noData('installed software');

    const totalSize = data.apps.reduce((sum, app) => sum + app.sizeKb * 1024, 0);
    let current = [...data.apps].sort(SORTS[0].compare);
    let title = 'ALL APPLICATIONS';

    for (;;) {
      ui.blank();
      ui.section('SUMMARY');
      ui.blank();
      kit.kv('Applications', system.formatNumber(data.apps.length));
      kit.kv('Microsoft Store Packages', system.formatNumber(data.store.length));
      kit.kv('Reported Total Size', totalSize ? system.formatBytes(totalSize) : null);
      kit.kv('Machine-wide', String(data.apps.filter((a) => a.scope === 'Machine').length));
      kit.kv('User-only', String(data.apps.filter((a) => a.scope === 'User').length));

      ui.blank();
      ui.section(title);
      ui.blank();
      renderTable(ui, current, 25);

      ui.blank();
      ui.menu(
        [
          { key: '1', label: 'Search by name or publisher' },
          { key: '2', label: 'Sort the list' },
          { key: '3', label: 'Show application details' },
          { key: '4', label: 'Show Microsoft Store packages' },
          { key: '5', label: 'Export as a TXT report' },
          { key: '6', label: 'Show the full list again' },
          { separator: true },
          { key: '0', label: 'Back', color: 'gray' },
        ],
        { title: 'INSTALLED SOFTWARE' }
      );
      ui.blank();

      const answer = await prompt.select();
      if (!answer && prompt.isClosed()) break;
      if (answer === '0') break;

      switch (answer) {
        case '1': {
          const term = (await prompt.text('Search for')).trim().toLowerCase();
          if (!term) break;
          current = data.apps.filter(
            (app) =>
              String(app.name).toLowerCase().includes(term) ||
              String(app.publisher || '').toLowerCase().includes(term)
          );
          title = `SEARCH: ${term.toUpperCase()} (${current.length} match(es))`;
          if (!current.length) {
            current = [...data.apps].sort(SORTS[0].compare);
            title = 'ALL APPLICATIONS';
            ui.blank();
            ui.warning('Nothing matched that search.');
            await prompt.pause();
          }
          break;
        }

        case '2': {
          const index = await prompt.choose('Sort by', SORTS.map((s) => s.label));
          if (index === null) break;
          current = [...current].sort(SORTS[index].compare);
          title = `SORTED BY ${SORTS[index].label.toUpperCase()}`;
          break;
        }

        case '3': {
          const index = await prompt.choose(
            'Which application?',
            current.slice(0, 30).map((app) => ({ label: app.name, hint: app.version || '' }))
          );
          if (index === null) break;
          const app = current[index];
          ui.blank();
          ui.section(app.name.toUpperCase());
          ui.blank();
          kit.kv('Application Name', app.name);
          kit.kv('Version', app.version);
          kit.kv('Publisher', app.publisher);
          kit.kv('Installation Date', formatInstallDate(app.installDate));
          kit.kv('Installation Location', app.location);
          kit.kv('Estimated Size', app.sizeKb ? system.formatBytes(app.sizeKb * 1024) : null);
          kit.kv('Scope', app.scope === 'User' ? 'Current user only' : 'All users');
          await prompt.pause();
          break;
        }

        case '4': {
          ui.blank();
          ui.section('MICROSOFT STORE PACKAGES');
          ui.blank();
          if (!data.store.length) {
            ui.println('  ' + ui.c.gray('No Store package was reported.'));
          } else {
            kit.table(
              ['PACKAGE', 'VERSION', 'PUBLISHER'],
              data.store.map((pkg) => [
                ui.c.brightWhite(ui.truncate(pkg.name || '-', 32)),
                ui.c.gray(pkg.version || '-'),
                ui.c.gray(ui.truncate(String(pkg.publisher || '-').replace(/^CN=/, ''), 24)),
              ]),
              { limit: 25 }
            );
          }
          await prompt.pause();
          break;
        }

        case '5': {
          const report = kit.report('Installed Software Report');
          const section = report.section('SUMMARY');
          section.add('Applications', system.formatNumber(data.apps.length));
          section.add('Store Packages', system.formatNumber(data.store.length));
          section.add('Reported Total Size', totalSize ? system.formatBytes(totalSize) : null);

          const list = report.section('APPLICATIONS');
          [...data.apps].sort(SORTS[0].compare).forEach((app, index) => {
            const group = list.group(`${index + 1}. ${app.name}`);
            group.add('Version', app.version);
            group.add('Publisher', app.publisher);
            group.add('Installation Date', formatInstallDate(app.installDate));
            group.add('Installation Location', app.location);
            group.add('Estimated Size', app.sizeKb ? system.formatBytes(app.sizeKb * 1024) : null);
          });

          const store = report.section('MICROSOFT STORE PACKAGES');
          data.store.forEach((pkg, index) => store.add(`${index + 1}. ${pkg.name}`, pkg.version));

          kit.saveReport(report, 'InstalledSoftware');
          await prompt.pause();
          break;
        }

        case '6':
          current = [...data.apps].sort(SORTS[0].compare);
          title = 'ALL APPLICATIONS';
          break;

        default:
          ui.warning('Enter one of the numbers shown above, or 0 to go back.');
          await prompt.pause();
      }
    }

    return kit.done('Software list closed.');
  },
};
