// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Protocol from '../../../generated/protocol.js';
import type {RenderBlocking, SyntheticNetworkRequest} from '../types/TraceEvents.js';

// Important: we purposefully treat `potentially_blocking` as
// non-render-blocking here because:
// 1. An async script can run on the main thread at any point, including before
//    the page is loaded
// 2. An async script will never block the parsing and rendering process of the
//    browser.
// 3. Therefore, from a developer's point of view, there is nothing more they
//    can do if they've put `async` on, and within the context of Insights, we
//    shouldn't report an async script as render-blocking.
// In the future we may want to consider suggesting the use of `defer` over
// `async`, as it doesn't have this concern, but for now we'll allow `async`
// and not report it as an issue.
const NON_RENDER_BLOCKING_VALUES = new Set<RenderBlocking>([
  'non_blocking',
  'dynamically_injected_non_blocking',
  'potentially_blocking',
]);

export function isSyntheticNetworkRequestEventRenderBlocking(event: SyntheticNetworkRequest): boolean {
  return !NON_RENDER_BLOCKING_VALUES.has(event.args.data.renderBlocking);
}

const HIGH_NETWORK_PRIORITIES = new Set<Protocol.Network.ResourcePriority>([
  Protocol.Network.ResourcePriority.VeryHigh,
  Protocol.Network.ResourcePriority.High,
  Protocol.Network.ResourcePriority.Medium,
]);

export function isSyntheticNetworkRequestHighPriority(event: SyntheticNetworkRequest): boolean {
  return HIGH_NETWORK_PRIORITIES.has(event.args.data.priority);
}

export interface CacheControl {
  'max-age'?: number;
  'no-cache'?: boolean;
  'no-store'?: boolean;
  'must-revalidate'?: boolean;
  // eslint-disable-next-line @stylistic/quote-props
  'private'?: boolean;
}

export const CACHEABLE_STATUS_CODES = new Set([200, 203, 206]);

/** @type {Set<LH.Crdp.Network.ResourceType>} */
export const STATIC_RESOURCE_TYPES = new Set([
  Protocol.Network.ResourceType.Font,
  Protocol.Network.ResourceType.Image,
  Protocol.Network.ResourceType.Media,
  Protocol.Network.ResourceType.Script,
  Protocol.Network.ResourceType.Stylesheet,
]);

export const NON_NETWORK_SCHEMES = [
  'blob',        // @see https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
  'data',        // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
  'intent',      // @see https://developer.chrome.com/docs/multidevice/android/intents/
  'file',        // @see https://en.wikipedia.org/wiki/File_URI_scheme
  'filesystem',  // @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystem
  'chrome-extension',
];

/**
 * Parses Cache-Control directives based on https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
 * eg. 'no-cache, no-store, max-age=0, no-transform, private' will return
 * {no-cache: true, no-store: true, max-age: 0, no-transform: true, private: true}
 */
export function parseCacheControl(header: string|null): CacheControl|null {
  if (!header) {
    return null;
  }

  const directives = header.split(',').map(directive => directive.trim());
  const cacheControlOptions: CacheControl = {};

  for (const directive of directives) {
    const [key, value] = directive.split('=').map(part => part.trim());

    switch (key) {
      case 'max-age': {
        const maxAge = parseInt(value, 10);
        if (!isNaN(maxAge)) {
          cacheControlOptions['max-age'] = maxAge;
        }
        break;
      }
      case 'no-cache':
        cacheControlOptions['no-cache'] = true;
        break;
      case 'no-store':
        cacheControlOptions['no-store'] = true;
        break;
      case 'must-revalidate':
        cacheControlOptions['must-revalidate'] = true;
        break;
      case 'private':
        cacheControlOptions['private'] = true;
        break;
      default:
        // Ignore unknown directives
        break;
    }
  }

  return cacheControlOptions;
}

const SECURE_LOCALHOST_DOMAINS = ['localhost', '127.0.0.1'];

/**
 * Is the host localhost-enough to satisfy the "secure context" definition
 * https://github.com/GoogleChrome/lighthouse/pull/11766#discussion_r582340683
 */
export function isSyntheticNetworkRequestLocalhost(event: SyntheticNetworkRequest): boolean {
  try {
    const hostname = new URL(event.args.data.url).hostname;
    // Any hostname terminating in `.localhost` is considered to be local.
    // https://w3c.github.io/webappsec-secure-contexts/#localhost
    // This method doesn't consider IPs that resolve to loopback, IPv6 or other loopback edgecases
    return SECURE_LOCALHOST_DOMAINS.includes(hostname) || hostname.endsWith('.localhost');
  } catch {
    return false;
  }
}
