/**
 * @license
 * Copyright 2023 Google Inc.
 * SPDX-License-Identifier: Apache-2.0
 */

import {execSync, exec} from 'node:child_process';
import {writeFile, readFile} from 'node:fs/promises';
import {promisify} from 'node:util';

import actions from '@actions/core';
import {resolveBuildId} from '@puppeteer/browsers';
import {SemVer} from 'semver';

import packageJson from '../packages/puppeteer-core/package.json' with {type: 'json'};
import versionData from '../versions.json' with {type: 'json'};

import {PUPPETEER_REVISIONS} from 'puppeteer-core/internal/revisions.js';

const execAsync = promisify(exec);

const BROWSER = process.env.BROWSER_TO_UPDATE;

if (!BROWSER) {
  console.error('No BROWSER_TO_UPDATE env variable supplied!');
  process.exit(1);
}

const BROWSER_CURRENT_VERSION = PUPPETEER_REVISIONS[BROWSER];

const touchedFiles = [];

function getCapitalize(text) {
  return text.charAt(0).toUpperCase() + text.slice(1);
}

/**
 *
 * @param {string} version
 * @returns {string}
 */
function normalizeVersionForCommit(browser, version) {
  switch (browser) {
    case 'firefox':
      // Splits the prefix of `stable_` for Firefox
      return version.split('_').at(-1);
    case 'chrome':
      return version;
  }

  throw new Error(`Unrecognized browser ${browser}`);
}

/**
 *
 * @param {string} version
 * @returns {string}
 */
function normalizeVersionToSemVer(browser, version) {
  switch (browser) {
    case 'firefox':
      // Splits the prefix of `stable_` for Firefox
      version = version.split('_').at(-1);
      // Firefox reports 129.0 instead of 129.0.0
      // Patch have the correct number 128.0.2
      if (version.split('.').length <= 2) {
        return `${version}.0`;
      }

      return version;
    case 'chrome':
      // For Chrome (example: 127.0.6533.99) is allowed as SemVer
      // as long as we use the loose option.
      return version;
  }

  throw new Error(`Unrecognized browser ${browser}`);
}

function checkIfNeedsUpdate(browser, oldVersion, newVersion) {
  const oldSemVer = new SemVer(
    normalizeVersionToSemVer(browser, oldVersion),
    true,
  );
  const newSemVer = new SemVer(
    normalizeVersionToSemVer(browser, newVersion),
    true,
  );
  let message = `roll to ${getCapitalize(browser)} ${normalizeVersionForCommit(browser, newVersion)}`;

  if (newSemVer.compare(oldSemVer) <= 0) {
    // Exit the process without setting up version
    console.warn(
      `Version ${newVersion} is older or the same as the current ${oldVersion}`,
    );
    process.exit(0);
  } else if (newSemVer.major === oldSemVer.major) {
    message = `fix: ${message}`;
  } else {
    message = `feat: ${message}`;
  }
  actions.setOutput('commit', message);
}

/**
 * We cant use `npm run format` as it's too slow
 * so we only scope the files we updated
 */
async function formatUpdateFiles() {
  await Promise.all(
    touchedFiles.map(file => {
      return execAsync(`npx eslint --fix ${file}`);
    }),
  );
  await Promise.all(
    touchedFiles.map(file => {
      return execAsync(`npx prettier --write ${file}`);
    }),
  );
}

async function replaceInFile(filePath, search, replace) {
  const buffer = await readFile(filePath);
  const update = buffer.toString().replaceAll(search, replace);

  await writeFile(filePath, update);

  touchedFiles.push(filePath);
}

async function getVersionForStable(browser) {
  return await resolveBuildId(browser, 'linux', 'stable');
}

async function updateDevToolsProtocolVersion(browserVersion) {
  const result = await fetch(
    'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json',
  ).then(response => {
    return response.json();
  });

  const {version, revision} = result.channels['Stable'];
  if (browserVersion !== version) {
    console.error(
      'The version from CfT website and @puppeteer/browser mismatch.',
    );
    process.exit(1);
  }

  const currentProtocol = packageJson.dependencies['devtools-protocol'];
  const command = `npm view "devtools-protocol@<=0.0.${revision}" version | tail -1`;

  const bestNewProtocol = execSync(command, {
    encoding: 'utf8',
  })
    .split(' ')[1]
    .replace(/'|\n/g, '');

  await replaceInFile(
    './packages/puppeteer-core/package.json',
    `"devtools-protocol": "${currentProtocol}"`,
    `"devtools-protocol": "${bestNewProtocol}"`,
  );

  await replaceInFile(
    './packages/puppeteer/package.json',
    `"devtools-protocol": "${currentProtocol}"`,
    `"devtools-protocol": "${bestNewProtocol}"`,
  );
}

async function saveVersionData() {
  await writeFile('./versions.json', JSON.stringify(versionData, null, 2));
  touchedFiles.push('./versions.json');
}

async function updateVersionData(browser, oldVersion, newVersion) {
  const browserVersions = versionData.versions.map(
    ([_puppeteerVersion, browserVersions]) => {
      return browserVersions[browser];
    },
  );
  if (browserVersions.indexOf(newVersion) !== -1) {
    // Already updated.
    return;
  }

  const nextVersionConfig = versionData.versions.find(([puppeteerVersion]) => {
    return puppeteerVersion === 'NEXT';
  });

  // If we have manually rolled Chrome but not yet released
  // We will have NEXT as value in the Map
  if (nextVersionConfig) {
    nextVersionConfig[1][browser] = newVersion;
    return;
  }

  versionData.versions.unshift([
    'NEXT',
    {
      ...versionData.versions.at(0).at(1),
      [browser]: newVersion,
    },
  ]);
}

async function updateLastMaintainedChromeVersion(oldVersion, newVersion) {
  const browserVersions = versionData.versions.map(
    ([_puppeteerVersion, browserVersions]) => {
      return browserVersions['chrome'];
    },
  );
  const oldSemVer = new SemVer(oldVersion, true);
  const newSemVer = new SemVer(newVersion, true);

  if (newSemVer.compareMain(oldSemVer) !== 0) {
    const newLastMaintainedMajor = newSemVer.major - 3;
    const nextMaintainedVersion = browserVersions.find(version => {
      return new SemVer(version, true).major === newLastMaintainedMajor;
    });

    versionData.lastMaintainedChromeVersion = nextMaintainedVersion;
  }
}

const version = await getVersionForStable(BROWSER);

checkIfNeedsUpdate(BROWSER, BROWSER_CURRENT_VERSION, version);

await replaceInFile(
  './packages/puppeteer-core/src/revisions.ts',
  BROWSER_CURRENT_VERSION,
  version,
);

await updateVersionData(BROWSER, BROWSER_CURRENT_VERSION, version);

if (BROWSER === 'chrome') {
  await updateLastMaintainedChromeVersion(BROWSER_CURRENT_VERSION, version);
  await updateDevToolsProtocolVersion(version);
  // Create new `package-lock.json` as we update devtools-protocol
  execSync('npm install --ignore-scripts');
}

await saveVersionData();

// Make sure we pass CI formatter check by running all the new files though it
await formatUpdateFiles();

// Keep this as they can be used to debug GitHub Actions if needed
actions.setOutput('version', version);
