// Copyright 2018 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 @devtools/no-imperative-dom-api */

import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
import {Icon, Link} from '../../ui/kit/kit.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';

import signedExchangeInfoTreeStyles from './signedExchangeInfoTree.css.js';
import signedExchangeInfoViewStyles from './signedExchangeInfoView.css.js';

const UIStrings = {
  /**
   * @description Text for errors
   */
  errors: 'Errors',
  /**
   * @description Text in Signed Exchange Info View of the Network panel
   */
  signedHttpExchange: 'Signed HTTP exchange',
  /**
   * @description Text for an option to learn more about something
   */
  learnmore: 'Learn more',
  /**
   * @description Text in Request Headers View of the Network panel
   */
  requestUrl: 'Request URL',
  /**
   * @description Text in Signed Exchange Info View of the Network panel
   */
  responseCode: 'Response code',
  /**
   * @description Text in Signed Exchange Info View of the Network panel
   */
  headerIntegrityHash: 'Header integrity hash',
  /**
   * @description Text in Signed Exchange Info View of the Network panel
   */
  responseHeaders: 'Response headers',
  /**
   * @description Text in Signed Exchange Info View of the Network panel
   */
  signature: 'Signature',
  /**
   * @description Text in Signed Exchange Info View of the Network panel
   */
  label: 'Label',
  /**
   * @description Text in Signed Exchange Info View of the Network panel
   */
  certificateUrl: 'Certificate URL',
  /**
   * @description Text to view a security certificate
   */
  viewCertificate: 'View certificate',
  /**
   * @description Text in Signed Exchange Info View of the Network panel
   */
  integrity: 'Integrity',
  /**
   * @description Text in Signed Exchange Info View of the Network panel
   */
  certificateSha: 'Certificate SHA256',
  /**
   * @description Text in Signed Exchange Info View of the Network panel
   */
  validityUrl: 'Validity URL',
  /**
   * @description Text in Signed Exchange Info View of the Network panel
   */
  date: 'Date',
  /**
   * @description Text in Signed Exchange Info View of the Network panel
   */
  expires: 'Expires',
  /**
   * @description Text for a security certificate
   */
  certificate: 'Certificate',
  /**
   * @description Text that refers to the subject of a security certificate
   */
  subject: 'Subject',
  /**
   * @description Text to show since when an item is valid
   */
  validFrom: 'Valid from',
  /**
   * @description Text to indicate the expiry date
   */
  validUntil: 'Valid until',
  /**
   * @description Text for the issuer of an item
   */
  issuer: 'Issuer',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/network/SignedExchangeInfoView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class SignedExchangeInfoView extends UI.Widget.VBox {
  private readonly responseHeadersItem?: UI.TreeOutline.TreeElement;

  constructor(request: SDK.NetworkRequest.NetworkRequest) {
    super();
    this.registerRequiredCSS(signedExchangeInfoViewStyles);
    console.assert(request.signedExchangeInfo() !== null);
    const signedExchangeInfo = (request.signedExchangeInfo() as Protocol.Network.SignedExchangeInfo);

    this.element.classList.add('signed-exchange-info-view');

    const root = new UI.TreeOutline.TreeOutlineInShadow();
    root.registerRequiredCSS(signedExchangeInfoTreeStyles);
    root.element.classList.add('signed-exchange-info-tree');
    root.setFocusable(false);
    root.setDense(true);
    root.expandTreeElementsWhenArrowing = true;
    this.element.appendChild(root.element);

    const errorFieldSetMap = new Map<number|undefined, Set<string>>();

    if (signedExchangeInfo.errors?.length) {
      const errorMessagesCategory = new Category(root, i18nString(UIStrings.errors));
      for (const error of signedExchangeInfo.errors) {
        const fragment = document.createDocumentFragment();
        const icon = new Icon();
        icon.name = 'cross-circle-filled';
        icon.classList.add('prompt-icon', 'small');
        fragment.appendChild(icon);
        fragment.createChild('div', 'error-log').textContent = error.message;
        errorMessagesCategory.createLeaf(fragment);
        if (error.errorField) {
          let errorFieldSet = errorFieldSetMap.get(error.signatureIndex);
          if (!errorFieldSet) {
            errorFieldSet = new Set();
            errorFieldSetMap.set(error.signatureIndex, errorFieldSet);
          }
          errorFieldSet.add(error.errorField);
        }
      }
    }

    const titleElement = document.createDocumentFragment();
    titleElement.createChild('div', 'header-name').textContent = i18nString(UIStrings.signedHttpExchange);
    const learnMoreNode = Link.create(
        'https://github.com/WICG/webpackage', i18nString(UIStrings.learnmore), 'header-toggle', 'learn-more');
    titleElement.appendChild(learnMoreNode);
    const headerCategory = new Category(root, titleElement);
    if (signedExchangeInfo.header) {
      const header = signedExchangeInfo.header;
      const redirectDestination = request.redirectDestination();
      const requestURLElement = this.formatHeader(i18nString(UIStrings.requestUrl), header.requestUrl);
      if (redirectDestination) {
        const viewRequestLink = Components.Linkifier.Linkifier.linkifyRevealable(
            redirectDestination, 'View request', undefined, undefined, undefined, 'redirect-destination-request');
        viewRequestLink.classList.add('header-toggle');
        requestURLElement.appendChild(viewRequestLink);
      }
      headerCategory.createLeaf(requestURLElement);
      headerCategory.createLeaf(this.formatHeader(i18nString(UIStrings.responseCode), String(header.responseCode)));
      headerCategory.createLeaf(this.formatHeader(i18nString(UIStrings.headerIntegrityHash), header.headerIntegrity));

      this.responseHeadersItem =
          headerCategory.createLeaf(this.formatHeader(i18nString(UIStrings.responseHeaders), ''));
      const responseHeaders = header.responseHeaders;
      for (const name in responseHeaders) {
        const headerTreeElement = new UI.TreeOutline.TreeElement(this.formatHeader(name, responseHeaders[name]));
        headerTreeElement.selectable = false;
        this.responseHeadersItem.appendChild(headerTreeElement);
      }
      this.responseHeadersItem.expand();

      for (let i = 0; i < header.signatures.length; ++i) {
        const errorFieldSet = errorFieldSetMap.get(i) || new Set();
        const signature = header.signatures[i];
        const signatureCategory = new Category(root, i18nString(UIStrings.signature));
        signatureCategory.createLeaf(this.formatHeader(i18nString(UIStrings.label), signature.label));
        signatureCategory.createLeaf(this.formatHeaderForHexData(
            i18nString(UIStrings.signature), signature.signature,
            errorFieldSet.has(Protocol.Network.SignedExchangeErrorField.SignatureSig)));

        if (signature.certUrl) {
          const certURLElement = this.formatHeader(
              i18nString(UIStrings.certificateUrl), signature.certUrl,
              errorFieldSet.has(Protocol.Network.SignedExchangeErrorField.SignatureCertUrl));
          if (signature.certificates) {
            const viewCertLink = certURLElement.createChild('span', 'devtools-link header-toggle');
            viewCertLink.textContent = i18nString(UIStrings.viewCertificate);
            viewCertLink.addEventListener(
                'click',
                Host.InspectorFrontendHost.InspectorFrontendHostInstance.showCertificateViewer.bind(
                    null, signature.certificates),
                false);
          }
          signatureCategory.createLeaf(certURLElement);
        }
        signatureCategory.createLeaf(this.formatHeader(
            i18nString(UIStrings.integrity), signature.integrity,
            errorFieldSet.has(Protocol.Network.SignedExchangeErrorField.SignatureIntegrity)));
        if (signature.certSha256) {
          signatureCategory.createLeaf(this.formatHeaderForHexData(
              i18nString(UIStrings.certificateSha), signature.certSha256,
              errorFieldSet.has(Protocol.Network.SignedExchangeErrorField.SignatureCertSha256)));
        }
        signatureCategory.createLeaf(this.formatHeader(
            i18nString(UIStrings.validityUrl), signature.validityUrl,
            errorFieldSet.has(Protocol.Network.SignedExchangeErrorField.SignatureValidityUrl)));
        signatureCategory.createLeaf().title = this.formatHeader(
            i18nString(UIStrings.date), new Date(1000 * signature.date).toUTCString(),
            errorFieldSet.has(Protocol.Network.SignedExchangeErrorField.SignatureTimestamps));
        signatureCategory.createLeaf().title = this.formatHeader(
            i18nString(UIStrings.expires), new Date(1000 * signature.expires).toUTCString(),
            errorFieldSet.has(Protocol.Network.SignedExchangeErrorField.SignatureTimestamps));
      }
    }
    if (signedExchangeInfo.securityDetails) {
      const securityDetails = signedExchangeInfo.securityDetails;
      const securityCategory = new Category(root, i18nString(UIStrings.certificate));
      securityCategory.createLeaf(this.formatHeader(i18nString(UIStrings.subject), securityDetails.subjectName));
      securityCategory.createLeaf(
          this.formatHeader(i18nString(UIStrings.validFrom), new Date(1000 * securityDetails.validFrom).toUTCString()));
      securityCategory.createLeaf(
          this.formatHeader(i18nString(UIStrings.validUntil), new Date(1000 * securityDetails.validTo).toUTCString()));
      securityCategory.createLeaf(this.formatHeader(i18nString(UIStrings.issuer), securityDetails.issuer));
    }
  }

  private formatHeader(name: string, value: string, highlighted?: boolean): DocumentFragment {
    const fragment = document.createDocumentFragment();
    const nameElement = fragment.createChild('div', 'header-name');
    nameElement.textContent = name + ': ';
    fragment.createChild('span', 'header-separator');
    const valueElement = fragment.createChild('div', 'header-value source-code');
    valueElement.textContent = value;
    if (highlighted) {
      nameElement.classList.add('error-field');
      valueElement.classList.add('error-field');
    }
    return fragment;
  }

  private formatHeaderForHexData(name: string, value: string, highlighted?: boolean): DocumentFragment {
    const fragment = document.createDocumentFragment();
    const nameElement = fragment.createChild('div', 'header-name');
    nameElement.textContent = name + ': ';
    fragment.createChild('span', 'header-separator');
    const valueElement = fragment.createChild('div', 'header-value source-code hex-data');
    valueElement.textContent = value.replace(/(.{2})/g, '$1 ');
    if (highlighted) {
      nameElement.classList.add('error-field');
      valueElement.classList.add('error-field');
    }
    return fragment;
  }
}

export class Category extends UI.TreeOutline.TreeElement {
  override toggleOnClick: boolean;
  override expanded: boolean;

  constructor(root: UI.TreeOutline.TreeOutline, title?: string|Node) {
    super(title, true);
    this.selectable = false;
    this.toggleOnClick = true;
    this.expanded = true;
    root.appendChild(this);
  }

  createLeaf(title?: string|Node): UI.TreeOutline.TreeElement {
    const leaf = new UI.TreeOutline.TreeElement(title);
    leaf.selectable = false;
    this.appendChild(leaf);
    return leaf;
  }
}
