// ui/assignee.jsx — who owns a finding and when it is due: the assignee pill and
// picker dialog, and the remediation due-date helpers + pill.
/* ---------------------------------------------------------- assignee */
// Up-to-two-letter monogram for the avatar chip (first + last token initials).
function initials(name) {
  const parts = (name || "?").trim().split(/[\s._-]+/).filter(Boolean);
  const first = (parts[0] || "?")[0];
  const last = parts.length > 1 ? parts[parts.length - 1][0] : "";
  return (first + last).toUpperCase();
}

// The assignees chip. `assignments` is an array of AssignmentDto (may be empty).
// Non-interactive renders a <span> so it can sit inside <button> rows (like StatusPill).
// Shows up to three stacked avatars, then a name ("alice") or a count ("3 assignees").
function AssigneePill({ assignments, interactive, onClick }) {
  const list = assignments || [];
  const n = list.length;
  const inner = n === 0 ? (
    <>
      <span className="assignee-avatar is-empty"><Icon name="user" size={12} /></span>
      <span className="assignee-name">Unassigned</span>
      {interactive && <Icon name="chevD" size={12} className="statuspill-chev" />}
    </>
  ) : (
    <>
      <span className="assignee-stack">
        {list.slice(0, 3).map((a) =>
          <span key={a.assigneeUserId} className="assignee-avatar" title={a.assigneeUsername}>{initials(a.assigneeUsername)}</span>
        )}
      </span>
      <span className="assignee-name">{n === 1 ? list[0].assigneeUsername : n + " assignees"}</span>
      {interactive && <Icon name="chevD" size={12} className="statuspill-chev" />}
    </>
  );
  const cls = "assigneepill" + (n === 0 ? " is-unassigned" : "");
  if (!interactive) return <span className={cls}>{inner}</span>;
  return <button className={cls + " assigneepill-int"} onClick={onClick} type="button">{inner}</button>;
}

// Assignee picker MODAL popup. Lazily loads the caller's company roster (cached in
// the API layer) and lists teammates as toggles; assigned teammates are checked.
// Multi-select — the popup stays open so several can be picked. Each toggle saves
// immediately via onToggle(user, isCurrentlyAssigned). `current` is the array of
// AssignmentDto for this vuln; onClearAll() removes everyone; onClose() dismisses.
function AssigneeModal({ vulnId, current, onToggle, onClearAll, onClose, deadline, status, onSetDeadline }) {
  const [users, setUsers] = useState(null);   // null = loading
  // The due date is OPTIONAL: assigning stays a one-click action, and the
  // severity-based SLA will fill in anything left blank. It is also independent of
  // the assignee list — the deadline belongs to the vulnerability, so clearing the
  // assignees does not clear it.
  const [due, setDue] = useState((deadline && deadline.dueOn) || "");
  const [dueReason, setDueReason] = useState((deadline && deadline.reason) || "");
  const [savingDue, setSavingDue] = useState(false);
  const [dueNote, setDueNote] = useState(null);   // { ok } | { err } — the last save's outcome
  const today = new Date().toISOString().slice(0, 10);
  const dueChanged = due !== ((deadline && deadline.dueOn) || "")
    || dueReason !== ((deadline && deadline.reason) || "");
  // A "yyyy-MM-dd" string compares as a date. The picker's min stops a past date
  // being PICKED; a past date can still be typed, and must not go to the server.
  const duePast = !!due && due < today;
  // What auto-save is allowed to send: a complete, not-past date that differs from
  // what is stored (or a new reason for a stored date). Clearing is never automatic.
  const dueSaveable = !!due && !duePast && dueChanged;

  // The date saves ITSELF, the way the assignees do — the dialog promises
  // "changes save immediately", and a date that only saved on a button a person did
  // not press was a date silently lost on Done. A short pause lets the picker's
  // segments and the reason's keystrokes settle into one write. The pending timer is
  // kept so Done can flush it rather than close over it.
  const dueTimer = useRef(null);
  const dueLatest = useRef({ due, dueReason });
  dueLatest.current = { due, dueReason };
  async function saveDue(now) {
    if (!onSetDeadline || savingDue) return false;
    if (dueTimer.current) { clearTimeout(dueTimer.current); dueTimer.current = null; }
    const { due: d, dueReason: r } = now || dueLatest.current;
    setSavingDue(true); setDueNote(null);
    const ok = await onSetDeadline(vulnId, d || null, r || null);
    setSavingDue(false);
    setDueNote(ok === false
      ? { err: d ? "The date could not be saved." : "The deadline could not be cleared." }
      : { ok: d ? "Saved" : "Cleared" });
    return ok !== false;
  }
  useEffect(() => {
    if (!dueSaveable) return;
    dueTimer.current = setTimeout(() => { dueTimer.current = null; saveDue(); }, 700);
    return () => { if (dueTimer.current) { clearTimeout(dueTimer.current); dueTimer.current = null; } };
  }, [due, dueReason]);
  // Done flushes a date still waiting on the timer, so nothing typed is lost.
  async function finish() {
    if (dueSaveable && !savingDue) await saveDue();
    onClose();
  }

  useEffect(() => {
    let live = true;
    window.VCApi.assignableUsers().then(
      (list) => { if (live) setUsers(Array.isArray(list) ? list : []); },
      () => { if (live) setUsers([]); });
    return () => { live = false; };
  }, []);
  useEffect(() => {
    function key(e) { if (e.key === "Escape") onClose(); }
    document.addEventListener("keydown", key);
    return () => document.removeEventListener("keydown", key);
  }, []);
  const currentIds = new Set((current || []).map((a) => a.assigneeUserId));
  const count = (current || []).length;
  // Portal out of the screen tree. `.page` runs a filling keyframe animation whose
  // frames set `transform`, which makes it the containing block for position:fixed
  // descendants — rendered in place, the scrim sizes and centres itself against the
  // whole scrolling page instead of the viewport, so on a long page (the detail
  // screen) the modal lands halfway down the document with the sidebar uncovered.
  // Host it on `.app` rather than document.body so it still inherits data-theme and
  // the CSS variables set there.
  const host = (typeof document !== "undefined" && document.querySelector(".app")) || document.body;
  return ReactDOM.createPortal(
    <div className="asm-scrim" onClick={onClose}>
      <div className="asm-modal" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
        <div className="asm-modal-head">
          <div>
            <span className="eyebrow">Assign</span>
            <h3 className="mono">{vulnId}</h3>
          </div>
          <button className="iconbtn" onClick={onClose} title="Close"><Icon name="close" size={16} /></button>
        </div>
        <div className="asm-modal-sub">Select one or more teammates in your company. Changes save immediately.</div>

        {onSetDeadline && (
          <div className="asm-due">
            <div className="asm-due-head">
              <span className="asm-due-lab"><Icon name="clock" size={13} />Remediation deadline</span>
              <DuePill deadline={deadline} status={status} />
            </div>
            <div className="asm-due-row">
              <input
                type="date"
                className="asm-due-input"
                value={due}
                min={today}
                onChange={(e) => setDue(e.target.value)}
                aria-label="Due date" />
              <input
                type="text"
                className="asm-due-reason"
                placeholder="Reason (optional)"
                value={dueReason}
                maxLength={200}
                onChange={(e) => setDueReason(e.target.value)} />
            </div>
            <div className="asm-due-foot">
              <span className={"asm-due-note" + (duePast || (dueNote && dueNote.err) ? " is-err" : dueNote && dueNote.ok && !dueChanged ? " is-ok" : "")}>
                {duePast
                  ? "That date is already in the past."
                  : savingDue
                    ? "Saving…"
                    : dueNote && dueNote.err
                      ? dueNote.err
                      : dueNote && dueNote.ok && !dueChanged
                        ? <><Icon name="check" size={12} />{dueNote.ok === "Cleared" ? "Cleared — no deadline on this vulnerability." : "Saved — the deadline is set for this vulnerability."}</>
                        : "Optional — one date for this vulnerability, shared by everyone assigned to it. Saves as soon as it is picked."}
              </span>
              {deadline && deadline.dueOn && (
                <button className="btn btn-ghost btn-sm" disabled={savingDue} onClick={() => { setDue(""); setDueReason(""); saveDue({ due: "", dueReason: "" }); }}>Clear</button>
              )}
            </div>
          </div>
        )}
        <div className="asm-modal-body">
          {users === null && <div className="asmenu-empty">Loading team…</div>}
          {users !== null && users.length === 0 && <div className="asmenu-empty">No teammates to assign.</div>}
          {(users || []).map((u) => {
            const on = currentIds.has(u.userId);
            return (
              <button key={u.userId} className={"asm-user" + (on ? " is-on" : "")} onClick={() => onToggle(u, on)} type="button">
                <span className="assignee-avatar">{initials(u.username)}</span>
                <span className="asm-user-meta">
                  <span className="asm-user-name">{u.username}</span>
                  {u.email && <span className="asm-user-email">{u.email}</span>}
                </span>
                <span className={"asm-check" + (on ? " is-on" : "")}>{on && <Icon name="check" size={13} />}</span>
              </button>
            );
          })}
        </div>
        <div className="asm-modal-foot">
          <span className="asm-count">{count} assigned</span>
          <span className="asm-foot-sp" />
          {count > 0 && <button className="btn btn-ghost btn-sm" onClick={onClearAll}>Clear all</button>}
          <button className="btn btn-sm" onClick={finish} disabled={savingDue}>
            {savingDue ? <span className="btn-spin" /> : null}Done
          </button>
        </div>
      </div>
    </div>,
    host
  );
}

/* ------------------------------------------------------- remediation due date */
/* A vulnerability's deadline, and how it reads right now.

   Overdue is DERIVED here, never stored: a stored flag would need a nightly job to
   stay true and would still be wrong the moment someone remediates. It also depends
   on triage status — something already remediated or risk-accepted is finished, so
   it is not overdue no matter how old the date is. Missing a deadline never changes
   the status itself; that would put a false statement into the compliance record. */
const DUE_SOON_DAYS = 7;

// Whole days from today to `iso` (a "yyyy-MM-dd" calendar day). Both sides are
// pinned to UTC midnight so a browser in any timezone counts the same number of
// days — comparing a date-only value against a local clock is what produces
// off-by-one "overdue since tomorrow" bugs.
function daysUntil(iso) {
  if (!iso) return null;
  const parts = String(iso).slice(0, 10).split("-");
  if (parts.length !== 3) return null;
  const due = Date.UTC(+parts[0], +parts[1] - 1, +parts[2]);
  const now = new Date();
  const today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
  if (isNaN(due)) return null;
  return Math.round((due - today) / 86400000);
}

// { state, label, days } for a deadline. state drives the colour:
//   done    — remediated/accepted, so the date no longer matters
//   overdue — past and still open
//   today   — due today
//   soon    — within DUE_SOON_DAYS
//   ok      — further out
function dueMeta(deadline, status) {
  if (!deadline || !deadline.dueOn) return { state: "none", label: "", short: "", days: null };
  const days = daysUntil(deadline.dueOn);
  if (days === null) return { state: "none", label: "", short: "", days: null };
  const finished = status === "remediated" || status === "accepted";
  const on = deadline.dueOn;
  if (finished) return { state: "done", label: "Due " + on, short: on, days };
  if (days < 0) { const d = Math.abs(days); return { state: "overdue", label: "Overdue by " + d + (d === 1 ? " day" : " days"), short: d + "d over", days }; }
  if (days === 0) return { state: "today", label: "Due today", short: "Today", days };
  if (days <= DUE_SOON_DAYS) return { state: "soon", label: "Due in " + days + (days === 1 ? " day" : " days"), short: days + "d left", days };
  return { state: "ok", label: "Due " + on, short: on, days };
}

// The due-date chip. Non-interactive renders a <span> so it can sit inside the
// ledger's <button> rows, matching StatusPill/AssigneePill.
function DuePill({ deadline, status, interactive, onClick, compact }) {
  const m = dueMeta(deadline, status);
  const inner = m.state === "none" ? (
    <>
      <Icon name="clock" size={12} />
      <span className="duepill-lab">{compact ? "—" : "No deadline"}</span>
      {interactive && <Icon name="chevD" size={12} className="statuspill-chev" />}
    </>
  ) : (
    <>
      <Icon name={m.state === "overdue" ? "alert" : "clock"} size={12} />
      <span className="duepill-lab">{compact ? m.short : m.label}</span>
      {interactive && <Icon name="chevD" size={12} className="statuspill-chev" />}
    </>
  );
  const cls = "duepill duepill-" + m.state;
  const title = m.state === "none" ? "No remediation deadline set"
    : "Due " + deadline.dueOn + (deadline.setByUsername ? " · set by " + deadline.setByUsername : "")
      + (deadline.reason ? " · " + deadline.reason : "");
  if (!interactive) return <span className={cls} title={title}>{inner}</span>;
  return <button className={cls + " duepill-int"} onClick={onClick} type="button" title={title}>{inner}</button>;
}

Object.assign(window, { AssigneePill, AssigneeModal, DuePill, dueMeta, daysUntil, DUE_SOON_DAYS });
