/* eslint-disable @typescript-eslint/member-ordering */
import {
  DestroyRef,
  Injectable,
  computed,
  effect,
  inject,
  signal
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Store } from '@ngrx/store';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../../../environments/environment';
import { isLocalDevPluginSourceUrl } from '../../domain/rules/plugin-source-url.rules';
import { RealtimeSessionFacade } from '../../../../core/realtime';
import { AppI18nService } from '../../../../core/i18n';
import { getUserScopedStorageKey } from '../../../../core/storage/current-user-storage';
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
import { jsonStorage } from '../../../../infrastructure/persistence/json-storage.service';
import type {
  PluginRequirementSummary,
  TojuPluginInstallScope,
  TojuPluginManifest
} from '../../../../shared-kernel';
import {
  selectCurrentRoom,
  selectCurrentRoomId,
  selectCurrentRoomName,
  selectSavedRooms
} from '../../../../store/rooms/rooms.selectors';
import { selectCurrentUser } from '../../../../store/users/users.selectors';
import { ServerDirectoryFacade, type ServerSourceSelector } from '../../../server-directory';
import { getPluginInstallScope } from '../../domain/logic/plugin-install-scope.logic';
import { validateTojuPluginManifest } from '../../domain/logic/plugin-manifest-validation.logic';
import type {
  InstalledStorePlugin,
  PersistedServerPluginInstallState,
  PersistedPluginStoreState,
  PluginStoreEntry,
  PluginStoreInstallState,
  PluginStoreReadme,
  PluginStoreSourceResult
} from '../../domain/models/plugin-store.models';
import { PluginHostService } from './plugin-host.service';
import { PluginCapabilityService } from './plugin-capability.service';
import { PluginDesktopStateService } from './plugin-desktop-state.service';
import { PluginRequirementService } from './plugin-requirement.service';
import { PluginRegistryService } from './plugin-registry.service';
import { fileUrlToPath, grantPluginReadRoots } from '../../domain/rules/plugin-local-file.rules';

const STORE_SCHEMA_VERSION = 2;
const STORAGE_KEY_PLUGIN_STORE = 'metoyou_plugin_store';
const STORAGE_KEY_SERVER_PLUGIN_INSTALLS = 'metoyou_server_plugin_installs';
const PLUGIN_CACHE_DIR = 'plugin-bundles';
const DEFAULT_PLUGIN_SOURCE_URLS = [...environment.pluginStore.defaultSourceUrls];

export interface PluginStoreInstallOptions {
  activate?: boolean;
  manifest?: TojuPluginManifest;
  optional?: boolean;
  serverId?: string;
}

interface ServerInstalledPluginsLoadState {
  actorUserId: string | null;
  loaded: boolean;
  loading: boolean;
  roomId: string | null;
}

@Injectable({ providedIn: 'root' })
export class PluginStoreService {
  private readonly electronBridge = inject(ElectronBridgeService);
  private readonly capabilities = inject(PluginCapabilityService);
  private readonly desktopState = inject(PluginDesktopStateService);
  private readonly destroyRef = inject(DestroyRef);
  private readonly host = inject(PluginHostService);
  private readonly jsonStorage = jsonStorage;
  private readonly pluginRequirements = inject(PluginRequirementService);
  private readonly realtime = inject(RealtimeSessionFacade, { optional: true });
  private readonly registry = inject(PluginRegistryService);
  private readonly serverDirectory = inject(ServerDirectoryFacade, { optional: true });
  private readonly store = inject(Store, { optional: true });
  private readonly appI18n = inject(AppI18nService);
  private readonly currentRoom = this.store?.selectSignal(selectCurrentRoom) ?? null;
  private readonly currentRoomId = this.store?.selectSignal(selectCurrentRoomId) ?? null;
  private readonly currentRoomName = this.store?.selectSignal(selectCurrentRoomName) ?? null;
  private readonly savedRooms = this.store?.selectSignal(selectSavedRooms) ?? null;
  private readonly currentUser = this.store?.selectSignal(selectCurrentUser) ?? null;
  private readonly sourceUrlsSignal = signal<string[]>([]);
  private readonly sourcesSignal = signal<PluginStoreSourceResult[]>([]);
  private readonly clientInstalledPluginsSignal = signal<InstalledStorePlugin[]>([]);
  private readonly serverInstalledPluginsSignal = signal<InstalledStorePlugin[]>([]);
  private readonly serverInstalledPluginsLoadStateSignal = signal<ServerInstalledPluginsLoadState>({
    actorUserId: null,
    loaded: false,
    loading: false,
    roomId: null
  });
  private readonly loadingSignal = signal(false);
  private refreshAbortController: AbortController | null = null;
  private refreshVersion = 0;
  private installedLoadVersion = 0;
  private autoUpdateInProgress = false;
  private stateMutated = false;

  readonly sourceUrls = this.sourceUrlsSignal.asReadonly();
  readonly sources = this.sourcesSignal.asReadonly();
  readonly installedPlugins = computed(() => {
    const installedPlugins = this.clientInstalledPluginsSignal().concat(this.serverInstalledPluginsSignal());

    return installedPlugins.sort(sortInstalledPlugins);
  });
  readonly isLoading = this.loadingSignal.asReadonly();
  readonly availablePlugins = computed(() => this.sources().flatMap((source) => source.plugins));
  readonly hasActiveServerInstallScope = computed(() => !!this.currentRoomId?.());
  readonly installedById = computed(() => new Map(this.installedPlugins().map((plugin) => [plugin.manifest.id, plugin])));
  readonly installScopeLabel = computed(() => this.currentRoomName?.() || 'this device');
  readonly serverInstalledPluginsReadyForCurrentRoom = computed(() => {
    const roomId = this.currentRoomId?.() ?? null;
    const actorUserId = this.currentActorUserId();

    if (!roomId || !actorUserId || !this.serverDirectory) {
      return true;
    }

    const loadState = this.serverInstalledPluginsLoadStateSignal();

    return loadState.loaded
      && !loadState.loading
      && loadState.roomId === roomId
      && loadState.actorUserId === actorUserId;
  });

  constructor() {
    const state = this.loadState();

    this.sourceUrlsSignal.set(state.sourceUrls);
    void this.applyInstalledPlugins(state.installedPlugins, 'client');

    if (state.sourceUrls.length > 0) {
      void this.bootstrapSourceRefresh(state.sourceUrls);
    }

    if (this.currentRoomId && this.currentUser && this.serverDirectory) {
      effect(() => {
        const roomId = this.currentRoomId?.() ?? null;
        const actorUserId = this.currentActorUserId();

        void this.loadInstalledPluginsForScope(roomId, actorUserId);
      });

      this.realtime?.onSignalingMessage
        .pipe(takeUntilDestroyed(this.destroyRef))
        .subscribe((message) => {
          if (isPluginRequirementsChangedMessage(message) && message.serverId === this.currentRoomId?.()) {
            void this.loadInstalledPluginsForScope(message.serverId, this.currentActorUserId());
          }
        });
    }

    void this.loadDesktopState();
  }

  async addSourceUrl(rawUrl: string): Promise<void> {
    const sourceUrl = normalizeSourceUrl(rawUrl, 'Plugin source URL');

    if (this.sourceUrls().includes(sourceUrl)) {
      throw new Error('Plugin source already exists');
    }

    this.sourceUrlsSignal.update((sourceUrls) => [...sourceUrls, sourceUrl]);
    this.saveState();
    await this.ensurePluginSourceReadRoot(sourceUrl);
    await this.refreshSources();
  }

  async removeSourceUrl(sourceUrl: string): Promise<void> {
    this.sourceUrlsSignal.update((sourceUrls) => sourceUrls.filter((candidate) => candidate !== sourceUrl));
    this.sourcesSignal.update((sources) => sources.filter((source) => source.url !== sourceUrl));
    this.saveState();
    await this.refreshSources();
  }

  async refreshSources(): Promise<void> {
    const currentRefresh = this.refreshVersion + 1;
    const abortController = new AbortController();

    this.refreshVersion = currentRefresh;
    this.refreshAbortController?.abort();
    this.refreshAbortController = abortController;
    this.loadingSignal.set(true);

    try {
      await this.ensurePluginSourceReadRoots(this.sourceUrls());
      const sources = await Promise.all(this.sourceUrls().map((sourceUrl) => this.loadSource(sourceUrl, abortController.signal)));

      if (this.refreshVersion === currentRefresh) {
        this.sourcesSignal.set(sources);
        void this.autoUpdateInstalledPlugins();
      }
    } finally {
      if (this.refreshVersion === currentRefresh) {
        this.refreshAbortController = null;
        this.loadingSignal.set(false);
      }
    }
  }

  async installPlugin(plugin: PluginStoreEntry, options: PluginStoreInstallOptions = {}): Promise<InstalledStorePlugin> {
    if (!plugin.installUrl) {
      throw new Error('Plugin does not provide an install manifest URL');
    }

    const manifest = this.withStoreBundleMetadata(options.manifest ?? await this.fetchPluginManifest(plugin.installUrl), plugin);
    const installScope = getPluginInstallScope(manifest);
    const targetServerId = this.resolveInstallTargetServerId(installScope, options.serverId);
    const now = Date.now();
    const currentScopePlugins = installScope === 'server'
      ? await this.installedPluginsForServer(targetServerId)
      : this.installedPluginsForScope(installScope);
    const existing = currentScopePlugins.find((candidate) => candidate.manifest.id === manifest.id);
    const installOptions = {
      ...options,
      activate: options.activate === true || (installScope === 'server' && !existing)
    };
    const installedPlugin = await this.cacheInstalledPlugin({
      bundleUrl: manifest.bundle?.url ?? plugin.bundleUrl,
      installedAt: existing?.installedAt ?? now,
      installUrl: plugin.installUrl,
      manifest,
      sourceUrl: plugin.sourceUrl,
      updatedAt: now
    });
    const nextInstalledPlugins = currentScopePlugins
      .filter((candidate) => candidate.manifest.id !== manifest.id)
      .concat(installedPlugin)
      .sort(sortInstalledPlugins);

    await this.persistInstallResult(installScope, targetServerId, nextInstalledPlugins, installedPlugin, installOptions);
    await this.registerInstallResult(installScope, targetServerId, nextInstalledPlugins, installedPlugin, installOptions);

    return installedPlugin;
  }

  private resolveInstallTargetServerId(installScope: TojuPluginInstallScope, requestedServerId: string | undefined): string | null {
    if (installScope !== 'server') {
      return null;
    }

    const targetServerId = requestedServerId ?? this.currentRoomId?.() ?? null;

    if (!targetServerId) {
      throw new Error('Open a chat server before installing server-scoped plugins');
    }

    return targetServerId;
  }

  private async persistInstallResult(
    installScope: TojuPluginInstallScope,
    targetServerId: string | null,
    nextInstalledPlugins: InstalledStorePlugin[],
    installedPlugin: InstalledStorePlugin,
    options: PluginStoreInstallOptions
  ): Promise<void> {
    if (installScope === 'server') {
      await this.saveServerPluginRequirement(installedPlugin, targetServerId, options.optional === true ? 'optional' : 'required');
      return;
    }

    await this.persistInstalledPlugins(nextInstalledPlugins, installScope);
  }

  private async registerInstallResult(
    installScope: TojuPluginInstallScope,
    targetServerId: string | null,
    nextInstalledPlugins: InstalledStorePlugin[],
    installedPlugin: InstalledStorePlugin,
    options: PluginStoreInstallOptions
  ): Promise<void> {
    if (installScope === 'server' && targetServerId) {
      await this.writeLocalServerInstalledPlugins(targetServerId, nextInstalledPlugins);
    }

    if (installScope === 'server' && options.activate) {
      this.registry.setEnabled(installedPlugin.manifest.id, true);
    }

    if (installScope !== 'client' && targetServerId !== this.currentRoomId?.()) {
      if (options.activate) {
        await this.host.rememberActivation(installedPlugin.manifest.id);
      }

      return;
    }

    const sourcePath = installedPlugin.cachedSourcePath ?? installedPlugin.installUrl;

    if (sourcePath?.startsWith('file://')) {
      await this.ensurePluginSourceReadRoot(sourcePath);
    }

    this.host.registerLocalManifest(installedPlugin.manifest, sourcePath);

    this.setInstalledPluginsForScope(installScope, nextInstalledPlugins);

    if (options.activate) {
      await this.host.activatePluginById(installedPlugin.manifest.id);
    }
  }

  async loadInstallManifest(plugin: PluginStoreEntry): Promise<TojuPluginManifest> {
    if (!plugin.installUrl) {
      throw new Error('Plugin does not provide an install manifest URL');
    }

    return await this.fetchPluginManifest(plugin.installUrl);
  }

  async uninstallPlugin(pluginId: string, scope?: TojuPluginInstallScope, options: { serverId?: string } = {}): Promise<void> {
    const installScope = scope ?? this.findInstalledPluginScope(pluginId) ?? 'client';
    const currentInstalledPlugins = installScope === 'server'
      ? await this.installedPluginsForServer(options.serverId ?? this.currentRoomId?.() ?? null)
      : this.installedPluginsForScope(installScope);
    const nextInstalledPlugins = currentInstalledPlugins.filter((installedPlugin) => installedPlugin.manifest.id !== pluginId);

    if (installScope === 'server') {
      await this.deleteServerPluginRequirement(pluginId, options.serverId);
    } else {
      await this.persistInstalledPlugins(nextInstalledPlugins, installScope);
    }

    if (installScope === 'server' && options.serverId && options.serverId !== this.currentRoomId?.()) {
      return;
    }

    await this.host.deactivatePlugin(pluginId, { forgetActivation: true });
    this.registry.unregister(pluginId);
    this.setInstalledPluginsForScope(installScope, nextInstalledPlugins);
  }

  async loadInstalledPluginsForServer(serverId: string): Promise<InstalledStorePlugin[]> {
    return await this.installedPluginsForServer(serverId);
  }

  async getLocalServerInstalledPluginIds(serverId: string): Promise<Set<string>> {
    const installedPlugins = await this.readLocalServerInstalledPlugins(serverId);

    return new Set(installedPlugins.map((installedPlugin) => installedPlugin.manifest.id));
  }

  async installServerRequirementsLocally(
    serverId: string,
    requirements: PluginRequirementSummary[],
    options: { activate?: boolean } = {}
  ): Promise<InstalledStorePlugin[]> {
    const installedPlugins: InstalledStorePlugin[] = [];

    for (const requirement of requirements) {
      const installedPlugin = await this.resolveLocalInstallFromRequirement(requirement);

      if (installedPlugin) {
        installedPlugins.push(installedPlugin);
      }
    }

    if (installedPlugins.length === 0) {
      return await this.readLocalServerInstalledPlugins(serverId);
    }

    const currentInstalledPlugins = await this.readLocalServerInstalledPlugins(serverId);
    const currentById = new Map(currentInstalledPlugins.map((installedPlugin) => [installedPlugin.manifest.id, installedPlugin]));
    const nextById = new Map(currentById);

    for (const installedPlugin of installedPlugins) {
      const existing = currentById.get(installedPlugin.manifest.id);
      const cachedPlugin = await this.cacheInstalledPlugin({
        ...installedPlugin,
        installedAt: existing?.installedAt ?? installedPlugin.installedAt,
        updatedAt: installedPlugin.updatedAt
      });

      if (options.activate) {
        this.capabilities.grantAll(cachedPlugin.manifest);
      }

      nextById.set(cachedPlugin.manifest.id, cachedPlugin);
    }

    const nextInstalledPlugins = Array.from(nextById.values()).sort(sortInstalledPlugins);

    if (options.activate) {
      for (const installedPlugin of installedPlugins) {
        this.registry.setEnabled(installedPlugin.manifest.id, true);
        await this.host.rememberActivation(installedPlugin.manifest.id);
      }
    }

    await this.writeLocalServerInstalledPlugins(serverId, nextInstalledPlugins);

    if (serverId === this.currentRoomId?.()) {
      await this.applyInstalledPlugins(nextInstalledPlugins, 'server');
    }

    return nextInstalledPlugins;
  }

  async loadReadme(plugin: PluginStoreEntry): Promise<PluginStoreReadme> {
    if (!plugin.readmeUrl) {
      throw new Error('Plugin does not provide a readme URL');
    }

    return {
      markdown: await this.fetchText(plugin.readmeUrl, 'text/markdown,text/plain,*/*'),
      pluginId: plugin.id,
      title: plugin.title,
      url: plugin.readmeUrl
    };
  }

  async loadRequirementReadme(requirement: PluginRequirementSummary): Promise<PluginStoreReadme> {
    const rawReadmeUrl = requirement.manifest?.readme;

    if (!rawReadmeUrl) {
      throw new Error('Plugin does not provide a readme URL');
    }

    const baseUrl = requirement.installUrl ?? requirement.sourceUrl ?? rawReadmeUrl;
    const readmeUrl = resolveOptionalUrl(baseUrl, rawReadmeUrl);

    if (!readmeUrl) {
      throw new Error('Plugin readme URL is not valid');
    }

    return {
      markdown: await this.fetchText(readmeUrl, 'text/markdown,text/plain,*/*'),
      pluginId: requirement.pluginId,
      title: requirement.manifest?.title ?? requirement.pluginId,
      url: readmeUrl
    };
  }

  getInstallState(plugin: PluginStoreEntry): PluginStoreInstallState {
    const installed = this.installedPluginForScope(plugin.id, getStoreEntryInstallScope(plugin));

    if (!installed) {
      return 'notInstalled';
    }

    return compareVersions(plugin.version, installed.manifest.version) > 0
      ? 'updateAvailable'
      : 'installed';
  }

  getActionLabel(plugin: PluginStoreEntry): string {
    const state = this.getInstallState(plugin);
    const serverScoped = getStoreEntryInstallScope(plugin) === 'server';

    if (state === 'updateAvailable') {
      return serverScoped
        ? this.appI18n.instant('plugins.store.actions.updateServer')
        : this.appI18n.instant('plugins.store.actions.update');
    }

    if (state === 'installed') {
      return serverScoped
        ? this.appI18n.instant('plugins.store.actions.removeFromServer')
        : this.appI18n.instant('plugins.store.actions.uninstall');
    }

    return serverScoped
      ? this.appI18n.instant('plugins.store.actions.installToServer')
      : this.appI18n.instant('plugins.store.actions.install');
  }

  canInstallPlugin(plugin: PluginStoreEntry): boolean {
    return getStoreEntryInstallScope(plugin) !== 'server' || this.hasActiveServerInstallScope();
  }

  private async loadSource(sourceUrl: string, signal: AbortSignal): Promise<PluginStoreSourceResult> {
    try {
      const sourceValue = await this.fetchJson(sourceUrl, signal);

      return parsePluginSource(sourceUrl, sourceValue);
    } catch (error) {
      return {
        error: error instanceof Error ? error.message : 'Unable to load plugin source',
        plugins: [],
        url: sourceUrl
      };
    }
  }

  private async fetchPluginManifest(manifestUrl: string): Promise<TojuPluginManifest> {
    const manifestValue = await this.fetchJson(manifestUrl);
    const validation = validateTojuPluginManifest(manifestValue);

    if (!validation.manifest) {
      throw new Error(validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join('\n'));
    }

    return validation.manifest;
  }

  private async fetchJson(url: string, signal?: AbortSignal): Promise<unknown> {
    return JSON.parse(await this.fetchText(url, 'application/json', signal)) as unknown;
  }

  private async fetchText(url: string, accept: string, signal?: AbortSignal): Promise<string> {
    if (url.startsWith('file://')) {
      return await this.readLocalFileUrl(url);
    }

    if (!url.startsWith('https://') && !isLocalDevPluginSourceUrl(url)) {
      throw new Error('Remote plugin store requests must use HTTPS');
    }

    const response = await fetch(url, { headers: { Accept: accept }, signal });

    if (!response.ok) {
      throw new Error(`Request returned ${response.status}`);
    }

    return await response.text();
  }

  private async bootstrapSourceRefresh(sourceUrls: readonly string[]): Promise<void> {
    await this.ensurePluginSourceReadRoots(sourceUrls);

    if (this.stateMutated) {
      return;
    }

    await this.refreshSources();
  }

  private async ensurePluginSourceReadRoots(sourceUrls: readonly string[]): Promise<void> {
    await Promise.all(sourceUrls.map((sourceUrl) => this.ensurePluginSourceReadRoot(sourceUrl)));
  }

  private async ensurePluginSourceReadRoot(sourceUrl: string): Promise<void> {
    if (!sourceUrl.startsWith('file://')) {
      return;
    }

    await grantPluginReadRoots(this.electronBridge.getApi(), sourceUrl);
  }

  private async readLocalFileUrl(fileUrl: string): Promise<string> {
    const api = this.electronBridge.getApi();

    if (!api) {
      throw new Error('Local plugin source paths require the desktop app');
    }

    await this.ensurePluginSourceReadRoot(fileUrl);
    const base64Data = await api.readFile(fileUrlToPath(fileUrl));

    if (!base64Data) {
      throw new Error(`Local plugin source is not readable: ${fileUrlToPath(fileUrl)}`);
    }

    const bytes = Uint8Array.from(atob(base64Data), (character) => character.charCodeAt(0));

    return new TextDecoder().decode(bytes);
  }

  private withStoreBundleMetadata(manifest: TojuPluginManifest, plugin: PluginStoreEntry): TojuPluginManifest {
    if (!plugin.bundleUrl || manifest.bundle?.url) {
      return manifest;
    }

    return {
      ...manifest,
      bundle: {
        entrypoint: './main.js',
        url: plugin.bundleUrl
      }
    };
  }

  private async cacheInstalledPlugin(installedPlugin: InstalledStorePlugin): Promise<InstalledStorePlugin> {
    if (installedPlugin.cachedSourcePath) {
      return installedPlugin;
    }

    const api = this.electronBridge.getApi();
    const entrypointSourceUrl = this.resolvePluginBundleSourceUrl(installedPlugin);
    const cachedEntrypoint = this.resolveCachedEntrypointPath(installedPlugin.manifest);

    if (!api || !entrypointSourceUrl || !cachedEntrypoint) {
      return installedPlugin;
    }

    const cachedManifest = this.toCachedRuntimeManifest(installedPlugin.manifest, cachedEntrypoint);
    const appDataPath = await api.getAppDataPath();
    const pluginCacheDir = joinLocalPath(
      appDataPath,
      PLUGIN_CACHE_DIR,
      sanitizePathSegment(installedPlugin.manifest.id),
      sanitizePathSegment(installedPlugin.manifest.version)
    );
    const manifestPath = joinLocalPath(pluginCacheDir, 'toju-plugin.json');
    const entrypointPath = joinLocalPath(pluginCacheDir, cachedEntrypoint);
    const cacheRootUrl = localPathToFileUrl(manifestPath);

    if (!cacheRootUrl) {
      return installedPlugin;
    }

    await api.ensureDir(dirnameLocalPath(entrypointPath));
    await api.writeFile(entrypointPath, bytesToBase64(new TextEncoder().encode(await this.fetchText(entrypointSourceUrl, 'text/javascript,*/*'))));
    await api.writeFile(manifestPath, bytesToBase64(new TextEncoder().encode(JSON.stringify(cachedManifest, null, 2))));

    return {
      ...installedPlugin,
      bundleUrl: installedPlugin.bundleUrl ?? installedPlugin.manifest.bundle?.url,
      cachedAt: Date.now(),
      cachedSourcePath: cacheRootUrl,
      manifest: cachedManifest
    };
  }

  private toCachedRuntimeManifest(manifest: TojuPluginManifest, cachedEntrypoint: string): TojuPluginManifest {
    if (!manifest.bundle?.url) {
      return manifest;
    }

    return {
      ...manifest,
      entrypoint: cachedEntrypoint.startsWith('./') ? cachedEntrypoint : `./${cachedEntrypoint}`
    };
  }

  private resolvePluginBundleSourceUrl(installedPlugin: InstalledStorePlugin): string | null {
    const bundleUrl = installedPlugin.bundleUrl ?? installedPlugin.manifest.bundle?.url;

    if (bundleUrl) {
      return bundleUrl;
    }

    const entrypoint = installedPlugin.manifest.entrypoint;

    if (!entrypoint || !installedPlugin.installUrl || isAbsolutePluginUrl(entrypoint)) {
      return null;
    }

    return resolveOptionalUrl(installedPlugin.installUrl, entrypoint) ?? null;
  }

  private resolveCachedEntrypointPath(manifest: TojuPluginManifest): string | null {
    const entrypoint = manifest.bundle?.url
      ? manifest.bundle.entrypoint ?? './main.js'
      : manifest.entrypoint;

    if (!entrypoint || isAbsolutePluginUrl(entrypoint)) {
      return null;
    }

    const normalized = entrypoint.replace(/^\.\//, '').replace(/\\/g, '/');

    if (!normalized || normalized.startsWith('/') || normalized.split('/').includes('..')) {
      return null;
    }

    return normalized;
  }

  private async applyInstalledPlugins(installedPlugins: InstalledStorePlugin[], scope: TojuPluginInstallScope): Promise<void> {
    const usableInstalledPlugins: InstalledStorePlugin[] = [];
    const scopedInstalledPlugins = installedPlugins.filter((installedPlugin) => getPluginInstallScope(installedPlugin.manifest) === scope);
    const nextIds = new Set(scopedInstalledPlugins.map((installedPlugin) => installedPlugin.manifest.id));

    for (const previousPlugin of this.installedPluginsForScope(scope)) {
      if (!nextIds.has(previousPlugin.manifest.id)) {
        await this.host.deactivatePlugin(previousPlugin.manifest.id);
        this.registry.unregister(previousPlugin.manifest.id);
      }
    }

    for (const installedPlugin of scopedInstalledPlugins) {
      try {
        const cachedPlugin = await this.cacheInstalledPlugin(installedPlugin);

        this.host.registerLocalManifest(cachedPlugin.manifest, cachedPlugin.cachedSourcePath ?? cachedPlugin.installUrl);
        usableInstalledPlugins.push(cachedPlugin);
      } catch {
        // Corrupt persisted manifests are ignored so the store can recover on next install.
      }
    }

    this.setInstalledPluginsForScope(scope, usableInstalledPlugins);

    if (scope === 'server') {
      await this.activateServerPlugins(usableInstalledPlugins);
    } else {
      await this.host.activatePersistedPlugins();
    }

    if (usableInstalledPlugins.length !== scopedInstalledPlugins.length) {
      if (scope === 'client') {
        await this.persistInstalledPlugins(usableInstalledPlugins, scope);
      }
    } else if (
      scope === 'client'
      && usableInstalledPlugins.some((plugin, index) => plugin.cachedSourcePath !== scopedInstalledPlugins[index]?.cachedSourcePath)
    ) {
      await this.persistInstalledPlugins(usableInstalledPlugins, scope);
    }
  }

  private async activateServerPlugins(installedPlugins: InstalledStorePlugin[]): Promise<void> {
    for (const installedPlugin of installedPlugins) {
      await this.host.activatePluginById(installedPlugin.manifest.id);
    }
  }

  private async autoUpdateInstalledPlugins(): Promise<void> {
    if (this.autoUpdateInProgress || this.sources().length === 0) {
      return;
    }

    this.autoUpdateInProgress = true;

    try {
      await this.autoUpdateScope('client');

      if (this.currentRoomId?.()) {
        await this.autoUpdateScope('server');
      }
    } finally {
      this.autoUpdateInProgress = false;
    }
  }

  private async autoUpdateScope(scope: TojuPluginInstallScope): Promise<void> {
    for (const installedPlugin of this.installedPluginsForScope(scope)) {
      const update = this.findUpdateCandidate(installedPlugin, scope);

      if (!update) {
        continue;
      }

      try {
        await this.installPlugin(update, {
          activate: this.host.isPluginActive(installedPlugin.manifest.id),
          serverId: scope === 'server' ? this.currentRoomId?.() ?? undefined : undefined
        });
      } catch {}
    }
  }

  private findUpdateCandidate(installedPlugin: InstalledStorePlugin, scope: TojuPluginInstallScope): PluginStoreEntry | null {
    const candidates = this.availablePlugins().filter((plugin) => {
      return plugin.id === installedPlugin.manifest.id
        && getStoreEntryInstallScope(plugin) === scope
        && (!installedPlugin.sourceUrl || plugin.sourceUrl === installedPlugin.sourceUrl);
    });

    return candidates
      .filter((plugin) => compareVersions(plugin.version, installedPlugin.manifest.version) > 0)
      .sort((left, right) => compareVersions(right.version, left.version))[0] ?? null;
  }

  private async loadInstalledPluginsForScope(roomId: string | null, actorUserId: string | null): Promise<void> {
    const currentLoad = this.installedLoadVersion + 1;

    this.installedLoadVersion = currentLoad;
    this.serverInstalledPluginsLoadStateSignal.set({
      actorUserId,
      loaded: false,
      loading: true,
      roomId
    });

    await Promise.resolve();

    if (!roomId || !actorUserId || !this.serverDirectory) {
      if (this.installedLoadVersion === currentLoad) {
        await this.applyInstalledPlugins([], 'server');
        this.serverInstalledPluginsLoadStateSignal.set({
          actorUserId,
          loaded: true,
          loading: false,
          roomId
        });
      }

      return;
    }

    try {
      const installedPlugins = await this.readLocalServerInstalledPlugins(roomId);

      if (this.installedLoadVersion === currentLoad && this.currentRoomId?.() === roomId) {
        await this.applyInstalledPlugins(installedPlugins, 'server');
        this.serverInstalledPluginsLoadStateSignal.set({
          actorUserId,
          loaded: true,
          loading: false,
          roomId
        });
      }
    } catch {
      if (this.installedLoadVersion === currentLoad && this.currentRoomId?.() === roomId) {
        await this.applyInstalledPlugins([], 'server');
        this.serverInstalledPluginsLoadStateSignal.set({
          actorUserId,
          loaded: true,
          loading: false,
          roomId
        });
      }
    }
  }

  private async persistInstalledPlugins(
    installedPlugins: InstalledStorePlugin[],
    scope: TojuPluginInstallScope,
    serverId?: string | null
  ): Promise<void> {
    const roomId = serverId ?? this.currentRoomId?.() ?? null;
    const actorUserId = this.currentActorUserId();

    if (scope === 'server') {
      if (!roomId || !actorUserId || !this.serverDirectory) {
        throw new Error('Open a chat server before saving server-scoped plugins');
      }

      await Promise.all(installedPlugins.map((installedPlugin) => this.saveServerPluginRequirement(installedPlugin, roomId, 'required')));
      return;
    }

    this.clientInstalledPluginsSignal.set(installedPlugins);
    this.saveState();
  }

  private async readServerInstalledPlugins(roomId: string): Promise<InstalledStorePlugin[]> {
    if (!this.serverDirectory) {
      return [];
    }

    const snapshot = await firstValueFrom(this.pluginRequirements.getSnapshot(this.getPluginApiBaseUrl(roomId), roomId));

    return snapshot.requirements
      .map((requirement) => installedPluginFromRequirement(requirement))
      .filter((installedPlugin): installedPlugin is InstalledStorePlugin => !!installedPlugin)
      .sort(sortInstalledPlugins);
  }

  private async resolveLocalInstallFromRequirement(requirement: PluginRequirementSummary): Promise<InstalledStorePlugin | null> {
    const existingPlugin = installedPluginFromRequirement(requirement, { includeOptional: true });

    if (existingPlugin) {
      return existingPlugin;
    }

    if (!requirement.installUrl) {
      return null;
    }

    const manifest = await this.fetchPluginManifest(requirement.installUrl);

    if (getPluginInstallScope(manifest) !== 'server') {
      return null;
    }

    return {
      bundleUrl: manifest.bundle?.url,
      installedAt: requirement.updatedAt,
      installUrl: requirement.installUrl,
      manifest,
      sourceUrl: requirement.sourceUrl,
      updatedAt: requirement.updatedAt
    };
  }

  private async readLocalServerInstalledPlugins(serverId: string): Promise<InstalledStorePlugin[]> {
    const state = await this.desktopState.readJson<PersistedServerPluginInstallState>(STORAGE_KEY_SERVER_PLUGIN_INSTALLS, {});
    const normalized = normalizePersistedServerPluginInstallState(state);

    return normalized.servers[serverId] ?? [];
  }

  private async writeLocalServerInstalledPlugins(serverId: string, installedPlugins: InstalledStorePlugin[]): Promise<void> {
    const state = await this.desktopState.readJson<PersistedServerPluginInstallState>(STORAGE_KEY_SERVER_PLUGIN_INSTALLS, {});
    const normalized = normalizePersistedServerPluginInstallState(state);
    const nextServers = installedPlugins.length === 0
      ? Object.fromEntries(Object.entries(normalized.servers).filter(([candidateServerId]) => candidateServerId !== serverId))
      : {
        ...normalized.servers,
        [serverId]: installedPlugins
      };

    await this.desktopState.writeJson(STORAGE_KEY_SERVER_PLUGIN_INSTALLS, {
      schemaVersion: STORE_SCHEMA_VERSION,
      servers: nextServers
    });
  }

  private async saveServerPluginRequirement(
    installedPlugin: InstalledStorePlugin,
    roomId: string | null,
    status: 'optional' | 'required'
  ): Promise<void> {
    const actorUserId = this.currentActorUserId();

    if (!roomId || !actorUserId || !this.serverDirectory) {
      throw new Error('Open a chat server before saving server-scoped plugins');
    }

    await firstValueFrom(this.pluginRequirements.upsertRequirement(
      this.getPluginApiBaseUrl(roomId),
      roomId,
      installedPlugin.manifest.id,
      {
        installUrl: installedPlugin.installUrl,
        manifest: installedPlugin.manifest,
        reason: installedPlugin.manifest.description,
        sourceUrl: installedPlugin.sourceUrl,
        status,
        versionRange: `^${installedPlugin.manifest.version}`
      }
    ));
  }

  private async deleteServerPluginRequirement(pluginId: string, serverId?: string): Promise<void> {
    const roomId = serverId ?? this.currentRoomId?.() ?? null;
    const actorUserId = this.currentActorUserId();

    if (!roomId || !actorUserId || !this.serverDirectory) {
      throw new Error('Open a chat server before removing server-scoped plugins');
    }

    await firstValueFrom(this.pluginRequirements.deleteRequirement(this.getPluginApiBaseUrl(roomId), roomId, pluginId));
  }

  private getPluginApiBaseUrl(serverId: string): string {
    const selector = this.serverSourceSelector(serverId);

    return this.serverDirectory?.getApiBaseUrl(selector) ?? '';
  }

  private serverSourceSelector(serverId: string): ServerSourceSelector | undefined {
    if (serverId === this.currentRoomId?.()) {
      return this.currentRoomSourceSelector();
    }

    const room = this.savedRooms?.().find((candidate) => candidate.id === serverId) ?? null;

    if (!room?.sourceId && !room?.sourceUrl) {
      return undefined;
    }

    return {
      sourceId: room.sourceId,
      sourceUrl: room.sourceUrl
    };
  }

  private currentRoomSourceSelector(): ServerSourceSelector | undefined {
    const room = this.currentRoom?.() ?? null;

    if (!room?.sourceId && !room?.sourceUrl) {
      return undefined;
    }

    return {
      sourceId: room.sourceId,
      sourceUrl: room.sourceUrl
    };
  }

  private currentActorUserId(): string | null {
    const user = this.currentUser?.() ?? null;

    return user?.oderId || user?.id || null;
  }

  private async installedPluginsForServer(serverId: string | null): Promise<InstalledStorePlugin[]> {
    if (!serverId) {
      return [];
    }

    const actorUserId = this.currentActorUserId();

    if (!actorUserId || !this.serverDirectory) {
      throw new Error('Unable to read server plugins without an active user and server directory');
    }

    try {
      return await this.readServerInstalledPlugins(serverId);
    } catch {
      return [];
    }
  }

  private loadState(): PersistedPluginStoreState {
    const parsed = this.jsonStorage.read<unknown>(
      getUserScopedStorageKey(STORAGE_KEY_PLUGIN_STORE),
      null
    );

    if (!parsed) {
      return createDefaultStoreState();
    }

    return normalizePersistedState(parsed);
  }

  private saveState(): void {
    this.stateMutated = true;

    const state = {
      installedPlugins: this.clientInstalledPluginsSignal(),
      schemaVersion: STORE_SCHEMA_VERSION,
      sourceUrls: this.sourceUrls()
    };

    this.jsonStorage.write(getUserScopedStorageKey(STORAGE_KEY_PLUGIN_STORE), state);
    void this.desktopState.writeJson(STORAGE_KEY_PLUGIN_STORE, state);
  }

  private async loadDesktopState(): Promise<void> {
    const state = await this.desktopState.readJson<PersistedPluginStoreState>(STORAGE_KEY_PLUGIN_STORE, this.loadState());

    if (this.stateMutated) {
      return;
    }

    const normalized = normalizePersistedState(state);
    const sourceUrlsChanged = !areStringArraysEqual(normalized.sourceUrls, this.sourceUrls());

    if (sourceUrlsChanged) {
      this.sourceUrlsSignal.set(normalized.sourceUrls);
      void this.bootstrapSourceRefresh(normalized.sourceUrls);
    }

    await this.applyInstalledPlugins(normalized.installedPlugins, 'client');
  }

  private installedPluginsForScope(scope: TojuPluginInstallScope): InstalledStorePlugin[] {
    return scope === 'server' ? this.serverInstalledPluginsSignal() : this.clientInstalledPluginsSignal();
  }

  private setInstalledPluginsForScope(scope: TojuPluginInstallScope, installedPlugins: InstalledStorePlugin[]): void {
    if (scope === 'server') {
      this.serverInstalledPluginsSignal.set(installedPlugins);
      return;
    }

    this.clientInstalledPluginsSignal.set(installedPlugins);
  }

  private installedPluginForScope(pluginId: string, scope: TojuPluginInstallScope): InstalledStorePlugin | undefined {
    return this.installedPluginsForScope(scope).find((installedPlugin) => installedPlugin.manifest.id === pluginId);
  }

  private findInstalledPluginScope(pluginId: string): TojuPluginInstallScope | null {
    if (this.serverInstalledPluginsSignal().some((installedPlugin) => installedPlugin.manifest.id === pluginId)) {
      return 'server';
    }

    if (this.clientInstalledPluginsSignal().some((installedPlugin) => installedPlugin.manifest.id === pluginId)) {
      return 'client';
    }

    return null;
  }
}

function isPluginRequirementsChangedMessage(message: unknown): message is { serverId: string; type: string } {
  if (!isRecord(message)) {
    return false;
  }

  return (message['type'] === 'plugin_requirements' || message['type'] === 'plugin_requirements_changed')
    && typeof message['serverId'] === 'string';
}

function installedPluginFromRequirement(
  requirement: PluginRequirementSummary,
  options: { includeOptional?: boolean } = {}
): InstalledStorePlugin | null {
  if (requirement.status === 'blocked' || requirement.status === 'incompatible') {
    return null;
  }

  if (requirement.status === 'optional' && options.includeOptional !== true) {
    return null;
  }

  const manifest = requirement.manifest;

  if (!manifest || !isInstalledStorePlugin({ manifest })) {
    return null;
  }

  return {
    bundleUrl: manifest.bundle?.url,
    installedAt: requirement.updatedAt,
    installUrl: requirement.installUrl,
    manifest,
    sourceUrl: requirement.sourceUrl,
    updatedAt: requirement.updatedAt
  };
}

function parsePluginSource(sourceUrl: string, sourceValue: unknown): PluginStoreSourceResult {
  const sourceRecord = isRecord(sourceValue) ? sourceValue : {};
  const sourceTitle = readString(sourceRecord, 'title', 'name') ?? new URL(sourceUrl).hostname;
  const rawPlugins = Array.isArray(sourceValue)
    ? sourceValue
    : Array.isArray(sourceRecord['plugins'])
      ? sourceRecord['plugins']
      : Array.isArray(sourceRecord['items'])
        ? sourceRecord['items']
        : [];
  const plugins = rawPlugins
    .map((entry) => parsePluginEntry(sourceUrl, sourceTitle, entry))
    .filter((entry): entry is PluginStoreEntry => !!entry)
    .sort((left, right) => left.title.localeCompare(right.title));

  return {
    loadedAt: Date.now(),
    plugins,
    title: sourceTitle,
    url: sourceUrl
  };
}

function sortInstalledPlugins(left: InstalledStorePlugin, right: InstalledStorePlugin): number {
  return left.manifest.title.localeCompare(right.manifest.title);
}

function parsePluginEntry(sourceUrl: string, sourceTitle: string, value: unknown): PluginStoreEntry | null {
  if (!isRecord(value)) {
    return null;
  }

  const id = readString(value, 'id', 'pluginId');
  const version = readString(value, 'version') ?? '0.0.0';

  if (!id) {
    return null;
  }

  return {
    author: readAuthor(value),
    bundleUrl: resolveOptionalUrl(sourceUrl, readString(value, 'bundle', 'bundleUrl')),
    description: readString(value, 'description', 'summary') ?? '',
    githubUrl: resolveOptionalUrl(sourceUrl, readGithubUrl(value)),
    homepageUrl: resolveOptionalUrl(sourceUrl, readString(value, 'homepage', 'homepageUrl', 'website')),
    id,
    imageUrl: resolveOptionalUrl(sourceUrl, readString(value, 'image', 'imageUrl', 'icon', 'iconUrl', 'banner')),
    installUrl: resolveOptionalUrl(sourceUrl, readString(value, 'install', 'installUrl', 'manifest', 'manifestUrl')),
    readmeUrl: resolveOptionalUrl(sourceUrl, readString(value, 'readme', 'readmeUrl')),
    scope: readPluginInstallScope(value),
    sourceTitle,
    sourceUrl,
    title: readString(value, 'title', 'name') ?? id,
    version
  };
}

function getStoreEntryInstallScope(plugin: PluginStoreEntry): TojuPluginInstallScope {
  return plugin.scope === 'server' ? 'server' : 'client';
}

function readPluginInstallScope(record: Record<string, unknown>): TojuPluginInstallScope | undefined {
  const scope = readString(record, 'scope', 'installScope', 'pluginScope');

  return scope === 'server' || scope === 'client' ? scope : undefined;
}

function areStringArraysEqual(first: readonly string[], second: readonly string[]): boolean {
  if (first === second) {
    return true;
  }

  if (first.length !== second.length) {
    return false;
  }

  for (let arrayIndex = 0; arrayIndex < first.length; arrayIndex++) {
    if (first[arrayIndex] !== second[arrayIndex]) {
      return false;
    }
  }

  return true;
}

function normalizePersistedState(value: unknown): PersistedPluginStoreState {
  if (!isRecord(value)) {
    return createDefaultStoreState();
  }

  const schemaVersion = typeof value['schemaVersion'] === 'number' ? value['schemaVersion'] : 0;
  const sourceUrls = Array.isArray(value['sourceUrls'])
    ? normalizePluginSourceUrls(value['sourceUrls'])
    : [];

  return {
    installedPlugins: Array.isArray(value['installedPlugins'])
      ? value['installedPlugins'].filter(isInstalledStorePlugin)
      : [],
    schemaVersion: STORE_SCHEMA_VERSION,
    sourceUrls: schemaVersion < STORE_SCHEMA_VERSION
      ? mergePluginSourceUrls(DEFAULT_PLUGIN_SOURCE_URLS, sourceUrls)
      : sourceUrls
  };
}

function createDefaultStoreState(): PersistedPluginStoreState {
  return {
    installedPlugins: [],
    schemaVersion: STORE_SCHEMA_VERSION,
    sourceUrls: [...DEFAULT_PLUGIN_SOURCE_URLS]
  };
}

function normalizePluginSourceUrls(sourceUrls: unknown[]): string[] {
  const normalizedSourceUrls: string[] = [];

  for (const entry of sourceUrls) {
    if (typeof entry !== 'string') {
      continue;
    }

    const sourceUrl = normalizeOptionalSourceUrl(entry);

    if (sourceUrl && !normalizedSourceUrls.includes(sourceUrl)) {
      normalizedSourceUrls.push(sourceUrl);
    }
  }

  return normalizedSourceUrls;
}

function mergePluginSourceUrls(defaultSourceUrls: string[], sourceUrls: string[]): string[] {
  const mergedSourceUrls: string[] = [];

  for (const sourceUrl of defaultSourceUrls.concat(sourceUrls)) {
    if (!mergedSourceUrls.includes(sourceUrl)) {
      mergedSourceUrls.push(sourceUrl);
    }
  }

  return mergedSourceUrls;
}

function normalizePersistedServerPluginInstallState(value: unknown): { servers: Record<string, InstalledStorePlugin[]> } {
  if (!isRecord(value) || !isRecord(value['servers'])) {
    return { servers: {} };
  }

  const servers: Record<string, InstalledStorePlugin[]> = {};

  for (const [serverId, installedPlugins] of Object.entries(value['servers'])) {
    if (!Array.isArray(installedPlugins)) {
      continue;
    }

    const normalizedPlugins = installedPlugins
      .filter(isInstalledStorePlugin)
      .filter((installedPlugin) => getPluginInstallScope(installedPlugin.manifest) === 'server')
      .sort(sortInstalledPlugins);

    if (normalizedPlugins.length > 0) {
      servers[serverId] = normalizedPlugins;
    }
  }

  return { servers };
}

function isInstalledStorePlugin(value: unknown): value is InstalledStorePlugin {
  if (!isRecord(value) || !isRecord(value['manifest'])) {
    return false;
  }

  const validation = validateTojuPluginManifest(value['manifest']);

  return !!validation.manifest;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return !!value && typeof value === 'object' && !Array.isArray(value);
}

function readString(record: Record<string, unknown>, ...keys: string[]): string | undefined {
  for (const key of keys) {
    const value = record[key];

    if (typeof value === 'string' && value.trim()) {
      return value.trim();
    }
  }

  return undefined;
}

function readAuthor(record: Record<string, unknown>): string | undefined {
  const author = readString(record, 'author');

  if (author) {
    return author;
  }

  const authors = record['authors'];

  if (!Array.isArray(authors)) {
    return undefined;
  }

  return authors
    .map((entry) => isRecord(entry) ? readString(entry, 'name') : typeof entry === 'string' ? entry.trim() : '')
    .filter(Boolean)
    .join(', ') || undefined;
}

function readGithubUrl(record: Record<string, unknown>): string | undefined {
  const directUrl = readString(record, 'github', 'githubUrl');

  if (directUrl) {
    return directUrl;
  }

  const repository = record['repository'];

  return isRecord(repository) ? readString(repository, 'url') : typeof repository === 'string' ? repository.trim() : undefined;
}

function normalizeSourceUrl(rawUrl: string, label: string): string {
  const url = normalizeOptionalSourceUrl(rawUrl);

  if (!url) {
    throw new Error(`${label} must be an http, https, file URL, or absolute local path`);
  }

  return url;
}

function normalizeOptionalSourceUrl(rawUrl: string): string | undefined {
  const trimmedUrl = rawUrl.trim();

  if (!trimmedUrl) {
    return undefined;
  }

  try {
    const url = new URL(trimmedUrl);

    if (!isAllowedPluginSourceProtocol(url.protocol)) {
      return undefined;
    }

    url.hash = '';
    return url.toString();
  } catch {
    return localPathToFileUrl(trimmedUrl);
  }
}

function resolveOptionalUrl(sourceUrl: string, rawUrl?: string): string | undefined {
  if (!rawUrl) {
    return undefined;
  }

  try {
    const url = new URL(rawUrl, sourceUrl);

    if (!isAllowedPluginSourceProtocol(url.protocol)) {
      return undefined;
    }

    url.hash = '';
    return url.toString();
  } catch {
    return undefined;
  }
}

function isAllowedPluginSourceProtocol(protocol: string): boolean {
  return protocol === 'http:' || protocol === 'https:' || protocol === 'file:';
}

function isAbsolutePluginUrl(value: string): boolean {
  try {
    const url = new URL(value);

    return isAllowedPluginSourceProtocol(url.protocol);
  } catch {
    return false;
  }
}

function sanitizePathSegment(value: string): string {
  return value.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 128) || 'plugin';
}

function joinLocalPath(...parts: string[]): string {
  return parts
    .map((part, index) => index === 0 ? part.replace(/[\\/]+$/, '') : part.replace(/^[\\/]+|[\\/]+$/g, ''))
    .filter(Boolean)
    .join('/');
}

function dirnameLocalPath(filePath: string): string {
  const normalized = filePath.replace(/\\/g, '/');
  const index = normalized.lastIndexOf('/');

  return index > 0 ? normalized.slice(0, index) : normalized;
}

function bytesToBase64(bytes: Uint8Array): string {
  let binary = '';

  for (const byte of bytes) {
    binary += String.fromCharCode(byte);
  }

  return btoa(binary);
}

function localPathToFileUrl(filePath: string): string | undefined {
  if (!isAbsoluteLocalPath(filePath)) {
    return undefined;
  }

  const normalizedPath = filePath.replace(/\\/g, '/');
  const pathWithLeadingSlash = /^[A-Za-z]:\//.test(normalizedPath)
    ? `/${normalizedPath}`
    : normalizedPath;

  return `file://${pathWithLeadingSlash.split('/')
    .map(encodeURIComponent)
    .join('/')}`;
}

function isAbsoluteLocalPath(filePath: string): boolean {
  return filePath.startsWith('/') || /^[A-Za-z]:[\\/]/.test(filePath);
}

function compareVersions(leftVersion: string, rightVersion: string): number {
  const leftParts = parseVersion(leftVersion);
  const rightParts = parseVersion(rightVersion);

  for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
    const leftPart = leftParts[index] ?? 0;
    const rightPart = rightParts[index] ?? 0;

    if (leftPart !== rightPart) {
      return leftPart - rightPart;
    }
  }

  return leftVersion.localeCompare(rightVersion);
}

function parseVersion(version: string): number[] {
  return version
    .split(/[.+-]/)
    .slice(0, 3)
    .map((part) => Number.parseInt(part, 10))
    .map((part) => Number.isFinite(part) ? part : 0);
}
