// =============================================================
// Shared utilities — formatters, icons, primitives
// =============================================================

const { useState, useEffect, useMemo, useRef, useCallback } = React;

// ---------- Formatters ----------
const fmtEUR = (n, opts = {}) => {
  if (n === null || n === undefined || !isFinite(n)) return "—";
  const { decimals = 2, showSign = false } = opts;
  const sign = showSign && n > 0 ? "+" : "";
  return sign + n.toLocaleString("fr-FR", { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
};
const fmtJPY = (n) => {
  if (n === null || n === undefined) return "—";
  return Math.round(n).toLocaleString("fr-FR");
};
const fmtInt = (n) => (n === null || n === undefined ? "—" : Math.round(n).toLocaleString("fr-FR"));
const fmtDate = (s) => {
  if (!s) return "—";
  const d = new Date(s);
  return d.toLocaleDateString("fr-FR", { day: "2-digit", month: "short", year: "2-digit" });
};
const fmtDateShort = (s) => {
  if (!s) return "—";
  const d = new Date(s);
  return d.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit" });
};

// ---------- Money components ----------
const Money = ({ eur, jpy, neg, pos, decimals = 2, showJpy = true, rate }) => {
  const cls = ["money"];
  if (neg || (eur !== undefined && eur < 0)) cls.push("neg");
  if (pos) cls.push("pos");
  return (
    <span className={cls.join(" ")}>
      <span className="cur">€</span>
      <span className="amt">{fmtEUR(eur, { decimals })}</span>
    </span>
  );
};

const MoneyPair = ({ eur, jpy, decimals = 2 }) => (
  <span className="money-pair">
    <Money eur={eur} decimals={decimals} />
    {jpy !== undefined && jpy !== null && (
      <span className="jpy">¥{fmtJPY(jpy)}</span>
    )}
  </span>
);

// ---------- Icons (inline SVG, 14×14) ----------
const Icon = ({ name, size = 14 }) => {
  const s = size;
  const common = { width: s, height: s, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" };
  const map = {
    home: <svg {...common}><path d="M2 7l6-5 6 5v7a1 1 0 0 1-1 1h-3v-5H6v5H3a1 1 0 0 1-1-1z"/></svg>,
    box: <svg {...common}><path d="M2 5l6-3 6 3v6l-6 3-6-3z"/><path d="M2 5l6 3 6-3M8 8v6"/></svg>,
    cart: <svg {...common}><circle cx="6" cy="14" r="1"/><circle cx="12" cy="14" r="1"/><path d="M1 2h2l2 9h8l2-7H4"/></svg>,
    layers: <svg {...common}><path d="M8 2l6 3-6 3-6-3z"/><path d="M2 8l6 3 6-3M2 11l6 3 6-3"/></svg>,
    tag: <svg {...common}><path d="M2 8V2h6l6 6-6 6z"/><circle cx="5.5" cy="5.5" r=".8" fill="currentColor"/></svg>,
    wallet: <svg {...common}><rect x="1.5" y="4" width="13" height="9" rx="1"/><path d="M11 8h2"/><path d="M1.5 5V3a1 1 0 0 1 1-1h9"/></svg>,
    plus: <svg {...common}><path d="M8 3v10M3 8h10"/></svg>,
    arrow: <svg {...common}><path d="M3 8h10M9 4l4 4-4 4"/></svg>,
    chev: <svg {...common}><path d="M6 4l4 4-4 4"/></svg>,
    close: <svg {...common}><path d="M3 3l10 10M13 3L3 13"/></svg>,
    search: <svg {...common}><circle cx="7" cy="7" r="4.5"/><path d="M11 11l3 3"/></svg>,
    chart: <svg {...common}><path d="M2 13h12M4 11V7M7 11V4M10 11V8M13 11V6"/></svg>,
    download: <svg {...common}><path d="M8 2v8M4 7l4 4 4-4M2 14h12"/></svg>,
    edit: <svg {...common}><path d="M11 2l3 3-8 8H3v-3z"/></svg>,
    trash: <svg {...common}><path d="M2 4h12M5 4V2h6v2M6 7v6M10 7v6M3 4l1 10h8l1-10"/></svg>,
    refresh: <svg {...common}><path d="M14 3v4h-4M2 13v-4h4"/><path d="M3.5 6a5 5 0 0 1 9 0M12.5 10a5 5 0 0 1-9 0"/></svg>,
    check: <svg {...common}><path d="M3 8l3 3 7-7"/></svg>,
    minus: <svg {...common}><path d="M3 8h10"/></svg>,
    external: <svg {...common}><path d="M9 2h5v5M14 2L7 9M11 8v5H2V4h5"/></svg>,
    filter: <svg {...common}><path d="M2 3h12l-4.5 6V14L6.5 12V9z"/></svg>,
    dots: <svg {...common}><circle cx="3" cy="8" r=".8" fill="currentColor"/><circle cx="8" cy="8" r=".8" fill="currentColor"/><circle cx="13" cy="8" r=".8" fill="currentColor"/></svg>,
    yen: <svg {...common}><path d="M3 2l5 6 5-6M5 8h6M5 11h6M8 8v6"/></svg>,
    truck: <svg {...common}><path d="M1.5 4h8v7h-8z"/><path d="M9.5 7h3l2 2v2h-5"/><circle cx="4" cy="12.5" r="1.2"/><circle cx="11.5" cy="12.5" r="1.2"/></svg>,
    package: <svg {...common}><path d="M8 1.5l6 3v6L8 13.5l-6-3v-6z"/><path d="M2 4.5L8 7.5l6-3M8 7.5v6"/></svg>,
    gauge: <svg {...common}><path d="M2 12a6 6 0 1 1 12 0"/><path d="M8 12l3-4"/></svg>,
    undo: <svg {...common}><path d="M4 6H10a4 4 0 0 1 0 8H8"/><path d="M4 6L7 3M4 6l3 3"/></svg>,
    settings: <svg {...common} viewBox="0 0 24 24" width={s} height={s}><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>,
  };
  return map[name] || null;
};

// ---------- Status helpers ----------
const STATUS_LABELS = {
  delivered: "Livré",
  in_transit: "En transit",
  ordered: "Commandé",
  created: "Créé",
  sold: "Vendu",
  finalized: "Finalisé",
  available: "Dispo",
};
const StatusPill = ({ status }) => (
  <span className="tag tag-ghost">
    <span className={`status-dot ${status}`}></span>
    {STATUS_LABELS[status] || status}
  </span>
);

// ---------- Rechargement colis : couverture & statut (partage PC + mobile) ----------
// Total JPY a recharger sur ZenMarket pour un colis (hors douane EUR).
function shipRechargeTotalJpy(shipment, purchases) {
  if (!shipment || (shipment.currency || "JPY") !== "JPY") return 0;
  var items = (purchases || []).filter(function (p) { return p.ship === shipment.id; });
  var itemsJpy = items.reduce(function (a, p) { return a + (p.jpy || 0) + (p.port_jp || 0); }, 0);
  var portJpy = shipment.port_jpy || shipment.intl_shipping_jpy || 0;
  var zm = items.reduce(function (a, p) { return a + (p.zm_fee || 0); }, 0);
  return itemsJpy + portJpy + zm;
}
// Couverture en % (topups lies / total du). null si total inconnu (pas JPY / pas d'achats).
function shipRechargePct(shipment, topups, purchases) {
  var totalJpy = shipRechargeTotalJpy(shipment, purchases);
  if (totalJpy <= 0) return null;
  var topupsJpy = (topups || [])
    .filter(function (t) { return t.shipment === shipment.id; })
    .reduce(function (a, t) { return a + (t.jpy || 0); }, 0);
  return Math.round(topupsJpy / totalJpy * 100);
}
// Statut rechargement -> {type, label, color, pct}. Validation manuelle prioritaire.
// Confirme seulement si couverture 95-100% ; <95 = manque ; >100 = surplus.
function shipRechargeStatus(shipment, topups, purchases) {
  if (shipment && shipment.recharge_confirmed) {
    var _tot = shipRechargeTotalJpy(shipment, purchases);
    var _tj = (topups || []).filter(function (t) { return t.shipment === shipment.id; }).reduce(function (a, t) { return a + (t.jpy || 0); }, 0);
    return { type: "manual", label: "✓ Validé", color: "pos", pct: shipRechargePct(shipment, topups, purchases), deltaJpy: _tot > 0 ? (_tj - _tot) : null };
  }
  var linked = (topups || []).filter(function (t) { return t.shipment === shipment.id; });
  var pct = shipRechargePct(shipment, topups, purchases);
  if (linked.length === 0) {
    if (shipment && shipment.rate) return { type: "estimation", label: "Estimation", color: "ghost", pct: pct };
    return { type: "none", label: "À recharger", color: "warn", pct: 0 };
  }
  var allHaveEur = linked.every(function (t) { return !!t.eur; });
  if (!allHaveEur) return { type: "pending", label: "€ à compléter", color: "warn", pct: pct };
  if (pct === null) return { type: "confirmed", label: "✓ Confirmé", color: "pos", pct: null };
  var totalJpy = shipRechargeTotalJpy(shipment, purchases);
  var topupsJpy = linked.reduce(function (a, t) { return a + (t.jpy || 0); }, 0);
  var deltaJpy = topupsJpy - totalJpy;
  if (pct < 95) return { type: "under", label: "Manque " + fmtJPY(totalJpy - topupsJpy) + "¥ (" + pct + "%)", color: "warn", pct: pct, deltaJpy: deltaJpy };
  if (pct > 100) return { type: "surplus", label: "Surplus +" + fmtJPY(topupsJpy - totalJpy) + "¥ (" + pct + "%)", color: "neg", pct: pct, deltaJpy: deltaJpy };
  return { type: "confirmed", label: "✓ Confirmé " + pct + "%", color: "pos", pct: pct, deltaJpy: deltaJpy };
}
// "Assez rechargé" -> exclure du selecteur de colis (couverture >=95% OU validation manuelle).
function shipIsRecharged(shipment, topups, purchases) {
  if (!shipment) return false;
  if (shipment.recharge_confirmed) return true;
  var pct = shipRechargePct(shipment, topups, purchases);
  return pct !== null && pct >= 95;
}
// Numero d'affichage d'un colis, COHERENT partout (commandes + rechargements).
// Utilise le displayNum du backend si present, sinon le recalcule (tri date ASC puis id ASC)
// pour ne pas retomber sur l'id brut sur un colis fraichement cree (optimistic, avant resync).
function shipNum(shipment, allShipments) {
  if (!shipment) return "?";
  if (shipment.displayNum) return shipment.displayNum;
  // fallback : recalculer sur la LISTE COMPLETE des colis. On privilegie la reference
  // globale (toujours la liste complete) car la liste passee peut etre filtree/partielle
  // (ex: selecteur qui masque les colis recharges) -> l'index repartirait a 1.
  var globalList = (typeof window !== "undefined" && window.__allShipments) || null;
  var list = (globalList && globalList.length) ? globalList : (allShipments || []);
  var sorted = list.slice().sort(function (a, b) {
    var da = a.date || "", db = b.date || "";
    if (da < db) return -1;
    if (da > db) return 1;
    return (a.id || 0) - (b.id || 0);
  });
  for (var i = 0; i < sorted.length; i++) {
    if (sorted[i].id === shipment.id) return i + 1;
  }
  return shipment.displayNum || shipment.id;
}

// ---------- Toast ----------
let toastTimer = null;
const useToast = () => {
  const [toast, setToast] = useState(null);
  const show = useCallback((msg, onUndo) => {
    setToast({ msg, onUndo });
    clearTimeout(toastTimer);
    toastTimer = setTimeout(() => setToast(null), 4500);
  }, []);
  const hide = useCallback(() => { clearTimeout(toastTimer); setToast(null); }, []);
  const node = toast ? (
    <div className="toast">
      <Icon name="check" /> <span>{toast.msg}</span>
      {toast.onUndo && (
        <button
          onClick={() => { toast.onUndo(); hide(); }}
          style={{
            marginLeft: 8, padding: "3px 10px", border: "1px solid oklch(0.5 0.01 60)",
            background: "transparent", color: "var(--bg)", borderRadius: 4,
            fontSize: 12, fontWeight: 500, fontFamily: "inherit"
          }}
        >
          Annuler <span style={{ opacity: 0.6, marginLeft: 4, fontFamily: "var(--font-mono)", fontSize: 10 }}>⌘Z</span>
        </button>
      )}
    </div>
  ) : null;
  return { show, hide, node };
};

// ---------- Drawer ----------
const Drawer = ({ open, onClose, title, sub, footer, wide, children }) => {
  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose]);
  if (!open) return null;
  return (
    <React.Fragment>
      <div className="drawer-backdrop" onClick={onClose}></div>
      <aside className={`drawer ${wide ? "wide" : ""}`}>
        <div className="drawer-head">
          <div>
            <div className="drawer-title">{title}</div>
            {sub && <div className="drawer-sub">{sub}</div>}
          </div>
          <button className="btn btn-ghost btn-icon" onClick={onClose}>
            <Icon name="close" />
          </button>
        </div>
        <div className="drawer-body">{children}</div>
        {footer && <div className="drawer-foot">{footer}</div>}
      </aside>
    </React.Fragment>
  );
};

// ---------- Sparkline ----------
const Sparkline = ({ data, height = 38 }) => {
  const W = 220;
  const H = height;
  if (!data || data.length === 0) return null;
  const max = Math.max(...data);
  const min = Math.min(...data);
  const range = Math.max(1, max - min);
  const pts = data.map((v, i) => {
    const x = (i / (data.length - 1)) * W;
    const y = H - 4 - ((v - min) / range) * (H - 8);
    return [x, y];
  });
  const path = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(" ");
  const area = path + ` L${W},${H} L0,${H} Z`;
  return (
    <svg className="sparkline" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none">
      <path className="area" d={area} />
      <path d={path} />
    </svg>
  );
};

// ---------- Toolbar / Tabs ----------
const Tabs = ({ value, onChange, options }) => (
  <div className="tabs">
    {options.map(opt => (
      <button key={opt.value}
              className={value === opt.value ? "active" : ""}
              onClick={() => onChange(opt.value)}>
        {opt.label}
      </button>
    ))}
  </div>
);

// ---------- NewCatInline ----------
// Affiche un select avec option "+ Nouvelle collection...".
// Quand selectionne, bascule sur un input inline + bouton Creer.
// Props :
//   categories   : tableau { id, name }
//   value        : id selectionne (number | string)
//   onChange     : fn(id) appelee apres selection ou creation
//   actions      : { createCategory(name) => Promise<cat> }
//   selectStyle  : styles supplementaires pour le <select> (optionnel)
//   placeholder  : texte initial du select (optionnel, ex: "— Choisir —")
const NewCatInline = ({ categories, value, onChange, actions, selectStyle, placeholder }) => {
  const [creating, setCreating] = useState(false);
  const [name, setName] = useState("");
  const [short, setShort] = useState("");
  const [loading, setLoading] = useState(false);
  const inputRef = useRef(null);

  // focus automatique quand le champ apparait
  useEffect(() => {
    if (creating && inputRef.current) inputRef.current.focus();
  }, [creating]);

  const handleCreate = async () => {
    const trimmedName = name.trim();
    if (!trimmedName) return;
    // short vide -> null, le backend genere auto a partir du nom
    const trimmedShort = short.trim() || null;
    setLoading(true);
    try {
      const cat = await actions.createCategory(trimmedName, trimmedShort);
      onChange(cat.id);
      setCreating(false);
      setName("");
      setShort("");
    } catch (e) {
      // l'action affiche deja un toast d'erreur via onFetchError
    } finally {
      setLoading(false);
    }
  };

  const handleKeyDown = (e) => {
    if (e.key === "Enter") { e.preventDefault(); handleCreate(); }
    if (e.key === "Escape") { setCreating(false); setName(""); setShort(""); }
  };

  const handleCancel = () => { setCreating(false); setName(""); setShort(""); };

  if (creating) {
    return (
      <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
        <input
          ref={inputRef}
          className="input"
          type="text"
          placeholder="Nom (ex: Battle Partners)"
          value={name}
          onChange={e => setName(e.target.value)}
          onKeyDown={handleKeyDown}
          disabled={loading}
          style={{ flex: 1 }}
        />
        <input
          className="input mono"
          type="text"
          placeholder="SV9"
          value={short}
          onChange={e => setShort(e.target.value)}
          onKeyDown={handleKeyDown}
          disabled={loading}
          style={{ width: 80, flexShrink: 0 }}
        />
        <button
          className="btn btn-primary btn-sm"
          onClick={handleCreate}
          disabled={loading || !name.trim()}
          style={{ whiteSpace: "nowrap" }}
        >
          {loading ? "..." : "Creer"}
        </button>
        <button
          className="btn btn-sm"
          onClick={handleCancel}
          disabled={loading}
          style={{ whiteSpace: "nowrap" }}
        >
          Annuler
        </button>
      </div>
    );
  }

  return (
    <select
      className="select"
      value={value || ""}
      onChange={e => {
        if (e.target.value === "__new__") {
          setCreating(true);
        } else {
          onChange(e.target.value);
        }
      }}
      style={selectStyle}
    >
      {placeholder && <option value="">{placeholder}</option>}
      {categories.map(c => (
        <option key={c.id} value={c.id}>{c.short || c.name}</option>
      ))}
      <option value="__new__" style={{ fontStyle: "italic", color: "var(--accent)" }}>+ Nouvelle collection...</option>
    </select>
  );
};

// ---------- Expose globally ----------
Object.assign(window, {
  fmtEUR, fmtJPY, fmtInt, fmtDate, fmtDateShort,
  Money, MoneyPair, Icon, StatusPill, STATUS_LABELS,
  useToast, Drawer, Sparkline, Tabs, NewCatInline,
  shipRechargeTotalJpy, shipRechargePct, shipRechargeStatus, shipIsRecharged, shipNum,
});
