// OrganicAI — extension logger (popup + service-worker + content scripts).
//
// Posts to Worker /logs/client with X-Session-Token so the server attaches
// the username automatically. Batches + throttles to avoid network spam.
//
// Kill switch (server-managed, NOT local):
//   GET  /logging/pref → { enabled: boolean }
//   POST /logging/pref { enabled }
//   The plugin polls every 60s and respects whatever the dashboard says.
//   Server ALSO enforces by silently 200-dropping logs from disabled users —
//   so even if a plugin runs an old build, the kill switch still works.
//
// Public API (import { obLog } from '../lib/logger.js'):
//   obLog.error(message, { stack, context })
//   obLog.warn(message,  { context })
//   obLog.info(message,  { context })
//   obLog.event(name,    { context })          ← activity (not an error)
//   obLog.flush()                              ← drain queue immediately
//   obLog.attachGlobalHandlers()               ← self.onerror + unhandledrejection
//
// IMPORTANT: this module must be safe to import from ANY context (SW, popup,
// content script). It uses `globalThis` everywhere, no `window`.

import { SYNC } from './config.js';

const ENDPOINT = `${SYNC.endpoint}/logs/client`;
const PREF_ENDPOINT = `${SYNC.endpoint}/logging/pref`;

// Per-context source tag — set by the caller via obLog.setSource(...) if it
// wants something more specific than the auto-detected default.
let _source = detectSource();

function detectSource() {
  try {
    if (typeof self !== 'undefined' && typeof ServiceWorkerGlobalScope !== 'undefined' && self instanceof ServiceWorkerGlobalScope) {
      return 'ext-sw';
    }
  } catch {}
  try {
    if (typeof location !== 'undefined' && location.href.includes('/popup/')) return 'ext-popup';
  } catch {}
  try {
    if (typeof location !== 'undefined' && location.href.includes('/onboarding/')) return 'ext-onboarding';
  } catch {}
  // Content scripts run in page context — best guess by hostname.
  try {
    if (typeof location !== 'undefined' && location.host) {
      if (location.host.includes('facebook.com')) return 'ext-content-fb';
      if (location.host.includes('linkedin.com')) return 'ext-content-li';
      if (location.host.includes('x.com') || location.host.includes('twitter.com')) return 'ext-content-tw';
      if (location.host.includes('reddit.com')) return 'ext-content-rd';
      if (location.host.includes('instagram.com')) return 'ext-content-ig';
    }
  } catch {}
  return 'ext-unknown';
}

// ---- Kill-switch (server-managed, cached 60s) ---------------------------
// We default to TRUE so first-load logs from a fresh install still land —
// and the server enforces independently (silently drops if disabled) so
// staleness can never *over-send*, only under-send.
let _enabledCache = true;
let _enabledFetched = 0;
const ENABLED_TTL = 60_000;

async function isEnabled() {
  const now = Date.now();
  if (now - _enabledFetched < ENABLED_TTL) return _enabledCache;
  _enabledFetched = now;
  const token = await getToken();
  if (!token) return _enabledCache; // anonymous → don't ping, just send (rate-limited).
  try {
    const r = await fetch(PREF_ENDPOINT, { headers: { 'X-Session-Token': token } });
    if (r.ok) {
      const j = await r.json();
      _enabledCache = j.enabled !== false;
    }
  } catch {}
  return _enabledCache;
}

// ---- Session token (lazy fetch, cached) ---------------------------------
let _tokenCache = null;
let _tokenFetched = 0;
const TOKEN_TTL = 60_000;

async function getToken() {
  const now = Date.now();
  if (_tokenCache && now - _tokenFetched < TOKEN_TTL) return _tokenCache;
  _tokenFetched = now;
  try {
    // Dynamic import to avoid circular dependency (auth.js → db.js → ...).
    const { getSession } = await import('./auth.js');
    const { token } = await getSession();
    _tokenCache = token || null;
  } catch {
    _tokenCache = null;
  }
  return _tokenCache;
}

// ---- Queue + flush ------------------------------------------------------
const queue = [];
let sendTimer = null;
let lastSendAt = 0;
let consecutiveFailures = 0;
const MAX_QUEUE = 200;

async function flush() {
  if (queue.length === 0) return;
  if (!(await isEnabled())) { queue.length = 0; return; }

  const batch = queue.splice(0, queue.length);
  lastSendAt = Date.now();
  const token = await getToken();

  // Send each item independently — the server endpoint accepts one per POST.
  await Promise.all(batch.map(entry =>
    fetch(ENDPOINT, {
      method: 'POST',
      headers: token
        ? { 'Content-Type': 'application/json', 'X-Session-Token': token }
        : { 'Content-Type': 'application/json' },
      body: JSON.stringify(entry),
      keepalive: true,
    }).then(r => {
      if (!r.ok) throw new Error('log post ' + r.status);
      consecutiveFailures = 0;
    }).catch(() => {
      consecutiveFailures++;
    })
  ));
}

function scheduleFlush() {
  if (sendTimer) return;
  const wait = Math.max(0, 1500 - (Date.now() - lastSendAt));
  sendTimer = setTimeout(() => {
    sendTimer = null;
    if (consecutiveFailures >= 3 && (Date.now() - lastSendAt) < 60_000) return;
    flush();
  }, wait);
}

function enqueue(level, message, opts = {}) {
  // Mirror to devtools console for live debugging.
  try {
    const fn = level === 'error' ? console.error
             : level === 'warn'  ? console.warn
             : console.info;
    // Serialize context into the string so Chrome's error viewer shows the real
    // detail instead of "[object Object]".
    let ctxStr = '';
    if (opts.context != null) {
      try { ctxStr = typeof opts.context === 'string' ? opts.context : JSON.stringify(opts.context); }
      catch { ctxStr = String(opts.context); }
    }
    fn(`[ob:${_source}] ${message}${ctxStr ? ' ' + ctxStr : ''}`);
  } catch {}

  const entry = {
    level,
    source: _source,
    url: (() => { try { return location?.href || null; } catch { return null; } })(),
    message: String(message).slice(0, 1024),
    stack: opts.stack ? String(opts.stack).slice(0, 4096) : null,
    context: opts.context || null,
  };
  queue.push(entry);
  if (queue.length > MAX_QUEUE) queue.splice(0, queue.length - MAX_QUEUE);
  scheduleFlush();
}

// ---- Public API ---------------------------------------------------------
export const obLog = {
  setSource(s) { _source = String(s); },
  error: (msg, opts) => enqueue('error', msg, opts),
  warn:  (msg, opts) => enqueue('warn',  msg, opts),
  info:  (msg, opts) => enqueue('info',  msg, opts),
  event: (name, opts) => enqueue('event', name, opts),
  flush,
  attachGlobalHandlers() {
    try {
      globalThis.addEventListener?.('error', (ev) => {
        if (!ev) return;
        enqueue('error', ev.message || String(ev.error || 'unknown error'), {
          stack: ev.error?.stack,
          context: { filename: ev.filename, line: ev.lineno, col: ev.colno },
        });
      });
    } catch {}
    try {
      globalThis.addEventListener?.('unhandledrejection', (ev) => {
        const r = ev?.reason;
        enqueue('error', (r?.message || String(r) || 'unhandled rejection').slice(0, 1024), {
          stack: r?.stack,
          context: { reason_type: typeof r },
        });
      });
    } catch {}
  },
};

// Default export for convenience.
export default obLog;
