// chat.jsx — 1:1 personal chat side-bubble + panel.
//
// Surfaces:
//   - ChatBubble: pinned bottom-right floating button. Click opens
//                 the panel. Shows an unread badge.
//   - ChatPanel:  240px conversation rail + active-thread pane
//                 + composer. Uses api.dm.* and listens for
//                 `flowboard:rt:dm.message` SSE events.
//
// Data:
//   - GET /api/dm/conversations   → left rail (with unread + last preview)
//   - GET /api/dm/messages?with=  → active thread
//   - POST /api/dm/messages       → send
//   - POST /api/dm/messages/read  → mark read on open
//
// Retention: messages older than 7 days are hard-deleted server-side
// by lib/dm-purge.js. The panel shows a small "Messages auto-delete
// after 7 days" hint at the top of the thread.

const _CHAT_LS_KEY = "fb.chat.openWith";

function _meId() {
  try {
    if (typeof window !== "undefined" && window.api && typeof window.api.getUser === "function") {
      const u = window.api.getUser();
      return u && u.id || null;
    }
  } catch {}
  return null;
}

function _personById(id) {
  if (!id) return null;
  if (typeof PEOPLE !== "undefined" && Array.isArray(PEOPLE)) {
    return PEOPLE.find(p => p.id === id) || null;
  }
  return null;
}

function _initials(name) {
  if (!name) return "?";
  return String(name).trim().split(/\s+/).map(s => s[0]).slice(0, 2).join("").toUpperCase();
}

function _fmtTime(iso) {
  if (!iso) return "";
  const d = new Date(iso);
  if (Number.isNaN(d.getTime())) return "";
  const h12 = ((d.getHours() + 11) % 12) + 1;
  const ampm = d.getHours() >= 12 ? "PM" : "AM";
  const mm = String(d.getMinutes()).padStart(2, "0");
  return `${h12}:${mm} ${ampm}`;
}
function _fmtDayLabel(iso) {
  if (!iso) return "";
  const d = new Date(iso);
  if (Number.isNaN(d.getTime())) return "";
  const today = new Date();
  const sameDay = (a, b) =>
    a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
  if (sameDay(d, today)) return "Today";
  const yest = new Date(today); yest.setDate(today.getDate() - 1);
  if (sameDay(d, yest)) return "Yesterday";
  return d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
}
function _fmtRelative(iso) {
  if (!iso) return "";
  const d = new Date(iso); if (Number.isNaN(d.getTime())) return "";
  const sec = Math.floor((Date.now() - d.getTime()) / 1000);
  if (sec < 60)        return "now";
  if (sec < 3600)      return Math.floor(sec / 60) + "m";
  if (sec < 86400)     return Math.floor(sec / 3600) + "h";
  if (sec < 7 * 86400) return Math.floor(sec / 86400) + "d";
  return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
}

// ── Avatar circle (re-used) ────────────────────────────────────────
function ChatAvatar({ person, size = 34 }) {
  const cls = "chat-row-avatar";
  const style = { width: size, height: size, fontSize: size <= 22 ? 9 : 13 };
  if (person && person.avatar) {
    return <span className={cls} style={style}><img src={person.avatar} alt=""/></span>;
  }
  return <span className={cls} style={style}>{_initials(person && person.name)}</span>;
}

// ── Floating bubble ────────────────────────────────────────────────
function ChatBubble({ open, onToggle, unread }) {
  return (
    <button
      className={"chat-bubble" + (open ? " is-open" : "")}
      title="Personal chat"
      onClick={onToggle}
      aria-label="Open personal chat"
    >
      <Icons.MessageSq size={22}/>
      {unread > 0 && !open && (
        <span className="chat-bubble-badge">{unread > 99 ? "99+" : unread}</span>
      )}
    </button>
  );
}

// ── Conversation row in the left rail ──────────────────────────────
function ChatRow({ convo, person, isActive, onPick }) {
  return (
    <div
      className={"chat-row" + (isActive ? " is-active" : "")}
      onClick={() => onPick(convo.other_user_id)}
    >
      <ChatAvatar person={person}/>
      <div className="chat-row-body">
        <div className="chat-row-name">{(person && person.name) || "Unknown"}</div>
        <div className="chat-row-preview">{convo.last_body || ""}</div>
      </div>
      <span className="chat-row-time">{_fmtRelative(convo.last_at)}</span>
      {convo.unread > 0 && (
        <span className="chat-row-unread">{convo.unread > 99 ? "99+" : convo.unread}</span>
      )}
    </div>
  );
}

// ── Active thread (messages + composer) ────────────────────────────
function _friendlyDmError(e) {
  const code = (e && (e.body && e.body.error)) || (e && e.message) || "";
  switch (String(code)) {
    case "recipient_not_found":  return "That teammate isn't reachable right now.";
    case "cannot_message_self":  return "You can't send a message to yourself.";
    case "missing_recipient":    return "Pick a teammate first.";
    case "empty_body":           return "Type something to send.";
    case "too_long":             return "Message is too long (4000 chars max).";
    case "dm_table_missing":     return "Chat isn't fully set up on the server yet — ask the admin to run the latest migrations.";
    default:                     return "Couldn't send: " + (code || "network error");
  }
}

function ChatThread({ withId, withPerson, onBack, onSent }) {
  const me = _meId();
  const [msgs, setMsgs] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [draft, setDraft] = React.useState("");
  const [sending, setSending] = React.useState(false);
  const [sendErr, setSendErr] = React.useState("");
  const scrollRef = React.useRef(null);
  const endRef    = React.useRef(null);
  const inputRef = React.useRef(null);
  // Tracks whether the user is currently parked near the bottom of
  // the thread. We only auto-scroll on new messages when this is true,
  // so a teammate's incoming message doesn't yank you out of the
  // history you're actively reading. Updated on every scroll event.
  const stickToBottomRef = React.useRef(true);
  // Bumped whenever a "new message just arrived from the other side"
  // — used to flash a "Jump to latest" pill if the user is scrolled up.
  const [hasNewBelow, setHasNewBelow] = React.useState(false);

  // Load messages on mount / when withId changes
  React.useEffect(() => {
    let stale = false;
    setLoading(true);
    setMsgs([]);
    if (!withId) return;
    (async () => {
      try {
        const data = await window.api.dm.messages(withId, 200);
        if (!stale) setMsgs(Array.isArray(data) ? data : []);
      } catch (e) {
        console.warn("[chat] messages load failed:", e && e.message);
      } finally {
        if (!stale) setLoading(false);
      }
    })();
    // Mark this thread read on open
    (async () => {
      try { await window.api.dm.markRead(withId); } catch {}
      try { window.dispatchEvent(new CustomEvent("flowboard:dm:read", { detail: { from_user_id: withId } })); } catch {}
    })();
    return () => { stale = true; };
  }, [withId]);

  // Realtime — append new messages from the other side; also reflect
  // our own sends via SSE echo (filtered by ids we've already inserted).
  React.useEffect(() => {
    if (!withId) return;
    const onMsg = (ev) => {
      const m = ev && ev.detail; if (!m || !m.id) return;
      const between =
        (m.sender_id === me && m.recipient_id === withId) ||
        (m.sender_id === withId && m.recipient_id === me);
      if (!between) return;
      setMsgs(prev => prev.some(x => x.id === m.id) ? prev : [...prev, m]);
      // If the new message is FROM the other user, mark read straight away
      if (m.sender_id === withId) {
        (async () => { try { await window.api.dm.markRead(withId); } catch {} })();
        // If the user is scrolled up reading history, surface a
        // "new message" pill instead of yanking them down.
        if (!stickToBottomRef.current) setHasNewBelow(true);
      }
    };
    const onDel = (ev) => {
      const id = ev && ev.detail && ev.detail.id; if (!id) return;
      setMsgs(prev => prev.filter(x => x.id !== id));
    };
    window.addEventListener("flowboard:rt:dm.message", onMsg);
    window.addEventListener("flowboard:rt:dm.deleted", onDel);
    return () => {
      window.removeEventListener("flowboard:rt:dm.message", onMsg);
      window.removeEventListener("flowboard:rt:dm.deleted", onDel);
    };
  }, [withId, me]);

  // Track whether the user is parked near the bottom of the scroller.
  // 80px tolerance is enough that a tiny scroll wheel nudge mid-message
  // doesn't disable autoscroll, but a deliberate scroll-up does.
  function onScroll() {
    const el = scrollRef.current; if (!el) return;
    const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
    const nearBottom = distance < 80;
    stickToBottomRef.current = nearBottom;
    if (nearBottom && hasNewBelow) setHasNewBelow(false);
  }

  function scrollToBottom(smooth) {
    const end = endRef.current; const el = scrollRef.current;
    if (!end || !el) return;
    // Defer to next paint so newly-inserted bubbles have laid out and
    // the final scrollHeight is accurate. Setting scrollTop in the
    // same tick we appended a message often lands a few pixels short.
    requestAnimationFrame(() => {
      try {
        end.scrollIntoView({ block: "end", behavior: smooth ? "smooth" : "auto" });
      } catch {
        el.scrollTop = el.scrollHeight;
      }
    });
  }

  // Auto-scroll on:
  //   - thread switch (always pin to bottom; resets stickiness)
  //   - first paint after messages finish loading
  //   - any new message *if* the user is currently sticking to the bottom
  React.useEffect(() => {
    // Thread changed — reset stickiness and pin to bottom of the new thread.
    stickToBottomRef.current = true;
    setHasNewBelow(false);
  }, [withId]);
  React.useEffect(() => {
    if (loading) return;            // wait until messages are in
    if (!stickToBottomRef.current) return;
    scrollToBottom(false);
  }, [loading, msgs.length, withId]);

  // Auto-focus the input when a thread opens
  React.useEffect(() => {
    if (withId && inputRef.current) inputRef.current.focus();
  }, [withId]);

  async function send() {
    const body = draft.trim();
    if (!body || sending) return;
    setSending(true);
    setSendErr("");
    setDraft("");
    // Optimistic insert
    const tempId = "tmp_" + Date.now();
    const optimistic = {
      id: tempId,
      sender_id: me,
      recipient_id: withId,
      body,
      created_at: new Date().toISOString(),
      read_at: null,
    };
    setMsgs(prev => [...prev, optimistic]);
    // Sending a message is an unambiguous "I want to see the latest"
    // signal — pin the view back to the bottom even if the user had
    // scrolled up to read history.
    stickToBottomRef.current = true;
    setHasNewBelow(false);
    try {
      const real = await window.api.dm.send(withId, body);
      setMsgs(prev => prev.map(m => m.id === tempId ? real : m));
      if (typeof onSent === "function") onSent(withId);
    } catch (e) {
      setMsgs(prev => prev.filter(m => m.id !== tempId));
      setDraft(body);
      const friendly = _friendlyDmError(e);
      setSendErr(friendly);
      // Also flash the global toast if available
      if (typeof window.fbToast === "function") window.fbToast(friendly, 4000);
      console.warn("[chat] send failed:", e && (e.body || e.message));
    } finally {
      setSending(false);
    }
  }

  function onKey(e) {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      send();
    }
  }

  // Auto-grow the textarea up to its CSS max-height. We don't read the
  // computed style every keystroke (cheap but unnecessary) — the max
  // here mirrors `.chat-input { max-height: 120px }` in chat.css. If
  // the user clears the field, snap back to the single-row baseline.
  function autoGrow(el) {
    if (!el) return;
    el.style.height = "auto";
    const next = Math.min(el.scrollHeight, 120);
    el.style.height = next + "px";
  }
  React.useEffect(() => { autoGrow(inputRef.current); }, [draft]);

  // Render messages with day separators + sender grouping
  const rendered = [];
  let lastDay = ""; let lastSender = "";
  msgs.forEach(m => {
    const day = _fmtDayLabel(m.created_at);
    if (day !== lastDay) {
      rendered.push({ kind: "day", key: "d-" + m.id, label: day });
      lastDay = day;
      lastSender = "";
    }
    const grouped = lastSender === m.sender_id;
    rendered.push({ kind: "msg", key: m.id, msg: m, grouped });
    lastSender = m.sender_id;
  });

  return (
    <div className="chat-thread-pane">
      <div className="chat-thread-head">
        <button className="back" onClick={onBack} aria-label="Back to conversations">
          <Icons.ChevronRt size={14} style={{ transform: "rotate(180deg)" }}/>
        </button>
        <ChatAvatar person={withPerson} size={28}/>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="chat-thread-name">{(withPerson && withPerson.name) || "Unknown"}</div>
          <div className="chat-thread-sub">
            Personal chat · auto-deletes in 7 days
          </div>
        </div>
        <button className="chat-thread-close" onClick={onBack} aria-label="Close" title="Back">
          <Icons.Close size={14}/>
        </button>
      </div>

      <div className="chat-thread-scroll" ref={scrollRef} onScroll={onScroll}>
        {!loading && msgs.length === 0 && (
          <div className="chat-thread-empty">
            <div style={{ fontSize: 28, marginBottom: 6 }}>💬</div>
            <div style={{ fontWeight: 700, color: "#0f1729", marginBottom: 4 }}>
              Say hi to {(withPerson && withPerson.name && withPerson.name.split(" ")[0]) || "your teammate"}
            </div>
            <div>Messages here are private and auto-delete after 7 days.</div>
          </div>
        )}
        {!loading && msgs.length > 0 && (
          <div className="chat-retention-note" title="Messages auto-delete after 7 days">
            Messages auto-delete after 7 days
          </div>
        )}
        {rendered.map(item => {
          if (item.kind === "day") return <div key={item.key} className="chat-day">{item.label}</div>;
          const m = item.msg;
          const mine = m.sender_id === me;
          return (
            <div
              key={item.key}
              className={
                "chat-msg " +
                (mine ? "chat-msg-mine" : "chat-msg-them") +
                (item.grouped ? " is-grouped" : "")
              }
            >
              {m.body}
              <div className="chat-msg-time">{_fmtTime(m.created_at)}</div>
            </div>
          );
        })}
        {/* Sentinel — scrollIntoView({block:"end"}) targets this so the
            real bottom (after gap + last bubble) is what gets revealed,
            regardless of intermediate margins. */}
        <div ref={endRef} aria-hidden="true" style={{ height: 1 }}/>
      </div>

      {hasNewBelow && (
        <button
          type="button"
          className="chat-jump-latest"
          onClick={() => {
            stickToBottomRef.current = true;
            setHasNewBelow(false);
            scrollToBottom(true);
          }}
          aria-label="Jump to latest message"
        >
          New messages ↓
        </button>
      )}

      {sendErr && (
        <div
          role="alert"
          style={{
            background: "#fdebee",
            color: "#a33345",
            borderTop: "1px solid #f4c4cc",
            padding: "6px 12px",
            fontSize: 12,
            display: "flex",
            alignItems: "center",
            gap: 8,
          }}
        >
          <span style={{ flex: 1 }}>{sendErr}</span>
          <button
            onClick={() => setSendErr("")}
            style={{
              border: 0, background: "transparent",
              color: "#a33345", cursor: "pointer", padding: 2,
            }}
            aria-label="Dismiss"
          ><Icons.Close size={11}/></button>
        </div>
      )}

      <div className="chat-composer">
        <textarea
          ref={inputRef}
          className="chat-input"
          placeholder="Write a message…"
          rows={1}
          value={draft}
          onChange={e => setDraft(e.target.value)}
          onKeyDown={onKey}
          maxLength={4000}
        />
        <button
          className="chat-send"
          disabled={!draft.trim() || sending}
          onClick={send}
          title="Send (Enter)"
        >
          <Icons.Arrow size={16}/>
        </button>
      </div>
    </div>
  );
}

// ── Empty thread placeholder ──────────────────────────────────────
function ChatPickPlaceholder() {
  return (
    <div className="chat-pick">
      <div className="chat-pick-icon"><Icons.MessageSq size={22}/></div>
      <div className="chat-pick-title">Personal chat</div>
      <div>Pick a teammate from the left to start a conversation.</div>
      <div style={{ fontSize: 11, color: "#99a0b0", marginTop: 12 }}>
        Messages auto-delete after 7 days.
      </div>
    </div>
  );
}

// ── Main panel ────────────────────────────────────────────────────
function ChatPanel({ onClose, openWithId, onPick, conversations, onConvosChange }) {
  const me = _meId();
  const [search, setSearch] = React.useState("");

  // Active thread person + convo
  const withPerson = openWithId ? _personById(openWithId) : null;

  // Build a "people not in conversations" list for the quick-start strip
  const convoIds = new Set((conversations || []).map(c => c.other_user_id));
  const teammates = (typeof PEOPLE !== "undefined" && Array.isArray(PEOPLE) ? PEOPLE : [])
    .filter(p => p.id && p.id !== me && !convoIds.has(p.id));

  const filteredConvos = (conversations || []).filter(c => {
    if (!search) return true;
    const p = _personById(c.other_user_id);
    const name = (p && p.name) || "";
    return name.toLowerCase().includes(search.toLowerCase());
  });
  const filteredTeammates = teammates.filter(p =>
    !search || (p.name || "").toLowerCase().includes(search.toLowerCase())
  );

  return (
    <div className={"chat-panel" + (openWithId ? " is-thread" : "")}>
      {/* Left rail */}
      <div className="chat-list-pane">
        <div className="chat-list-head">
          <div className="chat-list-title">
            <Icons.MessageSq size={14}/> Personal chat
            <button
              onClick={onClose}
              className="chat-thread-close"
              style={{ marginLeft: "auto" }}
              aria-label="Close"
              title="Close"
            >
              <Icons.Close size={14}/>
            </button>
          </div>
          <div className="chat-list-sub">Auto-deletes after 7 days</div>
          <input
            className="chat-search"
            type="text"
            placeholder="Search teammates…"
            value={search}
            onChange={e => setSearch(e.target.value)}
          />
        </div>

        <div className="chat-list-scroll">
          {filteredConvos.length === 0 && filteredTeammates.length === 0 && (
            <div className="chat-list-empty">No teammates yet.</div>
          )}

          {filteredConvos.map(c => {
            const p = _personById(c.other_user_id);
            return (
              <ChatRow
                key={c.other_user_id}
                convo={c}
                person={p}
                isActive={openWithId === c.other_user_id}
                onPick={onPick}
              />
            );
          })}

          {filteredTeammates.length > 0 && (
            <div className="chat-new">
              <div className="chat-new-label">Start new chat</div>
              <div className="chat-new-people">
                {filteredTeammates.slice(0, 30).map(p => (
                  <button key={p.id} className="chat-new-person" onClick={() => onPick(p.id)}>
                    {p.avatar
                      ? <span className="av"><img src={p.avatar} alt=""/></span>
                      : <span className="av">{_initials(p.name)}</span>}
                    {p.name}
                  </button>
                ))}
              </div>
            </div>
          )}
        </div>
      </div>

      {/* Right pane */}
      {openWithId
        ? <ChatThread
            withId={openWithId}
            withPerson={withPerson}
            onBack={() => onPick(null)}
            onSent={() => onConvosChange && onConvosChange()}
          />
        : <ChatPickPlaceholder/>}
    </div>
  );
}

// ── Top-level controller (mounted once in app.jsx) ────────────────
function ChatBubbleApp() {
  const me = _meId();
  const [open, setOpen] = React.useState(false);
  const [openWithId, _setOpenWithId] = React.useState(() => {
    try { return localStorage.getItem(_CHAT_LS_KEY) || null; } catch { return null; }
  });
  const [conversations, setConversations] = React.useState([]);
  const [unreadTotal, setUnreadTotal] = React.useState(0);

  function setOpenWithId(id) {
    _setOpenWithId(id);
    try { id ? localStorage.setItem(_CHAT_LS_KEY, id) : localStorage.removeItem(_CHAT_LS_KEY); } catch {}
  }

  // Load conversations whenever the panel opens or a new message arrives
  const loadConvos = React.useCallback(async () => {
    try {
      const list = await window.api.dm.conversations();
      setConversations(Array.isArray(list) ? list : []);
      const total = (Array.isArray(list) ? list : []).reduce((n, c) => n + Number(c.unread || 0), 0);
      setUnreadTotal(total);
    } catch (e) {
      // 401 / network — silently keep last known list
    }
  }, []);

  // Initial load + periodic refresh as a safety net (in case SSE drops)
  React.useEffect(() => {
    if (!me) return;
    loadConvos();
    const t = setInterval(() => { loadConvos(); }, 60 * 1000);
    return () => clearInterval(t);
  }, [me, loadConvos]);

  // Refresh when an SSE dm event arrives
  React.useEffect(() => {
    function onAny() { loadConvos(); }
    window.addEventListener("flowboard:rt:dm.message", onAny);
    window.addEventListener("flowboard:rt:dm.read",    onAny);
    window.addEventListener("flowboard:rt:dm.deleted", onAny);
    window.addEventListener("flowboard:dm:read",       onAny);
    return () => {
      window.removeEventListener("flowboard:rt:dm.message", onAny);
      window.removeEventListener("flowboard:rt:dm.read",    onAny);
      window.removeEventListener("flowboard:rt:dm.deleted", onAny);
      window.removeEventListener("flowboard:dm:read",       onAny);
    };
  }, [loadConvos]);

  // Allow other parts of the UI to programmatically open a chat with
  // someone — e.g. clicking a name in the team roster could
  // window.dispatchEvent(new CustomEvent("flowboard:chat:open", { detail: { userId } }))
  React.useEffect(() => {
    function onOpenReq(e) {
      const uid = e && e.detail && e.detail.userId;
      if (!uid) return;
      setOpen(true);
      setOpenWithId(uid);
    }
    window.addEventListener("flowboard:chat:open", onOpenReq);
    return () => window.removeEventListener("flowboard:chat:open", onOpenReq);
  }, []);

  // ESC closes the panel
  React.useEffect(() => {
    if (!open) return;
    function onKey(e) { if (e.key === "Escape") setOpen(false); }
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [open]);

  // Body class — chat.css uses this to fade out the bug-report button
  // while the chat panel is open, preventing visual overlap.
  React.useEffect(() => {
    try {
      if (open) document.body.classList.add("chat-panel-open");
      else      document.body.classList.remove("chat-panel-open");
    } catch {}
    return () => {
      try { document.body.classList.remove("chat-panel-open"); } catch {}
    };
  }, [open]);

  if (!me) return null;     // not logged in

  return (
    <>
      <ChatBubble
        open={open}
        unread={unreadTotal}
        onToggle={() => setOpen(o => !o)}
      />
      {open && (
        <ChatPanel
          onClose={() => setOpen(false)}
          openWithId={openWithId}
          onPick={setOpenWithId}
          conversations={conversations}
          onConvosChange={loadConvos}
        />
      )}
    </>
  );
}

Object.assign(window, { ChatBubble, ChatPanel, ChatThread, ChatBubbleApp });
