import { URL } from 'url';

/**
 * Fetches and parses Storybook manifests.
 * Includes SSRF protection and timeout handling.
 */
export class StorybookScanner {
  /**
   * Scans a published Storybook URL for components.
   * @param {string} baseUrl - The base URL of the deployed Storybook
   * @returns {Promise<Array<{name: string, storybookPath: string, variants: string[]}>>}
   */
  async scan(baseUrl) {
    if (!baseUrl) return [];

    try {
      // Security: SSRF Validation
      this._validateUrl(baseUrl);

      const storiesUrl = new URL('stories.json', baseUrl).toString();
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout

      try {
        const response = await fetch(storiesUrl, { signal: controller.signal });
        clearTimeout(timeoutId);

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const manifest = await response.json();
        return this._processManifest(manifest);

      } catch (fetchError) {
        clearTimeout(timeoutId);
        if (fetchError.name === 'AbortError') {
          throw new Error('Request timed out after 10 seconds');
        }
        throw fetchError;
      }

    } catch (error) {
      console.error('[StorybookScanner] Scan failed:', error.message);
      // Return empty array for graceful degradation
      return [];
    }
  }

  /**
   * Validates URL scheme and prevents calls to internal network ranges.
   * @private
   */
  _validateUrl(urlStr) {
    const url = new URL(urlStr);

    if (!['http:', 'https:'].includes(url.protocol)) {
      throw new Error('Invalid protocol. Only HTTP/HTTPS allowed.');
    }

    const hostname = url.hostname;
    // Basic private IP blocking (Production should use a dedicated library or proxy)
    const isPrivate = /^(localhost|127\.|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|0\.)/.test(hostname);

    if (isPrivate) {
      throw new Error('SSRF Protection: Access to internal IP addresses is denied.');
    }
  }

  /**
   * Transforms raw Storybook manifest into component list.
   * @private
   */
  _processManifest(manifest) {
    const stories = manifest.stories || {};
    const componentMap = new Map();

    Object.values(stories).forEach(story => {
      // Group by component title (e.g. "Components/Button")
      // We assume the last part of the kind/title is the component name if hierarchical
      const titleParts = story.title.split('/');
      const componentName = titleParts[titleParts.length - 1];

      // Extract variant from name (e.g. "Primary", "Large")
      const variant = story.name;

      if (!componentMap.has(componentName)) {
        componentMap.set(componentName, {
          name: componentName,
          storybookPath: story.title, // Base path in SB sidebar
          variants: new Set()
        });
      }

      componentMap.get(componentName).variants.add(variant);
    });

    return Array.from(componentMap.values()).map(comp => ({
      ...comp,
      variants: Array.from(comp.variants)
    }));
  }
}
