// App root — composes the screen into design canvas with multiple variants
const ConvoApp = ({ persona = "admin", permission = "full", defaultThread = "t_mia", defaultShowContact = false, defaultView = "all", showInboxCategories = false, handoffFrom = null, currentUserOverride = null, forceAIInContact = false, contactLayout = "current", showCoachingTab = false }) => {
  const [selected, setSelected] = useState(defaultThread);
  // Which pinned view the list is showing. Switching views clears the selection
  // so the thread column falls back to its empty state.
  const [view, setView] = useState(defaultView);
  // Open/Closed lens, independent of the pinned views. Open is the working
  // default — the inbox opens on what still needs action.
  const [status, setStatus] = useState("open");
  const [query, setQuery] = useState("");
  // Locally-created threads from the "Start New" modal. Merged with the
  // canonical THREADS list before rendering so they appear at the top of the
  // conversation list and can be selected normally.
  const [extraThreads, setExtraThreads] = useState([]);
  // "Start New" opens a modal to collect the from-line and destination.
  const [startNewOpen, setStartNewOpen] = useState(false);
  // Conversations closed from the list this session. Deliberately component
  // state, not storage — the movement resets when the session does.
  const [sessionClosed, setSessionClosed] = useState(() => new Set());
  // The row currently playing its exit animation, if the status change dropped
  // it out of the active view.
  const [exitingId, setExitingId] = useState(null);
  // Read/unread marked from the header menu, session-scoped like the closes.
  const [readOverride, setReadOverride] = useState({});
  // First-interaction messages for threads created in this session, keyed by
  // thread id — they have no entry in THREAD_MESSAGES.
  const [seededMessages, setSeededMessages] = useState({});
  // Right-side overlay state — only one of "contact" | "agent" | null at a time
  const [rightPane, setRightPane] = useState(defaultShowContact ? "contact" : null);
  const [agentSection, setAgentSection] = useState("brief");
  const showContact = rightPane === "contact";
  const showAgent = rightPane === "agent";
  const [activeCat, setActiveCat] = useState("inbox");
  const [categoriesCollapsed, setCategoriesCollapsed] = useState(false);
  // Used to flash a "focus" pulse on a specific section in the contact panel
  // when the user clicks "+N more" in the floating widget.
  const [contactFocus, setContactFocus] = useState({ key: null, ts: 0 });

  /* The prototype state controller for this page — app/convo-layout.js, one
     instance of the same gear the create flows and the call workspace carry.
     Its values are read into state and passed down as props rather than
     stamped on a root as classes: this tree is styled inline, so a class on an
     ancestor loses to every style attribute under it. */
  const listRef = useRef(null);
  const readCtl = () => {
    const C = window.jcConvoLayout;
    return {
      rows: C ? C.read("jc-cv-rows") : "r3",
      info: C ? C.read("jc-cv-info") : "single"
    };
  };
  const [ctl, setCtl] = useState(readCtl);
  useEffect(() => {
    const C = window.jcConvoLayout;
    if (!C) return;
    /* Nothing here re-mounts on a change: the open conversation, the list's
       scroll position and the composer's draft would all go with it. Both
       dimensions are one prop each, which is the test — a dimension that
       needed the tree rebuilt would be the wrong dimension. */
    C.show(() => setCtl(readCtl()));
    C.track(listRef.current);
    return () => {C.untrack();C.hide();};
  }, []);

  /* The call the agent is on, if it was placed from a conversation. The call
     UI itself lives outside React (app/call-experience.js) and only tells us
     that it connected — see cx:live there — so this is the whole of what
     Conversations knows about it. */
  const [liveCall, setLiveCall] = useState(() => window.__cxLive || null);
  useEffect(() => {
    const onLive = (e) => setLiveCall((e.detail && e.detail.call) || null);
    window.addEventListener("cx:live", onLive);
    return () => window.removeEventListener("cx:live", onLive);
  }, []);

  const toggleContact = () => {
    setRightPane(p => p === "contact" ? null : "contact");
  };
  const openAgent = (section = "brief") => {
    setAgentSection(section);
    setRightPane("agent");
  };
  const closeRight = () => { setRightPane(null); };

  // Listen for "+N more" clicks from the floating widget — open the contact
  // pane (AI insights tab) and pulse the requested section.
  useEffect(() => {
    const onOpenContactAI = (e) => {
      const focus = (e.detail && e.detail.focus) || "nextActions";
      setRightPane("contact");
      setContactFocus({ key: focus, ts: Date.now() });
    };
    window.addEventListener("convo:openContactAI", onOpenContactAI);
    return () => window.removeEventListener("convo:openContactAI", onOpenContactAI);
  }, []);

  // Auto-collapse the inbox categories panel whenever any right overlay opens
  useEffect(() => {
    if (rightPane) setCategoriesCollapsed(true);
  }, [rightPane]);

  // The contact sheet and the expanded primary side-nav compete for the same
  // horizontal room — ask the app shell to collapse the nav when it opens.
  useEffect(() => {
    if (rightPane === "contact") {
      window.dispatchEvent(new CustomEvent("convo:collapseSidebar"));
    }
  }, [rightPane]);

  // Mutually exclusive: if the inbox categories panel is expanded, hide right overlays
  useEffect(() => {
    if (showInboxCategories && !categoriesCollapsed && rightPane) {
      setRightPane(null);
    }
  }, [categoriesCollapsed, showInboxCategories]);

  const userProfile = CURRENT_USERS[persona];
  const baseUser = { ...userProfile, id: persona === "admin" ? "u_ayush" : "u_priya" };
  const currentUser = currentUserOverride ? { ...baseUser, ...currentUserOverride } : baseUser;

  const visibleThreads = useMemo(() => {
    // Conversations closed in this session are overlaid on the source data
    // rather than written back to it, so a reload restores the original state.
    const all = [...extraThreads, ...THREADS].map((t) => {
      let next = t;
      if (sessionClosed.has(t.id)) {
        next = { ...next, status: "closed", unread: 0, __sessionClosed: true };
      }
      // Marked read this session — drop the count. Marked unread — the row
      // reads as unread but shows a plain dot, since there's no real count.
      const r = readOverride[t.id];
      if (r === "read") next = { ...next, unread: 0, __dotUnread: false };
      if (r === "unread") next = { ...next, unread: 0, __dotUnread: true };
      return next;
    });
    if (persona === "admin") return all;
    return all.filter(t =>
      !t.assignee ||
      t.assignee.id === currentUser.id ||
      (handoffFrom && t.id === handoffFrom.threadId)
    );
  }, [persona, currentUser.id, handoffFrom, extraThreads, sessionClosed, readOverride]);

  // Close / reopen from the thread header. The conversation stays open so the
  // button's new state is visible and the action can be reversed; the row only
  // animates out of the list when the change drops it from the current view.
  const toggleClosed = (thread) => {
    if (!thread) return;
    const wasClosed = thread.status === "closed";
    setSessionClosed((prev) => {
      const next = new Set(prev);
      if (wasClosed) next.delete(thread.id); else next.add(thread.id);
      return next;
    });
    setExitingId(thread.id);
    setTimeout(() => setExitingId(null), 280);
    // ToastHost (inside ThreadView) listens for this; window.showAppToast that
    // other call sites use isn't actually defined anywhere.
    window.dispatchEvent(new CustomEvent("convo:toast", {
      detail: { msg: wasClosed ? "Conversation reopened" : "Conversation closed" }
    }));
  };

  const toggleRead = (thread, markRead) => {
    if (!thread) return;
    setReadOverride((prev) => ({ ...prev, [thread.id]: markRead ? "read" : "unread" }));
  };

  // Keep the selection valid — but `null` is a deliberate state (empty thread
  // column after a view switch), so don't auto-select over it.
  useEffect(() => {
    if (selected !== null && !visibleThreads.find(t => t.id === selected)) {
      setSelected(visibleThreads[0] ? visibleThreads[0].id : null);
    }
  }, [visibleThreads]);

  // Switching view resets the list and empties the thread column.
  const handleViewChange = (id) => {
    setView(id);
    setSelected(null);
    setRightPane(null);
  };

  // Narrowing to Open or Closed can drop the open conversation out of the list,
  // so it clears the selection the same way a view switch does.
  const handleStatusChange = (id) => {
    setStatus(id);
    setSelected(null);
    setRightPane(null);
  };

  // If the user un-pins the view we're on, fall back to the first one left.
  useEffect(() => {
    const sync = () => {
      const allowed = window.jcEnabledViews ? window.jcEnabledViews() : [];
      if (allowed.length && !allowed.find(v => v.id === view)) handleViewChange(allowed[0].id);
    };
    window.addEventListener("jc:convoViewsChanged", sync);
    window.addEventListener("jc:profileChanged", sync);
    return () => {
      window.removeEventListener("jc:convoViewsChanged", sync);
      window.removeEventListener("jc:profileChanged", sync);
    };
  }, [view]);

  const thread = selected === null ? null : (visibleThreads.find(t => t.id === selected) || null);
  const baseMessages = (thread && thread.__isNew) ?
    (seededMessages[thread.id] || []) :
    (THREAD_MESSAGES[thread?.id] || []);

  /* The ongoing call card, at the foot of the thread, for as long as the agent
     is on the call — the same card the campaign queue shows, from the same
     component (CallCard in messaging/thread-view.jsx, `status: "ongoing"`).
     Derived from the live call rather than appended to the thread, so it goes
     when the call does instead of leaving a call that has ended sitting there
     as though it were still up. */
  const messages = useMemo(() => {
    if (!thread || !liveCall || liveCall.threadId !== thread.id) return baseMessages;
    return [...baseMessages, {
      kind: "call", dir: "out", status: "ongoing",
      time: clockTime(),
      author: currentUser.name,
      lines: [liveCall.line || thread.line]
    }];
  }, [baseMessages, liveCall, thread && thread.id, currentUser.name]);

  // "Start New" opens the modal. Any open right sheet belongs to the
  // conversation being left behind, so close it.
  const onStartNew = () => {
    setRightPane(null);
    setStartNewOpen(true);
  };

  // Build a fresh thread placeholder from the compose view's recipient.
  // We accept "+1 5551234" or a free-typed name; the destination becomes the
  // thread name+phone so the row reads sensibly in the list.
  const makeNewThread = ({ to, line }) => {
    const looksLikePhone = /[0-9]/.test(to) && to.replace(/[^0-9]/g, "").length >= 4;
    const id = "t_new_" + Date.now();
    return {
      id,
      // The destination as typed is the thread's label either way; `phone` is
      // only set when that destination actually is a number.
      name: to,
      phone: looksLikePhone ? to : "",
      avatar: looksLikePhone ? "+" : (to.trim()[0] || "N").toUpperCase(),
      color: "#004CE6",
      channel: "sms",
      line: line ? line.label || line.name : "Mentor Ops — US",
      tags: [], showTags: false,
      preview: "",
      time: "now", unread: 0,
      assignee: { name: currentUser.name, id: currentUser.id },
      status: "open", isContact: false, pinned: false,
      __isNew: true
    };
  };

  // Shared by both modal actions: create the thread, drop it at the top of the
  // list and open it. `message` seeds the first bubble when there is one.
  const startConversation = ({ to, line, message }) => {
    const t = makeNewThread({ to, line });
    setExtraThreads((prev) => [t, ...prev]);
    if (message) setSeededMessages((prev) => ({ ...prev, [t.id]: [message] }));
    setSelected(t.id);
    setStartNewOpen(false);
    return t;
  };

  // Send SMS — open the new thread with the composer already in SMS mode so the
  // user lands where they can type.
  const handleStartMessage = ({ to, line }) => {
    startConversation({ to, line });
    requestAnimationFrame(() => {
      window.dispatchEvent(new CustomEvent("convo:composeMode", { detail: { mode: "sms" } }));
    });
  };

  /* Call — open the new thread and put the dialer on it.
   *
   * This used to seed the thread with a finished outbound call card, 00:00
   * long, because the thread had no other way to show that a call was being
   * placed. The thread draws the live call itself now — see the ongoing card
   * in `messages` above — so the seed would be the same call twice, one of
   * them a completed call that had not happened. The composer's Start Call
   * dropped a card of its own for the same reason and has stopped for the
   * same one. */
  const handleStartCall = ({ to, line }) => {
    const t = startConversation({ to, line });
    if (window.startCall) window.startCall(t);
  };

  return (
    <div style={{ height: "100%", display: "flex", flexDirection: "column", background: "#F9FAFB", fontFamily: "Inter, sans-serif", position: "relative" }}>
      {permission === "readonly" && (
        <div style={{
          background: "#FFFAEB", borderBottom: "1px solid #FEF0C7",
          padding: "6px 16px", display: "flex", alignItems: "center", gap: 8, fontSize: 12, color: "#B54708"
        }}>
          <I.lock size={13} stroke="#B54708" />
          <span>You have <strong>read-only</strong> access. Viewing only.</span>
        </div>
      )}

      <div style={{ display: "flex", flex: 1, minHeight: 0 }}>
        {!window.__EMBEDDED_MODE && <LeftRail activePage="conversations" />}

        <div style={{ display: "flex", flexDirection: "column", flex: 1, minWidth: 0 }}>
          {!window.__EMBEDDED_MODE && <TopBar user={currentUser} persona={persona} permission={permission} />}

          <div style={{ display: "flex", flex: 1, minHeight: 0, minWidth: 0 }}>
            {showInboxCategories && !categoriesCollapsed && <InboxCategories activeCat={activeCat} onCatChange={setActiveCat} />}
            <div style={{ position: "relative", display: "flex", flex: 1, minWidth: 0 }}>
            <ThreadList
              listRef={listRef}
              rowLines={ctl.rows}
              threads={visibleThreads}
              selected={selected}
              onSelect={(id) => setSelected(id)}
              view={view}
              onViewChange={handleViewChange}
              status={status}
              onStatusChange={handleStatusChange}
              currentUser={currentUser}
              query={query}
              onQueryChange={setQuery}
              showCollapseToggle={showInboxCategories}
              categoriesCollapsed={categoriesCollapsed}
              onToggleCategories={() => setCategoriesCollapsed(c => !c)}
              onStartNew={onStartNew}
              exitingId={exitingId}
            />
            {/* Nothing selected — a fresh view, or the user cleared it. */}
            {!thread && <EmptyThreadView />}
            {thread && (
              <div style={{ display: "flex", flex: 1, minWidth: 0 }}>
              <ThreadView
                thread={thread}
                messages={messages}
                permission={permission}
                persona={persona}
                user={currentUser}
                onToggleContact={toggleContact}
                showContact={showContact}
                showAIWidget={false}
                showInlineAI={false}
                aiFocus="natural"
                onOpenAgent={openAgent}
                handoffFrom={handoffFrom && thread.id === handoffFrom.threadId ? handoffFrom : null}
                onToggleClosed={() => toggleClosed(thread)}
                onToggleRead={(markRead) => toggleRead(thread, markRead)}
              />
              </div>
            )}
            </div>
            <RightSheetMount show={!!thread && rightPane === "contact"} onClose={closeRight} duration={180}>
              {thread && (
                <ContactPane
                  thread={thread}
                  permission={permission}
                  persona={persona}
                  showAIInsights={true}
                  forceAI={forceAIInContact}
                  layout={contactLayout}
                  infoLayout={ctl.info}
                  onClose={closeRight}
                  onOpenAgent={openAgent}
                  focus={contactFocus}
                  showCoachingTab={showCoachingTab}
                />
              )}
            </RightSheetMount>
            {thread && rightPane === "agent" && (
              <RightSheet onClose={closeRight}>
                <AIAgentPanel
                  thread={thread}
                  onClose={closeRight}
                  onSwitchToContact={() => setRightPane("contact")}
                  initialSection={agentSection}
                />
              </RightSheet>
            )}
          </div>
        </div>
      </div>
      <CallWidget />
      <StartNewModal
        open={startNewOpen}
        onClose={() => setStartNewOpen(false)}
        onMessage={handleStartMessage}
        onCall={handleStartCall} />
    </div>
  );
};

// The card's own clock counts from when it mounted, so this is only the
// timestamp beside it — the same shape the rest of the thread's messages use.
const clockTime = () => {
  const d = new Date();
  const h = d.getHours() % 12 || 12;
  return h + ":" + String(d.getMinutes()).padStart(2, "0") + " " + (d.getHours() < 12 ? "AM" : "PM");
};

// Wrap instance in a bordered frame that fits the viewport
const Frame = ({ children }) => (
  <div style={{
    width: "100%", height: "100%", background: "#FFFFFF",
    overflow: "hidden"
  }}>
    {children}
  </div>
);

const App = () => {
  const [t, setTweak] = useTweaks(window.__TWEAKS);
  const cfg = window.__ROLE_CONFIG || {};
  const showTweaks = cfg.showTweaks !== false;
  return (
    <Frame>
      <ConvoApp
        persona={cfg.persona || "admin"}
        permission={cfg.permission || "full"}
        defaultThread={cfg.defaultThread || "t_mia"}
        defaultShowContact={cfg.defaultShowContact !== false}
        showInboxCategories={cfg.showInboxCategories || false}
        handoffFrom={cfg.handoffFrom || null}
        currentUserOverride={cfg.currentUserOverride || null}
        forceAIInContact={cfg.forceAIInContact || false}
        contactLayout={t.contactLayout}
        showCoachingTab={cfg.showCoachingTab || false}
      />
      {showTweaks && (
        <TweaksPanel title="Tweaks">
          {cfg.showContactLayoutTweak !== false && (
            <TweakSection title="Contact panel layout">
              <TweakRadio
                label="Version"
                value={t.contactLayout}
                onChange={(v) => setTweak("contactLayout", v)}
                options={[
                  { value: "previous", label: "Previous" },
                  { value: "current", label: "Current" },
                ]}
              />
            </TweakSection>
          )}
        </TweaksPanel>
      )}
    </Frame>
  );
};

// Two boot modes:
// - Standalone (Convo-agent.html etc.) → mount immediately into #root.
// - Embedded inside JustCall Dashboard → mount into #convo-mount on demand.
//   The host calls window.__mountConvoApp() when the Messages page becomes active,
//   and window.__unmountConvoApp() when navigating away. We post a "convo:mounted"
//   event so the host can fade out its skeleton.
(function bootConvo(){
  let _root = null;
  let _container = null;
  window.__mountConvoApp = function () {
    const target = document.getElementById("convo-mount") || document.getElementById("root");
    if (!target) return;
    // If the host re-rendered #convo-mount (e.g. user navigated away and back),
    // the previous root is bound to a detached node. Tear it down before
    // creating a fresh root on the new element.
    if (_root && _container !== target) {
      try { _root.unmount(); } catch(e){}
      _root = null;
    }
    if (!_root) {
      _root = ReactDOM.createRoot(target);
      _container = target;
    }
    _root.render(<App />);
    requestAnimationFrame(() => window.dispatchEvent(new Event("convo:mounted")));
  };
  window.__unmountConvoApp = function () {
    if (_root) { try { _root.unmount(); } catch(e){} _root = null; _container = null; }
  };
  const standaloneRoot = document.getElementById("root");
  if (standaloneRoot && !document.getElementById("convo-mount")) {
    _root = ReactDOM.createRoot(standaloneRoot);
    _root.render(<App />);
  }
})();
