// PhishProtect Background Script - Enterprise Edition
// Integrates analyzer engine, policy management, telemetry, and enterprise features

// Core modules
import { AnalyzerEngine } from './core/analyzer-engine.js';
import { RuleRegistry } from './core/rule-registry.js';
import { ConfigManager } from './core/config-manager.js';
import { StatsCollector } from './core/stats-collector.js';

// Policy modules
import { PolicyEngine } from './policies/policy-engine.js';
import { PolicyManager } from './policies/policy-manager.js';
import { AuditLogger } from './policies/audit-logger.js';

// Telemetry modules
import { TelemetryCollector } from './telemetry/telemetry-collector.js';
import { PrivacyFilter } from './telemetry/privacy-filter.js';

// Dynamic rule updater
import { DynamicRuleUpdater } from './core/dynamic-rule-updater.js';

// Detection reporting & cloud analysis
import { DetectionReporter } from './telemetry/detection-reporter.js';
import { CloudQueryClient } from './cloud/cloud-query-client.js';

// Dynamic toolbar icon
import { updateIcon } from './core/icon-manager.js';

// v3: Signal-based architecture
import { SignalProviderRegistry } from './core/signal-provider.js';
import { FeatureStore } from './core/feature-store.js';
import { registerClipboardInjectionSignals } from './detectors/data-theft/clipboard-injection-signals.js';
import { registerClipboardPoisoningSignals } from './detectors/data-theft/clipboard-poisoning-signals.js';
import { registerCommandPatternSignals } from './detectors/social-engineering/command-pattern-signals.js';
import { registerDialogTrapSignals } from './detectors/social-engineering/dialog-trap-signals.js';
import { registerKeyboardInstructionSignals } from './detectors/social-engineering/keyboard-instruction-signals.js';
import { registerUrlDomainSignals } from './detectors/url-detectors/url-domain-signals.js';
import { registerDomStructureSignals } from './detectors/social-engineering/dom-structure-signals.js';
import { registerCredentialFormSignals } from './detectors/data-theft/credential-form-signals.js';
import { registerExfilSignals } from './detectors/data-theft/exfil-signals.js';
import { registerCodeInjectionSignals } from './detectors/code-injection/code-injection-signals.js';
import { registerPrivateNetworkSignals, isPublicHost } from './detectors/cross-site/private-network-signals.js';

// Evidence gathering modules
import { HeaderCapture } from './evidence/header-capture.js';
import { FrameChainBuilder } from './evidence/frame-chain-builder.js';

// Enterprise modules
import { MDMIntegration } from './enterprise/mdm-integration.js';
import { AdminConsoleConnector } from './enterprise/admin-console-connector.js';
import { SIEMReporter } from './enterprise/siem-reporter.js';
import { ClickHouseReporter } from './enterprise/clickhouse-reporter.js';

// AI Visibility
import { AIVisibilityReporter } from './ai-visibility/ai-visibility-reporter.js';
import { matchAIDomain } from './ai-visibility/ai-domains.js';
import { IdentityManager } from './ai-visibility/identity-manager.js';
import { ContentDetector } from './ai-visibility/content-detector.js';

// Global instances
let analyzerEngine;
let ruleRegistry;
let configManager;
let statsCollector;
let policyManager;
let auditLogger;
let telemetryCollector;
let privacyFilter;
let mdmIntegration;
let adminConsole;
let siemReporter;
let clickhouseReporter;
let dynamicRuleUpdater;
let detectionReporter;
let cloudQueryClient;
let headerCapture;
let frameChainBuilder;
let signalProviderRegistry;
let featureStore;
let aiVisibilityReporter;
let identityManager;
let contentDetector;

// Extension state
let extensionState = {
  enabled: true,
  initialized: false,
  detectorCount: 0,
  activePolicy: 'default'
};

// Statistics (backward compatibility with old popup.js)
let stats = {
  blockedUrls: 0,
  suspiciousResources: 0,
  permissionRequests: 0,
  clipboardAlerts: 0,
  httpsUpgrades: 0
};

// Store last detection for blocked page
let lastDetection = null;

// Per-tab accumulated detection state (populated by analyzeUrlRequest, clipboard handlers, etc.)
// Map<tabId, { url, detections: [], riskScore, riskLevel, decision, timestamp }>
const tabDetections = new Map();

// Per-tab AI content detection results
// tabId → { textScans: [{verdict, confidence, ...}], imageScans: [{verdict, confidence, ...}] }
const tabContentScans = new Map();

// ============================================
// On-device inference engine (offscreen document)
// ============================================
// Text detection runs locally by default (privacy: page text never leaves the
// device). The heavy ONNX models need a real Document for WebGPU/WASM, which a
// service worker can't provide, so inference lives in an offscreen document.
// 'cloud' is an opt-in/paid alternative that offloads to our cloud service.

let _engineStatus = { state: 'idle', device: null, progress: 0 }; // mirrored from offscreen
let _offscreenCreating = null;

// Coerce any saved dev/localhost cloud URL back to the real cloud endpoint.
// Early builds defaulted cloudAnalysisUrl to http://localhost:4011, which got
// persisted into users' settings; without this they'd silently point at a
// dead local port.
function resolveCloudUrl(v) {
  if (!v || /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[?::1\]?)(:\d+)?/i.test(v)) {
    return 'https://cloud.nithic.ai';
  }
  return v;
}

async function getDetectionEngine() {
  // Returns 'local' | 'cloud' | 'hybrid'. Default 'local' (private). Forced to
  // 'cloud' where offscreen isn't available (e.g. Firefox) so detection still
  // works, at the cost of locality.
  if (typeof chrome === 'undefined' || !chrome.offscreen) return 'cloud';
  try {
    const { nithicSettings } = await chrome.storage.local.get('nithicSettings');
    const engine = nithicSettings?.detection?.engine;
    return (engine === 'cloud' || engine === 'hybrid') ? engine : 'local';
  } catch { return 'local'; }
}

// Is the on-device model loaded and ready right now? Uses the cached status,
// refreshing from the offscreen doc if we have one but no 'ready' cached yet.
async function isLocalEngineReady() {
  if (_engineStatus.state === 'ready') return true;
  try {
    if (chrome.offscreen && await hasOffscreenDocument()) {
      const live = await chrome.runtime.sendMessage({ target: 'nithic-offscreen', type: 'status' });
      if (live && live.state) { _engineStatus = { ..._engineStatus, ...live }; return live.state === 'ready'; }
    }
  } catch (_) {}
  return false;
}

async function hasOffscreenDocument() {
  if (chrome.runtime.getContexts) {
    const ctxs = await chrome.runtime.getContexts({ contextTypes: ['OFFSCREEN_DOCUMENT'] });
    return ctxs.length > 0;
  }
  return false;
}

async function ensureOffscreenDocument() {
  if (await hasOffscreenDocument()) return;
  if (_offscreenCreating) { await _offscreenCreating; return; }
  _offscreenCreating = (async () => {
    try {
      await chrome.offscreen.createDocument({
        url: 'offscreen.html',
        // BLOBS is the closest valid reason — we process binary model weights
        // and build runtime tensors from them.
        reasons: ['BLOBS'],
        justification: 'Run on-device AI-text-detection models (WebGPU/WASM) so page text stays local.'
      });
      console.log('[Nithic] offscreen document created');
    } catch (e) {
      // "Only a single offscreen document" can happen on a race — tolerate it.
      if (!/single offscreen/i.test(e?.message || '')) {
        _engineStatus = { ..._engineStatus, state: 'error', error: 'offscreen create failed: ' + (e?.message || e) };
        console.error('[Nithic] offscreen create failed:', e);
        throw e;
      }
    }
  })();
  try { await _offscreenCreating; } finally { _offscreenCreating = null; }
}

// Send to the offscreen doc, surfacing the real failure. A rejection with
// "Receiving end does not exist" means the document loaded but offscreen.js
// never registered its listener — i.e. the module failed to load.
// `timeoutMs` guards status queries from hanging if the engine thread is busy
// doing synchronous WASM work.
async function sendToOffscreen(msg, timeoutMs = 0) {
  await ensureOffscreenDocument();
  try {
    const send = chrome.runtime.sendMessage(msg);
    if (!timeoutMs) return await send;
    return await Promise.race([
      send,
      new Promise((_, rej) => setTimeout(() => rej(new Error('timeout after ' + timeoutMs + 'ms')), timeoutMs))
    ]);
  } catch (e) {
    const err = e?.message || String(e);
    _engineStatus = { ..._engineStatus, state: 'error', error: 'offscreen not responding: ' + err };
    console.error('[Nithic] offscreen sendMessage failed:', err);
    throw new Error(err);
  }
}

async function inferTextLocal(chunks) {
  const resp = await sendToOffscreen({ target: 'nithic-offscreen', type: 'infer', chunks });
  return Array.isArray(resp?.chunks) ? resp.chunks : [];
}

// ============================================
// v3 enhanced-engine content detection
// ============================================
// The page-content channel (command-pattern, keyboard-instruction/ClickFix)
// runs through the enhanced engine and reports findings. Clipboard enforcement
// stays on the page-world interceptor (synchronous blocking the background
// can't replicate).

// Per-tab dedup of reported behavioral findings (the sensors re-send growing
// cumulative state, so without this we'd re-report the same trap on every tick).
// Cleared on navigation + tab close alongside the feature store.
const _v3DomReported = new Map();
const _v3ClipReported = new Map();
const _v3DomStructReported = new Map();
const _v3ExfilReported = new Map();

// Core v3 evaluation. Runs every loaded ruleset whose id matches `rulesetRegex`
// against `target` on `channel`. Returns
// { findings:[{rulesetId,ruleId,severity,weight}], latencyMs, rulesets } or null.
function evalV3Target(target, rulesetRegex, channel, tabId, frameId) {
  try {
    if (!ruleRegistry || typeof ruleRegistry.getEnhancedEngine !== 'function') return null;
    const engine = ruleRegistry.getEnhancedEngine();
    if (!engine || typeof engine.getAllCompiledRulesets !== 'function') return null;
    const compiled = engine.getAllCompiledRulesets()
      .filter(r => r.rulesetId && rulesetRegex.test(r.rulesetId));
    if (compiled.length === 0) return null;

    target.type = channel;
    target._tabId = tabId;
    target._frameId = frameId || 0;
    const opts = { signalProviderRegistry, featureStore };
    const t0 = performance.now();
    const findings = [];
    for (const c of compiled) {
      const r = engine.evaluateRuleset(c, target, opts);
      for (const f of (r.findings || [])) {
        findings.push({ rulesetId: f.rulesetId, ruleId: f.ruleId, severity: f.severity, weight: f.weight });
      }
    }
    return { findings, latencyMs: performance.now() - t0, rulesets: compiled.length };
  } catch (e) {
    console.warn('[Nithic][v3] eval failed:', e?.message || e);
    return null;
  }
}

// Text-channel evaluation: the target is just a string mapped to content/channel.
function evalV3Text(content, rulesetRegex, channel, tabId, frameId) {
  if (!content) return null;
  return evalV3Target({ content, [channel]: content }, rulesetRegex, channel, tabId, frameId);
}

// Page-content channel: command-pattern + keyboard-instruction (ClickFix).
function evalV3Content(content, tabId, frameId) {
  return evalV3Text(content, /command-pattern|keyboard-instruction/i, 'content', tabId, frameId);
}

// DOM (behavioral) channel: dialog-trap. The target is the cumulative
// event-state emitted by injected/dom-event-sensor.js (dialogEvents, popupCount,
// hasBeforeUnload, historyLoopDetected, …) — spread directly so the providers
// read their fields.
function evalV3Dom(stateObj, tabId, frameId) {
  if (!stateObj || typeof stateObj !== 'object') return null;
  // Spread the state fields so providers read them at top level; `dom` is the
  // channel key checkChannels() gates on (must be truthy for dom-channel rules).
  return evalV3Target({ ...stateObj, dom: stateObj }, /dialog-trap/i, 'dom', tabId, frameId);
}

// Clipboard (behavioral) channel: clipboard-poisoning. The target is the
// cumulative clipboard event-state emitted by the clipboard interceptor
// (clipboardWrites, copyEvents, clipboardReadCount, permissions). Only the
// *poisoning* ruleset runs here — command-injection blocking stays synchronous
// in the page-world interceptor, so we don't run clipboard-injection again.
function evalV3ClipboardState(stateObj, tabId, frameId) {
  if (!stateObj || typeof stateObj !== 'object') return null;
  return evalV3Target({ ...stateObj, clipboard: stateObj }, /clipboard-poisoning/i, 'clipboard', tabId, frameId);
}

// URL channel: typosquatting + lookalike-domain. Evaluated once per top-frame
// navigation against the page hostname. The providers self-suppress on real
// brand domains and their subdomains, so this is safe to run on every page.
function evalV3Url(url, tabId, frameId) {
  if (!url || typeof url !== 'string' || !/^https?:/i.test(url)) return null;
  // Navigation-URL rulesets: typosquatting + lookalike-domain + reflected-XSS.
  return evalV3Target({ url }, /typosquatting|lookalike-domain|reflected-xss/i, 'url', tabId, frameId);
}

// Request channel: exfil-channel. Evaluated per outbound request. The target
// carries the request URL + cross-origin flag (computed from initiator) +
// request type. Runs only the exfil ruleset (not the navigation url rulesets).
function evalV3Exfil(url, crossOrigin, requestType, tabId, frameId) {
  if (!url || typeof url !== 'string' || !/^https?:/i.test(url) || url.indexOf('?') === -1) return null;
  return evalV3Target({ url, crossOrigin: !!crossOrigin, requestType }, /exfil-channel/i, 'url', tabId, frameId);
}

// Request channel: cryptojacking. Evaluated per outbound request (no query
// needed) — matches known miner libraries / mining pools in the request URL.
function evalV3Cryptojacking(url, tabId, frameId) {
  if (!url || typeof url !== 'string' || !/^(https?|wss?):/i.test(url)) return null;
  return evalV3Target({ url }, /cryptojacking/i, 'url', tabId, frameId);
}

// Download channel: html-smuggling. Evaluated on download creation — flags an
// executable downloaded from a client-assembled blob:/data: URL.
function evalV3Download(url, filename, tabId) {
  if (!url || typeof url !== 'string') return null;
  return evalV3Target({ url, filename: filename || '' }, /html-smuggling/i, 'url', tabId, 0);
}

// Request channel: private-network. A public page reaching into the local
// network. initiatorPublic is the FP gate (intranet/dev self-traffic is fine).
function evalV3PrivateNetwork(url, initiatorPublic, tabId, frameId) {
  if (!url || typeof url !== 'string' || !/^https?:/i.test(url) || !initiatorPublic) return null;
  return evalV3Target({ url, initiatorPublic: true }, /private-network/i, 'url', tabId, frameId);
}

// DOM-structure channel: browser-in-browser, fake-chrome, clickjacking. The
// target is the structural snapshot from content.js collectDomStructure().
// Shares the 'dom' channel with dialog-trap but reads disjoint fields
// (target.dom.iframes/bitbCandidates/… vs the behavioral dialogEvents), so the
// two never cross-fire.
function evalV3DomStructure(snapshot, tabId, frameId) {
  if (!snapshot || typeof snapshot !== 'object') return null;
  return evalV3Target({ dom: snapshot }, /browser-in-browser|fake-chrome|clickjacking|overlay-credential|autofill-harvesting/i, 'dom', tabId, frameId);
}

// Report v3 shadow findings as (non-blocking) detection telemetry so they show
// up in the portal with real per-ruleset/per-rule attribution. The finding
// carries metadata.rulesetId + pattern=ruleId, which detection-reporter
// forwards and the dashboard aggregates.
function reportV3Findings(v3, tabId, url, frameId, reason = 'v3-detect') {
  if (!v3 || !v3.findings.length || !detectionReporter) return;
  const maxWeight = Math.max(0, ...v3.findings.map(f => f.weight || 0));
  const riskScore = Math.min(maxWeight * 2, 20);
  const detection = {
    detector: 'v3-engine',
    matched: true,
    findings: v3.findings.map(f => ({
      detector: f.rulesetId,
      type: f.ruleId,
      pattern: f.ruleId,
      severity: f.severity,
      weight: f.weight,
      category: 'v3',
      metadata: { rulesetId: f.rulesetId, ruleName: f.ruleId },
    })),
    riskScore,
    riskLevel: riskScore >= 15 ? 'critical' : riskScore >= 10 ? 'high' : 'medium',
  };
  try {
    detectionReporter.reportBlockingDetection(
      detection, { action: 'report', reason }, tabId, url || null, frameId || 0
    ).catch(() => {});
  } catch (_) {}
}

// Kick off model loading early so first scan isn't cold. Returns a result so
// callers (the diagnostics panel) can show success/failure.
async function warmupLocalEngine() {
  if (!chrome.offscreen) return { ok: false, error: 'offscreen API unavailable' };
  try {
    const r = await sendToOffscreen({ target: 'nithic-offscreen', type: 'warmup' });
    return r || { ok: true };
  } catch (e) {
    return { ok: false, error: e?.message || String(e) };
  }
}

/**
 * Derive icon state from tabDetections + URL for a given tab.
 * Lightweight sync function — no async chrome.tabs.get needed when url is passed in.
 */
function getTabIconState(tabId, url) {
  if (!extensionState.enabled) {
    return { security: 'disabled', findingsCount: 0, aiContent: 'unknown' };
  }

  const tabState = tabDetections.get(tabId);
  const isHttp = url?.startsWith('http://');
  const isHttps = url?.startsWith('https://');

  let security = 'unknown'; // non-web page (chrome://, about:, etc.)
  if (isHttps || isHttp) {
    if (tabState?.action === 'block' || tabState?.riskLevel === 'critical' || tabState?.riskLevel === 'high' || isHttp) {
      security = 'danger';
    } else if (tabState?.action === 'warn' || tabState?.riskLevel === 'medium') {
      security = 'warning';
    } else {
      security = 'safe';
    }
  }

  // Check if this tab is on an AI domain
  let aiContent = 'unknown';
  if (aiVisibilityReporter) {
    const aiInfo = aiVisibilityReporter.getTabAIInfo(tabId);
    if (aiInfo) aiContent = aiInfo.service;
  }

  // Count AI content detections (synthetic text/images flagged on this page)
  let aiContentCount = 0;
  const scans = tabContentScans.get(tabId);
  if (scans) {
    const flagged = (s) => {
      const v = s?.verdict;
      return v === 'ai_generated' || v === 'manipulated' || v === 'mixed';
    };
    aiContentCount = (scans.textScans || []).filter(flagged).length
                   + (scans.imageScans || []).filter(flagged).length;
    if (aiContentCount > 0) aiContent = aiContent === 'unknown' ? 'detected' : aiContent;
  }

  return {
    security,
    findingsCount: tabState?.findings?.length || 0,
    aiContent,
    aiContentCount
  };
}

/**
 * Update the toolbar icon for a tab. Accepts optional url to avoid async lookup.
 * If url is not provided, does an async chrome.tabs.get.
 */
async function updateTabIcon(tabId, url) {
  try {
    if (!url) {
      const tab = await chrome.tabs.get(tabId);
      url = tab.url || tab.pendingUrl || '';
    }
    const state = getTabIconState(tabId, url);
    await updateIcon(tabId, state);
  } catch (err) {
    // Tab may have been closed
    if (!err.message?.includes('No tab with id')) {
      console.error('updateTabIcon error:', err.message);
    }
  }
}

/**
 * Initialize PhishProtect extension
 */
async function initializeExtension() {
  console.log('Initializing PhishProtect Enterprise...');

  try {
    // Step 1: Load configuration
    configManager = new ConfigManager();
    await configManager.loadConfig();
    console.log('✓ Configuration loaded');

    // Step 2: Initialize rule registry
    ruleRegistry = new RuleRegistry(configManager.config);
    await ruleRegistry.loadAllRules();
    console.log('✓ Rules loaded');

    // Step 2a: Load v3 enhanced rulesets from local files
    const v3Rulesets = [
      { category: 'data-theft', subcategory: 'clipboard-injection', path: 'rules/v3/data-theft/clipboard-injection.json' },
      { category: 'data-theft', subcategory: 'clipboard-poisoning', path: 'rules/v3/data-theft/clipboard-poisoning.json' },
      { category: 'social-engineering', subcategory: 'command-pattern', path: 'rules/v3/social-engineering/command-pattern.json' },
      { category: 'social-engineering', subcategory: 'dialog-trap', path: 'rules/v3/social-engineering/dialog-trap.json' },
      { category: 'social-engineering', subcategory: 'keyboard-instruction', path: 'rules/v3/social-engineering/keyboard-instruction.json' },
      { category: 'social-engineering', subcategory: 'typosquatting', path: 'rules/v3/social-engineering/typosquatting.json' },
      { category: 'url-detectors', subcategory: 'lookalike-domain', path: 'rules/v3/url-detectors/lookalike-domain.json' },
      { category: 'social-engineering', subcategory: 'browser-in-browser', path: 'rules/v3/social-engineering/browser-in-browser.json' },
      { category: 'social-engineering', subcategory: 'fake-chrome', path: 'rules/v3/social-engineering/fake-chrome.json' },
      { category: 'social-engineering', subcategory: 'clickjacking', path: 'rules/v3/social-engineering/clickjacking.json' },
      { category: 'data-theft', subcategory: 'overlay-credential', path: 'rules/v3/data-theft/overlay-credential.json' },
      { category: 'data-theft', subcategory: 'autofill-harvesting', path: 'rules/v3/data-theft/autofill-harvesting.json' },
      { category: 'data-theft', subcategory: 'exfil-channel', path: 'rules/v3/data-theft/exfil-channel.json' },
      { category: 'code-injection', subcategory: 'xss', path: 'rules/v3/code-injection/reflected-xss.json' },
      { category: 'code-injection', subcategory: 'html-smuggling', path: 'rules/v3/code-injection/html-smuggling.json' },
      { category: 'code-injection', subcategory: 'wasm-mining', path: 'rules/v3/code-injection/cryptojacking.json' },
      { category: 'cross-site', subcategory: 'private-network', path: 'rules/v3/cross-site/private-network.json' },
    ];
    for (const rs of v3Rulesets) {
      await ruleRegistry.loadRulesFromFile(rs.category, rs.subcategory, rs.path).catch(err => {
        console.warn(`Failed to load v3 ruleset ${rs.path}:`, err.message);
      });
    }
    console.log('✓ v3 rulesets loaded');

    // Step 2a-ii: Load v2 fingerprint rulesets
    const v2FingerprintRulesets = [
      { category: 'fingerprint', subcategory: 'web-servers', path: 'rules/v2/fingerprint/webservers.json' },
      { category: 'fingerprint', subcategory: 'cms', path: 'rules/v2/fingerprint/wordpress.json' },
    ];
    for (const rs of v2FingerprintRulesets) {
      await ruleRegistry.loadRulesFromFile(rs.category, rs.subcategory, rs.path).catch(err => {
        console.warn(`Failed to load v2 fingerprint ruleset ${rs.path}:`, err.message);
      });
    }
    console.log('✓ v2 fingerprint rulesets loaded');

    // Step 2b: Initialize dynamic rule updater
    dynamicRuleUpdater = new DynamicRuleUpdater(ruleRegistry, {
      ruleApiUrl: configManager.get('ruleApiUrl') || 'https://api.nithic.ai'
    });
    dynamicRuleUpdater.initialize().catch(error => {
      console.warn('Dynamic rule updater initialization failed (will use local rules):', error.message);
    });
    console.log('✓ Dynamic rule updater started');

    // Step 2c: Initialize v3 signal-based architecture
    featureStore = new FeatureStore();
    await featureStore.restore(); // Restore from session storage (survives SW restart)
    console.log('✓ FeatureStore initialized');

    signalProviderRegistry = new SignalProviderRegistry();
    registerClipboardInjectionSignals(signalProviderRegistry);
    registerClipboardPoisoningSignals(signalProviderRegistry);
    registerCommandPatternSignals(signalProviderRegistry);
    registerDialogTrapSignals(signalProviderRegistry);
    registerKeyboardInstructionSignals(signalProviderRegistry);
    registerUrlDomainSignals(signalProviderRegistry);
    registerDomStructureSignals(signalProviderRegistry);
    registerCredentialFormSignals(signalProviderRegistry);
    registerExfilSignals(signalProviderRegistry);
    registerCodeInjectionSignals(signalProviderRegistry);
    registerPrivateNetworkSignals(signalProviderRegistry);
    console.log(`✓ SignalProviderRegistry initialized (${signalProviderRegistry.getStats().registered} providers)`);

    // Step 3: Initialize analyzer engine
    analyzerEngine = new AnalyzerEngine(ruleRegistry, configManager.config);
    analyzerEngine.setFeatureStore(featureStore);
    analyzerEngine.setProviderRegistry(signalProviderRegistry);
    await analyzerEngine.initialize();
    console.log(`✓ Analyzer engine initialized with ${analyzerEngine.getDetectorCount()} detectors`);
    extensionState.detectorCount = analyzerEngine.getDetectorCount();

    // Step 4: Initialize policy management
    policyManager = new PolicyManager(configManager.config);
    await policyManager.initialize();
    extensionState.activePolicy = policyManager.getActivePolicyId();
    console.log(`✓ Policy manager initialized (active: ${extensionState.activePolicy})`);

    // Step 5: Initialize audit logger
    auditLogger = new AuditLogger(configManager.config);
    await auditLogger.initializeLogger();
    console.log('✓ Audit logger initialized');

    // Step 6: Initialize telemetry
    privacyFilter = new PrivacyFilter(configManager.config);
    telemetryCollector = new TelemetryCollector(configManager.config);
    console.log('✓ Telemetry collector initialized');

    // Step 7: Initialize statistics collector
    statsCollector = new StatsCollector(configManager.config);
    console.log('✓ Statistics collector initialized');

    // Step 7b: Generate or load persistent device ID
    const deviceId = await getOrCreateDeviceId();
    console.log(`✓ Device ID: ${deviceId}`);

    // Step 7c: Collect system context (browser, OS, extension config)
    const systemContext = await collectSystemContext();
    console.log(`✓ System context: ${systemContext.browser} on ${systemContext.os}`);

    // Step 7d: Initialize evidence gathering (header capture + frame chain builder)
    // Must be before detection reporter so frameChainBuilder is available at construction
    headerCapture = new HeaderCapture();
    headerCapture.initialize();
    frameChainBuilder = new FrameChainBuilder(headerCapture);
    await frameChainBuilder.initialize();
    console.log('✓ Evidence gathering initialized (header capture + frame chain)');

    // Step 7e: Initialize detection reporter (cloud telemetry)
    const reporterSettings = await loadCloudTelemetrySettings();
    const extensionVersion = chrome.runtime.getManifest().version;
    detectionReporter = new DetectionReporter({
      apiUrl: configManager.get('ruleApiUrl') || 'https://api.nithic.ai',
      enabled: reporterSettings.detectionReporting !== false,
      shareUrls: reporterSettings.shareUrls !== false,
      screenshotMode: reporterSettings.screenshotMode || 'full',
      deviceId: deviceId,
      extensionVersion: extensionVersion,
      systemContext: systemContext,
      frameChainBuilder: frameChainBuilder
    });
    console.log('✓ Detection reporter initialized');

    // Step 7f: Initialize AI Visibility Reporter
    aiVisibilityReporter = new AIVisibilityReporter({
      apiUrl: configManager.get('dashboardApiUrl') || 'https://api.portal.nithic.ai',
      enabled: true,
      deviceId: deviceId,
      extensionVersion: extensionVersion
    });

    // Step 7f-ii: Initialize Identity Manager
    identityManager = new IdentityManager({
      apiUrl: configManager.get('dashboardApiUrl') || 'https://api.portal.nithic.ai',
      enabled: true
    });

    // Step 7f-iii: Initialize Content Detector
    contentDetector = new ContentDetector({
      cloudUrl: resolveCloudUrl(reporterSettings.cloudAnalysisUrl),
      dashboardUrl: configManager.get('dashboardApiUrl') || 'https://api.portal.nithic.ai',
      enabled: true,
      deviceId: deviceId
    });

    // Step 7g: Initialize cloud query client
    cloudQueryClient = new CloudQueryClient({
      cloudUrl: resolveCloudUrl(reporterSettings.cloudAnalysisUrl),
      enabled: reporterSettings.cloudAnalysis || false,
      deviceId: deviceId,
      extensionVersion: extensionVersion
    });
    console.log('✓ Cloud query client initialized');

    // NOTE: we deliberately do NOT pre-warm the on-device model on startup —
    // the ~153MB download happens lazily on the first page that actually has
    // text to analyze (via the content scan → inferTextLocal path), so a fresh
    // install doesn't pull weights before the user browses anywhere.

    // Step 8: Initialize enterprise integrations (if enabled)
    if (configManager.get('enterpriseMode')) {
      console.log('Enterprise mode enabled - initializing integrations...');

      // MDM Integration
      if (configManager.get('mdmEnabled')) {
        mdmIntegration = new MDMIntegration(configManager.config);
        await mdmIntegration.initialize();
        console.log('✓ MDM integration initialized');
      }

      // Admin Console
      if (configManager.get('adminConsoleEnabled')) {
        adminConsole = new AdminConsoleConnector(configManager.config);
        await adminConsole.initialize();
        console.log('✓ Admin console connector initialized');
      }

      // SIEM Reporter
      if (configManager.get('siemEnabled')) {
        siemReporter = new SIEMReporter(configManager.config);
        await siemReporter.initialize();
        console.log('✓ SIEM reporter initialized');
      }

      // ClickHouse Reporter
      if (configManager.get('clickhouseEnabled')) {
        clickhouseReporter = new ClickHouseReporter(configManager.config);
        await clickhouseReporter.initialize();
        console.log('✓ ClickHouse reporter initialized');
      }
    }

    // Step 9: Load legacy stats for backward compatibility
    await loadLegacyStats();

    extensionState.initialized = true;
    console.log('✅ PhishProtect Enterprise initialized successfully');

  } catch (error) {
    console.error('❌ Failed to initialize PhishProtect:', error);
    extensionState.initialized = false;
  }
}

/**
 * Load legacy stats from storage for backward compatibility
 */
async function loadLegacyStats() {
  return new Promise((resolve) => {
    chrome.storage.local.get(['stats'], (result) => {
      if (result.stats) {
        stats = result.stats;
        console.log('Loaded legacy stats from storage:', stats);
      }
      resolve();
    });
  });
}

/**
 * Load cloud telemetry settings from storage
 */
async function loadCloudTelemetrySettings() {
  return new Promise((resolve) => {
    chrome.storage.local.get(['cloudTelemetrySettings'], (result) => {
      resolve(result.cloudTelemetrySettings || {
        detectionReporting: true,
        shareUrls: true,
        screenshotMode: 'full',
        cloudAnalysis: false,
        cloudAnalysisUrl: 'https://cloud.nithic.ai'
      });
    });
  });
}

/**
 * Generate or load a persistent device ID (UUID stored in chrome.storage.local)
 */
async function getOrCreateDeviceId() {
  return new Promise((resolve) => {
    chrome.storage.local.get(['phishprotect_device_id'], (result) => {
      if (result.phishprotect_device_id) {
        resolve(result.phishprotect_device_id);
      } else {
        // Generate a new UUID
        const id = crypto.randomUUID();
        chrome.storage.local.set({ phishprotect_device_id: id }, () => {
          resolve(id);
        });
      }
    });
  });
}

/**
 * Collect system context: browser, OS, extension config
 */
async function collectSystemContext() {
  const manifest = chrome.runtime.getManifest();
  const platformInfo = await chrome.runtime.getPlatformInfo();

  // Parse user agent for browser info
  const ua = navigator.userAgent;
  let browser = 'Unknown';
  const chromeMatch = ua.match(/Chrome\/(\d+\.\d+\.\d+\.\d+)/);
  const edgeMatch = ua.match(/Edg\/(\d+\.\d+\.\d+\.\d+)/);
  const braveMatch = ua.match(/Brave\/(\d+)/);

  if (edgeMatch) {
    browser = `Edge ${edgeMatch[1]}`;
  } else if (braveMatch) {
    browser = `Brave`;
  } else if (chromeMatch) {
    browser = `Chrome ${chromeMatch[1]}`;
  }

  return {
    browser: browser,
    os: `${platformInfo.os} ${platformInfo.arch}`,
    platform: platformInfo.os,
    arch: platformInfo.arch,
    extension_version: manifest.version,
    extension_name: manifest.name,
    manifest_version: manifest.manifest_version,
    user_agent: ua
  };
}

/**
 * Save legacy stats to storage
 */
function saveLegacyStats() {
  chrome.storage.local.set({ stats }, () => {
    if (chrome.runtime.lastError) {
      console.error('Failed to save stats:', chrome.runtime.lastError);
    }
  });
}

/**
 * Notify popup of stats update
 */
function notifyStatsUpdate() {
  chrome.runtime.sendMessage({
    type: 'statsUpdated',
    data: stats
  }).catch(() => {
    // Ignore errors if popup is not open
  });
}

/**
 * Analyze URL request using new analyzer engine
 */
async function analyzeUrlRequest(details) {
  if (!extensionState.initialized || !extensionState.enabled) {
    return; // Non-blocking, no return value needed
  }

  // v3 exfil-channel: inspect outbound sub-resource requests for credentials /
  // auth tokens leaking in the URL. Report-only; deduped per tab+host+rule so a
  // repeated beacon reports once. Skip main-frame navigations (handled by the
  // navigation url channel).
  if (details.type !== 'main_frame' && details.url && details.url.indexOf('?') !== -1) {
    try {
      let crossOrigin = false;
      try {
        const reqOrigin = new URL(details.url).origin;
        crossOrigin = details.initiator ? reqOrigin !== details.initiator : false;
      } catch (_) {}
      const v3x = evalV3Exfil(details.url, crossOrigin, details.type, details.tabId, details.frameId || 0);
      if (v3x && v3x.findings.length > 0) {
        let host = ''; try { host = new URL(details.url).host; } catch (_) {}
        const seen = _v3ExfilReported.get(details.tabId) || new Set();
        const fresh = v3x.findings.filter(f => !seen.has(`${f.ruleId}|${host}`));
        if (fresh.length > 0) {
          fresh.forEach(f => seen.add(`${f.ruleId}|${host}`));
          _v3ExfilReported.set(details.tabId, seen);
          console.log(`[Nithic][v3] exfil: ${fresh.map(f => f.ruleId).join(', ')} → ${host}`);
          reportV3Findings({ ...v3x, findings: fresh }, details.tabId, details.url, details.frameId || 0, 'v3-exfil');
        }
      }
    } catch (_) {}
  }

  // v3 cryptojacking: any outbound request to a known miner library / pool.
  if (details.type !== 'main_frame' && details.url) {
    try {
      const v3m = evalV3Cryptojacking(details.url, details.tabId, details.frameId || 0);
      if (v3m && v3m.findings.length > 0) {
        let host = ''; try { host = new URL(details.url).host; } catch (_) {}
        const seen = _v3ExfilReported.get(details.tabId) || new Set();
        if (!seen.has(`miner|${host}`)) {
          seen.add(`miner|${host}`); _v3ExfilReported.set(details.tabId, seen);
          console.log(`[Nithic][v3] cryptojacking: ${host}`);
          reportV3Findings(v3m, details.tabId, details.url, details.frameId || 0, 'v3-cryptojacking');
        }
      }
    } catch (_) {}
  }

  // v3 private-network: a public page reaching into the local network.
  if (details.type !== 'main_frame' && details.url && details.initiator) {
    try {
      let initHost = ''; try { initHost = new URL(details.initiator).hostname; } catch (_) {}
      if (initHost && isPublicHost(initHost)) {
        const v3p = evalV3PrivateNetwork(details.url, true, details.tabId, details.frameId || 0);
        if (v3p && v3p.findings.length > 0) {
          let host = ''; try { host = new URL(details.url).host; } catch (_) {}
          const seen = _v3ExfilReported.get(details.tabId) || new Set();
          const fresh = v3p.findings.filter(f => !seen.has(`${f.ruleId}|${host}`));
          if (fresh.length > 0) {
            fresh.forEach(f => seen.add(`${f.ruleId}|${host}`));
            _v3ExfilReported.set(details.tabId, seen);
            console.log(`[Nithic][v3] private-network: ${initHost} → ${host}`);
            reportV3Findings({ ...v3p, findings: fresh }, details.tabId, details.url, details.frameId || 0, 'v3-private-network');
          }
        }
      }
    } catch (_) {}
  }

  const startTime = performance.now();

  try {
    // Analyze URL with analyzer engine
    const detection = await analyzerEngine.analyzeUrl(details.url, {
      tabId: details.tabId,
      frameId: details.frameId
    });

    // Record performance
    const latency = performance.now() - startTime;
    telemetryCollector.recordPerformance('url-analysis', latency, detection.matched);

    // Always store the latest analysis result for this tab (even clean ones)
    if (details.tabId && details.tabId !== -1) {
      const existing = tabDetections.get(details.tabId);
      if (!existing || detection.matched) {
        // For matched detections, accumulate findings; for clean, store baseline
        const prev = existing || { url: details.url, detections: [], findings: [], riskScore: 0, riskLevel: 'none', action: 'allow', timestamp: Date.now() };
        if (detection.matched) {
          prev.detections.push(detection);
          prev.findings = prev.findings.concat(detection.findings || []);
          if (detection.riskScore > prev.riskScore) {
            prev.riskScore = detection.riskScore;
            prev.riskLevel = detection.riskLevel;
          }
        }
        prev.url = details.url;
        prev.timestamp = Date.now();
        tabDetections.set(details.tabId, prev);
      }
    }

    if (!detection.matched) {
      return; // Non-blocking, no return value needed
    }

    // Evaluate policy
    const decision = policyManager.evaluateDetection(detection);

    // Store the policy action for this tab
    if (details.tabId && details.tabId !== -1) {
      const tabState = tabDetections.get(details.tabId);
      if (tabState) {
        const actionPriority = { block: 3, quarantine: 2, warn: 1, allow: 0 };
        if ((actionPriority[decision.action] || 0) > (actionPriority[tabState.action] || 0)) {
          tabState.action = decision.action;
        }
      }
    }

    // Update toolbar icon to reflect new detection state
    if (details.tabId && details.tabId !== -1) {
      updateTabIcon(details.tabId, details.url);
    }

    // Record telemetry
    telemetryCollector.recordDetection(detection, { tabId: details.tabId });
    telemetryCollector.recordAction(decision.action);

    // Record audit log
    await auditLogger.logDecision(decision, {
      policyId: policyManager.getActivePolicyId(),
      tabId: details.tabId
    });

    // Report to enterprise systems
    if (siemReporter) {
      await siemReporter.reportDetection(detection, { deviceId: configManager.get('deviceId') });
      await siemReporter.reportPolicyDecision(decision, { policyId: policyManager.getActivePolicyId() });
    }

    if (clickhouseReporter) {
      clickhouseReporter.reportDetection(detection, { deviceId: configManager.get('deviceId') });
      clickhouseReporter.reportPolicyDecision(decision, { policyId: policyManager.getActivePolicyId() });
    }

    // v3: Attach feature store snapshot to detection for cloud/telemetry
    if (featureStore && featureStore.hasTab(details.tabId)) {
      detection.features = featureStore.getSnapshot(details.tabId);
    }

    // Report ALL detections with findings to cloud telemetry
    if (detection.findings && detection.findings.length > 0) {
      console.log(`[Nithic] URL detection: ${detection.findings.length} finding(s) on ${details.url?.slice(0, 80)} - action=${decision.action} - reporter=${!!detectionReporter}`);
      if (detectionReporter) {
        detectionReporter.reportBlockingDetection(
          detection, decision, details.tabId, details.url, details.frameId || 0
        ).catch(err => {
          console.error('[Nithic] Detection reporter error:', err.message);
        });
      } else {
        console.warn('[Nithic] reportBlockingDetection skipped — reporter not initialized');
      }
    }

    // Handle policy action
    if (decision.action === 'block') {
      stats.blockedUrls++;
      saveLegacyStats();
      notifyStatsUpdate();

      // Store last detection for blocked page
      lastDetection = {
        url: details.url,
        detection,
        decision,
        timestamp: Date.now()
      };

      console.log('🚫 Blocked URL:', {
        riskScore: detection.riskScore,
        findingCount: detection.findings.length,
        action: decision.action
      });

      // Note: Actual blocking now done by declarativeNetRequest
      // This is just for logging/telemetry
    } else if (decision.action === 'warn') {
      // Show warning but allow
      chrome.tabs.sendMessage(details.tabId, {
        type: 'showWarning',
        data: {
          message: decision.enforcement.message,
          riskScore: detection.riskScore,
          findings: privacyFilter.filterDetection(detection)
        }
      }).catch(() => {});

      // For medium-risk / warn detections, query cloud analysis service
      if (cloudQueryClient && cloudQueryClient.shouldEscalate(detection, decision)) {
        cloudQueryClient.queryCloud(details.url, detection).then(cloudResult => {
          if (cloudResult.determination === 'malicious' && cloudResult.confidence > 0.7) {
            // Upgrade to block based on cloud analysis
            console.log('🔄 Cloud escalation: upgrading warn to block', cloudResult);
            stats.blockedUrls++;
            saveLegacyStats();
            notifyStatsUpdate();

            lastDetection = {
              url: details.url,
              detection,
              decision: { ...decision, action: 'block', cloudEscalated: true },
              timestamp: Date.now()
            };

            // Report escalated detection
            if (detectionReporter) {
              detectionReporter.reportBlockingDetection(
                detection, { ...decision, action: 'block', cloudEscalated: true },
                details.tabId, details.url, details.frameId || 0
              ).catch(() => {});
            }
          }
        }).catch(err => {
          console.error('Cloud query error:', err.message);
        });
      }
    }

  } catch (error) {
    console.error('Error analyzing URL:', error);
  }
}

/**
 * Analyze resource request
 */
async function analyzeResourceRequest(details) {
  if (!extensionState.initialized || !extensionState.enabled) {
    return; // Non-blocking
  }

  const startTime = performance.now();

  try {
    // Analyze resource
    const detection = await analyzerEngine.analyzeResource(details);

    // Record performance
    const latency = performance.now() - startTime;
    telemetryCollector.recordPerformance('resource-analysis', latency, detection.matched);

    if (!detection.matched) {
      return; // Non-blocking
    }

    // Evaluate policy
    const decision = policyManager.evaluateDetection(detection);

    // Record telemetry and audit
    telemetryCollector.recordDetection(detection, { tabId: details.tabId });
    telemetryCollector.recordAction(decision.action);
    await auditLogger.logDecision(decision, { policyId: policyManager.getActivePolicyId() });

    // Report to enterprise systems
    if (clickhouseReporter) {
      clickhouseReporter.reportDetection(detection);
      clickhouseReporter.reportPolicyDecision(decision);
    }

    // Handle policy action
    if (decision.action === 'block' || decision.action === 'quarantine') {
      stats.suspiciousResources++;
      saveLegacyStats();
      notifyStatsUpdate();

      console.log('🚫 Blocked resource:', {
        type: details.type,
        riskScore: detection.riskScore,
        action: decision.action
      });

      // Note: Actual blocking done by declarativeNetRequest
    }

  } catch (error) {
    console.error('Error analyzing resource:', error);
  }
}

/**
 * Handle permission addition
 */
async function handlePermissionAdd(permissions) {
  if (!extensionState.initialized || !extensionState.enabled) {
    return;
  }

  try {
    // Analyze permissions
    const detection = await analyzerEngine.analyzePermissions(permissions);

    if (!detection.matched) {
      return;
    }

    // Evaluate policy
    const decision = policyManager.evaluateDetection(detection);

    // Record telemetry and audit
    telemetryCollector.recordDetection(detection);
    telemetryCollector.recordAction(decision.action);
    await auditLogger.logDecision(decision, { policyId: policyManager.getActivePolicyId() });

    // Update stats
    stats.permissionRequests++;
    saveLegacyStats();
    notifyStatsUpdate();

    // Show warning if needed
    if (decision.action === 'warn' || decision.action === 'block') {
      console.warn('⚠️ Suspicious permissions detected:', {
        permissions: permissions.permissions,
        riskScore: detection.riskScore,
        action: decision.action
      });
    }

  } catch (error) {
    console.error('Error analyzing permissions:', error);
  }
}

/**
 * Handle messages from content scripts and popup
 */
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  // On-device engine status pushed from the offscreen document. Cache it so
  // the popup can show download progress / device (webgpu vs wasm).
  if (message && message.target === 'nithic-bg' && message.type === 'offscreen-status') {
    const { target, type, ...snap } = message;
    _engineStatus = { ..._engineStatus, ...snap };
    return false; // no response
  }

  // Popup/options asks us to start loading the on-device model. Returns the
  // real outcome so the panel can show progress or the actual error.
  if (message.type === 'warmupEngine') {
    warmupLocalEngine().then(r => sendResponse(r)).catch(e => sendResponse({ ok: false, error: String(e?.message || e) }));
    return true;
  }

  // Diagnostics panel controls — relay to the offscreen document.
  if (message.type === 'engineControl') {
    (async () => {
      try {
        if (!chrome.offscreen) { sendResponse({ ok: false, error: 'offscreen API unavailable on this browser' }); return; }
        // action: 'reload' | 'checkUpdates' | 'setDevice'
        const fwd = { target: 'nithic-offscreen', type: message.action };
        if (message.action === 'reload') fwd.clearCache = !!message.clearCache;
        if (message.action === 'setDevice') fwd.device = message.device;
        const resp = await sendToOffscreen(fwd);
        sendResponse(resp || { ok: false, error: 'no response from engine' });
      } catch (e) {
        sendResponse({ ok: false, error: String(e && e.message || e) });
      }
    })();
    return true;
  }

  // Popup queries current engine + load status. Prefer a live query to the
  // offscreen document (its state survives SW restarts); fall back to cache.
  if (message.type === 'getEngineStatus') {
    (async () => {
      const engine = await getDetectionEngine();
      let status = _engineStatus;
      // For local/hybrid, ensure the offscreen doc exists and query it live.
      // This makes simply opening the diagnostics panel bring the engine up and
      // surface any creation/load error.
      if (chrome.offscreen && engine !== 'cloud') {
        try {
          const live = await sendToOffscreen({ target: 'nithic-offscreen', type: 'status' }, 8000);
          if (live && live.state) { status = { ..._engineStatus, ...live }; _engineStatus = status; }
        } catch (e) {
          status = { ..._engineStatus, state: 'error', error: 'engine not responding: ' + (e?.message || e) };
        }
      }
      sendResponse({ engine, ...status, available: !!chrome.offscreen });
    })();
    return true;
  }

  // Legacy stats API (backward compatibility)
  if (message.type === 'getStats') {
    sendResponse(stats);
    return true;
  }

  // Legacy analysis API
  if (message.type === 'GET_ANALYSIS') {
    sendResponse({
      blockedUrls: stats.blockedUrls,
      suspiciousResources: stats.suspiciousResources,
      permissionRequests: stats.permissionRequests
    });
    return true;
  }

  // Identity detected from content script (login form, OAuth)
  if (message.type === 'identity_detected') {
    if (identityManager && message.data.identityDomain) {
      identityManager.setIdentity(
        message.data.site,
        message.data.identityDomain,
        message.data.method || 'form_login'
      );
    }
    return false;
  }

  // AI Visibility: policy check before allowing action
  if (message.type === 'ai_visibility_policy_check') {
    if (identityManager) {
      const result = identityManager.evaluatePolicy(
        message.data.aiService,
        message.data.action,
        message.data.siteDomain
      );
      sendResponse(result);
    } else {
      sendResponse({ allowed: true });
    }
    return true; // async sendResponse
  }

  // Content detection: scan text for AI generation
  if (message.type === 'content_scan_text') {
    const tabId = sender?.tab?.id;
    if (contentDetector) {
      contentDetector.analyzeText(message.data.text, {
        url: message.data.url,
        domain: message.data.domain,
        aiService: message.data.aiService
      }).then(result => {
        if (result) {
          // Store per-tab result
          if (tabId) {
            if (!tabContentScans.has(tabId)) tabContentScans.set(tabId, { textScans: [], imageScans: [] });
            const scans = tabContentScans.get(tabId);
            scans.textScans.push({
              verdict: result.cloud?.cloudVerdict || result.client?.verdict || 'uncertain',
              confidence: result.cloud?.cloudConfidence || result.client?.confidence || 0,
              timestamp: Date.now()
            });
            // Cap stored scans
            if (scans.textScans.length > 20) scans.textScans = scans.textScans.slice(-20);
            updateTabIcon(tabId).catch(() => {});
          }
          if (result.cloud) {
            console.log(`Content scan: text ${result.cloud.cloudVerdict} (${result.cloud.cloudConfidence})`);
          }
        }
        sendResponse(result);
      }).catch(e => {
        console.error('Content scan text error:', e);
        sendResponse(null);
      });
    } else {
      sendResponse(null);
    }
    return true; // async sendResponse
  }

  // Page scanner progress (passages found / currently analyzing) for the popup.
  if (message.type === 'page_scan_meta') {
    const tabId = sender?.tab?.id;
    if (tabId != null) {
      if (!tabContentScans.has(tabId)) tabContentScans.set(tabId, { textScans: [], imageScans: [] });
      const scans = tabContentScans.get(tabId);
      scans.pageMeta = {
        identified: message.data?.identified || 0,
        analyzing: message.data?.analyzing || 0
      };
    }
    return false; // no response
  }

  // v3 page-content scan (active). Content script sends the visible page text;
  // we run the v3 content-channel rulesets (command-pattern, keyboard-instruction
  // / ClickFix) and report findings as non-blocking detections.
  if (message.type === 'content_scan_v3') {
    const v3 = evalV3Content(message.data?.content || '', sender?.tab?.id, sender?.frameId || 0);
    if (v3 && v3.findings.length > 0) {
      console.log(`[Nithic][v3] page-content: ${v3.findings.length} finding(s) in ${v3.latencyMs.toFixed(2)}ms`,
        v3.findings.map(f => `${f.rulesetId}/${f.ruleId} (${f.severity})`));
      reportV3Findings(v3, sender?.tab?.id, message.data?.url || sender?.tab?.url, sender?.frameId || 0);
      if (telemetryCollector?.recordPerformance) telemetryCollector.recordPerformance('v3-content-eval', v3.latencyMs, true);
    } else if (v3 && telemetryCollector?.recordPerformance) {
      telemetryCollector.recordPerformance('v3-content-eval', v3.latencyMs, false);
    }
    return false; // no response
  }

  // v3 dom-channel (behavioral): cumulative dialog/popup/history event-state
  // from injected/dom-event-sensor.js. Runs the dialog-trap ruleset and reports
  // *new* findings only — the sensor re-sends growing state, so we dedup per tab
  // (per page; cleared on navigation alongside the feature store).
  if (message.type === 'dom_event_state') {
    const domTabId = sender?.tab?.id;
    const v3 = evalV3Dom(message.data?.state, domTabId, sender?.frameId || 0);
    if (v3 && v3.findings.length > 0) {
      const seen = _v3DomReported.get(domTabId) || new Set();
      const fresh = v3.findings.filter(f => !seen.has(`${f.rulesetId}/${f.ruleId}`));
      if (fresh.length > 0) {
        fresh.forEach(f => seen.add(`${f.rulesetId}/${f.ruleId}`));
        _v3DomReported.set(domTabId, seen);
        console.log(`[Nithic][v3] dialog-trap: ${fresh.length} new finding(s) in ${v3.latencyMs.toFixed(2)}ms`,
          fresh.map(f => `${f.rulesetId}/${f.ruleId} (${f.severity})`));
        reportV3Findings({ ...v3, findings: fresh }, domTabId, message.data?.url || sender?.tab?.url, sender?.frameId || 0, 'v3-dom');
      }
      if (telemetryCollector?.recordPerformance) telemetryCollector.recordPerformance('v3-dom-eval', v3.latencyMs, true);
    } else if (v3 && telemetryCollector?.recordPerformance) {
      telemetryCollector.recordPerformance('v3-dom-eval', v3.latencyMs, false);
    }
    return false; // no response
  }

  // v3 clipboard-channel (behavioral): cumulative clipboard event-state from the
  // page-world interceptor. Runs the clipboard-poisoning ruleset (crypto-swap,
  // pastejacking, sensitive-data) and reports *new* findings only (per-tab dedup;
  // cleared on navigation). Command-injection blocking remains synchronous in
  // the interceptor — this is report-only and does not double-report it.
  if (message.type === 'clipboard_state') {
    const clipTabId = sender?.tab?.id;
    const v3 = evalV3ClipboardState(message.data?.state, clipTabId, sender?.frameId || 0);
    if (v3 && v3.findings.length > 0) {
      const seen = _v3ClipReported.get(clipTabId) || new Set();
      const fresh = v3.findings.filter(f => !seen.has(`${f.rulesetId}/${f.ruleId}`));
      if (fresh.length > 0) {
        fresh.forEach(f => seen.add(`${f.rulesetId}/${f.ruleId}`));
        _v3ClipReported.set(clipTabId, seen);
        console.log(`[Nithic][v3] clipboard-poisoning: ${fresh.length} new finding(s) in ${v3.latencyMs.toFixed(2)}ms`,
          fresh.map(f => `${f.rulesetId}/${f.ruleId} (${f.severity})`));
        reportV3Findings({ ...v3, findings: fresh }, clipTabId, message.data?.url || sender?.tab?.url, sender?.frameId || 0, 'v3-clipboard');
      }
      if (telemetryCollector?.recordPerformance) telemetryCollector.recordPerformance('v3-clipboard-eval', v3.latencyMs, true);
    } else if (v3 && telemetryCollector?.recordPerformance) {
      telemetryCollector.recordPerformance('v3-clipboard-eval', v3.latencyMs, false);
    }
    return false; // no response
  }

  // v3 dom-structure channel: bounded DOM snapshot from content.js. Runs the
  // browser-in-browser / fake-chrome / clickjacking rulesets and reports new
  // findings only (per-tab dedup; cleared on navigation).
  if (message.type === 'dom_structure') {
    const dsTabId = sender?.tab?.id;
    const v3 = evalV3DomStructure(message.data?.snapshot, dsTabId, sender?.frameId || 0);
    if (v3 && v3.findings.length > 0) {
      const seen = _v3DomStructReported.get(dsTabId) || new Set();
      const fresh = v3.findings.filter(f => !seen.has(`${f.rulesetId}/${f.ruleId}`));
      if (fresh.length > 0) {
        fresh.forEach(f => seen.add(`${f.rulesetId}/${f.ruleId}`));
        _v3DomStructReported.set(dsTabId, seen);
        console.log(`[Nithic][v3] dom-structure: ${fresh.length} new finding(s) in ${v3.latencyMs.toFixed(2)}ms`,
          fresh.map(f => `${f.rulesetId}/${f.ruleId} (${f.severity})`));
        reportV3Findings({ ...v3, findings: fresh }, dsTabId, message.data?.url || sender?.tab?.url, sender?.frameId || 0, 'v3-dom-structure');
      }
      if (telemetryCollector?.recordPerformance) telemetryCollector.recordPerformance('v3-domstruct-eval', v3.latencyMs, true);
    } else if (v3 && telemetryCollector?.recordPerformance) {
      telemetryCollector.recordPerformance('v3-domstruct-eval', v3.latencyMs, false);
    }
    return false; // no response
  }

  // Content detection: batch text scan (passive page-text highlighting).
  // Routed by engine: 'local' (default — on-device, private) runs in the
  // offscreen document; 'cloud' (opt-in / paid) uses the cloud service.
  if (message.type === 'content_scan_text_batch') {
    const tabId = sender?.tab?.id;
    const chunks = message.data?.chunks;
    if (!Array.isArray(chunks)) { sendResponse({ chunks: [] }); return true; }

    (async () => {
      let resultChunks = [];
      const runCloud = async () => {
        if (!contentDetector) return [];
        const r = await contentDetector.analyzeTextBatch(chunks, {
          url: message.data.url, domain: message.data.domain, aiService: message.data.aiService
        });
        return Array.isArray(r?.chunks) ? r.chunks : [];
      };
      try {
        const engine = await getDetectionEngine();
        if (engine === 'cloud') {
          resultChunks = await runCloud();
        } else if (engine === 'hybrid') {
          // Use on-device when the model is ready; otherwise serve from cloud
          // so the user isn't blocked during the first ~153MB download. The
          // download still proceeds in the background.
          if (await isLocalEngineReady()) {
            resultChunks = await inferTextLocal(chunks);
          } else {
            warmupLocalEngine();
            resultChunks = await runCloud();
          }
        } else {
          // local (default) — private, never touches the cloud.
          resultChunks = await inferTextLocal(chunks);
        }
      } catch (e) {
        console.error('Content scan batch text error:', e);
        resultChunks = [];
      }

      if (tabId && resultChunks.length > 0) {
        if (!tabContentScans.has(tabId)) tabContentScans.set(tabId, { textScans: [], imageScans: [] });
        const scans = tabContentScans.get(tabId);
        for (const c of resultChunks) {
          if (!c) continue;
          scans.textScans.push({
            verdict: c.cloudVerdict || c.verdict || 'uncertain',
            confidence: c.cloudConfidence ?? c.confidence ?? 0,
            source: 'page',
            timestamp: Date.now()
          });
        }
        if (scans.textScans.length > 50) scans.textScans = scans.textScans.slice(-50);
        updateTabIcon(tabId).catch(() => {});
      }
      sendResponse({ chunks: resultChunks });
    })();
    return true; // async
  }

  // Content detection: scan image for AI generation / deepfake / manipulation
  if (message.type === 'content_scan_image') {
    const tabId = sender?.tab?.id;
    if (contentDetector) {
      contentDetector.analyzeImage(message.data.imageData, {
        url: message.data.url,
        domain: message.data.domain,
        aiService: message.data.aiService
      }).then(result => {
        if (result) {
          // Store per-tab result
          if (tabId) {
            if (!tabContentScans.has(tabId)) tabContentScans.set(tabId, { textScans: [], imageScans: [] });
            const scans = tabContentScans.get(tabId);
            scans.imageScans.push({
              verdict: result.cloud?.cloudVerdict || result.client?.verdict || 'uncertain',
              confidence: result.cloud?.cloudConfidence || result.client?.confidence || 0,
              manipulationDetected: result.cloud?.manipulationDetected || false,
              manipulationType: result.cloud?.manipulationType || null,
              timestamp: Date.now()
            });
            if (scans.imageScans.length > 20) scans.imageScans = scans.imageScans.slice(-20);
            // Refresh badge: AI count may have changed
            updateTabIcon(tabId).catch(() => {});
          }
          if (result.cloud) {
            console.log(`Content scan: image ${result.cloud.cloudVerdict} (${result.cloud.cloudConfidence})${result.cloud.manipulationDetected ? ' [MANIPULATION]' : ''}`);
          }
        }
        sendResponse(result);
      }).catch(e => {
        console.error('Content scan image error:', e);
        sendResponse(null);
      });
    } else {
      sendResponse(null);
    }
    return true; // async sendResponse
  }

  // AI Visibility: user action on AI domain (enrich with identity)
  if (message.type === 'ai_visibility_event') {
    if (aiVisibilityReporter) {
      const eventData = { ...message.data };
      // Enrich with identity if available
      if (identityManager) {
        const identity = identityManager.getIdentityForHost(eventData.domain);
        if (identity) {
          eventData.identityDomain = identity.identityDomain;
          eventData.identityType = identity.identityType;
        }
      }
      aiVisibilityReporter.record(eventData);
    }
    return false;
  }

  // Clipboard alerts — clickfix-style copy/paste blocks
  // Build a detection event and report to telemetry so it shows up in the
  // portal's Hunting / Investigation pages (was previously dead-ending in stats).
  if (message.type === 'suspicious_copy' || message.type === 'suspicious_paste') {
    stats.clipboardAlerts++;
    saveLegacyStats();
    notifyStatsUpdate();

    const direction = message.type === 'suspicious_copy' ? 'copy' : 'paste';
    console.log(`[Nithic] Clipboard ${direction} blocked: pattern=${message.data?.patternId} host=${sender?.tab?.url ? new URL(sender.tab.url).host : '?'} reporter=${!!detectionReporter}`);
    if (detectionReporter && sender?.tab) {
      const tab = sender.tab;
      const data = message.data || {};
      const detection = {
        riskScore: 8,
        riskLevel: 'high',
        // rule_id is a varchar column on telemetry_events — string is fine.
        // pattern_id / technique_id are integer FKs, leave null (no matching rows yet).
        ruleId: data.patternId || ('clipboard-' + direction),
        findings: [{
          // Match the schema older clipboard-injection events used so the
          // portal's Hunting review panel renders the full evidence panel.
          type: data.patternName || ('Suspicious clipboard ' + direction),
          weight: 5,
          severity: 'critical',
          category: 'social-engineering',
          subcategory: 'command-pattern',
          detector: 'clipboard-monitor',
          rule_id: data.patternId || ('clipboard-' + direction),
          pattern: data.patternId || null,
          matched: data.patternName || null,
          description: 'Suspicious command pattern detected in clipboard (' + direction + ')',
          evidence: {
            threat_types: [data.patternName].filter(Boolean),
            clipboard_source: data.clipboardSource || 'user-' + direction,
            clipboard_content: data.fullText || data.textPreview || null,
            text_length: data.length || 0,
            page_url: data.pageUrl || tab.url,
            page_title: data.pageTitle || tab.title,
            page_host: data.pageHost || null,
            selection_type: data.selectionType || null,
            active_tag: data.activeTag || null,
            inline_scripts: data.inlineScripts || [],
            timestamp: data.timestamp || Date.now()
          }
        }],
        metadata: {
          detectorsRun: 1,
          detectorsMatched: 1,
          targetType: 'clipboard',
          latency: 0
        }
      };
      const decision = { action: 'block', reason: 'suspicious-clipboard-' + direction };
      detectionReporter.reportBlockingDetection(detection, decision, tab.id, tab.url, sender.frameId || 0)
        .catch(err => console.error('Clipboard telemetry error:', err.message));
    }
    return false;
  }

  // Clipboard injection blocked (from injected script)
  if (message.type === 'clipboard_injection_blocked') {
    stats.clipboardAlerts++;
    saveLegacyStats();
    notifyStatsUpdate();

    console.log('🚫 Clipboard injection blocked:', {
      threats: message.data.threats,
      source: message.data.source,
      timestamp: message.data.timestamp
    });

    // Record in telemetry for risk distribution
    const riskScore = message.data.threats.length * 5;
    const detection = {
      detector: 'clipboard-injection',
      matched: true,
      findings: message.data.threats.map(t => ({
        detector: 'clipboard-injection',
        type: t.type,
        severity: t.severity,
        weight: 5,
        category: 'data-theft'
      })),
      riskScore: riskScore,
      riskLevel: riskScore >= 15 ? 'critical' : riskScore >= 10 ? 'high' : riskScore >= 5 ? 'medium' : 'low',
      // Attach clipboard-specific evidence for telemetry
      clipboardEvidence: {
        source: message.data.source,
        contentPreview: message.data.contentPreview || null,
        threatTypes: message.data.threats.map(t => t.type),
        stackTrace: message.data.stackTrace || null,
        sourceSnippets: message.data.sourceSnippets || null,
        inlineScripts: message.data.inlineScripts || null,
        dynamicScripts: message.data.dynamicScripts || null
      }
    };

    // CVE escalation: boost risk score if site has known vulnerabilities
    const clipTabId = sender?.tab?.id;
    if (clipTabId) {
      const clipVulns = tabDetections.get(clipTabId)?.vulnerabilities;
      if (clipVulns?.summary?.critical > 0) {
        detection.riskScore = riskScore + 10;
        detection.riskLevel = detection.riskScore >= 15 ? 'critical' : detection.riskScore >= 10 ? 'high' : detection.riskScore >= 5 ? 'medium' : 'low';
        detection.cveEscalation = clipVulns.summary;
        detection.vulnerabilities = clipVulns.summary;
      } else if (clipVulns?.summary?.high > 0) {
        detection.riskScore = riskScore + 5;
        detection.riskLevel = detection.riskScore >= 15 ? 'critical' : detection.riskScore >= 10 ? 'high' : detection.riskScore >= 5 ? 'medium' : 'low';
        detection.cveEscalation = clipVulns.summary;
        detection.vulnerabilities = clipVulns.summary;
      }
    }

    // v3: Attach feature store snapshot for cloud/telemetry
    if (featureStore && clipTabId && featureStore.hasTab(clipTabId)) {
      detection.features = featureStore.getSnapshot(clipTabId);
    }

    // Store clipboard detection in per-tab state
    if (clipTabId) {
      const prev = tabDetections.get(clipTabId) || { url: sender?.tab?.url || '', detections: [], findings: [], riskScore: 0, riskLevel: 'none', action: 'allow', timestamp: Date.now() };
      prev.detections.push(detection);
      prev.findings = prev.findings.concat(detection.findings || []);
      if (riskScore > prev.riskScore) {
        prev.riskScore = riskScore;
        prev.riskLevel = detection.riskLevel;
      }
      prev.action = 'block';
      prev.timestamp = Date.now();
      tabDetections.set(clipTabId, prev);

      // Update toolbar icon to reflect clipboard threat
      updateTabIcon(clipTabId, sender?.tab?.url);
    }

    if (telemetryCollector) {
      telemetryCollector.recordDetection(detection, {
        source: message.data.source,
        timestamp: message.data.timestamp
      });
    }

    // NOTE: clipboard enforcement stays on the page-world interceptor — it
    // blocks synchronously, which a background async eval can't. The v3
    // clipboard ruleset stays validated/loaded for a future move of clipboard
    // enforcement onto the engine; we don't double-report it here.

    // Log to audit trail
    if (auditLogger) {
      const threatTypes = message.data.threats.map(t => t.type).join(', ');
      auditLogger.logUserAction('clipboard-injection-blocked', {
        threats: message.data.threats,
        source: message.data.source,
        timestamp: message.data.timestamp,
        threatSummary: threatTypes
      }).catch(error => {
        console.error('Failed to log clipboard injection block:', error);
      });
    }

    // Report to cloud telemetry
    if (detectionReporter) {
      const decision = { action: 'block', reason: 'clipboard-injection' };
      const tabUrl = sender?.tab?.url || sender?.url || null;
      detectionReporter.reportBlockingDetection(
        detection, decision, sender?.tab?.id, tabUrl, sender?.frameId || 0
      ).catch(error => {
        console.error('Failed to report clipboard detection:', error);
      });
    }

    // Report to enterprise systems
    if (siemReporter) {
      siemReporter.reportDetection(detection, {
        source: message.data.source,
        deviceId: configManager.get('deviceId')
      }).catch(error => {
        console.error('Failed to report clipboard injection to SIEM:', error);
      });
    }

    // Escalate LOLBin clipboard detections to cloud for real-time learning
    if (cloudQueryClient && cloudQueryClient.enabled) {
      const tabUrl = sender?.tab?.url || sender?.url || '';
      cloudQueryClient.queryCloud(tabUrl, detection, {
        source: 'clipboard',
        clipboardContent: message.data.contentPreview,
        threatTypes: message.data.threats.map(t => t.type)
      }).then(cloudResult => {
        console.log('Cloud clipboard analysis:', cloudResult);
      }).catch(err => {
        console.error('Cloud clipboard query error:', err.message);
      });
    }

    return false;
  }

  // False positive report (from user)
  if (message.type === 'false_positive_report') {
    console.log('📝 False positive report received:', {
      threats: message.data.threats,
      contentHash: message.data.contentHash,
      timestamp: message.data.timestamp
    });

    // Log to audit trail
    if (auditLogger) {
      const threatTypes = message.data.threats.map(t => t.type).join(', ');
      auditLogger.logUserAction('false-positive-report', {
        threats: message.data.threats,
        source: message.data.source,
        contentHash: message.data.contentHash,
        contentLength: message.data.contentLength,
        timestamp: message.data.timestamp,
        url: message.data.url,
        threatSummary: threatTypes
      }).catch(error => {
        console.error('Failed to log false positive report:', error);
      });
    }

    // Send to telemetry for analysis
    if (telemetryReporter && telemetryReporter.enabled) {
      telemetryReporter.reportDetection({
        detector: 'false-positive-report',
        matched: true,
        findings: [{
          detector: 'user-feedback',
          type: 'false-positive-report',
          severity: 'low',
          weight: 1,
          description: 'User reported false positive',
          metadata: {
            originalThreats: message.data.threats,
            contentHash: message.data.contentHash
          }
        }],
        riskScore: 0,
        riskLevel: 'low'
      }, {
        url: message.data.url
      }, {
        userFeedback: true,
        feedbackType: 'false-positive'
      }).catch(error => {
        console.error('Failed to send false positive report to telemetry:', error);
      });
    }

    return false;
  }

  // Frame context report from content script (evidence gathering)
  if (message.type === 'frame_context_report') {
    if (frameChainBuilder && sender.tab) {
      frameChainBuilder.mergeFrameContextReport(
        sender.tab.id, sender.frameId, message.data
      );
    }
    return false;
  }

  // Technology fingerprint report from content script
  if (message.type === 'tech_fingerprint_report') {
    if (frameChainBuilder && sender.tab) {
      frameChainBuilder.mergeTechFingerprint(
        sender.tab.id, sender.frameId, message.data
      );
    }
    // Fire-and-forget CVE lookup for main frame only
    if (cloudQueryClient && sender.tab && sender.frameId === 0) {
      cloudQueryClient.lookupVulnerabilities(message.data)
        .then(cveResult => {
          if (cveResult.summary?.total_cves > 0) {
            const state = tabDetections.get(sender.tab.id);
            if (state) state.vulnerabilities = cveResult;
          }
        })
        .catch(() => {}); // Non-blocking, best-effort
    }
    return false;
  }

  // Suspicious form/link (legacy)
  if (message.type === 'suspicious_form' || message.type === 'suspicious_link') {
    console.log('Suspicious content detected:', message.data);
    return false;
  }

  // Enhanced telemetry API
  if (message.type === 'getTelemetry') {
    const telemetry = {
      session: telemetryCollector.getSessionStats(),
      detectors: telemetryCollector.getTopDetectors(10),
      techniques: telemetryCollector.getTopTechniques(10),
      riskDistribution: telemetryCollector.getRiskDistribution(),
      actionDistribution: telemetryCollector.getActionDistribution()
    };
    sendResponse(telemetry);
    return true;
  }

  // Per-tab status API (for popup dual-shield display)
  // Reads from accumulated state — no new analysis triggered
  if (message.type === 'getTabStatus') {
    (async () => {
      try {
        // Determine tab ID
        let tabId = message.tabId;
        if (!tabId && sender.tab) tabId = sender.tab.id;
        if (!tabId) {
          const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
          tabId = activeTab?.id;
        }
        if (!tabId) {
          sendResponse({ error: 'no-tab', url: '', security: { overall: 'unknown' }, aiContent: { overall: 'unknown' } });
          return;
        }

        // Get tab URL from Chrome
        let tabUrl = '';
        try {
          const tab = await chrome.tabs.get(tabId);
          tabUrl = tab.url || tab.pendingUrl || '';
        } catch (e) {
          tabUrl = '';
        }

        const isHttps = tabUrl.startsWith('https://');
        const isHttp = tabUrl.startsWith('http://');
        const isWebPage = isHttps || isHttp;

        // --- Read from accumulated per-tab state ---
        const tabState = tabDetections.get(tabId) || null;
        const riskScore = tabState?.riskScore || 0;
        const riskLevel = tabState?.riskLevel || 'none';
        const findings = tabState?.findings || [];
        const findingsCount = findings.length;
        const tabAction = tabState?.action || 'allow';

        // --- Connection Security (derived from URL protocol) ---
        let connection = { level: 'unknown', details: 'Not a web page' };
        if (isHttps) connection = { level: 'secure', details: 'HTTPS connection' };
        else if (isHttp) connection = { level: 'insecure', details: 'HTTP \u2014 not encrypted' };

        // --- Domain Reputation (derived from FeatureStore signals) ---
        let reputation = { level: 'unknown', details: isWebPage ? 'No reputation data' : 'N/A' };
        if (featureStore && featureStore.hasTab(tabId)) {
          const snapshot = featureStore.getSnapshot(tabId);
          const repKeys = Object.entries(snapshot).filter(([k]) =>
            k.includes('reputation') || k.includes('domain') || k.includes('blacklist') || k.includes('blocklist')
          );
          if (repKeys.length > 0) {
            const hasMalicious = repKeys.some(([, f]) => f.value === true || f.value === 'malicious');
            const hasSuspicious = repKeys.some(([, f]) => f.value === 'suspicious');
            if (hasMalicious) reputation = { level: 'malicious', details: 'Domain flagged as malicious' };
            else if (hasSuspicious) reputation = { level: 'suspicious', details: 'Suspicious indicators found' };
            else reputation = { level: 'trusted', details: 'No negative signals' };
          }
        }

        // --- Server Fingerprint (tech stack + CVE enrichment) ---
        let fingerprint = { level: 'normal', details: 'Standard configuration' };
        if (isWebPage) {
          // Build tech stack summary + flat tech array from frame chain
          let techSummary = '';
          let techItems = []; // flat array of {name, version, category} for popup rendering
          try {
            const chain = frameChainBuilder ? frameChainBuilder.getChain(tabId, 0) : [];
            const techData = chain.length > 0 ? chain[0].technologies : null;
            if (techData) {
              const parts = [];
              // CMS: array of {name, version, ...}
              if (techData.cms && techData.cms.length > 0) {
                for (const c of techData.cms) {
                  techItems.push({ name: c.name, version: c.version || null, category: 'CMS' });
                }
                const c = techData.cms[0];
                parts.push(c.name + (c.version ? ' ' + c.version : ''));
              }
              // Client libraries: array of {name, version, ...}
              if (techData.clientLibs && techData.clientLibs.length > 0) {
                for (const lib of techData.clientLibs) {
                  techItems.push({ name: lib.name, version: lib.version || null, category: 'Library' });
                }
                const lib = techData.clientLibs[0];
                parts.push(lib.name + (lib.version ? ' ' + lib.version : ''));
                if (techData.clientLibs.length > 1) parts.push(`${techData.clientLibs.length - 1} more`);
              }
              // WordPress plugins
              if (techData.wpPlugins && techData.wpPlugins.length > 0) {
                for (const p of techData.wpPlugins) {
                  techItems.push({ name: p.name, version: p.version || null, category: 'Plugin' });
                }
                parts.push(`${techData.wpPlugins.length} plugin${techData.wpPlugins.length !== 1 ? 's' : ''}`);
              }
              // Frameworks
              if (techData.frameworks && techData.frameworks.length > 0) {
                for (const fw of techData.frameworks) {
                  techItems.push({ name: fw.name, version: fw.version || null, category: 'Framework' });
                }
                if (parts.length === 0) {
                  const fw = techData.frameworks[0];
                  parts.push(fw.name + (fw.version ? ' ' + fw.version : ''));
                }
              }
              techSummary = parts.join(' \u00b7 ');
            }
          } catch (e) { /* best effort */ }

          // Check CVE data cached in tabDetections
          const tabVulns = tabState?.vulnerabilities;
          // Tag each tech item with its worst CVE severity
          if (tabVulns?.vulnerabilities?.length > 0) {
            for (const item of techItems) {
              const matching = tabVulns.vulnerabilities.filter(v =>
                v.affected_technology?.name?.toLowerCase() === item.name.toLowerCase()
              );
              if (matching.length > 0) {
                const sevOrder = { critical: 4, high: 3, medium: 2, low: 1 };
                const worst = matching.reduce((w, v) => sevOrder[v.severity] > sevOrder[w.severity] ? v : w, matching[0]);
                item.cveCount = matching.length;
                item.worstSeverity = worst.severity;
              }
            }
          }

          const vulnPayload = tabVulns ? {
            summary: tabVulns.summary,
            vulns: (tabVulns.vulnerabilities || []).slice(0, 20)
          } : null;

          if (tabVulns?.summary?.critical > 0) {
            fingerprint = {
              level: 'vulnerable',
              details: techSummary || 'Vulnerable software detected',
              techStack: techSummary,
              technologies: techItems,
              vulnerabilities: vulnPayload
            };
          } else if (tabVulns?.summary?.high > 0) {
            fingerprint = {
              level: 'vulnerable',
              details: techSummary || 'High-severity CVEs detected',
              techStack: techSummary,
              technologies: techItems,
              vulnerabilities: vulnPayload
            };
          } else if (tabVulns?.summary?.medium > 0 || tabVulns?.summary?.low > 0) {
            fingerprint = {
              level: 'weak',
              details: techSummary || 'Minor vulnerabilities detected',
              techStack: techSummary,
              technologies: techItems,
              vulnerabilities: vulnPayload
            };
          } else if (techSummary) {
            fingerprint = { level: 'normal', details: techSummary, techStack: techSummary, technologies: techItems };
          }
        }

        // --- Content & Behavior (derived from accumulated detections + feature store) ---
        let behavior = { level: isWebPage ? 'clean' : 'unknown', details: isWebPage ? 'No issues detected' : 'N/A', monitoring: isWebPage };

        if (isWebPage) {
          // Check accumulated findings first — these are real detections
          if (findingsCount > 0) {
            const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
            const maxSeverity = findings.reduce((max, f) => {
              return (severityOrder[f.severity] || 0) > (severityOrder[max] || 0) ? f.severity : max;
            }, 'low');

            if (maxSeverity === 'critical' || tabAction === 'block') {
              behavior = { level: 'dangerous', details: `${findingsCount} threat(s) \u2014 ${maxSeverity} severity`, monitoring: true };
            } else if (maxSeverity === 'high' || tabAction === 'warn') {
              behavior = { level: 'suspicious', details: `${findingsCount} threat(s) \u2014 ${maxSeverity} severity`, monitoring: true };
            } else {
              behavior = { level: 'suspicious', details: `${findingsCount} finding(s) detected`, monitoring: true };
            }
          } else {
            // No detections yet — check FeatureStore for behavioral signals
            if (featureStore && featureStore.hasTab(tabId)) {
              const snapshot = featureStore.getSnapshot(tabId);
              const behaviorKeys = Object.entries(snapshot).filter(([k]) =>
                k.includes('clipboard') || k.includes('injection') || k.includes('dialog') ||
                k.includes('command') || k.includes('social') || k.includes('credential') ||
                k.includes('suspicious') || k.includes('phish')
              );
              if (behaviorKeys.length > 0) {
                const hasPositive = behaviorKeys.some(([, f]) => f.value === true);
                if (hasPositive) {
                  behavior = { level: 'suspicious', details: `${behaviorKeys.length} behavioral signal(s)`, monitoring: true };
                }
                // If signals exist but all false, stay clean — they were checked and passed
              }
            }
          }
        }

        // --- Compute overall security level ---
        // Priority: detections/action > sub-indicator levels
        let securityOverall;
        if (tabAction === 'block' || riskLevel === 'critical' || riskLevel === 'high') {
          securityOverall = 'danger';
        } else if (tabAction === 'warn' || riskLevel === 'medium') {
          securityOverall = 'warning';
        } else {
          // Fall back to sub-indicator levels
          const levels = [connection.level, reputation.level, fingerprint.level, behavior.level];
          if (levels.includes('malicious') || levels.includes('dangerous') || levels.includes('insecure')) {
            securityOverall = 'danger';
          } else if (levels.includes('suspicious') || levels.includes('weak') || levels.includes('vulnerable')) {
            securityOverall = 'warning';
          } else if (isWebPage && levels.every(l => l !== 'unknown')) {
            securityOverall = 'safe';
          } else if (isWebPage) {
            securityOverall = 'safe'; // Default web page with no issues
          } else {
            securityOverall = 'unknown';
          }
        }

        sendResponse({
          url: tabUrl,
          security: {
            overall: securityOverall,
            connection,
            reputation,
            fingerprint,
            behavior
          },
          aiContent: (() => {
            const scans = tabContentScans.get(tabId);
            const pm = (scans && scans.pageMeta) || { identified: 0, analyzing: 0 };
            if (!scans || (scans.textScans.length === 0 && scans.imageScans.length === 0)) {
              return {
                overall: 'unknown',
                text: { level: 'unknown', count: 0 },
                textOnPage: { level: 'unknown', count: 0, aiCount: 0, identified: pm.identified, analyzing: pm.analyzing },
                images: { level: 'unknown', count: 0 },
                video: { level: 'unknown', count: 0 },
                authenticityScore: null
              };
            }

            // Text summary
            const textAI = scans.textScans.filter(s => s.verdict === 'ai_generated' || s.verdict === 'mixed' || s.verdict === 'likely_ai');
            const textUncertain = scans.textScans.filter(s => s.verdict === 'uncertain');
            const textLevel = scans.textScans.length === 0 ? 'unknown'
              : textAI.length > 0 ? 'danger'
              : textUncertain.length > 0 ? 'warning' : 'safe';

            // On-page passive scan summary — only the scans the page scanner
            // produced (tagged source:'page'), so it reflects the highlight
            // feature specifically rather than clipboard scans.
            const pageScans = scans.textScans.filter(s => s.source === 'page');
            const pageAI = pageScans.filter(s => s.verdict === 'ai_generated' || s.verdict === 'mixed' || s.verdict === 'likely_ai');
            const pageUncertain = pageScans.filter(s => s.verdict === 'uncertain');
            const pageLevel = pageScans.length === 0 ? 'unknown'
              : pageAI.length > 0 ? 'danger'
              : pageUncertain.length > 0 ? 'warning' : 'safe';

            // Verdict helpers — verdicts come from two paths:
            //   cloud:  ai_generated | manipulated | mixed | uncertain | human
            //   client: likely_ai | uncertain | likely_human
            // We normalize both so a "clean" image counts the same whether the
            // cloud was reachable or only the local heuristic ran.
            const isAi    = (v) => v === 'ai_generated' || v === 'manipulated' || v === 'mixed' || v === 'likely_ai';
            const isHuman = (v) => v === 'human' || v === 'likely_human';

            // Image summary
            const imageAI = scans.imageScans.filter(s => isAi(s.verdict));
            const imageUncertain = scans.imageScans.filter(s => s.verdict === 'uncertain');
            const imageLevel = scans.imageScans.length === 0 ? 'unknown'
              : imageAI.length > 0 ? 'danger'
              : imageUncertain.length > 0 ? 'warning' : 'safe';

            // Authenticity score:
            // human / likely_human → 100, uncertain → 50, ai-family → 0
            const allScans = [...scans.textScans, ...scans.imageScans];
            const scoreSum = allScans.reduce((sum, s) => {
              if (isHuman(s.verdict)) return sum + 100;
              if (s.verdict === 'uncertain') return sum + 50;
              return sum; // ai_generated, manipulated, mixed, likely_ai = 0
            }, 0);
            const authenticityScore = allScans.length > 0 ? Math.round(scoreSum / allScans.length) : null;

            // Overall: ignore 'unknown' categories (not scanned yet ≠ problem)
            const knownLevels = [textLevel, imageLevel].filter(l => l !== 'unknown');
            const overall = knownLevels.length === 0 ? 'unknown'
              : knownLevels.includes('danger') ? 'danger'
              : knownLevels.includes('warning') ? 'warning'
              : 'safe';

            return {
              overall,
              text: {
                level: textLevel,
                count: scans.textScans.length,
                aiCount: textAI.length,
                latestVerdict: scans.textScans.length > 0 ? scans.textScans[scans.textScans.length - 1].verdict : null,
                latestConfidence: scans.textScans.length > 0 ? scans.textScans[scans.textScans.length - 1].confidence : 0
              },
              textOnPage: {
                level: pageLevel,
                count: pageScans.length,
                aiCount: pageAI.length,
                identified: pm.identified,
                analyzing: pm.analyzing
              },
              images: {
                level: imageLevel,
                count: scans.imageScans.length,
                aiCount: imageAI.length,
                uncertainCount: imageUncertain.length,
                manipulated: scans.imageScans.filter(s => s.manipulationDetected).length,
                humanCount: scans.imageScans.filter(s => isHuman(s.verdict)).length,
                latestVerdict: scans.imageScans.length > 0 ? scans.imageScans[scans.imageScans.length - 1].verdict : null,
                latestConfidence: scans.imageScans.length > 0 ? scans.imageScans[scans.imageScans.length - 1].confidence : 0
              },
              video: { level: 'unknown', count: 0 },
              authenticityScore
            };
          })(),
          riskScore,
          riskLevel,
          findingsCount,
          findings: findings.slice(0, 10)
        });
      } catch (err) {
        console.error('getTabStatus error:', err);
        sendResponse({ error: err.message, url: '', security: { overall: 'unknown' }, aiContent: { overall: 'unknown' } });
      }
    })();
    return true;
  }

  // CVE status check (used by content.js form gate)
  if (message.type === 'getCveStatus') {
    const cveTabId = sender?.tab?.id;
    const tabVulns = cveTabId ? tabDetections.get(cveTabId)?.vulnerabilities : null;
    const summary = tabVulns?.summary;
    sendResponse({
      hasCriticalCves: (summary?.critical || 0) > 0 || (summary?.high || 0) > 0,
      totalCves: summary?.total_cves || 0,
      criticalCount: summary?.critical || 0,
      highCount: summary?.high || 0
    });
    return true;
  }

  // Policy management API
  if (message.type === 'getActivePolicy') {
    sendResponse({
      policyId: policyManager.getActivePolicyId(),
      policies: policyManager.getAllPolicies()
    });
    return true;
  }

  // Enterprise status API
  if (message.type === 'getEnterpriseStatus') {
    const status = {
      enterpriseMode: configManager.get('enterpriseMode'),
      mdm: mdmIntegration ? mdmIntegration.exportStatus() : null,
      adminConsole: adminConsole ? adminConsole.getConnectionStatus() : null,
      siem: siemReporter ? siemReporter.getStatistics() : null,
      clickhouse: clickhouseReporter ? clickhouseReporter.getStatistics() : null
    };
    sendResponse(status);
    return true;
  }

  // MDM configuration update
  if (message.type === 'mdm-config-update') {
    console.log('MDM configuration update:', message.category, message.data);

    // Handle configuration updates from MDM
    if (message.category === 'policy' && message.data.url) {
      policyManager.loadEnterprisePolicy().catch(error => {
        console.error('Failed to load MDM policy:', error);
      });
    }

    return false;
  }

  // Admin console configuration update
  if (message.type === 'admin-console-config-update') {
    console.log('Admin console configuration update:', message.category, message.data);
    return false;
  }

  // Get last detection (for blocked page)
  if (message.type === 'getLastDetection') {
    if (lastDetection) {
      sendResponse({
        findings: lastDetection.detection.findings,
        riskScore: lastDetection.detection.riskScore,
        riskLevel: lastDetection.detection.riskLevel,
        url: lastDetection.url,
        timestamp: lastDetection.timestamp
      });
    } else {
      sendResponse(null);
    }
    return true;
  }

  // Report false positive
  if (message.type === 'reportFalsePositive') {
    console.log('📢 False positive reported:', {
      url: message.url,
      riskScore: message.riskScore,
      timestamp: message.timestamp
    });

    // Log to audit
    if (auditLogger) {
      auditLogger.logUserAction('false-positive-report', {
        url: message.url,
        riskScore: message.riskScore,
        reportedAt: message.timestamp
      }).catch(error => {
        console.error('Failed to log false positive report:', error);
      });
    }

    // Report to admin console
    if (adminConsole && adminConsole.isConnected()) {
      adminConsole.reportFalsePositive({
        url: message.url,
        riskScore: message.riskScore,
        deviceId: configManager.get('deviceId'),
        organizationId: configManager.get('organizationId'),
        timestamp: message.timestamp
      }).catch(error => {
        console.error('Failed to report false positive to admin console:', error);
      });
    }

    sendResponse({ success: true });
    return true;
  }

  // HTTP upgraded to HTTPS
  if (message.type === 'httpUpgraded') {
    console.log('🔒 HTTP upgraded to HTTPS:', message.originalUrl, '->', message.httpsUrl);

    // Show success notification
    chrome.notifications.create({
      type: 'basic',
      iconUrl: 'icons/icon48.png',
      title: 'Connection Upgraded',
      message: 'Successfully upgraded to secure HTTPS connection',
      priority: 1
    });

    // Log to audit
    if (auditLogger) {
      auditLogger.logUserAction('http-upgraded', {
        originalUrl: message.originalUrl,
        httpsUrl: message.httpsUrl,
        timestamp: message.timestamp
      }).catch(error => {
        console.error('Failed to log HTTP upgrade:', error);
      });
    }

    sendResponse({ success: true });
    return true;
  }

  // Rule update status (for popup)
  if (message.type === 'getRuleUpdateStatus') {
    if (dynamicRuleUpdater) {
      sendResponse(dynamicRuleUpdater.getStatus());
    } else {
      sendResponse({ currentVersion: null, lastUpdateTime: null, isUpdating: false, stats: {} });
    }
    return true;
  }

  // Force rule update (from popup)
  if (message.type === 'forceRuleUpdate') {
    if (dynamicRuleUpdater) {
      dynamicRuleUpdater.forceUpdate().then(() => {
        sendResponse(dynamicRuleUpdater.getStatus());
      }).catch(error => {
        sendResponse({ error: error.message, ...dynamicRuleUpdater.getStatus() });
      });
    } else {
      sendResponse({ error: 'Dynamic rule updater not initialized' });
    }
    return true;
  }

  // Cloud telemetry settings updated
  if (message.type === 'cloudTelemetrySettingsUpdated') {
    const settings = message.settings;
    if (detectionReporter) {
      detectionReporter.updateConfig({
        enabled: settings.detectionReporting !== false,
        shareUrls: settings.shareUrls !== false,
        screenshotMode: settings.screenshotMode || 'full'
      });
    }
    if (cloudQueryClient) {
      cloudQueryClient.updateConfig({
        enabled: settings.cloudAnalysis || false,
        cloudUrl: resolveCloudUrl(settings.cloudAnalysisUrl)
      });
    }
    sendResponse({ success: true });
    return true;
  }

  // Get cloud telemetry status
  if (message.type === 'getCloudTelemetryStatus') {
    sendResponse({
      detectionReporter: detectionReporter ? detectionReporter.getStatus() : null,
      cloudQueryClient: cloudQueryClient ? cloudQueryClient.getStatus() : null
    });
    return true;
  }

  // User allowed HTTP site
  if (message.type === 'allowHttp') {
    console.log('⚠️ User allowed HTTP site:', message.url);

    // Add to whitelist
    addToHttpWhitelist(message.url);

    // Log to audit
    if (auditLogger) {
      auditLogger.logUserAction('http-allowed', {
        url: message.url,
        timestamp: message.timestamp
      }).catch(error => {
        console.error('Failed to log HTTP allowance:', error);
      });
    }

    sendResponse({ success: true });
    return true;
  }

  return false;
});

// Web request listener for URL analysis (non-blocking, for telemetry only)
// Note: Actual blocking is done via declarativeNetRequest rules
// Not available in Firefox MV3 — guarded with feature check
if (chrome.webRequest?.onBeforeRequest) {
  chrome.webRequest.onBeforeRequest.addListener(
    analyzeUrlRequest,
    { urls: ["<all_urls>"] }
    // Removed "blocking" - not available in Manifest V3 without enterprise install
  );
}

// Permission monitoring
chrome.permissions.onAdded.addListener(handlePermissionAdd);

// v3: FeatureStore lifecycle — create/reset tab state on navigation, clean on tab close
chrome.webNavigation.onCommitted.addListener((details) => {
  if (details.frameId === 0) {
    // Main frame navigation — reset per-tab detection state
    tabDetections.delete(details.tabId);
    tabContentScans.delete(details.tabId);
    _v3DomReported.delete(details.tabId);
    _v3ClipReported.delete(details.tabId);
    _v3DomStructReported.delete(details.tabId);
    _v3ExfilReported.delete(details.tabId);
    // Clear content detector cache so images get re-scanned on refresh
    if (contentDetector) contentDetector.clearCache();
    // Reset feature store
    if (featureStore) featureStore.createTab(details.tabId, details.url);

    // v3 URL channel: typosquatting + lookalike-domain on the new hostname.
    // Report-only; providers self-suppress on real brands.
    try {
      const v3u = evalV3Url(details.url, details.tabId, 0);
      if (v3u && v3u.findings.length > 0) {
        console.log(`[Nithic][v3] url: ${v3u.findings.length} finding(s) in ${v3u.latencyMs.toFixed(2)}ms`,
          v3u.findings.map(f => `${f.rulesetId}/${f.ruleId} (${f.severity})`));
        reportV3Findings(v3u, details.tabId, details.url, 0, 'v3-url');
      }
    } catch (_) {}

    // Update icon to reflect clean/unknown state for new page
    updateTabIcon(details.tabId, details.url);

    // AI Visibility: track if this is an AI domain
    if (aiVisibilityReporter) {
      try {
        const hostname = new URL(details.url).hostname;
        const aiMatch = matchAIDomain(hostname);
        if (aiMatch) {
          aiVisibilityReporter.trackTab(details.tabId, aiMatch.domain, aiMatch.service);
          // Record a login/visit event
          aiVisibilityReporter.record({
            eventType: 'login',
            domain: aiMatch.domain,
            service: aiMatch.service
          });
        } else {
          aiVisibilityReporter.untrackTab(details.tabId);
        }
      } catch (e) {
        // Invalid URL, ignore
      }
    }
  } else {
    // Sub-frame navigation — create frame entry
    if (featureStore) featureStore.createFrame(details.tabId, details.frameId);
  }
});

chrome.tabs.onRemoved.addListener((tabId) => {
  tabDetections.delete(tabId);
  tabContentScans.delete(tabId);
  _v3DomReported.delete(tabId);
  _v3ClipReported.delete(tabId);
  _v3DomStructReported.delete(tabId);
  _v3ExfilReported.delete(tabId);
  if (featureStore) featureStore.removeTab(tabId);
  if (aiVisibilityReporter) aiVisibilityReporter.untrackTab(tabId);
});

// Update toolbar icon when user switches tabs
chrome.tabs.onActivated.addListener(({ tabId }) => {
  updateTabIcon(tabId);
});

// Update toolbar icon when a tab finishes loading (covers redirects, SPA navigations)
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete') {
    updateTabIcon(tabId, tab.url);
  }
});

// HTTP Site Whitelist - Sites user explicitly allowed to visit over HTTP
let httpWhitelist = new Set();

// Load HTTP whitelist from storage
chrome.storage.local.get(['httpWhitelist'], (result) => {
  if (result.httpWhitelist) {
    httpWhitelist = new Set(result.httpWhitelist);
    console.log('Loaded HTTP whitelist:', httpWhitelist.size, 'entries');
  }
});

// Save HTTP whitelist to storage
function saveHttpWhitelist() {
  chrome.storage.local.set({
    httpWhitelist: Array.from(httpWhitelist)
  }, () => {
    if (chrome.runtime.lastError) {
      console.error('Failed to save HTTP whitelist:', chrome.runtime.lastError);
    }
  });
}

// Check if URL is in HTTP whitelist
function isHttpWhitelisted(url) {
  try {
    const urlObj = new URL(url);
    const baseUrl = `${urlObj.protocol}//${urlObj.hostname}${urlObj.pathname}`;
    return httpWhitelist.has(baseUrl) || httpWhitelist.has(url);
  } catch (e) {
    return false;
  }
}

// Add URL to HTTP whitelist
function addToHttpWhitelist(url) {
  try {
    const urlObj = new URL(url);
    const baseUrl = `${urlObj.protocol}//${urlObj.hostname}${urlObj.pathname}`;
    httpWhitelist.add(baseUrl);
    saveHttpWhitelist();
    console.log('Added to HTTP whitelist:', baseUrl);
  } catch (e) {
    console.error('Failed to add to HTTP whitelist:', e);
  }
}

// OAuth/IdP redirect detection for identity tracking
const IDP_URL_PATTERNS = [
  /accounts\.google\.com/, /login\.microsoftonline\.com/, /.*\.okta\.com/,
  /login\.live\.com/, /github\.com\/login/, /auth0\.com/
];

chrome.webNavigation.onBeforeNavigate.addListener((details) => {
  if (details.frameId !== 0 || !identityManager) return;
  try {
    const url = new URL(details.url);
    const isIdP = IDP_URL_PATTERNS.some(p => p.test(url.hostname));
    if (isIdP) {
      // Track that this tab is going through an IdP flow
      console.log(`Identity: IdP redirect detected → ${url.hostname}`);
    }
  } catch (e) {
    // Invalid URL
  }
});

// HTTP navigation interception
chrome.webNavigation.onBeforeNavigate.addListener(
  async (details) => {
    // Only process main frame navigations
    if (details.frameId !== 0) return;

    const url = details.url;

    // Check if URL is HTTP (not HTTPS)
    if (!url.startsWith('http://')) return;

    // Skip localhost and loopback addresses
    if (url.match(/^http:\/\/(localhost|127\.0\.0\.1|::1|0\.0\.0\.0)/)) return;

    // Skip extension pages
    if (url.startsWith('chrome-extension://')) return;

    // Check if user has whitelisted this URL
    if (isHttpWhitelisted(url)) {
      console.log('HTTP site allowed (whitelisted):', url);
      return;
    }

    // Load settings to check if auto-upgrade is enabled
    const result = await chrome.storage.local.get(['settings']);
    const settings = result.settings || { autoUpgradeHttps: true };
    const autoUpgradeEnabled = settings.autoUpgradeHttps !== false; // Default to true

    console.log('HTTP detected:', url, '| Auto-upgrade:', autoUpgradeEnabled);

    // If auto-upgrade is enabled, try HTTPS first
    if (autoUpgradeEnabled) {
      const httpsUrl = url.replace(/^http:\/\//, 'https://');

      try {
        // Test if HTTPS is available
        const httpsAvailable = await testHttpsAvailability(httpsUrl);

        if (httpsAvailable) {
          // HTTPS works! Redirect directly
          console.log('✅ HTTPS available, upgrading automatically:', httpsUrl);

          // Increment HTTPS upgrades counter
          stats.httpsUpgrades++;
          saveLegacyStats();
          notifyStatsUpdate();

          // Show success notification
          chrome.notifications.create({
            type: 'basic',
            iconUrl: 'icons/icon48.png',
            title: 'Connection Upgraded to HTTPS',
            message: `Automatically upgraded to secure connection`,
            priority: 1
          });

          // Log to audit
          if (auditLogger) {
            auditLogger.logUserAction('http-auto-upgraded', {
              originalUrl: url,
              httpsUrl: httpsUrl,
              timestamp: Date.now()
            }).catch(error => {
              console.error('Failed to log HTTP auto-upgrade:', error);
            });
          }

          // Redirect to HTTPS
          chrome.tabs.update(details.tabId, {
            url: httpsUrl
          });

          return; // Exit early, upgrade successful
        }
      } catch (error) {
        console.log('❌ HTTPS not available, showing warning page:', error.message);
      }
    }

    // If we got here, either auto-upgrade is disabled or HTTPS failed
    // Show warning page
    const warningUrl = chrome.runtime.getURL('http-warning.html') +
                       '?url=' + encodeURIComponent(url) +
                       '&autoUpgrade=false';

    console.log('Showing HTTP warning page:', url);

    // Redirect to warning page
    chrome.tabs.update(details.tabId, {
      url: warningUrl
    });
  },
  { url: [{ schemes: ['http'] }] }
);

// Test if HTTPS is available for a URL
async function testHttpsAvailability(httpsUrl) {
  return new Promise((resolve) => {
    // Create a simple test using fetch with a timeout
    const controller = new AbortController();
    const timeoutId = setTimeout(() => {
      controller.abort();
      resolve(false);
    }, 3000); // 3 second timeout

    fetch(httpsUrl, {
      method: 'HEAD',
      mode: 'no-cors', // Don't care about CORS for this test
      signal: controller.signal,
      cache: 'no-cache'
    })
      .then(() => {
        clearTimeout(timeoutId);
        resolve(true); // HTTPS is available
      })
      .catch((error) => {
        clearTimeout(timeoutId);
        // If it's an abort error, HTTPS timed out
        if (error.name === 'AbortError') {
          resolve(false);
        } else {
          // Any other error means HTTPS isn't working
          resolve(false);
        }
      });
  });
}

// Download interception for HTTP sites
// chrome.downloads.onDeterminingFilename is not available in Firefox
if (chrome.downloads?.onDeterminingFilename) {
  chrome.downloads.onDeterminingFilename.addListener((downloadItem, suggest) => {
    // Check if download is from HTTP site
    if (downloadItem.url.startsWith('http://')) {
      // Show warning notification
      chrome.notifications.create({
        type: 'basic',
        iconUrl: 'icons/icon48.png',
        title: 'Insecure Download Warning',
        message: `Warning: Downloading from insecure HTTP site. File: ${downloadItem.filename}`,
        priority: 2
      });

      // Log the download
      console.log('HTTP download warning:', downloadItem.url);

      stats.suspiciousResources++;
      saveLegacyStats();
      notifyStatsUpdate();
    }

    // CVE-based download warning
    const dlTabId = downloadItem.tabId || -1;
    if (dlTabId > 0) {
      const tabVulns = tabDetections.get(dlTabId)?.vulnerabilities;
      if (tabVulns?.summary?.critical > 0 || tabVulns?.summary?.high > 0) {
        chrome.notifications.create({
          type: 'basic',
          iconUrl: 'icons/icon48.png',
          title: 'Download from Vulnerable Site',
          message: `This site has ${tabVulns.summary.total_cves} known CVE${tabVulns.summary.total_cves !== 1 ? 's' : ''} (${tabVulns.summary.critical} critical). File: ${downloadItem.filename}`,
          priority: 2
        });
        stats.suspiciousResources++;
        saveLegacyStats();
        notifyStatsUpdate();
      }
    }

    // v3 html-smuggling: executable downloaded from a client-assembled
    // blob:/data: URL. Report-only (non-blocking).
    try {
      const v3d = evalV3Download(downloadItem.url || downloadItem.finalUrl, downloadItem.filename, dlTabId);
      if (v3d && v3d.findings.length > 0) {
        console.log(`[Nithic][v3] html-smuggling: ${downloadItem.filename} from ${(downloadItem.url || '').slice(0, 12)}…`);
        reportV3Findings(v3d, dlTabId > 0 ? dlTabId : undefined, downloadItem.referrer || downloadItem.url, 0, 'v3-smuggling');
      }
    } catch (_) {}

    // Allow download to proceed
    suggest({ filename: downloadItem.filename });
  });
}

// Extension installation/update handler
chrome.runtime.onInstalled.addListener(async (details) => {
  console.log('PhishProtect installed/updated:', details.reason);

  // Initialize extension
  await initializeExtension();

  // Log installation event
  if (auditLogger) {
    await auditLogger.logPolicyChange('install', 'default', {
      source: 'extension',
      triggeredBy: details.reason
    });
  }
});

// Extension startup handler
chrome.runtime.onStartup.addListener(async () => {
  console.log('PhishProtect starting up...');
  await initializeExtension();
});

// Warm up the on-device engine when the user explicitly turns Text Analysis ON
// (a transition off→on is clear intent, so we don't make them wait for the
// first scan). We do NOT warm up on unrelated settings tweaks.
chrome.storage.onChanged.addListener((changes, area) => {
  if (area !== 'local') return;
  if (!changes.nithicSettings) return;
  const prev = changes.nithicSettings.oldValue?.detection || {};
  const det  = changes.nithicSettings.newValue?.detection || {};
  const turnedOn = prev.text === false && det.text !== false;
  if (turnedOn && det.engine !== 'cloud') {
    warmupLocalEngine();
  }
});

// Periodic tasks
setInterval(async () => {
  // Sync enterprise policy (if enabled)
  if (policyManager && configManager.get('enterpriseMode')) {
    await policyManager.syncEnterprisePolicy();
  }

  // Flush telemetry
  if (siemReporter) {
    await siemReporter.forceFlush();
  }

  if (clickhouseReporter) {
    await clickhouseReporter.forceFlush();
  }

  // Clean audit logs
  if (auditLogger) {
    await auditLogger.cleanOldLogs();
  }

}, 3600000); // Every hour

// Initialize on script load
initializeExtension().catch(error => {
  console.error('Failed to initialize on load:', error);
});

console.log('PhishProtect Enterprise background script loaded');
