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

import '../../../ui/kit/kit.js';
import '../../../ui/components/report_view/report_view.js';

import * as Common from '../../../core/common/common.js';
import * as i18n from '../../../core/i18n/i18n.js';
import type * as Platform from '../../../core/platform/platform.js';
import * as SDK from '../../../core/sdk/sdk.js';
import * as Protocol from '../../../generated/protocol.js';
import * as NetworkForward from '../../../panels/network/forward/forward.js';
import * as Buttons from '../../../ui/components/buttons/buttons.js';
import * as UI from '../../../ui/legacy/legacy.js';
import {html, type LitTemplate, nothing, render, type TemplateResult} from '../../../ui/lit/lit.js';
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';

import permissionsPolicySectionStyles from './permissionsPolicySection.css.js';

const UIStrings = {
  /**
   * @description Label for a button. When clicked more details (for the content this button refers to) will be shown.
   */
  showDetails: 'Show details',
  /**
   * @description Label for a button. When clicked some details (for the content this button refers to) will be hidden.
   */
  hideDetails: 'Hide details',
  /**
   * @description Label for a list of features which are allowed according to the current Permissions policy
   *(a mechanism that allows developers to enable/disable browser features and APIs (e.g. camera, geolocation, autoplay))
   */
  allowedFeatures: 'Allowed Features',
  /**
   * @description Label for a list of features which are disabled according to the current Permissions policy
   *(a mechanism that allows developers to enable/disable browser features and APIs (e.g. camera, geolocation, autoplay))
   */
  disabledFeatures: 'Disabled Features',
  /**
   * @description Tooltip text for a link to a specific request's headers in the Network panel.
   */
  clickToShowHeader: 'Click to reveal the request whose "`Permissions-Policy`" HTTP header disables this feature.',
  /**
   * @description Tooltip text for a link to a specific iframe in the Elements panel (Iframes can be nested, the link goes
   *  to the outer-most iframe which blocks a certain feature).
   */
  clickToShowIframe: 'Click to reveal the top-most iframe which does not allow this feature in the elements panel.',
  /**
   * @description Text describing that a specific feature is blocked by not being included in the iframe's "allow" attribute.
   */
  disabledByIframe: 'missing in iframe "`allow`" attribute',
  /**
   * @description Text describing that a specific feature is blocked by a Permissions Policy specified in a request header.
   */
  disabledByHeader: 'disabled by "`Permissions-Policy`" header',
  /**
   * @description Text describing that a specific feature is blocked by virtue of being inside a fenced frame tree.
   */
  disabledByFencedFrame: 'disabled inside a `fencedframe`',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/components/PermissionsPolicySection.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export interface PermissionsPolicySectionData {
  policies: Protocol.Page.PermissionsPolicyFeatureState[];
  showDetails: boolean;
}

export function renderIconLink(
    iconName: string, title: Platform.UIString.LocalizedString, clickHandler: (() => void)|(() => Promise<void>),
    jsLogContext: string): TemplateResult {
  // Disabled until https://crbug.com/1079231 is fixed.
  // clang-format off
  return html`
    <devtools-button
      .iconName=${iconName}
      title=${title}
      aria-label=${title}
      .variant=${Buttons.Button.Variant.ICON}
      .size=${Buttons.Button.Size.SMALL}
      @click=${clickHandler}
      jslog=${VisualLogging.action().track({click: true}).context(jsLogContext)}>
    </devtools-button>`;
  // clang-format on
}

function renderAllowed(allowed: Protocol.Page.PermissionsPolicyFeatureState[]): LitTemplate {
  if (!allowed.length) {
    return nothing;
  }
  return html`
    <devtools-report-key>${i18nString(UIStrings.allowedFeatures)}</devtools-report-key>
    <devtools-report-value>${allowed.map(({feature}) => feature).join(', ')}</devtools-report-value>`;
}

function renderDisallowed(
    data: Array<{
      policy: Protocol.Page.PermissionsPolicyFeatureState,
      blockReason?: Protocol.Page.PermissionsPolicyBlockReason,
      linkTargetDOMNode?: SDK.DOMModel.DOMNode,
      linkTargetRequest?: SDK.NetworkRequest.NetworkRequest,
    }>,
    showDetails: boolean,
    onToggleShowDetails: () => void,
    onRevealDOMNode: (linkTargetDOMNode: SDK.DOMModel.DOMNode) => Promise<void>,
    onRevealHeader: (linkTargetRequest: SDK.NetworkRequest.NetworkRequest) => Promise<void>,
    ): LitTemplate {
  if (!data.length) {
    return nothing;
  }
  if (!showDetails) {
    // clang-format off
    return html`
      <devtools-report-key>${i18nString(UIStrings.disabledFeatures)}</devtools-report-key>
      <devtools-report-value>
        ${data.map(({policy}) => policy.feature).join(', ')}
        <devtools-button
            class="disabled-features-button"
            .variant=${Buttons.Button.Variant.OUTLINED}
            @click=${onToggleShowDetails}
            jslog=${VisualLogging.action('show-disabled-features-details').track({click: true})}>
          ${i18nString(UIStrings.showDetails)}
        </devtools-button>
      </devtools-report-value>`;
    // clang-format on
  }

  const featureRows = data.map(({policy, blockReason, linkTargetDOMNode, linkTargetRequest}) => {
    const blockReasonText = (() => {
      switch (blockReason) {
        case Protocol.Page.PermissionsPolicyBlockReason.IframeAttribute:
          return i18nString(UIStrings.disabledByIframe);
        case Protocol.Page.PermissionsPolicyBlockReason.Header:
          return i18nString(UIStrings.disabledByHeader);
        case Protocol.Page.PermissionsPolicyBlockReason.InFencedFrameTree:
          return i18nString(UIStrings.disabledByFencedFrame);
        default:
          return '';
      }
    })();
    // Disabled until https://crbug.com/1079231 is fixed.
    // clang-format off
    return html`
      <div class="permissions-row">
        <div>
          <devtools-icon class="allowed-icon extra-large" name="cross-circle">
          </devtools-icon>
        </div>
        <div class="feature-name text-ellipsis">${policy.feature}</div>
        <div class="block-reason">${blockReasonText}</div>
        <div>
          ${linkTargetDOMNode ?
            renderIconLink('code-circle', i18nString(UIStrings.clickToShowIframe),
                           () => onRevealDOMNode(linkTargetDOMNode), 'reveal-in-elements') :
            nothing}
          ${linkTargetRequest ?
            renderIconLink('arrow-up-down-circle', i18nString(UIStrings.clickToShowHeader),
                           () => onRevealHeader(linkTargetRequest), 'reveal-in-network') :
            nothing}
        </div>
      </div>`;
    // clang-format on
  });

  // clang-format off
  return html`
    <devtools-report-key>${i18nString(UIStrings.disabledFeatures)}</devtools-report-key>
    <devtools-report-value class="policies-list">
      ${featureRows}
      <div class="permissions-row">
        <devtools-button
            .variant=${Buttons.Button.Variant.OUTLINED}
            @click=${onToggleShowDetails}
            jslog=${VisualLogging.action('hide-disabled-features-details').track({click: true})}>
          ${i18nString(UIStrings.hideDetails)}
        </devtools-button>
      </div>
    </devtools-report-value>`;
  // clang-format on
}

interface ViewInput {
  allowed: Protocol.Page.PermissionsPolicyFeatureState[];
  disallowed: Array<{
    policy: Protocol.Page.PermissionsPolicyFeatureState,
    blockReason?: Protocol.Page.PermissionsPolicyBlockReason,
    linkTargetDOMNode?: SDK.DOMModel.DOMNode,
    linkTargetRequest?: SDK.NetworkRequest.NetworkRequest,
  }>;
  showDetails: boolean;
  onToggleShowDetails: () => void;
  onRevealDOMNode: (linkTargetDOMNode: SDK.DOMModel.DOMNode) => Promise<void>;
  onRevealHeader: (linkTargetRequest: SDK.NetworkRequest.NetworkRequest) => Promise<void>;
}

type View = (input: ViewInput, output: undefined, target: HTMLElement) => void;

const DEFAULT_VIEW: View = (input, output, target) => {
  // clang-format off
  render(html`
    <style>${permissionsPolicySectionStyles}</style>
    <devtools-report-section-header>
      ${i18n.i18n.lockedString('Permissions Policy')}
    </devtools-report-section-header>
    ${renderAllowed(input.allowed)}
    ${(input.allowed.length > 0 && input.disallowed.length > 0) ?
      html`<devtools-report-divider class="subsection-divider"></devtools-report-divider>` : nothing}
    ${renderDisallowed(
        input.disallowed, input.showDetails,
        input.onToggleShowDetails, input.onRevealDOMNode, input.onRevealHeader)}
    <devtools-report-divider></devtools-report-divider>`, target);
  // clang-format on
};

export class PermissionsPolicySection extends UI.Widget.Widget {
  #policies: Protocol.Page.PermissionsPolicyFeatureState[] = [];
  #showDetails = false;
  #view: View;

  constructor(element?: HTMLElement, view = DEFAULT_VIEW) {
    super(element, {useShadowDom: false});
    this.#view = view;
  }

  set policies(policies: Protocol.Page.PermissionsPolicyFeatureState[]) {
    this.#policies = policies;
    this.requestUpdate();
  }

  get policies(): Protocol.Page.PermissionsPolicyFeatureState[] {
    return this.#policies;
  }

  set showDetails(showDetails: boolean) {
    this.#showDetails = showDetails;
    this.requestUpdate();
  }

  get showDetails(): boolean {
    return this.#showDetails;
  }

  #toggleShowPermissionsDisallowedDetails(): void {
    this.showDetails = !this.showDetails;
  }

  async #revealDOMNode(linkTargetDOMNode: SDK.DOMModel.DOMNode): Promise<void> {
    await Common.Revealer.reveal(linkTargetDOMNode);
  }

  async #revealHeader(linkTargetRequest: SDK.NetworkRequest.NetworkRequest): Promise<void> {
    if (!linkTargetRequest) {
      return;
    }
    const headerName =
        linkTargetRequest.responseHeaderValue('permissions-policy') ? 'permissions-policy' : 'feature-policy';
    const requestLocation = NetworkForward.UIRequestLocation.UIRequestLocation.responseHeaderMatch(
        linkTargetRequest,
        {name: headerName, value: ''},
    );
    await Common.Revealer.reveal(requestLocation);
  }

  override async performUpdate(): Promise<void> {
    const frameManager = SDK.FrameManager.FrameManager.instance();
    const policies = this.#policies.sort((a, b) => a.feature.localeCompare(b.feature));
    const allowed = policies.filter(p => p.allowed).sort((a, b) => a.feature.localeCompare(b.feature));
    const disallowed = policies.filter(p => !p.allowed).sort((a, b) => a.feature.localeCompare(b.feature));
    const disallowedData = this.#showDetails ? await Promise.all(disallowed.map(async policy => {
      const frame = policy.locator ? frameManager.getFrame(policy.locator.frameId) : undefined;
      const blockReason = policy.locator?.blockReason;
      const linkTargetDOMNode = await ((blockReason === Protocol.Page.PermissionsPolicyBlockReason.IframeAttribute &&
                                        frame?.getOwnerDOMNodeOrDocument()) ||
                                       undefined);
      const resource = frame?.resourceForURL(frame.url);
      const linkTargetRequest =
          (blockReason === Protocol.Page.PermissionsPolicyBlockReason.Header && resource?.request) || undefined;
      return {policy, blockReason, linkTargetDOMNode, linkTargetRequest};
    })) :
                                               disallowed.map(policy => ({policy}));

    this.#view(
        {
          allowed,
          disallowed: disallowedData,
          showDetails: this.#showDetails,
          onToggleShowDetails: this.#toggleShowPermissionsDisallowedDetails.bind(this),
          onRevealDOMNode: this.#revealDOMNode.bind(this),
          onRevealHeader: this.#revealHeader.bind(this),
        },
        undefined, this.contentElement);
  }
}
