// PhishProtect Evidence: Source Snippet Collector
// Runs as content script at document_idle. Exposes collectSourceSnippets()
// as a global for content.js to call when clipboard detections occur.
// Parses V8 stack frames, fetches external script source,
// and extracts context lines around each referenced line.
// Also handles inline scripts (URL matches page) with byte offsets,
// and checks dynamic script log for injected-then-removed scripts.

(function() {
  'use strict';

  const MAX_EXTERNAL_FETCHES = 3;
  const MAX_SCRIPT_LINES = 500;
  const CONTEXT_LINES = 5;
  const MAX_INLINE_SCRIPTS = 5;

  /**
   * Parse a V8 stack frame line into { url, line, col, funcName } or null.
   */
  function parseFrame(frameLine) {
    if (!frameLine || typeof frameLine !== 'string') return null;
    if (frameLine.includes('chrome-extension://')) return null;
    if (frameLine.includes('<anonymous>')) return null;
    if (frameLine.includes('native code')) return null;
    if (frameLine.includes('eval at')) return null;

    const match = frameLine.match(/(https?:\/\/[^\s)]+):(\d+):(\d+)/);
    if (!match) return null;

    // Extract function name if present
    let funcName = null;
    const funcMatch = frameLine.match(/at\s+([^\s(]+)\s*\(/);
    if (funcMatch) funcName = funcMatch[1];

    return {
      url: match[1],
      line: parseInt(match[2], 10),
      col: parseInt(match[3], 10),
      funcName
    };
  }

  /**
   * Check if a URL refers to the current page (inline script).
   * V8 reports inline script line numbers relative to the full HTML document.
   */
  function isPageUrl(url) {
    try {
      // Strip hash/query for comparison
      const pageBase = location.href.split('#')[0].split('?')[0];
      const urlBase = url.split('#')[0].split('?')[0];
      return pageBase === urlBase;
    } catch (e) {
      return false;
    }
  }

  /**
   * Parse the page HTML to find inline <script> block boundaries.
   * Returns array of { startLine, endLine, startByte, endByte, index, text }
   */
  function parseInlineScriptBlocks(html) {
    const blocks = [];
    // Match <script> tags without src attribute
    const regex = /<script(?![^>]*\bsrc\s*=)[^>]*>([\s\S]*?)<\/script>/gi;
    let match;
    let blockIndex = 0;

    while ((match = regex.exec(html)) !== null) {
      const scriptContent = match[1];
      if (!scriptContent.trim()) continue;

      // Calculate byte offset of this match in the HTML
      const startByte = match.index;
      const tagOpenEnd = html.indexOf('>', match.index) + 1; // end of opening <script...>

      // Calculate line number of the script content start
      const beforeScript = html.substring(0, tagOpenEnd);
      const startLine = beforeScript.split('\n').length;

      const scriptLines = scriptContent.split('\n');
      const endLine = startLine + scriptLines.length - 1;

      blocks.push({
        index: blockIndex++,
        startLine,
        endLine,
        startByte,
        contentStartByte: tagOpenEnd,
        text: scriptContent,
        lineCount: scriptLines.length
      });
    }

    return blocks;
  }

  /**
   * For an inline stack frame, find which <script> block it falls in
   * and extract context lines with offset info.
   */
  function resolveInlineFrame(htmlLines, scriptBlocks, targetLine, targetCol, url) {
    // Find which block contains this line
    const block = scriptBlocks.find(b => targetLine >= b.startLine && targetLine <= b.endLine);

    const result = {
      url: url,
      targetLine: targetLine,
      targetCol: targetCol,
      type: 'inline',
      lines: [],
      scriptBlock: null
    };

    if (block) {
      result.scriptBlock = {
        index: block.index,
        startLine: block.startLine,
        endLine: block.endLine,
        byteOffset: block.contentStartByte,
        lineCount: block.lineCount
      };

      // Extract context from the HTML source around the target line
      const start = Math.max(0, targetLine - 1 - CONTEXT_LINES);
      const end = Math.min(htmlLines.length, targetLine + CONTEXT_LINES);

      for (let i = start; i < end; i++) {
        result.lines.push({
          lineNumber: i + 1,
          text: htmlLines[i],
          isTarget: (i + 1) === targetLine
        });
      }
    } else {
      // Couldn't find the block — just show the raw HTML lines
      const start = Math.max(0, targetLine - 1 - CONTEXT_LINES);
      const end = Math.min(htmlLines.length, targetLine + CONTEXT_LINES);
      for (let i = start; i < end; i++) {
        result.lines.push({
          lineNumber: i + 1,
          text: htmlLines[i],
          isTarget: (i + 1) === targetLine
        });
      }
    }

    return result;
  }

  /**
   * Fetch an external script's source text.
   */
  async function fetchScriptSource(url) {
    try {
      const response = await fetch(url, { credentials: 'omit', cache: 'force-cache' });
      if (!response.ok) return null;

      const contentType = response.headers.get('content-type') || '';
      if (!contentType.includes('javascript') && !contentType.includes('text/') && !contentType.includes('application/')) {
        return null;
      }

      const text = await response.text();
      const lines = text.split('\n');

      // Skip minified megafiles
      if (lines.length > MAX_SCRIPT_LINES) return null;
      if (lines.length <= 2 && text.length > 5000) return null;

      return lines;
    } catch (e) {
      return null;
    }
  }

  /**
   * Extract context lines around a target line number for external scripts.
   */
  function extractContext(scriptLines, targetLine, targetCol, url, funcName) {
    const start = Math.max(0, targetLine - 1 - CONTEXT_LINES);
    const end = Math.min(scriptLines.length, targetLine + CONTEXT_LINES);

    const contextLines = [];
    for (let i = start; i < end; i++) {
      contextLines.push({
        lineNumber: i + 1,
        text: scriptLines[i],
        isTarget: (i + 1) === targetLine
      });
    }

    return {
      url: url,
      targetLine: targetLine,
      targetCol: targetCol,
      funcName: funcName || null,
      type: 'external',
      lines: contextLines
    };
  }

  /**
   * Collect inline script sources from the current DOM.
   */
  function collectInlineScripts() {
    const scripts = [];
    try {
      const inlineScripts = document.querySelectorAll('script:not([src])');
      let count = 0;
      for (const script of inlineScripts) {
        if (count >= MAX_INLINE_SCRIPTS) break;
        const text = script.textContent || '';
        if (!text.trim() || text.length < 20) continue;

        const lines = text.split('\n');
        if (lines.length > MAX_SCRIPT_LINES) continue;

        scripts.push({
          index: count,
          lineCount: lines.length,
          preview: text.substring(0, 300),
          present: true // still in DOM
        });
        count++;
      }
    } catch (e) {}
    return scripts;
  }

  /**
   * Collect dynamically injected scripts from the script-tracker log.
   * These may have been removed from the DOM after executing.
   */
  function collectDynamicScripts() {
    const dynamic = [];
    try {
      const tracked = window.__phishprotect_dynamicScripts;
      if (!tracked || !Array.isArray(tracked)) return dynamic;

      for (const entry of tracked) {
        dynamic.push({
          src: entry.src,
          textContent: entry.textContent,
          textLength: entry.textLength,
          insertionStack: entry.insertionStack,
          timestamp: entry.timestamp,
          present: false // may have been removed
        });
      }
    } catch (e) {}
    return dynamic;
  }

  /**
   * Main collection function. Given a stack trace array,
   * parse frames, resolve inline vs external, fetch source, extract context.
   */
  async function collectSourceSnippets(stackTrace) {
    if (!stackTrace || !Array.isArray(stackTrace)) {
      return { snippets: [], inlineScripts: [], dynamicScripts: [] };
    }

    const snippets = [];
    const fetchedUrls = new Map(); // url -> lines[] or null
    let fetchCount = 0;

    // Pre-fetch page HTML for inline frame resolution
    let pageHtml = null;
    let pageHtmlLines = null;
    let scriptBlocks = null;
    const hasInlineFrames = stackTrace.some(f => {
      const parsed = parseFrame(f);
      return parsed && isPageUrl(parsed.url);
    });

    if (hasInlineFrames) {
      try {
        const resp = await fetch(location.href, { credentials: 'omit', cache: 'force-cache' });
        if (resp.ok) {
          pageHtml = await resp.text();
          pageHtmlLines = pageHtml.split('\n');
          scriptBlocks = parseInlineScriptBlocks(pageHtml);
        }
      } catch (e) {}
    }

    for (const frameLine of stackTrace) {
      const parsed = parseFrame(frameLine);
      if (!parsed) continue;

      // --- Inline script (URL = page URL) ---
      if (isPageUrl(parsed.url) && pageHtmlLines && scriptBlocks) {
        const snippet = resolveInlineFrame(pageHtmlLines, scriptBlocks, parsed.line, parsed.col, parsed.url);
        snippet.funcName = parsed.funcName;
        snippets.push(snippet);
        continue;
      }

      // --- External script ---
      let scriptLines = null;

      if (fetchedUrls.has(parsed.url)) {
        scriptLines = fetchedUrls.get(parsed.url);
      } else if (fetchCount < MAX_EXTERNAL_FETCHES) {
        fetchCount++;
        scriptLines = await fetchScriptSource(parsed.url);
        fetchedUrls.set(parsed.url, scriptLines);
      } else {
        fetchedUrls.set(parsed.url, null);
        continue;
      }

      if (!scriptLines) continue;

      const context = extractContext(scriptLines, parsed.line, parsed.col, parsed.url, parsed.funcName);
      snippets.push(context);
    }

    const inlineScripts = collectInlineScripts();
    const dynamicScripts = collectDynamicScripts();

    return { snippets, inlineScripts, dynamicScripts };
  }

  // Expose globally for content.js to call
  window.__phishprotect_collectSourceSnippets = collectSourceSnippets;
})();
