/* global React, Ic */
const { useState, useEffect, useRef, useMemo, useCallback, createContext, useContext } = React;

// ============ Store ============
const StoreContext = createContext(null);

function StoreProvider({ children }) {
  const [state, setState] = useState(() => window.WMSData.load());
  const [sessionId, setSessionId] = useState(() => window.WMSData.loadSession());

  useEffect(() => { window.WMSData.save(state); }, [state]);

  // Realtime: apply state saved by other users/devices (Supabase Realtime)
  useEffect(() => {
    const client = window.__wmsSupabase;
    if (!client || !client.channel) return;
    const rowId = window.__WMS_ROW_ID || 'main';
    const ch = client
      .channel('wms_app_state_rt')
      .on('postgres_changes',
        { event: '*', schema: 'public', table: 'wms_app_state', filter: `id=eq.${rowId}` },
        (payload) => {
          const remote = payload && payload.new && payload.new.data;
          if (!remote) return;
          window.__wmsRemoteLoaded = true; // a realtime payload confirms the remote → writes may resume
          // Track the latest remote version so our next save's optimistic check is current.
          if (payload.new.updated_at) window.__wmsBaseUpdatedAt = payload.new.updated_at;
          let remoteJson;
          try { remoteJson = JSON.stringify(remote); } catch (e) { return; }
          // Ignore our own echo / already-applied updates
          if (remoteJson === window.WMSData.lastSavedJson()) return;
          window.WMSData.noteRemote(remote); // stop the save effect from re-uploading it
          setState(remote);
          window.dispatchEvent(new CustomEvent('wms-remote-update'));
        })
      .subscribe();
    return () => { try { client.removeChannel(ch); } catch (e) {} };
  }, []);

  // Save conflict: data.js pulled the winning remote state — apply it to the store.
  useEffect(() => {
    const handler = (e) => { if (e.detail) setState(e.detail); };
    window.addEventListener('wms-remote-replace', handler);
    return () => window.removeEventListener('wms-remote-replace', handler);
  }, []);

  const currentUser = useMemo(
    () => (state.users || []).find(u => u.id === sessionId) || null,
    [state.users, sessionId]
  );

  const api = useMemo(() => ({
    state,
    setState,

    // ----- auth / session -----
    currentUser,
    isAdmin: () => currentUser?.role === 'admin',
    hasPerm: (id) => {
      if (!currentUser) return false;
      if (currentUser.role === 'admin') return true;
      return (currentUser.perms || []).includes(id);
    },
    firstAllowedPage: () => {
      if (!currentUser) return 'dashboard';
      if (currentUser.role === 'admin') return 'dashboard';
      const order = ['dashboard','receive','issue','transfer','count','scanner','items','locations','stock-report','movement','lifeshelf','documents','users'];
      return order.find(p => (currentUser.perms || []).includes(p)) || 'dashboard';
    },
    login: (username, password) => {
      const u = (state.users || []).find(x => x.username.toLowerCase() === String(username).trim().toLowerCase());
      if (!u) return { ok: false, error: 'ไม่พบชื่อผู้ใช้นี้ในระบบ' };
      if (!u.active) return { ok: false, error: 'บัญชีนี้ถูกปิดใช้งาน — ติดต่อผู้ดูแลระบบ' };
      if (u.password !== password) return { ok: false, error: 'รหัสผ่านไม่ถูกต้อง' };
      const today = new Date().toISOString().slice(0, 10);
      setState(s => ({ ...s, users: s.users.map(x => x.id === u.id ? { ...x, lastLogin: today } : x) }));
      window.WMSData.saveSession(u.id);
      setSessionId(u.id);
      return { ok: true, user: u };
    },
    logout: () => { window.WMSData.clearSession(); setSessionId(null); },

    // ----- users CRUD -----
    addUser: (u) => setState(s => ({ ...s, users: [...s.users, u] })),
    updateUser: (id, patch) => setState(s => ({ ...s, users: s.users.map(u => u.id === id ? { ...u, ...patch } : u) })),
    deleteUser: (id) => setState(s => ({ ...s, users: s.users.filter(u => u.id !== id) })),
    toggleUserActive: (id) => setState(s => ({ ...s, users: s.users.map(u => u.id === id ? { ...u, active: !u.active } : u) })),

    // ----- items -----
    getItem: (sku) => state.items.find(i => i.sku === sku),
    addItem: (item) => setState(s => s.items.find(i => i.sku === item.sku) ? s : { ...s, items: [...s.items, item] }),
    updateItem: (sku, patch) => setState(s => ({ ...s, items: s.items.map(i => i.sku === sku ? { ...i, ...patch } : i) })),
    deleteItem: (sku) => setState(s => {
      const totalQty = s.stock.filter(x => x.sku === sku).reduce((a, b) => a + b.qty, 0);
      if (totalQty > 0) return s; // safety
      return {
        ...s,
        items: s.items.filter(i => i.sku !== sku),
        stock: s.stock.filter(x => x.sku !== sku), // clean up zero-qty stock rows
      };
    }),
    canDeleteItem: (sku) => {
      const totalQty = state.stock.filter(x => x.sku === sku).reduce((a, b) => a + b.qty, 0);
      return { ok: totalQty === 0, totalQty };
    },

    // ----- customers -----
    addCustomer: (name) => setState(s => {
      const trimmed = name.trim();
      if (!trimmed) return s;
      if (s.customers.find(c => c.name === trimmed)) return s;
      const id = `CUS-${String(Date.now()).slice(-5)}`;
      return { ...s, customers: [...s.customers, { id, name: trimmed }] };
    }),
    deleteCustomer: (id) => setState(s => ({ ...s, customers: s.customers.filter(c => c.id !== id) })),
    renameCustomer: (id, newName) => setState(s => {
      const trimmed = newName.trim();
      if (!trimmed) return s;
      const old = s.customers.find(c => c.id === id);
      if (!old) return s;
      return { ...s, customers: s.customers.map(c => c.id === id ? { ...c, name: trimmed } : c) };
    }),

    // ----- suppliers -----
    addSupplier: (name) => setState(s => {
      const trimmed = name.trim();
      if (!trimmed || s.suppliers.find(sp => sp.name === trimmed)) return s;
      const id = `SUP-${String(Date.now()).slice(-5)}`;
      return { ...s, suppliers: [...s.suppliers, { id, name: trimmed, contact: '—', phone: '—' }] };
    }),
    deleteSupplier: (id) => setState(s => ({ ...s, suppliers: s.suppliers.filter(sp => sp.id !== id) })),
    renameSupplier: (id, newName) => setState(s => {
      const trimmed = newName.trim();
      if (!trimmed) return s;
      const old = s.suppliers.find(sp => sp.id === id);
      if (!old) return s;
      return {
        ...s,
        suppliers: s.suppliers.map(sp => sp.id === id ? { ...sp, name: trimmed } : sp),
        // cascade rename to items that reference the old supplier name
        items: s.items.map(it => it.supplier === old.name ? { ...it, supplier: trimmed } : it),
      };
    }),

    // ----- categories -----
    addCategory: (name) => setState(s => {
      const trimmed = name.trim();
      const list = s.categories || [];
      if (!trimmed || list.find(c => c.name === trimmed)) return s;
      const id = `CAT-${String(Date.now()).slice(-5)}`;
      return { ...s, categories: [...list, { id, name: trimmed }] };
    }),
    deleteCategory: (id) => setState(s => ({ ...s, categories: (s.categories || []).filter(c => c.id !== id) })),
    renameCategory: (id, newName) => setState(s => {
      const trimmed = newName.trim();
      if (!trimmed) return s;
      const list = s.categories || [];
      const old = list.find(c => c.id === id);
      if (!old) return s;
      return {
        ...s,
        categories: list.map(c => c.id === id ? { ...c, name: trimmed } : c),
        // cascade rename to items that reference the old category name
        items: s.items.map(it => it.category === old.name ? { ...it, category: trimmed } : it),
      };
    }),

    // ----- retailers (Life Shelf Monitor) -----
    addRetailer: (r) => setState(s => {
      const name = String(r.name || '').trim();
      if (!name) return s;
      const id = `RT-${String(Date.now()).slice(-6)}`;
      return { ...s, retailers: [...(s.retailers || []), { id, name, minLife: Number(r.minLife) || 0, logo: r.logo || '', skus: [] }] };
    }),
    updateRetailer: (id, patch) => setState(s => ({
      ...s,
      retailers: (s.retailers || []).map(r => r.id === id
        ? { ...r, ...patch, ...(patch.minLife !== undefined ? { minLife: Number(patch.minLife) || 0 } : {}) }
        : r),
    })),
    deleteRetailer: (id) => setState(s => ({ ...s, retailers: (s.retailers || []).filter(r => r.id !== id) })),
    setRetailerSkus: (id, skus) => setState(s => ({
      ...s,
      retailers: (s.retailers || []).map(r => r.id === id ? { ...r, skus: [...skus] } : r),
    })),

    // ----- warehouses -----
    getWarehouse: (id) => state.warehouses.find(w => w.id === id),
    currentWarehouse: () => state.warehouses.find(w => w.id === state.currentWh) || state.warehouses[0],
    setCurrentWh: (id) => setState(s => ({ ...s, currentWh: id })),
    addWarehouse: (wh) => setState(s => ({ ...s, warehouses: [...s.warehouses, wh] })),
    updateWarehouse: (id, patch) => setState(s => ({
      ...s,
      warehouses: s.warehouses.map(w => w.id === id ? { ...w, ...patch } : w),
      locations: s.locations.map(l => l.wh === id ? { ...l, whName: patch.name || l.whName } : l),
    })),
    // Rename a warehouse ID — cascades to its locations, stock, movements, currentWh
    renameWarehouse: (oldId, newId) => setState(s => {
      newId = String(newId).trim();
      if (!newId || newId === oldId) return s;
      if (s.warehouses.find(w => w.id === newId)) return s; // duplicate guard
      const reId = (id) => (id && id.startsWith(oldId + '-')) ? newId + id.slice(oldId.length) : id;
      return {
        ...s,
        warehouses: s.warehouses.map(w => w.id === oldId ? { ...w, id: newId, code: newId } : w),
        locations: s.locations.map(l => l.wh === oldId ? { ...l, wh: newId, id: reId(l.id) } : l),
        stock: s.stock.map(x => ({ ...x, locId: reId(x.locId) })),
        movements: s.movements.map(m => ({
          ...m, from: reId(m.from), to: reId(m.to),
          fromWh: m.fromWh === oldId ? newId : m.fromWh,
          toWh: m.toWh === oldId ? newId : m.toWh,
        })),
        currentWh: s.currentWh === oldId ? newId : s.currentWh,
      };
    }),
    deleteWarehouse: (id) => setState(s => {
      // Safety: at least 1 warehouse must remain
      if (s.warehouses.length <= 1) return s;
      // Safety: no stock at any of its locations
      const whLocIds = new Set(s.locations.filter(l => l.wh === id).map(l => l.id));
      const stockHere = s.stock.filter(x => whLocIds.has(x.locId) && x.qty > 0);
      if (stockHere.length > 0) return s;
      const remaining = s.warehouses.filter(w => w.id !== id);
      return {
        ...s,
        warehouses: remaining,
        locations: s.locations.filter(l => l.wh !== id),
        stock: s.stock.filter(x => !whLocIds.has(x.locId)),
        currentWh: s.currentWh === id ? remaining[0].id : s.currentWh,
      };
    }),
    canDeleteWarehouse: (id) => {
      if (state.warehouses.length <= 1) return { ok: false, reason: 'ต้องมีอย่างน้อย 1 คลัง' };
      const whLocIds = new Set(state.locations.filter(l => l.wh === id).map(l => l.id));
      const totalStock = state.stock.filter(x => whLocIds.has(x.locId)).reduce((a, b) => a + b.qty, 0);
      if (totalStock > 0) return { ok: false, reason: `ยังมีสต็อก ${totalStock} units ใน ${whLocIds.size} locations`, totalStock, locCount: whLocIds.size };
      return { ok: true, locCount: whLocIds.size };
    },

    // ----- locations -----
    addLocation: (loc) => setState(s => ({ ...s, locations: [...s.locations, loc] })),
    getLocationsByWh: (whId) => state.locations.filter(l => l.wh === whId),
    updateLocation: (oldId, patch) => setState(s => {
      // Compute new ID from patch fields (zone/rack/shelf)
      const loc = s.locations.find(l => l.id === oldId);
      if (!loc) return s;
      const merged = { ...loc, ...patch };
      const newId = `${merged.wh}-${merged.zone}-${merged.rack}-${merged.shelf}`;
      return {
        ...s,
        locations: s.locations.map(l => l.id === oldId ? { ...merged, id: newId } : l),
        stock: oldId === newId ? s.stock : s.stock.map(x => x.locId === oldId ? { ...x, locId: newId } : x),
        movements: oldId === newId ? s.movements : s.movements.map(m => ({
          ...m,
          from: m.from === oldId ? newId : m.from,
          to: m.to === oldId ? newId : m.to,
        })),
      };
    }),
    deleteLocation: (id) => setState(s => {
      const stockHere = s.stock.filter(x => x.locId === id && x.qty > 0);
      if (stockHere.length > 0) return s; // safety
      return {
        ...s,
        locations: s.locations.filter(l => l.id !== id),
        stock: s.stock.filter(x => x.locId !== id),
      };
    }),
    canDeleteLocation: (id) => state.stock.filter(x => x.locId === id).reduce((a, b) => a + b.qty, 0) === 0,
    // Rename a zone's display name (e.g. Receiving -> something) + cascade to its locations
    renameZone: (whId, zone, newName) => setState(s => {
      const trimmed = String(newName).trim();
      if (!trimmed) return s;
      return {
        ...s,
        warehouses: s.warehouses.map(w => w.id === whId
          ? { ...w, zones: (w.zones || []).map(z => z.zone === zone ? { ...z, name: trimmed } : z) }
          : w),
        locations: s.locations.map(l => (l.wh === whId && l.zone === zone) ? { ...l, zoneName: trimmed } : l),
      };
    }),

    // ----- stock -----
    getStockBySku: (sku) => state.stock.filter(x => x.sku === sku),
    getStockByLoc: (locId) => state.stock.filter(x => x.locId === locId),
    getTotalQty: (sku) => state.stock.filter(x => x.sku === sku).reduce((a, b) => a + b.qty, 0),
    getZoneTotal: (zone) => {
      const locIds = state.locations.filter(l => l.zone === zone).map(l => l.id);
      return state.stock.filter(s => locIds.includes(s.locId)).reduce((a, b) => a + b.qty, 0);
    },

    // ----- transactions -----
    receive: (lines, ref = null) => {
      setState(s => {
        const newStock = [...s.stock];
        const newMov = [];
        const docNo = `GR-${s.docs.gr + 1}`;
        // Primary doc reference = the user's PO Reference if given, else the auto GR number
        const docRef = (ref && String(ref).trim()) || docNo;
        const day = 86400000;
        lines.forEach(line => {
          const item = s.items.find(i => i.sku === line.sku);
          const sl = item?.shelfLife || 0;
          const mfgDate = line.mfgDate || Date.now();
          const expiry = sl > 0 ? mfgDate + sl * day : (line.expiry || null);
          // For expiry-tracked items, append as separate lot row instead of merging
          // (different mfgDate => different lot, FEFO requires distinct rows)
          const mergeIdx = sl === 0
            ? newStock.findIndex(x => x.sku === line.sku && x.locId === line.locId)
            : newStock.findIndex(x => x.sku === line.sku && x.locId === line.locId && x.expiry === expiry);
          if (mergeIdx >= 0) newStock[mergeIdx] = { ...newStock[mergeIdx], qty: newStock[mergeIdx].qty + line.qty };
          else newStock.push({
            sku: line.sku, locId: line.locId, qty: line.qty,
            lot: line.lot || `L${Date.now().toString(36).toUpperCase().slice(-6)}`,
            mfgDate, expiry,
          });
          newMov.push({ id: `MV${(s.docs.mv + newMov.length + 1)}`, ts: Date.now(), type: 'IN', sku: line.sku, qty: line.qty, to: line.locId, ref: docRef, docNo, mfgDate, expiry, user: currentUser?.name || 'คุณ (You)' });
        });
        return {
          ...s,
          stock: newStock,
          movements: [...newMov, ...s.movements].slice(0, window.WMSData.MOVEMENT_CAP || 2000),
          docs: { ...s.docs, gr: s.docs.gr + 1, mv: s.docs.mv + newMov.length },
        };
      });
    },

    issue: (lines, ref = null, customer = null) => {
      setState(s => {
        const newStock = [...s.stock];
        const newMov = [];
        const docNo = `GI-${s.docs.gi + 1}`;
        // Primary doc reference = the user's SO Reference if given, else the auto GI number
        const docRef = (ref && String(ref).trim()) || docNo;
        const dest = (customer && String(customer).trim()) || null;
        lines.forEach(line => {
          // Match by sku + locId + lot (lot-precise), fallback to sku+locId
          const idx = line.lot
            ? newStock.findIndex(x => x.sku === line.sku && x.locId === line.locId && x.lot === line.lot)
            : newStock.findIndex(x => x.sku === line.sku && x.locId === line.locId);
          if (idx >= 0) newStock[idx] = { ...newStock[idx], qty: Math.max(0, newStock[idx].qty - line.qty) };
          newMov.push({
            id: `MV${(s.docs.mv + newMov.length + 1)}`,
            ts: Date.now(), type: 'OUT',
            sku: line.sku, qty: line.qty,
            lot: line.lot,
            from: line.locId,
            to: dest,          // customer / destination for goods issue
            customer: dest,
            ref: docRef,
            docNo,
            soRef: (ref && String(ref).trim()) || null,
            user: currentUser?.name || 'คุณ (You)',
          });
        });
        return {
          ...s,
          stock: newStock,
          movements: [...newMov, ...s.movements].slice(0, window.WMSData.MOVEMENT_CAP || 2000),
          docs: { ...s.docs, gi: s.docs.gi + 1, mv: s.docs.mv + newMov.length },
        };
      });
    },

    transfer: (sku, qty, fromLoc, toLoc, lot) => {
      setState(s => {
        const newStock = [...s.stock];
        // Deduct from specific lot (lot-precise)
        const fi = lot
          ? newStock.findIndex(x => x.sku === sku && x.locId === fromLoc && x.lot === lot)
          : newStock.findIndex(x => x.sku === sku && x.locId === fromLoc);
        const srcRow = fi >= 0 ? newStock[fi] : null;
        if (fi >= 0) newStock[fi] = { ...newStock[fi], qty: Math.max(0, newStock[fi].qty - qty) };

        // Add to destination — match by sku + locId + lot (no merge across lots)
        // Works across warehouses: toLoc can be in a different WH
        const srcLot = srcRow?.lot;
        const srcExpiry = srcRow?.expiry;
        const srcMfgDate = srcRow?.mfgDate;
        const ti = newStock.findIndex(x => x.sku === sku && x.locId === toLoc && x.lot === srcLot);
        if (ti >= 0) newStock[ti] = { ...newStock[ti], qty: newStock[ti].qty + qty };
        else newStock.push({ sku, locId: toLoc, qty, lot: srcLot || `L${Date.now()}`, expiry: srcExpiry || null, mfgDate: srcMfgDate || null });

        // Record which warehouses are involved (inter-WH or intra-WH)
        const fromWhId = s.locations.find(l => l.id === fromLoc)?.wh;
        const toWhId = s.locations.find(l => l.id === toLoc)?.wh;
        const interWH = fromWhId !== toWhId;

        const mv = {
          id: `MV${s.docs.mv + 1}`, ts: Date.now(), type: 'TRANSFER',
          sku, qty, lot: srcLot,
          from: fromLoc, to: toLoc,
          fromWh: fromWhId, toWh: toWhId,
          interWH,
          ref: `TR-${s.docs.gi + 100}`, user: currentUser?.name || 'คุณ (You)',
        };
        return { ...s, stock: newStock, movements: [mv, ...s.movements].slice(0, window.WMSData.MOVEMENT_CAP || 2000), docs: { ...s.docs, mv: s.docs.mv + 1 } };
      });
    },

    adjustCount: (sku, locId, newQty, note = '', lot = null) => {
      setState(s => {
        const newStock = [...s.stock];
        // lot-precise match: if lot provided, match sku+locId+lot; else first sku+locId
        const idx = lot
          ? newStock.findIndex(x => x.sku === sku && x.locId === locId && x.lot === lot)
          : newStock.findIndex(x => x.sku === sku && x.locId === locId);
        const oldQty = idx >= 0 ? newStock[idx].qty : 0;
        const diff = newQty - oldQty;
        if (idx >= 0) newStock[idx] = { ...newStock[idx], qty: newQty };
        else newStock.push({ sku, locId, qty: newQty, lot: lot || `L${Date.now()}` });
        const mv = { id: `MV${s.docs.mv + 1}`, ts: Date.now(), type: 'COUNT', sku, qty: Math.abs(diff), delta: diff, lot, from: locId, ref: `CT-${s.docs.ct + 1}`, note, user: currentUser?.name || 'คุณ (You)' };
        return { ...s, stock: newStock, movements: [mv, ...s.movements].slice(0, window.WMSData.MOVEMENT_CAP || 2000), docs: { ...s.docs, mv: s.docs.mv + 1, ct: s.docs.ct + 1 } };
      });
    },

    // ----- admin: clear test/mock data (keeps users, categories, suppliers, customers, warehouses) -----
    clearItems: () => setState(s => ({ ...s, items: [], stock: [] })),       // products + their stock
    clearStock: () => setState(s => ({ ...s, stock: [] })),                   // balances only, keep products
    clearMovements: () => setState(s => ({ ...s, movements: [] })),           // history only
    clearLocations: () => setState(s => ({ ...s, locations: [], stock: [] })),// bins + stock at them, keep warehouses
    clearAllData: () => setState(s => ({ ...s, items: [], stock: [], movements: [], locations: [] })),
  }), [state, currentUser, sessionId]);

  return React.createElement(StoreContext.Provider, { value: api }, children);
}

const useStore = () => useContext(StoreContext);

// ============ Toast ============
const ToastContext = createContext(null);
function ToastProvider({ children }) {
  const [toasts, setToasts] = useState([]);
  const push = useCallback((t) => {
    const id = Math.random().toString(36).slice(2);
    setToasts(prev => [...prev, { id, ...t }]);
    setTimeout(() => setToasts(prev => prev.map(x => x.id === id ? { ...x, out: true } : x)), 3200);
    setTimeout(() => setToasts(prev => prev.filter(x => x.id !== id)), 3600);
  }, []);
  const api = { push, success: (title, msg) => push({ type: 'success', title, msg }), error: (title, msg) => push({ type: 'error', title, msg }), info: (title, msg) => push({ type: 'info', title, msg }) };
  return (
    <ToastContext.Provider value={api}>
      {children}
      <div className="toast-wrap">
        {toasts.map(t => (
          <div key={t.id} className={`toast ${t.type} ${t.out ? 'out' : ''}`}>
            <div className="toast-icon">
              <Ic name={t.type === 'success' ? 'check' : t.type === 'error' ? 'alert' : 'info'} size={15}/>
            </div>
            <div className="toast-body">
              <div className="toast-title">{t.title}</div>
              {t.msg && <div className="toast-msg">{t.msg}</div>}
            </div>
          </div>
        ))}
      </div>
    </ToastContext.Provider>
  );
}
const useToast = () => useContext(ToastContext);

// ============ Confetti ============
function fireConfetti() {
  const canvas = document.createElement('canvas');
  canvas.className = 'confetti-canvas';
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  document.body.appendChild(canvas);
  const ctx = canvas.getContext('2d');
  const colors = ['#22d3ee', '#10d29a', '#4d8bff', '#a78bfa', '#f5b32a'];
  const N = 110;
  const cx = window.innerWidth / 2;
  const cy = window.innerHeight * 0.42;
  const parts = Array.from({ length: N }, () => ({
    x: cx, y: cy,
    vx: (Math.random() - 0.5) * 14,
    vy: -Math.random() * 12 - 4,
    g: 0.35 + Math.random() * 0.15,
    r: 4 + Math.random() * 4,
    rot: Math.random() * Math.PI,
    vr: (Math.random() - 0.5) * 0.3,
    color: colors[Math.floor(Math.random() * colors.length)],
    life: 1,
  }));
  let frame = 0;
  function loop() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    parts.forEach(p => {
      p.x += p.vx; p.y += p.vy; p.vy += p.g; p.vx *= 0.992; p.rot += p.vr;
      p.life -= 0.008;
      ctx.save();
      ctx.translate(p.x, p.y); ctx.rotate(p.rot);
      ctx.fillStyle = p.color;
      ctx.globalAlpha = Math.max(0, p.life);
      ctx.fillRect(-p.r/2, -p.r/2, p.r, p.r * 0.5);
      ctx.restore();
    });
    frame++;
    if (frame < 130) requestAnimationFrame(loop);
    else canvas.remove();
  }
  loop();
}

// ============ Modal ============
function Modal({ open, onClose, title, children, footer, width }) {
  if (!open) return null;
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" style={{ width: width || 540 }} onClick={e => e.stopPropagation()}>
        <div className="modal-head">
          <div className="modal-title">{title}</div>
          <button className="icon-btn" onClick={onClose}><Ic name="x" size={16}/></button>
        </div>
        <div className="modal-body">{children}</div>
        {footer && <div className="modal-foot">{footer}</div>}
      </div>
    </div>
  );
}

// ============ Animated number ============
function AnimNumber({ value, duration = 900, decimals = 0, prefix = '', suffix = '' }) {
  const [n, setN] = useState(value);
  const ref = useRef({ start: 0, from: value, to: value, timer: 0 });
  const mounted = useRef(false);
  useEffect(() => {
    const from = mounted.current ? n : 0;
    mounted.current = true;
    ref.current.from = from;
    ref.current.to = value;
    ref.current.start = performance.now();
    clearInterval(ref.current.timer);
    const tick = () => {
      const t = performance.now();
      const p = Math.min(1, (t - ref.current.start) / duration);
      const eased = 1 - Math.pow(1 - p, 3);
      const cur = ref.current.from + (ref.current.to - ref.current.from) * eased;
      setN(cur);
      if (p >= 1) clearInterval(ref.current.timer);
    };
    ref.current.timer = setInterval(tick, 16);
    tick();
    return () => clearInterval(ref.current.timer);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [value]);
  const fmt = (v) => Math.round(v).toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
  return <span>{prefix}{fmt(n)}{suffix}</span>;
}

// ============ Mini line chart (sparkline) ============
function Sparkline({ data, color = 'currentColor', height = 36, fill = true }) {
  if (!data || data.length === 0) return null;
  const max = Math.max(...data);
  const min = Math.min(...data);
  const range = max - min || 1;
  const w = 100;
  const points = data.map((d, i) => {
    const x = (i / (data.length - 1)) * w;
    const y = height - ((d - min) / range) * (height - 4) - 2;
    return [x, y];
  });
  const path = points.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' ');
  const areaPath = path + ` L${w},${height} L0,${height} Z`;
  const gid = useMemo(() => `gs${Math.random().toString(36).slice(2,8)}`, []);
  return (
    <svg viewBox={`0 0 ${w} ${height}`} preserveAspectRatio="none" className="chart-svg" style={{ height }}>
      <defs>
        <linearGradient id={gid} x1="0" x2="0" y1="0" y2="1">
          <stop offset="0%" stopColor={color} stopOpacity="0.35"/>
          <stop offset="100%" stopColor={color} stopOpacity="0"/>
        </linearGradient>
      </defs>
      {fill && <path d={areaPath} fill={`url(#${gid})`} className="chart-area"/>}
      <path d={path} fill="none" stroke={color} strokeWidth="1.4" className="chart-line"/>
    </svg>
  );
}

Object.assign(window, { StoreProvider, useStore, ToastProvider, useToast, Modal, AnimNumber, Sparkline, fireConfetti });

// ============ Page entry animation (React mount-state, immune to DOM patches) ============
function useEntryAnim() {
  const [ready, setReady] = React.useState(false);
  React.useEffect(() => {
    const id = requestAnimationFrame(() => setReady(true));
    return () => cancelAnimationFrame(id);
  }, []);
  return {
    opacity: ready ? 1 : 0,
    transform: ready ? 'none' : 'translateY(6px)',
    transition: ready ? 'opacity 0.32s ease, transform 0.32s ease' : 'none',
  };
}

// PageWrap: replaces <div className="page-enter"> in every Page component
function PageWrap({ children }) {
  const style = useEntryAnim();
  return <div style={style}>{children}</div>;
}

// ============ Expiry helpers ============
const DAY = 86400000;

function daysUntil(ts) {
  if (!ts) return null;
  return Math.floor((ts - Date.now()) / DAY);
}

function expiryStatus(expiry) {
  if (!expiry) return { cls: 'none', label: '—', days: null };
  const d = daysUntil(expiry);
  if (d < 0) return { cls: 'expired', label: 'EXPIRED', days: d, badge: 'crit' };
  if (d <= 30) return { cls: 'crit', label: `${d}d LEFT`, days: d, badge: 'crit' };
  if (d <= 90) return { cls: 'warn', label: `${d}d`, days: d, badge: 'warn' };
  if (d <= 180) return { cls: 'soon', label: `${d}d`, days: d, badge: 'info' };
  return { cls: 'ok', label: `${d}d`, days: d, badge: 'ok' };
}

function fmtDate(ts) {
  if (!ts) return '—';
  const d = new Date(ts);
  return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: '2-digit' });
}

function fmtDateInput(ts) {
  if (!ts) return '';
  const d = new Date(ts);
  return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
}

function parseDateInput(s) {
  if (!s) return null;
  const [y, m, d] = s.split('-').map(Number);
  return new Date(y, m - 1, d).getTime();
}

function fmtShelfLife(days) {
  if (!days) return '—';
  if (days < 30) return `${days} วัน`;
  if (days < 365) return `${Math.round(days / 30)} เดือน`;
  const y = Math.floor(days / 365);
  const rem = days - y * 365;
  return rem > 30 ? `${y}.${Math.round(rem / 365 * 10)} ปี` : `${y} ปี`;
}

// Build + download a CSV file (flat/tabular). BOM + CRLF so Thai opens correctly in Excel.
function downloadCSV(filename, headers, rows) {
  const esc = (v) => {
    const s = (v === null || v === undefined) ? '' : String(v);
    return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
  };
  const lines = [headers.map(esc).join(',')].concat(rows.map(r => r.map(esc).join(',')));
  const csv = '﻿' + lines.join('\r\n');
  const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = filename;
  document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1500);
}

// Order items by Thai/!A-Z name, natural numeric (so "...01" comes before "...02"/"...10")
function cmpItem(a, b) {
  return String((a && (a.name_th || a.name)) || '').localeCompare(
    String((b && (b.name_th || b.name)) || ''), 'th', { numeric: true, sensitivity: 'base' });
}

// Order locations by Zone (A,B,C…) then Rack then Shelf, A-Z / 0-9 (natural numeric)
function cmpLoc(a, b) {
  const nz = { numeric: true, sensitivity: 'base' };
  return String(a.zone || '').localeCompare(String(b.zone || ''), 'en', nz)
    || String(a.rack || '').localeCompare(String(b.rack || ''), 'en', nz)
    || String(a.shelf || '').localeCompare(String(b.shelf || ''), 'en', nz)
    || String(a.id || '').localeCompare(String(b.id || ''), 'en', nz);
}

Object.assign(window, { StoreProvider, useStore, ToastProvider, useToast, Modal, AnimNumber, Sparkline, fireConfetti, useEntryAnim, PageWrap, cmpLoc, cmpItem, downloadCSV });
