/**
 * Content extraction helpers for the website capture pipeline.
 *
 * Handles library detection, visible text extraction, Gemini captioning,
 * and asset description generation.
 *
 * All page.evaluate() calls use string expressions to avoid
 * tsx/esbuild __name injection (see esbuild issue #1031).
 */

import type { Page } from "puppeteer-core";
import { existsSync, readdirSync, statSync, readFileSync } from "node:fs";
import { join } from "node:path";
import type { CatalogedAsset } from "./assetCataloger.js";
import type { DesignTokens } from "./types.js";

/**
 * Detect JS libraries via window globals, DOM fingerprints, script URLs,
 * and WebGL shader analysis.
 *
 * Returns a deduplicated list of detected library names.
 */
export async function detectLibraries(
  page: Page,
  capturedShaders?: Array<{ type: string; source: string }>,
): Promise<string[]> {
  let detectedLibraries: string[] = [];
  try {
    detectedLibraries = (await page.evaluate(`(() => {
      var libs = [];
      function add(name) { if (libs.indexOf(name) === -1) libs.push(name); }

      // 1. Window globals (works for CDN-loaded / non-bundled libraries)
      if (typeof window.gsap !== 'undefined' || typeof window.TweenMax !== 'undefined') add('GSAP');
      if (typeof window.ScrollTrigger !== 'undefined') add('GSAP ScrollTrigger');
      if (typeof window.THREE !== 'undefined') add('Three.js');
      if (typeof window.PIXI !== 'undefined') add('PixiJS');
      if (typeof window.BABYLON !== 'undefined') add('Babylon.js');
      if (typeof window.Lottie !== 'undefined' || typeof window.lottie !== 'undefined') add('Lottie');
      if (typeof window.__NEXT_DATA__ !== 'undefined') add('Next.js');
      if (typeof window.__NUXT__ !== 'undefined') add('Nuxt');
      if (typeof window.Webflow !== 'undefined') add('Webflow');

      // 2. DOM fingerprints (survive bundling — most reliable for modern sites)
      // Three.js sets data-engine on every canvas it creates
      var threeCanvas = document.querySelector('canvas[data-engine*="three"]');
      if (threeCanvas) add('Three.js (' + (threeCanvas.getAttribute('data-engine') || '') + ')');
      // Babylon.js also sets data-engine
      var babylonCanvas = document.querySelector('canvas[data-engine*="Babylon"]');
      if (babylonCanvas) add('Babylon.js');
      // Lottie web components
      if (document.querySelector('dotlottie-wc, lottie-player, dotlottie-player')) add('Lottie');
      // Rive
      if (document.querySelector('canvas[class*="rive"], rive-canvas')) add('Rive');
      // React/Next.js
      if (document.getElementById('__next')) add('Next.js');
      if (document.getElementById('__nuxt')) add('Nuxt');
      if (document.querySelector('[data-reactroot], [data-react-helmet]')) add('React');
      // Svelte
      if (document.querySelector('[class*="svelte-"]')) add('Svelte');
      // Tailwind (utility class detection)
      if (document.querySelector('[class*="flex "], [class*="grid "], [class*="px-"], [class*="py-"]')) add('Tailwind CSS');
      // Framer Motion
      if (document.querySelector('[style*="--framer-"], [data-framer-component-type]')) add('Framer Motion');

      // 3. Script URL patterns
      document.querySelectorAll('script[src]').forEach(function(s) {
        var src = s.src.toLowerCase();
        if (src.includes('gsap') || src.includes('tweenmax') || src.includes('greensock')) add('GSAP');
        if (src.includes('scrolltrigger')) add('GSAP ScrollTrigger');
        if (src.includes('three.module') || src.includes('three.min')) add('Three.js');
        if (src.includes('pixi')) add('PixiJS');
        if (src.includes('lottie') || src.includes('bodymovin')) add('Lottie');
        if (src.includes('framer-motion')) add('Framer Motion');
        if (src.includes('anime.min') || src.includes('animejs')) add('Anime.js');
        if (src.includes('matter.min') || src.includes('matter-js')) add('Matter.js');
        if (src.includes('lenis')) add('Lenis (smooth scroll)');
      });

      return libs;
    })()`)) as string[];
  } catch {
    // Non-blocking
  }

  // 4. Shader fingerprinting — infer WebGL framework from captured GLSL
  try {
    const shaders = capturedShaders || [];
    if (shaders.length > 0) {
      const allSource = shaders.map((s) => s.source).join("\n");
      const add = (name: string) => {
        if (!detectedLibraries.includes(name)) detectedLibraries.push(name);
      };
      add("WebGL");
      // Three.js shader fingerprints (built-in uniforms that survive bundling)
      if (allSource.includes("modelViewMatrix") && allSource.includes("projectionMatrix"))
        add("Three.js (confirmed via shaders)");
      // PixiJS shader fingerprints
      else if (
        allSource.includes("vTextureCoord") &&
        allSource.includes("uSampler") &&
        !allSource.includes("modelViewMatrix")
      )
        add("PixiJS (confirmed via shaders)");
      // Babylon.js shader fingerprints
      else if (allSource.includes("viewProjection") && allSource.includes("world"))
        add("Babylon.js (confirmed via shaders)");
    }
  } catch {
    /* non-blocking */
  }

  return detectedLibraries;
}

/**
 * Extract all visible text from the page in DOM order using a TreeWalker.
 * Truncates to ~30K chars to avoid blowing up downstream prompts.
 */
export async function extractVisibleText(page: Page): Promise<string> {
  let visibleTextContent = "";
  try {
    visibleTextContent = (await page.evaluate(`(() => {
      var cookieRe = /^(accept|cookie|privacy|that's fine|got it|i agree|reject all|accept all|manage cookies|consent)$/i;
      var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
      var texts = [];
      var node;
      while (node = walker.nextNode()) {
        var text = (node.textContent || '').trim();
        if (text.length < 3) continue;
        var el = node.parentElement;
        if (!el) continue;
        var style = getComputedStyle(el);
        if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
        var tag = el.tagName.toLowerCase();
        if (tag === 'script' || tag === 'style' || tag === 'noscript') continue;
        // Skip very short text inside nav/footer (catches single-word nav links)
        // Threshold is 8 chars to preserve footer copy like "© 2026 Stripe" (16 chars)
        var inNavOrFooter = el.closest('nav, footer, [role="navigation"]');
        if (inNavOrFooter && text.length < 8) continue;
        // Skip common cookie/consent patterns
        if (cookieRe.test(text)) continue;
        texts.push('[' + tag + '] ' + text);
      }
      return texts.join('\\n');
    })()`)) as string;
    // Truncate to ~30K chars to avoid blowing up the prompt
    if (visibleTextContent.length > 30000) {
      visibleTextContent = visibleTextContent.slice(0, 30000) + "\n[...truncated]";
    }
  } catch {
    // Non-blocking
  }
  return visibleTextContent;
}

/**
 * Caption downloaded images using Gemini vision API.
 *
 * Batches requests to stay under free-tier rate limits.
 * Returns a map of filename -> caption string.
 */
export async function captionImagesWithGemini(
  outputDir: string,
  progress: (stage: string, detail?: string) => void,
  warnings: string[],
): Promise<Record<string, string>> {
  const geminiCaptions: Record<string, string> = {};
  const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
  if (!geminiKey) return geminiCaptions;

  progress("design", "Captioning images with Gemini vision...");
  try {
    const { GoogleGenAI } = await import("@google/genai");
    const ai = new GoogleGenAI({ apiKey: geminiKey });
    const imageFiles = readdirSync(join(outputDir, "assets")).filter((f: string) =>
      /\.(png|jpg|jpeg|webp|gif)$/i.test(f),
    );

    // Caption in parallel batches via Gemini vision API.
    // Free tier: 5 RPM → batch 5, 12s pause (~$0 but slow)
    // Paid tier: 2000 RPM → batch 20, 1s pause (~$0.001/image, fast)
    // We try a larger batch first; if rate-limited, fall back to smaller batches.
    // Default is a preview model — update when GA ships.
    // Benchmark (49 images, paid tier): 3.1-flash-lite-preview ~507ms/img 131ch avg,
    // 2.5-flash-lite ~230ms/img 117ch avg. Preview has richer captions but higher variance.
    // Override: HYPERFRAMES_GEMINI_MODEL=gemini-2.5-flash-lite
    const model = process.env.HYPERFRAMES_GEMINI_MODEL || "gemini-3.1-flash-lite-preview";
    const BATCH_SIZE = 20;
    for (let i = 0; i < imageFiles.length; i += BATCH_SIZE) {
      const batch = imageFiles.slice(i, i + BATCH_SIZE);
      const results = await Promise.allSettled(
        batch.map(async (file: string) => {
          const filePath = join(outputDir, "assets", file);
          const stat = statSync(filePath);
          if (stat.size > 4_000_000) return { file, caption: "" }; // skip images > 4 MB (Gemini inline limit)
          const buffer = readFileSync(filePath);
          const base64 = buffer.toString("base64");
          const ext = file.split(".").pop()?.toLowerCase() || "png";
          const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
          const response = await ai.models.generateContent({
            model,
            contents: [
              {
                role: "user",
                parts: [
                  { inlineData: { mimeType, data: base64 } },
                  {
                    text: "Describe this website image in ONE short sentence for a video storyboard. Focus on: what it shows, dominant colors, whether background is light or dark. Be factual, not creative.",
                  },
                ],
              },
            ],
            config: { maxOutputTokens: 500 },
          });
          return { file, caption: response.text?.trim() || "" };
        }),
      );
      for (const result of results) {
        if (result.status === "fulfilled" && result.value.caption) {
          geminiCaptions[result.value.file] = result.value.caption;
        }
      }
      // Pace requests between batches (paid tier: 2000+ RPM, free tier: rate-limited)
      if (i + BATCH_SIZE < imageFiles.length) {
        await new Promise((r) => setTimeout(r, 2000)); // 2s pause between batches — paid tier handles 2000 RPM, free tier retries via Promise.allSettled
      }
      progress(
        "design",
        `Captioned ${Math.min(i + BATCH_SIZE, imageFiles.length)}/${imageFiles.length} images...`,
      );
    }
    progress("design", `${Object.keys(geminiCaptions).length} images captioned with Gemini`);

    // Caption SVGs by sending source code as text (vision API rejects image/svg+xml).
    const svgFiles: Array<{ file: string; relPath: string }> = [];
    const assetsDir = join(outputDir, "assets");
    for (const f of readdirSync(assetsDir)) {
      if (/\.svg$/i.test(f)) svgFiles.push({ file: f, relPath: f });
    }
    const svgsSubdir = join(assetsDir, "svgs");
    if (existsSync(svgsSubdir)) {
      for (const f of readdirSync(svgsSubdir)) {
        if (/\.svg$/i.test(f)) svgFiles.push({ file: f, relPath: `svgs/${f}` });
      }
    }

    if (svgFiles.length > 0) {
      progress("design", `Captioning ${svgFiles.length} SVGs via code analysis...`);
      const SVG_BATCH = 20;
      const MAX_SVG_CHARS = 10_000;
      for (let i = 0; i < svgFiles.length; i += SVG_BATCH) {
        const batch = svgFiles.slice(i, i + SVG_BATCH);
        const results = await Promise.allSettled(
          batch.map(async ({ relPath }) => {
            const filePath = join(assetsDir, relPath);
            let svgText = readFileSync(filePath, "utf-8");
            if (svgText.length > MAX_SVG_CHARS) {
              svgText = svgText.slice(0, MAX_SVG_CHARS) + "\n<!-- truncated -->";
            }
            const response = await ai.models.generateContent({
              model,
              contents: [
                {
                  role: "user",
                  parts: [
                    {
                      text:
                        "This SVG code is from a website. Describe what it renders in ONE short sentence " +
                        "for a video storyboard. Focus on: what shape/icon/illustration it is, its colors. " +
                        "Be factual.\n\n" +
                        svgText,
                    },
                  ],
                },
              ],
              config: { maxOutputTokens: 300 },
            });
            return { file: relPath, caption: response.text?.trim() || "" };
          }),
        );
        for (const result of results) {
          if (result.status === "fulfilled" && result.value.caption) {
            geminiCaptions[result.value.file] = result.value.caption;
          }
        }
        if (i + SVG_BATCH < svgFiles.length) {
          await new Promise((r) => setTimeout(r, 2000));
        }
        progress(
          "design",
          `Captioned ${Math.min(i + SVG_BATCH, svgFiles.length)}/${svgFiles.length} SVGs...`,
        );
      }
      progress("design", `${Object.keys(geminiCaptions).length} total assets captioned`);
    }
  } catch (err) {
    warnings.push(`Gemini captioning failed: ${err}`);
  }

  return geminiCaptions;
}

/**
 * Generate asset-descriptions.md — one-line descriptions for each downloaded asset.
 *
 * Returns the description lines (without the markdown header).
 */
export function generateAssetDescriptions(
  outputDir: string,
  tokens: DesignTokens,
  catalogedAssets: CatalogedAsset[],
  geminiCaptions: Record<string, string>,
): string[] {
  // Sort: Gemini-captioned images first (richest descriptions), then uncaptioned, then SVGs, then fonts
  const captionedLines: string[] = [];
  const uncaptionedLines: string[] = [];
  const svgLines: string[] = [];
  const fontLines: string[] = [];

  // Describe downloaded images
  const assetsPath = join(outputDir, "assets");
  try {
    for (const file of readdirSync(assetsPath)) {
      if (file === "svgs" || file === "fonts" || file === "lottie" || file === "videos") continue;
      const filePath = join(assetsPath, file);
      const stat = statSync(filePath);
      if (!stat.isFile()) continue;
      const sizeKb = Math.round(stat.size / 1024);
      const catalogMatch = catalogedAssets.find(
        (a) => a.url && file.includes(a.url.split("/").pop()?.split("?")[0]?.slice(0, 20) || "___"),
      );
      const desc = catalogMatch?.description || catalogMatch?.notes || "";
      const heading = catalogMatch?.nearestHeading || "";
      const section = catalogMatch?.sectionClasses || "";
      const aboveFold = catalogMatch?.aboveFold ? "above fold" : "";
      const geminiCaption = geminiCaptions[file];
      const cleanName = file.replace(/\.[^.]+$/, "").replace(/[-_]/g, " ");
      const parts = [`${file} — ${sizeKb}KB`];
      if (geminiCaption) {
        parts.push(geminiCaption);
        captionedLines.push(parts.join(", "));
      } else {
        if (desc) parts.push(`"${desc.slice(0, 80)}"`);
        if (heading) parts.push(`section: "${heading.slice(0, 60)}"`);
        else if (section) parts.push(`in: ${section.split(" ").slice(0, 3).join(" ")}`);
        if (aboveFold) parts.push(aboveFold);
        if (!desc && !heading) parts.push(cleanName);
        uncaptionedLines.push(parts.join(", "));
      }
    }
  } catch {
    /* no assets dir */
  }

  // Describe SVGs
  try {
    const svgsPath = join(assetsPath, "svgs");
    for (const file of readdirSync(svgsPath)) {
      if (!file.endsWith(".svg")) continue;
      const geminiCaption = geminiCaptions[`svgs/${file}`];
      if (geminiCaption) {
        svgLines.push(`svgs/${file} — ${geminiCaption}`);
        continue;
      }
      const svgMatch = tokens.svgs.find(
        (s) =>
          s.label &&
          file.includes(
            s.label
              .toLowerCase()
              .replace(/[^a-z0-9]/g, "-")
              .slice(0, 15),
          ),
      );
      const label = svgMatch?.label || file.replace(".svg", "").replace(/-/g, " ");
      const isLogo = svgMatch?.isLogo || file.includes("logo");
      svgLines.push(`svgs/${file} — ${isLogo ? "logo: " : "icon: "}${label}`);
    }
  } catch {
    /* no svgs dir */
  }

  // Describe fonts
  try {
    const fontsPath = join(assetsPath, "fonts");
    for (const file of readdirSync(fontsPath)) {
      fontLines.push(`fonts/${file} — font file`);
    }
  } catch {
    /* no fonts dir */
  }

  return [...captionedLines, ...uncaptionedLines, ...svgLines, ...fontLines];
}
