// DOM Event Sensor (page-world)
// Observes dialog / popup / history behavior so the v3 enhanced engine's
// dom-channel ruleset (se-dialog-trap-v3) can run on real page behavior.
//
// This is a SENSOR, not an enforcer: it wraps the native APIs, records typed
// event-state, and ALWAYS calls through to the original (the page behaves
// normally). Detection is report-only in the background — consistent with the
// content-channel flip. The browser already throttles repeated dialogs and
// offers "prevent additional dialogs", so we don't suppress here.
//
// Emits cumulative state via window.postMessage({ type: 'nithic-dom-event-state' });
// content.js bridges it to the background, which evaluates + reports.

(function () {
  'use strict';
  if (window.__nithicDomSensor) return; // guard against double-injection
  window.__nithicDomSensor = true;

  // ── Cumulative per-page state (matches dialog-trap-signals target shape) ──
  const state = {
    dialogEvents: [],          // [{ type, message, timestamp, inInfiniteLoop }]
    hasBeforeUnload: false,
    beforeUnloadMessage: '',
    popupCount: 0,
    historyLoopDetected: false,
    preventsAutoClose: false,
  };

  const MAX_DIALOGS = 50;      // cap memory; rapid-loop is detectable well under this
  const RAPID_GAP_MS = 400;    // dialogs closer than this count as a tight burst
  const BURST_FOR_LOOP = 4;    // this many tight-burst dialogs ⇒ infinite-loop flag

  // ── Emit (debounced) ──────────────────────────────────────────────────────
  // Only emit when there's a behavioral signal worth evaluating. A lone
  // beforeunload handler (extremely common on legit SPAs) is NOT a trigger —
  // it rides along in the payload so it can corroborate a compound trap, but
  // never fires solo telemetry.
  let emitTimer = null;
  function hasSignal() {
    return state.dialogEvents.length > 0 || state.popupCount > 0 ||
           state.historyLoopDetected || state.preventsAutoClose;
  }
  function emit() {
    if (!hasSignal()) return;
    try {
      window.postMessage({
        type: 'nithic-dom-event-state',
        state: {
          dialogEvents: state.dialogEvents.slice(-MAX_DIALOGS),
          hasBeforeUnload: state.hasBeforeUnload,
          beforeUnloadMessage: state.beforeUnloadMessage.slice(0, 200),
          popupCount: state.popupCount,
          historyLoopDetected: state.historyLoopDetected,
          preventsAutoClose: state.preventsAutoClose,
        },
        url: location.href,
        timestamp: Date.now(),
      }, '*');
    } catch (_) { /* ignore */ }
  }
  function scheduleEmit() {
    if (emitTimer) return;            // coalesce a burst into one post
    emitTimer = setTimeout(() => { emitTimer = null; emit(); }, 500);
  }

  // ── Dialog hooks: alert / confirm / prompt ────────────────────────────────
  function recordDialog(type, message) {
    const ts = Date.now();
    const prev = state.dialogEvents[state.dialogEvents.length - 1];
    const tight = prev && (ts - prev.timestamp) < RAPID_GAP_MS;
    // Count the trailing tight-burst length; mark as infinite-loop past the
    // threshold (a page firing 4+ dialogs back-to-back is not a legit prompt).
    let burst = 1;
    for (let i = state.dialogEvents.length - 1; i >= 0; i--) {
      const a = state.dialogEvents[i];
      const b = state.dialogEvents[i + 1] || { timestamp: ts };
      if ((b.timestamp - a.timestamp) < RAPID_GAP_MS) burst++; else break;
    }
    const inInfiniteLoop = tight && burst >= BURST_FOR_LOOP;
    state.dialogEvents.push({
      type,
      message: typeof message === 'string' ? message.slice(0, 500) : String(message ?? '').slice(0, 500),
      timestamp: ts,
      inInfiniteLoop,
    });
    if (state.dialogEvents.length > MAX_DIALOGS) state.dialogEvents.shift();
    scheduleEmit();
  }

  function wrapDialog(name, returnOnError) {
    const original = window[name];
    if (typeof original !== 'function') return;
    const wrapped = function (...args) {
      try { recordDialog(name, args[0]); } catch (_) {}
      try { return original.apply(this, args); }
      catch (_) { return returnOnError; }
    };
    try {
      Object.defineProperty(window, name, { value: wrapped, writable: true, configurable: true });
    } catch (_) {
      try { window[name] = wrapped; } catch (_) {}
    }
  }
  wrapDialog('alert', undefined);
  wrapDialog('confirm', false);
  wrapDialog('prompt', null);

  // ── Popup hook: window.open ───────────────────────────────────────────────
  const originalOpen = window.open;
  if (typeof originalOpen === 'function') {
    const wrappedOpen = function (...args) {
      try { state.popupCount++; scheduleEmit(); } catch (_) {}
      try { return originalOpen.apply(this, args); } catch (_) { return null; }
    };
    try {
      Object.defineProperty(window, 'open', { value: wrappedOpen, writable: true, configurable: true });
    } catch (_) {
      try { window.open = wrappedOpen; } catch (_) {}
    }
  }

  // ── beforeunload presence (context-only, not an emit trigger) ─────────────
  const originalAddEventListener = EventTarget.prototype.addEventListener;
  EventTarget.prototype.addEventListener = function (type, listener, options) {
    try {
      if (type === 'beforeunload' && (this === window || this === document)) {
        state.hasBeforeUnload = true;
      }
    } catch (_) {}
    return originalAddEventListener.call(this, type, listener, options);
  };
  try {
    const desc = Object.getOwnPropertyDescriptor(window, 'onbeforeunload') ||
                 Object.getOwnPropertyDescriptor(Window.prototype, 'onbeforeunload');
    if (desc && desc.set) {
      Object.defineProperty(window, 'onbeforeunload', {
        configurable: true,
        get() { return desc.get ? desc.get.call(this) : undefined; },
        set(v) { if (v) state.hasBeforeUnload = true; return desc.set.call(this, v); },
      });
    }
  } catch (_) {}
  // Capture the actual message on a real unload attempt (best-effort; rides the
  // next page-context emit only if a signal already exists, so it stays low-FP).
  originalAddEventListener.call(window, 'beforeunload', (e) => {
    try {
      const m = e.returnValue;
      if (typeof m === 'string' && m) state.beforeUnloadMessage = m;
    } catch (_) {}
  });

  // ── History-API loop: pushState/replaceState flooding ─────────────────────
  let historyCalls = [];
  const HISTORY_WINDOW_MS = 3000;
  const HISTORY_THRESHOLD = 20;
  function noteHistory() {
    const now = Date.now();
    historyCalls.push(now);
    historyCalls = historyCalls.filter(t => now - t <= HISTORY_WINDOW_MS);
    if (historyCalls.length >= HISTORY_THRESHOLD && !state.historyLoopDetected) {
      state.historyLoopDetected = true;
      scheduleEmit();
    }
  }
  ['pushState', 'replaceState'].forEach((fn) => {
    const orig = history[fn];
    if (typeof orig !== 'function') return;
    history[fn] = function (...args) {
      try { noteHistory(); } catch (_) {}
      return orig.apply(this, args);
    };
  });

  // ── window.close override ⇒ page prevents auto-close ──────────────────────
  try {
    const originalClose = window.close;
    Object.defineProperty(window, 'close', {
      configurable: true,
      get() { return originalClose; },
      set() {
        // The page reassigned window.close (classic "you can't leave" trap).
        state.preventsAutoClose = true;
        scheduleEmit();
        return true;
      },
    });
  } catch (_) {}

  console.log('[Nithic] DOM event sensor active');
})();
