<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'
import { MESSAGE_LIST_PIN_THRESHOLD_PX } from '../lib/constants'
import { formatMessage, messages } from '../lib/messages'
import type { AssistantTurn, ChatTurn } from '../lib/models'
import AssistantMessage from './AssistantMessage.vue'
import EmptyState from './EmptyState.vue'
import UserMessage from './UserMessage.vue'

const props = defineProps<{
  turns: ChatTurn[]
  suggestions: readonly string[]
  canSave: boolean
}>()
defineEmits<{
  pick: [suggestion: string]
  save: [turnId: string]
  retry: [query: string]
}>()

const list = ref<HTMLElement | null>(null)
const pinned = ref(true)
function turnFingerprint(turn: ChatTurn): string {
  if (turn.role === 'user') return turn.id
  return `${turn.id}:${turn.tracks.length}:${turn.warnings.length}:${turn.status}`
}

function findLatestAssistant(): AssistantTurn | undefined {
  for (let index = props.turns.length - 1; index >= 0; index -= 1) {
    const turn = props.turns[index]
    if (turn?.role === 'assistant') return turn
  }
  return undefined
}

const fingerprint = computed(() => props.turns.map(turnFingerprint).join('|'))
const streaming = computed(() =>
  props.turns.some((turn) => turn.role === 'assistant' && turn.status === 'streaming'),
)
const latestAssistant = computed(findLatestAssistant)
const announcement = computed(() => {
  const turn = latestAssistant.value
  if (!turn) return ''
  if (turn.status === 'error') return messages.messageListRequestFailed
  if (turn.status === 'done') {
    const trackCount = turn.completion?.track_count ?? turn.tracks.length
    return formatMessage('messageListTracksReady', { count: trackCount })
  }
  const warning = turn.warnings.at(-1)
  if (warning) {
    return formatMessage('messageListWarning', {
      count: turn.warnings.length,
      message: warning.message,
    })
  }
  if (turn.requestId === null) return messages.messageListReadingRequest
  if (turn.tracks.length === 0) return messages.messageListCheckingCandidates
  return formatMessage('messageListVerifiedTracksReceived', { count: turn.tracks.length })
})

function onScroll(): void {
  const element = list.value
  if (!element) return
  const distanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight
  pinned.value = distanceFromBottom < MESSAGE_LIST_PIN_THRESHOLD_PX
}

async function scrollToLatest(): Promise<void> {
  if (!pinned.value) return
  await nextTick()
  requestAnimationFrame(() => {
    if (list.value) list.value.scrollTop = list.value.scrollHeight
  })
}

watch(fingerprint, scrollToLatest, { flush: 'post' })
watch(streaming, (value) => {
  if (value) pinned.value = true
})
</script>

<template>
  <main
    ref="list"
    class="list"
    :aria-label="messages.messageListConversationLabel"
    @scroll="onScroll"
  >
    <p class="visually-hidden" aria-live="polite" aria-atomic="true">
      {{ announcement }}
    </p>
    <div class="column">
      <EmptyState
        v-if="turns.length === 0"
        :suggestions="suggestions"
        @pick="$emit('pick', $event)"
      />
      <div v-for="turn in turns" :key="turn.id" class="turn">
        <UserMessage v-if="turn.role === 'user'" :text="turn.text" />
        <AssistantMessage
          v-else
          :turn="turn"
          :can-save="canSave"
          @save="$emit('save', turn.id)"
          @retry="$emit('retry', $event)"
        />
      </div>
    </div>
  </main>
</template>

<style scoped>
.list {
  flex: 1 1 auto;
  min-height: 0;
  padding: 32px var(--gutter) 28px;
  overflow-y: auto;
}

.column {
  display: flex;
  flex-direction: column;
  gap: var(--s-9);
  max-width: var(--measure);
  margin: 0 auto;
}

.turn {
  animation: message-in var(--dur-message) ease both;
}

@keyframes message-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

@media (max-width: 560px) {
  .list {
    padding-top: 24px;
  }
}
</style>
