/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import * as fs from 'node:fs/promises';
import {
  fetchWithTimeout,
  resolveToRealPath,
  isPrivateIp,
} from '@google/gemini-cli-core';
import { AsyncFzf } from 'fzf';

export interface RegistryExtension {
  id: string;
  rank: number;
  url: string;
  fullName: string;
  repoDescription: string;
  stars: number;
  lastUpdated: string;
  extensionName: string;
  extensionVersion: string;
  extensionDescription: string;
  avatarUrl: string;
  hasMCP: boolean;
  hasContext: boolean;
  hasHooks: boolean;
  hasSkills: boolean;
  hasCustomCommands: boolean;
  isGoogleOwned: boolean;
  licenseKey: string;
}

export class ExtensionRegistryClient {
  static readonly DEFAULT_REGISTRY_URL =
    'https://geminicli.com/extensions.json';
  private static readonly FETCH_TIMEOUT_MS = 10000; // 10 seconds

  private static fetchPromise: Promise<RegistryExtension[]> | null = null;

  private readonly registryURI: string;

  constructor(registryURI?: string) {
    this.registryURI =
      registryURI || ExtensionRegistryClient.DEFAULT_REGISTRY_URL;
  }

  /** @internal */
  static resetCache() {
    ExtensionRegistryClient.fetchPromise = null;
  }

  async getExtensions(
    page: number = 1,
    limit: number = 10,
    orderBy: 'ranking' | 'alphabetical' = 'ranking',
  ): Promise<{ extensions: RegistryExtension[]; total: number }> {
    const allExtensions = [...(await this.fetchAllExtensions())];

    switch (orderBy) {
      case 'ranking':
        allExtensions.sort((a, b) => a.rank - b.rank);
        break;
      case 'alphabetical':
        allExtensions.sort((a, b) =>
          a.extensionName.localeCompare(b.extensionName),
        );
        break;
      default: {
        const _exhaustiveCheck: never = orderBy;
        throw new Error(`Unhandled orderBy: ${_exhaustiveCheck}`);
      }
    }

    const startIndex = (page - 1) * limit;
    const endIndex = startIndex + limit;
    return {
      extensions: allExtensions.slice(startIndex, endIndex),
      total: allExtensions.length,
    };
  }

  async searchExtensions(query: string): Promise<RegistryExtension[]> {
    const allExtensions = await this.fetchAllExtensions();
    if (!query.trim()) {
      return allExtensions;
    }

    const fzf = new AsyncFzf(allExtensions, {
      selector: (ext: RegistryExtension) =>
        `${ext.extensionName} ${ext.extensionDescription} ${ext.fullName}`,
      fuzzy: true,
    });
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const results: Array<{ item: RegistryExtension }> = await fzf.find(query);
    return results.map((r) => r.item);
  }

  async getExtension(id: string): Promise<RegistryExtension | undefined> {
    const allExtensions = await this.fetchAllExtensions();
    return allExtensions.find((ext) => ext.id === id);
  }

  private async fetchAllExtensions(): Promise<RegistryExtension[]> {
    if (ExtensionRegistryClient.fetchPromise) {
      return ExtensionRegistryClient.fetchPromise;
    }

    const uri = this.registryURI;
    ExtensionRegistryClient.fetchPromise = (async () => {
      try {
        if (uri.startsWith('http')) {
          if (isPrivateIp(uri)) {
            throw new Error(
              'Private IP addresses are not allowed for the extension registry.',
            );
          }
          const response = await fetchWithTimeout(
            uri,
            ExtensionRegistryClient.FETCH_TIMEOUT_MS,
          );
          if (!response.ok) {
            throw new Error(
              `Failed to fetch extensions: ${response.statusText}`,
            );
          }

          // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
          return (await response.json()) as RegistryExtension[];
        } else {
          // Handle local file path
          const filePath = resolveToRealPath(uri);
          const content = await fs.readFile(filePath, 'utf-8');
          // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
          return JSON.parse(content) as RegistryExtension[];
        }
      } catch (error) {
        ExtensionRegistryClient.fetchPromise = null;
        throw error;
      }
    })();

    return ExtensionRegistryClient.fetchPromise;
  }
}
