/* Nutri — Global Error Logger + QA Panel (window.NutriLogger)
   Step 2 of the App Stabilization plan.

   Captures:
   - window.onerror / unhandledrejection
   - Firebase write failures (auto-patched via NutriLogger.wrapCloud)
   - API failures (TMDB / OMDB)
   - Manual logs via NutriLogger.log(level, module, action, msg, meta)

   All logs stay in memory (max 500) + localStorage 'nutri_qa_log' (last 200).
   Never sent anywhere; never modifies user data. */
(function () {
  'use strict';

  // ── Constants ──────────────────────────────────────────────────────────
  const MAX_MEM  = 500;
  const MAX_DISK = 200;
  const STORE_KEY = 'nutri_qa_log';
  const LEVELS = { info: 0, warning: 1, error: 2, critical: 3 };

  // ── In-memory log ──────────────────────────────────────────────────────
  let _logs = [];
  let _seq  = 0;

  function _ts() { return new Date().toISOString(); }
  function _uid() { try { return (window.Store && Store.uid && Store.uid()) || null; } catch (_) { return null; } }
  function _route() {
    try {
      // NutriShell stores the active project/tab in the DOM title or we can read a sentinel.
      const title = document.title || '';
      return title.slice(0, 60) || window.location.pathname;
    } catch (_) { return 'unknown'; }
  }

  function _persist(entry) {
    try {
      const saved = JSON.parse(localStorage.getItem(STORE_KEY) || '[]');
      saved.push(entry);
      if (saved.length > MAX_DISK) saved.splice(0, saved.length - MAX_DISK);
      localStorage.setItem(STORE_KEY, JSON.stringify(saved));
    } catch (_) {} // never crash on log failure
  }

  function _addEntry(level, module, action, message, meta) {
    const entry = {
      seq: ++_seq,
      ts: _ts(),
      level,                                    // 'info' | 'warning' | 'error' | 'critical'
      module: module || 'app',                  // e.g. 'firebase', 'tmdb', 'calories'
      action: action || '',                     // e.g. 'writeFinance', 'search'
      message: String(message || '').slice(0, 500),
      stack:  (meta && meta.stack) ? String(meta.stack).slice(0, 1000) : null,
      userId: _uid(),
      route:  _route(),
      recordId: (meta && meta.recordId) || null,
      extra: meta ? Object.fromEntries(
        Object.entries(meta).filter(([k]) => !['stack','recordId'].includes(k)).map(([k,v]) => [k, String(v).slice(0,200)])
      ) : null,
    };
    _logs.push(entry);
    if (_logs.length > MAX_MEM) _logs.shift();
    if (LEVELS[level] >= LEVELS.warning) _persist(entry); // only persist warning+ to save space
    return entry;
  }

  // ── Public log API ─────────────────────────────────────────────────────
  function log(level, module, action, message, meta) {
    return _addEntry(level, module, action, message, meta);
  }
  function info(module, action, msg, meta)     { return _addEntry('info',     module, action, msg, meta); }
  function warn(module, action, msg, meta)     { return _addEntry('warning',  module, action, msg, meta); }
  function error(module, action, msg, meta)    { return _addEntry('error',    module, action, msg, meta); }
  function critical(module, action, msg, meta) { return _addEntry('critical', module, action, msg, meta); }

  // ── Load persisted logs on boot ────────────────────────────────────────
  (function loadSaved() {
    try {
      const saved = JSON.parse(localStorage.getItem(STORE_KEY) || '[]');
      if (Array.isArray(saved) && saved.length) {
        // Mark them as "from last session"
        saved.forEach(e => { e._prevSession = true; });
        _logs = saved.concat(_logs);
        if (_logs.length > MAX_MEM) _logs = _logs.slice(-MAX_MEM);
      }
    } catch (_) {}
  })();

  // ── Global error hooks ─────────────────────────────────────────────────
  const _prevOnerror = window.onerror;
  window.onerror = function (msg, src, line, col, err) {
    error('runtime', 'window.onerror', msg, {
      stack: err && err.stack,
      extra_src: src, extra_line: line, extra_col: col,
    });
    if (_prevOnerror) return _prevOnerror.apply(this, arguments);
    return false;
  };

  const _prevUnhandled = window.onunhandledrejection;
  window.addEventListener('unhandledrejection', function (e) {
    const reason = e.reason;
    const msg = reason && reason.message ? reason.message : String(reason || 'Unhandled rejection');
    error('runtime', 'unhandledrejection', msg, { stack: reason && reason.stack });
  });

  // ── Firebase write wrapper ─────────────────────────────────────────────
  // Called once after Cloud is initialised (from nutri-store or nutri-app).
  function wrapCloud(cloud) {
    if (!cloud || cloud.__nutri_wrapped) return;
    cloud.__nutri_wrapped = true;
    const WRITERS = [
      'writeMeals','writeFinance','writeGoals','writeDiary','writeMacro',
      'writeAssistant','writeSettings','writeLocalLibrary','writeMovies','writeProfile',
    ];
    WRITERS.forEach(name => {
      if (typeof cloud[name] !== 'function') return;
      const orig = cloud[name];
      cloud[name] = async function (...args) {
        try {
          const result = await orig.apply(this, args);
          return result;
        } catch (e) {
          error('firebase', name, (e && e.message) || String(e), {
            stack: e && e.stack,
            extra_code: e && e.code,
          });
          throw e; // re-throw so Store's queueCloud still updates syncStatus
        }
      };
    });
  }

  // ── TMDB/OMDB wrapper ─────────────────────────────────────────────────
  // Called after TMDB/OMDB are initialised (from nutri-movies-tmdb/omdb).
  function wrapTMDB(tmdb) {
    if (!tmdb || tmdb.__nutri_wrapped) return;
    tmdb.__nutri_wrapped = true;
    const METHODS = ['search','details','externalIds','similar','recommendations','trending',
      'popular','discover','topRated','findByImdb','genres','testConnection'];
    METHODS.forEach(name => {
      if (typeof tmdb[name] !== 'function') return;
      const orig = tmdb[name];
      tmdb[name] = async function (...args) {
        try { return await orig.apply(this, args); }
        catch (e) {
          const lvl = (e && e.code === 'no-creds') ? 'warning' : 'error';
          _addEntry(lvl, 'tmdb', name, (e && e.message) || String(e), { stack: e && e.stack, extra_code: e && e.code });
          throw e;
        }
      };
    });
  }

  function wrapOMDB(omdb) {
    if (!omdb || omdb.__nutri_wrapped) return;
    omdb.__nutri_wrapped = true;
    const orig = omdb.byImdb;
    if (typeof orig !== 'function') return;
    omdb.byImdb = async function (...args) {
      try { return await orig.apply(this, args); }
      catch (e) {
        _addEntry('error', 'omdb', 'byImdb', (e && e.message) || String(e), { stack: e && e.stack, extra_code: e && e.code });
        throw e;
      }
    };
  }

  // ── Query helpers ──────────────────────────────────────────────────────
  function getLogs(opts) {
    const o = opts || {};
    let list = _logs.slice().reverse(); // newest first
    if (o.level) {
      const minLevel = LEVELS[o.level] || 0;
      list = list.filter(e => (LEVELS[e.level] || 0) >= minLevel);
    }
    if (o.module) list = list.filter(e => e.module === o.module);
    if (o.limit)  list = list.slice(0, o.limit);
    return list;
  }

  function clearLogs() {
    _logs = [];
    try { localStorage.removeItem(STORE_KEY); } catch (_) {}
  }

  // ── Diagnostics report ────────────────────────────────────────────────
  function buildReport() {
    const errors   = getLogs({ level: 'error', limit: 30 });
    const warnings = getLogs({ level: 'warning', limit: 20 }).filter(e => e.level === 'warning');
    const all      = getLogs({ limit: 50 });
    const uid      = _uid();
    const store    = window.Store || {};
    const stats = {
      meals:    store.meals   ? store.meals().length   : '?',
      finance:  store.finance ? store.finance().length : '?',
      goals:    store.goals   ? store.goals().length   : '?',
      diary:    store.diary   ? store.diary().length   : '?',
      movies:   store.movies  ? store.movies().length  : '?',
    };
    return [
      '=== Nutri Diagnostics Report ===',
      'Generated: ' + _ts(),
      'App version: ' + (window.__NUTRI_VERSION || 'dev'),
      'User: ' + (uid ? uid.slice(0, 8) + '…' : 'not signed in'),
      'Sync status: ' + (store.syncStatus ? store.syncStatus() : '?'),
      'Last sync: ' + (store.lastSyncAt ? store.lastSyncAt() : '?'),
      'Data: ' + JSON.stringify(stats),
      'Platform: ' + navigator.userAgent.slice(0, 120),
      '',
      '--- Errors (' + errors.length + ') ---',
      ...errors.map(e => `[${e.ts}] [${e.level.toUpperCase()}] ${e.module}::${e.action} — ${e.message}` + (e.stack ? '\n  ' + e.stack.split('\n')[1] : '')),
      '',
      '--- Warnings (' + warnings.length + ') ---',
      ...warnings.map(e => `[${e.ts}] ${e.module}::${e.action} — ${e.message}`),
      '',
      '--- Recent log tail (last 20) ---',
      ...all.slice(0, 20).map(e => `[${e.ts.slice(11, 19)}] [${e.level.toUpperCase().padEnd(8)}] ${e.module}::${e.action} — ${e.message.slice(0, 120)}`),
    ].join('\n');
  }

  // ── Health Check ──────────────────────────────────────────────────────
  async function runHealthCheck() {
    const checks = [];
    function add(name, passed, detail, severity) {
      const status = passed === true ? 'pass' : passed === false ? 'fail' : 'warn';
      checks.push({ name, status, detail: detail || '', severity: severity || (status === 'fail' ? 'error' : status === 'warn' ? 'warning' : 'info') });
    }

    // 1. Local date function
    try {
      const d = new Date('2026-06-03T01:00:00+03:00');
      const local = isoOf(d);
      add('Local date key (isoOf)', local === '2026-06-03', 'isoOf → ' + local + ' (expect 2026-06-03 for UTC+3 01:00)');
    } catch (e) { add('Local date key (isoOf)', false, String(e)); }

    // 2. todayISO function
    try {
      const t = todayISO();
      const now = new Date();
      const exp = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0');
      add('todayISO()', t === exp, t + ' (expect ' + exp + ')');
    } catch (e) { add('todayISO()', false, String(e)); }

    // 3. localStorage available
    try {
      localStorage.setItem('__nutri_hc', '1');
      localStorage.removeItem('__nutri_hc');
      add('localStorage', true);
    } catch (e) { add('localStorage', false, String(e)); }

    // 4. Firebase SDK loaded
    const fbLoaded = !!(window.firebase && firebase.app);
    add('Firebase SDK', fbLoaded);

    // 5. Auth state
    const uid = _uid();
    add('User signed in', !!uid, uid ? 'uid: ' + uid.slice(0,8) + '…' : 'Not signed in');

    // 6. Firebase Firestore reachable (lightweight read of user doc)
    if (uid && window.Cloud) {
      try {
        const ref = Cloud.rootRef(uid);
        const snap = await Promise.race([ref.get(), new Promise((_, rj) => setTimeout(() => rj(new Error('timeout')), 5000))]);
        add('Firestore read', snap.exists !== undefined, 'User doc exists: ' + snap.exists);
      } catch (e) { add('Firestore read', false, (e && e.message) || String(e)); }
    } else {
      add('Firestore read', null, 'Skipped — not signed in or Cloud not loaded');
    }

    // 7. Store data counts
    if (window.Store) {
      const syncStatus = Store.syncStatus();
      add('Store sync', syncStatus === 'idle' || syncStatus === 'syncing', 'syncStatus: ' + syncStatus + ', error: ' + (Store.syncError() || 'none'));
    }

    // 8. Movie API proxy
    if (window.TMDB && TMDB.proxyHealth) {
      try {
        const h = await Promise.race([TMDB.proxyHealth(), new Promise(r => setTimeout(() => r({ ok: false, reason: 'timeout' }), 5000))]);
        add('Movie API proxy', h.ok ? true : null, h.ok ? 'Connected (tmdb:' + h.tmdb + ' omdb:' + h.omdb + ')' : 'Not connected — ' + (h.reason || ''));
      } catch (e) { add('Movie API proxy', null, 'Error: ' + (e && e.message)); }
    }

    // 9. TMDB direct (fallback)
    if (window.TMDB) {
      const hasCreds = TMDB.hasCredentials();
      add('TMDb credentials', hasCreds, hasCreds ? 'Connected (proxy or local key)' : 'No TMDb credentials');
    }

    // 10. OMDB key
    if (window.OMDB) {
      const hasKey = OMDB.hasKey();
      add('OMDb (IMDb ratings)', hasKey, hasKey ? 'Key present' : 'No key — IMDb ratings unavailable');
    }

    // 11. Service Worker
    if ('serviceWorker' in navigator) {
      const regs = await navigator.serviceWorker.getRegistrations().catch(() => []);
      add('Service Worker', regs.length > 0, regs.length + ' registration(s)');
    } else {
      add('Service Worker', null, 'Not supported in this browser');
    }

    // 12. Notification permission
    if ('Notification' in window) {
      const p = Notification.permission;
      add('Notification permission', p === 'granted', p, p === 'denied' ? 'error' : 'info');
    }

    // 13. Online status
    add('Network', navigator.onLine, navigator.onLine ? 'Online' : 'Offline');

    return checks;
  }

  // ── Project validation ─────────────────────────────────────────────────
  function runProjectValidation() {
    const results = [];
    const store = window.Store;
    if (!store) { results.push({ project: 'Store', ok: false, msg: 'Store not loaded' }); return results; }

    function check(project, test, msg) {
      try { const ok = test(); results.push({ project, ok: ok !== false, msg: ok === false ? msg : ('OK' + (typeof ok === 'string' ? ' — ' + ok : '')) }); }
      catch (e) { results.push({ project, ok: false, msg: msg + ': ' + (e.message || e) }); }
    }

    // Calories
    check('Calories', () => { const m = store.meals(); return m.length + ' meals'; }, 'meals() failed');
    check('Calories', () => { const g = store.macroGoals(); return g && g.calories ? g.calories + ' cal goal' : false; }, 'No macro goals');

    // Finance
    check('Finance', () => { const f = store.finance(); return f.length + ' txns'; }, 'finance() failed');

    // Goals
    check('Goals', () => { const g = store.goals(); return g.length + ' goals'; }, 'goals() failed');
    const goals = store.goals ? store.goals() : [];
    const malformed = goals.filter(g => !g.id || !g.title);
    if (malformed.length) results.push({ project: 'Goals', ok: false, msg: malformed.length + ' goals without id/title' });

    // Diary
    check('Diary', () => { const d = store.diary(); return d.length + ' entries'; }, 'diary() failed');
    const diary = store.diary ? store.diary() : [];
    const badDates = diary.filter(e => e.date && !/^\d{4}-\d{2}-\d{2}$/.test(e.date));
    if (badDates.length) results.push({ project: 'Diary', ok: false, msg: badDates.length + ' entries with malformed dates' });

    // Movie Night
    check('Movies', () => { const m = store.movies(); return m.length + ' movies'; }, 'movies() failed');
    const movies = store.movies ? store.movies() : [];
    const noTitle = movies.filter(m => !m.title);
    if (noTitle.length) results.push({ project: 'Movies', ok: false, msg: noTitle.length + ' movies without title' });
    const orphanedLists = store.movieSyncStats ? store.movieSyncStats().orphanedLists : null;
    if (orphanedLists > 0) results.push({ project: 'Movies', ok: false, msg: 'Orphaned list refs: ' + orphanedLists + ' (tap Recover Lost Lists in Movie Night Settings)' });

    // Date key validation across Calories
    const meals = store.meals ? store.meals() : [];
    const badMealDates = meals.filter(m => m.date && !/^\d{4}-\d{2}-\d{2}$/.test(m.date));
    if (badMealDates.length) results.push({ project: 'Calories', ok: false, msg: badMealDates.length + ' meals with malformed dates' });

    return results;
  }

  // ── Expose globally ────────────────────────────────────────────────────
  window.NutriLogger = {
    log, info, warn, error, critical,
    getLogs, clearLogs, buildReport,
    runHealthCheck, runProjectValidation,
    wrapCloud, wrapTMDB, wrapOMDB,
  };

  // ── Auto-wrap once Cloud / TMDB / OMDB are available ──────────────────
  // Retry with short delay since these globals are loaded after nutri-logger.
  let _wrapAttempts = 0;
  function _tryWrap() {
    if (window.Cloud) wrapCloud(window.Cloud);
    if (window.TMDB)  wrapTMDB(window.TMDB);
    if (window.OMDB)  wrapOMDB(window.OMDB);
    if (_wrapAttempts++ < 20 && (!window.Cloud || !window.TMDB)) setTimeout(_tryWrap, 500);
  }
  setTimeout(_tryWrap, 200);

  // Log app start
  info('app', 'boot', 'Nutri logger initialised', { extra_ua: navigator.userAgent.slice(0, 80) });
})();
