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

import type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';

/* eslint-disable @typescript-eslint/no-explicit-any */

class HARBase {
  readonly custom: Map<string, any>;
  constructor(data: any) {
    if (!data || typeof data !== 'object') {
      throw new Error('First parameter is expected to be an object');
    }
    this.custom = new Map();
  }

  static safeDate(data: any): Date {
    const date = new Date(data);
    if (!Number.isNaN(date.getTime())) {
      return date;
    }
    throw new Error('Invalid date format');
  }

  static safeNumber(data: any): number {
    const result = Number(data);
    if (!Number.isNaN(result)) {
      return result;
    }
    throw new Error('Casting to number results in NaN');
  }

  static optionalNumber(data: any): number|undefined {
    return data !== undefined ? HARBase.safeNumber(data) : undefined;
  }

  static optionalString(data: any): string|undefined {
    return data !== undefined ? String(data) : undefined;
  }

  customAsString(name: string): string|undefined {
    const value = this.custom.get(name);
    if (!value) {
      return undefined;
    }
    return String(value);
  }

  customAsNumber(name: string): number|undefined {
    const value = this.custom.get(name);
    if (!value) {
      return undefined;
    }
    const numberValue = Number(value);
    if (Number.isNaN(numberValue)) {
      return undefined;
    }
    return numberValue;
  }

  customAsArray(name: string): any[]|undefined {
    const value = this.custom.get(name);
    if (!value) {
      return undefined;
    }
    return Array.isArray(value) ? value : undefined;
  }

  customInitiator(): HARInitiator|undefined {
    return this.custom.get('initiator');
  }
}

export class HARRoot extends HARBase {
  log: HARLog;
  constructor(data: any) {
    super(data);
    this.log = new HARLog(data['log']);
  }
}

export class HARLog extends HARBase {
  version: string;
  creator: HARCreator;
  browser: HARCreator|undefined;
  pages: HARPage[];
  entries: HAREntry[];
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.version = String(data['version']);
    this.creator = new HARCreator(data['creator']);
    this.browser = data['browser'] ? new HARCreator(data['browser']) : undefined;
    this.pages = Array.isArray(data['pages']) ? data['pages'].map(page => new HARPage(page)) : [];
    if (!Array.isArray(data['entries'])) {
      throw new Error('log.entries is expected to be an array');
    }
    this.entries = data['entries'].map(entry => new HAREntry(entry));
    this.comment = HARBase.optionalString(data['comment']);
  }
}

class HARCreator extends HARBase {
  name: string;
  version: string;
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.name = String(data['name']);
    this.version = String(data['version']);
    this.comment = HARBase.optionalString(data['comment']);
  }
}

export class HARPage extends HARBase {
  startedDateTime: Date;
  id: string;
  title: string;
  pageTimings: HARPageTimings;
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.startedDateTime = HARBase.safeDate(data['startedDateTime']);
    this.id = String(data['id']);
    this.title = String(data['title']);
    this.pageTimings = new HARPageTimings(data['pageTimings']);
    this.comment = HARBase.optionalString(data['comment']);
  }
}

class HARPageTimings extends HARBase {
  onContentLoad: number|undefined;
  onLoad: number|undefined;
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.onContentLoad = HARBase.optionalNumber(data['onContentLoad']);
    this.onLoad = HARBase.optionalNumber(data['onLoad']);
    this.comment = HARBase.optionalString(data['comment']);
  }
}

export class HAREntry extends HARBase {
  pageref: string|undefined;
  startedDateTime: Date;
  time: number;
  request: HARRequest;
  response: HARResponse;
  timings: HARTimings;
  serverIPAddress: string|undefined;
  connection: string|undefined;
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.pageref = HARBase.optionalString(data['pageref']);
    this.startedDateTime = HARBase.safeDate(data['startedDateTime']);
    this.time = HARBase.safeNumber(data['time']);
    this.request = new HARRequest(data['request']);
    this.response = new HARResponse(data['response']);
    this.timings = new HARTimings(data['timings']);
    this.serverIPAddress = HARBase.optionalString(data['serverIPAddress']);
    this.connection = HARBase.optionalString(data['connection']);
    this.comment = HARBase.optionalString(data['comment']);

    // Chrome specific.
    this.custom.set('connectionId', HARBase.optionalString(data['_connectionId']));
    this.custom.set('fromCache', HARBase.optionalString(data['_fromCache']));
    this.custom.set('initiator', this.importInitiator(data['_initiator']));
    this.custom.set('priority', HARBase.optionalString(data['_priority']));
    this.custom.set('resourceType', HARBase.optionalString(data['_resourceType']));
    this.custom.set('eventSourceMessages', this.#importEventSourceMessages(data['_eventSourceMessages']));
    this.custom.set('webSocketMessages', this.importWebSocketMessages(data['_webSocketMessages']));
  }

  private importInitiator(initiator: any): HARInitiator|undefined {
    if (typeof initiator !== 'object') {
      return;
    }

    return new HARInitiator(initiator);
  }

  private importWebSocketMessages(inputMessages: any): HARWebSocketMessage[]|undefined {
    if (!Array.isArray(inputMessages)) {
      return;
    }

    const outputMessages = [];
    for (const message of inputMessages) {
      if (typeof message !== 'object') {
        return;
      }
      outputMessages.push(new HARWebSocketMessage(message));
    }
    return outputMessages;
  }

  #importEventSourceMessages(inputMessages: any): HAREventSourceMessage[]|undefined {
    if (!Array.isArray(inputMessages)) {
      return;
    }

    const outputMessages = [];
    for (const message of inputMessages) {
      if (typeof message !== 'object') {
        return;
      }
      outputMessages.push(new HAREventSourceMessage(message));
    }
    return outputMessages;
  }
}

class HARRequest extends HARBase {
  method: string;
  url: Platform.DevToolsPath.UrlString;
  httpVersion: string;
  cookies: HARCookie[];
  headers: HARHeader[];
  queryString: HARQueryString[];
  postData: HARPostData|undefined;
  headersSize: number;
  bodySize: number;
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.method = String(data['method']);
    this.url = String(data['url']) as Platform.DevToolsPath.UrlString;
    this.httpVersion = String(data['httpVersion']);
    this.cookies = Array.isArray(data['cookies']) ? data['cookies'].map(cookie => new HARCookie(cookie)) : [];
    this.headers = Array.isArray(data['headers']) ? data['headers'].map(header => new HARHeader(header)) : [];
    this.queryString = Array.isArray(data['queryString']) ? data['queryString'].map(qs => new HARQueryString(qs)) : [];
    this.postData = data['postData'] ? new HARPostData(data['postData']) : undefined;
    this.headersSize = HARBase.safeNumber(data['headersSize']);
    this.bodySize = HARBase.safeNumber(data['bodySize']);
    this.comment = HARBase.optionalString(data['comment']);
  }
}

class HARResponse extends HARBase {
  status: number;
  statusText: string;
  httpVersion: string;
  cookies: HARCookie[];
  headers: HARHeader[];
  content: HARContent;
  redirectURL: string;
  headersSize: number;
  bodySize: number;
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.status = HARBase.safeNumber(data['status']);
    this.statusText = String(data['statusText']);
    this.httpVersion = String(data['httpVersion']);
    this.cookies = Array.isArray(data['cookies']) ? data['cookies'].map(cookie => new HARCookie(cookie)) : [];
    this.headers = Array.isArray(data['headers']) ? data['headers'].map(header => new HARHeader(header)) : [];
    this.content = new HARContent(data['content']);
    this.redirectURL = String(data['redirectURL']);
    this.headersSize = HARBase.safeNumber(data['headersSize']);
    this.bodySize = HARBase.safeNumber(data['bodySize']);
    this.comment = HARBase.optionalString(data['comment']);

    // Chrome specific.
    this.custom.set('transferSize', HARBase.optionalNumber(data['_transferSize']));
    this.custom.set('error', HARBase.optionalString(data['_error']));
    this.custom.set('fetchedViaServiceWorker', Boolean(data['_fetchedViaServiceWorker']));
    this.custom.set('responseCacheStorageCacheName', HARBase.optionalString(data['_responseCacheStorageCacheName']));
    this.custom.set('serviceWorkerResponseSource', HARBase.optionalString(data['_serviceWorkerResponseSource']));
    this.custom.set(
        'serviceWorkerRouterRuleIdMatched', HARBase.optionalNumber(data['_serviceWorkerRouterRuleIdMatched']));
    this.custom.set(
        'serviceWorkerRouterMatchedSourceType', HARBase.optionalString(data['_serviceWorkerRouterMatchedSourceType']));
    this.custom.set(
        'serviceWorkerRouterActualSourceType', HARBase.optionalString(data['_serviceWorkerRouterActualSourceType']));
  }
}

export class HARCookie extends HARBase {
  name: string;
  value: string;
  path: string|undefined;
  domain: string|undefined;
  expires: Date|undefined;
  httpOnly: boolean|undefined;
  secure: boolean|undefined;
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.name = String(data['name']);
    this.value = String(data['value']);
    this.path = HARBase.optionalString(data['path']);
    this.domain = HARBase.optionalString(data['domain']);
    this.expires = data['expires'] ? HARBase.safeDate(data['expires']) : undefined;
    this.httpOnly = data['httpOnly'] !== undefined ? Boolean(data['httpOnly']) : undefined;
    this.secure = data['secure'] !== undefined ? Boolean(data['secure']) : undefined;
    this.comment = HARBase.optionalString(data['comment']);
  }
}

class HARHeader extends HARBase {
  name: string;
  value: string;
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.name = String(data['name']);
    this.value = String(data['value']);
    this.comment = HARBase.optionalString(data['comment']);
  }
}

class HARQueryString extends HARBase {
  name: string;
  value: string;
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.name = String(data['name']);
    this.value = String(data['value']);
    this.comment = HARBase.optionalString(data['comment']);
  }
}

class HARPostData extends HARBase {
  mimeType: string;
  params: HARParam[];
  text: string;
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.mimeType = String(data['mimeType']);
    this.params = Array.isArray(data['params']) ? data['params'].map(param => new HARParam(param)) : [];
    this.text = String(data['text']);
    this.comment = HARBase.optionalString(data['comment']);
  }
}

export class HARParam extends HARBase {
  name: string;
  value: string|undefined;
  fileName: string|undefined;
  contentType: string|undefined;
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.name = String(data['name']);
    this.value = HARBase.optionalString(data['value']);
    this.fileName = HARBase.optionalString(data['fileName']);
    this.contentType = HARBase.optionalString(data['contentType']);
    this.comment = HARBase.optionalString(data['comment']);
  }
}

class HARContent extends HARBase {
  size: number;
  compression: number|undefined;
  mimeType: string;
  text: string|undefined;
  encoding: string|undefined;
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.size = HARBase.safeNumber(data['size']);
    this.compression = HARBase.optionalNumber(data['compression']);
    this.mimeType = String(data['mimeType']);
    this.text = HARBase.optionalString(data['text']);
    this.encoding = HARBase.optionalString(data['encoding']);
    this.comment = HARBase.optionalString(data['comment']);
  }
}

export class HARTimings extends HARBase {
  blocked: number|undefined;
  dns: number|undefined;
  connect: number|undefined;
  send: number;
  wait: number;
  receive: number;
  ssl: number|undefined;
  comment: string|undefined;
  constructor(data: any) {
    super(data);
    this.blocked = HARBase.optionalNumber(data['blocked']);
    this.dns = HARBase.optionalNumber(data['dns']);
    this.connect = HARBase.optionalNumber(data['connect']);
    this.send = HARBase.safeNumber(data['send']);
    this.wait = HARBase.safeNumber(data['wait']);
    this.receive = HARBase.safeNumber(data['receive']);
    this.ssl = HARBase.optionalNumber(data['ssl']);
    this.comment = HARBase.optionalString(data['comment']);

    // Chrome specific.
    this.custom.set('blocked_queueing', HARBase.optionalNumber(data['_blocked_queueing']));
    this.custom.set('blocked_proxy', HARBase.optionalNumber(data['_blocked_proxy']));

    // Service Worker timing info (Chrome specific).
    this.custom.set('workerStart', HARBase.optionalNumber(data['_workerStart']));
    this.custom.set('workerReady', HARBase.optionalNumber(data['_workerReady']));
    this.custom.set('workerFetchStart', HARBase.optionalNumber(data['_workerFetchStart']));
    this.custom.set('workerRespondWithSettled', HARBase.optionalNumber(data['_workerRespondWithSettled']));
    this.custom.set('workerRouterEvaluationStart', HARBase.optionalNumber(data['_workerRouterEvaluationStart']));
    this.custom.set('workerCacheLookupStart', HARBase.optionalNumber(data['_workerCacheLookupStart']));
  }
}

export class HARInitiator extends HARBase {
  type: Protocol.Network.InitiatorType;
  url?: string;
  lineNumber?: number;
  requestId?: Protocol.Network.RequestId;
  stack?: HARStack;
  /**
   * Based on Protocol.Network.Initiator defined in browser_protocol.pdl
   */
  constructor(data: any) {
    super(data);
    this.type = (HARBase.optionalString(data['type']) ?? SDK.NetworkRequest.InitiatorType.OTHER) as
        Protocol.Network.InitiatorType;
    this.url = HARBase.optionalString(data['url']);
    this.lineNumber = HARBase.optionalNumber(data['lineNumber']);
    this.requestId = HARBase.optionalString(data['requestId']) as Protocol.Network.RequestId;
    if (data['stack']) {
      this.stack = new HARStack(data['stack']);
    }
  }
}

export class HARStack extends HARBase {
  description?: string;
  callFrames: HARCallFrame[];
  parent?: HARStack;
  parentId?: {
    id: string,
    debuggerId?: Protocol.Runtime.UniqueDebuggerId,
  };
  /**
   * Based on Protocol.Runtime.StackTrace defined in browser_protocol.pdl
   */
  constructor(data: any) {
    super(data);

    this.callFrames = Array.isArray(data.callFrames) ?
        data.callFrames.map((item: any) => item ? new HARCallFrame(item) : null).filter(Boolean) :
        [];

    if (data['parent']) {
      this.parent = new HARStack(data['parent']);
    }

    this.description = HARBase.optionalString(data['description']);

    const parentId = data['parentId'];
    if (parentId) {
      this.parentId = {
        id: HARBase.optionalString(parentId['id']) ?? '',
        debuggerId: HARBase.optionalString(parentId['debuggerId']) as Protocol.Runtime.UniqueDebuggerId | undefined,
      };
    }
  }
}

export class HARCallFrame extends HARBase {
  functionName: string;
  scriptId: Protocol.Runtime.ScriptId;
  url = '';
  lineNumber = -1;
  columnNumber = -1;
  /**
   * Based on Protocol.Runtime.CallFrame defined in browser_protocol.pdl
   */
  constructor(data: any) {
    super(data);

    this.functionName = HARBase.optionalString(data['functionName']) ?? '';
    this.scriptId = (HARBase.optionalString(data['scriptId']) ?? '') as Protocol.Runtime.ScriptId;
    this.url = HARBase.optionalString(data['url']) ?? '';
    this.lineNumber = HARBase.optionalNumber(data['lineNumber']) ?? -1;
    this.columnNumber = HARBase.optionalNumber(data['columnNumber']) ?? -1;
  }
}

class HARWebSocketMessage extends HARBase {
  time: number|undefined;
  opcode: number|undefined;
  data: string|undefined;
  type: string|undefined;
  constructor(data: any) {
    super(data);
    this.time = HARBase.optionalNumber(data['time']);
    this.opcode = HARBase.optionalNumber(data['opcode']);
    this.data = HARBase.optionalString(data['data']);
    this.type = HARBase.optionalString(data['type']);
  }
}

class HAREventSourceMessage extends HARBase {
  time: number|undefined;
  eventName: string|undefined;
  eventId: string|undefined;
  data: string|undefined;
  constructor(data: any) {
    super(data);
    this.time = HARBase.optionalNumber(data['time']);
    this.eventName = HARBase.optionalString(data['eventName']);
    this.eventId = HARBase.optionalString(data['eventId']);
    this.data = HARBase.optionalString(data['data']);
  }
}
