// =================================================================
// lib.jsx — Cross-cutting utilities used everywhere.
//   - fmt: currency, number, date, bytes, relative time
//   - cookies: get / set / has / remove / all
//   - useHashRoute: drives `route` from `window.location.hash` for
//     shareable URLs and surviving refresh
// =================================================================

// ============== Formatters ==============
const fmt = {
  /** Currency in USD by default. Accepts number; returns $1,234.56. */
  currency(n, currency = "USD", opts = {}) {
    if (n == null || isNaN(n)) return "—";
    return new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 2, ...opts }).format(n);
  },
  /** Compact number: 1.2K, 4.5M, 1.8B. */
  compact(n) {
    if (n == null || isNaN(n)) return "—";
    return new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }).format(n);
  },
  /** Plain number with thousands separator. */
  number(n) {
    if (n == null || isNaN(n)) return "—";
    return new Intl.NumberFormat("en-US").format(n);
  },
  /** Percentage. fmt.percent(0.183) → "18.3%". */
  percent(n, digits = 1) {
    if (n == null || isNaN(n)) return "—";
    return (n * 100).toFixed(digits) + "%";
  },
  /** Date as "Apr 30, 2026". Accepts Date, string, or ms. */
  date(d) {
    if (!d) return "—";
    const date = d instanceof Date ? d : new Date(d);
    if (isNaN(date)) return "—";
    return date.toLocaleDateString("en-US", { month: "short", day: "2-digit", year: "numeric" });
  },
  /** "Apr 30, 2026, 3:42 PM". */
  dateTime(d) {
    if (!d) return "—";
    const date = d instanceof Date ? d : new Date(d);
    if (isNaN(date)) return "—";
    return date.toLocaleString("en-US", { month: "short", day: "2-digit", year: "numeric", hour: "numeric", minute: "2-digit" });
  },
  /** Bytes: 1024 → 1 KB, 1048576 → 1 MB. */
  bytes(b) {
    if (b == null || isNaN(b)) return "—";
    if (b < 1024) return `${b} B`;
    if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`;
    if (b < 1024 * 1024 * 1024) return `${(b / (1024 * 1024)).toFixed(1)} MB`;
    if (b < 1024 * 1024 * 1024 * 1024) return `${(b / (1024 * 1024 * 1024)).toFixed(2)} GB`;
    return `${(b / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
  },
  /** "2 hours ago", "Yesterday", "Just now". */
  relativeTime(d) {
    if (!d) return "—";
    const date = d instanceof Date ? d : new Date(d);
    if (isNaN(date)) return "—";
    const diff = (Date.now() - date.getTime()) / 1000; // seconds
    if (diff < 60) return "Just now";
    if (diff < 3600) return `${Math.floor(diff / 60)} min ago`;
    if (diff < 86400) return `${Math.floor(diff / 3600)} hr ago`;
    if (diff < 172800) return "Yesterday";
    if (diff < 604800) return `${Math.floor(diff / 86400)} days ago`;
    return fmt.date(date);
  },
  /** Truncate with ellipsis. */
  truncate(s, n = 40) {
    if (!s) return "";
    return s.length > n ? s.slice(0, n - 1) + "…" : s;
  },
  /** "12 invoices", "1 invoice". */
  plural(n, singular, plural) {
    return `${n} ${n === 1 ? singular : (plural || singular + "s")}`;
  },
};

// ============== Cookie management ==============
// A small wrapper around document.cookie. Values are URL-encoded; reading
// returns the decoded value or null. Set defaults to 365-day expiry,
// SameSite=Lax, and path=/ — override per-call when needed.
const cookies = {
  /** Get one cookie by name. Returns null if not present. */
  get(name) {
    if (typeof document === "undefined") return null;
    const target = encodeURIComponent(name) + "=";
    const all = document.cookie ? document.cookie.split("; ") : [];
    for (const c of all) {
      if (c.startsWith(target)) return decodeURIComponent(c.slice(target.length));
    }
    return null;
  },
  /** Set a cookie. value is coerced to a string; objects are JSON-stringified.
   *  opts: { days = 365, path = "/", domain, secure, sameSite = "Lax" } */
  set(name, value, opts = {}) {
    if (typeof document === "undefined") return;
    const o = { days: 365, path: "/", sameSite: "Lax", ...opts };
    const v = typeof value === "object" ? JSON.stringify(value) : String(value);
    let str = encodeURIComponent(name) + "=" + encodeURIComponent(v);
    if (o.days != null) {
      const expires = new Date(Date.now() + o.days * 86400 * 1000).toUTCString();
      str += "; expires=" + expires;
    }
    if (o.path) str += "; path=" + o.path;
    if (o.domain) str += "; domain=" + o.domain;
    if (o.secure) str += "; secure";
    if (o.sameSite) str += "; samesite=" + o.sameSite;
    document.cookie = str;
  },
  /** Remove a cookie by name. Honors path/domain so it actually clears. */
  remove(name, opts = {}) {
    cookies.set(name, "", { ...opts, days: -1 });
  },
  /** True/false. */
  has(name) {
    return cookies.get(name) !== null;
  },
  /** Return all cookies as an object. */
  all() {
    if (typeof document === "undefined") return {};
    const out = {};
    const list = document.cookie ? document.cookie.split("; ") : [];
    for (const c of list) {
      const eq = c.indexOf("=");
      if (eq === -1) continue;
      try {
        out[decodeURIComponent(c.slice(0, eq))] = decodeURIComponent(c.slice(eq + 1));
      } catch (e) {}
    }
    return out;
  },
  /** Read JSON-shaped cookie back into an object, or null. */
  getJSON(name) {
    const v = cookies.get(name);
    if (v == null) return null;
    try { return JSON.parse(v); } catch (e) { return null; }
  },
};

// ============== useHashRoute ==============
// Reads/writes window.location.hash so each route gets a real URL. The
// hash format is "#/<route>[?param=value]" — simple and back-button friendly.
const parseHash = () => {
  const h = (typeof window !== "undefined" ? window.location.hash : "") || "";
  const m = h.match(/^#\/([^?]*)(?:\?(.*))?$/);
  if (!m) return { route: null, param: null };
  const params = {};
  if (m[2]) {
    m[2].split("&").forEach(pair => {
      const [k, v] = pair.split("=");
      if (k) params[decodeURIComponent(k)] = v ? decodeURIComponent(v) : "";
    });
  }
  return { route: decodeURIComponent(m[1] || ""), param: params.id || null, params };
};

const stringifyHash = (route, param) => {
  let h = "#/" + encodeURIComponent(route);
  if (param) h += "?id=" + encodeURIComponent(param);
  return h;
};

function useHashRoute(defaultRoute) {
  const init = parseHash();
  const [route, _setRoute] = React.useState(init.route || defaultRoute);
  const [param, _setParam] = React.useState(init.param);

  // External hash change → state
  React.useEffect(() => {
    const onHash = () => {
      const { route: r, param: p } = parseHash();
      _setRoute(r || defaultRoute);
      _setParam(p);
    };
    window.addEventListener("hashchange", onHash);
    return () => window.removeEventListener("hashchange", onHash);
  }, [defaultRoute]);

  // Initial sync — write hash if missing
  React.useEffect(() => {
    if (!window.location.hash) {
      history.replaceState(null, "", stringifyHash(route, param));
    }
  // eslint-disable-next-line
  }, []);

  const setRoute = React.useCallback((r, p) => {
    _setRoute(r);
    _setParam(p || null);
    const next = stringifyHash(r, p);
    if (window.location.hash !== next) {
      history.pushState(null, "", next);
    }
  }, []);

  return [route, param, setRoute];
}

Object.assign(window, { fmt, cookies, useHashRoute, parseHash, stringifyHash });
