// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/* eslint-disable @typescript-eslint/naming-convention */

import * as Common from '../common/common.js';
import * as i18n from '../i18n/i18n.js';

import {InspectorFrontendHostInstance} from './InspectorFrontendHost.js';
import type {LoadNetworkResourceResult} from './InspectorFrontendHostAPI.js';

const UIStrings = {
  /**
   * @description Name of an error category used in error messages
   */
  systemError: 'System error',
  /**
   * @description Name of an error category used in error messages
   */
  connectionError: 'Connection error',
  /**
   * @description Name of an error category used in error messages
   */
  certificateError: 'Certificate error',
  /**
   * @description Name of an error category used in error messages
   */
  httpError: 'HTTP error',
  /**
   * @description Name of an error category used in error messages
   */
  cacheError: 'Cache error',
  /**
   * @description Name of an error category used in error messages
   */
  signedExchangeError: 'Signed Exchange error',
  /**
   * @description Name of an error category used in error messages
   */
  ftpError: 'FTP error',
  /**
   * @description Name of an error category used in error messages
   */
  certificateManagerError: 'Certificate manager error',
  /**
   * @description Name of an error category used in error messages
   */
  dnsResolverError: 'DNS resolver error',
  /**
   * @description Name of an error category used in error messages
   */
  unknownError: 'Unknown error',
  /**
   * @description Phrase used in error messages that carry a network error name
   * @example {404} PH1
   * @example {net::ERR_INSUFFICIENT_RESOURCES} PH2
   */
  httpErrorStatusCodeSS: 'HTTP error: status code {PH1}, {PH2}',
  /**
   * @description Name of an error category used in error messages
   */
  invalidUrl: 'Invalid URL',
  /**
   * @description Name of an error category used in error messages
   */
  decodingDataUrlFailed: 'Decoding Data URL failed',
} as const;
const str_ = i18n.i18n.registerUIStrings('core/host/ResourceLoader.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export const ResourceLoader = {};

let _lastStreamId = 0;

const _boundStreams: Record<number, Common.StringOutputStream.OutputStream> = {};

export const bindOutputStream = function(stream: Common.StringOutputStream.OutputStream): number {
  _boundStreams[++_lastStreamId] = stream;
  return _lastStreamId;
};

export const discardOutputStream = function(id: number): void {
  void _boundStreams[id].close();
  delete _boundStreams[id];
};

export const streamWrite = function(id: number, chunk: string): void {
  void _boundStreams[id].write(chunk);
};
export interface LoadErrorDescription {
  statusCode: number;
  netError?: number;
  netErrorName?: string;
  urlValid?: boolean;
  message?: string;
}

export const load = function(
    url: string, headers: Record<string, string>|null,
    callback: (
        arg0: boolean,
        arg1: Record<string, string>,
        arg2: string,
        arg3: LoadErrorDescription,
        ) => void,
    allowRemoteFilePaths: boolean): void {
  const stream = new Common.StringOutputStream.StringOutputStream();
  loadAsStream(url, headers, stream, mycallback, allowRemoteFilePaths);

  function mycallback(
      success: boolean,
      headers: Record<string, string>,
      errorDescription: LoadErrorDescription,
      ): void {
    callback(success, headers, stream.data(), errorDescription);
  }
};

function getNetErrorCategory(netError: number): string {
  if (netError > -100) {
    return i18nString(UIStrings.systemError);
  }
  if (netError > -200) {
    return i18nString(UIStrings.connectionError);
  }
  if (netError > -300) {
    return i18nString(UIStrings.certificateError);
  }
  if (netError > -400) {
    return i18nString(UIStrings.httpError);
  }
  if (netError > -500) {
    return i18nString(UIStrings.cacheError);
  }
  if (netError > -600) {
    return i18nString(UIStrings.signedExchangeError);
  }
  if (netError > -700) {
    return i18nString(UIStrings.ftpError);
  }
  if (netError > -800) {
    return i18nString(UIStrings.certificateManagerError);
  }
  if (netError > -900) {
    return i18nString(UIStrings.dnsResolverError);
  }
  return i18nString(UIStrings.unknownError);
}

function isHTTPError(netError: number): boolean {
  return netError <= -300 && netError > -400;
}

export function netErrorToMessage(
    netError: number|undefined, httpStatusCode: number|undefined, netErrorName: string|undefined): string|null {
  if (netError === undefined || netErrorName === undefined) {
    return null;
  }
  if (netError !== 0) {
    if (isHTTPError(netError)) {
      return i18nString(UIStrings.httpErrorStatusCodeSS, {PH1: String(httpStatusCode), PH2: netErrorName});
    }
    const errorCategory = getNetErrorCategory(netError);
    // We don't localize here, as `errorCategory` is already localized,
    // and `netErrorName` is an error code like 'net::ERR_CERT_AUTHORITY_INVALID'.
    return `${errorCategory}: ${netErrorName}`;
  }
  return null;
}

function createErrorMessageFromResponse(response: LoadNetworkResourceResult): {
  success: boolean,
  description: LoadErrorDescription,
} {
  const {statusCode, netError, netErrorName, urlValid, messageOverride} = response;
  let message = '';
  const success = statusCode >= 200 && statusCode < 300;
  if (typeof messageOverride === 'string') {
    message = messageOverride;
  } else if (!success) {
    if (typeof netError === 'undefined') {
      if (urlValid === false) {
        message = i18nString(UIStrings.invalidUrl);
      } else {
        message = i18nString(UIStrings.unknownError);
      }
    } else {
      const maybeMessage = netErrorToMessage(netError, statusCode, netErrorName);
      if (maybeMessage) {
        message = maybeMessage;
      }
    }
  }
  console.assert(success === (message.length === 0));
  return {success, description: {statusCode, netError, netErrorName, urlValid, message}};
}

async function fetchToString(url: string): Promise<string> {
  try {
    const response = await fetch(url);
    return await response.text();
  } catch (cause) {
    throw new Error(`Failed to fetch ${url}`, {cause});
  }
}

function canBeRemoteFilePath(url: string): boolean {
  try {
    const urlObject = new URL(new URL(url).toString());  // Normalize first.
    return urlObject.protocol === 'file:' && urlObject.host !== '';
  } catch {
    return false;
  }
}

export const loadAsStream = function(
    url: string,
    headers: Record<string, string>|null,
    stream: Common.StringOutputStream.OutputStream,
    callback?: ((arg0: boolean, arg1: Record<string, string>, arg2: LoadErrorDescription) => void),
    allowRemoteFilePaths?: boolean,
    ): void {
  const streamId = bindOutputStream(stream);
  const parsedURL = new Common.ParsedURL.ParsedURL(url);
  if (parsedURL.isDataURL()) {
    fetchToString(url).then(dataURLDecodeSuccessful).catch(dataURLDecodeFailed);
    return;
  }

  if (!allowRemoteFilePaths && canBeRemoteFilePath(url)) {
    // Remote file paths can cause security problems, see crbug.com/1342722.
    if (callback) {
      callback(/* success */ false, /* headers */ {}, {
        statusCode: 400,  // BAD_REQUEST
        netError: -20,    // BLOCKED_BY_CLIENT
        netErrorName: 'net::BLOCKED_BY_CLIENT',
        message: 'Loading from a remote file path is prohibited for security reasons.',
      });
    }
    return;
  }

  const rawHeaders = [];
  if (headers) {
    for (const key in headers) {
      rawHeaders.push(key + ': ' + headers[key]);
    }
  }
  InspectorFrontendHostInstance.loadNetworkResource(url, rawHeaders.join('\r\n'), streamId, finishedCallback);

  function finishedCallback(response: LoadNetworkResourceResult): void {
    if (callback) {
      const {success, description} = createErrorMessageFromResponse(response);
      callback(success, response.headers || {}, description);
    }
    discardOutputStream(streamId);
  }

  function dataURLDecodeSuccessful(text: string): void {
    streamWrite(streamId, text);
    finishedCallback(({statusCode: 200}));
  }

  function dataURLDecodeFailed(_xhrStatus: Error): void {
    const messageOverride: string = i18nString(UIStrings.decodingDataUrlFailed);
    finishedCallback(({statusCode: 404, messageOverride}));
  }
};
