<app>
  <div class="app-container">
    <!-- Auth screens -->
    <template if={!state.user}>
      <login-page if={state.authView === 'login'} cb-login={handleLogin} cb-switch={() => update({ authView: 'register' })} />
      <register-page if={state.authView === 'register'} cb-register={handleLogin} cb-switch={() => update({ authView: 'login' })} />
    </template>

    <!-- Main chat layout -->
    <template if={state.user}>
      <div class="chat-layout">
        <chat-sidebar
          rooms={state.rooms}
          active-room-id={state.activeRoomId}
          user={state.user}
          cb-select-room={selectRoom}
          cb-create-room={() => update({ showCreateModal: true })}
          cb-profile={() => update({ showProfileModal: true })}
          cb-logout={handleLogout}
        />
        <main class="chat-main">
          <chat-room
            if={state.activeRoom}
            room={state.activeRoom}
            messages={state.messages}
            user={state.user}
            ai-typing={state.aiTyping}
            ai-tool-status={state.aiToolStatus}
            typing-users={state.typingUsers}
            streaming-message={state.streamingMessage}
            cb-send={sendMessage}
            cb-invite={() => update({ showInviteModal: true })}
            cb-delete-room={() => update({ showDeleteModal: true })}
            cb-clear-room={() => update({ showClearModal: true })}
          />
          <div if={state.linkError} class="no-room">
            <div class="no-room-content link-error">
              <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
                <circle cx="12" cy="12" r="10"/>
                <line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/>
              </svg>
              <h2>{state.linkError}</h2>
              <p>The message link you followed could not be opened.</p>
              <button class="btn btn-ghost" onclick={() => update({ linkError: null })}>Dismiss</button>
            </div>
          </div>
          <div if={!state.activeRoom && !state.linkError} class="no-room">
            <div class="no-room-content">
              <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
                <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
              </svg>
              <h2>Welcome to GroupChat</h2>
              <p>Select a room or create a new one to start chatting</p>
            </div>
          </div>
        </main>
      </div>

      <create-room-modal
        if={state.showCreateModal}
        cb-create={handleCreateRoom}
        cb-close={() => update({ showCreateModal: false })}
      />

      <invite-modal
        if={state.showInviteModal}
        room-id={state.activeRoomId}
        cb-close={() => update({ showInviteModal: false })}
      />

      <delete-room-modal
        if={state.showDeleteModal}
        room={state.activeRoom}
        cb-delete={handleDeleteRoom}
        cb-close={() => update({ showDeleteModal: false })}
      />

      <clear-confirm-modal
        if={state.showClearModal}
        room={state.activeRoom}
        cb-confirm={confirmClearRoom}
        cb-close={() => update({ showClearModal: false })}
      />

      <profile-page
        if={state.showProfileModal}
        user={state.user}
        cb-profile-update={handleProfileUpdate}
        cb-close={() => update({ showProfileModal: false })}
      />
    </template>
  </div>

  <style>
    .app-container {
      height: 100vh;
      display: flex;
      width: 100%;
    }

    .app-container > :first-child {
      width: 100%;
    }

    .chat-layout {
      display: flex;
      width: 100%;
      height: 100%;
    }

    .chat-main {
      flex: 1;
      display: flex;
      flex-direction: column;
      min-width: 0;
      min-height: 0;
      overflow: hidden;
    }

    /* Riot.js custom element wrappers must fill their flex parent */
    .chat-layout > chat-sidebar {
      display: flex;
      flex-shrink: 0;
    }

    .chat-main > chat-room {
      flex: 1;
      display: flex;
      flex-direction: column;
      min-height: 0;
    }

    .no-room {
      flex: 1;
      display: flex;
      align-items: center;
      justify-content: center;
      background: var(--bg-secondary);
    }

    .no-room-content {
      text-align: center;
      color: var(--text-muted);
    }

    .no-room-content svg {
      margin-bottom: var(--space-md);
      opacity: 0.5;
    }

    .no-room-content h2 {
      color: var(--text-secondary);
      margin-bottom: var(--space-sm);
      font-weight: 500;
    }

    .no-room-content p {
      font-size: var(--text-sm);
    }

    .link-error svg {
      color: var(--error, #e53e3e);
      opacity: 0.8;
    }

    .link-error h2 {
      color: var(--error, #e53e3e);
    }

    .link-error .btn {
      margin-top: var(--space-md);
    }

    :global(.hash-highlight) {
      animation: hash-flash 4s ease;
      border-radius: 8px;
      box-shadow: 0 0 0 2px rgba(108, 92, 231, 0.5), 0 0 12px rgba(108, 92, 231, 0.2);
    }

    @keyframes hash-flash {
      0%, 30% {
        background: rgba(108, 92, 231, 0.15);
        box-shadow: 0 0 0 2px rgba(108, 92, 231, 0.5), 0 0 16px rgba(108, 92, 231, 0.25);
      }
      100% {
        background: transparent;
        box-shadow: none;
      }
    }
  </style>

  <script>
    import { api, saveAuth, getUser, clearAuth, isAuthenticated, setOnUnauthorized } from '../services/api.js'
    import { ws } from '../services/websocket.js'
    import { StreamBuffer } from '../services/stream-buffer.js'

    export default {
      state: {
        // Auth + top-level app view state.
        user: null,
        authView: 'login',

        // Active chat data.
        rooms: [],
        activeRoomId: null,
        activeRoom: null,
        messages: [],

        // Modal visibility.
        showCreateModal: false,
        showInviteModal: false,
        showDeleteModal: false,
        showClearModal: false,
        showProfileModal: false,

        // Live AI / typing state for the currently open room.
        aiTyping: false,
        aiToolStatus: null,
        streamingMessage: null,
        typingUsers: [],

        // Used when a shared message link cannot be opened.
        linkError: null,
      },

      onMounted() {
        // Register global 401 handler so any expired-token API call triggers logout
        setOnUnauthorized(() => this.handleLogout())

        // Listen for hash changes to navigate to linked messages
        this._onHashChange = () => {
          this.navigateToMessageLink()
        }
        window.addEventListener('hashchange', this._onHashChange)

        const user = getUser()
        if (user && isAuthenticated()) {
          // Verify the token is still valid with the server before trusting it
          this.verifyAndInit(user)
        } else {
          // Not logged in — stash permalink and invite for after login
          this.stashPendingLink()
          this.checkPendingInvite()
        }
      },

      async verifyAndInit(cachedUser) {
        try {
          // Ask the server if our stored token is still good
          const freshUser = await api.me()
          // Use the fresh data from the server (display_name etc. may have changed)
          this.update({ user: freshUser })
          this.initChat()
        } catch (err) {
          // Token is expired or invalid — force back to login screen
          console.warn('Stored token is no longer valid, logging out:', err.message)
          clearAuth()
          ws.disconnect()
          this.update({ user: null })
          this.checkPendingInvite()
        }
      },

      onUnmounted() {
        ws.disconnect()
        window.removeEventListener('hashchange', this._onHashChange)
      },

      async initChat() {
        const token = localStorage.getItem('token')
        ws.connect(token)

        // If WebSocket detects auth failure (expired/invalid token), auto-logout
        ws.on('auth_failed', () => {
          console.warn('WebSocket auth failed — logging out')
          this.handleLogout()
        })

        ws.on('new_message', (msg) => {
          if (msg.message.room_id === this.state.activeRoomId) {
            // If we were streaming this message, cancel the buffer and remove placeholder
            const isStreamReplacement = this.state.streamingMessage?.id === msg.message.id
            if (isStreamReplacement && this.streamBuffer) {
              this.streamBuffer.cancel()
              this.streamBuffer = null
              this._streamMsgId = null
              this._streamContent = ''
            }
            this.update({
              messages: [...this.state.messages, msg.message],
              aiTyping: false,
              aiToolStatus: null,
              streamingMessage: isStreamReplacement ? null : this.state.streamingMessage,
            })
            this.scrollToBottom()
          }
        })

        ws.on('ai_stream_chunk', (msg) => {
          if (msg.room_id === this.state.activeRoomId) {
            if (!this.streamBuffer || this._streamMsgId !== msg.message_id) {
              // First chunk for a new message — create buffer + streaming message
              if (this.streamBuffer) this.streamBuffer.cancel()
              this._streamMsgId = msg.message_id
              this._streamContent = ''

              this.streamBuffer = new StreamBuffer(
                // onText: drip chars into the displayed message
                (text) => {
                  this._streamContent += text
                  this.update({
                    streamingMessage: {
                      id: this._streamMsgId,
                      room_id: msg.room_id,
                      sender_id: 'ai-assistant',
                      sender_name: this.state.activeRoom?.ai_name || 'AI Assistant',
                      content: this._streamContent,
                      mentions: [],
                      is_ai: true,
                      streaming: true,
                      avatar_hash: '',
                    },
                  })
                  this.scrollToBottom()
                },
                // onDone: buffer fully drained after stream ended.
                // At this point the UI can remove the "still typing" cursor effect.
                () => {
                  if (this.state.streamingMessage?.id === this._streamMsgId) {
                    this.update({
                      streamingMessage: { ...this.state.streamingMessage, streaming: false },
                    })
                  }
                }
              )

              this.update({ aiTyping: false })
            }
            // Push every chunk into the buffer (it drains smoothly via rAF)
            this.streamBuffer.push(msg.delta)
          }
        })

        ws.on('ai_stream_end', (msg) => {
          if (msg.room_id === this.state.activeRoomId) {
            // Tell buffer to flush remaining text, then signal done
            if (this.streamBuffer && this._streamMsgId === msg.message_id) {
              this.streamBuffer.finish()
            }
          }
        })

        ws.on('ai_typing', (msg) => {
          if (msg.room_id === this.state.activeRoomId) {
            this.update({ aiTyping: true })
          }
        })

        ws.on('ai_tool_usage', (msg) => {
          if (msg.room_id === this.state.activeRoomId) {
            this.update({
              aiToolStatus: { tool: msg.tool_name, status: msg.status },
            })
          }
        })

        ws.on('user_typing', (msg) => {
          if (msg.room_id === this.state.activeRoomId && msg.user_id !== this.state.user.id) {
            // Replace any existing entry for the same user so repeated typing events
            // refresh the timeout instead of duplicating the person in the UI.
            const users = this.state.typingUsers.filter(u => u.user_id !== msg.user_id)
            users.push({ user_id: msg.user_id, display_name: msg.display_name })
            this.update({ typingUsers: users })
            setTimeout(() => {
              this.update({
                typingUsers: this.state.typingUsers.filter(u => u.user_id !== msg.user_id)
              })
            }, 3000)
          }
        })

        ws.on('room_deleted', (msg) => {
          const rooms = this.state.rooms.filter(r => r.id !== msg.room_id)
          const resetActive = this.state.activeRoomId === msg.room_id
          this.update({
            rooms,
            activeRoomId: resetActive ? null : this.state.activeRoomId,
            activeRoom: resetActive ? null : this.state.activeRoom,
            messages: resetActive ? [] : this.state.messages,
          })
        })

        ws.on('room_cleared', (msg) => {
          if (msg.room_id === this.state.activeRoomId) {
            this.update({ messages: [] })
          }
        })

        try {
          const rooms = await api.listRooms()
          this.update({ rooms })
        } catch (e) {
          console.error('Failed to load rooms:', e)
        }

        // Process any pending invite token
        await this.processInviteToken()

        // Check for a message permalink in the URL hash (e.g. #roomId/messageHash)
        this.navigateToMessageLink()
      },

      handleLogin(data) {
        saveAuth(data.token, data.user)
        this.update({ user: data.user })
        this.initChat()
      },

      handleLogout() {
        ws.disconnect()
        clearAuth()
        this.update({
          user: null,
          rooms: [],
          activeRoomId: null,
          activeRoom: null,
          messages: [],
        })
      },

      async selectRoom(roomId) {
        // Cancel any active stream buffer when switching rooms
        if (this.streamBuffer) {
          this.streamBuffer.cancel()
          this.streamBuffer = null
          this._streamMsgId = null
          this._streamContent = ''
        }
        const [room, messages] = await Promise.all([
          api.getRoom(roomId),
          api.getMessages(roomId),
        ])
        this.update({
          activeRoomId: roomId,
          activeRoom: room,
          messages,
          aiTyping: false,
          aiToolStatus: null,
          streamingMessage: null,
          typingUsers: [],
          linkError: null,
        })
        ws.joinRoom(roomId)
        // Joining after room data loads avoids subscribing the UI to a room that failed to open.
        // Only scroll to bottom if not navigating to a specific message
        if (!this._pendingScrollHash) {
          this.scrollToBottom()
        }
      },

      async handleCreateRoom(data) {
        try {
          const room = await api.createRoom(data)
          const rooms = [room, ...this.state.rooms]
          this.update({ rooms, showCreateModal: false })
          this.selectRoom(room.id)
        } catch (e) {
          console.error('Failed to create room:', e)
        }
      },

      sendMessage({ content, mentions, imageUrl }) {
        ws.sendMessage(this.state.activeRoomId, content, mentions, imageUrl)
      },

      handleDeleteRoom(roomId) {
        const rooms = this.state.rooms.filter(r => r.id !== roomId)
        const resetActive = this.state.activeRoomId === roomId
        this.update({
          rooms,
          showDeleteModal: false,
          activeRoomId: resetActive ? null : this.state.activeRoomId,
          activeRoom: resetActive ? null : this.state.activeRoom,
          messages: resetActive ? [] : this.state.messages,
        })
      },

      confirmClearRoom() {
        this.update({ messages: [], showClearModal: false })
      },

      handleProfileUpdate(user) {
        this.update({ user })
      },

      /** Stash a message permalink hash so it survives the login flow */
      stashPendingLink() {
        const fragment = window.location.hash?.slice(1)
        if (fragment) {
          sessionStorage.setItem('pendingMessageLink', fragment)
        }
      },

      /** Check URL for /invite/:token and stash it for after login if needed */
      checkPendingInvite() {
        const match = window.location.pathname.match(/^\/invite\/(.+)$/)
        if (match) {
          sessionStorage.setItem('pendingInvite', match[1])
          // Clean URL so it doesn't confuse things
          window.history.replaceState(null, '', '/')
        }
      },

      /** Accept an invite token — called after login + rooms load */
      async processInviteToken() {
        // Check URL first, then sessionStorage (for post-login flow)
        let token = null
        const urlMatch = window.location.pathname.match(/^\/invite\/(.+)$/)
        if (urlMatch) {
          token = urlMatch[1]
          window.history.replaceState(null, '', '/')
        } else {
          token = sessionStorage.getItem('pendingInvite')
        }

        if (!token) return
        sessionStorage.removeItem('pendingInvite')

        try {
          const result = await api.acceptInvite(token)
          // Refresh room list — the accepted room should now appear
          const rooms = await api.listRooms()
          this.update({ rooms })
          // Auto-select the room the user just joined
          if (result?.room_id) {
            this.selectRoom(result.room_id)
          }
        } catch (e) {
          console.error('Failed to accept invite:', e)
        }
      },

      scrollToBottom() {
        requestAnimationFrame(() => {
          // Query the DOM after the frame so Riot has already rendered the latest message list.
          const container = document.querySelector('.messages-list')
          if (container) {
            container.scrollTop = container.scrollHeight
          }
        })
      },

      /** Parse #roomId/messageHash or #messageHash from URL or sessionStorage, load the room, and scroll */
      async navigateToMessageLink() {
        // Try URL hash first, then sessionStorage (for post-login flow)
        let fragment = window.location.hash?.slice(1)
        if (!fragment) {
          fragment = sessionStorage.getItem('pendingMessageLink')
          sessionStorage.removeItem('pendingMessageLink')
        }
        if (!fragment) return

        let roomId, msgHash

        if (fragment.includes('/')) {
          // New format: #roomId/messageHash
          const slashIdx = fragment.indexOf('/')
          roomId = fragment.slice(0, slashIdx)
          msgHash = fragment.slice(slashIdx + 1)
        } else {
          // Old format: #messageHash — resolve room via API
          msgHash = fragment
          try {
            const result = await api.resolveMessageHash(msgHash)
            roomId = result.room_id
          } catch (e) {
            const status = e?.status
            if (status === 404) {
              this.update({ linkError: 'Message not found or you don\'t have access' })
            } else {
              this.update({ linkError: 'Could not find this message' })
            }
            window.history.replaceState(null, '', window.location.pathname)
            return
          }
        }

        if (!roomId || !msgHash) return

        // Store the target hash so selectRoom skips scrollToBottom
        this._pendingScrollHash = msgHash

        // Load the room if not already active
        if (this.state.activeRoomId !== roomId) {
          try {
            await this.selectRoom(roomId)
          } catch (e) {
            this._pendingScrollHash = null
            const status = e?.status
            if (status === 403) {
              this.update({ linkError: 'You don\'t have access to this room' })
            } else if (status === 404) {
              this.update({ linkError: 'Room not found' })
            } else {
              this.update({ linkError: 'Could not load this room' })
            }
            window.history.replaceState(null, '', window.location.pathname)
            return
          }
        }

        // Wait for DOM to render the messages, then scroll
        this._pendingScrollHash = null
        await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))
        this.scrollToHash(msgHash)

        // Clean the hash from URL after navigating
        window.history.replaceState(null, '', window.location.pathname)
      },

      scrollToHash(hash) {
        const el = document.getElementById('msg-' + hash)
        if (el) {
          el.scrollIntoView({ behavior: 'smooth', block: 'center' })
          el.classList.add('hash-highlight')
          setTimeout(() => el.classList.remove('hash-highlight'), 4000)
        }
      },
    }
  </script>
</app>
