// Copyright (c) 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as Root from '../../core/root/root.js';
import * as Puppeteer from '../../services/puppeteer/puppeteer.js';
import type * as SDK from '../../core/sdk/sdk.js';

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

/**
 * LegacyPort is provided to Lighthouse as the CDP connection in legacyNavigation mode.
 * Its complement is https://github.com/GoogleChrome/lighthouse/blob/v9.3.1/lighthouse-core/gather/connections/raw.js
 * It speaks pure CDP via notifyFrontendViaWorkerMessage
 *
 * Any message that comes back from Lighthouse has to go via a so-called "port".
 * This class holds the relevant callbacks that Lighthouse provides and that
 * can be called in the onmessage callback of the worker, so that the frontend
 * can communicate to Lighthouse. Lighthouse itself communicates to the frontend
 * via status updates defined below.
 */
class LegacyPort {
  onMessage?: (message: string) => void;
  onClose?: () => void;
  on(eventName: string, callback: (arg?: string) => void): void {
    if (eventName === 'message') {
      this.onMessage = callback;
    } else if (eventName === 'close') {
      this.onClose = callback;
    }
  }

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

/**
 * ConnectionProxy is a SDK interface, but the implementation has no knowledge it's a parallelConnection.
 * The CDP traffic is smuggled back and forth by the system described in LighthouseProtocolService
*/
class ConnectionProxy implements SDK.Connections.ParallelConnectionInterface {
  sessionId: string;
  onMessage: ((arg0: Object) => void)|null;
  onDisconnect: ((arg0: string) => void)|null;

  constructor(sessionId: string) {
    this.sessionId = sessionId;
    this.onMessage = null;
    this.onDisconnect = null;
  }

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

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

  getOnDisconnect(): (((arg0: string) => void)|null) {
    return this.onDisconnect;
  }

  getSessionId(): string {
    return this.sessionId;
  }

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

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

const legacyPort = new LegacyPort();
let cdpConnection: ConnectionProxy|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 puppeteerConnection: Awaited<ReturnType<typeof Puppeteer.PuppeteerConnection['getPuppeteerConnection']>>|
      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;

    // TODO: Remove this filter once pubads is mode restricted
    // https://github.com/googleads/publisher-ads-lighthouse-plugin/pull/339
    if (action === 'startTimespan' || action === 'snapshot') {
      args.categoryIDs = args.categoryIDs.filter((c: string) => c !== 'lighthouse-plugin-publisher-ads');
    }

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

    // Handle legacy Lighthouse runner path.
    if (action === 'navigation' && flags.legacyNavigation) {
      // @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
      const connection = self.setUpWorkerConnection(legacyPort);
      // @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
      return await self.runLighthouse(url, flags, config, connection);
    }

    const {mainTargetId, mainFrameId, mainSessionId} = args.target;
    cdpConnection = new ConnectionProxy(mainSessionId);
    puppeteerConnection =
        await Puppeteer.PuppeteerConnection.getPuppeteerConnection(cdpConnection, mainFrameId, mainTargetId);
    const {page} = puppeteerConnection;
    const configContext = {
      logLevel: flags.logLevel,
      settingsOverrides: flags,
    };

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

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

    // @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
    return await self.runLighthouseNavigation(url, {config, page, configContext});
  } 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') {
      puppeteerConnection?.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 && 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<string>(
        (resolve, 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(JSON.stringify({action, args}));
}

async function onFrontendMessage(event: MessageEvent): Promise<void> {
  const messageFromFrontend = JSON.parse(event.data);
  switch (messageFromFrontend.action) {
    case 'startTimespan':
    case 'endTimespan':
    case 'snapshot':
    case 'navigation': {
      const result = await invokeLH(messageFromFrontend.action, messageFromFrontend.args);
      self.postMessage(JSON.stringify({id: messageFromFrontend.id, result}));
      break;
    }
    case 'dispatchProtocolMessage': {
      cdpConnection?.onMessage?.(
          JSON.parse(messageFromFrontend.args.message),
      );
      legacyPort.onMessage?.(messageFromFrontend.args.message);
      break;
    }
    default: {
      throw new Error(`Unknown event: ${event.data}`);
    }
  }
}

self.onmessage = onFrontendMessage;

// Make lighthouse and traceviewer happy.
// @ts-ignore https://github.com/GoogleChrome/lighthouse/issues/11628
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',
};
