/**
 * Function/Network/ping-test.js
 * ---------------------------------------------------------------------------
 * PING TEST
 *
 * Connectivity test against a host name or IP address: packets sent and
 * received, packet loss and minimum/maximum/average latency.
 *
 * Uses the .NET Ping class through PowerShell rather than parsing ping.exe
 * output, which is localised and therefore unreliable to read.
 * ---------------------------------------------------------------------------
 */

'use strict';

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

/** Only plausible host names / IPs reach PowerShell. */
const TARGET_PATTERN = /^[A-Za-z0-9._:-]{1,253}$/;

const PRESETS = [
  { label: 'Google DNS        (8.8.8.8)', value: '8.8.8.8' },
  { label: 'Cloudflare DNS    (1.1.1.1)', value: '1.1.1.1' },
  { label: 'google.com', value: 'google.com' },
  { label: 'Default gateway', value: '__gateway__' },
  { label: 'Enter another host or IP...', value: null },
];

const GATEWAY_QUERY =
  '$ErrorActionPreference = "SilentlyContinue"; ' +
  '(Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "IPEnabled = True" | ' +
  'Where-Object { $_.DefaultIPGateway } | Select-Object -First 1).DefaultIPGateway | ' +
  'Select-Object -First 1 | ConvertTo-Json -Compress';

function buildScript(target, count, timeoutMs) {
  return [
    '$ErrorActionPreference = "SilentlyContinue"',
    `$target = "${target}"`,
    `$count = ${count}`,
    `$timeout = ${timeoutMs}`,
    '$ping = New-Object System.Net.NetworkInformation.Ping',
    '$results = @()',
    '$address = $null',
    '$resolvedText = $null',
    'try { $address = [System.Net.Dns]::GetHostAddresses($target) | Select-Object -First 1 } catch { }',
    'if ($address) { $resolvedText = $address.ToString() }',
    'for ($i = 0; $i -lt $count; $i++) {',
    '  try {',
    '    $reply = $ping.Send($target, $timeout)',
    '    $results += [ordered]@{ Status = $reply.Status.ToString(); Time = $reply.RoundtripTime; Address = $reply.Address.ToString() }',
    '  } catch {',
    '    $results += [ordered]@{ Status = "Exception"; Time = $null; Address = $null }',
    '  }',
    '  Start-Sleep -Milliseconds 200',
    '}',
    '[ordered]@{ target = $target; resolved = $resolvedText; replies = @($results) } | ConvertTo-Json -Depth 4 -Compress',
  ].join('\n');
}

async function ping(target, count, timeoutMs) {
  const result = await system.runPowerShellJson(buildScript(target, count, timeoutMs), {
    timeoutMs: count * (timeoutMs + 500) + 20000,
  });
  if (!result.ok || !result.data) return null;
  return {
    target: result.data.target,
    resolved: result.data.resolved,
    replies: system.asArray(result.data.replies),
  };
}

function summarise(data) {
  const replies = data.replies;
  const successful = replies.filter((r) => String(r.Status) === 'Success');
  const times = successful.map((r) => Number(r.Time)).filter((t) => Number.isFinite(t));
  const sent = replies.length;
  const received = successful.length;

  return {
    sent,
    received,
    lost: sent - received,
    lossPercent: sent ? Math.round(((sent - received) / sent) * 100) : 0,
    min: times.length ? Math.min(...times) : null,
    max: times.length ? Math.max(...times) : null,
    avg: times.length ? Math.round(times.reduce((a, b) => a + b, 0) / times.length) : null,
    address: successful.length ? successful[0].Address : data.resolved,
  };
}

module.exports = {
  name: 'Ping Test',
  menuName: 'Ping Test',
  description: 'Tests connectivity to a host: packets, loss and minimum/maximum/average latency.',
  version: '1.0.0',
  author: 'InfinitySniper HFX',
  category: 'Network',
  order: 20,
  requiredPermissions: 'normal',
  destructive: false,
  dependencies: [],
  minimumAppVersion: '1.0.0',
  permissions: ['network:icmp'],
  tags: ['ping', 'network', 'latency', 'connectivity', 'test'],
  config: { timeoutMs: 2000 },
  documentation:
    'Sends ICMP echo requests to a host name or IP address and reports how many replies came back, ' +
    'the packet loss and the minimum, maximum and average round-trip time. The number of packets ' +
    'comes from the tools.pingCount setting and can be changed per run.',

  async selfTest(ctx) {
    if (!ctx.system.IS_WINDOWS) return { ok: false, message: 'Windows only' };
    const data = await ping('127.0.0.1', 1, 1000);
    return data && data.replies.length
      ? { ok: true, message: 'loopback reachable' }
      : { ok: false, message: 'ping unavailable' };
  },

  async execute(ctx) {
    const { ui, prompt, settings, config } = ctx;
    kit.header('Ping Test', ['Check whether a host answers and how fast it responds.']);
    if (!kit.requireWindows('The ping test')) return { ok: false, reason: 'unsupported-platform' };

    for (;;) {
      const choice = await prompt.choose('Which target?', PRESETS.map((p) => p.label));
      if (choice === null) break;

      let target = PRESETS[choice].value;

      if (target === '__gateway__') {
        const gateway = await system.runPowerShellJson(GATEWAY_QUERY, { timeoutMs: 20000 });
        target = gateway.ok && gateway.data ? String(gateway.data) : null;
        if (!target) {
          ui.blank();
          ui.warning('No default gateway was found on this system.');
          continue;
        }
        ui.blank();
        ui.info(`Default gateway: ${target}`);
      }

      if (!target) {
        target = await prompt.text('Host name or IP address');
        if (!target) continue;
      }

      target = target.trim();
      if (!TARGET_PATTERN.test(target)) {
        ui.blank();
        ui.error('That does not look like a host name or IP address.');
        continue;
      }

      const defaultCount = Number(settings.tools.pingCount) || 4;
      const chosenCount = await prompt.number(`Number of packets (ENTER for ${defaultCount})`, 1, 50);
      const count = chosenCount === null ? defaultCount : chosenCount;

      ui.blank();
      const spin = ui.spinner(`Pinging ${target} with ${count} packet(s)...`);
      const data = await ping(target, count, Number(config.timeoutMs) || 2000);
      spin.stop();

      if (!data) {
        ui.error('The ping test could not be executed on this system.');
        continue;
      }

      const stats = summarise(data);

      ui.blank();
      ui.section('RESULT');
      ui.blank();
      kit.kv('Target', target);
      kit.kv('Resolved Address', stats.address || 'not resolved');
      kit.kv('Packets Sent', String(stats.sent));
      kit.kv('Packets Received', String(stats.received), stats.received ? 'brightGreen' : 'brightRed');
      kit.kv('Packet Loss', `${stats.lossPercent} %`, stats.lossPercent > 0 ? 'brightYellow' : 'brightGreen');
      kit.kv('Minimum Latency', stats.min === null ? null : `${stats.min} ms`);
      kit.kv('Maximum Latency', stats.max === null ? null : `${stats.max} ms`);
      kit.kv('Average Latency', stats.avg === null ? null : `${stats.avg} ms`, 'brightGreen');

      ui.blank();
      ui.section('REPLIES');
      ui.blank();
      data.replies.forEach((reply, index) => {
        const ok = String(reply.Status) === 'Success';
        ui.println(
          '  ' + ui.c.gray(`#${index + 1} `) +
            (ok ? ui.c.brightGreen(ui.padEnd('reply', 10)) : ui.c.brightRed(ui.padEnd(String(reply.Status), 10))) +
            (ok ? ui.c.brightWhite(`${reply.Time} ms`) : ui.c.gray('no answer'))
        );
      });

      ui.blank();
      if (stats.received === 0) ui.error('The target did not answer any packet.');
      else if (stats.lossPercent > 0) ui.warning(`${stats.lossPercent}% packet loss - the connection is unstable.`);
      else if (stats.avg !== null && stats.avg < 50) ui.completed('Excellent connection.');
      else ui.ok('The target is reachable.');

      await kit.offerExport('Ping Test Report', 'PingTest', (report) => {
        const section = report.section('PING TEST');
        section.add('Target', target);
        section.add('Resolved Address', stats.address);
        section.add('Packets Sent', String(stats.sent));
        section.add('Packets Received', String(stats.received));
        section.add('Packet Loss', `${stats.lossPercent} %`);
        section.add('Minimum Latency', stats.min === null ? null : `${stats.min} ms`);
        section.add('Maximum Latency', stats.max === null ? null : `${stats.max} ms`);
        section.add('Average Latency', stats.avg === null ? null : `${stats.avg} ms`);
        const replies = report.section('REPLIES');
        data.replies.forEach((reply, index) => {
          replies.add(`Packet ${index + 1}`, `${reply.Status}${reply.Time !== null ? ` - ${reply.Time} ms` : ''}`);
        });
      });

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

    return kit.done('Ping test finished.');
  },
};
