// Copyright 2016 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 ProtocolClient from '../../core/protocol_client/protocol_client.js';
import * as Root from '../../core/root/root.js';
import * as PuppeteerService from '../../services/puppeteer/puppeteer.js';
import * as ThirdPartyWeb from '../../third_party/third-party-web/third-party-web.js';

function disableLoggingForTest(): void {
  console.log = (): void => undefined;  // eslint-disable-line no-console
}

/**
 * WorkerConnectionTransport is a DevTools `ConnectionTransport` implementation that talks
 * CDP via web worker postMessage. The system is described in LighthouseProtocolService.
 */
class WorkerConnectionTransport implements ProtocolClient.ConnectionTransport.ConnectionTransport {
  onMessage: ((arg0: Object) => void)|null = null;
  onDisconnect: ((arg0: string) => void)|null = null;

  setOnMessage(onMessage: (arg0: Object|string) => void): void {
    this.onMessage = onMessage;
  }

  setOnDisconnect(onDisconnect: (arg0: string) => void): void {
    this.onDisconnect = onDisconnect;
  }

  sendRawMessage(message: string): void {
    notifyFrontendViaWorkerMessage('sendProtocolMessage', {message});
  }

  async disconnect(): Promise<void> {
    this.onDisconnect?.('force disconnect');
    this.onDisconnect = null;
    this.onMessage = null;
  }
}

let cdpTransport: WorkerConnectionTransport|undefined;
let endTimespan: (() => unknown)|undefined;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function invokeLH(action: string, args: any): Promise<unknown> {
  if (Root.Runtime.Runtime.queryParam('isUnderTest')) {
    disableLoggingForTest();
    args.flags.maxWaitForLoad = 2 * 1000;
  }

  // @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
  self.listenForStatus(message => {
    notifyFrontendViaWorkerMessage('statusUpdate', {message: message[1]});
  });

  let puppeteerHandle: Awaited<ReturnType<
      typeof PuppeteerService.PuppeteerConnection.PuppeteerConnectionHelper['connectPuppeteerToConnectionViaTab']>>|
      undefined;

  try {
    // For timespan we only need to perform setup on startTimespan.
    // Config, flags, locale, etc. should be stored in the closure of endTimespan.
    if (action === 'endTimespan') {
      if (!endTimespan) {
        throw new Error('Cannot end a timespan before starting one');
      }
      const result = await endTimespan();
      endTimespan = undefined;
      return result;
    }

    const locale = await fetchLocaleData(args.locales);
    const flags = args.flags;
    flags.logLevel = flags.logLevel || 'info';
    flags.channel = 'devtools';
    flags.locale = locale;

    // @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
    const config = args.config || self.createConfig(args.categoryIDs, flags.formFactor);
    const url = args.url;

    // @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
    self.thirdPartyWeb.provideThirdPartyWeb(ThirdPartyWeb.ThirdPartyWeb);

    const {rootTargetId, mainSessionId} = args;
    cdpTransport = new WorkerConnectionTransport();
    const connection = new ProtocolClient.DevToolsCDPConnection.DevToolsCDPConnection(cdpTransport);
    puppeteerHandle =
        await PuppeteerService.PuppeteerConnection.PuppeteerConnectionHelper.connectPuppeteerToConnectionViaTab({
          connection,
          targetId: rootTargetId,
          sessionId: mainSessionId,
          // Lighthouse can only audit normal pages.
          isPageTargetCallback: targetInfo => targetInfo.type === 'page',
        });

    const {page} = puppeteerHandle;
    if (!page) {
      throw new Error('Could not create page handle for the target page');
    }

    if (action === 'snapshot') {
      // @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
      return await self.snapshot(page, {config, flags});
    }

    if (action === 'startTimespan') {
      // @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
      const timespan = await self.startTimespan(page, {config, flags});
      endTimespan = timespan.endTimespan;
      return;
    }

    // @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
    return await self.navigation(page, url, {config, flags});
  } catch (err) {
    return ({
      fatal: true,
      message: err.message,
      stack: err.stack,
    });
  } finally {
    // endTimespan will need to use the same connection as startTimespan.
    if (action !== 'startTimespan') {
      await puppeteerHandle?.browser.disconnect();
    }
  }
}

/**
 * Finds a locale supported by Lighthouse from the user's system locales.
 * If no matching locale is found, or if fetching locale data fails, this function returns nothing
 * and Lighthouse will use `en-US` by default.
 */
async function fetchLocaleData(locales: string[]): Promise<string|void> {
  // @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
  const locale = self.lookupLocale(locales);

  // If the locale is en-US, no need to fetch locale data.
  if (locale === 'en-US' || locale === 'en') {
    return;
  }

  // Try to load the locale data.
  try {
    const remoteBase = Root.Runtime.getRemoteBase();
    let localeUrl: string;
    if (remoteBase?.base) {
      localeUrl = `${remoteBase.base}third_party/lighthouse/locales/${locale}.json`;
    } else {
      localeUrl = new URL(`../../third_party/lighthouse/locales/${locale}.json`, import.meta.url).toString();
    }

    const timeoutPromise =
        new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timed out fetching locale')), 5000));
    const localeData = await Promise.race([timeoutPromise, fetch(localeUrl).then(result => result.json())]);
    // @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
    self.registerLocaleData(locale, localeData);
    return locale;
  } catch (err) {
    console.error(err);
  }

  return;
}

/**
 * `notifyFrontendViaWorkerMessage` and `onFrontendMessage` work with the FE's ProtocolService.
 *
 * onFrontendMessage takes action-wrapped messages and either invoking lighthouse or delivering it CDP traffic.
 * notifyFrontendViaWorkerMessage posts action-wrapped messages to the FE.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function notifyFrontendViaWorkerMessage(action: string, args: any): void {
  self.postMessage({action, args});
}

async function onFrontendMessage(event: MessageEvent): Promise<void> {
  const messageFromFrontend = event.data;
  switch (messageFromFrontend.action) {
    case 'startTimespan':
    case 'endTimespan':
    case 'snapshot':
    case 'navigation': {
      const result = await invokeLH(messageFromFrontend.action, messageFromFrontend.args);
      if (result && typeof result === 'object') {
        // Report isn't used upstream.
        if ('report' in result) {
          delete result.report;
        }

        // Logger PerformanceTiming objects cannot be cloned by this worker's `postMessage` function.
        if ('artifacts' in result) {
          // @ts-expect-error
          result.artifacts.Timing = JSON.parse(JSON.stringify(result.artifacts.Timing));
        }
      }
      self.postMessage({id: messageFromFrontend.id, result});
      break;
    }
    case 'dispatchProtocolMessage': {
      cdpTransport?.onMessage?.(messageFromFrontend.args.message);
      break;
    }
    default: {
      throw new Error(`Unknown event: ${event.data}`);
    }
  }
}

self.onmessage = onFrontendMessage;

// Make lighthouse and traceviewer happy.
globalThis.global = self;
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
globalThis.global.isVinn = true;
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
globalThis.global.document = {};
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
globalThis.global.document.documentElement = {};
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
globalThis.global.document.documentElement.style = {
  WebkitAppearance: 'WebkitAppearance',
};
