/**
 * Function/Windows/driver-info.js
 * ---------------------------------------------------------------------------
 * DRIVER INFORMATION
 *
 * Installed device drivers with device, manufacturer, version, date and
 * status - with the outdated and problem drivers highlighted. Read-only.
 * ---------------------------------------------------------------------------
 */

'use strict';

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

const QUERY = [
  '$ErrorActionPreference = "SilentlyContinue"',
  'function SafeGet([scriptblock] $block) { try { & $block } catch { $null } }',
  '$signed = SafeGet { @(Get-CimInstance Win32_PnPSignedDriver -ErrorAction Stop | Select-Object DeviceName, Manufacturer, DriverVersion, DriverDate, DriverProviderName, DeviceClass, IsSigned, InfName, DeviceID) }',
  '$problems = SafeGet { @(Get-CimInstance Win32_PnPEntity -Filter "ConfigManagerErrorCode <> 0" -ErrorAction Stop | Select-Object Name, DeviceID, ConfigManagerErrorCode, Status) }',
  '[ordered]@{ drivers = @($signed); problems = @($problems) } | ConvertTo-Json -Depth 4 -Compress',
].join('\n');

/** Device Manager error codes worth explaining. */
const ERROR_CODES = {
  1: 'not configured correctly',
  3: 'the driver may be corrupted, or memory is low',
  10: 'the device cannot start',
  12: 'not enough free resources',
  14: 'the computer must be restarted',
  18: 'the drivers must be reinstalled',
  19: 'the registry configuration is damaged',
  21: 'Windows is removing the device',
  22: 'the device is disabled',
  24: 'the device is not present or is not working',
  28: 'the drivers are not installed',
  31: 'Windows cannot load the required drivers',
  37: 'the driver returned a failure on initialisation',
  39: 'the driver may be corrupted or missing',
  43: 'the device reported a problem and was stopped',
  45: 'the device is not connected',
};

async function collect() {
  const result = await system.runPowerShellJson(QUERY, { timeoutMs: 120000 });
  if (!result.ok || !result.data) return null;

  const drivers = system.asArray(result.data.drivers)
    .map((driver) => ({
      device: system.clean(driver.DeviceName),
      manufacturer: system.clean(driver.Manufacturer),
      provider: system.clean(driver.DriverProviderName),
      version: system.clean(driver.DriverVersion),
      date: system.formatWmiDate(driver.DriverDate),
      deviceClass: system.clean(driver.DeviceClass),
      signed: driver.IsSigned === true,
      inf: system.clean(driver.InfName),
    }))
    .filter((driver) => driver.device);

  return { drivers, problems: system.asArray(result.data.problems) };
}

/** Age of a driver in years, for the "outdated" view. */
function ageYears(dateText) {
  if (!dateText) return null;
  const parsed = new Date(String(dateText).replace(' ', 'T'));
  if (Number.isNaN(parsed.getTime())) return null;
  return (Date.now() - parsed.getTime()) / (365.25 * 24 * 3600 * 1000);
}

/** Driver classes users actually care about. */
const KEY_CLASSES = ['Display', 'Net', 'System', 'HDC', 'SCSIAdapter', 'MEDIA', 'USB', 'Bluetooth', 'Mouse', 'Keyboard'];

module.exports = {
  name: 'Driver Information',
  menuName: 'Driver Information',
  description: 'Installed drivers with device, manufacturer, version, date, signature and problems.',
  version: '1.0.0',
  author: 'InfinitySniper HFX',
  category: 'Windows',
  order: 30,
  requiredPermissions: 'normal',
  destructive: false,
  dependencies: [],
  minimumAppVersion: '1.0.0',
  permissions: ['system:read', 'wmi:query', 'filesystem:write(reports)'],
  tags: ['drivers', 'devices', 'hardware', 'signed', 'version'],
  documentation:
    'Lists the signed device drivers Windows has installed with the device name, manufacturer, ' +
    'version, release date, driver class and signature state. Views are available for the important ' +
    'device classes, for drivers older than five years and for devices reporting a Device Manager ' +
    'error code (translated into plain language). Read-only: no driver is installed, updated or ' +
    'removed by this tool.',

  async selfTest(ctx) {
    if (!ctx.system.IS_WINDOWS) return { ok: false, message: 'Windows only' };
    const data = await collect();
    return data && data.drivers.length
      ? { ok: true, message: `${data.drivers.length} driver(s)` }
      : { ok: false, message: 'no driver data' };
  },

  async execute(ctx) {
    const { ui, prompt } = ctx;
    kit.header('Driver Information', ['Installed device drivers and their versions.']);
    if (!kit.requireWindows('Driver information')) return { ok: false, reason: 'unsupported-platform' };

    const spin = ui.spinner('Reading installed drivers (this takes a moment)...');
    const data = await collect();
    spin.stop();
    if (!data || !data.drivers.length) return kit.noData('driver information');

    const unsigned = data.drivers.filter((driver) => !driver.signed);
    const outdated = data.drivers.filter((driver) => {
      const age = ageYears(driver.date);
      return age !== null && age > 5;
    });
    const important = data.drivers.filter((driver) => KEY_CLASSES.includes(String(driver.deviceClass)));

    let view = important;
    let title = 'KEY DEVICE DRIVERS';

    for (;;) {
      ui.blank();
      ui.section('SUMMARY');
      ui.blank();
      kit.kv('Installed Drivers', system.formatNumber(data.drivers.length));
      kit.kv('Key Device Drivers', system.formatNumber(important.length));
      kit.kv('Older Than 5 Years', system.formatNumber(outdated.length), outdated.length ? 'brightYellow' : 'brightGreen');
      kit.kv('Unsigned Drivers', system.formatNumber(unsigned.length), unsigned.length ? 'brightYellow' : 'brightGreen');
      kit.kv('Devices With Errors', system.formatNumber(data.problems.length), data.problems.length ? 'brightRed' : 'brightGreen');

      ui.blank();
      ui.section(title);
      ui.blank();
      kit.table(
        ['DEVICE', 'VERSION', 'DATE', 'PROVIDER'],
        view.map((driver) => [
          ui.c.brightWhite(ui.truncate(driver.device, 30)),
          ui.c.gray(ui.truncate(driver.version || '-', 16)),
          ui.c.gray((driver.date || '-').slice(0, 10)),
          ui.c.brightCyan(ui.truncate(driver.provider || driver.manufacturer || '-', 18)),
        ]),
        { limit: 22 }
      );

      ui.blank();
      ui.menu(
        [
          { key: '1', label: 'Key device drivers' },
          { key: '2', label: 'All drivers' },
          { key: '3', label: 'Drivers older than 5 years' },
          { key: '4', label: 'Unsigned drivers' },
          { key: '5', label: 'Devices reporting an error' },
          { key: '6', label: 'Search by device name' },
          { key: '7', label: 'Export the driver list' },
          { separator: true },
          { key: '0', label: 'Back', color: 'gray' },
        ],
        { title: 'DRIVERS' }
      );
      ui.blank();

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

      switch (answer) {
        case '1':
          view = important;
          title = 'KEY DEVICE DRIVERS';
          break;
        case '2':
          view = data.drivers;
          title = `ALL DRIVERS (${data.drivers.length})`;
          break;
        case '3':
          view = outdated;
          title = `DRIVERS OLDER THAN 5 YEARS (${outdated.length})`;
          break;
        case '4':
          view = unsigned;
          title = `UNSIGNED DRIVERS (${unsigned.length})`;
          break;
        case '5': {
          ui.blank();
          ui.section('DEVICES REPORTING AN ERROR');
          ui.blank();
          if (!data.problems.length) {
            ui.println('  ' + ui.c.brightGreen('No device reports a problem.'));
          } else {
            for (const problem of data.problems) {
              const code = Number(problem.ConfigManagerErrorCode);
              ui.println('  ' + ui.c.brightRed(ui.glyphs().cross + ' ') + ui.c.brightWhite(ui.truncate(system.clean(problem.Name) || 'Unknown device', 44)));
              ui.println('      ' + ui.c.gray(`Code ${code}: ${ERROR_CODES[code] || 'see Device Manager for details'}`));
            }
          }
          await prompt.pause();
          break;
        }
        case '6': {
          const term = (await prompt.text('Device name contains')).trim().toLowerCase();
          if (!term) break;
          view = data.drivers.filter(
            (driver) =>
              driver.device.toLowerCase().includes(term) ||
              String(driver.provider || '').toLowerCase().includes(term)
          );
          title = `SEARCH: ${term.toUpperCase()} (${view.length})`;
          break;
        }
        case '7': {
          const report = kit.report('Driver Information Report');
          const section = report.section('SUMMARY');
          section.add('Installed Drivers', system.formatNumber(data.drivers.length));
          section.add('Older Than 5 Years', system.formatNumber(outdated.length));
          section.add('Unsigned Drivers', system.formatNumber(unsigned.length));
          section.add('Devices With Errors', system.formatNumber(data.problems.length));

          const list = report.section('DRIVERS');
          [...data.drivers]
            .sort((a, b) => String(a.deviceClass).localeCompare(String(b.deviceClass)))
            .forEach((driver, index) => {
              list.add(
                `${index + 1}. ${driver.device}`,
                [driver.version, driver.date, driver.provider, driver.signed ? 'signed' : 'UNSIGNED']
                  .filter(Boolean)
                  .join(' | ')
              );
            });

          if (data.problems.length) {
            const problems = report.section('DEVICES WITH ERRORS');
            data.problems.forEach((problem, index) => {
              const code = Number(problem.ConfigManagerErrorCode);
              problems.add(`${index + 1}. ${system.clean(problem.Name)}`, `Code ${code} - ${ERROR_CODES[code] || 'unknown'}`);
            });
          }

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

    return kit.done('Driver information closed.');
  },
};
