// ui.jsx — shared atomic components: Icons, Avatars, Pills, Popovers

// ── Icons (feather-style) ────────────────────────────────────────────
const Icon = ({ d, size = 16, stroke = "currentColor", fill = "none", ...rest }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill={fill} stroke={stroke}
       strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...rest}>
    {typeof d === "string" ? <path d={d} /> : d}
  </svg>
);

const Icons = {
  Home:       (p) => <Icon {...p} d="M3 12l9-9 9 9M5 10v10h14V10" />,
  Inbox:      (p) => <Icon {...p} d={<><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></>} />,
  Folder:     (p) => <Icon {...p} d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />,
  Users:      (p) => <Icon {...p} d={<><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></>} />,
  Search:     (p) => <Icon {...p} d={<><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></>} />,
  Bell:       (p) => <Icon {...p} d={<><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></>} />,
  Help:       (p) => <Icon {...p} d={<><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></>} />,
  Plus:       (p) => <Icon {...p} d="M12 5v14M5 12h14" />,
  ChevronRt:  (p) => <Icon {...p} d="m9 18 6-6-6-6" />,
  ChevronDn:  (p) => <Icon {...p} d="m6 9 6 6 6-6" />,
  ChevronSm:  (p) => <Icon {...p} d="m6 9 6 6 6-6" size={14} />,
  Filter:     (p) => <Icon {...p} d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z" />,
  Sort:       (p) => <Icon {...p} d={<><path d="M11 5h10"/><path d="M11 9h7"/><path d="M11 13h4"/><path d="m3 17 3 3 3-3"/><path d="M6 18V4"/></>} />,
  Eye:        (p) => <Icon {...p} d={<><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></>} />,
  Star:       (p) => <Icon {...p} d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />,
  Calendar:   (p) => <Icon {...p} d={<><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></>} />,
  Table:      (p) => <Icon {...p} d={<><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/></>} />,
  Board:      (p) => <Icon {...p} d={<><rect x="3" y="3" width="7" height="18" rx="1"/><rect x="14" y="3" width="7" height="10" rx="1"/></>} />,
  Timeline:   (p) => <Icon {...p} d={<><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="15" y2="12"/><line x1="3" y1="18" x2="18" y2="18"/></>} />,
  More:       (p) => <Icon {...p} d={<><circle cx="5" cy="12" r="1.3" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.3" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.3" fill="currentColor" stroke="none"/></>} />,
  Close:      (p) => <Icon {...p} d="M18 6 6 18M6 6l12 12" />,
  Check:      (p) => <Icon {...p} d="M20 6 9 17l-5-5" />,
  Link:       (p) => <Icon {...p} d={<><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></>} />,
  Flag:       (p) => <Icon {...p} d={<><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></>} />,
  Lightning:  (p) => <Icon {...p} d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" />,
  ArrowUp:    (p) => <Icon {...p} d="M12 19V5M5 12l7-7 7 7" />,
  Activity:   (p) => <Icon {...p} d="M22 12h-4l-3 9L9 3l-3 9H2" />,
  Paperclip:  (p) => <Icon {...p} d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.48-8.48l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />,
  MessageSq:  (p) => <Icon {...p} d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />,
  GitBranch:  (p) => <Icon {...p} d={<><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></>} />,
  Grip:       (p) => <Icon {...p} d={<><circle cx="9" cy="5" r="1" fill="currentColor" stroke="none"/><circle cx="9" cy="12" r="1" fill="currentColor" stroke="none"/><circle cx="9" cy="19" r="1" fill="currentColor" stroke="none"/><circle cx="15" cy="5" r="1" fill="currentColor" stroke="none"/><circle cx="15" cy="12" r="1" fill="currentColor" stroke="none"/><circle cx="15" cy="19" r="1" fill="currentColor" stroke="none"/></>} />,
  Alert:      (p) => <Icon {...p} d={<><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></>} />,
  Clock:      (p) => <Icon {...p} d={<><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></>} />,
  Arrow:      (p) => <Icon {...p} d="M5 12h14M13 5l7 7-7 7" />,
  Send:       (p) => <Icon {...p} d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" />,
  Trash:      (p) => <Icon {...p} d={<><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/></>} />,
  Image:      (p) => <Icon {...p} d={<><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></>} />,
  Sparkle:    (p) => <Icon {...p} d={<><path d="M12 3 L13.6 10.4 L21 12 L13.6 13.6 L12 21 L10.4 13.6 L3 12 L10.4 10.4 Z"/><path d="M19 3 L19.6 5 L21.5 5.5 L19.6 6 L19 8 L18.4 6 L16.5 5.5 L18.4 5 Z"/></>} />,
  Lock:       (p) => <Icon {...p} d={<><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></>} />,
  Trash:      (p) => <Icon {...p} d={<><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/></>} />,
  List:       (p) => <Icon {...p} d={<><line x1="8" y1="6"  x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6"  x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></>} />,
  Note:       (p) => <Icon {...p} d={<><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="13" y2="17"/></>} />,
  Headset:    (p) => <Icon {...p} d={<><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1v-6h3z"/><path d="M3 19a2 2 0 0 0 2 2h1v-6H3z"/></>} />,
};

// ── Avatar ────────────────────────────────────────────────────
// Renders the user's uploaded avatar image when `person.avatar` is set
// (a URL like "/uploads/avatars/<id>_<ts>.webp"), otherwise falls back
// to coloured initials. The image is forced to fill the circle via
// the .avatar img CSS rule (object-fit: cover) so a 256x256 source
// always crops cleanly into the avatar's circular frame.
// Bumps every time celebration ids (birthday/anniversary) reload, so
// Avatars that mounted before the fetch finished re-render and pick up
// the ring + badge.
function useCelebrationTick() {
  const [, force] = React.useState(0);
  React.useEffect(() => {
    const h = () => force(v => v + 1);
    window.addEventListener("flowboard:bday-loaded", h);
    return () => window.removeEventListener("flowboard:bday-loaded", h);
  }, []);
}
function Avatar({ person, size }) {
  useCelebrationTick();
  if (!person) return null;
  const initials = person.name.split(/\s+/).map(p => p[0]).slice(0,2).join("");
  const cls = size === "sm" ? "avatar avatar-sm"
          : size === "md" ? "avatar avatar-md"
          : size === "lg" ? "avatar avatar-lg"
          : size === "xl" ? "avatar avatar-xl"
          : "avatar";
  const hasImg = !!(person.avatar);
  // Birthday celebration: when this person's id is in the global bday
  // set (loaded once at app start), wrap the avatar with an animated
  // rainbow ring + a tiny 🎂 badge. The wrapper is added ONLY when
  // celebrating so non-celebrating avatars keep their exact original
  // markup and layout.
  const isBday = !!(person.id && typeof window !== "undefined"
    && window.__fbBdayIds__ && window.__fbBdayIds__.has(person.id));
  // Work anniversary celebration: same idea as birthday but a gold/teal
  // ring + 🎉 badge so the two are visually distinct. Birthday wins if
  // a person happens to have both today (cake takes priority).
  const isAnniv = !isBday && !!(person.id && typeof window !== "undefined"
    && window.__fbAnnivIds__ && window.__fbAnnivIds__.has(person.id));
  const titleTag = isBday ? " — Birthday today 🎂"
                : isAnniv ? " — Work anniversary today 🎉"
                : "";
  const inner = (
    <span className={cls + (hasImg ? " avatar-has-img" : "")}
          style={{ background: hasImg ? "transparent" : person.color }}
          title={person.name + titleTag}
          onClick={(e) => {
            if (person._onPeek) { e.stopPropagation(); person._onPeek(person, e.currentTarget); }
          }}>
      {hasImg
        ? <img src={person.avatar} alt={person.name} draggable={false}/>
        : initials}
    </span>
  );
  if (!isBday && !isAnniv) return inner;
  const sizeTag = size === "sm" ? "sm" : size === "md" ? "md" : size === "lg" ? "lg" : size === "xl" ? "xl" : "";
  const wrapCls = isBday
    ? ("avatar-bday-wrap" + (sizeTag ? " avatar-bday-" + sizeTag : ""))
    : ("avatar-anniv-wrap" + (sizeTag ? " avatar-anniv-" + sizeTag : ""));
  return (
    <span className={wrapCls}
          aria-label={isBday ? "Birthday today" : "Work anniversary today"}>
      {inner}
    </span>
  );
}

function AvatarStack({ ids, max = 3, size }) {
  const people = ids.map(id => PEOPLE.find(p => p.id === id)).filter(Boolean);
  const shown = people.slice(0, max);
  const extra = people.length - shown.length;
  return (
    <div className="avatar-stack">
      {shown.map(p => <Avatar key={p.id} person={p} size={size} />)}
      {extra > 0 && (
        <span className={size === "sm" ? "avatar avatar-sm" : "avatar"}
              style={{ background: "#e6e9ef", color: "#676879" }}>
          +{extra}
        </span>
      )}
    </div>
  );
}

// ── Pills ─────────────────────────────────────────────────────
function StatusPill({ status, onClick, fill = true }) {
  const s = STATUSES.find(x => x.id === status);
  const prev = React.useRef(status);
  const [celebrate, setCelebrate] = React.useState(false);
  React.useEffect(() => {
    if (prev.current !== "done" && status === "done") {
      setCelebrate(true);
      const t = setTimeout(() => setCelebrate(false), 900);
      prev.current = status;
      return () => clearTimeout(t);
    }
    prev.current = status;
  }, [status]);
  if (!s) return null;
  return (
    <span className={`pill ${fill ? "pill-cell" : ""} ${s.cls}${celebrate ? " is-celebrate" : ""}`} onClick={onClick}>
      {s.label}
      {celebrate && (
        <span className="celebrate-spark" aria-hidden="true">
          <span/><span/><span/><span/><span/><span/>
        </span>
      )}
    </span>
  );
}
function PriorityPill({ prio, onClick, fill = true }) {
  const p = PRIORITIES.find(x => x.id === prio);
  if (!p) return null;
  return (
    <span className={`pill ${fill ? "pill-cell" : ""} ${p.cls}`} onClick={onClick}>
      {p.label}
    </span>
  );
}

// ── Popover ───────────────────────────────────────────────────
//
// Positioning rules:
// 1. Default: anchor's bottom edge + 4px (open downward).
// 2. If the popover would extend past the viewport's bottom AND there's
//    room above the anchor, flip and open upward.
// 3. Clamp `left` so the popover never spills off the right edge.
// 4. Fallback: maxHeight + internal scroll so a popover that's too tall
//    for either direction still works (e.g. very long picker on a short
//    viewport).
//
// We do a two-pass layout: first render pops with the default position
// so the DOM exists, then a requestAnimationFrame callback measures the
// actual height and flips/clamps if needed.
function Popover({ anchor, children, onClose }) {
  const [pos, setPos] = React.useState(null);
  const popRef = React.useRef(null);

  React.useEffect(() => {
    if (!anchor) return;

    function update() {
      const r = anchor.getBoundingClientRect();
      const vh = window.innerHeight;
      const vw = window.innerWidth;
      const ph = popRef.current ? popRef.current.offsetHeight : 0;
      const minWidth = Math.max(r.width, 140);
      const pw = popRef.current ? popRef.current.offsetWidth : minWidth;
      // Safe gap from viewport edges. Used to be 8px, which left the
      // calendar's right chevron flush against the screen edge on the
      // Personal page when the Due chip sits in the rightmost column.
      // 16px gives breathing room without wasting space on small screens.
      const MARGIN = 16;

      // Anchors that sit in the bottom 40% of the viewport open
      // UPWARD by default. Catches the common case of bottom-pinned
      // chrome (the .bulk-bar at bottom: 24px, the floating bug-report
      // button, the connection chip) where there's barely any room
      // below — and avoids the first-frame flash where ph=0 makes the
      // post-measure flip skip on initial render. We use an estimated
      // height (200px) on first render so the upward placement is
      // approximately right immediately; the rAF re-measure refines.
      const anchorIsLow = r.bottom > vh * 0.6;

      let top;
      if (anchorIsLow) {
        const estimated = ph || 200;
        top = r.top - estimated - 4;
        if (top < MARGIN) {
          // Not enough room above either — clamp so the popover sits
          // fully inside the viewport and let internal scroll absorb
          // any overflow.
          top = Math.max(MARGIN, vh - estimated - MARGIN);
        }
      } else {
        // Default: open below. Flip upward only if not enough room.
        top = r.bottom + 4;
        if (ph > 0 && top + ph > vh - MARGIN) {
          const upTop = r.top - ph - 4;
          if (upTop >= MARGIN) {
            top = upTop;
          } else {
            // Neither above nor below fits comfortably. Instead of
            // pinning the popover to the viewport's top edge (which
            // visually detaches it from the trigger), center it on
            // the anchor's vertical midpoint and let internal scroll
            // handle the height. This keeps the calendar near where
            // the user clicked.
            const anchorMid = r.top + r.height / 2;
            top = Math.max(MARGIN, Math.min(vh - ph - MARGIN, anchorMid - ph / 2));
          }
        }
      }

      // Horizontal clamp — keep the popover inside the viewport with
      // a comfortable margin on either side.
      let left = r.left;
      if (left + pw > vw - MARGIN) left = Math.max(MARGIN, vw - pw - MARGIN);
      if (left < MARGIN) left = MARGIN;

      setPos({ top, left, width: minWidth });
    }

    update();
    // Re-measure after the popover actually renders so the flip decision
    // sees the real height (offsetHeight is 0 before first paint).
    //
    // Three rAFs back-to-back rather than one — children with deferred
    // layout (e.g. MiniCalendar's 7-column grid) can still be measuring
    // 140px (the bare minWidth) on the first rAF and only settle to
    // their true 254px on the second or third frame. Without the chain
    // the horizontal clamp uses pw=minWidth=140, decides no clamp is
    // needed, and the popover ends up shifted ~80px past the right
    // viewport edge — the "Due cannot be set" symptom in BG_94404F77F9.
    const rafs = [];
    function chainUpdate() {
      update();
      rafs.push(requestAnimationFrame(() => {
        update();
        rafs.push(requestAnimationFrame(update));
      }));
    }
    rafs.push(requestAnimationFrame(chainUpdate));

    // Belt-and-braces: as soon as the popover (or any of its content)
    // resizes, re-run the position calculation. This catches cases where
    // images, fonts, or async-rendered subcomponents change the
    // popover's measured dimensions AFTER the rAF chain has settled.
    let ro = null;
    let roFirst = true;
    const RO = typeof window !== "undefined" ? window.ResizeObserver : null;
    if (RO) {
      ro = new RO(() => {
        // Skip the very first callback (it fires synchronously on
        // observe()) so we don't double-run update().
        if (roFirst) { roFirst = false; return; }
        update();
      });
    }
    // Attach the observer once popRef.current is non-null. We can't
    // attach inside the render path, so poll a couple of frames.
    let attachTries = 0;
    function tryAttachRO() {
      if (!ro) return;
      if (popRef.current) {
        ro.observe(popRef.current);
        return;
      }
      if (attachTries++ < 8) requestAnimationFrame(tryAttachRO);
    }
    requestAnimationFrame(tryAttachRO);

    function close(e) { if (!anchor.contains(e.target)) onClose(); }
    setTimeout(() => document.addEventListener("mousedown", close), 0);
    window.addEventListener("scroll", update, true);
    window.addEventListener("resize", update);
    return () => {
      for (const id of rafs) cancelAnimationFrame(id);
      if (ro) ro.disconnect();
      document.removeEventListener("mousedown", close);
      window.removeEventListener("scroll", update, true);
      window.removeEventListener("resize", update);
    };
  }, [anchor]);

  if (!pos) return null;
  return ReactDOM.createPortal(
    <div ref={popRef} className="popover"
         style={{
           position: "fixed", top: pos.top, left: pos.left,
           minWidth: pos.width,
           maxHeight: "calc(100vh - 16px)",
           overflowY: "auto",
         }}
         onMouseDown={(e) => e.stopPropagation()}>
      {children}
    </div>,
    document.body
  );
}

Object.assign(window, { Icon, Icons, Avatar, AvatarStack, StatusPill, PriorityPill, Popover });
