﻿<script setup>
import {
  LoginStore,
  MessageInputStore,
  MessageListStore,
  MessageStatus,
  MessageType
} from 'tuikit-atomicx-vue3/chat'
import { formatTrtcChatError } from '~/utils/trtc-chat/error-map'

const props = defineProps({
  context: {
    type: Object,
    default: () => ({})
  }
})

const emit = defineEmits(['close'])

const config = useRuntimeConfig()
const route = useRoute()

const connectionState = ref('connecting')
const connectionError = ref('')
const messageList = ref([])
const hasOlderMessages = ref(false)
const loadingInitial = ref(false)
const loadingOlder = ref(false)
const sending = ref(false)
const errorMsg = ref('')
const text = ref('')
const newMessageCount = ref(0)
const isNearBottom = ref(true)
const didInitialScroll = ref(false)

const textareaEl = ref(null)
const scrollContainer = ref(null)
const imageInputEl = ref(null)

const targetType = computed(() =>
  String(config.public.trtcChatTargetType || 'C2C').toUpperCase() === 'GROUP' ? 'GROUP' : 'C2C'
)
const targetID = computed(() => config.public.trtcChatCustomerServiceID || 'administrator')
const conversationID = computed(() => `${targetType.value}${targetID.value}`)

let messageListStore = null
let messageInputStore = null
let loginStore = null
let unsubscribeLoginEvent = null
let unsubscribeMessageEvent = null
let unsubscribeMessageState = null

const filteredMessages = computed(() =>
  (messageList.value || []).filter((message) => message.status !== MessageStatus.Deleted)
)

watch(text, (value) => {
  if (value && errorMsg.value) errorMsg.value = ''
})

function createLoginStore() {
  if (typeof LoginStore.create === 'function') return LoginStore.create()
  return LoginStore()
}

function syncMessageState() {
  messageList.value = messageListStore?.messageList?.value || []
  hasOlderMessages.value = Boolean(messageListStore?.hasOlderMessages?.value)
}

async function resolveCredentials(userID) {
  const endpoint = config.public.trtcChatTokenEndpoint
  if (endpoint) {
    const response = await $fetch(endpoint, {
      method: 'GET',
      query: { userID, sourcePage: route.fullPath }
    })
    const sdkAppID = Number(response?.SDKAppID || response?.sdkAppID)
    if (!sdkAppID || !response?.userSig) {
      throw new Error('UserSig 接口需要返回 SDKAppID 和 userSig。')
    }
    return { sdkAppID, userSig: response.userSig }
  }

  throw new Error('请先配置 NUXT_PUBLIC_TRTC_CHAT_TOKEN_ENDPOINT，用后端接口签发 UserSig。')
}

function getVisitorID() {
  const key = 'official-consultation-user-id'
  const existing = localStorage.getItem(key)
  if (existing) return existing

  const prefix = config.public.trtcChatVisitorPrefix || 'official_visitor'
  const id = `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
  localStorage.setItem(key, id)
  return id
}

async function loginAndPrepareConversation() {
  connectionState.value = 'connecting'
  connectionError.value = ''
  loadingInitial.value = true

  cleanupStores()

  try {
    const userID = getVisitorID()
    const { sdkAppID, userSig } = await resolveCredentials(userID)
    loginStore = createLoginStore()
    await loginStore.login({
      sdkAppID,
      userID,
      userSig,
      scene: config.public.trtcChatScene || '5000'
    })

    messageListStore = MessageListStore.create(conversationID.value)
    messageInputStore = MessageInputStore.create(conversationID.value)

    syncMessageState()
    unsubscribeMessageState = watch(
      [messageListStore.messageList, messageListStore.hasOlderMessages],
      syncMessageState
    )
    unsubscribeMessageEvent = messageListStore.onEvent((event) => {
      switch (event.type) {
        case 'onReceiveNewMessage':
          handleNewMessage(event.message)
          break
        default:
          break
      }
    })
    unsubscribeLoginEvent = loginStore.onEvent((event) => {
      if (event.type === 'kickedOffline') {
        connectionState.value = 'error'
        connectionError.value = '您的账号在其他设备登录，当前咨询已断开。'
      }
    })

    await messageListStore.loadMessages()
    syncMessageState()
    connectionState.value = 'connected'
    await scrollToBottom('auto')
    didInitialScroll.value = true
  } catch (error) {
    connectionState.value = 'error'
    connectionError.value = formatTrtcChatError(error)
  } finally {
    loadingInitial.value = false
  }
}

function cleanupStores() {
  unsubscribeLoginEvent?.()
  unsubscribeMessageEvent?.()
  unsubscribeMessageState?.()
  unsubscribeLoginEvent = null
  unsubscribeMessageEvent = null
  unsubscribeMessageState = null
  messageInputStore?.destroy?.()
  messageListStore?.destroy?.()
  loginStore?.logout?.().catch(() => {})
  messageInputStore = null
  messageListStore = null
  loginStore = null
}

async function scrollToBottom(behavior = 'auto') {
  await nextTick()
  const element = scrollContainer.value
  if (!element) return
  if (behavior === 'smooth') {
    element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' })
  } else {
    element.scrollTop = element.scrollHeight
  }
  checkNearBottom()
}

function checkNearBottom() {
  const element = scrollContainer.value
  if (!element) return
  isNearBottom.value = element.scrollHeight - element.scrollTop - element.clientHeight < 150
}

function handleScroll() {
  checkNearBottom()
  if (isNearBottom.value) newMessageCount.value = 0
}

function handleNewMessage(message) {
  if (message?.isSentBySelf || isNearBottom.value) {
    scrollToBottom('smooth')
    return
  }
  newMessageCount.value += 1
}

watch(messageList, (next, previous) => {
  if (!didInitialScroll.value || loadingOlder.value) return
  if ((next?.length || 0) <= (previous?.length || 0)) return
  const last = next[next.length - 1]
  if (last?.isSentBySelf) scrollToBottom('smooth')
})

async function loadOlderMessages() {
  if (!messageListStore || loadingOlder.value || !hasOlderMessages.value) return
  const element = scrollContainer.value
  const oldScrollHeight = element?.scrollHeight || 0
  loadingOlder.value = true
  try {
    await messageListStore.loadOlderMessages()
    syncMessageState()
    await nextTick()
    if (element) element.scrollTop = element.scrollHeight - oldScrollHeight
  } catch (error) {
    errorMsg.value = formatTrtcChatError(error)
  } finally {
    loadingOlder.value = false
  }
}

async function sendTextMessage() {
  const content = text.value.trim()
  if (!content || sending.value || !messageInputStore) return

  sending.value = true
  errorMsg.value = ''
  try {
    await messageInputStore.sendMessage({ type: 'textMessage', text: content })
    text.value = ''
    await scrollToBottom('smooth')
  } catch (error) {
    errorMsg.value = formatTrtcChatError(error)
  } finally {
    sending.value = false
    await nextTick()
    textareaEl.value?.focus()
  }
}

function onKeydown(event) {
  if (event.key === 'Enter' && !event.shiftKey) {
    event.preventDefault()
    sendTextMessage()
  }
}

function chooseImage() {
  if (sending.value) return
  imageInputEl.value?.click()
}

async function onImageSelected(event) {
  const input = event.target
  const file = input.files?.[0]
  input.value = ''
  if (!file || !messageInputStore) return
  if (!file.type.startsWith('image/')) {
    errorMsg.value = '请选择图片文件。'
    return
  }
  if (file.size > 20 * 1024 * 1024) {
    errorMsg.value = '图片不能超过 20MB。'
    return
  }

  sending.value = true
  errorMsg.value = ''
  try {
    await messageInputStore.sendMessage({ type: 'imageMessage', file })
    await scrollToBottom('smooth')
  } catch (error) {
    errorMsg.value = formatTrtcChatError(error)
  } finally {
    sending.value = false
  }
}

function displayName(message) {
  const from = message.from || {}
  return from.friendRemark || from.nameCard || from.nickname || from.userID || '访客'
}

function avatarLetter(message) {
  return [...displayName(message)][0]?.toUpperCase() || '客'
}

function avatarStyle(message) {
  const palette = ['#1478fb', '#2f8f9d', '#5964d8', '#9465cf']
  const id = message.from?.userID || displayName(message)
  const index = [...id].reduce((total, char) => total + char.charCodeAt(0), 0) % palette.length
  return { background: palette[index] }
}

function shouldShowTimeLabel(current, previous) {
  if (!current?.timestamp) return false
  if (!previous?.timestamp) return true
  return Math.abs(current.timestamp.getTime() - previous.timestamp.getTime()) > 5 * 60 * 1000
}

function formatTimeLabel(date) {
  const now = new Date()
  const sameDay = now.toDateString() === date.toDateString()
  const time = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
  return sameDay ? time : `${date.getMonth() + 1}/${date.getDate()} ${time}`
}

function imageSource(payload = {}) {
  return payload.thumbImageURL || payload.largeImageURL || payload.originalImageURL || ''
}

function formatFileSize(size = 0) {
  if (size < 1024) return `${size}B`
  if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)}KB`
  return `${(size / 1024 / 1024).toFixed(1)}MB`
}

function customText(payload = {}) {
  if (!payload.customData) return payload.description || '[自定义消息]'
  if (typeof payload.customData !== 'string') return payload.description || '[自定义消息]'
  try {
    const parsed = JSON.parse(payload.customData)
    return parsed.title || parsed.text || payload.description || '[自定义消息]'
  } catch {
    return payload.description || payload.customData
  }
}

onMounted(() => {
  loginAndPrepareConversation()
})

onBeforeUnmount(() => {
  cleanupStores()
})
</script>

<template>
  <section class="official-chat" role="dialog" aria-modal="true" aria-labelledby="official-chat-title">
    <header class="official-chat-header">
      <div>
        <p>{{ props.context.source || '官网在线咨询' }}</p>
        <h2 id="official-chat-title">在线客服</h2>
      </div>
      <button class="chat-icon-button" type="button" aria-label="关闭聊天窗口" @click="emit('close')">
        <AppIcon name="close" />
      </button>
    </header>

    <div v-if="connectionState === 'connecting'" class="chat-state-panel">
      <span class="chat-spinner" aria-hidden="true"></span>
      <strong>正在连接客服...</strong>
      <p>我们会为你打开一个专属咨询会话。</p>
    </div>

    <div v-else-if="connectionState === 'error'" class="chat-state-panel is-error">
      <strong>暂时无法连接</strong>
      <p>{{ connectionError }}</p>
      <div class="chat-state-actions">
        <button type="button" @click="loginAndPrepareConversation">重试</button>
        <button type="button" class="is-ghost" @click="emit('close')">返回</button>
      </div>
    </div>

    <template v-else>
      <div ref="scrollContainer" class="chat-message-list" @scroll.passive="handleScroll">
        <div v-if="loadingInitial" class="chat-inline-state">正在加载消息...</div>
        <button
          v-else-if="hasOlderMessages"
          class="load-older-button"
          type="button"
          :disabled="loadingOlder"
          @click="loadOlderMessages"
        >
          {{ loadingOlder ? '加载中...' : '加载更早消息' }}
        </button>

        <p v-if="!loadingInitial && filteredMessages.length === 0" class="chat-empty">
          你好，这里是大马棒在线客服。请直接输入你的问题，我们会尽快回复。
        </p>

        <template v-for="(message, index) in filteredMessages" :key="message.msgID || message.id">
          <div v-if="shouldShowTimeLabel(message, filteredMessages[index - 1])" class="time-label">
            {{ formatTimeLabel(message.timestamp) }}
          </div>

          <div v-if="message.status === MessageStatus.Recalled" class="system-tip">
            {{ message.isSentBySelf ? '你撤回了一条消息' : `${displayName(message)} 撤回了一条消息` }}
          </div>

          <div v-else-if="message.messageType === MessageType.Tips" class="system-tip">
            系统通知
          </div>

          <div
            v-else-if="message.messageType === MessageType.Custom"
            class="message-row custom-row"
            :class="{ 'is-self': message.isSentBySelf }"
          >
            <article class="custom-card">{{ customText(message.messagePayload) }}</article>
          </div>

          <div v-else class="message-row" :class="{ 'is-self': message.isSentBySelf }">
            <div v-if="!message.isSentBySelf" class="message-avatar" :style="avatarStyle(message)">
              {{ avatarLetter(message) }}
            </div>
            <div class="message-main">
              <span v-if="!message.isSentBySelf" class="message-name">{{ displayName(message) }}</span>
              <div class="message-bubble">
                <p v-if="message.messageType === MessageType.Text" class="message-text">
                  {{ message.messagePayload.text }}
                </p>
                <a
                  v-else-if="message.messageType === MessageType.Image"
                  class="message-image-link"
                  :href="message.messagePayload.originalImageURL || imageSource(message.messagePayload)"
                  target="_blank"
                  rel="noreferrer"
                >
                  <img :src="imageSource(message.messagePayload)" alt="图片消息" />
                </a>
                <video
                  v-else-if="message.messageType === MessageType.Video"
                  class="message-video"
                  :src="message.messagePayload.videoURL"
                  :poster="message.messagePayload.videoSnapshotURL"
                  controls
                ></video>
                <audio
                  v-else-if="message.messageType === MessageType.Audio"
                  class="message-audio"
                  :src="message.messagePayload.audioURL"
                  controls
                ></audio>
                <a
                  v-else-if="message.messageType === MessageType.File"
                  class="message-file"
                  :href="message.messagePayload.fileURL"
                  :download="message.messagePayload.fileName"
                >
                  <strong>{{ message.messagePayload.fileName || '文件消息' }}</strong>
                  <span>{{ formatFileSize(message.messagePayload.fileSize) }}</span>
                </a>
                <span v-else-if="message.messageType === MessageType.Face">
                  [表情：{{ message.messagePayload.faceIndex }}]
                </span>
                <div v-else-if="message.messageType === MessageType.Merged">
                  {{ message.messagePayload.title }}
                </div>
                <div v-else-if="message.messageType === MessageType.Stream">
                  {{ message.messagePayload.markdown }}
                </div>
                <span v-else>[Unsupported: {{ message.messageType }}]</span>
              </div>
              <small v-if="message.isSentBySelf && message.status === MessageStatus.Sending">发送中</small>
              <small v-else-if="message.isSentBySelf && message.status === MessageStatus.SendFail">发送失败</small>
            </div>
          </div>
        </template>
      </div>

      <button
        v-if="newMessageCount > 0 && !isNearBottom"
        class="new-message-badge"
        type="button"
        @click="scrollToBottom('smooth')"
      >
        {{ newMessageCount }} 条新消息
      </button>

      <form class="chat-input-area" @submit.prevent="sendTextMessage">
        <p v-if="errorMsg" class="input-error">{{ errorMsg }}</p>
        <textarea
          ref="textareaEl"
          v-model="text"
          :disabled="sending"
          rows="2"
          placeholder="输入咨询内容，Enter 发送，Shift + Enter 换行"
          @keydown="onKeydown"
        ></textarea>
        <div class="chat-toolbar">
          <button
            class="toolbar-button"
            type="button"
            :disabled="sending"
            title="发送图片"
            aria-label="发送图片"
            @click="chooseImage"
          >
            <AppIcon name="picture" />
          </button>
          <span class="toolbar-hint">支持文本和图片消息</span>
          <button class="send-button" type="submit" :disabled="sending || !text.trim()">
            {{ sending ? '发送中' : '发送' }}
            <AppIcon name="promotion" />
          </button>
        </div>
        <input ref="imageInputEl" type="file" accept="image/*" hidden @change="onImageSelected" />
      </form>
    </template>
  </section>
</template>

<style scoped>
.official-chat {
  width: min(420px, calc(100vw - 32px));
  height: min(640px, calc(100dvh - 32px));
  display: flex;
  overflow: hidden;
  flex-direction: column;
  border: 1px solid rgba(215, 233, 255, 0.9);
  border-radius: 12px;
  background: #fff;
  box-shadow: 0 24px 64px rgba(37, 94, 160, 0.18);
}
.official-chat-header {
  min-height: 72px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px;
  color: #fff;
  background: var(--gradient-action);
}
.official-chat-header p,
.official-chat-header h2 {
  margin: 0;
}
.official-chat-header p {
  font-size: 12px;
  opacity: 0.86;
}
.official-chat-header h2 {
  margin-top: 4px;
  font-size: 18px;
  font-weight: 600;
}
.chat-icon-button,
.toolbar-button,
.send-button,
.load-older-button,
.chat-state-actions button,
.new-message-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  transition:
    transform 0.2s,
    opacity 0.2s,
    background 0.2s,
    box-shadow 0.2s;
}
.chat-icon-button {
  width: 36px;
  height: 36px;
  border-radius: 8px;
  color: #fff;
  background: rgba(255, 255, 255, 0.16);
}
.chat-icon-button:hover,
.toolbar-button:hover,
.send-button:hover,
.chat-state-actions button:hover,
.new-message-badge:hover {
  transform: translateY(-1px);
}
.chat-state-panel,
.chat-inline-state,
.chat-empty {
  display: grid;
  place-items: center;
  align-content: center;
  gap: 12px;
  min-height: 0;
  flex: 1;
  padding: 32px;
  color: #52657d;
  text-align: center;
}
.chat-state-panel strong {
  color: #2f4056;
  font-size: 18px;
  font-weight: 600;
}
.chat-state-panel p,
.chat-empty {
  margin: 0;
  font-size: 14px;
  line-height: 1.6;
}
.chat-spinner {
  width: 32px;
  height: 32px;
  border: 3px solid #d7e9ff;
  border-top-color: var(--color-brand-primary);
  border-radius: 999px;
  animation: chat-spin 0.8s linear infinite;
}
.chat-state-actions {
  display: flex;
  gap: 12px;
}
.chat-state-actions button {
  height: 40px;
  padding: 0 16px;
  border-radius: 8px;
  color: #fff;
  background: var(--color-brand-primary);
}
.chat-state-actions .is-ghost {
  color: #41536a;
  background: #edf8ff;
}
.chat-message-list {
  position: relative;
  min-height: 0;
  flex: 1;
  overflow-x: hidden;
  overflow-y: auto;
  padding: 16px;
  background: linear-gradient(180deg, #f9fbff, #edf8ff);
}
.chat-message-list::-webkit-scrollbar {
  width: 6px;
}
.chat-message-list::-webkit-scrollbar-track {
  background: transparent;
}
.chat-message-list::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: #cbd5e1;
}
.load-older-button {
  height: 32px;
  margin: 0 auto 16px;
  padding: 0 16px;
  border-radius: 999px;
  color: #41536a;
  background: #fff;
  box-shadow: var(--shadow-card);
  font-size: 12px;
}
.time-label,
.system-tip {
  width: fit-content;
  max-width: 80%;
  margin: 16px auto;
  padding: 4px 8px;
  border-radius: 999px;
  color: #667a91;
  background: rgba(255, 255, 255, 0.76);
  font-size: 12px;
}
.message-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-top: 12px;
}
.message-row.is-self {
  justify-content: flex-end;
}
.message-avatar {
  width: 32px;
  height: 32px;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  border: 2px solid #fff;
  border-radius: 999px;
  color: #fff;
  box-shadow: 0 2px 8px rgba(37, 94, 160, 0.12);
  font-size: 12px;
  font-weight: 600;
}
.message-main {
  max-width: 72%;
  display: grid;
  gap: 4px;
}
.message-row.is-self .message-main {
  justify-items: end;
}
.message-name {
  color: #667a91;
  font-size: 12px;
}
.message-bubble {
  max-width: 100%;
  padding: 10px 12px;
  overflow-wrap: break-word;
  border: 1px solid rgba(215, 233, 255, 0.9);
  border-radius: 4px 12px 12px 12px;
  color: #2f4056;
  background: #fff;
  box-shadow: 0 1px 6px rgba(37, 94, 160, 0.06);
  font-size: 14px;
  line-height: 1.55;
}
.message-row.is-self .message-bubble {
  border-color: transparent;
  border-radius: 12px 4px 12px 12px;
  color: #fff;
  background: var(--color-brand-primary);
}
.message-text {
  margin: 0;
  white-space: pre-wrap;
}
.message-image-link,
.message-video,
.message-audio {
  display: block;
  max-width: 220px;
}
.message-image-link img,
.message-video {
  max-width: 100%;
  border-radius: 8px;
}
.message-file {
  display: grid;
  gap: 4px;
  color: inherit;
  text-decoration: none;
}
.message-file span,
.message-main small {
  font-size: 12px;
  opacity: 0.76;
}
.custom-row.is-self {
  justify-content: flex-end;
}
.custom-card {
  max-width: 72%;
  padding: 12px;
  border: 1px solid rgba(215, 233, 255, 0.9);
  border-radius: 8px;
  color: #2f4056;
  background: #fff;
  box-shadow: var(--shadow-card);
  font-size: 14px;
}
.new-message-badge {
  position: absolute;
  right: 50%;
  bottom: 104px;
  z-index: 2;
  height: 32px;
  padding: 0 12px;
  border-radius: 999px;
  color: #fff;
  background: var(--color-brand-primary);
  box-shadow: var(--shadow-button);
  font-size: 12px;
  transform: translateX(50%);
}
.chat-input-area {
  display: grid;
  gap: 8px;
  padding: 12px;
  border-top: 1px solid rgba(215, 233, 255, 0.9);
  background: #fff;
}
.input-error {
  margin: 0;
  color: #d54b55;
  font-size: 12px;
  line-height: 1.45;
}
.chat-input-area textarea {
  width: 100%;
  min-height: 72px;
  max-height: 120px;
  resize: vertical;
  border: 1px solid #dbe7f5;
  border-radius: 8px;
  outline: 0;
  padding: 10px 12px;
  color: #2f4056;
  background: #f9fbff;
  font: inherit;
  font-size: 14px;
  line-height: 1.55;
  transition:
    border-color 0.2s,
    box-shadow 0.2s,
    background 0.2s;
}
.chat-input-area textarea:focus {
  border-color: #66a8fa;
  background: #fff;
  box-shadow: 0 0 0 4px rgba(43, 128, 239, 0.1);
}
.chat-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
}
.toolbar-button {
  width: 36px;
  height: 36px;
  border-radius: 999px;
  color: #2379e8;
  background: #edf8ff;
}
.toolbar-button:disabled,
.send-button:disabled,
.load-older-button:disabled {
  cursor: not-allowed;
  opacity: 0.58;
  transform: none;
}
.toolbar-hint {
  flex: 1;
  color: #667a91;
  font-size: 12px;
}
.send-button {
  height: 36px;
  gap: 6px;
  padding: 0 14px;
  border-radius: 999px;
  color: #fff;
  background: var(--color-brand-primary);
  box-shadow: var(--shadow-button);
  font-size: 14px;
  font-weight: 600;
}
@keyframes chat-spin {
  to {
    transform: rotate(360deg);
  }
}
@media (max-width: 640px) {
  .official-chat {
    width: 100vw;
    height: min(720px, calc(100dvh - 24px));
    border-radius: 12px 12px 0 0;
  }
  .message-main,
  .custom-card {
    max-width: 78%;
  }
}
</style>

