/* ========================================================================
   App shell — Sidebar, Header, Router, shared atoms (Milo reskin)
   ======================================================================== */

const { useState, useEffect, useMemo } = React;

/* ---------- Tweakable defaults ---------- */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "density": "comfortable",
  "stat_card_style": "subtle",
  "row_style": "zebra",
  "pool_usage_demo": "live",
  "finder_style": "panel"
} /*EDITMODE-END*/;

/* ---------- Icon — lucide via <i data-lucide>; rendered after each navigation ----------
   The <i> is wrapped in a sized <span> so the SVG that lucide injects fills the
   wrapper at the right pixel size. Each Icon owns its own createIcons() call
   on mount so newly-rendered icons (e.g. after a filter change inside a child
   component) get processed without depending on the App-level effect. */
function Icon({ name, size = 18, stroke = 1.75, className = "", style = {} }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (window.lucide && ref.current) {
      try { window.lucide.createIcons({ nameAttr: "data-lucide", attrs: {}, icons: undefined, scope: ref.current }); } catch (e) {
        // older lucide builds don't accept a scope — fall back to global
        try { window.lucide.createIcons(); } catch (e2) {}
      }
    }
  });
  return (
    <span
      ref={ref}
      className={"mp-ic " + className}
      style={{
        width: size, height: size,
        ...style
      }}>
      <i data-lucide={name} style={{ strokeWidth: stroke }} />
    </span>);
}

/* ---------- Router (hash-based) ---------- */
function useRoute() {
  const [route, setRoute] = useState(() => parseHash());
  useEffect(() => {
    const onHash = () => setRoute(parseHash());
    window.addEventListener("hashchange", onHash);
    return () => window.removeEventListener("hashchange", onHash);
  }, []);
  return route;
}
function parseHash() {
  const raw = window.location.hash.replace(/^#/, "") || "/dashboard";
  const [h, qs = ""] = raw.split("?");
  const parts = (h || "/dashboard").split("/").filter(Boolean);
  return { path: "/" + parts.join("/"), parts, query: qs };
}
function navigate(path) {
  window.location.hash = path;
}

/* ---------- Sidebar ---------- */
const NAV_ITEMS = [
{ path: "/dashboard", label: "Overview", icon: "chart-column", group: "Manage" },
{ path: "/users", label: "User Management", icon: "users-round", group: "Manage" },
{ path: "/sims", label: "SIM Management", icon: "card-sim", group: "Manage" },
{ path: "/alerts", label: "Alerts", icon: "bell", group: "Manage" },
{ path: "/co2", label: "Environmental Impact", icon: "leaf", group: "Manage" },
{ path: "/subscriptions", label: "Subscription & Billing", icon: "wallet", group: "Account" },
{ path: "/support", label: "Support & Help", icon: "life-buoy", group: "Account" },
{ path: "/settings", label: "Settings", icon: "settings", group: "Account" }];


function Sidebar({ activePath }) {
  const grouped = useMemo(() => {
    const g = {};
    NAV_ITEMS.forEach((it) => {(g[it.group] ??= []).push(it);});
    return g;
  }, []);
  const [collapsed, setCollapsed] = useState(() => {
    try { return localStorage.getItem("mp-side-collapsed") === "1"; }
    catch (e) { return false; }
  });
  const toggle = () => {
    setCollapsed((c) => {
      const next = !c;
      try { localStorage.setItem("mp-side-collapsed", next ? "1" : "0"); } catch (e) {}
      return next;
    });
  };
  return (
    <aside className={"mp-side" + (collapsed ? " is-collapsed" : "")}>
      <div className="mp-side-inner">
        <div className="mp-side-brand">
          <img src="assets/logos/icon-colour-dark.svg" alt="" />
          <div className="mp-side-brand-text">
            <div className="mp-side-brand-name">Meaningful<br />Planet</div>
          </div>
        </div>
        <div className="mp-side-workspace">
          <Icon name="building-2" size={15} className="mp-side-workspace-icon" />
          <span className="mp-side-workspace-name">{window.MILO_DATA.COMPANY}</span>
        </div>
        <nav className="mp-side-nav">
          {Object.entries(grouped).map(([group, items]) =>
          <div key={group} className="mp-side-section">
              <div className="mp-side-group">{group}</div>
              {items.map((it) => {
              const active = activePath === it.path || activePath.startsWith(it.path + "/");
              return (
                <a
                  key={it.path}
                  href={"#" + it.path}
                  className={"mp-side-item" + (active ? " is-active" : "")}>
                  
                    <Icon name={it.icon} size={18} />
                    <span>{it.label}</span>
                  </a>);

            })}
            </div>
          )}
        </nav>
        <button
          type="button"
          className="mp-side-toggle"
          onClick={toggle}
          aria-label={collapsed ? "Expand navigation" : "Collapse navigation"}
          title={collapsed ? "Expand navigation" : "Collapse navigation"}>
          <Icon name="panel-left-close" size={16} className={collapsed ? "mp-side-toggle-ic is-flipped" : "mp-side-toggle-ic"} />
          <span>{collapsed ? "Expand Navigation" : "Collapse Navigation"}</span>
        </button>
        <div className="mp-side-user">
          <span className="mp-side-av">{window.MILO_DATA.CURRENT_USER.initials}</span>
          <div className="mp-side-user-meta">
            <div className="mp-side-user-name">{window.MILO_DATA.CURRENT_USER.full_name}</div>
            <div className="mp-side-user-role">{window.MILO_DATA.CURRENT_USER.email}</div>
          </div>
        </div>
        <button type="button" className="mp-side-logout" onClick={() => {
          if (window.DEMO_TOAST) window.DEMO_TOAST("Log out ends your session and returns you to the sign-in page in the live platform.", arguments[0]);
        }}>
          <Icon name="log-out" size={16} />
          <span>Log Out</span>
        </button>
      </div>
    </aside>);

}

/* ---------- Header — no longer rendered; topbar removed per latest brief ---------- */
function Header() { return null; }

/* ---------- Shared atoms ---------- */

function PageHead({ title, subtitle, children }) {
  return (
    <div className="mp-page-head">
      <div className="mp-page-head-text">
        <h1 className="mp-page-head-title">{title}</h1>
        {subtitle ? <p className="mp-page-head-subtitle">{subtitle}</p> : null}
      </div>
      {children ? <div className="mp-page-head-aside">{children}</div> : null}
    </div>);

}

function Card({ children, className = "", style }) {
  return <div className={"mp-card-ui " + className} style={style}>{children}</div>;
}

function StatCard({ label, value, valueSuffix, hint, tint, info, icon, iconTone, valueColor }) {
  const isZero = isZeroValue(value);
  return (
    <div className={"mp-stat " + (tint ? "mp-stat--" + tint : "")}>
      <div className="mp-stat-top">
        <div className="mp-stat-label">{label}</div>
        {icon ?
        <div className={"mp-stat-icon" + (iconTone ? " mp-stat-icon--" + iconTone : "")}><Icon name={icon} size={16} /></div> :
        info ?
        <div className="mp-stat-info" title={info}><Icon name="info" size={14} /></div> :
        null}
      </div>
      <div className={"mp-stat-value" + (isZero ? " mp-stat-value--zero" : "")} style={!isZero && valueColor ? { color: valueColor } : null}>
        {value}{valueSuffix ? <span className="mp-stat-suffix">{valueSuffix}</span> : null}
      </div>
      {hint ? <div className="mp-stat-hint">{hint}</div> : null}
    </div>);

}

/* Detect "empty" stat values so we can mute them (Suspended SIMs = 0 etc.) */
function isZeroValue(v) {
  if (v === 0) return true;
  if (typeof v === "string") {
    const s = v.trim();
    return s === "0" || s === "0%" || /^£\s?0(\.0+)?$/.test(s);
  }
  return false;
}

function Badge({ children, variant = "default", icon }) {
  return (
    <span className={"mp-badge mp-badge--" + variant}>
      {icon ? <Icon name={icon} size={11} /> : null}
      {children}
    </span>);

}

function StatusBadge({ status, sim }) {
  /* An eSIM waiting to be set up has one status, and it is not Active. */
  if (sim && window.MP_ESIM) status = window.MP_ESIM.displayStatus(sim, status);
  const s = (status || "").toLowerCase();
  if (s === "ready to install") return <Badge variant="clay" icon="qr-code">Ready to Install</Badge>;
  if (s === "active")           return <Badge variant="ok"      icon="circle-check">Active</Badge>;
  if (s === "activated")        return <Badge variant="lime"    icon="zap">Activated</Badge>;
  if (s === "for activation")   return <Badge variant="clay"    icon="clock">For Activation</Badge>;
  if (s === "suspended")        return <Badge variant="cream"   icon="circle-pause">Suspended</Badge>;
  if (s === "terminated")       return <Badge variant="ember"   icon="circle-slash">Terminated</Badge>;
  if (s === "trialing")         return <Badge variant="teal"    icon="zap">Trialing</Badge>;
  return <Badge variant="default">{status}</Badge>;
}

/* What a SIM costs each month. International and travel SIMs carry their own
   price; UK SIMs take it from their plan. */
function simMonthly(sim) {
  if (!sim) return 0;
  if (sim.monthly_price != null) return sim.monthly_price;
  if (sim.group === "travel" && window.MP_TRAVEL) {
    const p = window.MP_TRAVEL.primaryPlan(sim.id);
    return p && p.type === "recurring" ? p.price : 0;
  }
  const plan = (window.MILO_DATA.PLANS || {})[sim.plan];
  return plan ? plan.price : 0;
}

const SIM_TYPE_META = {
  eSIM: { icon: "qr-code", label: "eSIMs" },
  Physical: { icon: "card-sim", label: "Physical SIMs" },
  IoT: { icon: "cpu", label: "IoT SIMs" }
};
/* What a set of SIMs is made of. Counting named types only would hide a line
   made of anything else, which is how an IoT line came to state no size. */
function simTypeCounts(sims) {
  const counts = {};
  (sims || []).forEach((s) => { counts[s.sim_type] = (counts[s.sim_type] || 0) + 1; });
  return Object.keys(SIM_TYPE_META).concat(Object.keys(counts).filter((t) => !SIM_TYPE_META[t]))
    .filter((t) => counts[t])
    .map((t) => ({ type: t, count: counts[t], icon: (SIM_TYPE_META[t] || {}).icon || "card-sim",
      label: (SIM_TYPE_META[t] || {}).label || t + " SIMs" }));
}

/* A SIM is billed once it is in service. Stock awaiting activation and
   terminated SIMs are not costing anything, so no total may include them. */
function isBilled(sim) {
  return !!sim && (sim.status === "Active" || sim.status === "Activated" || sim.status === "Suspended");
}
/* The monthly cost of any set of SIMs. Every total in the product uses this,
   so no screen can apply the rule to a different subset. */
function billedMonthly(sims) {
  return (sims || []).filter(isBilled).reduce((sum, s) => sum + simMonthly(s), 0);
}

/* What a SIM was charged, and whether it repeats. A one-off travel plan is a
   real charge but not a monthly one, so it says so rather than reading £0.00
   a month on a billing screen. */
function simCharge(sim) {
  if (sim && sim.group === "travel" && window.MP_TRAVEL) {
    const p = window.MP_TRAVEL.primaryPlan(sim.id);
    if (p && p.type === "one_off") return { amount: p.price, recurring: false };
    return { amount: p && p.type === "recurring" ? p.price : 0, recurring: true };
  }
  return { amount: simMonthly(sim), recurring: true };
}

/* How long ago an alert happened. A bare duration takes " ago"; a phrase that
   already reads as a time does not, or it says "Yesterday ago". */
function fmtAgo(stamp) {
  const s = String(stamp || "").trim();
  if (!s) return "";
  return /^(just now|yesterday|today|earlier today|now)$/i.test(s) ? s : s + " ago";
}

/* Stored dates are ISO; people read long dates with the year. */
function fmtActivation(iso) {
  if (!iso) return "Not active";
  const d = new Date(iso + "T00:00:00");
  return isNaN(d) ? iso : d.toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" });
}

function SimTypeBadge({ type }) {
  if (type === "eSIM")     return <Badge variant="type-esim"     icon="qr-code">eSIM</Badge>;
  if (type === "Physical") return <Badge variant="type-physical" icon="card-sim">Physical</Badge>;
  if (type === "IoT")      return <Badge variant="type-iot"      icon="cpu">IoT</Badge>;
  return <Badge variant="default">{type}</Badge>;
}

function Button({ variant = "primary", icon, iconAfter, children, onClick, href, size = "md", className = "", type = "button" }) {
  const cls = "mp-btn mp-btn--" + variant + " mp-btn--" + size + " " + className;
  const inner =
  <>
      {icon ? <Icon name={icon} size={size === "sm" ? 14 : 16} /> : null}
      <span>{children}</span>
      {iconAfter ? <Icon name={iconAfter} size={size === "sm" ? 14 : 16} /> : null}
    </>;

  if (href) return <a href={href} className={cls} onClick={onClick}>{inner}</a>;
  return <button type={type} className={cls} onClick={onClick}>{inner}</button>;
}

function BackLink({ href, children }) {
  return (
    <a href={href} className="mp-backlink">
      <Icon name="arrow-left" size={14} /><span>{children}</span>
    </a>);

}

function ProgressBar({ pct, intent }) {
  const safe = Math.max(0, Math.min(100, pct));
  // Thresholds match the % Used column dot: <80 ok · 80–89 warn · 90–96 high · 97+ crit
  const tone = intent || (safe >= 97 ? "crit" : safe >= 90 ? "high" : safe >= 80 ? "warn" : "ok");
  return (
    <div className={"mp-bar mp-bar--" + tone}>
      <span style={{ width: safe + "%" }}></span>
    </div>);

}

/* % Used cell indicator — colored dot + value */
function PctIndicator({ pct }) {
  const v = Math.round(pct);
  const tone =
    v >= 97 ? "crit" :
    v >= 90 ? "high" :
    v >= 80 ? "warn" :
    "ok";
  return (
    <span className="mp-pct">
      <span className={"mp-pct-dot mp-pct-dot--" + tone}></span>
      <span className="mp-pct-val">{v}%</span>
    </span>
  );
}

function SearchInput({ placeholder = "Search...", value, onChange }) {
  return (
    <div className="mp-input-wrap">
      <Icon name="search" size={14} className="mp-input-leading" />
      <input
        className="mp-input"
        type="search"
        placeholder={placeholder}
        value={value || ""}
        onChange={(e) => onChange(e.target.value)} />
      
    </div>);

}

function Select({ value, onChange, options, placeholder }) {
  return (
    <div className="mp-select-wrap">
      <select className="mp-select" value={value || ""} onChange={(e) => onChange(e.target.value)}>
        {placeholder ? <option value="">{placeholder}</option> : null}
        {options.map((o) =>
        <option key={typeof o === "string" ? o : o.value} value={typeof o === "string" ? o : o.value}>
            {typeof o === "string" ? o : o.label}
          </option>
        )}
      </select>
      <Icon name="chevron-down" size={14} className="mp-select-caret" />
    </div>);

}

function Avatar({ initials, size = 32, tone = "cream" }) {
  return (
    <span className={"mp-av mp-av--" + tone} style={{ width: size, height: size, fontSize: Math.max(12, Math.round(size * 0.38)) }}>
      {initials}
    </span>);

}

function SortHeader({ children, field, sortField, sortDir, onSort }) {
  const active = field === sortField;
  return (
    <button className={"mp-th-sort" + (active ? " is-active" : "")} onClick={() => onSort(field)}>
      <span>{children}</span>
      <span className="mp-sort-pair">
        <Icon name="chevron-up" size={10} className={active && sortDir === "asc" ? "is-sort-on" : ""} />
        <Icon name="chevron-down" size={10} className={active && sortDir === "desc" ? "is-sort-on" : ""} />
      </span>
    </button>);

}

/* An IoT SIM has no voice service and so no number. It is identified by its
   serial in any place that would otherwise headline a number. */
function simLabel(sim) { return sim.msisdn || sim.serial; }

function ActionsMenu({ items, align = "right" }) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState(null);
  const wrapRef = React.useRef(null);
  const menuRef = React.useRef(null);
  useEffect(() => {
    if (!open) return;
    const close = () => setOpen(false);
    window.addEventListener("click", close);
    window.addEventListener("resize", close);
    /* Any scroll would leave a fixed menu behind its trigger. */
    window.addEventListener("scroll", close, true);
    return () => {
      window.removeEventListener("click", close);
      window.removeEventListener("resize", close);
      window.removeEventListener("scroll", close, true);
    };
  }, [open]);
  /* The menu is rendered into <body> at fixed coordinates rather than inside
     the row. Table wraps set overflow-x, which forces overflow-y and clips
     anything overhanging the last rows; flipping upward only helped while
     there was room above, so a filtered short table clipped it either way.
     Escaping the container removes the problem instead of negotiating it. */
  React.useLayoutEffect(() => {
    if (!open || !wrapRef.current || !menuRef.current) { if (!open) setPos(null); return; }
    const t = wrapRef.current.getBoundingClientRect();
    const w = menuRef.current.offsetWidth;
    const h = menuRef.current.offsetHeight;
    const gap = 6, edge = 8;
    let top = t.bottom + gap;
    if (top + h > window.innerHeight - edge) {
      const above = t.top - gap - h;
      top = above >= edge ? above : t.bottom + gap;
    }
    /* Clamp unconditionally: a trigger near either edge must not push the
       menu out of the viewport in the direction we just chose. */
    top = Math.min(Math.max(edge, top), Math.max(edge, window.innerHeight - edge - h));
    let left = align === "left" ? t.left : t.right - w;
    left = Math.min(Math.max(edge, left), window.innerWidth - w - edge);
    setPos({ top, left });
  }, [open, align]);
  const menu =
    <div ref={menuRef} className="mp-actions-menu mp-actions-menu--fixed"
      style={pos ? { top: pos.top, left: pos.left } : { top: -9999, left: -9999 }}
      onClick={(e) => e.stopPropagation()}>
      {items.map((it, i) =>
        it.note ?
        <div key={i} className="mp-actions-note">{it.note}</div> :
        <a key={i} href={it.href || "#"} className="mp-actions-item" onClick={(e) => {
          if (it.onClick) { e.preventDefault(); it.onClick(); }
          setOpen(false);
        }}>
          {it.label}
        </a>
      )}
    </div>;
  return (
    <div className={"mp-actions" + (open ? " is-open" : "")} ref={wrapRef} onClick={(e) => e.stopPropagation()}>
      <button className="mp-actions-trigger mp-actions-trigger--icon" onClick={() => setOpen((v) => !v)} aria-label="Actions">
        <Icon name="more-horizontal" size={16} />
      </button>
      {open ? ReactDOM.createPortal(menu, document.body) : null}
    </div>);

}

/* Horizontal scroll shell for wide tables — sticky actions column, chevron
   controls and an always-visible scrollbar so the hidden columns are legible. */
function TableScroll({ children, className = "", narrowCards }) {
  const ref = React.useRef(null);
  const [s, setS] = useState({ over: false, start: true, end: false });
  const navRef = React.useRef(null);
  const measureRef = React.useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const measure = () => {
      const max = el.scrollWidth - el.clientWidth;
      /* Centre the chevron pair on the pinned actions cell, whose width varies
         per table — positioning against the shell's edge overhangs narrow ones.
         Written straight to the node: keeping it in state can oscillate. */
      const th = el.querySelector(".mp-table thead th:last-child");
      /* Align on the row menu itself — cell padding varies per table, so
         centring on the cell leaves the pair a few px off the dots. */
      const anchor = el.querySelector(".mp-table tbody td:last-child .mp-actions-trigger") || th;
      if (anchor && navRef.current) {
        const ar = anchor.getBoundingClientRect(), er = el.getBoundingClientRect();
        navRef.current.style.right = Math.max(2, Math.round(er.right - ar.right + (ar.width - 55) / 2)) + "px";
      }
      const next = { over: max > 4, start: el.scrollLeft <= 2, end: max > 4 && el.scrollLeft >= max - 2 };
      setS((prev) => (prev.over === next.over && prev.start === next.start && prev.end === next.end) ? prev : next);
    };
    measureRef.current = measure;
    measure();
    el.addEventListener("scroll", measure, { passive: true });
    let ro;
    if (window.ResizeObserver) {
      ro = new ResizeObserver(measure);
      ro.observe(el);
      const t = el.querySelector("table");
      if (t) ro.observe(t);
    }
    return () => { el.removeEventListener("scroll", measure); if (ro) ro.disconnect(); };
  }, []);
  /* Re-measure after every render — layout shifts when the buttons mount or a
     filter changes the rows. The setS guard above makes this loop-safe. */
  useEffect(() => { if (measureRef.current) measureRef.current(); });
  const nudge = (dir) => {
    const el = ref.current;
    /* Instant, not smooth — smooth scrolling is suppressed under
       prefers-reduced-motion, which silently made the control do nothing. */
    if (el) el.scrollBy({ left: dir * Math.max(240, el.clientWidth * 0.55), behavior: "auto" });
    /* Measure straight away rather than waiting on a scroll event */
    if (measureRef.current) measureRef.current();
  };
  return (
    <div className={"mp-tscroll" + (s.over ? " is-over" : "") + (s.start ? " at-start" : "") + (s.end ? " at-end" : "") + (narrowCards ? " mp-tscroll--cards" : "")}>
      <div className={"mp-table-wrap has-sticky-actions " + className} ref={ref}>{children}</div>
      {s.over ?
        <div className="mp-tscroll-nav" ref={navRef} style={{ right: 3 }}>
          <button type="button" className={"mp-tscroll-btn" + (s.start ? " is-idle" : "")} onClick={() => nudge(-1)} aria-label="Scroll columns left" tabIndex={s.start ? -1 : 0}>
            <Icon name="chevron-left" size={15} />
          </button>
          <button type="button" className={"mp-tscroll-btn" + (s.end ? " is-idle" : "")} onClick={() => nudge(1)} aria-label="Scroll columns right" tabIndex={s.end ? -1 : 0}>
            <Icon name="chevron-right" size={15} />
          </button>
        </div> : null}
    </div>
  );
}

function Pagination({ page, totalPages, onChange }) {
  if (totalPages <= 1) return null;
  return (
    <nav className="mp-pagination">
      <button className="mp-pg-btn" disabled={page === 1} onClick={() => onChange(page - 1)}>
        <Icon name="chevron-left" size={14} />
      </button>
      {Array.from({ length: totalPages }).map((_, i) =>
      <button
        key={i}
        className={"mp-pg-btn" + (page === i + 1 ? " is-active" : "")}
        onClick={() => onChange(i + 1)}>
        {i + 1}</button>
      )}
      <button className="mp-pg-btn" disabled={page === totalPages} onClick={() => onChange(page + 1)}>
        <Icon name="chevron-right" size={14} />
      </button>
    </nav>);

}

function Empty({ icon, title, message }) {
  return (
    <div className="mp-empty">
      <div className="mp-empty-ic-flat"><Icon name={icon} size={32} /></div>
      <div className="mp-empty-title">{title}</div>
      <div className="mp-empty-msg">{message}</div>
    </div>);

}

/* ---------- Export ---------- */
Object.assign(window, {
  TableScroll,
  useRoute, navigate, Icon,
  Sidebar, Header, PageHead, Card, StatCard, isZeroValue, Badge, StatusBadge, SimTypeBadge, fmtActivation, simMonthly, simCharge, isBilled, billedMonthly, simTypeCounts, fmtAgo,
  Button, BackLink, ProgressBar, PctIndicator, SearchInput, Select, Avatar, SortHeader, ActionsMenu, Pagination, Empty, simLabel,
  TWEAK_DEFAULTS
});