// appui/assets/js/alpine.js
import Alpine from 'alpinejs'
import sort from '@alpinejs/sort'
import collapse from '@alpinejs/collapse'
import intersect from '@alpinejs/intersect'

import TimeAgo from '@marcreichel/alpine-timeago';
import { format } from 'date-fns'
import { de, enUS } from 'date-fns/locale'

Alpine.plugin(collapse)
Alpine.plugin(sort)
Alpine.plugin(intersect)
// locale from PHP
let localeCode = window.APP_LOCALE || 'en'
let locale = enUS

switch (localeCode) {
    case 'de':
        locale = de
        break
    case 'en':
        locale = enUS
        break
    default:
        locale = enUS
        break
}

Alpine.plugin(TimeAgo.configure({
    locale: locale,
}))

Alpine.data('animatedNumber', (initialValue) => ({
    displayValue: initialValue,
    isAnimating: true,
    update(newValue) {
        if (this.displayValue === newValue) return;

        this.isAnimating = false; // Reset animation state
        this.displayValue = newValue;

        this.$nextTick(() => {
            this.$el.offsetHeight; // Force reflow to restart animation
            this.isAnimating = true;
        });
    },
    get digits() {
        return String(this.displayValue).split('');
    }
}));

window.Alpine = Alpine
Alpine.start()

// ---- BeezUI globals (date-fns based formatting + schedule summary helper) ----
;(function(){

  const g = (typeof window !== 'undefined') ? window : globalThis
  g.BeezUI = g.BeezUI || {}

  // Expose the chosen date-fns locale so helpers can use it
  g.BeezUI.locale = (typeof locale !== 'undefined') ? locale : enUS

  // Safe date parser for "YYYY-MM-DD" or any Date-parsable input
  function toDateSafe(v) {
    if (!v) return null
    try {
      const s = String(v)
      // Prefer interpreting as local midnight when date-only string
      const d = (s.length >= 10 && /^\d{4}-\d{2}-\d{2}/.test(s)) ? new Date(s + 'T00:00:00') : new Date(s)
      return isNaN(d.getTime()) ? null : d
    } catch (_) { return null }
  }

  // Localized date formatter. Default pattern uses locale-appropriate medium date.
  g.BeezUI.formatDate = function(value, pattern = 'PP') {
    const d = toDateSafe(value)
    if (!d) return String(value ?? '')
    try { return format(d, pattern, { locale: g.BeezUI.locale || enUS }) } catch (_) { return d.toLocaleDateString() }
  }

  // Helper to seed start/end if empty: start = tomorrow, end = start+7 days
  // src: optional reactive form object; falls back to window.form
  g.BeezUI.seedDefaultStartEnd = function(startField, endField, src) {
    try {
      const form = src || g.form
      if (!form) return
      const sName = String(startField)
      const eName = String(endField)
      const d = new Date(); d.setHours(0,0,0,0); d.setDate(d.getDate()+1)
      const fmt = (dd)=> dd.getFullYear()+ '-' + String(dd.getMonth()+1).padStart(2,'0') + '-' + String(dd.getDate()).padStart(2,'0')

      const isInvalid = (v) => !v || v === '1000-01-01' || v === '0000-00-00' || v.startsWith('0000');

      if (isInvalid(form[sName])) form[sName] = fmt(d)

      // Only seed end date if schedule type requires it (e.g. scheduled_window)
      // Bulk mail and Automation default to no end date (completed / never)
      const st = form['schedule_type'] || 'bulk_mail'
      if (st !== 'bulk_mail' && st !== 'automation') {
        if (isInvalid(form[eName])) {
          const e = new Date(d); e.setDate(e.getDate()+7)
          form[eName] = fmt(e)
        }
      }
    } catch(_) { /* no-op */ }
  }

  // Build a localized schedule summary line using a provided source (Alpine form) or global form
  // fieldMap: { schedule_type, start_mode, start_date, end_date, automation_end }
  // tokens: { starts_now, starts_on, stops_finished, stops_on, runs_indefinitely, on_word? }
  // src: the reactive object that holds field values (e.g., Alpine 'form'); falls back to window.form
  g.BeezUI.scheduleSummary = function(fieldMap, tokens, src) {
    try {
      const form = src || g.form || {}
      const f = fieldMap || {}
      const t = tokens || {}
      const sm = form[f.start_mode] || 'now'
      const sd = form[f.start_date] || ''
      const st = form[f.schedule_type] || ''
      const ed = form[f.end_date] || ''
      const ae = form[f.automation_end] || 'never'

      // Locale-aware preposition between verb and date (e.g., "on" in EN, "am" in DE)
      const lc = String(g.APP_LOCALE || '').toLowerCase()
      const joiner = ' '; // (typeof t.on_word === 'string' && t.on_word) ? ' ' + t.on_word.trim() + ' ': (lc.startsWith('de') ? ' am ' : ' on ')

      const parts = []
      if (sm === 'date' && sd) parts.push((t.starts_on || 'Starts') + joiner + g.BeezUI.formatDate(sd))
      else parts.push(t.starts_now || 'Starts immediately')

      if (st === 'bulk_mail') {
        parts.push(t.stops_finished || 'Stops when finished')
        if (ed) parts.push((t.stops_on || 'Stops') + joiner + g.BeezUI.formatDate(ed))
      } else if (st === 'scheduled_window') {
        if (ed) parts.push((t.stops_on || 'Stops') + joiner + g.BeezUI.formatDate(ed))
      } else if (st === 'automation') {
        if (ae === 'date' && ed) parts.push((t.stops_on || 'Stops') + joiner + g.BeezUI.formatDate(ed))
        else parts.push(t.runs_indefinitely || 'Runs indefinitely')
      }
      return parts.join('. ') + '.'
    } catch(_) { return '' }
  }

  // Managed Queue for Audience Count Refreshes
  ;(function() {
      const g = window.BeezUI = window.BeezUI || {};
      let queue = [], inflight = 0, maxConcurrency = 1;
      // Track the DOM elements whose requests are currently in-flight so we can
      // abort them when the user navigates away (non-blocking page switches).
      let inflightEls = new Set();
      let massRefresh = { active: false, total: 0, processed: 0, config: null };

      // Pump the queue at low priority so these heavy, multi-second count
      // requests never compete with user interactions or page navigation.
      // Falls back to a throttled setTimeout when requestIdleCallback is absent.
      function scheduledPump(delay) {
          const run = function() { g.pumpAudienceQueue(); };
          if (typeof window.requestIdleCallback === 'function') {
              window.requestIdleCallback(run, { timeout: 2000 });
          } else {
              setTimeout(run, typeof delay === 'number' ? delay : 250);
          }
      }

      function htmxRef() {
          return window.htmx || (typeof htmx !== 'undefined' ? htmx : null);
      }

      // Parse an hx-swap attribute (e.g. "outerHTML swap:1s settle:200ms") into
      // the swapSpec object expected by htmx.swap(). Modifiers like swap:/settle:
      // are converted to milliseconds using htmx.parseInterval when available.
      function parseSwapSpec(spec, h) {
          const out = { swapStyle: 'outerHTML' };
          if (!spec) return out;
          const parts = String(spec).trim().split(/\s+/);
          if (parts[0]) out.swapStyle = parts[0];
          for (let i = 1; i < parts.length; i++) {
              const p = parts[i];
              const toMs = function(v) {
                  if (h && typeof h.parseInterval === 'function') {
                      const t = h.parseInterval(v);
                      return (typeof t === 'number' && !isNaN(t)) ? t : undefined;
                  }
                  const f = parseFloat(v);
                  return isNaN(f) ? undefined : f;
              };
              if (p.indexOf('swap:') === 0) {
                  const t = toMs(p.substring(5));
                  if (t !== undefined) out.swapDelay = t;
              } else if (p.indexOf('settle:') === 0) {
                  const t = toMs(p.substring(7));
                  if (t !== undefined) out.settleDelay = t;
              }
          }
          return out;
      }

      g.updateRefreshButton = function() {
          if (!massRefresh.active || !massRefresh.config || !massRefresh.config.buttonId) return;
          const btn = document.getElementById(massRefresh.config.buttonId);
          if (!btn) return;

          if (btn._originalHtml === undefined) btn._originalHtml = btn.innerHTML;
          btn.disabled = true;

          let text = massRefresh.config.textRefreshing || 'Refreshing...';
          if (massRefresh.total > 0) {
              let progressText = massRefresh.config.textProgress || 'Refreshing %s of %s';
              let displayProcessed = Math.min(massRefresh.processed + 1, massRefresh.total);
              text = progressText.replace('%s', displayProcessed).replace('%s', massRefresh.total);
          }

          btn.innerHTML = '<span>' + text + '</span>' + (massRefresh.config.spinner || '');
      };

      g.pumpAudienceQueue = function() {
          if (inflight >= maxConcurrency || queue.length === 0) return;
          const el = queue.shift();
          if (!el || !el.isConnected) { g.pumpAudienceQueue(); return; }
          if (el._beezInflight) { g.pumpAudienceQueue(); return; }

          const url = el.getAttribute('hx-get');
          if (!url) { g.pumpAudienceQueue(); return; }

          el._beezInflight = true;
          inflight++;
          inflightEls.add(el);

          let done = false;
          const cleanup = () => {
              if (done) return;
              done = true;
              el._beezInflight = false;
              el._beezAbort = null;
              inflightEls.delete(el);
              inflight = Math.max(0, inflight - 1);

              if (massRefresh.active && el._isMassRefresh) {
                  el._isMassRefresh = false;
                  massRefresh.processed++;
                  if (massRefresh.processed >= massRefresh.total) {
                      massRefresh.active = false;
                      const btn = document.getElementById(massRefresh.config.buttonId);
                      if (btn) {
                          btn.disabled = false;
                          if (btn._originalHtml !== undefined) {
                              btn.innerHTML = btn._originalHtml;
                          }
                      }
                  } else {
                      g.updateRefreshButton();
                  }
              }

              scheduledPump(250);
          };

          // Use a timeout as a fail-safe in case the fetch never settles (e.g. element removed from DOM unexpectedly)
          const failSafe = setTimeout(cleanup, 10000);

          // Surface the inline spinner while the request is in-flight. This mirrors
          // the hx-on::before-request hook baked into the count anchor markup, which
          // no longer fires now that we drive the request via fetch() instead of htmx.
          try {
              const sp = el.querySelector('.hb-spinner');
              if (sp) sp.classList.remove('hidden');
          } catch (e) {}

          // Allow purgeAudienceForContainer() to abort this request when the user
          // navigates away or the surrounding list is swapped out.
          const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
          el._beezAbort = controller;

          const h = htmxRef();
          const swapSpec = parseSwapSpec(el.getAttribute('hx-swap') || 'outerHTML', h);

          // Fetch the updated count at LOW priority (Fetch Priority API) so these
          // heavy, multi-second queries never compete with navigation or other
          // user-initiated requests. htmx.ajax() would default the browser request
          // to High priority; a plain fetch lets us opt down explicitly. The
          // returned HTML is then swapped in via htmx.swap() to preserve the
          // existing swap semantics (outerHTML swap:1s) and re-wire hx-* triggers.
          fetch(url, {
              priority: 'low',
              headers: { 'HX-Request': 'true' },
              signal: controller ? controller.signal : undefined
          })
              .then(function(res) { return res.text(); })
              .then(function(html) {
                  if (!el.isConnected) return;
                  const hx = htmxRef();
                  if (hx && typeof hx.swap === 'function') {
                      hx.swap(el, html, swapSpec);
                  } else {
                      el.outerHTML = html;
                  }
              })
              .catch(function() { /* aborted or network error: keep stale label */ })
              .finally(function() {
                  clearTimeout(failSafe);
                  cleanup();
              });
      };

      g.initAudienceObserver = function() {
          if (!g._audienceObserver) {
              g._audienceObserver = new IntersectionObserver((entries) => {
                  entries.forEach(entry => {
                      if (entry.isIntersecting) {
                          g._audienceObserver.unobserve(entry.target);
                          if (!queue.includes(entry.target)) {
                              queue.push(entry.target);
                              scheduledPump(0);
                          }
                      }
                  });
              }, { rootMargin: '100px' });
          }
          document.querySelectorAll('[data-lazy-refresh="1"]').forEach(el => {
              el.removeAttribute('data-lazy-refresh');
              g._audienceObserver.observe(el);
          });
      };

      //
      g.refreshAudienceCounts = function(concurrency, config) {
          if (massRefresh.active) return;
          if (concurrency) maxConcurrency = concurrency;

          const items = document.querySelectorAll('[data-audience-count="1"]');
          if (items.length === 0) return;

          massRefresh.active = true;
          massRefresh.total = items.length;
          massRefresh.processed = 0;
          massRefresh.config = config || null;

          g.updateRefreshButton();

          items.forEach(el => {
              el._isMassRefresh = true;
              el.innerHTML = '<span class="loading loading-spinner loading-xs opacity-50"></span>';
              if (!queue.includes(el)) {
                  queue.push(el);
              }
          });
          g.pumpAudienceQueue();
      };

      // Cancel pending and in-flight count requests for anchors that are about
      // to be removed from the DOM (e.g. when navigating to another page or when
      // the list container refreshes). This keeps page switches snappy and avoids
      // wasting server time on results the user will never see.
      g.purgeAudienceForContainer = function(container) {
          if (!container) return;
          // Drop queued anchors that live inside the container being swapped out.
          queue = queue.filter(function(el) { return !(el && container.contains(el)); });
          // Abort in-flight requests for anchors inside the container.
          inflightEls.forEach(function(el) {
              if (container.contains(el)) {
                  try { if (el._beezAbort && typeof el._beezAbort.abort === 'function') el._beezAbort.abort(); } catch (e) {}
              }
          });
      };

      document.addEventListener('htmx:beforeSwap', function(evt) {
          try {
              const t = evt && evt.detail && evt.detail.target;
              if (!t) return;
              // Ignore a count anchor swapping itself (a legitimate refresh).
              if (t.matches && t.matches('[data-audience-count]')) return;
              // Only act on container-level swaps that destroy managed anchors.
              if (t.querySelector && t.querySelector('[data-audience-count], [data-lazy-refresh]')) {
                  g.purgeAudienceForContainer(t);
              }
          } catch (e) {}
      });

      document.addEventListener('htmx:afterSettle', g.initAudienceObserver);
      if (document.readyState === 'complete' || document.readyState === 'interactive') {
          setTimeout(g.initAudienceObserver, 100);
      }
  })();

  // Managed Queue for Performance Dashboard Refreshes
  // The performance micro-dashboards refresh their metrics with a plain fetch()
  // triggered on intersect. When many flows are expanded and the user scrolls,
  // dozens of these multi-second requests can fire at once and overload the
  // server. This queue serializes them (single in-flight request) and pumps at
  // low priority via requestIdleCallback, mirroring the audience-count queue.
  ;(function() {
      const g = window.BeezUI = window.BeezUI || {};
      let pq = [], pqInflight = 0, pqMax = 1;

      function pqPump() {
          if (pqInflight >= pqMax || pq.length === 0) return;
          const job = pq.shift();
          pqInflight++;
          Promise.resolve()
              .then(job)
              .catch(function() {})
              .then(function() {
                  pqInflight = Math.max(0, pqInflight - 1);
                  pqSchedule();
              });
      }

      function pqSchedule() {
          if (typeof window.requestIdleCallback === 'function') {
              window.requestIdleCallback(pqPump, { timeout: 2000 });
          } else {
              setTimeout(pqPump, 200);
          }
      }

      // Enqueue a task (a function that returns a promise). Returns a promise
      // that settles when the task actually runs, so callers can await it and
      // keep their own UI state (e.g. a spinner) in sync. Heavy work is deferred
      // to idle time and never runs more than one at a time.
      g.queuePerformanceRefresh = function(task) {
          return new Promise(function(resolve, reject) {
              pq.push(function() {
                  return Promise.resolve().then(task).then(resolve, reject);
              });
              pqSchedule();
          });
      };
  })();

})()