/**
 * Function/Network/speed-test.js
 * ---------------------------------------------------------------------------
 * INTERNET SPEED TEST
 *
 * Measures latency, download and upload throughput against Cloudflare's public
 * speed-test endpoints. No account, no API key, no third-party package.
 *
 * A console speed test is an indication, not a laboratory measurement: it is a
 * single stream and is limited by the endpoint, the route and the machine.
 * ---------------------------------------------------------------------------
 */

'use strict';

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

const HOST = 'speed.cloudflare.com';
const DOWNLOAD_PATH = '/__down?bytes=';
const UPLOAD_PATH = '/__up';

/** Timed HTTPS GET that discards the body. */
function download(bytes, timeoutMs = 30000) {
  return new Promise((resolve) => {
    const started = Date.now();
    let received = 0;
    let settled = false;
    const finish = (value) => {
      if (!settled) {
        settled = true;
        resolve(value);
      }
    };

    const request = https.get(
      { host: HOST, path: DOWNLOAD_PATH + bytes, headers: { 'User-Agent': 'InfinitySniper-HFX' }, timeout: timeoutMs },
      (response) => {
        if (response.statusCode !== 200) {
          response.resume();
          return finish({ ok: false, error: `HTTP ${response.statusCode}` });
        }
        response.on('data', (chunk) => {
          received += chunk.length;
        });
        response.on('end', () => {
          const ms = Date.now() - started;
          finish({ ok: true, bytes: received, ms, bps: ms > 0 ? (received * 8) / (ms / 1000) : 0 });
        });
        return undefined;
      }
    );

    request.on('timeout', () => {
      request.destroy();
      finish({ ok: false, error: 'timeout' });
    });
    request.on('error', (err) => finish({ ok: false, error: err.code || err.message }));
  });
}

/** Timed HTTPS POST of generated data. */
function upload(bytes, timeoutMs = 30000) {
  return new Promise((resolve) => {
    const payload = Buffer.alloc(bytes, 0x41);
    const started = Date.now();
    let settled = false;
    const finish = (value) => {
      if (!settled) {
        settled = true;
        resolve(value);
      }
    };

    const request = https.request(
      {
        host: HOST,
        path: UPLOAD_PATH,
        method: 'POST',
        headers: {
          'Content-Type': 'application/octet-stream',
          'Content-Length': payload.length,
          'User-Agent': 'InfinitySniper-HFX',
        },
        timeout: timeoutMs,
      },
      (response) => {
        response.resume();
        response.on('end', () => {
          const ms = Date.now() - started;
          finish({ ok: true, bytes: payload.length, ms, bps: ms > 0 ? (payload.length * 8) / (ms / 1000) : 0 });
        });
      }
    );

    request.on('timeout', () => {
      request.destroy();
      finish({ ok: false, error: 'timeout' });
    });
    request.on('error', (err) => finish({ ok: false, error: err.code || err.message }));
    request.end(payload);
  });
}

/** Latency = time to first byte of a tiny download, best of several. */
async function latency(samples = 5) {
  const times = [];
  for (let i = 0; i < samples; i += 1) {
    // eslint-disable-next-line no-await-in-loop
    const result = await download(1, 8000);
    if (result.ok) times.push(result.ms);
  }
  if (!times.length) return null;
  return {
    min: Math.min(...times),
    max: Math.max(...times),
    avg: Math.round(times.reduce((a, b) => a + b, 0) / times.length),
    jitter: Math.round(Math.max(...times) - Math.min(...times)),
  };
}

function mbps(bitsPerSecond) {
  return `${(bitsPerSecond / 1e6).toFixed(2)} Mbps`;
}

module.exports = {
  name: 'Internet Speed Test',
  menuName: 'Internet Speed Test',
  description: 'Measures latency, download and upload speed against a public endpoint.',
  version: '1.0.0',
  author: 'InfinitySniper HFX',
  category: 'Network',
  order: 70,
  requiredPermissions: 'normal',
  destructive: false,
  dependencies: [],
  minimumAppVersion: '1.0.0',
  permissions: ['network:outbound(https)'],
  tags: ['speed', 'bandwidth', 'download', 'upload', 'internet', 'test'],
  config: { downloadMb: 25, uploadMb: 8 },
  documentation:
    'Runs three measurements against Cloudflare\'s public speed-test endpoint: latency (time to first ' +
    'byte, best of five), download throughput and upload throughput. It uses a single connection, ' +
    'so on very fast lines the result can be lower than a browser test that opens many streams in ' +
    'parallel. Treat the numbers as an indication of the current connection quality.',

  async selfTest() {
    return { ok: true, message: 'ready (requires internet at run time)' };
  },

  async execute(ctx) {
    const { ui, prompt, config } = ctx;
    kit.header('Internet Speed Test', [
      'Latency, download and upload against a public endpoint.',
      'A single-stream test - treat the result as an indication.',
    ]);

    const downloadBytes = Math.max(1, Number(config.downloadMb) || 25) * 1024 * 1024;
    const uploadBytes = Math.max(1, Number(config.uploadMb) || 8) * 1024 * 1024;

    ui.blank();
    kit.kv('Endpoint', HOST);
    kit.kv('Download Size', system.formatBytes(downloadBytes));
    kit.kv('Upload Size', system.formatBytes(uploadBytes));
    ui.blank();

    const start = await prompt.confirm('Start the speed test now?');
    if (start !== true) return kit.done('Speed test cancelled.');

    // ---- latency --------------------------------------------------------
    ui.blank();
    const spin1 = ui.spinner('Measuring latency...');
    const ping = await latency();
    spin1.stop();

    if (!ping) {
      ui.blank();
      ui.error('The speed-test endpoint could not be reached.');
      ui.println('  ' + ui.c.gray('Check your internet connection, proxy or firewall.'));
      return { ok: false, reason: 'offline' };
    }
    ui.ok(`Latency: ${ping.avg} ms (min ${ping.min} ms, jitter ${ping.jitter} ms)`);

    // ---- download -------------------------------------------------------
    const spin2 = ui.spinner(`Downloading ${system.formatBytes(downloadBytes)}...`);
    const down = await download(downloadBytes, 60000);
    spin2.stop();
    if (down.ok) ui.ok(`Download: ${mbps(down.bps)}`);
    else ui.warning(`Download failed: ${down.error}`);

    // ---- upload ---------------------------------------------------------
    const spin3 = ui.spinner(`Uploading ${system.formatBytes(uploadBytes)}...`);
    const up = await upload(uploadBytes, 60000);
    spin3.stop();
    if (up.ok) ui.ok(`Upload: ${mbps(up.bps)}`);
    else ui.warning(`Upload failed: ${up.error}`);

    // ---- result ---------------------------------------------------------
    ui.blank();
    ui.section('RESULT');
    ui.blank();
    kit.kv('Ping (average)', `${ping.avg} ms`, 'brightGreen');
    kit.kv('Ping (min / max)', `${ping.min} ms / ${ping.max} ms`);
    kit.kv('Jitter', `${ping.jitter} ms`);
    kit.kv('Download Speed', down.ok ? mbps(down.bps) : null, 'brightGreen');
    kit.kv('Download Transferred', down.ok ? `${system.formatBytes(down.bytes)} in ${(down.ms / 1000).toFixed(1)}s` : null);
    kit.kv('Upload Speed', up.ok ? mbps(up.bps) : null, 'brightGreen');
    kit.kv('Upload Transferred', up.ok ? `${system.formatBytes(up.bytes)} in ${(up.ms / 1000).toFixed(1)}s` : null);

    ui.blank();
    if (down.ok) {
      const mbit = down.bps / 1e6;
      if (mbit > 500) ui.completed('Very fast connection.');
      else if (mbit > 100) ui.completed('Fast connection.');
      else if (mbit > 25) ui.ok('Good connection for streaming and gaming.');
      else if (mbit > 5) ui.warning('Modest connection - large downloads will take a while.');
      else ui.warning('Slow connection.');
    }

    await kit.offerExport('Internet Speed Test', 'SpeedTest', (report) => {
      const section = report.section('SPEED TEST');
      section.add('Endpoint', HOST);
      section.add('Ping (average)', `${ping.avg} ms`);
      section.add('Ping (min / max)', `${ping.min} ms / ${ping.max} ms`);
      section.add('Jitter', `${ping.jitter} ms`);
      section.add('Download Speed', down.ok ? mbps(down.bps) : 'failed');
      section.add('Upload Speed', up.ok ? mbps(up.bps) : 'failed');
      section.add('Download Size', system.formatBytes(downloadBytes));
      section.add('Upload Size', system.formatBytes(uploadBytes));
      section.note('Single-stream measurement - a browser test with parallel streams may report more.');
    });

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