// PhishProtect Content Script - Optimized Edition
// Lightweight monitoring with background delegation
// Reduced from 427 lines to ~150 lines

// Inject page-world scripts into main world context
function injectPageScript(path) {
  const script = document.createElement('script');
  script.src = chrome.runtime.getURL(path);
  script.onload = function() { this.remove(); };
  (document.head || document.documentElement).appendChild(script);
}

// Inject immediately (before any page scripts run)
// Script tracker must go first so it hooks appendChild before clipboard interceptor loads
injectPageScript('injected/script-tracker.js');
injectPageScript('injected/clipboard-interceptor.js');
// DOM event sensor: feeds the v3 dom-channel ruleset (dialog-trap). Must wrap
// alert/confirm/prompt/open before page scripts run, so inject early too.
injectPageScript('injected/dom-event-sensor.js');
console.log('PhishProtect: Page-world scripts injected');

// Bridge: page-world clipboard-interceptor posts results via window.postMessage,
// but content scripts run in an isolated world — the background can only see
// chrome.runtime messages. Forward the relevant page-world events across.
// Without this, every JS-driven clipboard hijack (the classic clickfix vector)
// gets blocked but never reaches the portal's Hunting feed.
window.addEventListener('message', (event) => {
  // Only trust same-window messages from our own origin
  if (event.source !== window) return;
  const msg = event.data;
  if (!msg || typeof msg !== 'object') return;

  if (msg.type === 'phishprotect-clipboard-blocked') {
    try {
      chrome.runtime.sendMessage({
        type: 'clipboard_injection_blocked',
        data: {
          threats:        msg.threats || [],
          source:         msg.source || 'unknown',
          timestamp:      msg.timestamp || Date.now(),
          contentPreview: msg.contentPreview || null,
          stackTrace:     msg.stackTrace || null,
          inlineScripts:  msg.inlineScripts || null,
          dynamicScripts: msg.dynamicScripts || null,
          sourceSnippets: msg.sourceSnippets || null,
          url:            location.href
        }
      });
    } catch (e) {
      // Extension may be reloading — ignore.
    }
  }

  // DOM event sensor → v3 dom-channel (dialog-trap). Cumulative behavioral
  // event-state; the background runs the enhanced engine and reports findings.
  if (msg.type === 'nithic-dom-event-state') {
    if (_nithicSettings?.detection?.contentRules === false) return; // same master toggle
    try {
      chrome.runtime.sendMessage({
        type: 'dom_event_state',
        data: { state: msg.state || {}, url: msg.url || location.href }
      }).catch(() => {});
    } catch (_) {
      // Extension may be reloading — ignore.
    }
  }

  // Clipboard interceptor → v3 clipboard-channel (poisoning). Behavioral
  // event-state (writes/copies/reads + permission) for crypto-swap /
  // pastejacking / sensitive-data detection. Command-injection blocking still
  // happens synchronously in the interceptor — this is the report-only sibling.
  if (msg.type === 'nithic-clipboard-state') {
    if (_nithicSettings?.detection?.contentRules === false) return; // same master toggle
    try {
      chrome.runtime.sendMessage({
        type: 'clipboard_state',
        data: { state: msg.state || {}, url: msg.url || location.href }
      }).catch(() => {});
    } catch (_) {
      // Extension may be reloading — ignore.
    }
  }
});

// Initialize protection
// ============================================
// User settings — cached + reactive
// ============================================
// Detection always runs (telemetry/dashboard need it). What these flags control:
//   - overlays.enabled : whether the visual indicators (rectangles, badges,
//                        corner brackets, image outline) are SHOWN on the page
//   - detection.{text,image,video} : whether the local content script even
//                                    *queues* a scan for that modality
const NITHIC_DEFAULT_SETTINGS = {
  // overlays = how detections are SHOWN on the page.
  //   enabled       : master switch (text + image overlays)
  //   text / image  : per-type visibility (granular, set in Settings)
  //   textDetail    : 'detailed' (underline + tint + tooltip) | 'minimal' (underline only)
  //   minConfidence : 0-100; hide text highlights below this confidence
  overlays: { enabled: true, text: true, image: true, textDetail: 'detailed', minConfidence: 0, snoozeUntil: null },
  // detection = what gets ANALYZED. 'text' now drives the on-page passive scan
  // too (the model downloads lazily on the first page with substantial text).
  // engine: 'local' (default — on-device, private) | 'cloud' | 'hybrid'
  // contentRules: v3 enhanced-engine page-content detection (command-pattern,
  // keyboard-instruction / ClickFix). On by default; reports, doesn't block.
  detection: { image: true, text: true, video: true, engine: 'local', contentRules: true }
};
let _nithicSettings = JSON.parse(JSON.stringify(NITHIC_DEFAULT_SETTINGS));

function nsOverlaysEnabled() {
  const o = _nithicSettings.overlays || {};
  if (o.enabled === false) return false;
  if (o.snoozeUntil && Date.now() < o.snoozeUntil) return false;
  return true;
}
function nsDetectionEnabled(modality) {
  return _nithicSettings.detection?.[modality] !== false;
}

function nsSyncOverlayVisibility() {
  // content_scripts run at document_start, before <body> exists. If we try
  // to flip the class too early we silently no-op and the toggle appears
  // dead after every reload. Defer until body is in the tree, then re-sync.
  if (!document.body) {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', nsSyncOverlayVisibility, { once: true });
    } else {
      // documentElement exists but no body yet (unusual) — try next microtask
      queueMicrotask(nsSyncOverlayVisibility);
    }
    return;
  }
  const o = _nithicSettings.overlays || {};
  const masterOff = !nsOverlaysEnabled();
  // Master switch + per-type granularity (Settings → Overlays). Detection still
  // runs regardless; these only control what's painted on the page.
  document.body.classList.toggle('ns-overlays-off', masterOff);
  document.body.classList.toggle('ns-text-overlays-off',  o.text === false);
  document.body.classList.toggle('ns-image-overlays-off', o.image === false);
  document.body.classList.toggle('ns-text-detail-minimal', o.textDetail === 'minimal');
}

(async function loadNithicSettings() {
  try {
    const result = await chrome.storage.local.get('nithicSettings');
    if (result.nithicSettings) {
      _nithicSettings = { ..._nithicSettings, ...result.nithicSettings };
      // Deep-merge nested
      if (result.nithicSettings.overlays)  _nithicSettings.overlays  = { ...NITHIC_DEFAULT_SETTINGS.overlays,  ...result.nithicSettings.overlays  };
      if (result.nithicSettings.detection) _nithicSettings.detection = { ...NITHIC_DEFAULT_SETTINGS.detection, ...result.nithicSettings.detection };
    }
    nsSyncOverlayVisibility();
  } catch (_) {}
})();

// Live-update on settings change — toggle in popup applies without page reload
try {
  chrome.storage.onChanged.addListener((changes, area) => {
    if (area !== 'local' || !changes.nithicSettings) return;
    const next = changes.nithicSettings.newValue || NITHIC_DEFAULT_SETTINGS;
    _nithicSettings = {
      overlays:  { ...NITHIC_DEFAULT_SETTINGS.overlays,  ...(next.overlays  || {}) },
      detection: { ...NITHIC_DEFAULT_SETTINGS.detection, ...(next.detection || {}) }
    };
    nsSyncOverlayVisibility();
  });
} catch (_) {}

function initializeProtection() {
  // Monitor clipboard operations (backup monitoring for manual copy/paste)
  const clipboardMonitor = new ClipboardMonitor();
  clipboardMonitor.initialize();

  // Monitor DOM for dynamic content
  monitorDOMChanges();

  // Initialize HTTP protection
  initializeHttpProtection();

  // CVE-aware credential-form gate (non-blocking on safe sites)
  interceptCveFormSubmissions();
}

// CVE gate on sensitive form submissions
// IMPORTANT: never call preventDefault() on a 'maybe-safe' form — that breaks
// React/SPA forms (their onSubmit must run synchronously; a manual form.submit()
// afterward bypasses React entirely, causing a native page POST that loses state).
// Pre-fetch CVE status once per page; on submit, only block if confirmed risk.
let _cachedCveStatus = null;
function refreshCveStatus() {
  try {
    chrome.runtime.sendMessage({ type: 'getCveStatus' }, (result) => {
      if (chrome.runtime.lastError) return;
      _cachedCveStatus = result || null;
    });
  } catch { /* ignore */ }
}
function interceptCveFormSubmissions() {
  refreshCveStatus();
  setInterval(refreshCveStatus, 30000);

  document.addEventListener('submit', (event) => {
    const form = event.target;
    if (!form || form.tagName !== 'FORM') return;
    if (window.location.protocol !== 'https:') return;

    const inputs = Array.from(form.getElementsByTagName('input'));
    const hasPassword = inputs.some(i => i.type === 'password');
    const hasEmail = inputs.some(i => i.type === 'email');
    if (!hasPassword && !hasEmail) return;

    if (_cachedCveStatus && _cachedCveStatus.hasCriticalCves) {
      const total = _cachedCveStatus.totalCves || 0;
      const crit = _cachedCveStatus.criticalCount || 0;
      const high = _cachedCveStatus.highCount || 0;
      const proceed = confirm(
        '⚠️ VULNERABILITY WARNING\n\n' +
        'This site has ' + total + ' known CVE' + (total !== 1 ? 's' : '') +
        ' (' + crit + ' critical, ' + high + ' high).\n\n' +
        'Submitting credentials to a site with known vulnerabilities is risky.\n' +
        'Your data may be exposed due to unpatched software.\n\n' +
        'Do you want to proceed?'
      );
      if (!proceed) {
        event.preventDefault();
        event.stopPropagation();
      }
    }
  }, true);
}

// Monitor DOM for dynamically added forms and links
function monitorDOMChanges() {
  const observer = new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      for (const node of mutation.addedNodes) {
        if (node.nodeType === Node.ELEMENT_NODE) {
          // Extract form data for background analysis
          if (node.tagName === 'FORM') {
            const formData = extractFormData(node);
            notifyBackground('dom_form_added', formData);
          }
          // Extract link data for background analysis
          if (node.tagName === 'A') {
            const linkData = extractLinkData(node);
            notifyBackground('dom_link_added', linkData);
          }
        }
      }
    }
  });

  // Start observing when body is ready
  if (document.body) {
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  } else {
    // Wait for body
    document.addEventListener('DOMContentLoaded', () => {
      if (document.body) {
        observer.observe(document.body, {
          childList: true,
          subtree: true
        });
      }
    });
  }
}

// Extract form data for background analysis (NO analysis here)
function extractFormData(form) {
  const inputs = Array.from(form.getElementsByTagName('input'));
  return {
    url: window.location.href,
    action: form.action,
    method: form.method,
    inputCount: inputs.length,
    inputTypes: inputs.map(i => ({ type: i.type, name: i.name })),
    hasPassword: inputs.some(i => i.type === 'password'),
    hasEmail: inputs.some(i => i.type === 'email')
  };
}

// Extract link data for background analysis (NO analysis here)
function extractLinkData(link) {
  return {
    pageUrl: window.location.href,
    href: link.href,
    text: link.textContent.trim(),
    rel: link.rel,
    target: link.target
  };
}

// Capture a clean stack trace at the current point
function captureStackTrace() {
  try {
    const stack = new Error().stack;
    return stack.split('\n')
      .filter(line => !line.includes('chrome-extension://'))
      .slice(0, 10)
      .map(line => line.trim());
  } catch (e) {
    return null;
  }
}

// Notify background script
function notifyBackground(type, data) {
  chrome.runtime.sendMessage({
    type: type,
    data: data
  }).catch(() => {
    // Ignore errors if background is unavailable
  });
}

// Show warning overlay (from background decision)
function showWarningOverlay(data) {
  // Remove existing overlay if present
  const existing = document.getElementById('phishprotect-warning-overlay');
  if (existing) {
    existing.remove();
  }

  // Create overlay
  const overlay = document.createElement('div');
  overlay.id = 'phishprotect-warning-overlay';
  overlay.style.cssText = `
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.8);
    z-index: 2147483647;
    display: flex;
    justify-content: center;
    align-items: center;
    font-family: Arial, sans-serif;
  `;

  // Create warning box
  const box = document.createElement('div');
  box.style.cssText = `
    background: #fff;
    padding: 30px;
    border-radius: 8px;
    max-width: 500px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  `;

  // Title
  const title = document.createElement('h2');
  title.textContent = '⚠️ Security Warning';
  title.style.cssText = 'color: #ea4335; margin: 0 0 15px 0;';
  box.appendChild(title);

  // Message
  const message = document.createElement('p');
  message.textContent = data.message || 'This page may contain security threats.';
  message.style.cssText = 'margin: 0 0 15px 0; line-height: 1.5;';
  box.appendChild(message);

  // Risk score
  if (data.riskScore) {
    const riskInfo = document.createElement('p');
    riskInfo.textContent = `Risk Score: ${data.riskScore}`;
    riskInfo.style.cssText = 'margin: 0 0 20px 0; font-weight: bold; color: #666;';
    box.appendChild(riskInfo);
  }

  // Close button
  const closeBtn = document.createElement('button');
  closeBtn.textContent = 'I Understand';
  closeBtn.style.cssText = `
    background: #1a73e8;
    color: #fff;
    border: none;
    padding: 10px 20px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
  `;
  closeBtn.onclick = () => overlay.remove();
  box.appendChild(closeBtn);

  overlay.appendChild(box);
  document.body.appendChild(overlay);
}

// Initialize HTTP protection
function initializeHttpProtection() {
  // Check if current page is HTTP (not HTTPS)
  if (window.location.protocol !== 'http:') return;

  // Skip localhost
  if (window.location.hostname === 'localhost' ||
      window.location.hostname === '127.0.0.1' ||
      window.location.hostname === '::1') return;

  console.log('PhishProtect: HTTP site detected, initializing protection');

  // Show persistent HTTP warning banner
  showHttpWarningBanner();

  // Intercept form submissions
  interceptFormSubmissions();

  // Warn about file inputs
  warnAboutFileInputs();
}

// Show persistent warning banner for HTTP sites
function showHttpWarningBanner() {
  const banner = document.createElement('div');
  banner.id = 'phishprotect-http-banner';
  banner.style.cssText = `
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
    color: #fff;
    padding: 12px 20px;
    z-index: 2147483646;
    font-family: Arial, sans-serif;
    font-size: 14px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
  `;

  banner.innerHTML = `
    <span style="margin-right: 10px;">🔓</span>
    <strong>Insecure Connection (HTTP)</strong>
    <span style="margin: 0 10px;">•</span>
    Your data is not encrypted on this site
  `;

  // Insert at the beginning of body
  if (document.body) {
    document.body.insertBefore(banner, document.body.firstChild);
  } else {
    document.addEventListener('DOMContentLoaded', () => {
      if (document.body) {
        document.body.insertBefore(banner, document.body.firstChild);
      }
    });
  }
}

// Intercept form submissions with password fields
function interceptFormSubmissions() {
  document.addEventListener('submit', (event) => {
    const form = event.target;
    if (!form || form.tagName !== 'FORM') return;

    // Check if form contains password or sensitive inputs
    const inputs = Array.from(form.getElementsByTagName('input'));
    const hasPassword = inputs.some(input => input.type === 'password');
    const hasEmail = inputs.some(input => input.type === 'email');
    const hasSensitiveData = hasPassword || hasEmail;

    if (hasSensitiveData) {
      event.preventDefault();
      event.stopPropagation();

      // Show warning dialog
      const proceed = confirm(
        '⚠️ SECURITY WARNING\n\n' +
        'You are about to submit sensitive information (password/email) over an INSECURE HTTP connection.\n\n' +
        'Risks:\n' +
        '• Your password will be transmitted in plain text\n' +
        '• Anyone on your network can intercept your credentials\n' +
        '• This connection is NOT encrypted\n\n' +
        'PhishProtect has BLOCKED this form submission for your safety.\n\n' +
        'Recommendation: Only enter passwords on secure HTTPS sites.\n\n' +
        'Do you really want to proceed? (NOT RECOMMENDED)'
      );

      if (!proceed) {
        console.log('PhishProtect: Blocked HTTP form submission with password');
        return false;
      }

      // User chose to proceed anyway - show one more warning
      const finalConfirm = confirm(
        'FINAL WARNING\n\n' +
        'Are you absolutely sure you want to send your password over an unencrypted connection?\n\n' +
        'This is extremely dangerous and NOT recommended.\n\n' +
        'Click OK to proceed at your own risk.'
      );

      if (finalConfirm) {
        console.log('PhishProtect: User confirmed HTTP form submission');
        // Remove the event listener temporarily to allow submission
        form.removeEventListener('submit', arguments.callee);
        form.submit();
      }

      return false;
    }
  }, true);
}

// Warn about file inputs on HTTP sites
function warnAboutFileInputs() {
  document.addEventListener('change', (event) => {
    const input = event.target;
    if (!input || input.type !== 'file') return;

    if (input.files && input.files.length > 0) {
      alert(
        '⚠️ File Upload Warning\n\n' +
        'You are about to upload a file over an INSECURE HTTP connection.\n\n' +
        'Your file will NOT be encrypted during transmission.\n\n' +
        'Recommendation: Only upload files on secure HTTPS sites.'
      );
    }
  }, true);
}

// (interceptCveFormSubmissions defined above)

// Clipboard Monitor - Lightweight with background delegation
class ClipboardMonitor {
  constructor() {
    this.enabled = true;
  }

  initialize() {
    document.addEventListener('copy', this.handleCopy.bind(this), true);
    document.addEventListener('paste', this.handlePaste.bind(this), true);
  }

  async handleCopy(event) {
    if (!this.enabled) return;

    const text = window.getSelection().toString();
    if (!text || text.length < 10) return;

    const threat = this.detectThreats(text);
    if (threat) {
      event.preventDefault();
      this.showClipboardAlert('Suspicious content blocked from clipboard');
      notifyBackground('suspicious_copy', this._buildEvidence(text, threat, 'user-copy'));
    }
  }

  async handlePaste(event) {
    if (!this.enabled) return;

    try {
      const text = event.clipboardData.getData('text');
      if (!text || text.length < 10) return;

      const threat = this.detectThreats(text);
      if (threat) {
        event.preventDefault();
        this.showClipboardAlert('Suspicious paste operation blocked');
        notifyBackground('suspicious_paste', this._buildEvidence(text, threat, 'user-paste'));
      }
    } catch (error) {
      // Ignore clipboard access errors
    }
  }

  // Build the rich evidence payload the portal expects (matches the older
  // clipboard-injection schema so Hunting renders the full review panel).
  _buildEvidence(text, threat, source) {
    // Capture up to 3 inline-script previews from the page — many clickfix
    // pages stage the payload via inline JS, so the analyst can correlate.
    let inlineScripts = [];
    try {
      const scripts = Array.from(document.scripts).filter(s => !s.src && s.textContent);
      inlineScripts = scripts.slice(0, 3).map((s, idx) => ({
        index: idx,
        preview: s.textContent.slice(0, 300),
        lineCount: s.textContent.split('\n').length
      }));
    } catch (_) {}

    return {
      // Core
      length: text.length,
      patternId: threat.id,
      patternName: threat.name,
      // Full clipboard content (capped at 2KB so we always get the whole command)
      fullText: text.length <= 2000 ? text : text.slice(0, 2000) + '... [truncated]',
      textPreview: text.slice(0, 500),
      // Page context
      pageUrl: location.href,
      pageTitle: document.title,
      pageHost: location.hostname,
      // Selection context
      selectionType: window.getSelection().type,
      activeTag: document.activeElement?.tagName?.toLowerCase() || null,
      // Evidence of staging
      inlineScripts,
      clipboardSource: source,
      // Timestamps
      timestamp: Date.now()
    };
  }

  // Quick threat detection — returns the matched pattern (id + name) or null.
  // Background uses this metadata to build a telemetry event for the portal.
  detectThreats(text) {
    const host = (window.location.hostname || '').toLowerCase();
    // Microsoft sign-in origins where this code is legitimately rendered.
    // Outside these origins, a Microsoft OAuth response URL in the clipboard
    // is the core ConsentFix signal — the attacker site is asking the user to
    // paste it into a form so they can exchange the code for tokens.
    const microsoftOrigins = [
      'login.microsoftonline.com', 'login.live.com', 'login.microsoft.com',
      'login.windows.net', 'microsoft.com', 'microsoftonline.com', 'live.com',
      'office.com', 'office365.com', 'outlook.com', 'sharepoint.com', 'azure.com'
    ];
    const onMicrosoft = microsoftOrigins.some(d => host === d || host.endsWith('.' + d));

    const patterns = [
      { id: 'clickfix-powershell-http',  name: 'PowerShell + HTTP invocation',   re: /powershell.*invoke.*http/i },
      { id: 'clickfix-certutil-urlcache',name: 'certutil URL cache (LOLBin)',    re: /certutil.*urlcache/i },
      { id: 'clickfix-private-key',      name: 'Private key in clipboard',       re: /-----BEGIN.*PRIVATE KEY-----/ },
      { id: 'clickfix-cmd-http',         name: 'cmd.exe /c + HTTP',              re: /cmd\.exe.*\/c.*http/i }
    ];
    for (const p of patterns) {
      if (p.re.test(text)) return p;
    }

    // ConsentFix — only flag when we're NOT on a Microsoft sign-in origin.
    // Pattern matches Microsoft Entra ID and live.com OAuth response shapes:
    //   http://localhost:PORT/?code=0.AXoA…&state=…  (Entra desktop-app flow)
    //   …nativeclient#code=0.A…&state=…
    //   ?code=M.C…_…&state=…  (Microsoft account)
    //   ?code=0.A[A-Za-z0-9_.-]+&state=…
    if (!onMicrosoft) {
      const consentFixRe = /([?&#]code=(0\.A[A-Za-z0-9_.-]{20,}|M\.C[A-Za-z0-9_-]{20,})).{0,200}[?&]state=[A-Za-z0-9_-]{6,}/i;
      const localhostRedirectRe = /https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/[^ \n]*[?&]code=0\.A/i;
      if (consentFixRe.test(text) || localhostRedirectRe.test(text)) {
        return {
          id: 'consentfix-oauth-paste',
          name: 'ConsentFix: Microsoft OAuth code on non-Microsoft origin'
        };
      }
    }
    return null;
  }

  // Back-compat shim (other code paths may still call this)
  hasObviousThreats(text) { return !!this.detectThreats(text); }

  showClipboardAlert(message) {
    const alert = document.createElement('div');
    alert.style.cssText = `
      position: fixed;
      top: 20px;
      right: 20px;
      background: #ea4335;
      color: #fff;
      padding: 15px 20px;
      border-radius: 4px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
      z-index: 2147483647;
      font-family: Arial, sans-serif;
      font-size: 14px;
    `;
    alert.textContent = message;
    document.body.appendChild(alert);

    // Auto-remove after 5 seconds
    setTimeout(() => alert.remove(), 5000);
  }
}

// ============================================
// Identity Detector - Login Form & OAuth Monitor
// ============================================
// Detects user identity (domain only) on ALL sites for AI visibility policy enforcement

const KNOWN_IDP_PATTERNS = [
  /accounts\.google\.com/, /login\.microsoftonline\.com/, /.*\.okta\.com/,
  /login\.live\.com/, /github\.com\/login/, /auth0\.com/
];

const OAUTH_BUTTON_PATTERNS = [
  /sign\s*in\s*with\s*(google|microsoft|github|apple|facebook)/i,
  /continue\s*with\s*(google|microsoft|github|apple|facebook)/i,
  /log\s*in\s*with\s*(google|microsoft|github|apple|facebook)/i
];

function initializeIdentityDetector() {
  // A. Form-based login detection
  document.addEventListener('submit', (event) => {
    const form = event.target;
    if (!form || form.tagName !== 'FORM') return;

    const inputs = Array.from(form.getElementsByTagName('input'));
    const hasPassword = inputs.some(i => i.type === 'password');
    if (!hasPassword) return;

    // Find email/username field
    const emailInput = inputs.find(i =>
      i.type === 'email' ||
      /email|username|user|login|account/i.test(i.name) ||
      /email|username|user|login|account/i.test(i.id) ||
      /email|username|user|login|account/i.test(i.getAttribute('autocomplete') || '')
    );

    if (!emailInput || !emailInput.value) return;

    const value = emailInput.value.trim();
    const atIndex = value.indexOf('@');
    if (atIndex === -1) return; // No domain to extract

    // Extract domain only (discard username for privacy)
    const identityDomain = value.substring(atIndex + 1).toLowerCase();
    if (!identityDomain || !identityDomain.includes('.')) return;

    notifyBackground('identity_detected', {
      identityDomain,
      site: window.location.hostname,
      method: 'form_login'
    });
  }, true);

  // B. OAuth/SSO button click detection
  document.addEventListener('click', (event) => {
    const el = event.target?.closest?.('a, button, [role="button"]');
    if (!el) return;

    const text = (el.textContent || '').trim();
    const isOAuth = OAUTH_BUTTON_PATTERNS.some(p => p.test(text));
    if (!isOAuth) return;

    notifyBackground('identity_detected', {
      identityDomain: null, // Domain will be resolved after OAuth redirect
      site: window.location.hostname,
      method: 'oauth_click',
      provider: text
    });
  }, true);

  // C. IdP URL detection (if we're ON an IdP page)
  const currentHost = window.location.hostname;
  const isIdP = KNOWN_IDP_PATTERNS.some(p => p.test(currentHost));
  if (isIdP) {
    notifyBackground('identity_detected', {
      identityDomain: null,
      site: currentHost,
      method: 'idp_visit',
      idpDomain: currentHost
    });
  }
}

// ============================================
// AI Visibility - Policy Enforcement (Real-time)
// ============================================

function showPolicyBlockToast(message, policyName) {
  const toast = document.createElement('div');
  toast.style.cssText = `
    position: fixed;
    top: 20px;
    right: 20px;
    background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
    color: #fff;
    padding: 16px 20px;
    border-radius: 8px;
    box-shadow: 0 4px 20px rgba(220, 38, 38, 0.4);
    z-index: 2147483647;
    font-family: Arial, sans-serif;
    font-size: 14px;
    max-width: 380px;
    line-height: 1.4;
  `;
  toast.innerHTML = `
    <div style="font-weight: 700; margin-bottom: 6px;">Action Blocked</div>
    <div style="opacity: 0.9;">${message}</div>
    <div style="margin-top: 8px; font-size: 11px; opacity: 0.7;">Policy: ${policyName}</div>
  `;
  document.body.appendChild(toast);
  setTimeout(() => toast.remove(), 6000);
}

// ============================================
// AI Visibility - User Action Monitor
// ============================================

// Inline AI domain list (lightweight check, no module import needed)
const AI_DOMAIN_MAP = {
  'chatgpt.com': 'ChatGPT', 'chat.openai.com': 'ChatGPT', 'openai.com': 'ChatGPT',
  'copilot.microsoft.com': 'Copilot Cloud', 'copilot.cloud.microsoft': 'Copilot Cloud',
  'github.copilot.com': 'Copilot Cloud', 'copilot.github.com': 'Copilot Cloud',
  'gemini.google.com': 'Gemini', 'bard.google.com': 'Gemini', 'aistudio.google.com': 'Gemini',
  'grok.com': 'Grok', 'grok.x.ai': 'Grok', 'x.ai': 'Grok',
  'perplexity.ai': 'Perplexity', 'www.perplexity.ai': 'Perplexity',
  'claude.ai': 'Claude', 'console.anthropic.com': 'Claude',
  'meta.ai': 'Meta AI', 'www.meta.ai': 'Meta AI',
  'chat.mistral.ai': 'Mistral', 'mistral.ai': 'Mistral',
  'huggingface.co': 'Hugging Face',
  'chat.deepseek.com': 'DeepSeek', 'deepseek.com': 'DeepSeek',
  'poe.com': 'Poe', 'you.com': 'You.com',
  'coral.cohere.com': 'Cohere', 'dashboard.cohere.com': 'Cohere',
  'app.jasper.ai': 'Jasper', 'cursor.sh': 'Cursor', 'replit.com': 'Replit'
};

function getAIService() {
  const host = window.location.hostname.toLowerCase();
  if (AI_DOMAIN_MAP[host]) return { domain: host, service: AI_DOMAIN_MAP[host] };
  const noWww = host.replace(/^www\./, '');
  if (AI_DOMAIN_MAP[noWww]) return { domain: noWww, service: AI_DOMAIN_MAP[noWww] };
  const parts = host.split('.');
  if (parts.length > 2) {
    const parent = parts.slice(1).join('.');
    if (AI_DOMAIN_MAP[parent]) return { domain: parent, service: AI_DOMAIN_MAP[parent] };
  }
  return null;
}

function initializeAIVisibilityMonitor() {
  const aiInfo = getAIService();
  if (!aiInfo) return; // Not an AI domain, skip

  console.log(`PhishProtect AI Visibility: Monitoring ${aiInfo.service} (${aiInfo.domain})`);

  function sendAIEvent(eventType, extra = {}) {
    notifyBackground('ai_visibility_event', {
      eventType,
      domain: aiInfo.domain,
      service: aiInfo.service,
      contentSize: extra.contentSize || 0,
      metadata: extra.metadata || {}
    });
  }

  // Policy check helper: asks background if action is allowed
  function checkPolicyAndBlock(eventType, event) {
    return new Promise((resolve) => {
      try {
        chrome.runtime.sendMessage({
          type: 'ai_visibility_policy_check',
          data: {
            aiService: aiInfo.service,
            action: eventType,
            siteDomain: window.location.hostname
          }
        }, (response) => {
          if (response && !response.allowed) {
            event.preventDefault();
            event.stopImmediatePropagation();
            showPolicyBlockToast(response.reason, response.policyName);
            // Record violation event
            notifyBackground('ai_visibility_event', {
              eventType,
              domain: aiInfo.domain,
              service: aiInfo.service,
              contentSize: 0,
              isViolation: true,
              policyName: response.policyName,
              alertType: eventType
            });
            resolve(false);
          } else {
            resolve(true);
          }
        });
      } catch (e) {
        resolve(true); // Allow on error
      }
    });
  }

  // Monitor paste events (with policy enforcement)
  document.addEventListener('paste', async (event) => {
    const allowed = await checkPolicyAndBlock('paste', event);
    if (!allowed) return;
    try {
      const text = event.clipboardData?.getData('text') || '';
      sendAIEvent('paste', { contentSize: text.length });
    } catch (e) {
      sendAIEvent('paste');
    }
  }, true);

  // Monitor copy events
  document.addEventListener('copy', () => {
    const text = window.getSelection()?.toString() || '';
    sendAIEvent('copy', { contentSize: text.length });
  }, true);

  // Monitor file uploads (input[type=file] change events, with policy enforcement)
  document.addEventListener('change', async (event) => {
    const input = event.target;
    if (input?.type === 'file' && input.files?.length > 0) {
      const allowed = await checkPolicyAndBlock('file_upload', event);
      if (!allowed) {
        input.value = ''; // Clear the file input
        return;
      }
      const totalSize = Array.from(input.files).reduce((sum, f) => sum + f.size, 0);
      sendAIEvent('file_upload', {
        contentSize: totalSize,
        metadata: { fileCount: input.files.length, fileNames: Array.from(input.files).map(f => f.name) }
      });
    }
  }, true);

  // Monitor drag-and-drop file uploads
  document.addEventListener('drop', (event) => {
    const files = event.dataTransfer?.files;
    if (files && files.length > 0) {
      const totalSize = Array.from(files).reduce((sum, f) => sum + f.size, 0);
      sendAIEvent('file_upload', {
        contentSize: totalSize,
        metadata: { fileCount: files.length, method: 'drag-drop' }
      });
    }
  }, true);

  // Monitor keyboard input in text areas / contenteditable (manual input)
  let inputDebounce = null;
  let inputCharCount = 0;

  function handleManualInput(event) {
    // Only track actual typing in input/textarea/contenteditable
    const el = event.target;
    if (!el) return;
    const isInput = el.tagName === 'TEXTAREA' || el.tagName === 'INPUT' ||
                    el.isContentEditable || el.getAttribute('contenteditable') === 'true';
    if (!isInput) return;

    inputCharCount += (event.data?.length || 1);

    // Debounce: report after 3 seconds of inactivity
    clearTimeout(inputDebounce);
    inputDebounce = setTimeout(() => {
      if (inputCharCount > 5) { // Ignore trivial input
        sendAIEvent('manual_input', { contentSize: inputCharCount });
      }
      inputCharCount = 0;
    }, 3000);
  }
  document.addEventListener('input', handleManualInput, true);

  // Monitor right-click (context menu)
  document.addEventListener('contextmenu', () => {
    sendAIEvent('right_click');
  }, true);

  // Monitor downloads (listen for link clicks with download attribute)
  document.addEventListener('click', (event) => {
    const link = event.target?.closest?.('a[href]');
    if (!link) return;

    if (link.hasAttribute('download') || link.href?.match(/\.(zip|tar|gz|pdf|csv|json|txt|py|js|ts|xlsx|docx|pptx)(\?|$)/i)) {
      sendAIEvent('file_download', {
        metadata: { href: link.href, download: link.getAttribute('download') || true }
      });
    }
  }, true);

  // Monitor Print Screen / screen capture attempts
  document.addEventListener('keydown', (event) => {
    if (event.key === 'PrintScreen' ||
        (event.ctrlKey && event.shiftKey && event.key === 'S') || // Win+Shift+S snipping
        (event.metaKey && event.shiftKey && (event.key === '3' || event.key === '4' || event.key === '5'))) { // Mac screenshots
      sendAIEvent('screen_capture');
    }
  }, true);

}

// ============================================
// AI Content Detection - Runs on ALL sites
// ============================================
// Detects AI-generated text and images everywhere, not just on AI domains.
// The value is catching AI content when it leaves AI services and appears
// in Google Docs, Slack, LinkedIn, email, etc.

function initializeContentScanning() {
  const hostname = window.location.hostname;

  // Skip scanning on our own infrastructure — avoids feedback loops where the
  // gallery's rendered thumbnails get re-scanned and re-ingested back into it.
  // Covers all six Nithic TLDs and any subdomain (portal, api, cloud, etc.)
  const NITHIC_HOST_RE = /(^|\.)nithic\.(ai|com|io|info|net|org)$/i;
  if (NITHIC_HOST_RE.test(hostname)) {
    console.log('[Nithic] Skipping content scan on own domain:', hostname);
    return;
  }

  const scannedImages = new WeakSet();
  let scannedTextHashes = new Set();

  // Determine if we're on an AI domain (for context tagging, not gating)
  const aiInfo = getAIService(); // may be null on non-AI sites

  // --- Text scanning: paste events on ALL sites ---
  document.addEventListener('paste', (event) => {
    if (!nsDetectionEnabled('text')) return; // user disabled text analysis
    try {
      const text = event.clipboardData?.getData('text') || '';
      if (text.length < 200) return; // Skip short pastes

      // Simple hash to avoid re-scanning same text
      const hash = text.substring(0, 100) + text.length;
      if (scannedTextHashes.has(hash)) return;
      scannedTextHashes.add(hash);
      if (scannedTextHashes.size > 50) scannedTextHashes = new Set([...scannedTextHashes].slice(-30));

      chrome.runtime.sendMessage({
        type: 'content_scan_text',
        data: {
          text,
          url: window.location.href,
          domain: hostname,
          aiService: aiInfo?.service || null
        }
      }).catch(() => {});
    } catch (e) {
      // Ignore clipboard access errors
    }
  }, true);

  // --- Text scanning: copy events (detect copying AI output from any page) ---
  document.addEventListener('copy', () => {
    if (!nsDetectionEnabled('text')) return;
    try {
      const text = window.getSelection()?.toString() || '';
      if (text.length < 200) return;

      const hash = text.substring(0, 100) + text.length;
      if (scannedTextHashes.has(hash)) return;
      scannedTextHashes.add(hash);

      chrome.runtime.sendMessage({
        type: 'content_scan_text',
        data: {
          text,
          url: window.location.href,
          domain: hostname,
          aiService: aiInfo?.service || null
        }
      }).catch(() => {});
    } catch (e) {
      // Ignore
    }
  }, true);

  // --- Passive page-text scan + highlight ---
  // Opt-in via popup ("Highlight AI text on page"). Walks ALL visible text on
  // the page (structure-agnostic — works on LinkedIn/Twitter/etc. where copy
  // lives in <span>s, not <p>s), groups it into the tightest block container,
  // ranks blocks by a cheap client heuristic, and batches the top candidates
  // to the cloud classifier. AI-flagged blocks get a subtle violet underline
  // that the overlay-off class hides instantly (no rescan on toggle).
  //
  // Design note: the heuristic does NOT gate what gets sent — modern LLM text
  // (especially lightly-edited social posts) rarely trips phrase heuristics,
  // so a hard gate would suppress exactly what we want to catch. The heuristic
  // only RANKS; the model is the real decision-maker. The per-page cap bounds
  // cost instead.
  const AI_PHRASES = [
    "it's worth noting", "it is worth noting", "it's important to note",
    "in conclusion", "here are some", "there are several",
    "additionally", "furthermore", "moreover", "it's essential",
    "plays a crucial role", "a testament to", "navigating the",
    "in the realm of", "delve into", "stands as a", "serves as a",
    "underscores the", "a comprehensive", "it's crucial", "in today's",
    "leverage", "harness", "foster", "streamline",
    "multifaceted", "nuanced", "holistic", "tapestry",
    "paramount", "aforementioned", "pertaining to"
  ];

  // Hard-exclude any element inside one of these (interactive, code, chrome).
  const EXCLUDE_CLOSEST = 'input, textarea, select, button, code, pre, kbd, samp, nav, header, footer, [contenteditable="true"], [role="navigation"], [role="banner"], [role="contentinfo"], [role="menu"], .ns-ai-text-flag';
  // Tags that never count as a "text block" even if block-level.
  const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'SVG', 'CANVAS', 'A', 'BUTTON', 'NAV', 'HEADER', 'FOOTER', 'CODE', 'PRE']);

  const MIN_BLOCK_LENGTH = 120;        // long enough for the classifier to be meaningful
  const MAX_BLOCKS_PER_BATCH = 40;     // sent to cloud per scan pass
  const MAX_BLOCKS_PER_PAGE = 160;     // lifetime cap (infinite scroll safety)
  const MAX_TEXT_NODES = 8000;         // walker safety bound
  const SCAN_DEBOUNCE_MS = 1200;

  const scannedBlocks = new WeakSet();   // block element → already evaluated
  const flaggedBlocks = new Map();       // block element → { verdict, confidence }
  const _displayCache = new WeakMap();   // element → computed display (per pass it's fine to keep)
  let totalSent = 0;
  let totalIdentified = 0;               // passages found + dispatched this page
  let _pageInFlight = 0;                 // passages currently awaiting a verdict

  function clientHeuristic(text) {
    const lower = text.toLowerCase();
    let phraseHits = 0;
    for (const p of AI_PHRASES) if (lower.includes(p)) phraseHits++;
    const phraseScore = Math.min(1, phraseHits / 3);

    const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
    let structureScore = 0.3;
    if (sentences.length >= 3) {
      const lengths = sentences.map(s => s.trim().split(/\s+/).length);
      const avg = lengths.reduce((a, b) => a + b, 0) / lengths.length;
      const variance = lengths.reduce((sum, l) => sum + Math.pow(l - avg, 2), 0) / lengths.length;
      const cv = Math.sqrt(variance) / Math.max(avg, 1);
      if (cv < 0.2) structureScore = 0.85;
      else if (cv < 0.35) structureScore = 0.6;
      else if (cv < 0.5) structureScore = 0.4;
      else structureScore = 0.15;
    }
    const combined = Math.min(1, phraseScore * 0.55 + structureScore * 0.35 + (text.length > 800 ? 0.1 : 0));
    return { combined, phraseScore, structureScore };
  }

  function isBlockish(el) {
    let d = _displayCache.get(el);
    if (d === undefined) {
      try { d = getComputedStyle(el).display; } catch { d = ''; }
      _displayCache.set(el, d);
    }
    return d === 'block' || d === 'list-item' || d === 'flex' ||
           d === 'grid'  || d === 'table-cell' || d === 'flow-root';
  }

  // Climb from a text node's parent to the tightest block-level container.
  // Returns null if the nearest block is <body> (loose text) or excluded.
  function nearestBlock(start) {
    let el = start, depth = 0;
    while (el && el !== document.body && depth++ < 16) {
      if (el.nodeType === 1) {
        if (SKIP_TAGS.has(el.tagName)) return null;
        if (isBlockish(el)) return el;
      }
      el = el.parentElement;
    }
    return null;
  }

  // Group all substantial visible text into tightest block containers.
  function collectBlocks() {
    if (!document.body) return [];
    const byBlock = new Map(); // block el → accumulated text
    const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
      acceptNode(node) {
        const v = node.nodeValue;
        if (!v || v.replace(/\s+/g, ' ').trim().length < 20) return NodeFilter.FILTER_REJECT;
        return NodeFilter.FILTER_ACCEPT;
      }
    });
    let node, walked = 0;
    while ((node = walker.nextNode())) {
      if (++walked > MAX_TEXT_NODES) break;
      const block = nearestBlock(node.parentElement);
      if (!block) continue;
      if (scannedBlocks.has(block)) continue;
      if (block.closest(EXCLUDE_CLOSEST)) continue;
      const prev = byBlock.get(block) || '';
      if (prev.length > 4000) continue; // already enough for this block
      byBlock.set(block, (prev + ' ' + node.nodeValue).replace(/\s+/g, ' ').trim());
    }

    const out = [];
    for (const [el, text] of byBlock) {
      if (text.length < MIN_BLOCK_LENGTH) continue;
      // Visibility: skip display:none / 0-size (off-screen below fold is fine).
      const r = el.getBoundingClientRect();
      if (r.width === 0 && r.height === 0) continue;
      out.push({ el, text: text.slice(0, 4000), h: clientHeuristic(text) });
    }
    return out;
  }

  function applyHighlight(el, verdict, confidence) {
    if (flaggedBlocks.has(el)) return;
    // Respect the min-confidence preference (Settings → Overlays). Below the
    // threshold we don't paint a highlight at all (the scan still counts it).
    const minConf = (_nithicSettings.overlays?.minConfidence || 0) / 100;
    if ((confidence || 0) < minConf) return;
    flaggedBlocks.set(el, { verdict, confidence });
    el.classList.add('ns-ai-text-flag');
    if (verdict === 'ai_generated' || (verdict === 'mixed' && confidence >= 0.7)) {
      el.classList.add('ns-ai-text-flag--strong');
    } else {
      el.classList.add('ns-ai-text-flag--weak');
    }
    el.dataset.nsAiVerdict = verdict;
    el.dataset.nsAiConfidence = String(Math.round((confidence || 0) * 100));
    el.title = `Likely AI-generated (${el.dataset.nsAiConfidence}% confidence)`;
  }

  let scanTimer = null;
  function schedulePageScan() {
    if (scanTimer) clearTimeout(scanTimer);
    scanTimer = setTimeout(runPageScan, SCAN_DEBOUNCE_MS);
  }

  async function runPageScan() {
    scanTimer = null;
    if (!nsDetectionEnabled('text')) return;
    if (totalSent >= MAX_BLOCKS_PER_PAGE) return; // lifetime cost ceiling

    // Rank by heuristic (most AI-looking first) and take the top batch. The
    // heuristic only orders candidates — it never filters them out — so subtle
    // AI text still reaches the model as long as there's room in the batch.
    const candidates = collectBlocks();
    if (candidates.length === 0) return;
    candidates.sort((a, b) => b.h.combined - a.h.combined);

    const budget = Math.min(MAX_BLOCKS_PER_BATCH, MAX_BLOCKS_PER_PAGE - totalSent);
    const toSend = candidates.slice(0, budget);
    // Mark everything we collected as scanned so the next mutation pass doesn't
    // re-rank the same blocks; below-budget ones get a chance next pass since
    // they stay unscanned.
    for (const c of toSend) scannedBlocks.add(c.el);
    toSend.forEach((c, i) => { c.id = i; });
    totalSent += toSend.length;
    totalIdentified += toSend.length;

    // Tell the popup how many passages we've found and are analyzing right now.
    reportScanMeta(toSend.length);

    try {
      const response = await chrome.runtime.sendMessage({
        type: 'content_scan_text_batch',
        data: {
          chunks: toSend.map(c => ({
            id: c.id,
            text: c.text,
            clientFeatures: {
              verdict: c.h.combined >= 0.65 ? 'likely_ai' : 'uncertain',
              confidence: Math.round(c.h.combined * 100) / 100,
              phraseScore: c.h.phraseScore,
              structureScore: c.h.structureScore,
              model: 'client-heuristic-v1'
            }
          })),
          url: window.location.href,
          domain: hostname,
          aiService: aiInfo?.service || null
        }
      });
      const cloudChunks = response?.chunks || [];
      const byId = new Map(cloudChunks.map(c => [c.id, c]));
      for (const candidate of toSend) {
        const r = byId.get(candidate.id);
        if (!r) continue;
        const verdict = r.cloudVerdict || r.verdict;
        const confidence = r.cloudConfidence ?? r.confidence ?? 0;
        if (verdict === 'ai_generated' || verdict === 'mixed') {
          applyHighlight(candidate.el, verdict, confidence);
        }
      }
    } catch (e) {
      // Service worker not ready or backend slow — retry on next mutation tick.
      // Un-mark so these blocks are reconsidered.
      for (const c of toSend) scannedBlocks.delete(c.el);
      totalSent -= toSend.length;
      totalIdentified -= toSend.length;
    } finally {
      // This batch is no longer in flight.
      reportScanMeta(-toSend.length);
    }
  }

  // Push scan counts to the background so the popup can show progress.
  // identified = total passages found so far; analyzing = currently in flight.
  function reportScanMeta(inFlightDelta) {
    _pageInFlight = Math.max(0, _pageInFlight + inFlightDelta);
    try {
      chrome.runtime.sendMessage({
        type: 'page_scan_meta',
        data: { identified: totalIdentified, analyzing: _pageInFlight }
      }).catch(() => {});
    } catch (_) {}
  }

  // Initial scan after a settle delay so SPAs that render after load are
  // included. MutationObserver catches subsequent dynamic content.
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', schedulePageScan, { once: true });
  } else {
    schedulePageScan();
  }

  // Re-scan when settings flip to on, and clear highlights when they flip off.
  // Reuses storage.onChanged from the settings loader above — we add a local
  // listener so we don't need to expose internal state to it.
  try {
    chrome.storage.onChanged.addListener((changes, area) => {
      if (area !== 'local' || !changes.nithicSettings) return;
      const next = changes.nithicSettings.newValue || {};
      const det = next.detection || {};
      if (det.text === false) {
        // Text analysis turned off — strip every highlight we applied.
        for (const el of flaggedBlocks.keys()) {
          el.classList.remove('ns-ai-text-flag', 'ns-ai-text-flag--strong', 'ns-ai-text-flag--weak');
          el.removeAttribute('data-ns-ai-verdict');
          el.removeAttribute('data-ns-ai-confidence');
          el.removeAttribute('title');
        }
        flaggedBlocks.clear();
      } else if (det.text !== false) {
        schedulePageScan();
      }
    });
  } catch (_) {}

  // Re-scan on DOM changes — debounced so a busy SPA doesn't run this every
  // tick. We only re-scan; existing highlights stay put.
  if (document.body) {
    new MutationObserver(() => schedulePageScan())
      .observe(document.body, { childList: true, subtree: true });
  } else {
    document.addEventListener('DOMContentLoaded', () => {
      new MutationObserver(() => schedulePageScan())
        .observe(document.body, { childList: true, subtree: true });
    }, { once: true });
  }

  // --- Image overlay effects (futuristic / glowy) ---
  // SAFE: Only applies outline + box-shadow to the img element itself.
  // Badge floats from document.body using fixed positioning.
  // We NEVER modify any parent element or reparent the image.

  const activeOverlays = new Map(); // img → { badge, cleanup }

  function applyImageOverlay(img, verdict, confidence, manipulationType) {
    let color1, glowRgba, badgeText;

    if (verdict === 'ai_generated') {
      // Wholly synthetic — entire image is AI. Solid red outline, no regions.
      color1 = '#ef4444';
      glowRgba = 'rgba(239, 68, 68, 0.4)';
      badgeText = 'AI GENERATED';
    } else if (verdict === 'manipulated') {
      // Real photo with edits — outline + region rectangles indicate edits.
      color1 = '#ef4444';
      glowRgba = 'rgba(239, 68, 68, 0.4)';
      badgeText = manipulationType === 'splice'  ? 'SPLICE'
                : manipulationType === 'inpaint' ? 'INPAINT'
                : manipulationType === 'clone'   ? 'CLONE'
                : 'ALTERED';
    } else if (verdict === 'mixed') {
      color1 = '#ef4444';
      glowRgba = 'rgba(239, 68, 68, 0.4)';
      badgeText = 'AI + ALTERED';
    } else if (verdict === 'uncertain') {
      color1 = '#f59e0b';
      glowRgba = 'rgba(245, 158, 11, 0.3)';
      badgeText = '?';
    } else {
      return;
    }

    const pct = Math.round((confidence || 0) * 100);

    // Style the image itself — safe, no layout impact
    img.style.outline = `3px solid ${color1}`;
    img.style.outlineOffset = '-3px';
    img.style.setProperty('--ns-glow-color', glowRgba);
    img.style.animation = 'ns-glow-pulse 3s ease-in-out infinite';
    img.dataset.nsVerdict = verdict;

    // Floating badge — appended to document.body with fixed positioning
    const badge = document.createElement('div');
    badge.className = 'ns-float-badge';
    badge.style.cssText = `
      position:fixed; z-index:2147483646; pointer-events:none;
      background:rgba(0,0,0,0.8); backdrop-filter:blur(6px);
      border:1px solid ${color1}; border-radius:4px;
      padding:2px 7px;
      font-family:Arial,sans-serif; font-size:10px; font-weight:700;
      color:${color1}; letter-spacing:0.5px;
      box-shadow:0 0 10px ${glowRgba};
      display:flex; align-items:center; gap:4px;
      transition:top 0.1s,left 0.1s;
      opacity:0;
    `;
    badge.innerHTML = `<span style="width:5px;height:5px;border-radius:50%;background:${color1};box-shadow:0 0 4px ${color1};"></span>${badgeText}<span style="opacity:0.6;font-size:9px;">${pct}%</span>`;
    document.body.appendChild(badge);

    // Position the badge relative to the image
    function positionBadge() {
      const rect = img.getBoundingClientRect();
      if (rect.width === 0 || rect.height === 0) { badge.style.opacity = '0'; return; }
      badge.style.top = `${rect.top + 4}px`;
      badge.style.left = `${rect.left + 4}px`;
      badge.style.opacity = '1';
    }

    positionBadge();

    // Reposition on scroll/resize
    const reposition = () => requestAnimationFrame(positionBadge);
    window.addEventListener('scroll', reposition, { passive: true });
    window.addEventListener('resize', reposition, { passive: true });

    // Cleanup function
    const cleanup = () => {
      badge.remove();
      window.removeEventListener('scroll', reposition);
      window.removeEventListener('resize', reposition);
    };

    // Clean up previous overlay if re-scanning
    if (activeOverlays.has(img)) activeOverlays.get(img).cleanup();
    activeOverlays.set(img, { badge, cleanup });

    // Clean up if image is removed from DOM
    const observer = new MutationObserver(() => {
      if (!document.contains(img)) { cleanup(); observer.disconnect(); activeOverlays.delete(img); }
    });
    observer.observe(document.body, { childList: true, subtree: true });
  }

  /**
   * Draw manipulation region rectangles as floating divs over the image.
   * Only for images large enough (>200px displayed).
   */
  function applyManipulationRegions(img, regions) {
    if (!regions || regions.length === 0) return;

    const displayW = img.clientWidth || img.offsetWidth;
    const displayH = img.clientHeight || img.offsetHeight;
    if (displayW < 120 || displayH < 120) return;

    const scaleX = displayW / (img.naturalWidth || displayW);
    const scaleY = displayH / (img.naturalHeight || displayH);

    // Cap to the most-significant regions: rank by confidence × area, then take top N
    const MAX_REGIONS = 5;
    const ranked = regions
      .map(r => ({ region: r, score: (r.confidence ?? 0.5) * (r.w || 0) * (r.h || 0) }))
      .sort((a, b) => b.score - a.score)
      .slice(0, MAX_REGIONS)
      .map(x => x.region);

    const rects = [];

    ranked.forEach((region, i) => {
      const rect = document.createElement('div');
      rect.className = 'ns-float-region';
      const isClone = region.type && region.type.indexOf('clone') !== -1;
      const stroke = isClone ? 'rgba(245, 158, 11, 0.95)' : 'rgba(239, 68, 68, 0.95)';
      const glow   = isClone ? 'rgba(245, 158, 11, 0.4)'  : 'rgba(239, 68, 68, 0.45)';
      rect.style.cssText = `
        position:fixed; z-index:2147483645; pointer-events:none;
        border:2px solid ${stroke}; border-radius:2px;
        box-shadow:0 0 8px ${glow}, inset 0 0 8px ${glow};
        animation:ns-corner-markers 2.5s ease-in-out infinite;
        animation-delay:${i * 0.3}s;
        opacity:0;
      `;
      // Tiny label for the region type — hidden by default, fades in on image hover
      let label = null;
      if (region.type) {
        label = document.createElement('span');
        label.className = 'ns-float-region-label';
        label.style.cssText = `
          position:absolute; top:-18px; left:0;
          font-family:Arial,sans-serif; font-size:9px; font-weight:700;
          letter-spacing:0.6px; color:${stroke}; text-transform:uppercase;
          background:rgba(0,0,0,0.78); padding:1px 5px; border-radius:2px;
          white-space:nowrap;
          opacity:0; transform:translateY(2px);
          transition:opacity 0.18s ease, transform 0.18s ease;
        `;
        label.textContent = region.type.replace(/_/g, ' ');
        rect.appendChild(label);
      }
      document.body.appendChild(rect);
      rects.push({ el: rect, region, label });
    });

    // Hover labels — geometry-based check via document mousemove (rAF-throttled)
    // Robust against pages that intercept mouseenter on images.
    let labelsVisible = false;
    let mmPending = false;
    let lastClientX = 0, lastClientY = 0;
    const showLabels = () => rects.forEach(r => {
      if (r.label) { r.label.style.opacity = '1'; r.label.style.transform = 'translateY(0)'; }
    });
    const hideLabels = () => rects.forEach(r => {
      if (r.label) { r.label.style.opacity = '0'; r.label.style.transform = 'translateY(2px)'; }
    });
    const onMouseMove = (e) => {
      lastClientX = e.clientX; lastClientY = e.clientY;
      if (mmPending) return;
      mmPending = true;
      requestAnimationFrame(() => {
        mmPending = false;
        const r = img.getBoundingClientRect();
        const inside = r.width > 0 && r.height > 0
          && lastClientX >= r.left && lastClientX <= r.right
          && lastClientY >= r.top  && lastClientY <= r.bottom;
        if (inside !== labelsVisible) {
          labelsVisible = inside;
          if (inside) showLabels(); else hideLabels();
        }
      });
    };
    document.addEventListener('mousemove', onMouseMove, { passive: true });

    // Hidden-image guard — used both initially and during scroll
    function isImgVisible() {
      if (!img.isConnected) return false;
      if (img.clientWidth === 0 || img.clientHeight === 0) return false;
      const cs = getComputedStyle(img);
      if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
      return true;
    }

    function positionRects() {
      if (!isImgVisible()) {
        rects.forEach(r => r.el.style.opacity = '0');
        return;
      }
      const imgRect = img.getBoundingClientRect();
      if (imgRect.width === 0 || imgRect.height === 0) {
        rects.forEach(r => r.el.style.opacity = '0');
        return;
      }
      rects.forEach(({ el, region }) => {
        const x = imgRect.left + region.x * scaleX;
        const y = imgRect.top  + region.y * scaleY;
        // GPU-accelerated transform avoids layout/paint per scroll frame (much smoother than left/top)
        el.style.transform = `translate3d(${x}px, ${y}px, 0)`;
        el.style.width = `${region.w * scaleX}px`;
        el.style.height = `${region.h * scaleY}px`;
        el.style.opacity = '1';
      });
    }

    // GPU hint — keeps the rectangle in its own composited layer
    rects.forEach(r => {
      r.el.style.willChange = 'transform';
      r.el.style.left = '0';
      r.el.style.top = '0';
    });

    positionRects();
    const reposition = () => requestAnimationFrame(positionRects);
    window.addEventListener('scroll', reposition, { passive: true });
    window.addEventListener('resize', reposition, { passive: true });
  }

  /**
   * Fallback: when verdict is "ai_generated" or "manipulated" but the cloud
   * returned no specific regions, draw four corner brackets around the image.
   * Communicates "the entire image is implicated" without misleading rectangles.
   */
  function applyCornerBrackets(img, color) {
    const displayW = img.clientWidth || img.offsetWidth;
    const displayH = img.clientHeight || img.offsetHeight;
    if (displayW < 80 || displayH < 80) return;

    const sz = Math.max(14, Math.min(28, Math.round(displayW * 0.07)));
    const corners = ['tl', 'tr', 'bl', 'br'];
    const els = corners.map(pos => {
      const c = document.createElement('div');
      c.className = 'ns-corner';
      const bw = '3px';
      const css = `position:fixed; z-index:2147483645; pointer-events:none;
        width:${sz}px; height:${sz}px; opacity:0;
        animation:ns-corner-markers 2.5s ease-in-out infinite;`;
      const borders = {
        tl: `border-top:${bw} solid ${color}; border-left:${bw} solid ${color};`,
        tr: `border-top:${bw} solid ${color}; border-right:${bw} solid ${color};`,
        bl: `border-bottom:${bw} solid ${color}; border-left:${bw} solid ${color};`,
        br: `border-bottom:${bw} solid ${color}; border-right:${bw} solid ${color};`,
      };
      c.style.cssText = css + borders[pos];
      document.body.appendChild(c);
      return { el: c, pos };
    });

    // Cache for hidden-image check
    function cornerImgVisible() {
      if (!img.isConnected) return false;
      if (img.clientWidth === 0 || img.clientHeight === 0) return false;
      const cs = getComputedStyle(img);
      return !(cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0');
    }
    // GPU hint
    els.forEach(({ el }) => { el.style.willChange = 'transform'; el.style.left = '0'; el.style.top = '0'; });

    function positionCorners() {
      if (!cornerImgVisible()) { els.forEach(e => e.el.style.opacity = '0'); return; }
      const r = img.getBoundingClientRect();
      if (r.width === 0 || r.height === 0) { els.forEach(e => e.el.style.opacity = '0'); return; }
      els.forEach(({ el, pos }) => {
        let x, y;
        if (pos === 'tl') { x = r.left;       y = r.top; }
        if (pos === 'tr') { x = r.right - sz; y = r.top; }
        if (pos === 'bl') { x = r.left;       y = r.bottom - sz; }
        if (pos === 'br') { x = r.right - sz; y = r.bottom - sz; }
        el.style.transform = `translate3d(${x}px, ${y}px, 0)`;
        el.style.opacity = '1';
      });
    }
    positionCorners();
    const reposition = () => requestAnimationFrame(positionCorners);
    window.addEventListener('scroll', reposition, { passive: true });
    window.addEventListener('resize', reposition, { passive: true });
  }

  // Inject keyframe animations (once)
  if (!document.getElementById('ns-scan-styles')) {
    const style = document.createElement('style');
    style.id = 'ns-scan-styles';
    style.textContent = `
      @keyframes ns-scanline {
        0% { top: -2px; opacity: 0; }
        10% { opacity: 1; }
        90% { opacity: 0.6; }
        100% { top: calc(100% + 2px); opacity: 0; }
      }
      @keyframes ns-glow-pulse {
        0%, 100% { box-shadow: 0 0 8px 2px var(--ns-glow-color); }
        50% { box-shadow: 0 0 20px 6px var(--ns-glow-color); }
      }
      @keyframes ns-border-rotate {
        0% { background-position: 0% 50%; }
        100% { background-position: 200% 50%; }
      }
      @keyframes ns-badge-appear {
        0% { transform: translateY(-8px) scale(0.8); opacity: 0; }
        100% { transform: translateY(0) scale(1); opacity: 1; }
      }
      @keyframes ns-corner-markers {
        0%, 100% { opacity: 0.5; }
        50% { opacity: 1; }
      }
      /* Snooze: overlays toggled off via popup hide instantly without reload.
         Detection still runs in the background; only the visual overlays go
         dark. Inline styles need !important to lose to the class rule. */
      body.ns-overlays-off .ns-float-badge,
      body.ns-overlays-off .ns-float-region,
      body.ns-overlays-off .ns-corner {
        display: none !important;
      }
      body.ns-overlays-off img[data-ns-verdict] {
        outline: none !important;
        animation: none !important;
        box-shadow: none !important;
      }
      /* AI text-on-page highlights. Brand violet — explicitly NOT red, which
         is reserved for threats. Strong = wholly AI; weak = mixed / uncertain. */
      .ns-ai-text-flag {
        position: relative;
        border-left: 3px solid #a78bfa;
        padding-left: 8px;
        background: linear-gradient(180deg, rgba(167,139,250,0.06), rgba(167,139,250,0.02));
        text-decoration: underline wavy rgba(167,139,250,0.55);
        text-decoration-skip-ink: auto;
        text-underline-offset: 3px;
        cursor: help;
      }
      .ns-ai-text-flag--strong {
        border-left-color: #7c3aed;
        background: linear-gradient(180deg, rgba(124,58,237,0.10), rgba(124,58,237,0.04));
        text-decoration-color: rgba(124,58,237,0.7);
      }
      body.ns-overlays-off .ns-ai-text-flag,
      body.ns-text-overlays-off .ns-ai-text-flag {
        border-left: none !important;
        padding-left: 0 !important;
        background: transparent !important;
        text-decoration: inherit !important;
        cursor: inherit !important;
      }
      /* Granular: hide only image overlays (Settings → Overlays → image off). */
      body.ns-image-overlays-off .ns-float-badge,
      body.ns-image-overlays-off .ns-float-region,
      body.ns-image-overlays-off .ns-corner {
        display: none !important;
      }
      body.ns-image-overlays-off img[data-ns-verdict] {
        outline: none !important;
        animation: none !important;
        box-shadow: none !important;
      }
      /* Minimal text detail: keep just the wavy underline, drop the tint +
         left border (Settings → Overlays → Text detail: Minimal). */
      body.ns-text-detail-minimal .ns-ai-text-flag {
        border-left: none !important;
        padding-left: 0 !important;
        background: transparent !important;
      }
    `;
    (document.head || document.documentElement).appendChild(style);
  }

  // --- Image scanning: all images on the page ---
  // Per-skip logging is gated by NITHIC_DEBUG=true on window so it stays quiet
  // by default. Toggle in DevTools: window.NITHIC_DEBUG = true; then reload.
  const skipLog = (reason, img) => {
    if (window.NITHIC_DEBUG) {
      console.log('[Nithic] Skip image:', reason, img.src?.slice(0, 80), `${img.naturalWidth}x${img.naturalHeight}`);
    }
  };
  function scanImage(img) {
    if (!nsDetectionEnabled('image')) return; // user disabled image analysis
    if (scannedImages.has(img)) return;
    if (!img.src) { skipLog('no src', img); return; }
    if (img.src.startsWith('data:') && img.src.length < 1000) { skipLog('tiny data URL', img); return; }
    if (img.naturalWidth < 200 || img.naturalHeight < 200) { skipLog('too small', img); return; }

    if (!img.isConnected) { skipLog('detached', img); return; }
    const cs = getComputedStyle(img);
    if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') {
      skipLog('hidden (' + cs.display + '/' + cs.visibility + '/' + cs.opacity + ')', img);
      return;
    }
    if (img.clientWidth === 0 || img.clientHeight === 0) { skipLog('zero box', img); return; }

    scannedImages.add(img);

    chrome.runtime.sendMessage({
      type: 'content_scan_image',
      data: {
        imageData: {
          src: img.src,
          naturalWidth: img.naturalWidth,
          naturalHeight: img.naturalHeight,
          alt: img.alt || '',
          fileSize: 0
        },
        url: window.location.href,
        domain: hostname,
        aiService: aiInfo?.service || null
      }
    }, (result) => {
      if (chrome.runtime.lastError) return;
      if (!result) return;

      // Apply visual overlay based on verdict
      const verdict = result.cloud?.cloudVerdict || result.client?.verdict;
      const confidence = result.cloud?.cloudConfidence || result.client?.confidence || 0;
      const manipType = result.cloud?.manipulationType;
      const regions = result.cloud?.manipulationRegions || [];

      // Diagnostic so verdicts/regions are inspectable from DevTools
      console.log('[Nithic] Image scan:', {
        src: img.src.slice(0, 80),
        verdict, confidence, manipType,
        regionCount: regions.length,
        regions: regions.slice(0, 3)
      });

      if (verdict && verdict !== 'human') {
        applyImageOverlay(img, verdict, confidence, manipType);
        if (regions.length > 0) {
          applyManipulationRegions(img, regions);
        } else if (verdict === 'ai_generated' || verdict === 'manipulated' || verdict === 'mixed') {
          // No specific altered regions returned — communicate "whole image" via corner brackets
          applyCornerBrackets(img, '#ef4444');
        }
      }
    });
  }

  // IntersectionObserver triggers scans when an image enters the viewport.
  // This handles lazy-loaded images, hidden tabs/panels that become visible,
  // infinite scroll, etc. — patterns used by Gemini, ChatGPT, photoai, etc.
  // The visibility guard inside scanImage() then gates the cloud request itself.
  const tryScan = (img) => {
    if (img.complete && img.naturalWidth > 0) {
      scanImage(img);
    } else {
      img.addEventListener('load', () => scanImage(img), { once: true });
    }
  };

  const intersectionObserver = new IntersectionObserver((entries) => {
    for (const entry of entries) {
      if (entry.isIntersecting) tryScan(entry.target);
    }
  }, { rootMargin: '300px', threshold: 0.01 });

  // Watch existing imgs for src changes (lazy-loading via src swap on the same element)
  const srcChangeObserver = new MutationObserver((mutations) => {
    for (const m of mutations) {
      if (m.type === 'attributes' && m.attributeName === 'src' && m.target.tagName === 'IMG') {
        // Allow re-scan: same element, new src is a new piece of content
        scannedImages.delete(m.target);
        tryScan(m.target);
      }
    }
  });

  const watchImg = (img) => {
    intersectionObserver.observe(img);
    srcChangeObserver.observe(img, { attributes: true, attributeFilter: ['src'] });
  };

  // Observe all existing images
  document.querySelectorAll('img').forEach(watchImg);

  // Watch for dynamically added images (DOM insertions)
  const imageObserver = new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      for (const node of mutation.addedNodes) {
        if (node.nodeType !== Node.ELEMENT_NODE) continue;
        if (node.tagName === 'IMG') watchImg(node);
        const imgs = node.querySelectorAll?.('img');
        if (imgs) imgs.forEach(watchImg);
      }
    }
  });

  if (document.body) {
    imageObserver.observe(document.body, { childList: true, subtree: true });
  }

  // --- Image scanning: file uploads (input[type=file]) on any site ---
  document.addEventListener('change', (event) => {
    const input = event.target;
    if (input?.type !== 'file' || !input.files?.length) return;

    for (const file of input.files) {
      if (!file.type.startsWith('image/')) continue;
      chrome.runtime.sendMessage({
        type: 'content_scan_image',
        data: {
          imageData: {
            src: file.name,
            naturalWidth: 0,
            naturalHeight: 0,
            alt: file.name,
            fileSize: file.size
          },
          url: window.location.href,
          domain: hostname,
          aiService: aiInfo?.service || null
        }
      }).catch(() => {});
    }
  }, true);
}

// Initialize when DOM is ready
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => {
    initializeProtection();
    initializeIdentityDetector();
    initializeContentScanning();
    initializeAIVisibilityMonitor();
  });
} else {
  initializeProtection();
  initializeIdentityDetector();
  initializeContentScanning();
  initializeAIVisibilityMonitor();
}

console.log('PhishProtect content script loaded (optimized)');

// --- v3 page-content scan ---
// Sends the visible page text to the background, which runs the v3
// content-channel rulesets (command-pattern, keyboard-instruction/ClickFix).
// On by default (detection.contentRules), top frame only, debounced, dedups
// identical content. Reports detections; does not block the page.
(function nsV3ContentScan() {
  if (window.top !== window) return; // top frame only
  let lastHash = '';
  let timer = null;

  // Default ON — disabled only if the user explicitly turns contentRules off.
  function enabled() { return _nithicSettings?.detection?.contentRules !== false; }
  function hash(s) { let h = 0; for (let i = 0; i < s.length; i += 97) h = (h * 31 + s.charCodeAt(i)) | 0; return h + ':' + s.length; }

  function scan() {
    if (!enabled() || !document.body) return;
    let text = '';
    try { text = (document.body.innerText || '').slice(0, 50000); } catch { return; }
    if (text.length < 20) return;
    const h = hash(text);
    if (h === lastHash) return;
    lastHash = h;
    try { chrome.runtime.sendMessage({ type: 'content_scan_v3', data: { content: text, url: location.href } }).catch(() => {}); } catch (_) {}
  }
  function schedule() { if (timer) clearTimeout(timer); timer = setTimeout(scan, 2000); }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', schedule, { once: true });
  else schedule();
  try { new MutationObserver(() => schedule()).observe(document.documentElement, { childList: true, subtree: true }); } catch (_) {}
})();

// --- v3 DOM-structure scan ---
// Builds a bounded snapshot of structural attack surface (transparent/hidden
// iframes, fake browser windows, fake address bars) and sends it to the
// background, which runs the dom-channel rulesets (browser-in-browser,
// fake-chrome, clickjacking). Top frame only, debounced; only emits when the
// snapshot actually contains something interesting, so most pages send nothing.
(function nsV3DomStructureScan() {
  if (window.top !== window) return; // top frame only
  let timer = null;
  let lastSig = '';
  const URL_LIKE = /(https?:\/\/|chrome:\/\/|www\.)/i;

  function enabled() { return _nithicSettings?.detection?.contentRules !== false; }

  function collect() {
    const vw = window.innerWidth || 1, vh = window.innerHeight || 1;
    const vpArea = vw * vh;

    // iframes (cap 40)
    const iframes = [];
    let frameEls = [];
    try { frameEls = [...document.querySelectorAll('iframe')].slice(0, 40); } catch { frameEls = []; }
    for (const f of frameEls) {
      let cs, r;
      try { cs = getComputedStyle(f); r = f.getBoundingClientRect(); } catch { continue; }
      const area = Math.max(0, r.width) * Math.max(0, r.height);
      let crossOrigin = false;
      try { crossOrigin = new URL(f.src || '', location.href).origin !== location.origin; } catch {}
      iframes.push({
        opacity: parseFloat(cs.opacity),
        coverage: vpArea ? area / vpArea : 0,
        position: cs.position,
        zIndex: parseInt(cs.zIndex) || 0,
        pointerEvents: cs.pointerEvents,
        visibility: cs.visibility,
        crossOrigin,
      });
    }

    // BitB candidates: positioned, sizable, chrome-styled modals (cap scan 400)
    const bitbCandidates = [];
    let candEls = [];
    try { candEls = [...document.querySelectorAll('div,section,dialog,form')].slice(0, 400); } catch {}
    for (const el of candEls) {
      let cs, r;
      try { cs = getComputedStyle(el); r = el.getBoundingClientRect(); } catch { continue; }
      if (cs.position !== 'fixed' && cs.position !== 'absolute') continue;
      if (r.width < 240 || r.height < 160) continue;                 // too small for a window
      if (r.width > vw * 0.98 && r.height > vh * 0.95) continue;     // full-page = overlay, not window
      const hasBoxShadow = !!cs.boxShadow && cs.boxShadow !== 'none';
      const hasBorder = (parseFloat(cs.borderTopWidth) || 0) > 0;
      if (!hasBoxShadow && !hasBorder) continue;                     // windows have chrome
      let hasAddressBarText = false, fakeUrl = '';
      let kids = [];
      try { kids = [...el.querySelectorAll('*')].slice(0, 250); } catch {}
      for (const d of kids) {
        const t = (d.textContent || '').trim();
        if (t && t.length < 120 && URL_LIKE.test(t)) {
          let dr; try { dr = d.getBoundingClientRect(); } catch { continue; }
          if (dr.top - r.top < r.height * 0.4) { hasAddressBarText = true; fakeUrl = t.slice(0, 80); break; }
        }
        const ph = d.getAttribute && d.getAttribute('placeholder');
        if (ph && URL_LIKE.test(ph)) { hasAddressBarText = true; fakeUrl = ph.slice(0, 80); break; }
      }
      let hasLoginForm = false;
      try { hasLoginForm = !!el.querySelector('input[type="password"]'); } catch {}
      const centerX = r.left + r.width / 2;
      const isCentered = Math.abs(centerX - vw / 2) < vw * 0.15;
      bitbCandidates.push({
        hasAddressBarText, fakeUrl, hasLoginForm, isCentered, hasBoxShadow, hasBorder,
        widthPercent: Math.round((r.width / vw) * 100),
      });
    }

    // fake-chrome: wide fixed/absolute bars pinned to the top showing a URL (cap 300)
    const fakeChromeTop = [];
    let topEls = [];
    try { topEls = [...document.querySelectorAll('div,header,nav,section')].slice(0, 300); } catch {}
    for (const el of topEls) {
      let cs, r;
      try { cs = getComputedStyle(el); r = el.getBoundingClientRect(); } catch { continue; }
      if (cs.position !== 'fixed' && cs.position !== 'absolute') continue;
      if (r.top > 80 || r.width < vw * 0.6) continue;
      const txt = (el.textContent || '').slice(0, 300);
      const hasUrlText = URL_LIKE.test(txt);
      let inputUrl = false;
      try {
        const i = el.querySelector('input');
        const p = i && (i.getAttribute('placeholder') || i.value || '');
        inputUrl = !!p && URL_LIKE.test(p);
      } catch {}
      if (hasUrlText || inputUrl) {
        fakeChromeTop.push({ top: Math.round(r.top), widthPercent: Math.round((r.width / vw) * 100), zIndex: parseInt(cs.zIndex) || 0, hasUrlText, hasInput: inputUrl });
      }
    }

    // credential overlays: forms containing a password/OTP field. Capture
    // cross-origin action + positioning + text (data-theft: overlay-credential).
    const credentialOverlays = [];
    const curOrigin = location.origin;
    let forms = [];
    try { forms = [...document.querySelectorAll('form')].slice(0, 60); } catch {}
    for (const form of forms) {
      let pw = false, otp = false, user = false, email = false;
      let fields = [];
      try { fields = [...form.querySelectorAll('input,select')].slice(0, 80); } catch {}
      for (const i of fields) {
        const type = (i.type || '').toLowerCase();
        const ac = (i.getAttribute('autocomplete') || '').toLowerCase();
        const nm = ((i.name || '') + ' ' + (i.getAttribute('placeholder') || '')).toLowerCase();
        if (type === 'password' || ac.includes('password')) pw = true;
        if (ac === 'one-time-code' || /\b(otp|one[- ]?time|2fa|mfa|verification code|auth(?:entication)? code|security code)\b/.test(nm)) otp = true;
        if (type === 'email' || ac.includes('email')) email = true;
        if (ac === 'username' || /user(name)?|login|account/.test(nm)) user = true;
      }
      if (!pw && !otp) continue;                    // only credential forms matter
      let crossOrigin = false;
      const action = form.getAttribute('action') || '';
      if (action) { try { crossOrigin = new URL(action, location.href).origin !== curOrigin; } catch {} }
      let cs, r;
      try { cs = getComputedStyle(form); r = form.getBoundingClientRect(); } catch { cs = {}; r = { width: 0, height: 0 }; }
      credentialOverlays.push({
        hasPassword: pw, hasOtp: otp, hasEmail: email, hasUsername: user,
        crossOrigin,
        zIndex: parseInt(cs.zIndex) || 0,
        positioned: cs.position === 'fixed' || cs.position === 'absolute',
        coveragePct: vpArea ? (Math.max(0, r.width) * Math.max(0, r.height)) / vpArea : 0,
        text: (form.textContent || '').slice(0, 400),
      });
    }

    // hidden autofill-harvest inputs: sensitive-autocomplete fields rendered
    // invisibly (opacity:0 / off-screen / 1×1). display:none is EXCLUDED — Chrome
    // won't autofill it, and it's the common anti-bot honeypot pattern.
    const SENSITIVE_AC = ['cc-number', 'cc-exp', 'cc-csc', 'cc-name', 'current-password', 'new-password'];
    const hiddenAutofillInputs = [];
    let acInputs = [];
    try { acInputs = [...document.querySelectorAll('input[autocomplete]')].slice(0, 120); } catch {}
    for (const i of acInputs) {
      const ac = (i.getAttribute('autocomplete') || '').toLowerCase();
      if (!SENSITIVE_AC.some(s => ac.includes(s))) continue;
      let cs, r;
      try { cs = getComputedStyle(i); r = i.getBoundingClientRect(); } catch { continue; }
      if (cs.display === 'none') continue;          // not autofillable → skip
      const op = parseFloat(cs.opacity);
      const invisible = cs.visibility === 'hidden' || (op >= 0 && op < 0.1);
      const zerosize = r.width < 1 || r.height < 1;
      const offscreen = r.top < -100 || r.left < -100 || r.top > 10000 || r.left > 10000;
      if (invisible || zerosize || offscreen) {
        hiddenAutofillInputs.push({ autocomplete: ac, reason: invisible ? 'invisible' : (zerosize ? 'zerosize' : 'offscreen') });
      }
    }

    return {
      iframes, bitbCandidates, fakeChromeTop,
      credentialOverlays, hiddenAutofillInputs, currentOrigin: curOrigin,
      isFramed: window.top !== window.self,
      isFullscreen: !!document.fullscreenElement,
      vw, vh,
    };
  }

  // Only worth sending if something is structurally suspicious.
  function interesting(s) {
    if (s.isFullscreen) return true;
    if (s.fakeChromeTop.length > 0) return true;
    if (s.bitbCandidates.some(c => c.hasAddressBarText)) return true;
    if (s.iframes.some(f => (f.coverage > 0.4 && f.opacity < 0.5) || f.visibility === 'hidden')) return true;
    if ((s.hiddenAutofillInputs || []).length > 0) return true;
    if ((s.credentialOverlays || []).some(c => c.crossOrigin)) return true;
    return false;
  }

  function scan() {
    if (!enabled() || !document.body) return;
    let snap; try { snap = collect(); } catch { return; }
    if (!interesting(snap)) return;
    const sig = `${snap.iframes.length}|${snap.bitbCandidates.length}|${snap.fakeChromeTop.length}|${snap.isFullscreen}|${snap.hiddenAutofillInputs.length}|${snap.credentialOverlays.length}`;
    if (sig === lastSig) return;   // cheap dedup on structural shape
    lastSig = sig;
    try { chrome.runtime.sendMessage({ type: 'dom_structure', data: { snapshot: snap, url: location.href } }).catch(() => {}); } catch (_) {}
  }
  function schedule() { if (timer) clearTimeout(timer); timer = setTimeout(scan, 2500); }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', schedule, { once: true });
  else schedule();
  try { new MutationObserver(() => schedule()).observe(document.documentElement, { childList: true, subtree: true }); } catch (_) {}
  document.addEventListener('fullscreenchange', schedule);
})();
