// Copyright 2026 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 Platform from '../platform/platform.js';
import * as Root from '../root/root.js';

/* eslint @devtools/enforce-version-controller-methods: "error" */

import {type Setting, Settings, type SettingsStorage, SettingStorageType} from './Settings.js';

// The VersionController does a lot of mapping and restructuring which often need
// typecasting to any, allow it in there
/* eslint-disable @typescript-eslint/no-explicit-any */
export class VersionController {
  static readonly GLOBAL_VERSION_SETTING_NAME = 'inspectorVersion';
  static readonly SYNCED_VERSION_SETTING_NAME = 'syncedInspectorVersion';
  static readonly LOCAL_VERSION_SETTING_NAME = 'localInspectorVersion';

  static readonly CURRENT_VERSION = 44;

  readonly #settings: Settings;
  readonly #globalVersionSetting: Setting<number>;
  readonly #syncedVersionSetting: Setting<number>;
  readonly #localVersionSetting: Setting<number>;

  constructor(settings: Settings) {
    this.#settings = settings;
    // If no version setting is found, we initialize with the current version and don't do anything.
    this.#globalVersionSetting = this.#settings.createSetting(
        VersionController.GLOBAL_VERSION_SETTING_NAME, VersionController.CURRENT_VERSION, SettingStorageType.GLOBAL);
    this.#syncedVersionSetting = this.#settings.createSetting(
        VersionController.SYNCED_VERSION_SETTING_NAME, VersionController.CURRENT_VERSION, SettingStorageType.SYNCED);
    this.#localVersionSetting = this.#settings.createSetting(
        VersionController.LOCAL_VERSION_SETTING_NAME, VersionController.CURRENT_VERSION, SettingStorageType.LOCAL);
  }

  /**
   * Force re-sets all version number settings to the current version without
   * running any migrations.
   */
  resetToCurrent(): void {
    this.#globalVersionSetting.set(VersionController.CURRENT_VERSION);
    this.#syncedVersionSetting.set(VersionController.CURRENT_VERSION);
    this.#localVersionSetting.set(VersionController.CURRENT_VERSION);
  }

  #removeSetting(setting: {name: string, storage: SettingsStorage}): void {
    const name = setting.name;

    this.#settings.getRegistry().delete(name);
    this.#settings.moduleSettings.delete(name);

    setting.storage.remove(name);
  }

  /**
   * Runs the appropriate migrations and updates the version settings accordingly.
   *
   * To determine what migrations to run we take the minimum of all version number settings.
   *
   * IMPORTANT: All migrations must be idempotent since they might be applied multiple times.
   */
  updateVersion(): void {
    const currentVersion = VersionController.CURRENT_VERSION;
    const minimumVersion =
        Math.min(this.#globalVersionSetting.get(), this.#syncedVersionSetting.get(), this.#localVersionSetting.get());
    const methodsToRun = this.methodsToRunToUpdateVersion(minimumVersion, currentVersion);
    console.assert(
        // @ts-expect-error
        this[`updateVersionFrom${currentVersion}To${currentVersion + 1}`] === undefined,
        'Unexpected migration method found. Increment CURRENT_VERSION or remove the method.');
    for (const method of methodsToRun) {
      // @ts-expect-error Special version method matching
      this[method].call(this);
    }
    this.resetToCurrent();
  }

  private methodsToRunToUpdateVersion(oldVersion: number, currentVersion: number): string[] {
    const result = [];
    for (let i = oldVersion; i < currentVersion; ++i) {
      result.push('updateVersionFrom' + i + 'To' + (i + 1));
    }
    return result;
  }

  updateVersionFrom0To1(): void {
    this.clearBreakpointsWhenTooMany(this.#settings.createLocalSetting('breakpoints', []), 500000);
  }

  updateVersionFrom1To2(): void {
    this.#settings.createSetting('previouslyViewedFiles', []).set([]);
  }

  updateVersionFrom2To3(): void {
    this.#settings.createSetting('fileSystemMapping', {}).set({});
    this.#removeSetting(this.#settings.createSetting('fileMappingEntries', []));
  }

  updateVersionFrom3To4(): void {
    const advancedMode = this.#settings.createSetting('showHeaSnapshotObjectsHiddenProperties', false);
    this.#settings.moduleSetting('showAdvancedHeapSnapshotProperties').set(advancedMode.get());
    this.#removeSetting(advancedMode);
  }

  updateVersionFrom4To5(): void {
    const settingNames: Record<string, string> = {
      FileSystemViewSidebarWidth: 'fileSystemViewSplitViewState',
      elementsSidebarWidth: 'elementsPanelSplitViewState',
      StylesPaneSplitRatio: 'stylesPaneSplitViewState',
      heapSnapshotRetainersViewSize: 'heapSnapshotSplitViewState',
      'InspectorView.splitView': 'InspectorView.splitViewState',
      'InspectorView.screencastSplitView': 'InspectorView.screencastSplitViewState',
      'Inspector.drawerSplitView': 'Inspector.drawerSplitViewState',
      layerDetailsSplitView: 'layerDetailsSplitViewState',
      networkSidebarWidth: 'networkPanelSplitViewState',
      sourcesSidebarWidth: 'sourcesPanelSplitViewState',
      scriptsPanelNavigatorSidebarWidth: 'sourcesPanelNavigatorSplitViewState',
      sourcesPanelSplitSidebarRatio: 'sourcesPanelDebuggerSidebarSplitViewState',
      'timeline-details': 'timelinePanelDetailsSplitViewState',
      'timeline-split': 'timelinePanelRecorsSplitViewState',
      'timeline-view': 'timelinePanelTimelineStackSplitViewState',
      auditsSidebarWidth: 'auditsPanelSplitViewState',
      layersSidebarWidth: 'layersPanelSplitViewState',
      profilesSidebarWidth: 'profilesPanelSplitViewState',
      resourcesSidebarWidth: 'resourcesPanelSplitViewState',
    };
    const empty = {};
    for (const oldName in settingNames) {
      const newName = settingNames[oldName];
      const oldNameH = oldName + 'H';

      let newValue: object|null = null;
      const oldSetting = this.#settings.createSetting(oldName, empty);
      if (oldSetting.get() !== empty) {
        newValue = newValue || {};
        // @ts-expect-error
        newValue.vertical = {};
        // @ts-expect-error
        newValue.vertical.size = oldSetting.get();
        this.#removeSetting(oldSetting);
      }
      const oldSettingH = this.#settings.createSetting(oldNameH, empty);
      if (oldSettingH.get() !== empty) {
        newValue = newValue || {};
        // @ts-expect-error
        newValue.horizontal = {};
        // @ts-expect-error
        newValue.horizontal.size = oldSettingH.get();
        this.#removeSetting(oldSettingH);
      }
      if (newValue) {
        this.#settings.createSetting(newName, {}).set(newValue);
      }
    }
  }

  updateVersionFrom5To6(): void {
    const settingNames: Record<string, string> = {
      debuggerSidebarHidden: 'sourcesPanelSplitViewState',
      navigatorHidden: 'sourcesPanelNavigatorSplitViewState',
      'WebInspector.Drawer.showOnLoad': 'Inspector.drawerSplitViewState',
    };

    for (const oldName in settingNames) {
      const oldSetting = this.#settings.createSetting(oldName, null);
      if (oldSetting.get() === null) {
        this.#removeSetting(oldSetting);
        continue;
      }

      const newName = settingNames[oldName];
      const invert = oldName === 'WebInspector.Drawer.showOnLoad';
      const hidden = oldSetting.get() !== invert;
      this.#removeSetting(oldSetting);
      const showMode = hidden ? 'OnlyMain' : 'Both';

      const newSetting = this.#settings.createSetting(newName, {});
      const newValue = newSetting.get() || {};

      // @ts-expect-error
      newValue.vertical = newValue.vertical || {};

      // @ts-expect-error
      newValue.vertical.showMode = showMode;

      // @ts-expect-error
      newValue.horizontal = newValue.horizontal || {};

      // @ts-expect-error
      newValue.horizontal.showMode = showMode;
      newSetting.set(newValue);
    }
  }

  updateVersionFrom6To7(): void {
    const settingNames = {
      sourcesPanelNavigatorSplitViewState: 'sourcesPanelNavigatorSplitViewState',
      elementsPanelSplitViewState: 'elementsPanelSplitViewState',
      stylesPaneSplitViewState: 'stylesPaneSplitViewState',
      sourcesPanelDebuggerSidebarSplitViewState: 'sourcesPanelDebuggerSidebarSplitViewState',
    };

    const empty = {};
    for (const name in settingNames) {
      const setting =
          this.#settings.createSetting<{vertical?: {size?: number}, horizontal?: {size?: number}}>(name, empty);
      const value = setting.get();
      if (value === empty) {
        continue;
      }
      // Zero out saved percentage sizes, and they will be restored to defaults.
      if (value.vertical?.size && value.vertical.size < 1) {
        value.vertical.size = 0;
      }
      if (value.horizontal?.size && value.horizontal.size < 1) {
        value.horizontal.size = 0;
      }
      setting.set(value);
    }
  }

  updateVersionFrom7To8(): void {
  }

  updateVersionFrom8To9(): void {
    const settingNames = ['skipStackFramesPattern', 'workspaceFolderExcludePattern'];

    for (let i = 0; i < settingNames.length; ++i) {
      const setting = this.#settings.createSetting<string|unknown[]>(settingNames[i], '');
      let value = setting.get();
      if (!value) {
        return;
      }
      if (typeof value === 'string') {
        value = [value];
      }
      for (let j = 0; j < value.length; ++j) {
        if (typeof value[j] === 'string') {
          value[j] = {pattern: value[j]};
        }
      }
      setting.set(value);
    }
  }

  updateVersionFrom9To10(): void {
    // This one is localStorage specific, which is fine.
    if (!window.localStorage) {
      return;
    }
    for (const key in window.localStorage) {
      if (key.startsWith('revision-history')) {
        window.localStorage.removeItem(key);
      }
    }
  }

  updateVersionFrom10To11(): void {
    const oldSettingName = 'customDevicePresets';
    const newSettingName = 'customEmulatedDeviceList';
    const oldSetting = this.#settings.createSetting<unknown>(oldSettingName, undefined);
    const list = oldSetting.get();
    if (!Array.isArray(list)) {
      return;
    }
    const newList = [];
    for (let i = 0; i < list.length; ++i) {
      const value = list[i];

      const device: Record<string, any> = {};
      device['title'] = value['title'];
      device['type'] = 'unknown';
      device['user-agent'] = value['userAgent'];
      device['capabilities'] = [];
      if (value['touch']) {
        device['capabilities'].push('touch');
      }
      if (value['mobile']) {
        device['capabilities'].push('mobile');
      }
      device['screen'] = {};
      device['screen']['vertical'] = {width: value['width'], height: value['height']};
      device['screen']['horizontal'] = {width: value['height'], height: value['width']};
      device['screen']['device-pixel-ratio'] = value['deviceScaleFactor'];
      device['modes'] = [];
      device['show-by-default'] = true;
      device['show'] = 'Default';
      newList.push(device);
    }
    if (newList.length) {
      this.#settings.createSetting<unknown[]>(newSettingName, []).set(newList);
    }
    this.#removeSetting(oldSetting);
  }

  updateVersionFrom11To12(): void {
    this.migrateSettingsFromLocalStorage();
  }

  updateVersionFrom12To13(): void {
    this.migrateSettingsFromLocalStorage();
    this.#removeSetting(this.#settings.createSetting('timelineOverviewMode', ''));
  }

  updateVersionFrom13To14(): void {
    const defaultValue = {throughput: -1, latency: 0};
    this.#settings.createSetting('networkConditions', defaultValue).set(defaultValue);
  }

  updateVersionFrom14To15(): void {
    const setting = this.#settings.createLocalSetting<any>('workspaceExcludedFolders', {});
    const oldValue = setting.get();
    const newValue: Record<string, string[]> = {};
    for (const fileSystemPath in oldValue) {
      newValue[fileSystemPath] = [];
      for (const entry of oldValue[fileSystemPath]) {
        newValue[fileSystemPath].push(entry.path);
      }
    }
    setting.set(newValue);
  }

  updateVersionFrom15To16(): void {
    const setting = this.#settings.createSetting<any>('InspectorView.panelOrder', {});
    const tabOrders = setting.get();
    for (const key of Object.keys(tabOrders)) {
      tabOrders[key] = (tabOrders[key] + 1) * 10;
    }
    setting.set(tabOrders);
  }

  updateVersionFrom16To17(): void {
    const setting = this.#settings.createSetting<any>('networkConditionsCustomProfiles', []);
    const oldValue = setting.get();
    const newValue = [];
    if (Array.isArray(oldValue)) {
      for (const preset of oldValue) {
        if (typeof preset.title === 'string' && typeof preset.value === 'object' &&
            typeof preset.value.throughput === 'number' && typeof preset.value.latency === 'number') {
          newValue.push({
            title: preset.title,
            value: {download: preset.value.throughput, upload: preset.value.throughput, latency: preset.value.latency},
          });
        }
      }
    }
    setting.set(newValue);
  }

  updateVersionFrom17To18(): void {
    const setting = this.#settings.createLocalSetting<any>('workspaceExcludedFolders', {});
    const oldValue = setting.get();
    const newValue: Record<string, string> = {};
    for (const oldKey in oldValue) {
      let newKey = oldKey.replace(/\\/g, '/');
      if (!newKey.startsWith('file://')) {
        if (newKey.startsWith('/')) {
          newKey = 'file://' + newKey;
        } else {
          newKey = 'file:///' + newKey;
        }
      }
      newValue[newKey] = oldValue[oldKey];
    }
    setting.set(newValue);
  }

  updateVersionFrom18To19(): void {
    const defaultColumns = {status: true, type: true, initiator: true, size: true, time: true};
    const visibleColumnSettings = this.#settings.createSetting<any>('networkLogColumnsVisibility', defaultColumns);
    const visibleColumns = visibleColumnSettings.get();
    visibleColumns.name = true;
    visibleColumns.timeline = true;

    const configs: Record<string, {
      visible: number,
    }> = {};
    for (const columnId in visibleColumns) {
      if (!visibleColumns.hasOwnProperty(columnId)) {
        continue;
      }
      configs[columnId.toLowerCase()] = {visible: visibleColumns[columnId]};
    }
    const newSetting = this.#settings.createSetting('networkLogColumns', {});
    newSetting.set(configs);
    this.#removeSetting(visibleColumnSettings);
  }

  updateVersionFrom19To20(): void {
    const oldSetting = this.#settings.createSetting('InspectorView.panelOrder', {});
    const newSetting = this.#settings.createSetting('panel-tabOrder', {});
    newSetting.set(oldSetting.get());
    this.#removeSetting(oldSetting);
  }

  updateVersionFrom20To21(): void {
    const networkColumns = this.#settings.createSetting('networkLogColumns', {});
    const columns = (networkColumns.get() as Record<string, string>);
    delete columns['timeline'];
    delete columns['waterfall'];
    networkColumns.set(columns);
  }

  updateVersionFrom21To22(): void {
    const breakpointsSetting = this.#settings.createLocalSetting<any>('breakpoints', []);
    const breakpoints = breakpointsSetting.get();
    for (const breakpoint of breakpoints) {
      breakpoint['url'] = breakpoint['sourceFileId'];
      delete breakpoint['sourceFileId'];
    }
    breakpointsSetting.set(breakpoints);
  }

  updateVersionFrom22To23(): void {
    // This update is no-op.
  }

  updateVersionFrom23To24(): void {
    const oldSetting = this.#settings.createSetting('searchInContentScripts', false);
    const newSetting = this.#settings.createSetting('searchInAnonymousAndContentScripts', false);
    newSetting.set(oldSetting.get());
    this.#removeSetting(oldSetting);
  }

  updateVersionFrom24To25(): void {
    const defaultColumns = {status: true, type: true, initiator: true, size: true, time: true};

    const networkLogColumnsSetting = this.#settings.createSetting<any>('networkLogColumns', defaultColumns);
    const columns = networkLogColumnsSetting.get();
    delete columns.product;
    networkLogColumnsSetting.set(columns);
  }

  updateVersionFrom25To26(): void {
    const oldSetting = this.#settings.createSetting('messageURLFilters', {});
    const urls = Object.keys(oldSetting.get());
    const textFilter = urls.map(url => `-url:${url}`).join(' ');
    if (textFilter) {
      const textFilterSetting = this.#settings.createSetting<any>('console.textFilter', '');
      const suffix = textFilterSetting.get() ? ` ${textFilterSetting.get()}` : '';
      textFilterSetting.set(`${textFilter}${suffix}`);
    }
    this.#removeSetting(oldSetting);
  }

  updateVersionFrom26To27(): void {
    const settings = this.#settings;
    function renameKeyInObjectSetting(settingName: string, from: string, to: string): void {
      const setting = settings.createSetting<any>(settingName, {});
      const value = setting.get();
      if (from in value) {
        value[to] = value[from];
        delete value[from];
        setting.set(value);
      }
    }

    function renameInStringSetting(settingName: string, from: string, to: string): void {
      const setting = settings.createSetting(settingName, '');
      const value = setting.get();
      if (value === from) {
        setting.set(to);
      }
    }

    renameKeyInObjectSetting('panel-tabOrder', 'audits2', 'audits');
    renameKeyInObjectSetting('panel-closeableTabs', 'audits2', 'audits');
    renameInStringSetting('panel-selectedTab', 'audits2', 'audits');
  }

  updateVersionFrom27To28(): void {
    const setting = this.#settings.createSetting('uiTheme', 'systemPreferred');
    if (setting.get() === 'default') {
      setting.set('systemPreferred');
    }
  }

  updateVersionFrom28To29(): void {
    const settings = this.#settings;
    function renameKeyInObjectSetting(settingName: string, from: string, to: string): void {
      const setting = settings.createSetting<any>(settingName, {});
      const value = setting.get();
      if (from in value) {
        value[to] = value[from];
        delete value[from];
        setting.set(value);
      }
    }

    function renameInStringSetting(settingName: string, from: string, to: string): void {
      const setting = settings.createSetting(settingName, '');
      const value = setting.get();
      if (value === from) {
        setting.set(to);
      }
    }

    renameKeyInObjectSetting('panel-tabOrder', 'audits', 'lighthouse');
    renameKeyInObjectSetting('panel-closeableTabs', 'audits', 'lighthouse');
    renameInStringSetting('panel-selectedTab', 'audits', 'lighthouse');
  }

  updateVersionFrom29To30(): void {
    // Create new location agnostic setting
    const closeableTabSetting = this.#settings.createSetting('closeableTabs', {});

    // Read current settings
    const panelCloseableTabSetting = this.#settings.createSetting('panel-closeableTabs', {});
    const drawerCloseableTabSetting = this.#settings.createSetting('drawer-view-closeableTabs', {});
    const openTabsInPanel = panelCloseableTabSetting.get();
    const openTabsInDrawer = panelCloseableTabSetting.get();

    // Set #value of new setting
    const newValue = Object.assign(openTabsInDrawer, openTabsInPanel);
    closeableTabSetting.set(newValue);

    // Remove old settings
    this.#removeSetting(panelCloseableTabSetting);
    this.#removeSetting(drawerCloseableTabSetting);
  }

  updateVersionFrom30To31(): void {
    // Remove recorder_recordings setting that was used for storing recordings
    // by an old recorder experiment.
    const recordingsSetting = this.#settings.createSetting('recorder_recordings', []);
    this.#removeSetting(recordingsSetting);
  }

  updateVersionFrom31To32(): void {
    // Introduce the new 'resourceTypeName' property on stored breakpoints. Prior to
    // this change we synchronized the breakpoint only by URL, but since we don't
    // know on which resource type the given breakpoint was set, we just assume
    // 'script' here to keep things simple.

    const breakpointsSetting = this.#settings.createLocalSetting<any>('breakpoints', []);
    const breakpoints = breakpointsSetting.get();
    for (const breakpoint of breakpoints) {
      breakpoint['resourceTypeName'] = 'script';
    }
    breakpointsSetting.set(breakpoints);
  }

  updateVersionFrom32To33(): void {
    const previouslyViewedFilesSetting = this.#settings.createLocalSetting<any>('previouslyViewedFiles', []);
    let previouslyViewedFiles = previouslyViewedFilesSetting.get();

    // Discard old 'previouslyViewedFiles' items that don't have a 'url' property.

    previouslyViewedFiles = previouslyViewedFiles.filter((previouslyViewedFile: any) => 'url' in previouslyViewedFile);

    // Introduce the new 'resourceTypeName' property on previously viewed files.
    // Prior to this change we only keyed them based on the URL, but since we
    // don't know which resource type the given file had, we just assume 'script'
    // here to keep things simple.
    for (const previouslyViewedFile of previouslyViewedFiles) {
      previouslyViewedFile['resourceTypeName'] = 'script';
    }

    previouslyViewedFilesSetting.set(previouslyViewedFiles);
  }

  updateVersionFrom33To34(): void {
    // Introduces the 'isLogpoint' property on stored breakpoints. This information was
    // previously encoded in the 'condition' itself. This migration leaves the condition
    // alone but ensures that 'isLogpoint' is accurate for already stored breakpoints.
    // This enables us to use the 'isLogpoint' property in code.
    // A separate migration will remove the special encoding from the condition itself
    // once all refactorings are done.

    // The prefix/suffix are hardcoded here, since these constants will be removed in
    // the future.
    const logpointPrefix = '/** DEVTOOLS_LOGPOINT */ console.log(';
    const logpointSuffix = ')';

    const breakpointsSetting = this.#settings.createLocalSetting<any>('breakpoints', []);
    const breakpoints = breakpointsSetting.get();
    for (const breakpoint of breakpoints) {
      const isLogpoint =
          breakpoint.condition.startsWith(logpointPrefix) && breakpoint.condition.endsWith(logpointSuffix);
      breakpoint['isLogpoint'] = isLogpoint;
    }
    breakpointsSetting.set(breakpoints);
  }

  updateVersionFrom34To35(): void {
    // Uses the 'isLogpoint' property on stored breakpoints to remove the prefix/suffix
    // from logpoints. This way, we store the entered log point condition as the user
    // entered it.

    // The prefix/suffix are hardcoded here, since these constants will be removed in
    // the future.
    const logpointPrefix = '/** DEVTOOLS_LOGPOINT */ console.log(';
    const logpointSuffix = ')';

    const breakpointsSetting = this.#settings.createLocalSetting<any>('breakpoints', []);
    const breakpoints = breakpointsSetting.get();
    for (const breakpoint of breakpoints) {
      const {condition, isLogpoint} = breakpoint;
      if (isLogpoint) {
        breakpoint.condition = condition.slice(logpointPrefix.length, condition.length - logpointSuffix.length);
      }
    }
    breakpointsSetting.set(breakpoints);
  }

  updateVersionFrom35To36(): void {
    // We have changed the default from 'false' to 'true' and this updates the existing setting just for once.
    this.#settings.createSetting('showThirdPartyIssues', true).set(true);
  }

  updateVersionFrom36To37(): void {
    const updateStorage = (storage: SettingsStorage): void => {
      for (const key of storage.keys()) {
        const normalizedKey = Settings.normalizeSettingName(key);
        if (normalizedKey !== key) {
          const value = storage.get(key);
          this.#removeSetting({name: key, storage});
          storage.set(normalizedKey, value);
        }
      }
    };
    updateStorage(this.#settings.globalStorage);
    updateStorage(this.#settings.syncedStorage);
    updateStorage(this.#settings.localStorage);

    for (const key of this.#settings.globalStorage.keys()) {
      if ((key.startsWith('data-grid-') && key.endsWith('-column-weights')) || key.endsWith('-tab-order') ||
          key === 'views-location-override' || key === 'closeable-tabs') {
        const setting = this.#settings.createSetting(key, {});
        setting.set(Platform.StringUtilities.toKebabCaseKeys(setting.get()));
      }
      if (key.endsWith('-selected-tab')) {
        const setting = this.#settings.createSetting(key, '');
        setting.set(Platform.StringUtilities.toKebabCase(setting.get()));
      }
    }
  }

  updateVersionFrom37To38(): void {
    const getConsoleInsightsEnabledSetting = (): Setting<boolean>|undefined => {
      try {
        return this.#settings.moduleSetting('console-insights-enabled') as Setting<boolean>;
      } catch {
        return;
      }
    };

    const consoleInsightsEnabled = getConsoleInsightsEnabledSetting();
    const onboardingFinished = this.#settings.createLocalSetting('console-insights-onboarding-finished', false);

    if (consoleInsightsEnabled && consoleInsightsEnabled.get() === true && onboardingFinished.get() === false) {
      consoleInsightsEnabled.set(false);
    }
    if (consoleInsightsEnabled && consoleInsightsEnabled.get() === false) {
      onboardingFinished.set(false);
    }
  }

  updateVersionFrom38To39(): void {
    const PREFERRED_NETWORK_COND = 'preferred-network-condition';
    // crrev.com/c/5582013 renamed "Slow 3G" to "3G" and "Fast 3G" => "Slow 4G".
    // Any users with the old values need to have them moved to avoid breaking DevTools.
    // Note: we load the raw value via the globalStorage here because
    // `createSetting` creates if it is not present, and we do not want that;
    // we only want to update existing, old values.
    const setting = this.#settings.globalStorage.get(PREFERRED_NETWORK_COND);
    if (!setting) {
      return;
    }
    try {
      const networkSetting = JSON.parse(setting) as unknown as {
        // Can't use SDK type here as it creates a common<>sdk circular
        // dep. This type is not exhaustive but contains the fields we
        // need.
        title: string,
        i18nTitleKey?: string,
      };
      if (networkSetting.title === 'Slow 3G') {
        networkSetting.title = '3G';
        networkSetting.i18nTitleKey = '3G';
        this.#settings.globalStorage.set(PREFERRED_NETWORK_COND, JSON.stringify(networkSetting));
      } else if (networkSetting.title === 'Fast 3G') {
        networkSetting.title = 'Slow 4G';
        networkSetting.i18nTitleKey = 'Slow 4G';
        this.#settings.globalStorage.set(PREFERRED_NETWORK_COND, JSON.stringify(networkSetting));
      }
    } catch {
      // If parsing the setting threw, it's in some invalid state, so remove it.
      this.#settings.globalStorage.remove(PREFERRED_NETWORK_COND);
    }
  }

  /**
   * There are two related migrations here for handling network throttling persistence:
   * 1. Go through all user custom throttling conditions and add a `key` property.
   * 2. If the user has a 'preferred-network-condition' setting, take the value
   *    of that and set the right key for the new 'active-network-condition-key'
   *    setting. Then, remove the now-obsolete 'preferred-network-condition'
   *    setting.
   */
  updateVersionFrom39To40(): void {
    const hasCustomNetworkConditionsSetting = (): boolean => {
      try {
        // this will error if it does not exist
        this.#settings.moduleSetting('custom-network-conditions');
        return true;
      } catch {
        return false;
      }
    };
    if (hasCustomNetworkConditionsSetting()) {
      /**
       * We added keys to custom network throttling conditions in M140, so we
       * need to go through any existing profiles the user has and add the key to
       * them.
       * We can't use the SDK.NetworkManager.Condition here as it would be a
       * circular dependency. All that matters is that these conditions are
       * objects, and we need to set the right key on each one. The actual keys &
       * values in the object are not important.
       */
      const conditionsSetting =
          this.#settings.moduleSetting('custom-network-conditions') as Setting<Array<{key?: string}>>;
      const customConditions = conditionsSetting.get();
      if (customConditions?.length > 0) {
        customConditions.forEach((condition, i) => {
          // This could be run multiple times, make sure that we don't override any
          // existing keys.
          if (condition.key) {
            return;
          }
          // The format of this key is important: see
          // SDK.NetworkManager.UserDefinedThrottlingConditionKey
          condition.key = `USER_CUSTOM_SETTING_${i + 1}`;
        });
        conditionsSetting.set(customConditions);
      }
    }

    // Additionally, we need to make sure we persist the right throttling for
    // users who have a preferred-network-condition set.
    const PREFERRED_NETWORK_COND_SETTING = 'preferred-network-condition';
    // We shipped a change to how we persist network throttling conditions and
    // added a `key` property rather than rely on any user visible string which
    // is more likely to change. This migration step tries to update the
    // setting for users, or removes it if we fail, so they start fresh next
    // time they load DevTools.
    const setting = this.#settings.globalStorage.get(PREFERRED_NETWORK_COND_SETTING);
    if (!setting) {
      return;
    }
    // The keys here are the UI Strings as of July 2025 (shipped in M139).
    // This migration shipped in M140. The values are the values of the
    // `PredefinedThrottlingConditionKey` in SDK.NetworkManager.
    const UI_STRING_TO_NEW_KEY = {
      'Fast 4G': 'SPEED_FAST_4G',
      'Slow 4G': 'SPEED_SLOW_4G',
      '3G': 'SPEED_3G',
      'No throttling': 'NO_THROTTLING',
      Offline: 'OFFLINE'
    };
    try {
      const networkSetting = JSON.parse(setting) as unknown as {
        // Can't use SDK type here as it creates a common<>sdk circular
        // dep. We only rely on the i18nTitleKey.
        i18nTitleKey?: string,
      };
      if (networkSetting.i18nTitleKey && UI_STRING_TO_NEW_KEY.hasOwnProperty(networkSetting.i18nTitleKey)) {
        const key = UI_STRING_TO_NEW_KEY[networkSetting.i18nTitleKey as keyof typeof UI_STRING_TO_NEW_KEY];

        // The second argument is the default value, so it's important that we
        // set this to the default, and then update it to the new key.
        const newSetting = this.#settings.createSetting('active-network-condition-key', 'NO_THROTTLING');
        newSetting.set(key);
      }
    } finally {
      // This setting is now not used, so we can remove it.
      this.#settings.globalStorage.remove(PREFERRED_NETWORK_COND_SETTING);
    }
  }

  // This migration handles two setting renames that requires inverted logic
  // (from "Hide X" to "X") and flipped the default values to true.
  updateVersionFrom40To41(): void {
    // 1. Rename 'Hide network messages' to 'Network messages'
    if (this.#settings.syncedStorage.has('hide-network-messages')) {
      const oldNetworkSetting = this.#settings.createSetting('hide-network-messages', false, SettingStorageType.SYNCED);
      if (!this.#settings.syncedStorage.has('network-messages')) {
        const newNetworkSetting = this.#settings.createSetting('network-messages', true, SettingStorageType.SYNCED);
        // If the user had a saved preference for the old setting, migrate it by
        // inverting the value to match the new logic.
        newNetworkSetting.set(!oldNetworkSetting.get());
      }
      this.#removeSetting(oldNetworkSetting);
    }

    // 2. Rename 'Hide `chrome` frame in Layers view' to 'Chrome frame in Layers view'
    if (this.#settings.syncedStorage.has('frame-viewer-hide-chrome-window')) {
      const oldChromeFrameSetting =
          this.#settings.createSetting('frame-viewer-hide-chrome-window', false, SettingStorageType.SYNCED);
      if (!this.#settings.syncedStorage.has('frame-viewer-chrome-window')) {
        const newChromeFrameSetting =
            this.#settings.createSetting('frame-viewer-chrome-window', true, SettingStorageType.SYNCED);
        // Similar to above, move the preference and invert the boolean.
        newChromeFrameSetting.set(!oldChromeFrameSetting.get());
      }
      this.#removeSetting(oldChromeFrameSetting);
    }
  }

  /**
   * The recording in recorder panel may have unreasonably long titles
   * or a lot of steps which can cause renderer crashes.
   * Similar to https://crbug.com/40918380
   */
  updateVersionFrom41To42(): void {
    const recordingsSetting = this.#settings.createSetting<Array<{flow: {steps: unknown[], title: string}}>>(
        'recorder-recordings-ng',
        [],
    );
    const recordings = recordingsSetting.get();
    if (recordings.length === 0) {
      return;
    }

    for (const recording of recordings) {
      recording.flow.title = Platform.StringUtilities.trimEndWithMaxLength(recording.flow.title, 300);
      recording.flow.steps = recording.flow.steps.slice(0, 4096);
    }

    recordingsSetting.set(recordings);
  }

  updateVersionFrom42To43(): void {
    const timelineShowAllEventsExperimentEnabled =
        Root.Runtime.experiments.getValueFromStorage('timeline-show-all-events' as Root.ExperimentNames.ExperimentName);
    if (timelineShowAllEventsExperimentEnabled !== undefined) {
      if (this.#settings.syncedStorage.has('timeline-show-all-events')) {
        return;  // Already migrated
      }
      try {
        const timelineShowAllEventsSetting = this.#settings.moduleSetting('timeline-show-all-events');
        timelineShowAllEventsSetting.set(timelineShowAllEventsExperimentEnabled);
      } catch {
        // If the setting is not registered yet (e.g. in tests), skip.
      }
    }
  }

  updateVersionFrom43To44(): void {
    const apcaExperimentEnabled =
        Root.Runtime.experiments.getValueFromStorage('apca' as Root.ExperimentNames.ExperimentName);
    if (apcaExperimentEnabled !== undefined) {
      if (this.#settings.syncedStorage.has('apca')) {
        return;  // Already migrated
      }
      try {
        const apcaSetting = this.#settings.moduleSetting('apca');
        apcaSetting.set(apcaExperimentEnabled);
      } catch {
        // If the setting is not registered yet (e.g. in tests), skip.
      }
    }
  }

  /*
   * Any new migration should be added before this comment.
   *
   * IMPORTANT: Migrations must be idempotent, since they may be applied
   * multiple times! E.g. when renaming a setting one has to check that the
   * a setting with the new name does not yet exist.
   * ----------------------------------------------------------------------- */

  private migrateSettingsFromLocalStorage(): void {
    // This step migrates all the settings except for the ones below into the browser profile.
    const localSettings = new Set<string>([
      'advancedSearchConfig',
      'breakpoints',
      'consoleHistory',
      'domBreakpoints',
      'eventListenerBreakpoints',
      'fileSystemMapping',
      'lastSelectedSourcesSidebarPaneTab',
      'previouslyViewedFiles',
      'savedURLs',
      'watchExpressions',
      'workspaceExcludedFolders',
      'xhrBreakpoints',
    ]);
    if (!window.localStorage) {
      return;
    }

    for (const key in window.localStorage) {
      if (localSettings.has(key)) {
        continue;
      }
      const value = window.localStorage[key];
      window.localStorage.removeItem(key);
      this.#settings.globalStorage.set(key, value);
    }
  }

  private clearBreakpointsWhenTooMany(breakpointsSetting: Setting<unknown[]>, maxBreakpointsCount: number): void {
    // If there are too many breakpoints in a storage, it is likely due to a recent bug that caused
    // periodical breakpoints duplication leading to inspector slowness.
    if (breakpointsSetting.get().length > maxBreakpointsCount) {
      breakpointsSetting.set([]);
    }
  }
}
/* eslint-enable @typescript-eslint/no-explicit-any */
