<script setup lang="ts">
import type {
  ChatArtifactLoader,
  ChatDocumentLoader,
  ChatImageLoader,
  ChatTimelineErrorItem,
  ChatTimelineItem,
  ChatTimelineMessageItem,
  ChatTimelineMessagePart,
  ChatTimelineToolApprovalPayload,
} from './types';

import { computed, ref } from 'vue';

import ChatThinkingBlock from '../chat-thinking/ChatThinkingBlock.vue';
import ChatArtifactAttachment from './ChatArtifactAttachment.vue';
import ChatAssistantAvatar from './ChatAssistantAvatar.vue';
import ChatDocumentAttachments from './ChatDocumentAttachments.vue';
import ChatErrorNotice from './ChatErrorNotice.vue';
import ChatImageAttachments from './ChatImageAttachments.vue';
import ChatKnowledgeCard from './ChatKnowledgeCard.vue';
import ChatMessageToolbar from './ChatMessageToolbar.vue';
import ChatTextBlock from './ChatTextBlock.vue';
import ChatTimelineStatusRow from './ChatTimelineStatusRow.vue';
import ChatToolCard from './ChatToolCard.vue';

const props = defineProps<{
  approvalLoading?: boolean;
  artifactLoader?: ChatArtifactLoader;
  assistantActionsVisible?: boolean;
  assistantAvatar?: string;
  copyable?: boolean;
  copyAction?: (item: ChatTimelineMessageItem) => boolean | Promise<boolean>;
  documentLoader?: ChatDocumentLoader;
  errorActionDisabled?: boolean;
  errorActionLabel?: string;
  imageLoader?: ChatImageLoader;
  item: ChatTimelineItem;
  regenerable?: boolean;
  regenerateDisabled?: boolean;
  variantLoading?: boolean;
}>();

const emit = defineEmits<{
  approve: [payload: ChatTimelineToolApprovalPayload];
  copyMessage: [item: ChatTimelineMessageItem];
  errorAction: [item: ChatTimelineErrorItem];
  regenerateMessage: [item: ChatTimelineMessageItem];
  reject: [payload: ChatTimelineToolApprovalPayload];
  selectNextVariant: [item: ChatTimelineMessageItem];
  selectPreviousVariant: [item: ChatTimelineMessageItem];
  thinkingToggle: [];
}>();

const messageItem = computed(() =>
  props.item.type === 'message' ? props.item : undefined,
);
// 流式快照会替换消息对象，用户手动选择需要由稳定的组件实例单独保留。
const thinkingExpandedOverrides = ref<Record<string, boolean>>({});

const alignmentClass = computed(() => {
  if (props.item.type === 'message' && props.item.role === 'user') {
    return 'is-user';
  }
  return 'is-assistant';
});

const showAssistantAvatar = computed(
  () =>
    messageItem.value?.role === 'assistant' && Boolean(props.assistantAvatar),
);

const showStatusAvatar = computed(
  () =>
    props.item.type === 'status' &&
    props.item.presentation !== 'separator' &&
    Boolean(props.assistantAvatar),
);

const variantCurrent = computed(() => {
  const item = messageItem.value;
  return Number(item?.selectedVariantIndex || item?.variantIndex || 1);
});

const variantTotal = computed(() =>
  Number(messageItem.value?.variantCount || 1),
);

const showVariantNavigator = computed(() => {
  const item = messageItem.value;
  return Boolean(
    item?.role === 'assistant' &&
      item.roundCompleted &&
      item.roundId &&
      item.switchable !== false &&
      variantTotal.value > 1,
  );
});

const disabledVariantPrevious = computed(
  () => props.variantLoading || variantCurrent.value <= 1,
);

const disabledVariantNext = computed(
  () => props.variantLoading || variantCurrent.value >= variantTotal.value,
);

const showToolbar = computed(() => {
  const item = messageItem.value;
  return Boolean(
    item &&
      (item.role === 'assistant'
        ? (props.assistantActionsVisible ?? true) &&
          (props.copyable || props.regenerable || showVariantNavigator.value)
        : props.copyable),
  );
});

function getMessageParts(item: ChatTimelineMessageItem) {
  const textParts = item.parts.filter(
    (part): part is Extract<ChatTimelineMessagePart, { type: 'text' }> =>
      part.type === 'text',
  );
  if (textParts.length <= 1) {
    return item.parts;
  }
  return [
    ...item.parts.filter((part) => part.type !== 'text'),
    {
      content: textParts.map((part) => part.content).join(''),
      id: `${item.id}-merged-text`,
      type: 'text' as const,
    },
  ];
}

function hasMessageBubble(item: ChatTimelineMessageItem) {
  return (
    getMessageParts(item).length > 0 || Boolean(item.knowledgeItems?.length)
  );
}

function updateThinkingExpanded(partId: string, expanded: boolean) {
  const item = messageItem.value;
  if (!item) {
    return;
  }
  const part = item.parts.find((current) => current.id === partId);
  if (part?.type === 'thinking') {
    thinkingExpandedOverrides.value = {
      ...thinkingExpandedOverrides.value,
      [partId]: expanded,
    };
    emit('thinkingToggle');
    part.expanded = expanded;
  }
}

function resolveThinkingExpanded(
  part: Extract<ChatTimelineMessagePart, { type: 'thinking' }>,
) {
  return (
    thinkingExpandedOverrides.value[part.id] ??
    part.expanded ??
    part.status === 'thinking'
  );
}

function handleCopyAction() {
  const item = messageItem.value;
  if (!item || !props.copyAction) {
    return false;
  }
  return props.copyAction(item);
}
</script>

<template>
  <div class="chat-timeline-item" :class="alignmentClass">
    <div
      v-if="messageItem"
      class="chat-timeline-item__message-row"
      :class="[
        `is-${messageItem.role}`,
        {
          'has-assistant-avatar': showAssistantAvatar,
          'has-variant-navigator': showVariantNavigator,
        },
      ]"
    >
      <div
        v-if="showAssistantAvatar"
        class="chat-timeline-item__assistant-avatar"
      >
        <ChatAssistantAvatar :src="assistantAvatar" />
      </div>
      <div
        class="chat-timeline-item__message"
        :class="`is-${messageItem.role}`"
      >
        <ChatImageAttachments
          v-if="messageItem.images?.length"
          :items="messageItem.images"
          :image-loader="imageLoader"
          compact
        />
        <ChatDocumentAttachments
          v-if="messageItem.documents?.length"
          :items="messageItem.documents"
          :document-loader="documentLoader"
          compact
        />
        <div
          v-if="hasMessageBubble(messageItem)"
          class="chat-timeline-item__bubble"
          :class="`is-${messageItem.role}`"
        >
          <template v-for="part in getMessageParts(messageItem)" :key="part.id">
            <ChatThinkingBlock
              v-if="part.type === 'thinking'"
              :content="part.content"
              :expanded="resolveThinkingExpanded(part)"
              :status="part.status"
              class="chat-timeline-item__thinking"
              readonly
              @update:expanded="updateThinkingExpanded(part.id, $event)"
            />
            <ChatTextBlock
              v-else
              :content="part.content"
              :streaming="messageItem.status === 'streaming'"
            />
          </template>
          <ChatKnowledgeCard
            v-if="messageItem.knowledgeItems?.length"
            :items="messageItem.knowledgeItems"
          />
        </div>
        <ChatMessageToolbar
          v-if="showToolbar && messageItem"
          :align="messageItem.role === 'user' ? 'end' : 'start'"
          :allow-copy="copyable"
          :allow-regenerate="regenerable"
          :copy-action="copyAction ? handleCopyAction : undefined"
          :disabled-variant-next="disabledVariantNext"
          :disabled-variant-previous="disabledVariantPrevious"
          :regenerate-disabled="regenerateDisabled"
          :show-variant-navigator="showVariantNavigator"
          :variant-current="variantCurrent"
          :variant-loading="variantLoading"
          :variant-total="variantTotal"
          @copy="emit('copyMessage', messageItem)"
          @regenerate="emit('regenerateMessage', messageItem)"
          @select-next-variant="emit('selectNextVariant', messageItem)"
          @select-previous-variant="emit('selectPreviousVariant', messageItem)"
        />
      </div>
    </div>
    <ChatToolCard
      v-else-if="item.type === 'tool'"
      :item="item"
      :loading="approvalLoading"
      @approve="emit('approve', $event)"
      @reject="emit('reject', $event)"
    />
    <ChatArtifactAttachment
      v-else-if="item.type === 'artifact'"
      :artifact-loader="artifactLoader"
      :item="item"
    />
    <ChatKnowledgeCard
      v-else-if="item.type === 'knowledge'"
      :items="item.items"
    />
    <div
      v-else-if="item.type === 'status'"
      class="chat-timeline-item__status-row"
      :class="{ 'has-assistant-avatar': showStatusAvatar }"
    >
      <div v-if="showStatusAvatar" class="chat-timeline-item__assistant-avatar">
        <ChatAssistantAvatar :src="assistantAvatar" />
      </div>
      <ChatTimelineStatusRow :item="item" />
    </div>
    <ChatErrorNotice
      v-else-if="item.type === 'error'"
      :action-disabled="errorActionDisabled"
      :action-label="errorActionLabel"
      :message="item.message"
      @action="emit('errorAction', item)"
    />
  </div>
</template>

<style scoped>
.chat-timeline-item {
  display: flex;
  width: 100%;
  --chat-assistant-avatar-size: 28px;
  --chat-message-max-width: 680px;
}

.chat-timeline-item.is-user {
  justify-content: flex-end;
}

.chat-timeline-item.is-assistant {
  justify-content: flex-start;
}

.chat-timeline-item__message-row {
  display: flex;
  min-width: 0;
  max-width: min(78%, var(--chat-message-max-width));
}

.chat-timeline-item__message-row.is-user {
  justify-content: flex-end;
}

.chat-timeline-item__message-row.is-assistant {
  gap: var(--space-2);
  align-items: flex-start;
}

.chat-timeline-item__message-row.has-assistant-avatar {
  max-width: min(
    78%,
    calc(
      var(--chat-message-max-width) + var(--chat-assistant-avatar-size) +
        var(--space-2)
    )
  );
}

.chat-timeline-item__assistant-avatar {
  flex: 0 0 var(--chat-assistant-avatar-size);
  width: var(--chat-assistant-avatar-size);
  height: var(--chat-assistant-avatar-size);
  overflow: hidden;
  border-radius: var(--radius-control);
}

.chat-timeline-item__status-row {
  display: flex;
  gap: var(--space-2);
  align-items: flex-start;
  min-width: 0;
}

.chat-timeline-item__bubble {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: fit-content;
  min-width: 0;
  max-width: 100%;
}

.chat-timeline-item__message {
  display: flex;
  flex-direction: column;
  gap: 0;
  min-width: 0;
  max-width: var(--chat-message-max-width);
}

.chat-timeline-item__message.is-user {
  align-items: flex-end;
  color: var(--el-text-color-primary);
}

.chat-timeline-item__message.is-assistant,
.chat-timeline-item__message.is-system {
  align-items: flex-start;
}

.chat-timeline-item__message :deep(.chat-text-block),
.chat-timeline-item__message :deep(.chat-thinking-block) {
  padding: 0;
  color: var(--el-text-color-primary);
  background: transparent;
  border: 0;
}

.chat-timeline-item__message :deep(.chat-text-block) {
  font-size: 14px;
  line-height: 22px;
}

.chat-timeline-item__message-row.has-variant-navigator {
  width: min(78%, var(--chat-message-max-width));
}

.chat-timeline-item__message-row.has-assistant-avatar.has-variant-navigator {
  width: min(
    78%,
    calc(
      var(--chat-message-max-width) + var(--chat-assistant-avatar-size) +
        var(--space-2)
    )
  );
}

.chat-timeline-item__message-row.has-assistant-avatar.has-variant-navigator
  .chat-timeline-item__message {
  width: calc(100% - var(--chat-assistant-avatar-size) - var(--space-2));
}

.chat-timeline-item__bubble.is-assistant,
.chat-timeline-item__bubble.is-system {
  color: var(--el-text-color-primary);
}

.chat-timeline-item__thinking {
  margin-bottom: 0;
}
</style>
