// Shared primitives for the Zeranda dashboard kit.
const { useState, useEffect, useRef } = React;

// ---- Lucide icon, React-safe (renders into an isolated span) ----
function Icon({ name, size = 18, color, strokeWidth = 1.9, style }) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current; if (!el || !window.lucide) return;
    el.innerHTML = '';
    const i = document.createElement('i');
    i.setAttribute('data-lucide', name);
    el.appendChild(i);
    window.lucide.createIcons({ attrs: { 'stroke-width': strokeWidth, width: size, height: size } });
  }, [name, size, strokeWidth]);
  return <span className="ic" ref={ref} style={{ width: size, height: size, color, ...style }} />;
}

// tint -> [bgVar, fgVar]
const TINT = {
  gold:   ['var(--gold-100)',   'var(--gold-600)'],
  teal:   ['var(--teal-50)',    'var(--teal-500)'],
  purple: ['var(--purple-50)',  'var(--purple-500)'],
  green:  ['var(--green-50)',   'var(--green-600)'],
  blue:   ['var(--blue-50)',    'var(--blue-500)'],
  red:    ['var(--red-50)',     'var(--red-500)'],
  pink:   ['var(--pink-50)',    'var(--pink-500)'],
};
function IconChip({ icon, tint = 'gold', size = 36, radius = 10 }) {
  const [bg, fg] = TINT[tint] || TINT.gold;
  return (
    <span className="ic-chip" style={{ background: bg, width: size, height: size, borderRadius: radius }}>
      <Icon name={icon} size={Math.round(size * 0.5)} color={fg} />
    </span>
  );
}
function tintFg(t){ return (TINT[t]||TINT.gold)[1]; }
function tintBg(t){ return (TINT[t]||TINT.gold)[0]; }

function Btn({ variant = 'secondary', icon, children, onClick, style, kbd, title }) {
  return (
    <button className={`btn btn-${variant}`} onClick={onClick} style={style} title={title}>
      {icon && <Icon name={icon} size={16} />}{children}
      {kbd && <kbd className="btn-kbd">{kbd}</kbd>}
    </button>
  );
}

function SectionHeader({ title, sub, action, onAction, actionEl }) {
  return (
    <div className="section-h">
      <div>
        <div className="t">{title}</div>
        {sub && <div className="s">{sub}</div>}
      </div>
      {actionEl ? actionEl :
        action && <button className="viewall" onClick={onAction}>{action} <Icon name="arrow-right" size={14} /></button>}
    </div>
  );
}

// ---- Contextual quick-create shortcut (press C or N) ----------------
// The active screen registers its create action with useCreateShortcut().
// A single global key listener dispatches a 'zd-create' event, ignored while
// typing in a field or when a drawer/modal is already open.
if (typeof window !== 'undefined' && !window.__zdCreateBound) {
  window.__zdCreateBound = true;
  window.addEventListener('keydown', (e) => {
    if (e.metaKey || e.ctrlKey || e.altKey) return;
    const k = (e.key || '').toLowerCase();
    if (k !== 'c' && k !== 'n') return;
    const el = document.activeElement, tag = el && el.tagName;
    if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (el && el.isContentEditable)) return;
    if (document.querySelector('.drawer, .scrim')) return;   // a create flow is already open
    e.preventDefault();
    window.dispatchEvent(new CustomEvent('zd-create'));
  });
}
function useCreateShortcut(handler) {
  const ref = useRef(handler);
  ref.current = handler;
  useEffect(() => {
    const fn = () => { if (ref.current) ref.current(); };
    window.addEventListener('zd-create', fn);
    return () => window.removeEventListener('zd-create', fn);
  }, []);
}

// KPI stat strip card
function Kpi({ k, onClick }) {
  return (
    <div className="kpi rise" onClick={onClick} style={{ animationDelay: '.05s' }}>
      <div className="top">
        <span className="lbl">{k.label}</span>
        <span className="chip" style={{ background: tintBg(k.tint) }}>
          <Icon name={k.icon} size={16} color={tintFg(k.tint)} />
        </span>
      </div>
      <div className="val">{k.value}</div>
      <div className="sub">{k.sub}</div>
    </div>
  );
}

function CardEmpty({ icon = 'check-circle-2', text }) {
  return (
    <div className="card-empty">
      <Icon name={icon} size={26} color="var(--green-500)" />
      <span>{text}</span>
    </div>
  );
}

// ---- Table sorting -------------------------------------------------
function useSort(initialKey = null, initialDir = 'asc') {
  const [sortKey, setSortKey] = useState(initialKey);
  const [sortDir, setSortDir] = useState(initialDir);
  const toggle = key => {
    if (sortKey === key) { setSortDir(d => d === 'asc' ? 'desc' : 'asc'); }
    else { setSortKey(key); setSortDir('asc'); }
  };
  const apply = (rows, accessors) => {
    if (!sortKey || !accessors[sortKey]) return rows;
    const get = accessors[sortKey];
    const out = [...rows].sort((a, b) => {
      const av = get(a), bv = get(b);
      if (av < bv) return -1; if (av > bv) return 1; return 0;
    });
    return sortDir === 'asc' ? out : out.reverse();
  };
  const set = (key, dir) => { setSortKey(key); setSortDir(dir); };
  return { sortKey, sortDir, toggle, apply, set };
}

// Sortable <th>. align: 'left'|'right'. sort = the useSort object.
function SortTh({ label, k, sort, align = 'left' }) {
  const active = sort.sortKey === k;
  return (
    <th className={`sort-th ${active ? 'active' : ''}`} onClick={() => sort.toggle(k)}
      style={{ textAlign: align }}>
      <span className="sort-th-in" style={{ flexDirection: align === 'right' ? 'row-reverse' : 'row' }}>
        {label}
        <span className="sort-arrow">
          <Icon name={active ? (sort.sortDir === 'asc' ? 'arrow-up' : 'arrow-down') : 'chevrons-up-down'} size={13}
            color={active ? 'var(--gold-600)' : 'var(--fg-4)'} />
        </span>
      </span>
    </th>
  );
}

// Custom dropdown / select with a popover menu. options: [{value,label,icon,tint}]
function Dropdown({ value, options, onChange, icon, placeholder = 'Select', minWidth = 160 }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    if (!open) return;
    const h = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [open]);
  const sel = options.find(o => o.value === value);
  return (
    <div className="dd" ref={ref} style={{ minWidth }}>
      <button className={`dd-trigger ${open ? 'open' : ''}`} onClick={() => setOpen(o => !o)}>
        {icon && <Icon name={icon} size={15} color="var(--fg-3)" />}
        {sel && sel.tint && <span className="dd-dot" style={{ background: tintFg(sel.tint) }} />}
        <span className="dd-val">{sel ? sel.label : placeholder}</span>
        <Icon name="chevron-down" size={15} color="var(--fg-3)" />
      </button>
      {open && (
        <div className="dd-menu zd-scroll">
          {options.map(o => (
            <button key={o.value} className={`dd-opt ${o.value === value ? 'sel' : ''}`}
              onClick={() => { onChange(o.value); setOpen(false); }}>
              {o.icon && <span className="dd-opt-ic" style={{ background: o.tint ? tintBg(o.tint) : 'var(--surface-sunk)' }}><Icon name={o.icon} size={14} color={o.tint ? tintFg(o.tint) : 'var(--fg-3)'} /></span>}
              {!o.icon && o.tint && <span className="dd-dot" style={{ background: tintFg(o.tint) }} />}
              <span className="dd-opt-lbl">{o.label}</span>
              {o.value === value && <Icon name="check" size={15} color="var(--gold-600)" />}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

function money(str) { return parseFloat(String(str).replace(/[$,]/g, '')) || 0; }

// ---- Connected mailboxes ------------------------------------------------
// Each scanned inbox is identified by its OWN address (not a generic
// "Personal/Work" bucket), so any number of accounts — including several
// personal or several work inboxes — each read as a distinct source. Colour is
// derived deterministically from the full address; an optional nickname can
// override the label, and must be unique per account.
const MAILBOX_TINTS = ['teal','purple','blue','gold','pink','green'];
const MAILBOX_NICKS = {};   // e.g. { 'me@gmail.com': 'Personal' } — keep unique
// The inboxes currently connected, in connection order. Colour is assigned by
// slot so every connected account is visually distinct (hash is only a fallback
// for any address not in this list).
const CONNECTED_MAILBOXES = ['alex.carter@gmail.com', 'alex.c.projects@gmail.com', 'alex@acme.co', 'a.carter@globex.io'];
function mailboxMeta(addr) {
  if (!addr) return { addr:'', handle:'unknown', domain:'', label:'Unknown', tint:'gold' };
  const s = String(addr);
  const [handle = s, domain = ''] = s.split('@');
  const idx = CONNECTED_MAILBOXES.indexOf(s);
  let tint;
  if (idx >= 0) tint = MAILBOX_TINTS[idx % MAILBOX_TINTS.length];
  else { let h = 0; for (const c of s) h = (h * 31 + c.charCodeAt(0)) >>> 0; tint = MAILBOX_TINTS[h % MAILBOX_TINTS.length]; }
  return { addr: s, handle, domain, label: MAILBOX_NICKS[s] || handle, tint };
}
// Marker tying a record to its source inbox.
// variant: 'dot' | 'mini' (dot + handle, the default — small & self-describing)
//        | 'avatar' | 'tag' | 'full' (avatar + whole address, for legends).
function MailboxBadge({ addr, variant = 'mini', size = 20, title }) {
  const m = mailboxMeta(addr);
  const fg = tintFg(m.tint), bg = tintBg(m.tint);
  const tip = title || `Detected from ${m.addr}`;
  if (variant === 'dot') return <span className="mbx-dot" title={tip} style={{ background: fg }} />;
  if (variant === 'mini')
    return <span className="mbx-mini" title={tip}><span className="mbx-dot" style={{ background: fg }} />{m.label}</span>;
  const avatar = (
    <span className="mbx-av" title={tip} style={{ width:size, height:size, background:bg, color:fg }}>
      {(m.label || '?').trim()[0].toUpperCase()}
    </span>
  );
  if (variant === 'avatar') return avatar;
  if (variant === 'full')
    return <span className="mbx-tag" title={tip}>{avatar}<span className="mbx-tag-lbl"><b>{m.handle}</b><span className="mbx-dom">@{m.domain}</span></span></span>;
  return <span className="mbx-tag" title={tip}>{avatar}<span className="mbx-tag-lbl">{m.label}</span></span>;
}
// Decodes the colours currently in play (shows the full address); hides itself
// when only one inbox is used.
function MailboxLegend({ addrs, label = 'Sources' }) {
  const uniq = [...new Set((addrs || []).filter(Boolean))];
  if (uniq.length < 2) return null;
  return (
    <div className="mbx-legend">
      <span className="mbx-legend-lbl">{label}</span>
      {uniq.map(a => {
        const m = mailboxMeta(a);
        return <span className="mbx-legend-item" key={a} title={a}><span className="mbx-dot" style={{ background: tintFg(m.tint) }} />{a}</span>;
      })}
    </div>
  );
}
// Stable mailbox for any record: an explicit `mailbox` field wins, otherwise a
// deterministic pick from the connected inboxes (keyed off the record's id) so a
// record always shows the same source without needing per-row data.
function mailboxFor(record) {
  if (record && record.mailbox) return record.mailbox;
  const seed = String((record && (record.id || record.name || record.merchant)) || '');
  let h = 0; for (const c of seed) h = (h * 31 + c.charCodeAt(0)) >>> 0;
  return CONNECTED_MAILBOXES[h % CONNECTED_MAILBOXES.length];
}

// ---- Connected-accounts scope filter --------------------------------
// Reusable chip bar: "All accounts" + one chip per connected inbox, each keyed
// to the same mailbox palette used everywhere else. Optionally shows a per-
// account record count and a calm warning dot when an account is missing a given
// access scope — so the access story is visible right inside the filter.
// value is 'all' | account.id.
function AccountFilterBar({ accounts, value = 'all', onChange, counts, warnScope, warnTitle = 'Needs a reconnect' }) {
  const list = accounts || [];
  const total = counts ? (counts.all != null ? counts.all : list.reduce((s, a) => s + (counts[a.id] || 0), 0)) : null;
  const Chip = ({ id, dot, label, count, warn }) => (
    <button type="button" className={`afb-chip ${value === id ? 'on' : ''} ${warn ? 'warn' : ''}`}
      onClick={() => onChange && onChange(id)} title={warn ? warnTitle : label} role="tab" aria-selected={value === id}>
      {dot}
      <span className="afb-lbl">{label}</span>
      {count != null && <span className="afb-n">{count}</span>}
      {warn && <span className="afb-warn"><Icon name="alert-circle" size={13} color="var(--gold-700)" /></span>}
    </button>
  );
  return (
    <div className="afb zd-scroll" role="tablist" aria-label="Filter by account">
      <Chip id="all" dot={<span className="afb-all"><Icon name="layers" size={13} color={value === 'all' ? 'var(--gold-700)' : 'var(--fg-3)'} /></span>} label="All accounts" count={total} />
      {list.map(a => {
        const m = mailboxMeta(a.email);
        const warn = warnScope && !((a.granted || []).includes(warnScope));
        return <Chip key={a.id} id={a.id}
          dot={<span className="afb-dot" style={{ background: tintFg(m.tint) }} />}
          label={a.nickname || m.label} count={counts ? (counts[a.id] || 0) : null} warn={warn} />;
      })}
    </div>
  );
}

// ---- Currency / multi-currency display ------------------------------
const CCY_META = {
  USD: { sym:'$', locale:'en-US' },
  EUR: { sym:'€', locale:'de-DE' },
  GBP: { sym:'£', locale:'en-GB' },
};
function fmtCcy(amount, ccy) {
  const m = CCY_META[ccy] || { sym:'', locale:'en-US' };
  const n = Number(amount) || 0;
  return (n < 0 ? '-' : '') + m.sym + Math.abs(n).toLocaleString(m.locale, { minimumFractionDigits:2, maximumFractionDigits:2 });
}
// value of `amount` (in `ccy`) expressed in the home currency
function toHome(amount, ccy) {
  const fx = window.ZD_DATA.fx;
  return (Number(amount) || 0) * (fx.rates[ccy] ?? 1) / (fx.rates[fx.home] ?? 1);
}
// Two-line money: bold home-currency primary + muted native reference (only when foreign).
function Money({ amount, ccy, align = 'right', size, primaryClass = '', inline = false }) {
  const fx = window.ZD_DATA.fx;
  const home = fx.home;
  ccy = ccy || home;
  const foreign = ccy !== home;
  const homeAmount = foreign ? toHome(amount, ccy) : amount;
  return (
    <div className={`money ${inline ? 'money-inline' : ''}`} style={{ textAlign: align }}>
      <div className={`money-home ${primaryClass}`} style={size ? { fontSize: size } : undefined}>{fmtCcy(homeAmount, home)}</div>
      {foreign && <div className="money-native">{fmtCcy(amount, ccy)} <span className="money-code">{ccy}</span></div>}
    </div>
  );
}

// ---- Pagination -----------------------------------------------------
// Windowed page list with ellipses: 1 … 4 5 [6] 7 8 … 20
function pageWindow(cur, count) {
  if (count <= 7) return Array.from({ length: count }, (_, i) => i + 1);
  const out = [1];
  const lo = Math.max(2, cur - 1), hi = Math.min(count - 1, cur + 1);
  if (lo > 2) out.push('…');
  for (let p = lo; p <= hi; p++) out.push(p);
  if (hi < count - 1) out.push('…');
  out.push(count);
  return out;
}
function Pager({ page, pageCount, total, pageSize, onPage, noun = 'results' }) {
  if (pageCount <= 1) return null;
  const start = (page - 1) * pageSize + 1;
  const end = Math.min(page * pageSize, total);
  return (
    <div className="pager">
      <div className="pager-info">Showing <b>{start}–{end}</b> of <b>{total}</b> {noun}</div>
      <div className="pager-ctrls">
        <button className="pg-arrow" disabled={page <= 1} onClick={() => onPage(page - 1)} aria-label="Previous page"><Icon name="chevron-left" size={16} /></button>
        {pageWindow(page, pageCount).map((p, i) => p === '…'
          ? <span key={'g' + i} className="pg-gap">…</span>
          : <button key={p} className={`pg-btn ${p === page ? 'on' : ''}`} onClick={() => onPage(p)}>{p}</button>)}
        <button className="pg-arrow" disabled={page >= pageCount} onClick={() => onPage(page + 1)} aria-label="Next page"><Icon name="chevron-right" size={16} /></button>
      </div>
    </div>
  );
}

// Mobile-only sort control (column headers hide on phones). opts: [{value:'key:dir', label}]
function MobileSort({ sort, options }) {
  const value = `${sort.sortKey}:${sort.sortDir}`;
  return (
    <div className="sort-mobile">
      <Dropdown icon="arrow-up-down" minWidth={'100%'} value={value} options={options}
        onChange={v => { const [k, d] = v.split(':'); sort.set(k, d); }} />
    </div>
  );
}

// ---- Line items / contents -------------------------------------------
// One shared block for "what's in" a bill, receipt or package. Two modes:
//   priced   — money rows + a Total footer (bills, receipts)
//   contents — quantity rows, no prices, + an item-count footer (packages)
function LineItems({ items, ccy, total, mode = 'priced' }) {
  if (!items || !items.length) return null;
  const count = items.reduce((s, i) => s + (i.qty || 1), 0);
  return (
    <div className="lineitems">
      {items.map((it, i) => (
        <div className="li-row" key={i}>
          {it.qty != null && <span className="li-qty">{it.qty}×</span>}
          <div className="li-main">
            <div className="li-name">{it.label}</div>
            {it.meta && <div className="li-sub">{it.meta}</div>}
          </div>
          {mode === 'priced' && it.amt != null && (
            <span className={`li-amt ${it.amt < 0 ? 'neg' : ''}`}>{fmtCcy(it.amt, ccy)}</span>
          )}
        </div>
      ))}
      <div className="li-foot">
        {mode === 'priced' ? (
          <React.Fragment>
            <span className="lf-k">Total</span>
            <Money amount={total} ccy={ccy} size={16} primaryClass="zd-stat" />
          </React.Fragment>
        ) : (
          <span className="lf-k"><Icon name="package" size={14} color="var(--fg-3)" /><span>{count} item{count === 1 ? '' : 's'} in this package</span></span>
        )}
      </div>
    </div>
  );
}

Object.assign(window, { Icon, IconChip, Btn, SectionHeader, Kpi, CardEmpty, useSort, SortTh, Dropdown, MobileSort, money, Money, fmtCcy, toHome, CCY_META, Pager, useCreateShortcut, TINT, tintFg, tintBg, mailboxMeta, MailboxBadge, MailboxLegend, mailboxFor, AccountFilterBar, LineItems });
