import * as plugins from './smartnetwork.plugins.js';
import * as stats from './helpers/stats.js';

export class CloudflareSpeed {
  constructor() {}

  public async speedTest() {
    const latency = await this.measureLatency();

    const serverLocations = await this.fetchServerLocations();
    const cgiData = await this.fetchCfCdnCgiTrace();

    // lets test the download speed
    const testDown1 = await this.measureDownload(101000, 10);
    const testDown2 = await this.measureDownload(1001000, 8);
    const testDown3 = await this.measureDownload(10001000, 6);
    const testDown4 = await this.measureDownload(25001000, 4);
    const testDown5 = await this.measureDownload(100001000, 1);
    const downloadTests = [...testDown1, ...testDown2, ...testDown3, ...testDown4, ...testDown5];
    const speedDownload = stats.quartile(downloadTests, 0.9).toFixed(2);

    // lets test the upload speed
    const testUp1 = await this.measureUpload(11000, 10);
    const testUp2 = await this.measureUpload(101000, 10);
    const testUp3 = await this.measureUpload(1001000, 8);
    const uploadTests = [...testUp1, ...testUp2, ...testUp3];
    const speedUpload = stats.quartile(uploadTests, 0.9).toFixed(2);

    return {
      ...latency,
      ip: cgiData.ip,
      serverLocation: {
        shortId: cgiData.colo,
        name: serverLocations[cgiData.colo],
        availableLocations: serverLocations,
      },
      downloadSpeed: speedDownload,
      uploadSpeed: speedUpload,
    };
  }

  public async measureLatency() {
    const measurements: number[] = [];

    for (let i = 0; i < 20; i += 1) {
      await this.download(1000).then(
        (response) => {
          // TTFB - Server processing time
          measurements.push(response[4] - response[0] - response[6]);
        },
        (error) => {
          console.log(`Error: ${error}`);
        }
      );
    }

    return {
      maxTime: Math.max(...measurements),
      minTime: Math.min(...measurements),
      averageTime: stats.average(measurements),
      medianTime: stats.median(measurements),
      jitter: stats.jitter(measurements),
    };
  }

  public async measureDownload(bytes: number, iterations: number) {
    const measurements: number[] = [];

    for (let i = 0; i < iterations; i += 1) {
      await this.download(bytes).then(
        async (response) => {
          const transferTime = response[5] - response[4];
          measurements.push(await this.measureSpeed(bytes, transferTime));
        },
        (error) => {
          console.log(`Error: ${error}`);
        }
      );
    }

    return measurements;
  }

  public async measureUpload(bytes: number, iterations: number) {
    const measurements: number[] = [];

    for (let i = 0; i < iterations; i += 1) {
      await this.upload(bytes).then(
        async (response) => {
          const transferTime = response[6];
          measurements.push(await this.measureSpeed(bytes, transferTime));
        },
        (error) => {
          console.log(`Error: ${error}`);
        }
      );
    }

    return measurements;
  }

  public async measureSpeed(bytes: number, duration: number) {
    return (bytes * 8) / (duration / 1000) / 1e6;
  }

  public async fetchServerLocations(): Promise<{ [key: string]: string }> {
    const res = JSON.parse(await this.get('speed.cloudflare.com', '/locations'));

    return res.reduce((data: any, optionsArg: { iata: string; city: string }) => {
      // Bypass prettier "no-assign-param" rules
      const data1 = data;

      data1[optionsArg.iata] = optionsArg.city;
      return data1;
    }, {});
  }

  public async get(hostname: string, path: string): Promise<string> {
    return new Promise((resolve, reject) => {
      const req = plugins.https.request(
        {
          hostname,
          path,
          method: 'GET',
        },
        (res) => {
          const body: Array<Buffer> = [];
          res.on('data', (chunk) => {
            body.push(chunk);
          });
          res.on('end', () => {
            try {
              resolve(Buffer.concat(body).toString());
            } catch (e) {
              reject(e);
            }
          });
          req.on('error', (err) => {
            reject(err);
          });
        }
      );

      req.end();
    });
  }

  public async download(bytes: number) {
    const options = {
      hostname: 'speed.cloudflare.com',
      path: `/__down?bytes=${bytes}`,
      method: 'GET',
    };

    return this.request(options);
  }

  public async upload(bytes: number) {
    const data = '0'.repeat(bytes);
    const options = {
      hostname: 'speed.cloudflare.com',
      path: '/__up',
      method: 'POST',
      headers: {
        'Content-Length': Buffer.byteLength(data),
      },
    };

    return this.request(options, data);
  }

  public async request(options: plugins.https.RequestOptions, data = ''): Promise<number[]> {
    let started: number;
    let dnsLookup: number;
    let tcpHandshake: number;
    let sslHandshake: number;
    let ttfb: number;
    let ended: number;

    return new Promise((resolve, reject) => {
      started = plugins.perfHooks.performance.now();
      const req = plugins.https.request(options, (res) => {
        res.once('readable', () => {
          ttfb = plugins.perfHooks.performance.now();
        });
        res.on('data', () => {});
        res.on('end', () => {
          ended = plugins.perfHooks.performance.now();
          resolve([
            started,
            dnsLookup,
            tcpHandshake,
            sslHandshake,
            ttfb,
            ended,
            parseFloat(res.headers['server-timing'].slice(22) as any),
          ]);
        });
      });

      req.on('socket', (socket) => {
        socket.on('lookup', () => {
          dnsLookup = plugins.perfHooks.performance.now();
        });
        socket.on('connect', () => {
          tcpHandshake = plugins.perfHooks.performance.now();
        });
        socket.on('secureConnect', () => {
          sslHandshake = plugins.perfHooks.performance.now();
        });
      });

      req.on('error', (error) => {
        reject(error);
      });

      req.write(data);
      req.end();
    });
  }

  public async fetchCfCdnCgiTrace(): Promise<{
    fl: string;
    h: string;
    ip: string;
    ts: string;
    visit_scheme: string;
    uag: string;
    colo: string;
    http: string;
    loc: string;
    tls: string;
    sni: string;
    warp: string;
    gateway: string;
  }> {
    const parseCfCdnCgiTrace = (text: string) =>
      text
        .split('\n')
        .map((i) => {
          const j = i.split('=');

          return [j[0], j[1]];
        })
        .reduce((data: any, [k, v]) => {
          if (v === undefined) return data;

          // Bypass prettier "no-assign-param" rules
          const data1 = data;
          // Object.fromEntries is only supported by Node.js 12 or newer
          data1[k] = v;

          return data1;
        }, {});

    return this.get('speed.cloudflare.com', '/cdn-cgi/trace').then(parseCfCdnCgiTrace);
  }
}
