import 'server-only';

// Note: cheerio import is done dynamically to avoid client-side bundling issues
// Using any type for CheerioAPI to prevent bundling issues
import { 
  SitesConfig, 
  SiteConfig, 
  ScrapedStory, 
  ScrapedAuthorStory, 
  SelectorStrategy,
  MultiPageConfig,
  ScraperError 
} from './types';
import { RateLimiter } from './utils/rateLimit';
import { ScraperCache } from './utils/cache';
import { UrlParser } from './utils/urlParser';
import {
  extractByTextPattern,
  extractTextBlocks,
  extractDeviantArtContent,
  extractHtmlBetween,
  extractLinkText,
  extractLinkWithPath,
  extractHrefPattern,
  extractFirstImage,
  extractResponsiveImage,
  extractLazyLoadedImage,
  extractChapters,
  extractChapterContent,
  extractMultipleTypes,
  extractSchemaOrg,
  extractReactContent,
  cleanHtml,
  extractAttribute
} from './strategies';
import sitesConfig from './config/sites.json';

export class StoryScraper {
  private config: SitesConfig;
  private cache: ScraperCache;
  private rateLimiter: RateLimiter;
  
  constructor() {
    this.config = sitesConfig as SitesConfig;
    this.cache = new ScraperCache(this.config.globalOptions.cacheDuration);
    this.rateLimiter = new RateLimiter(this.config.globalOptions.rateLimitMs);
  }

  async scrapeStory(url: string): Promise<ScrapedStory> {
    try {
      if (!UrlParser.validateUrl(url)) {
        throw new Error(`Invalid URL: ${url}`);
      }

      const domain = UrlParser.getDomain(url);
      const siteConfig = this.config.sites[domain];
      
      if (!siteConfig) {
        throw new Error(`Unsupported site: ${domain}`);
      }
      
      const html = await this.fetchWithCache(url);
      const cheerio = await import('cheerio');
      const $ = cheerio.load(html);
      
      const story: ScrapedStory = {
        title: await this.extractFieldWithFallback($, siteConfig.story, 'title', html),
        author: await this.extractFieldWithFallback($, siteConfig.story, 'author', html),
        content: await this.extractContent($, siteConfig.story, url, html),
        sourceUrl: url
      };
      
      // Extract optional fields
      if (siteConfig.story.summary) {
        story.summary = await this.extractField($, siteConfig.story.summary, html, siteConfig.story.summaryAttribute);
      }
      
      if (siteConfig.story.coverImage) {
        story.coverImage = await this.extractField($, siteConfig.story.coverImage, html, siteConfig.story.coverImageAttribute);
      }
      
      if (siteConfig.story.tags) {
        const tagsResult = await this.extractTags($, siteConfig.story.tags, html, siteConfig.story.tagsAttribute);
        if (Array.isArray(tagsResult)) {
          // Resolve tag aliases to canonical names
          story.tags = await this.resolveTagAliases(tagsResult);
        } else if (typeof tagsResult === 'string' && tagsResult) {
          // Resolve tag aliases to canonical names
          story.tags = await this.resolveTagAliases([tagsResult]);
        }
      }
      
      // Apply post-processing
      story.title = this.applyTransforms(story.title, siteConfig.story.titleTransform);
      story.content = await cleanHtml(story.content);
      
      return story;
    } catch (error) {
      if (error instanceof Error) {
        throw new ScraperError(
          `Failed to scrape ${url}: ${error.message}`,
          url,
          error
        );
      }
      throw error;
    }
  }

  async scrapeAuthorPage(url: string): Promise<ScrapedAuthorStory[]> {
    try {
      if (!UrlParser.validateUrl(url)) {
        throw new Error(`Invalid URL: ${url}`);
      }

      const domain = UrlParser.getDomain(url);
      const siteConfig = this.config.sites[domain];
      
      if (!siteConfig || !siteConfig.authorPage) {
        throw new Error(`Author page scraping not supported for: ${domain}`);
      }
      
      const html = await this.fetchWithCache(url);
      const cheerio = await import('cheerio');
      const $ = cheerio.load(html);
      
      const storyLinks = await this.extractField($, siteConfig.authorPage.storyLinks, html);
      const stories: ScrapedAuthorStory[] = [];
      
      if (Array.isArray(storyLinks)) {
        for (const link of storyLinks) {
          const storyUrl = UrlParser.normalizeUrl(link, url);
          try {
            const scrapedStory = await this.scrapeStory(storyUrl);
            stories.push({
              url: storyUrl,
              title: scrapedStory.title,
              author: scrapedStory.author,
              summary: scrapedStory.summary
            });
          } catch (error) {
            console.warn(`Failed to scrape story ${storyUrl}:`, error);
          }
        }
      }
      
      return stories;
    } catch (error) {
      if (error instanceof Error) {
        throw new ScraperError(
          `Failed to scrape author page ${url}: ${error.message}`,
          url,
          error
        );
      }
      throw error;
    }
  }

  private async extractFieldWithFallback(
    $: any,
    config: any,
    fieldName: string,
    html: string
  ): Promise<string> {
    const primarySelector = config[fieldName];
    const fallbackSelector = config[`${fieldName}Fallback`];
    const attribute = config[`${fieldName}Attribute`];
    const fallbackAttribute = config[`${fieldName}FallbackAttribute`];

    // Try primary selector first
    if (primarySelector) {
      const result = await this.extractField($, primarySelector, html, attribute);
      if (result && result.trim()) {
        return result.trim();
      }
    }

    // Try fallback selector if primary failed
    if (fallbackSelector) {
      const result = await this.extractField($, fallbackSelector, html, fallbackAttribute);
      if (result && result.trim()) {
        return result.trim();
      }
    }

    return '';
  }

  private async extractField(
    $: any, 
    selector: string | SelectorStrategy,
    html: string,
    attribute?: string
  ): Promise<any> {
    if (typeof selector === 'string') {
      // Simple CSS selector - always return single value (first element)
      const element = $(selector).first();
      if (attribute) {
        // Extract specific attribute instead of text
        return element.attr(attribute) || '';
      }
      return element.text().trim();
    }
    
    // Strategy-based extraction
    return await this.executeStrategy($, selector, html);
  }

  private async extractTags(
    $: any, 
    selector: string | SelectorStrategy,
    html: string,
    attribute?: string
  ): Promise<any> {
    if (typeof selector === 'string') {
      // Simple CSS selector - collect ALL matching elements for tags
      const elements = $(selector);
      
      if (elements.length === 0) {
        return [];
      }
      
      const results: string[] = [];
      elements.each((_: any, elem: any) => {
        const $elem = $(elem);
        const value = attribute ? $elem.attr(attribute) : $elem.text().trim();
        if (value) {
          results.push(value);
        }
      });
      
      return results;
    }
    
    // Strategy-based extraction for tags
    return await this.executeStrategy($, selector, html);
  }

  private async executeStrategy(
    $: any,
    strategy: SelectorStrategy,
    html: string
  ): Promise<any> {
    switch (strategy.strategy) {
      case 'text-pattern':
        return extractByTextPattern(html, strategy as any);
      case 'link-with-path':
        return extractLinkWithPath($, strategy as any);
      case 'text-blocks':
        return extractTextBlocks($, strategy as any);
      case 'deviantart-content':
        return extractDeviantArtContent($, strategy as any);
      case 'href-pattern':
        return extractHrefPattern($, strategy as any);
      case 'html-between':
        return extractHtmlBetween(html, strategy as any);
      case 'link-text':
        return extractLinkText($, strategy as any);
      case 'first-image':
        return extractFirstImage($, strategy as any);
      case 'responsive-image':
        return extractResponsiveImage($, strategy as any);
      case 'lazy-loaded':
        return extractLazyLoadedImage($, strategy as any);
      case 'chapters':
        return extractChapters($, strategy as any);
      case 'chapter-content':
        return extractChapterContent($, strategy as any);
      case 'multiple-types':
        return extractMultipleTypes($, strategy as any);
      case 'schema-org':
        return extractSchemaOrg($, strategy as any);
      case 'react-content':
        return extractReactContent($, strategy as any);
      default:
        throw new Error(`Unknown strategy: ${strategy.strategy}`);
    }
  }

  private async extractContent(
    $: any,
    storyConfig: any,
    url: string,
    html: string
  ): Promise<string> {
    let content = await this.extractField($, storyConfig.content, html);
    
    if (storyConfig.multiPage?.enabled) {
      const additionalPages = await this.fetchAdditionalPages(
        $, 
        url, 
        storyConfig.multiPage
      );
      
      for (const pageHtml of additionalPages) {
        const cheerioPage = await import('cheerio');
        const $page = cheerioPage.load(pageHtml);
        const pageContent = await this.extractField(
          $page, 
          storyConfig.content, 
          pageHtml
        );
        content += '\n\n' + pageContent;
      }
    }
    
    return content;
  }

  private async fetchAdditionalPages(
    $: any,
    baseUrl: string,
    config: MultiPageConfig
  ): Promise<string[]> {
    const pages: string[] = [];
    let currentUrl = baseUrl;
    let pageNum = 2;
    
    while (pageNum <= (config.maxPages || 20)) {
      let nextUrl: string | null = null;
      
      if (config.strategy === 'url-pattern') {
        nextUrl = UrlParser.buildPageUrl(baseUrl, pageNum, config);
      } else if (config.nextPageSelector) {
        const nextLink = $(config.nextPageSelector).attr('href');
        if (nextLink) {
          nextUrl = UrlParser.normalizeUrl(nextLink, currentUrl);
        }
      }
      
      if (!nextUrl) break;
      
      try {
        await this.rateLimiter.throttle();
        const html = await this.fetchWithCache(nextUrl);
        pages.push(html);
        currentUrl = nextUrl;
        pageNum++;
      } catch (error) {
        console.error(`Failed to fetch page ${pageNum}:`, error);
        break;
      }
    }
    
    return pages;
  }

  private async fetchWithCache(url: string): Promise<string> {
    const cached = this.cache.get(url);
    if (cached) {
      return cached;
    }

    await this.rateLimiter.throttle();
    
    const response = await fetch(url, {
      headers: {
        'User-Agent': this.config.globalOptions.userAgent,
      },
      signal: AbortSignal.timeout(this.config.globalOptions.timeout)
    });

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

    const html = await response.text();
    this.cache.set(url, html);
    
    return html;
  }

  private applyTransforms(text: string, transform?: string): string {
    if (!transform) return text;
    
    if (transform.startsWith('remove-suffix:')) {
      const suffix = transform.substring('remove-suffix:'.length).trim();
      return text.replace(new RegExp(`${suffix}$`, 'i'), '').trim();
    }
    
    return text;
  }
  
  private async resolveTagAliases(tags: string[]): Promise<string[]> {
    try {
      // Import the tagApi dynamically to avoid circular dependencies
      const { tagApi } = await import('../api');
      
      // Resolve all tags to their canonical names
      const resolvedTags = await tagApi.resolveTags(tags);
      
      // Filter out empty tags
      return resolvedTags.filter(tag => tag && tag.trim().length > 0);
    } catch (error) {
      console.warn('Failed to resolve tag aliases during scraping:', error);
      // Fall back to original tags if resolution fails
      return tags.filter(tag => tag && tag.trim().length > 0);
    }
  }
}