/**
 * @license
 * Copyright 2023 Google Inc.
 * SPDX-License-Identifier: Apache-2.0
 */

import {createWriteStream} from 'node:fs';
import * as http from 'node:http';
import * as https from 'node:https';
import {URL, urlToHttpOptions} from 'node:url';

import {ProxyAgent} from 'proxy-agent';

export function headHttpRequest(url: URL): Promise<boolean> {
  return new Promise(resolve => {
    const request = httpRequest(
      url,
      'HEAD',
      response => {
        // consume response data free node process
        response.resume();
        resolve(response.statusCode === 200);
      },
      false,
    );
    request.on('error', () => {
      resolve(false);
    });
  });
}

export function httpRequest(
  url: URL,
  method: string,
  response: (x: http.IncomingMessage) => void,
  keepAlive = true,
): http.ClientRequest {
  const options: http.RequestOptions = {
    protocol: url.protocol,
    hostname: url.hostname,
    port: url.port,
    path: url.pathname + url.search,
    method,
    headers: keepAlive ? {Connection: 'keep-alive'} : undefined,
    auth: urlToHttpOptions(url).auth,
    agent: new ProxyAgent(),
  };

  const requestCallback = (res: http.IncomingMessage): void => {
    if (
      res.statusCode &&
      res.statusCode >= 300 &&
      res.statusCode < 400 &&
      res.headers.location
    ) {
      httpRequest(new URL(res.headers.location), method, response);
      // consume response data to free up memory
      // And prevents the connection from being kept alive
      res.resume();
    } else {
      response(res);
    }
  };
  const request =
    options.protocol === 'https:'
      ? https.request(options, requestCallback)
      : http.request(options, requestCallback);
  request.end();
  return request;
}

/**
 * @internal
 */
export function downloadFile(
  url: URL,
  destinationPath: string,
  progressCallback?: (downloadedBytes: number, totalBytes: number) => void,
): Promise<void> {
  return new Promise<void>((resolve, reject) => {
    let downloadedBytes = 0;
    let totalBytes = 0;

    function onData(chunk: string): void {
      downloadedBytes += chunk.length;
      progressCallback!(downloadedBytes, totalBytes);
    }

    const request = httpRequest(url, 'GET', response => {
      if (response.statusCode !== 200) {
        const error = new Error(
          `Download failed: server returned code ${response.statusCode}. URL: ${url}`,
        );
        // consume response data to free up memory
        response.resume();
        reject(error);
        return;
      }
      const file = createWriteStream(destinationPath);
      file.on('close', () => {
        // The 'close' event is emitted when the stream and any of its
        // underlying resources (a file descriptor, for example) have been
        // closed. The event indicates that no more events will be emitted, and
        // no further computation will occur.
        return resolve();
      });
      file.on('error', error => {
        // The 'error' event may be emitted by a Readable implementation at any
        // time. Typically, this may occur if the underlying stream is unable to
        // generate data due to an underlying internal failure, or when a stream
        // implementation attempts to push an invalid chunk of data.
        return reject(error);
      });
      response.pipe(file);
      totalBytes = parseInt(response.headers['content-length']!, 10);
      if (progressCallback) {
        response.on('data', onData);
      }
    });
    request.on('error', error => {
      return reject(error);
    });
  });
}

export async function getJSON(url: URL): Promise<unknown> {
  const text = await getText(url);
  try {
    return JSON.parse(text);
  } catch {
    throw new Error('Could not parse JSON from ' + url.toString());
  }
}

export function getText(url: URL): Promise<string> {
  return new Promise((resolve, reject) => {
    const request = httpRequest(
      url,
      'GET',
      response => {
        let data = '';
        if (response.statusCode && response.statusCode >= 400) {
          return reject(new Error(`Got status code ${response.statusCode}`));
        }
        response.on('data', chunk => {
          data += chunk;
        });
        response.on('end', () => {
          try {
            return resolve(String(data));
          } catch {
            return reject(
              new Error(`Failed to read text response from ${url}`),
            );
          }
        });
      },
      false,
    );
    request.on('error', err => {
      reject(err);
    });
  });
}
