import axios from 'axios'
import { io, type Socket } from 'socket.io-client'
import { useEffect, useMemo, useState, type MouseEvent as ReactMouseEvent } from 'react'
import { useAuth } from '../auth/auth-context'
import { renderMarkdown } from '../lib/markdown'
import { guildMembersCenter, joinGuildCenter } from '../lib/center-auth-client'

type Attachment = { url: string; name?: string; mimeType?: string }
type MessageItem = {
  messageId: string
  seq: number
  content: string
  isDeleted?: boolean
  authorUserId?: string
  createdAt?: string
  attachments?: Attachment[]
  // per-push metadata (only present on socket-delivered messages); the app is
  // normally metadata-agnostic — only surfaced in developer mode
  wakeup?: boolean
}

type CanvasFormat = 'md' | 'html' | 'text'
type CanvasDoc = {
  channelId: string
  sharerUserId: string
  title: string
  format: CanvasFormat
  source: string
  version: number
  updatedAt: string
}

const DEV_KEY = 'fabric.debug'

function initials(s: string): string {
  const t = (s || '?').trim()
  return t.slice(0, 2).toUpperCase()
}

function timeOf(iso?: string): string {
  if (!iso) return ''
  const d = new Date(iso)
  if (Number.isNaN(d.getTime())) return ''
  return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}

const X_TYPES = ['general', 'work', 'report', 'discuss', 'triage', 'custom'] as const
type XType = (typeof X_TYPES)[number]

type GuildChannel = { id: string; name: string; guildId?: string; xType?: XType; isMember?: boolean; isPublic?: boolean; closed?: boolean }
type MemberItem = { userId: string; email: string; name: string; status: string }

type MenuItem =
  | { kind: 'sep' }
  | { kind: 'item'; label: string; onClick: () => void; danger?: boolean; disabled?: boolean }
type CtxMenu = { x: number; y: number; title: string; items: MenuItem[] }

export default function ChatPage() {
  const { session, logout, ensureFreshToken, refreshGuilds, updateName } = useAuth()
  const [selectedGuildId, setSelectedGuildId] = useState('')
  const [selectedChannelId, setSelectedChannelId] = useState('')
  const [channelMemberIds, setChannelMemberIds] = useState<string[]>([])
  const [bypassMemberIds, setBypassMemberIds] = useState<string[]>([])
  const [channels, setChannels] = useState<GuildChannel[]>([])
  const [guildDbId, setGuildDbId] = useState('')
  const [members, setMembers] = useState<MemberItem[]>([])
  const [messages, setMessages] = useState<MessageItem[]>([])
  const [content, setContent] = useState('')
  const [newChannelName, setNewChannelName] = useState('')
  const [joinGuildNodeId, setJoinGuildNodeId] = useState('')
  const [selectedMemberIds, setSelectedMemberIds] = useState<string[]>([])
  const [newChannelPublic, setNewChannelPublic] = useState(false)
  const [newChannelXType, setNewChannelXType] = useState<XType>('general')
  const [newChannelOnDuty, setNewChannelOnDuty] = useState('')
  const [newChannelListeners, setNewChannelListeners] = useState<string[]>([])
  const [newChannelBypass, setNewChannelBypass] = useState<string[]>([])
  const [showCreateChannelModal, setShowCreateChannelModal] = useState(false)
  const [showSettingsModal, setShowSettingsModal] = useState(false)
  const [showAddGuildModal, setShowAddGuildModal] = useState(false)
  const [settingsName, setSettingsName] = useState('')
  const [showMembers, setShowMembers] = useState(true)
  const [devMode, setDevMode] = useState(() => localStorage.getItem(DEV_KEY) === '1')
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')
  const [ctxMenu, setCtxMenu] = useState<CtxMenu | null>(null)
  const [pendingFiles, setPendingFiles] = useState<File[]>([])
  const [uploading, setUploading] = useState(false)
  const [canvas, setCanvas] = useState<CanvasDoc | null>(null)
  const [canvasCollapsed, setCanvasCollapsed] = useState(false)
  const [showCanvasModal, setShowCanvasModal] = useState(false)
  const [canvasTitle, setCanvasTitle] = useState('')
  const [canvasFormat, setCanvasFormat] = useState<CanvasFormat>('md')
  const [canvasSource, setCanvasSource] = useState('')
  const [canvasEditMode, setCanvasEditMode] = useState(false)

  function openCtx(e: ReactMouseEvent, title: string, items: MenuItem[]) {
    e.preventDefault()
    e.stopPropagation()
    if (!items.length) return
    setCtxMenu({ x: e.clientX, y: e.clientY, title, items })
  }

  function copyText(text: string) {
    navigator.clipboard?.writeText(text).catch(() => {})
  }

  function insertMention(id: string) {
    setContent((c) => `${c.trim() ? c.replace(/\s*$/, '') + ' ' : ''}<@${id}> `)
  }

  useEffect(() => {
    if (!ctxMenu) return
    const close = () => setCtxMenu(null)
    const onKey = (ev: KeyboardEvent) => {
      if (ev.key === 'Escape') close()
    }
    window.addEventListener('click', close)
    window.addEventListener('resize', close)
    window.addEventListener('scroll', close, true)
    window.addEventListener('keydown', onKey)
    return () => {
      window.removeEventListener('click', close)
      window.removeEventListener('resize', close)
      window.removeEventListener('scroll', close, true)
      window.removeEventListener('keydown', onKey)
    }
  }, [ctxMenu])

  function toggleDevMode(on: boolean) {
    setDevMode(on)
    if (on) localStorage.setItem(DEV_KEY, '1')
    else localStorage.removeItem(DEV_KEY)
  }

  const guilds = session?.guilds ?? []

  useEffect(() => {
    if (!selectedGuildId && guilds.length) setSelectedGuildId(guilds[0].nodeId)
  }, [guilds, selectedGuildId])

  // Guild access tokens have a short TTL and are persisted in localStorage.
  // On (re)load they may be expired, so re-issue fresh ones from Center
  // before any guild API call. refreshGuilds also refreshes the center token.
  useEffect(() => {
    refreshGuilds().catch(() => {})
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  const guild = useMemo(() => guilds.find((g) => g.nodeId === selectedGuildId) ?? null, [guilds, selectedGuildId])
  const guildToken = useMemo(
    () => (session?.guildAccessTokens ?? []).find((x) => x.guildNodeId === selectedGuildId)?.token ?? '',
    [session, selectedGuildId],
  )

  function guildApi() {
    if (!guild || !guildToken) throw new Error('guild or token missing')
    return axios.create({
      baseURL: `${guild.endpoint}/api`,
      timeout: 10000,
      headers: { Authorization: `Bearer ${guildToken}` },
    })
  }

  const socket = useMemo<Socket | null>(() => {
    if (!guild || !guildToken) return null
    return io(`${guild.endpoint}/realtime`, {
      transports: ['websocket'],
      auth: { token: guildToken },
      autoConnect: false,
    })
  }, [guild, guildToken])

  async function loadChannels() {
    if (!guild || !guildToken) return
    setError('')
    try {
      const res = await guildApi().get('/channels', { params: guildDbId ? { guildId: guildDbId } : undefined })
      const list = Array.isArray(res.data) ? (res.data as GuildChannel[]) : []
      setChannels(list)
      if (!guildDbId && list[0]?.guildId) setGuildDbId(list[0].guildId)
      if (!selectedChannelId && list.length) setSelectedChannelId(list[0].id)
    } catch {
      setError('Failed to load channels')
      setChannels([])
    }
  }

  async function loadMembers() {
    if (!session || !selectedGuildId) return
    setError('')
    try {
      const token = await ensureFreshToken()
      if (!token) return
      const list = await guildMembersCenter(session.centerApiBase, token, selectedGuildId)
      setMembers(list)
    } catch {
      setError('Failed to load guild members')
      setMembers([])
    }
  }

  async function loadChannelMembers() {
    if (!guild || !guildToken || !selectedChannelId) {
      setChannelMemberIds([])
      setBypassMemberIds([])
      return
    }
    try {
      const res = await guildApi().get(`/channels/${selectedChannelId}/members`)
      const list = Array.isArray(res.data)
        ? (res.data as Array<{ userId: string; bypass?: boolean }>)
        : []
      setChannelMemberIds(list.map((x) => x.userId))
      setBypassMemberIds(list.filter((x) => x.bypass).map((x) => x.userId))
    } catch {
      setChannelMemberIds([])
      setBypassMemberIds([])
    }
  }

  async function pullMessages() {
    if (!selectedChannelId || !guild || !guildToken) return
    setLoading(true)
    setError('')
    try {
      const res = await guildApi().get(`/channels/${selectedChannelId}/messages`, {
        params: { seq_from: 1, seq_to: 999999, limit: 100 },
      })
      setMessages(res.data.items ?? [])
    } catch {
      setError('Failed to load messages')
    } finally {
      setLoading(false)
    }
  }

  async function uploadFile(file: File): Promise<Attachment> {
    const form = new FormData()
    form.append('file', file)
    const res = await guildApi().post(
      `/files?channelId=${encodeURIComponent(selectedChannelId)}`,
      form,
    )
    return { url: res.data.url as string, name: res.data.name as string, mimeType: res.data.mimeType as string }
  }

  async function sendMessage() {
    if (!selectedChannelId || !guild || !guildToken) return
    if (!content.trim() && pendingFiles.length === 0) return
    setError('')
    setUploading(true)
    try {
      const attachments: Attachment[] = []
      for (const f of pendingFiles) attachments.push(await uploadFile(f))
      await guildApi().post(`/channels/${selectedChannelId}/messages`, {
        content: content.trim() || (attachments.length ? `[${attachments.length} file(s)]` : ''),
        authorUserId: session?.user.id ?? 'unknown',
        attachments,
      })
      setContent('')
      setPendingFiles([])
      await pullMessages()
    } catch {
      setError('Failed to send message')
    } finally {
      setUploading(false)
    }
  }

  // ---- canvas (single pinned document per channel) ----
  async function loadCanvas() {
    if (!guild || !guildToken || !selectedChannelId) {
      setCanvas(null)
      return
    }
    try {
      const res = await guildApi().get(`/channels/${selectedChannelId}/canvas`)
      setCanvas(res.data && res.data.channelId ? (res.data as CanvasDoc) : null)
    } catch {
      setCanvas(null)
    }
  }

  function openCanvasShare() {
    setCanvasEditMode(false)
    setCanvasTitle('')
    setCanvasFormat('md')
    setCanvasSource('')
    setShowCanvasModal(true)
  }

  function openCanvasEdit() {
    if (!canvas) return
    setCanvasEditMode(true)
    setCanvasTitle(canvas.title)
    setCanvasFormat(canvas.format)
    setCanvasSource(canvas.source)
    setShowCanvasModal(true)
  }

  async function submitCanvas() {
    if (!guild || !guildToken || !selectedChannelId) return
    const body = { title: canvasTitle, format: canvasFormat, source: canvasSource }
    try {
      if (canvasEditMode) {
        await guildApi().patch(`/channels/${selectedChannelId}/canvas`, body)
      } else {
        await guildApi().put(`/channels/${selectedChannelId}/canvas`, body)
      }
      setShowCanvasModal(false)
      await loadCanvas()
    } catch {
      setError(canvasEditMode ? 'Failed to update canvas' : 'Failed to share canvas')
    }
  }

  async function removeCanvas() {
    if (!guild || !guildToken || !selectedChannelId) return
    try {
      await guildApi().delete(`/channels/${selectedChannelId}/canvas`)
      setCanvas(null)
    } catch {
      setError('Failed to remove canvas')
    }
  }

  async function addGuild() {
    if (!session || !joinGuildNodeId.trim()) return
    setError('')
    try {
      const token = await ensureFreshToken()
      if (!token) return
      await joinGuildCenter(session.centerApiBase, token, joinGuildNodeId.trim())
      await refreshGuilds()
      setJoinGuildNodeId('')
      setShowAddGuildModal(false)
    } catch {
      setError('Failed to add guild')
    }
  }

  function toggleMember(userId: string) {
    setSelectedMemberIds((prev) => (prev.includes(userId) ? prev.filter((x) => x !== userId) : [...prev, userId]))
  }

  function toggleListener(userId: string) {
    setNewChannelListeners((prev) => (prev.includes(userId) ? prev.filter((x) => x !== userId) : [...prev, userId]))
  }

  function toggleBypass(userId: string) {
    setNewChannelBypass((prev) => (prev.includes(userId) ? prev.filter((x) => x !== userId) : [...prev, userId]))
  }

  function openCreateChannel() {
    setNewChannelName('')
    setSelectedMemberIds([])
    setNewChannelPublic(false)
    setNewChannelXType('general')
    setNewChannelOnDuty(session?.user.id ?? '')
    setNewChannelListeners([])
    setNewChannelBypass([])
    setShowCreateChannelModal(true)
  }

  async function createChannel() {
    if (!guild || !guildToken || !newChannelName.trim()) return
    const effectiveGuildId = guildDbId || selectedGuildId
    if (!effectiveGuildId) {
      setError('Cannot create channel: no guild selected')
      return
    }
    if (newChannelXType === 'triage' && !newChannelOnDuty) {
      setError('Triage channels require an on-duty user')
      return
    }
    setError('')
    try {
      const payload = {
        name: newChannelName.trim(),
        guildId: effectiveGuildId,
        xType: newChannelXType,
        isPublic: newChannelPublic,
        memberUserIds: selectedMemberIds,
        onDuty: newChannelXType === 'triage' ? newChannelOnDuty : undefined,
        listeners: newChannelXType === 'custom' ? newChannelListeners : [],
        bypassUserIds:
          newChannelXType === 'discuss' || newChannelXType === 'work'
            ? newChannelBypass
            : [],
      }
      const res = await guildApi().post('/channels', payload)
      const createdId = res.data?.id as string | undefined
      setNewChannelName('')
      setSelectedMemberIds([])
      setNewChannelPublic(false)
      setNewChannelXType('general')
      setNewChannelOnDuty(session?.user.id ?? '')
      setNewChannelListeners([])
      setNewChannelBypass([])
      setShowCreateChannelModal(false)
      await loadChannels()
      if (createdId) setSelectedChannelId(createdId)
    } catch {
      setError('Failed to create channel')
    }
  }

  async function joinChannel(c: GuildChannel) {
    if (!guild || !guildToken) return
    try {
      await guildApi().post(`/channels/${c.id}/join`)
      await loadChannels()
      await loadMembers()
      await loadChannelMembers()
    } catch {
      setError('Failed to join channel')
    }
  }

  async function moveToBypass(userId: string) {
    if (!guild || !guildToken || !selectedChannelId) return
    try {
      await guildApi().post(`/channels/${selectedChannelId}/bypass`, { userId })
      await loadChannelMembers()
    } catch {
      setError('Failed to move member to bypass')
    }
  }

  async function closeChannel(c: GuildChannel) {
    if (!guild || !guildToken) return
    try {
      await guildApi().post(`/channels/${c.id}/close`)
      await loadChannels()
    } catch {
      setError('Failed to close channel')
    }
  }

  async function deleteMessage(m: MessageItem) {
    if (!guild || !guildToken || !selectedChannelId) return
    try {
      await guildApi().delete(`/channels/${selectedChannelId}/messages/${m.messageId}`)
      // socket message.deleted will reconcile; optimistic fallback:
      setMessages((prev) =>
        prev.map((x) =>
          x.messageId === m.messageId ? { ...x, isDeleted: true, content: '[deleted]' } : x,
        ),
      )
    } catch {
      setError('Failed to delete message')
    }
  }

  async function leaveChannel(c: GuildChannel) {
    if (!guild || !guildToken) return
    try {
      await guildApi().post(`/channels/${c.id}/leave`)
      if (selectedChannelId === c.id) {
        setSelectedChannelId('')
        setMessages([])
      }
      await loadChannels()
      await loadMembers()
      await loadChannelMembers()
    } catch {
      setError('Failed to leave channel')
    }
  }

  async function saveName() {
    const trimmed = settingsName.trim()
    if (!trimmed) {
      setError('Name must not be empty')
      return
    }
    try {
      await updateName(trimmed)
      setShowSettingsModal(false)
      await loadMembers()
    } catch {
      setError('Failed to update name')
    }
  }

  useEffect(() => {
    void loadMembers()
    setSelectedMemberIds([])
  }, [selectedGuildId])

  useEffect(() => {
    void loadChannels()
    setMessages([])
    setSelectedChannelId('')
  }, [selectedGuildId, guildDbId, guildToken])

  useEffect(() => {
    void pullMessages()
    void loadChannelMembers()
    void loadCanvas()
    setPendingFiles([])
    setCanvasCollapsed(false)
  }, [selectedChannelId])

  useEffect(() => {
    if (!socket || !selectedChannelId) return

    socket.on('message.created', (m: MessageItem) => setMessages((prev) => [...prev, m]))
    socket.on('message.updated', (m: MessageItem) =>
      setMessages((prev) => prev.map((x) => (x.messageId === m.messageId ? m : x))),
    )
    socket.on('message.deleted', (m: MessageItem) =>
      setMessages((prev) => prev.map((x) => (x.messageId === m.messageId ? { ...x, isDeleted: true, content: '[deleted]' } : x))),
    )
    socket.on('canvas.updated', (c: CanvasDoc) => {
      if (c.channelId === selectedChannelId) setCanvas(c)
    })
    socket.on('canvas.removed', (p: { channelId: string }) => {
      if (p.channelId === selectedChannelId) setCanvas(null)
    })

    socket.connect()
    socket.emit('join_channel', { channelId: selectedChannelId })

    return () => {
      socket.emit('leave_channel', { channelId: selectedChannelId })
      socket.removeAllListeners()
      socket.disconnect()
    }
  }, [socket, selectedChannelId])

  const currentChannel = channels.find((c) => c.id === selectedChannelId) ?? null
  const nameById = new Map(members.map((m) => [m.userId, m.name || m.email]))
  const authorLabel = (uid?: string) =>
    uid ? (uid === session?.user.id ? session?.user.name || 'You' : nameById.get(uid) || uid.slice(0, 8)) : 'unknown'
  const mentionName = (id: string) =>
    id === session?.user.id
      ? session?.user.name || session?.user.email || 'you'
      : nameById.get(id) || id.slice(0, 8)

  // Browser <img>/<a> can't send Authorization; pass the guild token in the
  // query (the guild ApiKeyGuard accepts ?access_token=).
  const fileUrl = (u: string) =>
    `${guild?.endpoint ?? ''}${u}${u.includes('?') ? '&' : '?'}access_token=${encodeURIComponent(guildToken)}`
  const isCanvasSharer = !!canvas && canvas.sharerUserId === session?.user.id

  const guildById = new Map(members.map((m) => [m.userId, m]))
  const channelMembers = channelMemberIds.map(
    (id) => guildById.get(id) ?? { userId: id, email: '', name: '', status: 'active' },
  )
  const rotationChannel =
    currentChannel?.xType === 'discuss' || currentChannel?.xType === 'work'
  const renderMember = (
    m: { userId: string; name?: string; email?: string },
    inChannel = false,
  ) => {
    const label = m.name || m.email || m.userId.slice(0, 8)
    const isBypass = bypassMemberIds.includes(m.userId)
    const showBypassUi = inChannel && rotationChannel
    return (
      <div
        key={m.userId}
        className="member-row"
        onContextMenu={(e) => openCtx(e, label, userMenu(m, inChannel))}
      >
        <div className="avatar dot">{initials(label)}</div>
        <div style={{ minWidth: 0, flex: 1 }}>
          <span className="nm">{label}</span>
          {m.userId === session?.user.id ? <span className="you">you</span> : null}
          {showBypassUi && isBypass ? <span className="chan-tag">bypass</span> : null}
        </div>
        {showBypassUi && !isBypass ? (
          <button
            className="btn btn-secondary btn-xs"
            title="Exclude from speaking rotation (only woken when @-mentioned)"
            onClick={() => moveToBypass(m.userId)}
          >
            → bypass
          </button>
        ) : null}
      </div>
    )
  }

  // ---- right-click context menus (per resource) ----
  function guildMenu(g: { nodeId: string; name: string }): MenuItem[] {
    return [
      { kind: 'item', label: 'Open', onClick: () => setSelectedGuildId(g.nodeId) },
      { kind: 'sep' },
      { kind: 'item', label: 'Copy node ID', onClick: () => copyText(g.nodeId) },
      { kind: 'item', label: 'Copy name', onClick: () => copyText(g.name) },
      { kind: 'sep' },
      { kind: 'item', label: 'Add guild…', onClick: () => setShowAddGuildModal(true) },
    ]
  }

  function channelMenu(c: GuildChannel): MenuItem[] {
    const items: MenuItem[] = [
      { kind: 'item', label: 'Open', onClick: () => setSelectedChannelId(c.id) },
    ]
    if (c.isMember)
      items.push({ kind: 'item', label: 'Leave channel', danger: true, onClick: () => leaveChannel(c) })
    else if (c.isPublic)
      items.push({ kind: 'item', label: 'Join channel', onClick: () => joinChannel(c) })
    if (!c.closed)
      items.push({ kind: 'item', label: 'Close channel', danger: true, onClick: () => closeChannel(c) })
    if (c.id === selectedChannelId) {
      items.push({ kind: 'sep' })
      if (!canvas)
        items.push({ kind: 'item', label: 'Share canvas…', onClick: openCanvasShare })
      else if (isCanvasSharer)
        items.push(
          { kind: 'item', label: 'Edit canvas…', onClick: openCanvasEdit },
          { kind: 'item', label: 'Remove canvas', danger: true, onClick: () => void removeCanvas() },
        )
    }
    items.push(
      { kind: 'sep' },
      { kind: 'item', label: 'Copy channel ID', onClick: () => copyText(c.id) },
      { kind: 'item', label: 'Copy name', onClick: () => copyText(c.name) },
      { kind: 'sep' },
      { kind: 'item', label: 'Create channel…', onClick: openCreateChannel },
    )
    return items
  }

  function messageMenu(m: MessageItem): MenuItem[] {
    const isGuild = m.authorUserId === 'guild'
    const items: MenuItem[] = [
      { kind: 'item', label: 'Copy text', onClick: () => copyText(m.content) },
      { kind: 'item', label: 'Copy message ID', onClick: () => copyText(m.messageId) },
    ]
    if (m.authorUserId && !isGuild)
      items.push({
        kind: 'item',
        label: 'Mention author',
        onClick: () => insertMention(m.authorUserId as string),
      })
    items.push(
      { kind: 'sep' },
      {
        kind: 'item',
        label: 'Delete message',
        danger: true,
        disabled: !!m.isDeleted || isGuild,
        onClick: () => deleteMessage(m),
      },
    )
    return items
  }

  function userMenu(
    m: { userId: string; name?: string; email?: string },
    inChannel: boolean,
  ): MenuItem[] {
    const isSelf = m.userId === session?.user.id
    const label = m.name || m.email || m.userId.slice(0, 8)
    const items: MenuItem[] = [
      { kind: 'item', label: 'Mention', onClick: () => insertMention(m.userId) },
      { kind: 'item', label: 'Copy user ID', onClick: () => copyText(m.userId) },
      { kind: 'item', label: 'Copy name', onClick: () => copyText(label) },
    ]
    if (inChannel && rotationChannel && !bypassMemberIds.includes(m.userId))
      items.push(
        { kind: 'sep' },
        { kind: 'item', label: 'Move to bypass', onClick: () => moveToBypass(m.userId) },
      )
    if (isSelf)
      items.push(
        { kind: 'sep' },
        {
          kind: 'item',
          label: 'Settings…',
          onClick: () => {
            setSettingsName(session?.user.name ?? '')
            setShowSettingsModal(true)
          },
        },
      )
    return items
  }

  function blankMenu(scope: 'guilds' | 'channels' | 'messages' | 'members' | 'root'): MenuItem[] {
    if (scope === 'guilds')
      return [{ kind: 'item', label: 'Add guild…', onClick: () => setShowAddGuildModal(true) }]
    if (scope === 'channels')
      return [
        { kind: 'item', label: 'Create channel…', disabled: !guild, onClick: openCreateChannel },
        { kind: 'item', label: 'Refresh channels', onClick: () => void loadChannels() },
      ]
    if (scope === 'messages')
      return [
        {
          kind: 'item',
          label: 'Refresh messages',
          disabled: !selectedChannelId,
          onClick: () => void pullMessages(),
        },
        {
          kind: 'item',
          label: canvas ? (isCanvasSharer ? 'Edit canvas…' : 'View canvas') : 'Share canvas…',
          disabled: !selectedChannelId,
          onClick: canvas
            ? isCanvasSharer
              ? openCanvasEdit
              : () => setCanvasCollapsed(false)
            : openCanvasShare,
        },
      ]
    if (scope === 'members')
      return [
        {
          kind: 'item',
          label: 'Refresh members',
          onClick: () => {
            void loadMembers()
            void loadChannelMembers()
          },
        },
      ]
    return [
      {
        kind: 'item',
        label: 'Refresh',
        onClick: () => {
          void loadChannels()
          void loadMembers()
        },
      },
    ]
  }

  function renderCtxMenu() {
    if (!ctxMenu) return null
    const MENU_W = 220
    const estH =
      36 +
      ctxMenu.items.reduce((a, it) => a + (it.kind === 'sep' ? 9 : 32), 0)
    const x = Math.max(6, Math.min(ctxMenu.x, window.innerWidth - MENU_W - 8))
    const y = Math.max(6, Math.min(ctxMenu.y, window.innerHeight - estH - 8))
    return (
      <div
        className="ctx-menu"
        style={{ left: x, top: y, width: MENU_W }}
        onContextMenu={(e) => e.preventDefault()}
        onClick={(e) => e.stopPropagation()}
      >
        <div className="ctx-title">{ctxMenu.title}</div>
        {ctxMenu.items.map((it, i) =>
          it.kind === 'sep' ? (
            <div key={i} className="ctx-sep" />
          ) : (
            <button
              key={i}
              className={`ctx-item ${it.danger ? 'danger' : ''}`}
              disabled={it.disabled}
              onClick={() => {
                if (it.disabled) return
                setCtxMenu(null)
                it.onClick()
              }}
            >
              {it.label}
            </button>
          ),
        )}
      </div>
    )
  }

  return (
    <div className="dc-shell" onContextMenu={(e) => openCtx(e, 'Fabric', blankMenu('root'))}>
      <nav className="dc-rail" onContextMenu={(e) => openCtx(e, 'Guilds', blankMenu('guilds'))}>
        {guilds.map((g) => (
          <button
            key={g.nodeId}
            className={`rail-btn ${selectedGuildId === g.nodeId ? 'active' : ''}`}
            title={g.name}
            onClick={() => setSelectedGuildId(g.nodeId)}
            onContextMenu={(e) => openCtx(e, g.name, guildMenu(g))}
          >
            {initials(g.name)}
          </button>
        ))}
        {guilds.length ? <div className="rail-sep" /> : null}
        <button className="rail-btn rail-add" title="Add guild" onClick={() => setShowAddGuildModal(true)}>
          +
        </button>
      </nav>

      <aside className="dc-sidebar">
        <div className="dc-sidebar-head">{guild?.name ?? 'No guild selected'}</div>
        <div
          className="dc-sidebar-scroll"
          onContextMenu={(e) => openCtx(e, 'Channels', blankMenu('channels'))}
        >
          <div className="dc-section-label">
            <span>Channels</span>
            <button title="Create channel" onClick={openCreateChannel}>
              +
            </button>
          </div>
          {channels.length ? (
            channels.map((c) => (
              <button
                key={c.id}
                className={`chan-btn xt-${c.xType ?? 'general'} ${selectedChannelId === c.id ? 'active' : ''}`}
                onClick={() => setSelectedChannelId(c.id)}
                onContextMenu={(e) => openCtx(e, `#${c.name}`, channelMenu(c))}
              >
                <span className="hash">#</span>
                <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.name}</span>
                {c.xType ? <span className="chan-tag">{c.xType}</span> : null}
              </button>
            ))
          ) : (
            <div className="dc-empty">{guild ? 'No channels yet' : 'Pick a guild from the left'}</div>
          )}
        </div>
        <div className="dc-userbar">
          <div className="avatar">{initials(session?.user.name || session?.user.email || '?')}</div>
          <div className="who">
            <div className="nm">{session?.user.name || session?.user.email}</div>
            <div className="sub">{session?.user.email}</div>
          </div>
          <button
            className="icon-btn"
            title="Settings"
            onClick={() => {
              setSettingsName(session?.user.name ?? '')
              setShowSettingsModal(true)
            }}
          >
            ⚙
          </button>
          <button className="icon-btn" title="Logout" onClick={() => void logout()}>
            ⏻
          </button>
        </div>
      </aside>

      <main className="dc-main">
        <header className="dc-topbar">
          {currentChannel ? (
            <span className="title">
              <span className="hash">#</span>
              {currentChannel.name}
            </span>
          ) : (
            <span className="title muted">Select a channel</span>
          )}
          <span className="spacer" />
          {currentChannel ? (
            currentChannel.isMember ? (
              <button className="btn btn-secondary" onClick={() => leaveChannel(currentChannel)}>
                Leave
              </button>
            ) : (
              <button className="btn" onClick={() => joinChannel(currentChannel)}>
                Join
              </button>
            )
          ) : null}
          {currentChannel ? (
            <button
              className="btn btn-secondary"
              title="Share a pinned document in this channel"
              onClick={canvas ? (isCanvasSharer ? openCanvasEdit : () => setCanvasCollapsed(false)) : openCanvasShare}
            >
              {canvas ? (isCanvasSharer ? 'Edit canvas' : 'Canvas') : 'Share canvas'}
            </button>
          ) : null}
          <button
            className="icon-btn"
            title={showMembers ? 'Hide members' : 'Show members'}
            onClick={() => setShowMembers((v) => !v)}
          >
            ☰
          </button>
        </header>

        {canvas ? (
          <section className="dc-canvas">
            <div className="dc-canvas-head">
              <span className="cv-title">🗎 {canvas.title}</span>
              <span className="cv-meta">
                shared by {authorLabel(canvas.sharerUserId)} · v{canvas.version} ·{' '}
                {timeOf(canvas.updatedAt)}
              </span>
              <span className="spacer" />
              {isCanvasSharer ? (
                <>
                  <button className="icon-btn" title="Edit canvas" onClick={openCanvasEdit}>
                    ✎
                  </button>
                  <button className="icon-btn" title="Remove canvas" onClick={removeCanvas}>
                    🗑
                  </button>
                </>
              ) : null}
              <button
                className="icon-btn"
                title={canvasCollapsed ? 'Expand' : 'Collapse'}
                onClick={() => setCanvasCollapsed((v) => !v)}
              >
                {canvasCollapsed ? '▸' : '▾'}
              </button>
            </div>
            {canvasCollapsed ? null : (
              <div className="dc-canvas-body">
                {canvas.format === 'html' ? (
                  <iframe
                    className="cv-frame"
                    title={canvas.title}
                    sandbox=""
                    srcDoc={canvas.source}
                  />
                ) : canvas.format === 'text' ? (
                  <pre className="cv-text">{canvas.source}</pre>
                ) : (
                  <div
                    className="text md cv-md"
                    dangerouslySetInnerHTML={{
                      __html: renderMarkdown(canvas.source, { resolveMention: mentionName }),
                    }}
                  />
                )}
              </div>
            )}
          </section>
        ) : null}

        <div
          className="dc-messages"
          onContextMenu={(e) => openCtx(e, 'Messages', blankMenu('messages'))}
        >
          {loading ? <p className="muted" style={{ padding: 8 }}>Loading…</p> : null}
          {!loading && !messages.length ? (
            <div className="dc-empty-center">
              {currentChannel ? `This is the start of #${currentChannel.name}` : 'No channel selected'}
            </div>
          ) : null}
          {messages
            .filter((m) => devMode || m.authorUserId !== 'guild')
            .map((m) => (
            <div
              key={m.messageId}
              className={`msg ${m.isDeleted ? 'deleted' : ''}`}
              onContextMenu={(e) => openCtx(e, authorLabel(m.authorUserId), messageMenu(m))}
            >
              <div className="avatar">{initials(authorLabel(m.authorUserId))}</div>
              <div className="body">
                <div className="meta">
                  <span className="author">{authorLabel(m.authorUserId)}</span>
                  <span className="time">{timeOf(m.createdAt)} · #{m.seq}</span>
                  {devMode && m.wakeup !== undefined ? (
                    <span className={`meta-badge ${m.wakeup ? 'on' : ''}`}>wakeup={String(m.wakeup)}</span>
                  ) : null}
                </div>
                <div
                  className="text md"
                  dangerouslySetInnerHTML={{ __html: renderMarkdown(m.content, { resolveMention: mentionName }) }}
                />
                {!m.isDeleted && m.attachments && m.attachments.length ? (
                  <div className="attachments">
                    {m.attachments.map((a, ai) => {
                      const isImg = (a.mimeType ?? '').startsWith('image/')
                      const href = fileUrl(a.url)
                      return isImg ? (
                        <a key={ai} href={href} target="_blank" rel="noopener noreferrer">
                          <img className="att-img" src={href} alt={a.name ?? 'image'} />
                        </a>
                      ) : (
                        <a
                          key={ai}
                          className="att-chip"
                          href={href}
                          target="_blank"
                          rel="noopener noreferrer"
                        >
                          📎 {a.name ?? 'file'}
                        </a>
                      )
                    })}
                  </div>
                ) : null}
                {devMode ? (
                  <pre className="meta-raw">
{JSON.stringify(
  {
    messageId: m.messageId,
    seq: m.seq,
    authorUserId: m.authorUserId,
    createdAt: m.createdAt,
    isDeleted: m.isDeleted ?? false,
    wakeup: m.wakeup,
  },
  null,
  0,
)}
                  </pre>
                ) : null}
              </div>
            </div>
          ))}
        </div>

        {currentChannel?.closed ? (
          <div className="dc-closed-banner">This channel is closed — history is read-only.</div>
        ) : (
          <div className="dc-composer-wrap">
            {pendingFiles.length ? (
              <div className="pending-files">
                {pendingFiles.map((f, fi) => (
                  <span key={fi} className="att-chip">
                    📎 {f.name}
                    <button
                      type="button"
                      className="pf-x"
                      title="Remove"
                      onClick={() => setPendingFiles((prev) => prev.filter((_, j) => j !== fi))}
                    >
                      ×
                    </button>
                  </span>
                ))}
              </div>
            ) : null}
            <form
              className="dc-composer"
              onSubmit={(e) => {
                e.preventDefault()
                void sendMessage()
              }}
            >
              <label
                className="icon-btn attach-btn"
                title="Attach files"
                aria-disabled={!currentChannel}
              >
                📎
                <input
                  type="file"
                  multiple
                  hidden
                  disabled={!currentChannel}
                  onChange={(e) => {
                    const files = Array.from(e.target.files ?? [])
                    if (files.length) setPendingFiles((prev) => [...prev, ...files])
                    e.target.value = ''
                  }}
                />
              </label>
              <input
                className="input"
                value={content}
                onChange={(e) => setContent(e.target.value)}
                placeholder={currentChannel ? `Message #${currentChannel.name}` : 'Select a channel first'}
                disabled={!currentChannel}
              />
              <button
                className="btn"
                type="submit"
                disabled={
                  !currentChannel ||
                  uploading ||
                  (!content.trim() && pendingFiles.length === 0)
                }
              >
                {uploading ? 'Sending…' : 'Send'}
              </button>
            </form>
          </div>
        )}
      </main>

      {showMembers ? (
        <aside
          className="dc-members"
          onContextMenu={(e) => openCtx(e, 'Members', blankMenu('members'))}
        >
          {currentChannel && !currentChannel.isPublic ? (
            <>
              <div className="dc-section-label">
                <span>In channel — {channelMembers.length}</span>
              </div>
              {channelMembers.map((m) => renderMember(m, true))}
              <div className="dc-section-label" style={{ marginTop: 14 }}>
                <span>Guild — {members.length}</span>
              </div>
              {members.map((m) => renderMember(m))}
            </>
          ) : (
            <>
              <div className="dc-section-label">
                <span>Members — {members.length}</span>
              </div>
              {members.map((m) => renderMember(m))}
            </>
          )}
        </aside>
      ) : null}

      {showAddGuildModal ? (
        <div className="modal-backdrop" onClick={() => setShowAddGuildModal(false)}>
          <div className="modal-card" onClick={(e) => e.stopPropagation()}>
            <h3>Add guild</h3>
            <p className="muted" style={{ marginBottom: 10 }}>Enter the guild node id to join.</p>
            <input
              className="input"
              value={joinGuildNodeId}
              onChange={(e) => setJoinGuildNodeId(e.target.value)}
              placeholder="e.g. test-guild1"
            />
            <div className="modal-actions">
              <button className="btn" onClick={addGuild}>Join</button>
              <button className="btn btn-secondary" onClick={() => setShowAddGuildModal(false)}>Cancel</button>
            </div>
          </div>
        </div>
      ) : null}

      {showCreateChannelModal ? (
        <div className="modal-backdrop" onClick={() => setShowCreateChannelModal(false)}>
          <div className="modal-card" onClick={(e) => e.stopPropagation()}>
            <h3>Create channel</h3>
            <input
              className="input"
              value={newChannelName}
              onChange={(e) => setNewChannelName(e.target.value)}
              placeholder="Channel name"
            />
            <div className="field" style={{ marginTop: 10 }}>
              <label>Type</label>
              <select
                className="input"
                value={newChannelXType}
                onChange={(e) => setNewChannelXType(e.target.value as XType)}
              >
                {X_TYPES.map((t) => (
                  <option key={t} value={t}>
                    {t}
                  </option>
                ))}
              </select>
            </div>

            {newChannelXType === 'triage' ? (
              <div className="field" style={{ marginTop: 10 }}>
                <label>On-duty (required)</label>
                <select
                  className="input"
                  value={newChannelOnDuty}
                  onChange={(e) => setNewChannelOnDuty(e.target.value)}
                >
                  {members.map((m) => (
                    <option key={m.userId} value={m.userId}>
                      {(m.name || m.email) + (m.userId === session?.user.id ? ' (you)' : '')}
                    </option>
                  ))}
                </select>
                <p className="muted" style={{ fontSize: 12 }}>
                  Added to the channel automatically and put into wake_mapping.
                </p>
              </div>
            ) : null}

            {newChannelXType === 'custom' ? (
              <div className="field" style={{ marginTop: 10 }}>
                <label>Listeners (optional)</label>
                <div className="modal-list">
                  {members.map((m) => (
                    <label key={m.userId} className="check-row">
                      <input
                        type="checkbox"
                        checked={newChannelListeners.includes(m.userId)}
                        onChange={() => toggleListener(m.userId)}
                      />
                      <span>
                        {m.name || m.email}
                        {m.userId === session?.user.id ? ' (you)' : ''}
                      </span>
                    </label>
                  ))}
                  {members.length === 0 ? <div className="dc-empty">No members in this guild.</div> : null}
                </div>
                <p className="muted" style={{ fontSize: 12 }}>
                  Each selected user gets a wake_mapping record for this channel.
                </p>
              </div>
            ) : null}

            {newChannelXType === 'discuss' || newChannelXType === 'work' ? (
              <div className="field" style={{ marginTop: 10 }}>
                <label>Bypass list (optional)</label>
                <div className="modal-list">
                  {members.map((m) => (
                    <label key={m.userId} className="check-row">
                      <input
                        type="checkbox"
                        checked={newChannelBypass.includes(m.userId)}
                        onChange={() => toggleBypass(m.userId)}
                      />
                      <span>
                        {m.name || m.email}
                        {m.userId === session?.user.id ? ' (you)' : ''}
                      </span>
                    </label>
                  ))}
                  {members.length === 0 ? <div className="dc-empty">No members in this guild.</div> : null}
                </div>
                <p className="muted" style={{ fontSize: 12 }}>
                  Bypass members are excluded from the speaking rotation and are
                  only woken when @-mentioned. Order and bypass partition the
                  channel members.
                </p>
              </div>
            ) : null}

            <label className="check-row" style={{ marginTop: 10 }}>
              <input type="checkbox" checked={newChannelPublic} onChange={(e) => setNewChannelPublic(e.target.checked)} />
              <span>Public — visible to all guild members</span>
            </label>
            <p className="muted" style={{ marginTop: 8, fontSize: 13 }}>
              {newChannelPublic
                ? "You're added automatically; every guild member can see it."
                : "You're added automatically. Pick who else to include:"}
            </p>
            <div className="modal-list">
              {members
                .filter((m) => m.userId !== session?.user.id)
                .map((m) => (
                  <label key={m.userId} className="check-row">
                    <input
                      type="checkbox"
                      checked={selectedMemberIds.includes(m.userId)}
                      disabled={newChannelPublic}
                      onChange={() => toggleMember(m.userId)}
                    />
                    <span>{m.name || m.email}</span>
                  </label>
                ))}
              {members.filter((m) => m.userId !== session?.user.id).length === 0 ? (
                <div className="dc-empty">No other members in this guild.</div>
              ) : null}
            </div>
            <div className="modal-actions">
              <button className="btn" onClick={createChannel}>Create</button>
              <button className="btn btn-secondary" onClick={() => setShowCreateChannelModal(false)}>Cancel</button>
            </div>
          </div>
        </div>
      ) : null}

      {showSettingsModal ? (
        <div className="modal-backdrop" onClick={() => setShowSettingsModal(false)}>
          <div className="modal-card" onClick={(e) => e.stopPropagation()}>
            <h3>Settings</h3>
            <p className="muted" style={{ marginBottom: 12 }}>Signed in as {session?.user.email}</p>
            <div className="field">
              <label>Display name</label>
              <input
                className="input"
                value={settingsName}
                onChange={(e) => setSettingsName(e.target.value)}
                placeholder="Your name"
              />
            </div>
            <label className="check-row" style={{ marginTop: 4 }}>
              <input
                type="checkbox"
                checked={devMode}
                onChange={(e) => toggleDevMode(e.target.checked)}
              />
              <span>Developer mode — show guild /ack &amp; message metadata</span>
            </label>
            <div className="modal-actions">
              <button className="btn" onClick={saveName}>Save</button>
              <button className="btn btn-secondary" onClick={() => setShowSettingsModal(false)}>Cancel</button>
            </div>
          </div>
        </div>
      ) : null}

      {showCanvasModal ? (
        <div className="modal-backdrop" onClick={() => setShowCanvasModal(false)}>
          <div className="modal-card modal-wide" onClick={(e) => e.stopPropagation()}>
            <h3>{canvasEditMode ? 'Edit canvas' : 'Share canvas'}</h3>
            <p className="muted" style={{ marginBottom: 10, fontSize: 13 }}>
              A single pinned document per channel. Only you (the sharer) can
              update it. HTML is rendered in a sandboxed frame.
            </p>
            <input
              className="input"
              value={canvasTitle}
              onChange={(e) => setCanvasTitle(e.target.value)}
              placeholder="Document title"
            />
            <div className="field" style={{ marginTop: 10 }}>
              <label>Format</label>
              <select
                className="input"
                value={canvasFormat}
                onChange={(e) => setCanvasFormat(e.target.value as CanvasFormat)}
              >
                <option value="md">Markdown</option>
                <option value="html">HTML</option>
                <option value="text">Plain text</option>
              </select>
            </div>
            <div className="field" style={{ marginTop: 10 }}>
              <label>Content</label>
              <textarea
                className="input canvas-src"
                value={canvasSource}
                onChange={(e) => setCanvasSource(e.target.value)}
                placeholder="Document source…"
                rows={14}
              />
            </div>
            <div className="modal-actions">
              <button className="btn" onClick={submitCanvas}>
                {canvasEditMode ? 'Save' : 'Share'}
              </button>
              <button className="btn btn-secondary" onClick={() => setShowCanvasModal(false)}>
                Cancel
              </button>
            </div>
          </div>
        </div>
      ) : null}

      {error ? (
        <div className="modal-backdrop" onClick={() => setError('')}>
          <div className="modal-card" onClick={(e) => e.stopPropagation()}>
            <h3>Something went wrong</h3>
            <p className="error-text">{error}</p>
            <div className="modal-actions">
              <button className="btn" onClick={() => setError('')}>OK</button>
            </div>
          </div>
        </div>
      ) : null}

      {renderCtxMenu()}
    </div>
  )
}
