<script setup lang="ts">
import { computed, inject, onMounted, onUnmounted, ref, watch } from 'vue'
import { Emitter } from 'mitt'
import { useI18n } from 'vue-i18n'
import { nanoid } from 'nanoid'
import { useRoute } from 'vue-router'
import { CloseSmall } from '@icon-park/vue-next'
import { fetchCustomEventSource } from '@/composables/useEventSource'
import { useUserStore } from '@/store/modules/user'
import { UploadStatus } from '@/enums/upload-status'
import { useDialogueFile } from '@/composables/useDialogueFile'
import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { TEXTTOSPEECH_WS_URL } from '@/config/base-url'
import WebSocketCtr from '@/utils/web-socket-ctr'
import { ChannelType } from '@/enums/channel'
import { showDialog } from 'vant'

interface Props {
  agentId: string
  dialogsId: string
  messageList: Map<string, ConversationMessageItem>
  continuousQuestionStatus: 'default' | 'close'
  isEnableDocumentParse: boolean
  isEnableVoice: boolean
  answerAudioAutoPlay: boolean
  answerAudioPlaying: boolean
  timbreId: string
}

const { t } = useI18n()

const { query } = useRoute()

const props = defineProps<Props>()

const emit = defineEmits<{
  addMessageItem: [messageId: string, value: ConversationMessageItem]
  updateSpecifyMessageItem: [messageId: string, newObj: Partial<ConversationMessageItem>]
  deleteMessageItem: [messageId: string]
  updatePageScroll: []
  clearAllMessage: []
  toLogin: []
  createContinueQuestions: [value: string]
  resetContinueQuestionList: []
  audioPlay: [messageItem: ConversationMessageItem, requestId?: string]
  audioPause: []
}>()

const { isMobile } = useLayoutConfig()

const userStore = useUserStore()

const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()

const isAnswerResponseWait = defineModel<boolean>('isAnswerResponseLoading', { required: true })
const isAnswerResponseInterrupt = defineModel<boolean>('isAnswerResponseInterrupt', { required: true })

const emitter = inject<Emitter<MittEvents>>('emitter')

const inputMessageContent = ref('')
const currentReplyContentSentenceExtractIndex = ref(0)
const sentenceFragmentSerialNo = ref(0)
const sentenceExtractCheckEnabled = ref(false)
const assistantFullAnswerContent = ref('')
const sentenceSpeechException = ref(false)
const messageAudioLoading = ref(false)
const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>())

let controller: AbortController | null = null

const isLogin = computed(() => {
  return userStore.isLogin
})

const isAllowClearMessage = computed(() => {
  return props.messageList.size > 0
})

const isSendBtnDisabled = computed(() => {
  return !inputMessageContent.value.trim()
})

const inputPlaceholder = computed(() => {
  return isLogin.value ? t('common_module.dialogue_module.question_input_placeholder') : ''
})

const isCreateContinueQuestions = computed(() => {
  return props.continuousQuestionStatus === 'default'
})

const isInputMessageDisabled = computed(() => {
  return (
    uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED) ||
    uploadImageList.value.some((imageItem) => imageItem.status !== UploadStatus.FINISHED)
  )
})

const uploadDocumentList = computed(() => {
  return uploadFileList.value.filter((fileItem) => fileItem.format === 'document')
})

const uploadImageList = computed(() => {
  return uploadFileList.value.filter((fileItem) => fileItem.format === 'image')
})

const isUploadFileDisabled = computed(() => {
  return uploadFileList.value.length === 1
})

watch(
  () => uploadImageList.value.length,
  () => {
    emit('updatePageScroll')
  },
)

const uploadFileIcon = (type: string) => {
  return `https://gsst-poe-sit.gz.bcebos.com/icon/${type}.svg`
}

onMounted(() => {
  emitter?.on('selectQuestion', (featuredQuestion) => {
    if (!isLogin.value) {
      window.$message.warning(t('common_module.not_login_text'))
      return
    }

    inputMessageContent.value = featuredQuestion
    handleMessageSend()
  })
})

onUnmounted(() => {
  blockMessageResponse()
  emitter?.off('selectQuestion')
})

function messageItemFactory(): ConversationMessageItem {
  return {
    timestamp: Date.now(),
    role: 'user',
    textContent: '',
    isEmptyContent: false,
    isTextContentLoading: false,
    isAnswerResponseLoading: false,
    isVoiceLoading: false,
    isVoicePlaying: false,
    voiceFragmentUrlList: [],
    imageUrl: '',
    reasoningContent: '',
    knowledgeContentResult: [],
    dbChainSQLContent: '',
    pluginResult: {
      displayFormat: 'none',
      pluginName: '',
      arguments: '',
      pluginContent: '',
    },
  }
}

function handleInputMessageEnter(event: KeyboardEvent) {
  if (event.key === 'Enter' && !event.shiftKey) {
    event.preventDefault()
    handleMessageSend()
  }
}

function handleMessageSend(lastQuestionContent?: string) {
  if (!isLogin.value) {
    window.$message.warning(t('common_module.not_login_text'))
    return
  }

  if (lastQuestionContent) {
    inputMessageContent.value = lastQuestionContent
  }

  if (!inputMessageContent.value.trim() || isInputMessageDisabled.value) {
    return
  }

  if (isAnswerResponseWait.value || messageAudioLoading.value) {
    window.$message.warning(t('common_module.dialogue_module.do_not_operate_until_the_reply_is_complete'))
    return
  }

  if (props.answerAudioPlaying) {
    window.$message.warning(t('common_module.dialogue_module.stop_playing_and_then_operate'))
    return
  }

  const latestUserMessageKey = nanoid()
  const latestAssistantMessageKey = nanoid()
  currentLatestMessageItemKeyMap.value.set('user', latestUserMessageKey)
  currentLatestMessageItemKeyMap.value.set('assistant', latestAssistantMessageKey)

  emit('resetContinueQuestionList')
  emit('addMessageItem', latestUserMessageKey, {
    ...messageItemFactory(),
    textContent: inputMessageContent.value,
    imageUrl: uploadImageList.value?.[0]?.url || '',
  })
  emit('updatePageScroll')

  emit('addMessageItem', latestAssistantMessageKey, {
    ...messageItemFactory(),
    role: 'assistant',
    isTextContentLoading: true,
    isAnswerResponseLoading: true,
    isVoiceLoading: true,
  })
  emit('updatePageScroll')

  let replyTextContent = ''
  let reasoningContent = ''
  isAnswerResponseWait.value = true
  isAnswerResponseInterrupt.value = false
  currentReplyContentSentenceExtractIndex.value = 0
  sentenceFragmentSerialNo.value = 0
  sentenceExtractCheckEnabled.value = false
  assistantFullAnswerContent.value = ''
  sentenceSpeechException.value = false
  messageAudioLoading.value = false

  controller = new AbortController()

  fetchCustomEventSource({
    path: '/api/rest/agentApplicationRest/callAgentApplication.json',
    payload: {
      agentId: props.agentId,
      dialogsId: props.dialogsId,
      fileUrls: uploadFileList.value.map((item) => item.url),
      input: inputMessageContent.value,
      channel: query.channel || ChannelType.link_share,
      imageUrl: uploadImageList.value?.[0]?.url || '',
    },
    controller,
    onResponse: (data) => {
      // 推理内容
      if (data.reasoningContent) {
        reasoningContent += data.reasoningContent

        emit('updateSpecifyMessageItem', latestAssistantMessageKey, { reasoningContent })
        emit('updatePageScroll')
      }

      // 插件
      if (data.function && data.function.name) {
        emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
          pluginResult: {
            displayFormat: data.function.displayFormat,
            pluginName: data.function.name,
            arguments: data.function.arguments,
            pluginContent: data.function.result,
          },
        })
        emit('updatePageScroll')
      }

      // 数据库
      if (data.dbChainResult && data.dbChainResult?.[0]) {
        emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
          dbChainSQLContent: data.dbChainResult?.[0]?.sql || '',
        })
        return
      }

      // 回复内容
      if (data.message) {
        replyTextContent += data.message

        emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
          textContent: replyTextContent,
          isTextContentLoading: false,
        })
        emit('updatePageScroll')

        assistantFullAnswerContent.value = (assistantFullAnswerContent.value + data.message).replace(
          /\^\[[\d\\[\]-]+?\]\^/g,
          '',
        )

        if (!sentenceExtractCheckEnabled.value && props.isEnableVoice) {
          sentenceExtract(latestAssistantMessageKey)
          sentenceExtractCheckEnabled.value = true
          messageAudioLoading.value = true
        }
      }
    },
    onRequestError: () => {
      errorMessageResponse()
    },
    onError: () => {
      errorMessageResponse()
    },
    onFinally: () => {
      emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
        isEmptyContent: !replyTextContent,
        isTextContentLoading: false,
        isAnswerResponseLoading: false,
      })

      isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent)
      emit('updatePageScroll')
      isAnswerResponseWait.value = false
      controller = null
      userStore.isLogin && userStore.fetchUpdateEquityInfo()
    },
  })

  inputMessageContent.value = ''
  uploadFileList.value = uploadFileList.value.filter((item) => item.format === 'document')
}

function errorMessageResponse() {
  emit('updateSpecifyMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!, {
    isTextContentLoading: false,
    textContent: '',
  })
  emit('deleteMessageItem', currentLatestMessageItemKeyMap.value.get('user')!)
  emit('deleteMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!)
  emit('audioPause')
  blockMessageResponse()
}

function handleClearAllMessage() {
  if (!isAllowClearMessage.value) return

  emit('clearAllMessage')
}

function blockMessageResponse() {
  controller?.abort()
  isAnswerResponseWait.value = false
  messageAudioLoading.value = false
  userStore.isLogin && userStore.fetchUpdateEquityInfo()
}

function handleToLogin() {
  emit('toLogin')
}

function handleSelectFile(cb: () => void) {
  if (isUploadFileDisabled.value) {
    showDialog({
      title: '',
      message: t('common_module.dialogue_module.overwrite_file_tip'),
      showCancelButton: true,
      cancelButtonText: t('common_module.cancel_btn_text'),
      confirmButtonText: t('common_module.confirm_btn_text'),
      confirmButtonColor: '#F25744',
    }).then(() => {
      cb()
    })

    return
  }

  cb()
}

function sentenceExtract(messageId: string) {
  const symbolRegExp = /[。！？；.!?;~]/g
  let sentenceDraft = assistantFullAnswerContent.value
    .replace(/\s{5,}/gi, '')
    .slice(currentReplyContentSentenceExtractIndex.value)

  let matchResult = symbolRegExp.exec(sentenceDraft)

  function matchExtract() {
    const article = assistantFullAnswerContent.value.replace(/\s{5,}/gi, '')

    if (matchResult && matchResult.index && matchResult.index > 60) {
      sentenceDraft = article.slice(
        currentReplyContentSentenceExtractIndex.value,
        currentReplyContentSentenceExtractIndex.value + matchResult.index + matchResult['0'].length,
      )

      currentReplyContentSentenceExtractIndex.value += sentenceDraft.length

      ttsSocketSendText(sentenceDraft, sentenceFragmentSerialNo.value, messageId)
      sentenceFragmentSerialNo.value += 1

      if (article.length - currentReplyContentSentenceExtractIndex.value > 60) {
        sentenceDraft = article.slice(currentReplyContentSentenceExtractIndex.value)
        matchResult = symbolRegExp.exec(sentenceDraft)
        matchExtract()
      } else {
        setTimeout(() => sentenceExtract(messageId), 600)
      }
    } else if (!isAnswerResponseWait.value) {
      /* 延时避免最后回复内容没有更新全 */
      setTimeout(() => {
        sentenceDraft = article.slice(currentReplyContentSentenceExtractIndex.value)

        ttsSocketSendText(sentenceDraft, sentenceFragmentSerialNo.value, messageId)
        sentenceFragmentSerialNo.value += 1
      }, 700)
    } else {
      sentenceDraft = assistantFullAnswerContent.value
        .replace(/\s{5,}/gi, '')
        .slice(currentReplyContentSentenceExtractIndex.value)
      matchResult = symbolRegExp.exec(sentenceDraft)

      setTimeout(() => matchExtract(), 500)
    }
  }

  if (matchResult) matchExtract()
  else if (!isAnswerResponseWait.value) matchExtract()
  else setTimeout(() => sentenceExtract(messageId), 600)
}

function ttsSocketSendText(text: string, audioUrlSerialNo: number, messageId: string) {
  if (sentenceSpeechException.value) {
    return
  }

  const ttsSocketCtl = new WebSocketCtr(TEXTTOSPEECH_WS_URL)

  ttsSocketCtl.onMessage = (data: { audio: string; replyVoiceUrl: string; final: boolean }) => {
    if (data.replyVoiceUrl) {
      if (props.messageList.get(messageId)?.voiceFragmentUrlList) {
        const voiceFragmentUrlListDraft = [...props.messageList.get(messageId)!.voiceFragmentUrlList]
        voiceFragmentUrlListDraft[audioUrlSerialNo] = data.replyVoiceUrl
        messageAudioLoading.value = false

        emit('updateSpecifyMessageItem', messageId, { voiceFragmentUrlList: voiceFragmentUrlListDraft })

        if (props.answerAudioAutoPlay && audioUrlSerialNo === 0 && voiceFragmentUrlListDraft[audioUrlSerialNo]) {
          emit('audioPlay', props.messageList.get(messageId)!)
        }

        if (!props.answerAudioAutoPlay) {
          emit('updateSpecifyMessageItem', messageId, { isVoiceLoading: false })
        }
      }
    }
  }

  ttsSocketCtl.onMessageError = () => {
    emit('updateSpecifyMessageItem', messageId, { isVoiceLoading: false, voiceFragmentUrlList: [] })
    props.messageList.get(messageId)?.isVoicePlaying && emit('audioPause')
    sentenceSpeechException.value = true
    messageAudioLoading.value = false
    window.$message.error(t('common_module.unplayable_tip'))
  }

  const content = (text || '').replace(/\^\[[\d\\[\]-]+?\]\^/g, '')

  if (content && props.timbreId) {
    ttsSocketCtl.connect(() => {
      ttsSocketCtl.send({
        codec: 'wav',
        sampleRate: 16000,
        speed: 0,
        voiceType: props.timbreId,
        volume: 0,
        content,
      })
    })
  }
}

defineExpose({
  blockMessageResponse,
  errorMessageResponse,
  handleMessageSend,
})
</script>

<template>
  <div class="footer-wrapper">
    <div class="footer-main-container">
      <n-upload
        :show-file-list="false"
        accept=".doc, .pdf, .docx, .txt, .md, image/png, image/jpeg, image/jpg, image/webp"
        :disabled="!isLogin"
        abstract
        @before-upload="handleLimitUpload"
        @change="handleUpload"
      >
        <n-upload-trigger #="{ handleClick }" abstract>
          <n-popover style="width: 210px" trigger="hover">
            <template #trigger>
              <div
                v-show="isEnableDocumentParse"
                class="upload-file-btn"
                :class="
                  isLogin ? 'text-theme-color cursor-pointer hover:opacity-80' : 'cursor-not-allowed text-[#b8babf]'
                "
                @click="handleSelectFile(handleClick)"
              >
                <i class="iconfont icon-upload" />
              </div>
            </template>

            <span class="btn-tip"> {{ t('common_module.dialogue_module.upload_file_limit') }} </span>
          </n-popover>
        </n-upload-trigger>
      </n-upload>

      <n-popover trigger="hover">
        <template #trigger>
          <div
            class="clear-message-btn"
            :class="
              isAllowClearMessage
                ? 'text-theme-color cursor-pointer hover:opacity-80'
                : 'cursor-not-allowed text-[#b8babf]'
            "
            @click="handleClearAllMessage"
          >
            <i class="iconfont icon-clear" />
          </div>
        </template>
        <span class="btn-tip"> {{ t('common_module.dialogue_module.clear_message_popover_message') }}</span>
      </n-popover>

      <div class="footer-content-container">
        <div class="upload-image-list-container">
          <div
            v-for="uploadImageItem in uploadImageList"
            :key="uploadImageItem.id"
            class="upload-image-item"
            :class="{ 'upload-image-item-error': uploadImageItem.status === UploadStatus.ERROR }"
          >
            <div class="close-btn" @click="handleRemoveFile(uploadImageItem.id)">
              <CloseSmall theme="outline" size="16" fill="#fff" />
            </div>

            <div class="upload-image-item-content">
              <n-spin v-show="uploadImageItem.status === UploadStatus.UPLOADING" :size="20" />

              <n-image
                v-show="uploadImageItem.status === UploadStatus.FINISHED"
                object-fit="contain"
                :src="uploadImageItem.url"
                preview-disabled
                class="upload-image"
              />
            </div>
          </div>
        </div>

        <ul v-show="uploadDocumentList.length > 0" class="upload-file-list-container">
          <li
            v-for="uploadDocumentItem in uploadDocumentList"
            :key="uploadDocumentItem.id"
            class="upload-file-item"
            :class="uploadDocumentItem.status === 'error' ? 'border-error-font-color' : 'border-transparent'"
          >
            <div class="upload-file-item-container">
              <div class="upload-file-item-content">
                <img :src="uploadFileIcon(uploadDocumentItem.type!)" class="upload-file-icon" />

                <div class="upload-file-name">
                  <n-ellipsis>
                    {{ uploadDocumentItem.name }}
                  </n-ellipsis>
                </div>
              </div>

              <n-progress
                v-show="!['finished', 'error'].includes(uploadDocumentItem.status)"
                class="upload-file-item-progress"
                type="line"
                rail-color="#F3F3F3"
                :height="4"
                :percentage="uploadDocumentItem.percentage"
                :show-indicator="false"
              />

              <div
                v-show="['finished', 'error'].includes(uploadDocumentItem.status)"
                class="group-hover:block"
                :class="isMobile ? 'block' : 'hidden'"
              >
                <n-popover trigger="hover" placement="top-end" :show-arrow="false">
                  <template #trigger>
                    <i
                      class="iconfont icon-close close-upload-file-icon"
                      :class="
                        uploadDocumentItem.status === 'error' ? 'upload-file-error-icon' : 'upload-file-success-icon'
                      "
                      @click="handleRemoveFile(uploadDocumentItem.id)"
                    />
                  </template>
                  <span>{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}</span>
                </n-popover>
              </div>
            </div>
          </li>
        </ul>
        <div class="footer-input-container">
          <n-input
            v-model:value="inputMessageContent"
            type="textarea"
            :autosize="{ minRows: 1, maxRows: 5 }"
            :placeholder="inputPlaceholder"
            :disabled="!isLogin || isInputMessageDisabled"
            class="question-textarea"
            @keydown="handleInputMessageEnter"
          />
          <div
            class="send-btn"
            :class="
              isSendBtnDisabled ||
              isAnswerResponseWait ||
              !isLogin ||
              isInputMessageDisabled ||
              answerAudioPlaying ||
              messageAudioLoading
                ? 'opacity-60'
                : 'cursor-pointer'
            "
            @click="handleMessageSend()"
          />

          <div v-show="!isLogin" class="no-login-input-placeholder">
            <span>{{ t('share_agent_module.please') }}</span>
            <span class="to-login-text" @click="handleToLogin">
              {{ t('common_module.login') }}
            </span>
            <span>{{ t('share_agent_module.after_action') }}</span>
          </div>
        </div>
      </div>
    </div>

    <div class="footer-tip-container">
      <span class="tip-text">
        {{ t('common_module.dialogue_module.generate_warning_message') }}
      </span>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.footer-main-container {
  @apply flex items-end gap-[10px];

  .upload-file-btn,
  .upload-image-btn,
  .clear-message-btn {
    @apply flex h-[35px] w-[35px] items-center justify-center rounded-full bg-[#F2F2F2] text-[12px];

    i {
      @apply flex h-[16px] w-[16px] items-center justify-center;
    }
  }

  .footer-content-container {
    @apply flex flex-1 flex-col;

    .upload-image-list-container {
      @apply flex gap-[14px];

      .upload-image-item {
        @apply border-inactive-border-color relative mb-[6px] h-[48px] w-[48px] rounded-[10px] border bg-white;

        .close-btn {
          @apply absolute right-[-4px] top-[-4px] flex h-[16px] w-[16px] cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.55)] hover:opacity-80;
        }

        &-content {
          @apply flex h-full w-full items-center justify-center overflow-hidden rounded-[10px];
        }

        .upload-image {
          @apply h-full w-full flex-shrink-0;
        }
      }

      .upload-image-item-error {
        @apply border-[#F25744]!;
      }
    }

    .upload-file-list-container {
      @apply mb-[6px] grid gap-[6px];

      .upload-file-item {
        @apply group relative flex h-[42px] w-full items-center overflow-hidden rounded-[10px] border bg-white/70;

        &-container {
          @apply flex w-full items-center justify-between;

          .upload-file-item-content {
            @apply flex w-full items-center overflow-hidden;

            .upload-file-icon {
              @apply h-[24px] w-[24px];
            }

            .upload-file-name {
              @apply mx-[8px] flex flex-1 flex-col overflow-hidden text-[12px];
            }
          }

          .upload-file-item-progress {
            @apply w-[calc(100%-78px)]! absolute bottom-0 left-[54px];
          }

          .close-upload-file-icon {
            @apply cursor-pointer text-[14px] hover:opacity-80;
          }

          .upload-file-error-icon {
            @apply text-error-font-color;
          }

          .upload-file-success-icon {
            @apply text-font-color;
          }
        }
      }
    }

    .footer-input-container {
      @apply relative flex-1;

      .question-textarea {
        @apply rounded-[20px]! shadow-[0_1px_#09122105,0_1px_1px_#09122105,0_3px_3px_#09122103,0_9px_9px_#09122103]! bg-[#F2F2F2]! pr-[38px];
      }

      .send-btn {
        @apply bg-px-send-png absolute bottom-[9px] right-[16px] h-[18px] w-[18px];
      }

      .no-login-input-placeholder {
        top: calc((100% - 12px) / 2);

        @apply absolute left-[12px] flex max-w-[calc(100%-60px)] items-center overflow-hidden text-[12px] leading-none text-[#84868c];

        span {
          @apply shrink-0;
        }

        .to-login-text {
          @apply text-theme-color cursor-pointer px-[4px] hover:opacity-80;
        }
      }
    }
  }
}

.footer-tip-container {
  @apply mt-[7px];

  .tip-text {
    @apply flex w-full justify-center text-[11px] text-[#b8babf];
  }
}

.btn-tip {
  @apply text-[12px];
}

:deep(.question-textarea.n-input--textarea) {
  --n-height: 35px !important;
  --n-border: none !important;
  --n-border-focus: none !important;
  --n-border-hover: none !important;
  --n-box-shadow-focus: none !important;
  --n-border-disabled: none !important;

  .n-input__placeholder {
    font-size: 12px;
    line-height: 24px;
  }
}
</style>
