export interface UrlImportFetchResult {
  source_url: string;
  html: string;
  json_ld_blocks: string[];
}

export type UrlImportErrorCode =
  | 'IMPORT_TIMEOUT'
  | 'IMPORT_NETWORK'
  | 'IMPORT_FETCH_FAILED'
  | 'IMPORT_UNSUPPORTED_CONTENT';

export class UrlImportError extends Error {
  constructor(
    public readonly code: UrlImportErrorCode,
    message: string,
    public readonly status?: number,
  ) {
    super(message);
    this.name = 'UrlImportError';
  }
}

/**
 * Foundation service for importing recipe content from public URLs.
 */
export class UrlImportService {
  private static readonly DEFAULT_TIMEOUT_MS = 10000;
  private static readonly MAX_RETRIES = 2;

  async fetchFromUrl(url: string): Promise<UrlImportFetchResult> {
    const html = await this.fetchHtmlWithRetry(url);
    const jsonLdBlocks = this.extractJsonLdBlocks(html);

    return {
      source_url: url,
      html,
      json_ld_blocks: jsonLdBlocks,
    };
  }

  private async fetchHtmlWithRetry(url: string): Promise<string> {
    let lastError: UrlImportError | null = null;

    for (let attempt = 0; attempt <= UrlImportService.MAX_RETRIES; attempt += 1) {
      try {
        return await this.fetchHtmlOnce(url);
      } catch (error) {
        if (error instanceof UrlImportError) {
          lastError = error;

          // Retry only on transient failures
          if (
            (error.code === 'IMPORT_TIMEOUT' || error.code === 'IMPORT_NETWORK' || (error.status !== undefined && error.status >= 500)) &&
            attempt < UrlImportService.MAX_RETRIES
          ) {
            continue;
          }

          throw error;
        }

        throw error;
      }
    }

    throw lastError ?? new UrlImportError('IMPORT_FETCH_FAILED', 'Unable to import this URL right now. Please try again.');
  }

  private async fetchHtmlOnce(url: string): Promise<string> {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), UrlImportService.DEFAULT_TIMEOUT_MS);

    try {
      const response = await fetch(url, {
        method: 'GET',
        signal: controller.signal,
        headers: {
          'User-Agent': 'RecipeManagerBot/1.0 (+https://recipes.paje.ca)',
          Accept: 'text/html,application/xhtml+xml',
        },
      });

      if (!response.ok) {
        throw new UrlImportError(
          'IMPORT_FETCH_FAILED',
          `Could not fetch the page (HTTP ${response.status}).`,
          response.status,
        );
      }

      const contentType = response.headers.get('content-type') ?? '';
      if (!contentType.includes('text/html')) {
        throw new UrlImportError(
          'IMPORT_UNSUPPORTED_CONTENT',
          'The URL did not return an HTML page. Please use a direct recipe page URL.',
        );
      }

      return await response.text();
    } catch (error) {
      if (error instanceof UrlImportError) {
        throw error;
      }

      if (error instanceof Error && error.name === 'AbortError') {
        throw new UrlImportError(
          'IMPORT_TIMEOUT',
          'Import timed out while contacting the recipe page. Please try again.',
        );
      }

      if (error instanceof Error) {
        throw new UrlImportError(
          'IMPORT_NETWORK',
          'Network error while fetching recipe URL. Please try again.',
        );
      }

      throw new UrlImportError('IMPORT_FETCH_FAILED', 'Unknown error while fetching URL');
    } finally {
      clearTimeout(timeout);
    }
  }

  private extractJsonLdBlocks(html: string): string[] {
    const scriptTagPattern = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
    const blocks: string[] = [];

    let match = scriptTagPattern.exec(html);
    while (match) {
      const content = match[1]?.trim();
      if (content) {
        blocks.push(content);
      }
      match = scriptTagPattern.exec(html);
    }

    return blocks;
  }
}
