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

import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';

import ChatAssistantAvatar from './ChatAssistantAvatar.vue';
import ChatTimelineItemView from './ChatTimelineItem.vue';

const props = defineProps<{
  actionAnchorId?: string;
  approvalLoading?: boolean;
  artifactLoader?: ChatArtifactLoader;
  assistantAvatar?: string;
  copyable?: (item: ChatTimelineMessageItem) => boolean;
  copyAction?: (item: ChatTimelineMessageItem) => boolean | Promise<boolean>;
  documentLoader?: ChatDocumentLoader;
  errorAction?: (item: ChatTimelineErrorItem) => string | undefined;
  errorActionDisabled?: boolean;
  imageLoader?: ChatImageLoader;
  items: ChatTimelineItem[];
  regenerable?: (item: ChatTimelineMessageItem) => boolean;
  regenerateDisabled?: boolean;
  roundId: string;
  variantLoading?: (item: ChatTimelineMessageItem) => boolean;
}>();

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

const turnSucceeded = computed(() =>
  props.items.some((item) => item.turnSucceeded === true),
);

const latestAssistantContentSource = computed(() =>
  [...props.items]
    .reverse()
    .find(
      (item): item is ChatTimelineMessageItem =>
        item.type === 'message' &&
        item.role === 'assistant' &&
        (item.parts.some((part) => part.type === 'text' && part.content) ||
          Boolean(item.knowledgeItems?.length)),
    ),
);

const finalMessageSource = computed(() =>
  turnSucceeded.value ? latestAssistantContentSource.value : undefined,
);

const finalMessage = computed<ChatTimelineMessageItem | undefined>(() => {
  const item = finalMessageSource.value;
  if (!item) {
    return undefined;
  }
  return {
    ...item,
    parts: item.parts.filter((part) => part.type === 'text'),
  };
});

const processItems = computed<ChatTimelineItem[]>(() => {
  const finalSource = finalMessageSource.value;
  return props.items.flatMap((item) => {
    if (item.type === 'artifact') {
      return [];
    }
    if (
      item.type === 'message' &&
      item.role === 'assistant' &&
      item.parts.length === 0 &&
      !item.knowledgeItems?.length &&
      !item.images?.length &&
      !item.documents?.length
    ) {
      return [];
    }
    if (item !== finalSource) {
      return [item];
    }
    const thinkingParts = item.parts.filter((part) => part.type === 'thinking');
    if (thinkingParts.length === 0) {
      return [];
    }
    return [
      {
        ...item,
        id: `${item.id}-process`,
        knowledgeItems: undefined,
        parts: thinkingParts,
        roundCompleted: false,
      },
    ];
  });
});

const artifactItems = computed(() =>
  props.items.filter((item) => item.type === 'artifact'),
);

const turnStartedAt = computed(() => {
  const starts = props.items
    .map((item) => item.turnStartedAt)
    .filter(
      (value): value is number =>
        typeof value === 'number' && Number.isFinite(value),
    );
  return starts.length > 0 ? Math.min(...starts) : undefined;
});
const turnFinishedAt = computed(() => {
  const finishes = props.items
    .map((item) => item.turnFinishedAt)
    .filter(
      (value): value is number =>
        typeof value === 'number' && Number.isFinite(value),
    );
  return finishes.length > 0 ? Math.max(...finishes) : undefined;
});
const turnFinished = computed(() => turnFinishedAt.value !== undefined);
const turnActive = computed(() => !turnSucceeded.value && !turnFinished.value);
const hasPendingApproval = computed(() =>
  props.items.some(
    (item) =>
      item.type === 'tool' &&
      (item.status === 'approving' || item.status === 'pending_approval'),
  ),
);
const canCollapse = computed(
  () =>
    turnSucceeded.value &&
    Boolean(finalMessage.value) &&
    !hasPendingApproval.value,
);
const processId = computed(() => `chat-turn-process-${props.roundId}`);
const expanded = ref(!canCollapse.value);

watch(
  canCollapse,
  (value, previous) => {
    if (value && !previous) {
      if (previous === false) {
        emit('layoutToggle', props.roundId);
      }
      expanded.value = false;
      if (processItems.value.length === 0) {
        void nextTick(() => emit('layoutChanged'));
      }
    } else if (!value) {
      expanded.value = true;
    }
  },
  { immediate: true },
);

const observedStartedAt = Date.now();
const clockNow = ref(observedStartedAt);
let elapsedTimer: ReturnType<typeof setInterval> | undefined;

function stopElapsedTimer() {
  if (elapsedTimer === undefined) {
    return;
  }
  clearInterval(elapsedTimer);
  elapsedTimer = undefined;
}

function syncElapsedTimer() {
  clockNow.value = Date.now();
  stopElapsedTimer();
  if (!turnActive.value) {
    return;
  }
  elapsedTimer = setInterval(() => {
    clockNow.value = Date.now();
  }, 1000);
}

watch([turnStartedAt, turnActive], syncElapsedTimer, { immediate: true });
onBeforeUnmount(stopElapsedTimer);

const durationLabel = computed(() => {
  const startedAt =
    turnStartedAt.value ?? (turnActive.value ? observedStartedAt : undefined);
  const finishedAt =
    turnFinishedAt.value ?? (turnActive.value ? clockNow.value : undefined);
  if (startedAt === undefined || finishedAt === undefined) {
    return '';
  }
  const seconds = Math.max(1, Math.round((finishedAt - startedAt) / 1000));
  if (seconds < 60) {
    return `${seconds} 秒`;
  }
  const minutes = Math.floor(seconds / 60);
  const remainingSeconds = seconds % 60;
  if (minutes < 60) {
    return `${minutes} 分 ${remainingSeconds} 秒`;
  }
  const hours = Math.floor(minutes / 60);
  const remainingMinutes = minutes % 60;
  return `${hours} 小时 ${remainingMinutes} 分 ${remainingSeconds} 秒`;
});

const summaryLabel = computed(() => {
  if (turnSucceeded.value) {
    return durationLabel.value ? `已处理 ${durationLabel.value}` : '已处理';
  }
  if (turnFinished.value) {
    return '处理未完成';
  }
  return `已处理 ${durationLabel.value || '1 秒'}`;
});

function isAssistantActionAnchor(item: ChatTimelineItem) {
  return item.type === 'message' && item.id === props.actionAnchorId;
}

function canCopyMessage(item: ChatTimelineItem) {
  return item.type === 'message' && (props.copyable?.(item) ?? false);
}

function canRegenerateMessage(item: ChatTimelineItem) {
  return item.type === 'message' && (props.regenerable?.(item) ?? false);
}

function errorActionLabel(item: ChatTimelineItem) {
  return item.type === 'error' ? props.errorAction?.(item) : undefined;
}

function isVariantLoading(item: ChatTimelineItem) {
  return item.type === 'message' && (props.variantLoading?.(item) ?? false);
}

function isLiveContentAnchor(item: ChatTimelineItem) {
  return !turnSucceeded.value && item === latestAssistantContentSource.value;
}

function toggleProcess() {
  if (!canCollapse.value) {
    return;
  }
  emit('layoutToggle', props.roundId);
  expanded.value = !expanded.value;
  if (processItems.value.length === 0) {
    void nextTick(() => emit('layoutChanged'));
  }
}

function handleNestedLayoutToggle() {
  emit('layoutToggle', props.roundId);
  void nextTick(() => emit('layoutChanged'));
}
</script>

<template>
  <section class="chat-timeline-turn" :data-round-id="roundId">
    <header class="chat-timeline-turn__header">
      <span class="chat-timeline-turn__avatar">
        <ChatAssistantAvatar :src="assistantAvatar" />
      </span>
      <button
        class="chat-timeline-turn__summary"
        :class="{ 'is-toggleable': canCollapse }"
        :disabled="!canCollapse"
        type="button"
        :aria-controls="canCollapse ? processId : undefined"
        :aria-expanded="canCollapse ? expanded : undefined"
        @click="toggleProcess"
      >
        <span>{{ summaryLabel }}</span>
        <svg
          v-if="canCollapse"
          class="chat-timeline-turn__chevron"
          :class="{ 'is-expanded': expanded }"
          aria-hidden="true"
          viewBox="0 0 16 16"
        >
          <path d="M5.5 3.5 10 8l-4.5 4.5" />
        </svg>
      </button>
    </header>

    <div class="chat-timeline-turn__content">
      <Transition
        name="chat-turn-process"
        @after-enter="emit('layoutChanged')"
        @after-leave="emit('layoutChanged')"
      >
        <div
          v-if="expanded && processItems.length > 0"
          :id="processId"
          class="chat-timeline-turn__process"
        >
          <template v-for="item in processItems" :key="item.id">
            <slot
              v-if="item.type === 'custom'"
              name="custom-item"
              :item="item"
            ></slot>
            <ChatTimelineItemView
              v-else
              :assistant-actions-visible="isAssistantActionAnchor(item)"
              :artifact-loader="artifactLoader"
              :data-chat-turn-live-anchor="
                isLiveContentAnchor(item) ? '' : undefined
              "
              :item="item"
              :document-loader="documentLoader"
              :error-action-disabled="errorActionDisabled"
              :error-action-label="errorActionLabel(item)"
              :image-loader="imageLoader"
              :approval-loading="approvalLoading"
              :copy-action="copyAction"
              :copyable="canCopyMessage(item)"
              :regenerable="canRegenerateMessage(item)"
              :regenerate-disabled="regenerateDisabled"
              :variant-loading="isVariantLoading(item)"
              @approve="emit('approve', $event)"
              @copy-message="emit('copyMessage', $event)"
              @error-action="emit('errorAction', $event)"
              @regenerate-message="emit('regenerateMessage', $event)"
              @reject="emit('reject', $event)"
              @select-next-variant="emit('selectNextVariant', $event)"
              @select-previous-variant="emit('selectPreviousVariant', $event)"
              @thinking-toggle="handleNestedLayoutToggle"
            />
          </template>
        </div>
      </Transition>

      <div v-if="finalMessage" data-chat-turn-final>
        <ChatTimelineItemView
          :assistant-actions-visible="isAssistantActionAnchor(finalMessage)"
          :artifact-loader="artifactLoader"
          :item="finalMessage"
          :document-loader="documentLoader"
          :error-action-disabled="errorActionDisabled"
          :error-action-label="errorActionLabel(finalMessage)"
          :image-loader="imageLoader"
          :copy-action="copyAction"
          :copyable="canCopyMessage(finalMessage)"
          :regenerable="canRegenerateMessage(finalMessage)"
          :regenerate-disabled="regenerateDisabled"
          :variant-loading="isVariantLoading(finalMessage)"
          @copy-message="emit('copyMessage', $event)"
          @error-action="emit('errorAction', $event)"
          @regenerate-message="emit('regenerateMessage', $event)"
          @select-next-variant="emit('selectNextVariant', $event)"
          @select-previous-variant="emit('selectPreviousVariant', $event)"
        />
      </div>
      <div
        v-if="artifactItems.length > 0"
        class="chat-timeline-turn__artifacts"
      >
        <ChatTimelineItemView
          v-for="item in artifactItems"
          :key="item.id"
          :artifact-loader="artifactLoader"
          :item="item"
        />
      </div>
    </div>
  </section>
</template>

<style scoped>
.chat-timeline-turn {
  --chat-turn-avatar-size: 28px;

  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: 100%;
  min-width: 0;
}

.chat-timeline-turn__header {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  min-height: var(--chat-turn-avatar-size);
}

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

.chat-timeline-turn__summary {
  display: inline-flex;
  gap: var(--space-1);
  align-items: center;
  min-width: 0;
  padding: 0;
  font-size: 13px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  line-height: 20px;
  color: var(--el-text-color-secondary);
  appearance: none;
  cursor: default;
  background: transparent;
  border: 0;
}

.chat-timeline-turn__summary.is-toggleable {
  cursor: pointer;
}

.chat-timeline-turn__summary.is-toggleable:hover {
  color: var(--el-text-color-primary);
}

.chat-timeline-turn__summary.is-toggleable:focus-visible {
  outline: 2px solid var(--el-color-primary-light-5);
  outline-offset: 3px;
  border-radius: var(--radius-control);
}

.chat-timeline-turn__summary:disabled {
  opacity: 1;
}

.chat-timeline-turn__chevron {
  width: 14px;
  height: 14px;
  fill: none;
  stroke: currentcolor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform var(--motion-duration-fast) var(--motion-ease-standard);
}

.chat-timeline-turn__chevron.is-expanded {
  transform: rotate(90deg);
}

.chat-timeline-turn__content {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
  padding-inline-start: calc(var(--chat-turn-avatar-size) + var(--space-2));
}

.chat-timeline-turn__process {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}

.chat-timeline-turn__artifacts {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

.chat-turn-process-enter-active,
.chat-turn-process-leave-active {
  transition:
    opacity var(--motion-duration-base) var(--motion-ease-standard),
    transform var(--motion-duration-base) var(--motion-ease-standard);
}

.chat-turn-process-enter-from,
.chat-turn-process-leave-to {
  opacity: 0;
  transform: translateY(-4px);
}

@media (prefers-reduced-motion: reduce) {
  .chat-timeline-turn__chevron,
  .chat-turn-process-enter-active,
  .chat-turn-process-leave-active {
    transition: none;
  }
}
</style>
