<script setup lang="ts">
import type {
  BubbleListInstance,
  BubbleListProps,
} from 'vue-element-plus-x/types/BubbleList';
import type { TypewriterInstance } from 'vue-element-plus-x/types/Typewriter';

import type { BotInfo, ChatTimeTimelineItem } from '@easyflow/types';

import {
  computed,
  nextTick,
  onBeforeUnmount,
  onMounted,
  ref,
  watch,
  watchEffect,
} from 'vue';
import ElBubbleList from 'vue-element-plus-x/es/BubbleList/index.js';
import ElSender from 'vue-element-plus-x/es/Sender/index.js';
import { useRoute, useRouter } from 'vue-router';

import { $t } from '@easyflow/locales';
import { useBotStore } from '@easyflow/stores';
import {
  createChatVariantSwitchController,
  ChatTimeHistoryMapper,
  ChatTimeTimelineBuilder,
  cn,
  uuid,
} from '@easyflow/utils';

import {
  ArrowDownBold,
  Paperclip,
} from '@element-plus/icons-vue';
import { ElButton, ElIcon, ElMessage, ElSpace } from 'element-plus';
import { tryit } from 'radash';

import { getMessageList, getPerQuestions } from '#/api';
import { api, sseClient } from '#/api/request';
import ChatTimeMessageContent from '#/components/chat/ChatTimeMessageContent.vue';
import ChatMessageActionBar from '#/components/chat-workspace/ChatMessageActionBar.vue';
import SendEnableIcon from '#/components/icons/SendEnableIcon.vue';
import SendIcon from '#/components/icons/SendIcon.vue';
import ChatFileUploader from '#/components/upload/ChatFileUploader.vue';

import BotAvatar from '../botAvatar/botAvatar.vue';
import SendingIcon from '../icons/SendingIcon.vue';

const props = defineProps<{
  bot?: BotInfo;
  conversationId?: string;
  // 是否显示对话列表
  showChatConversations?: boolean;
}>();
const botStore = useBotStore();
interface historyMessageType {
  role: string;
  content: string;
}
interface presetQuestionsType {
  key: string;
  description: string;
}
interface SendMessageOptions {
  prompt?: string;
  regenerateRoundId?: string;
}

interface SseRoundMeta {
  roundId?: string;
  roundNo?: number;
  selectedVariantIndex?: number;
  switchable?: boolean;
  variantCount?: number;
  variantIndex?: number;
}
const route = useRoute();
const botId = ref<string>((route.params.id as string) || '');
const router = useRouter();

const bubbleItems = ref<BubbleListProps<ChatTimeTimelineItem>['list']>([]);
const bubbleListRef = ref<BubbleListInstance>();
const messageContainerRef = ref<HTMLElement | null>(null);
const bubbleListScrollElement = ref<HTMLElement | null>(null);
const showBackToBottomButton = ref(false);
const senderRef = ref<InstanceType<typeof ElSender>>();
const senderValue = ref('');
const sending = ref(false);
const variantSwitchStateVersion = ref(0);
const BACK_TO_BOTTOM_THRESHOLD = 160;
const variantSwitchController = createChatVariantSwitchController<any, ChatTimeTimelineItem>({
  mapRecords: (records) => ChatTimeHistoryMapper.fromHistoryRecords(records),
  onError: () => ElMessage.error('答案版本切换失败'),
  onStateChange: () => {
    variantSwitchStateVersion.value += 1;
  },
  replaceRound: (items, roundId, nextItems) =>
    ChatTimeTimelineBuilder.replaceRoundMessages(items, roundId, nextItems),
});
const latestAssistantMessage = computed(() => {
  return [...bubbleItems.value].reverse().find((item) => item.role === 'assistant');
});
const getConversationId = async () => {
  const res = await api.get('/api/v1/bot/generateConversationId');
  return res.data;
};
const localeConversationId = ref<any>('');

const presetQuestions = ref<presetQuestionsType[]>([]);
defineExpose({
  clear() {
    bubbleItems.value = [];
    messages.value = [];
  },
});
const getPresetQuestions = () => {
  api
    .get('/api/v1/bot/detail', {
      params: {
        id: botId.value,
      },
    })
    .then((res) => {
      if (res.data.options?.presetQuestions) {
        presetQuestions.value = res.data.options?.presetQuestions
          .filter(
            (item: presetQuestionsType) =>
              item.description && item.description.trim() !== '',
          )
          .map((item: presetQuestionsType) => ({
            key: item.key,
            description: item.description,
          }));
      }
    });
};
onMounted(async () => {
  // 初始化 conversationId
  localeConversationId.value =
    props.conversationId && props.conversationId.length > 0
      ? props.conversationId
      : await getConversationId();
  getPresetQuestions();
  nextTick(() => {
    bindBubbleListScroll();
  });
});
watchEffect(async () => {
  if (props.bot && props.conversationId) {
    const [, res] = await tryit(getMessageList)({
      conversationId: props.conversationId,
      botId: props.bot.id,
      tempUserId: uuid() + props.bot.id,
    });

    if (res?.errorCode === 0) {
      bubbleItems.value = ChatTimeHistoryMapper.fromHistoryRecords(
        res.data as any[],
      );
      prefetchVisibleVariants();
    }
  } else {
    bubbleItems.value = [];
  }
});
const lastUserMessage = ref('');
const messages = ref<historyMessageType[]>([]);
const updateBackToBottomButtonVisible = () => {
  if (!bubbleListScrollElement.value) {
    showBackToBottomButton.value = false;
    return;
  }
  const { scrollTop, scrollHeight, clientHeight } =
    bubbleListScrollElement.value;
  showBackToBottomButton.value =
    scrollHeight - (scrollTop + clientHeight) > BACK_TO_BOTTOM_THRESHOLD;
};
const handleBubbleListScroll = () => {
  updateBackToBottomButtonVisible();
};
const bindBubbleListScroll = () => {
  const nextScrollElement = messageContainerRef.value?.querySelector(
    '.el-bubble-list',
  ) as HTMLElement | null;
  if (bubbleListScrollElement.value === nextScrollElement) {
    updateBackToBottomButtonVisible();
    return;
  }
  if (bubbleListScrollElement.value) {
    bubbleListScrollElement.value.removeEventListener(
      'scroll',
      handleBubbleListScroll,
    );
  }
  bubbleListScrollElement.value = nextScrollElement;
  if (bubbleListScrollElement.value) {
    bubbleListScrollElement.value.addEventListener(
      'scroll',
      handleBubbleListScroll,
      {
        passive: true,
      },
    );
  }
  updateBackToBottomButtonVisible();
};
const finalizeTimelineTail = () => {
  ChatTimeTimelineBuilder.finalize(bubbleItems.value);
  prefetchVisibleVariants();
};
const stopSse = () => {
  sseClient.abort();
  sending.value = false;
  finalizeTimelineTail();
};
const clearSenderFiles = () => {
  files.value = [];
  attachmentsRef.value?.clearFiles();
};
const handleSubmit = async (refreshContent: string, options: SendMessageOptions = {}) => {
  const attachments = attachmentsRef.value?.getFileList();
  const currentPrompt = (options.prompt || refreshContent || senderValue.value).trim();
  if (!currentPrompt) {
    return;
  }
  const regenerateRoundId = options.regenerateRoundId
    ? String(options.regenerateRoundId)
    : '';
  const isRegenerate = !!regenerateRoundId;
  sending.value = true;
  lastUserMessage.value = currentPrompt;
  if (!isRegenerate && latestAssistantMessage.value?.roundId) {
    ChatTimeTimelineBuilder.setRoundSwitchable(
      bubbleItems.value,
      latestAssistantMessage.value.roundId,
      false,
    );
  }
  messages.value.push({
    role: 'user',
    content: currentPrompt,
  });
  const copyMessages = [...messages.value];
  const data = {
    botId: botId.value,
    prompt: currentPrompt,
    conversationId: localeConversationId.value,
    messages: copyMessages,
    attachments,
    regenerateRoundId: regenerateRoundId || undefined,
  };
  clearSenderFiles();
  messages.value.pop();
  if (!isRegenerate) {
    const mockMessages = generateMockMessages(refreshContent || currentPrompt);
    bubbleItems.value.push(...mockMessages);
    senderRef.value?.clear();
  }
  let receivedAssistantPayload = false;
  sseClient.post('/api/v1/bot/chat', data, {
    onMessage(message) {
      const event = message.event;

      //  finish
      if (event === 'done') {
        sending.value = false;
        finalizeTimelineTail();
        return;
      }
      if (!message.data) {
        return;
      }
      // 处理系统错误
      const sseData = JSON.parse(message.data);
      const streamMeta = normalizeSseRoundMeta(sseData?.meta);
      ChatTimeTimelineBuilder.bindLatestPendingUserMessage(
        bubbleItems.value,
        streamMeta,
      );
      if (
        sseData?.domain === 'SYSTEM' &&
        sseData.payload?.code === 'SYSTEM_ERROR'
      ) {
        if (isRegenerate && !receivedAssistantPayload) {
          ElMessage.error(sseData.payload.message || '重新生成失败');
        } else {
          ChatTimeTimelineBuilder.applySystemError(
            bubbleItems.value,
            sseData.payload.message,
            Date.now(),
          );
        }
        return;
      }

      if (sseData?.domain === 'TOOL') {
        receivedAssistantPayload = true;
        if (sseData?.type === 'TOOL_CALL') {
          ChatTimeTimelineBuilder.upsertToolCall(bubbleItems.value, {
            created: Date.now(),
            ...streamMeta,
            name: sseData?.payload?.name,
            toolCallId: sseData?.payload?.tool_call_id,
            value: sseData?.payload?.arguments,
          });
        } else {
          ChatTimeTimelineBuilder.upsertToolResult(bubbleItems.value, {
            created: Date.now(),
            ...streamMeta,
            name: sseData?.payload?.name,
            result: sseData?.payload?.result,
            toolCallId: sseData?.payload?.tool_call_id,
          });
        }
        return;
      }

      // 处理流式消息
      const delta = sseData.payload?.delta;
      const role = sseData.payload?.role;

      if (delta) {
        if (sseData.type === 'THINKING') {
          receivedAssistantPayload = true;
          ChatTimeTimelineBuilder.appendThinkingDelta(
            bubbleItems.value,
            delta,
            Date.now(),
            streamMeta,
          );
        } else if (sseData.type === 'MESSAGE') {
          receivedAssistantPayload = true;
          ChatTimeTimelineBuilder.appendMessageDelta(
            bubbleItems.value,
            delta,
            Date.now(),
            streamMeta,
          );
        }
      }

      // 是否需要保存聊天记录
      if (event === 'needSaveMessage') {
        messages.value.push({
          role,
          content: sseData.payload?.content,
        });
      }
    },
    onFinished() {
      sending.value = false;
      finalizeTimelineTail();
    },
    onError(err) {
      console.error(err);
      sending.value = false;
      finalizeTimelineTail();
    },
  });
};

const handleComplete = (_: TypewriterInstance, index: number) => {
  if (
    index === bubbleItems.value.length - 1 &&
    props.conversationId &&
    props.conversationId.length <= 0 &&
    sending.value === false
  ) {
    setTimeout(() => {
      router.replace({
        params: { conversationId: localeConversationId.value },
      });
    }, 100);
  }
};

const generateMockMessages = (refreshContent: string) => {
  const userMessage: ChatTimeTimelineItem = {
    role: 'user',
    id: Date.now().toString(),
    content: refreshContent || senderValue.value,
    created: Date.now(),
    placement: 'end',
  };
  return [userMessage];
};

const handleRefresh = () => {
  const roundId = String(latestAssistantMessage.value?.roundId || '');
  if (!roundId) {
    return;
  }
  handleSubmit('', {
    prompt: resolveRoundPrompt(roundId),
    regenerateRoundId: roundId,
  });
};

const canRegenerateMessage = (item: ChatTimeTimelineItem) => {
  return (
    item.role === 'assistant' &&
    item.id === latestAssistantMessage.value?.id &&
    !!String(item.roundId || '').trim()
  );
};

const shouldShowVariantNavigator = (item: ChatTimeTimelineItem) => {
  return (
    item.role === 'assistant' &&
    isFinalAssistantInRound(item) &&
    Number(item.variantCount || 0) > 1 &&
    !!item.roundId
  );
};

const canSwitchVariant = (
  item: ChatTimeTimelineItem,
  direction: 'next' | 'previous',
) => {
  if (
    item.role !== 'assistant' ||
    !isFinalAssistantInRound(item) ||
    !item.switchable ||
    isVariantSwitching(item)
  ) {
    return false;
  }
  const current = Number(item.variantIndex || item.selectedVariantIndex || 1);
  const total = Number(item.variantCount || 1);
  if (direction === 'previous') {
    return current > 1;
  }
  return current < total;
};

const handleSelectVariant = async (
  item: ChatTimeTimelineItem,
  direction: 'next' | 'previous',
) => {
  if (!item.roundId || !localeConversationId.value) {
    return;
  }
  const current = Number(item.variantIndex || item.selectedVariantIndex || 1);
  const variantIndex = direction === 'previous' ? current - 1 : current + 1;
  await variantSwitchController.switchVariant({
    fetchVariants: () =>
      fetchRoundVariants(String(localeConversationId.value), String(item.roundId)),
    items: bubbleItems.value,
    persistVariant: async () => {
      const [, res] = await tryit(api.post)(
        `/api/v1/chatWorkspace/sessions/${localeConversationId.value}/rounds/${item.roundId}/selectVariant`,
        {
          variantIndex,
        },
      );
      if (res?.errorCode !== 0 || !res?.data) {
        throw new Error(res?.message || '答案版本切换失败');
      }
      return res.data;
    },
    roundId: item.roundId,
    sessionId: localeConversationId.value,
    targetVariantIndex: variantIndex,
  });
};

async function fetchRoundVariants(sessionId: string, roundId: string) {
  const [, res] = await tryit(api.get)(
    `/api/v1/chatWorkspace/sessions/${sessionId}/rounds/${roundId}/variants`,
  );
  if (res?.errorCode !== 0) {
    throw new Error(res?.message || '答案版本加载失败');
  }
  return res.data || [];
}

function isVariantSwitching(item: ChatTimeTimelineItem) {
  variantSwitchStateVersion.value;
  return variantSwitchController.isSwitching(localeConversationId.value, item.roundId);
}

function isFinalAssistantInRound(item: ChatTimeTimelineItem) {
  if (item.role !== 'assistant') {
    return false;
  }
  if (!item.roundId) {
    return true;
  }
  for (let index = bubbleItems.value.length - 1; index >= 0; index -= 1) {
    const candidate = bubbleItems.value[index];
    if (
      candidate?.role === 'assistant' &&
      candidate.roundId === item.roundId &&
      String(candidate.variantIndex || '') === String(item.variantIndex || '')
    ) {
      return candidate.id === item.id;
    }
  }
  return true;
}

function prefetchVisibleVariants() {
  if (!localeConversationId.value) {
    return;
  }
  for (const item of bubbleItems.value) {
    if (
      item.role === 'assistant' &&
      item.roundId &&
      Number(item.variantCount || 0) > 1
    ) {
      variantSwitchController.prefetchVariants({
        fetchVariants: () =>
          fetchRoundVariants(String(localeConversationId.value), String(item.roundId)),
        roundId: item.roundId,
        sessionId: localeConversationId.value,
      });
    }
  }
}
const scrollToBottom = () => {
  bubbleListRef.value?.scrollToBottom();
  if (!bubbleListRef.value && bubbleListScrollElement.value) {
    bubbleListScrollElement.value.scrollTop =
      bubbleListScrollElement.value.scrollHeight;
  }
  showBackToBottomButton.value = false;
};
const attachmentsRef = ref();
const files = ref<any[]>([]);
function handlePasteFile(_: any, fileList: FileList) {
  files.value = [...fileList];
}
function triggerFileSelect() {
  attachmentsRef.value?.triggerFileSelect?.();
}
function handleDeleteAllSenderFiles() {
  files.value = [];
}

function resolveRoundPrompt(roundId: string) {
  const target = [...bubbleItems.value].reverse().find(
    (item) => item.role === 'user' && item.roundId === roundId,
  );
  return String(target?.content || lastUserMessage.value || '').trim();
}

function normalizeSseRoundMeta(meta: any): SseRoundMeta {
  if (!meta || typeof meta !== 'object') {
    return {};
  }
  const variantIndex = meta.variantIndex ? Number(meta.variantIndex) : undefined;
  return {
    roundId: meta.roundId ? String(meta.roundId) : undefined,
    roundNo: meta.roundNo ? Number(meta.roundNo) : undefined,
    selectedVariantIndex: meta.selectedVariantIndex
      ? Number(meta.selectedVariantIndex)
      : variantIndex,
    switchable:
      typeof meta.switchable === 'boolean'
        ? meta.switchable
        : variantIndex != null
          ? true
          : undefined,
    variantCount: meta.variantCount ? Number(meta.variantCount) : variantIndex,
    variantIndex,
  };
}
watch(
  () => [localeConversationId.value, bubbleItems.value.length],
  () => {
    nextTick(() => {
      bindBubbleListScroll();
    });
  },
  { flush: 'post' },
);
onBeforeUnmount(() => {
  if (bubbleListScrollElement.value) {
    bubbleListScrollElement.value.removeEventListener(
      'scroll',
      handleBubbleListScroll,
    );
  }
});
</script>

<template>
  <div class="mx-auto h-full max-w-[780px]">
    <div
      :class="
        cn(
          'flex h-full w-full flex-col gap-3',
          !localeConversationId && 'items-center justify-center gap-8',
        )
      "
    >
      <!-- 对话列表 -->
      <div
        v-if="localeConversationId || bubbleItems.length > 0"
        ref="messageContainerRef"
        class="message-container w-full flex-1 overflow-hidden"
      >
        <ElBubbleList
          ref="bubbleListRef"
          class="!h-full"
          max-height="none"
          :show-back-button="false"
          :list="bubbleItems"
          @complete="handleComplete"
        >
          <template #header="{ item }">
            <div class="flex flex-col">
              <span class="chat-bubble-item-time-style">
                {{ new Date(item.created).toLocaleString() }}
              </span>
            </div>
          </template>
          <!-- 自定义头像 -->
          <template #avatar="{ item }">
            <BotAvatar
              v-if="item.role !== 'user'"
              :src="bot?.icon"
              :size="40"
            />
          </template>
          <template #content="{ item }">
            <ChatTimeMessageContent :item="item" />
          </template>
          <!-- 自定义底部 -->
          <template #footer="{ item }">
            <ChatMessageActionBar
              v-if="item.role !== 'tool'"
              :align="item.role === 'user' ? 'end' : 'start'"
              :allow-copy="!!String(item.content || '').trim()"
              :allow-regenerate="canRegenerateMessage(item)"
              :copy-error-message="$t('message.copyFail')"
              :copy-text="String(item.content || '').trim()"
              :disabled-variant-next="!canSwitchVariant(item, 'next')"
              :disabled-variant-previous="!canSwitchVariant(item, 'previous')"
              :regenerate-disabled="sending"
              :show-variant-navigator="shouldShowVariantNavigator(item)"
              :variant-loading="isVariantSwitching(item)"
              :variant-current="
                Number(item.variantIndex || item.selectedVariantIndex || 1)
              "
              :variant-total="Number(item.variantCount || 1)"
              @regenerate="handleRefresh"
              @select-next-variant="handleSelectVariant(item, 'next')"
              @select-previous-variant="handleSelectVariant(item, 'previous')"
            />
          </template>
        </ElBubbleList>
        <button
          v-if="showBackToBottomButton"
          type="button"
          class="chat-back-to-bottom-btn"
          @click="scrollToBottom"
        >
          <ElIcon size="16">
            <ArrowDownBold />
          </ElIcon>
        </button>
      </div>

      <!-- 新对话显示bot信息 -->
      <div v-else class="flex flex-col items-center gap-3.5">
        <BotAvatar :src="bot?.icon" :size="88" />
        <h1 class="text-base font-medium text-black/85">
          {{ bot?.title }}
        </h1>
        <span class="text-sm text-[#757575]">{{ bot?.description }}</span>
      </div>

      <!--问题预设-->
      <div
        class="questions-preset-container"
        v-if="botStore.presetQuestions.length > 0"
      >
        <ElButton
          v-for="item in getPerQuestions(botStore.presetQuestions)"
          :key="item.key"
          @click="handleSubmit(item.description)"
        >
          {{ item.description }}
        </ElButton>
      </div>
      <!-- Sender -->
      <ChatFileUploader
        ref="attachmentsRef"
        :external-files="files"
        :max-size="10"
        @delete-all="handleDeleteAllSenderFiles"
      />
      <ElSender
        ref="senderRef"
        class="w-full"
        v-model="senderValue"
        :placeholder="$t('message.pleaseInputContent')"
        variant="updown"
        :auto-size="{ minRows: 3, maxRows: 6 }"
        allow-speech
        @submit="handleSubmit"
        @paste-file="handlePasteFile"
      >
        <template #action-list>
          <ElSpace>
            <ElButton circle @click="triggerFileSelect">
              <ElIcon><Paperclip /></ElIcon>
            </ElButton>
            <!--<ElButton circle @click="uploadRef.triggerFileSelect()">
              <ElIcon><Paperclip /></ElIcon>
            </ElButton>
            <ElButton circle>
              <ElIcon><Microphone /></ElIcon>
              &lt;!&ndash; <ElIcon color="#0066FF"><RecordingIcon /></ElIcon> &ndash;&gt;
            </ElButton>-->
            <ElButton v-if="sending" circle @click="stopSse">
              <ElIcon size="30" color="#409eff"><SendingIcon /></ElIcon>
            </ElButton>
            <template v-else>
              <ElButton v-if="!senderValue" circle disabled>
                <SendIcon />
              </ElButton>
              <ElButton v-else circle @click="handleSubmit('')">
                <SendEnableIcon />
              </ElButton>
            </template>
          </ElSpace>
        </template>
      </ElSender>
    </div>
  </div>
</template>

<style scoped>
.questions-preset-container {
  display: flex;
  flex-flow: row nowrap;
  gap: 10px;
  align-items: center;
  justify-content: flex-start;
  width: 100%;
  overflow: auto;
}

.message-container {
  position: relative;
  padding: 8px;
  background-color: var(--bot-chat-message-container);
  border-radius: 8px;
}

.dark .message-container {
  border: 1px solid hsl(var(--border));
}

.chat-back-to-bottom-btn {
  position: absolute;
  right: 16px;
  bottom: 16px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  color: var(--el-text-color-regular);
  cursor: pointer;
  background: var(--el-bg-color-overlay);
  border: 1px solid var(--el-border-color-light);
  border-radius: 999px;
  box-shadow: 0 6px 10px rgb(0 0 0 / 8%);
  transition: all 0.2s ease;
}

.chat-back-to-bottom-btn:hover {
  color: var(--el-color-primary);
  transform: translateY(-1px);
}

:deep(.el-bubble-content-wrapper .el-bubble-content-filled[data-v-a52d8fe0]) {
  background-color: var(--bot-chat-message-item-back);
}

.chat-bubble-item-time-style {
  font-size: 12px;
  color: var(--common-font-placeholder-color);
}

.el-bubble-list :deep(.el-bubble.el-bubble-start) {
  --bubble-content-max-width: calc(
    100% - var(--el-bubble-avatar-placeholder-gap)
  ) !important;
}

.el-bubble-list :deep(.el-bubble.el-bubble-end) {
  --bubble-content-max-width: calc(
    100% -
      calc(
        var(--el-bubble-avatar-placeholder-gap) + var(--el-avatar-size, 40px)
      )
  ) !important;
}

:deep(.el-bubble-header) {
  width: 100%;
}

:deep(.el-bubble-end .el-bubble-header) {
  width: fit-content;
}
</style>
