<script setup lang="ts">
/**
 * Generic chat-message bubble composition.
 *
 * Display-only — renders a single chat bubble in the style of Slack /
 * Discord / iMessage. Side-aligned by role, color-tinted by variant,
 * optional avatar, role badge, timestamp, typing indicator, inline
 * error, and a row of ghost-button actions.
 *
 * No platform-specific integration. Form-data store is not touched.
 *
 *   <ChatMessage
 *     role="assistant"
 *     authorName="Assistant"
 *     message="Hello, how can I help?"
 *     :timestamp="new Date()"
 *   />
 */
import { computed, ref, watch, type Component } from "vue"
import { cva, type VariantProps } from "class-variance-authority"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"

export type ChatMessageAlign = "auto" | "start" | "end"
export type ChatMessageVariant =
  | "default"
  | "muted"
  | "accent"
  | "destructive"

export interface ChatMessageAction {
  id: string
  label: string
  icon?: string
}

export interface ChatMessageProps {
  message?: string
  role?: "user" | "assistant" | "system" | "tool" | string
  authorName?: string
  authorAvatarUrl?: string
  authorAvatarFallback?: string
  timestamp?: string | Date
  pending?: boolean
  error?: string
  actions?: ChatMessageAction[]
  align?: ChatMessageAlign
  variant?: ChatMessageVariant
  allowHtml?: boolean
  classList?: string[] | string
}

const props = withDefaults(defineProps<ChatMessageProps>(), {
  role: "user",
  align: "auto",
  pending: false,
  allowHtml: false,
  actions: () => [],
})

const emit = defineEmits<{
  (e: "action", payload: { id: string }): void
}>()

/* ---------- alignment ---------- */

const resolvedAlign = computed<"start" | "end">(() => {
  if (props.align === "start" || props.align === "end") {
    return props.align
  }
  return props.role === "user" ? "end" : "start"
})

const isEnd = computed<boolean>(() => resolvedAlign.value === "end")

/* ---------- variant ---------- */

const ROLE_VARIANT: Record<string, ChatMessageVariant> = {
  user: "accent",
  assistant: "muted",
  system: "default",
  tool: "muted",
}

const resolvedVariant = computed<ChatMessageVariant>(() => {
  if (props.variant) {
    return props.variant
  }
  return ROLE_VARIANT[props.role] ?? "default"
})

const bubbleVariants = cva(
  "max-w-[36rem] min-w-0 break-words rounded-2xl border px-3 py-2 text-sm shadow-sm",
  {
    variants: {
      variant: {
        default: "bg-card text-card-foreground border-border",
        muted: "bg-muted text-foreground border-transparent",
        accent: "bg-primary text-primary-foreground border-transparent",
        destructive:
          "bg-destructive text-destructive-foreground border-transparent",
      },
      align: {
        start: "rounded-tl-sm",
        end: "rounded-tr-sm",
      },
    },
    defaultVariants: {
      variant: "default",
      align: "start",
    },
  },
)

type BubbleVariants = VariantProps<typeof bubbleVariants>

const bubbleClass = computed<string>(() =>
  bubbleVariants({
    variant: resolvedVariant.value as BubbleVariants["variant"],
    align: resolvedAlign.value,
  }),
)

/* ---------- avatar fallback ---------- */

const derivedAvatarFallback = computed<string>(() => {
  if (props.authorAvatarFallback) {
    return props.authorAvatarFallback
  }
  const source = (props.authorName ?? props.role ?? "?").trim()
  return source ? source.charAt(0).toUpperCase() : "?"
})

/* ---------- timestamp ---------- */

const formattedTimestamp = computed<string>(() => {
  const ts = props.timestamp
  if (!ts) {
    return ""
  }
  const date = ts instanceof Date ? ts : new Date(ts)
  if (Number.isNaN(date.getTime())) {
    return ""
  }
  try {
    return new Intl.DateTimeFormat(undefined, {
      hour: "numeric",
      minute: "2-digit",
    }).format(date)
  } catch {
    return date.toISOString()
  }
})

/* ---------- root class ---------- */

const rootClass = computed<string>(() => {
  const cl = props.classList
  if (!cl) {
    return ""
  }
  return Array.isArray(cl) ? cl.join(" ") : cl
})

/* ---------- role badge ---------- */

const showRoleBadge = computed<boolean>(() => {
  return Boolean(props.role) && props.role !== "user"
})

/* ---------- actions (lucide icons, lazy) ---------- */

const ActionIcons = ref<Record<string, Component | null>>({})

function toPascalCase(input: string): string {
  return input
    .replace(/[-_\s]+(.)?/g, (_, ch?: string) => (ch ? ch.toUpperCase() : ""))
    .replace(/^(.)/, (m) => m.toUpperCase())
}

async function loadActionIcons(actions: ChatMessageAction[]): Promise<void> {
  const needed = actions.filter((a) => a.icon).map((a) => a.icon as string)
  if (needed.length === 0) {
    return
  }
  const mod = await import("@lucide/vue")
  const registry = mod as unknown as Record<string, Component | undefined>
  const next: Record<string, Component | null> = { ...ActionIcons.value }
  for (const raw of needed) {
    const name = toPascalCase(raw)
    next[raw] = registry[name] ?? null
  }
  ActionIcons.value = next
}

watch(
  () => props.actions,
  (next) => {
    void loadActionIcons(next ?? [])
  },
  { immediate: true, deep: true },
)

function onAction(id: string): void {
  emit("action", { id })
}
</script>

<template>
  <div
    :class="
      cn(
        'flex w-full items-end gap-2',
        isEnd ? 'justify-end' : 'justify-start',
        rootClass,
      )
    "
    :data-role="props.role"
    :data-align="resolvedAlign"
    :data-variant="resolvedVariant"
    data-testid="chat-message"
  >
    <!-- left-side avatar -->
    <Avatar
      v-if="!isEnd"
      size="sm"
      class="mb-1"
      data-testid="chat-message-avatar"
    >
      <AvatarImage
        v-if="props.authorAvatarUrl"
        :src="props.authorAvatarUrl"
        :alt="props.authorName ?? props.role"
      />
      <AvatarFallback>{{ derivedAvatarFallback }}</AvatarFallback>
    </Avatar>

    <div
      :class="
        cn(
          'flex min-w-0 flex-col gap-1',
          isEnd ? 'items-end' : 'items-start',
        )
      "
    >
      <!-- bubble -->
      <div :class="bubbleClass" data-testid="chat-message-bubble">
        <!-- header -->
        <div
          v-if="props.authorName || showRoleBadge || formattedTimestamp"
          :class="
            cn(
              'mb-1 flex w-full items-center gap-2 text-xs',
              isEnd ? 'flex-row-reverse' : 'flex-row',
            )
          "
        >
          <span
            v-if="props.authorName"
            class="font-medium"
            data-testid="chat-message-author"
          >
            {{ props.authorName }}
          </span>
          <Badge
            v-if="showRoleBadge"
            variant="outline"
            size="sm"
            data-testid="chat-message-role-badge"
          >
            {{ props.role }}
          </Badge>
          <span
            v-if="formattedTimestamp"
            :class="
              cn(
                'text-[11px] opacity-70',
                resolvedVariant === 'accent' || resolvedVariant === 'destructive'
                  ? ''
                  : 'text-muted-foreground',
                isEnd ? 'mr-auto' : 'ml-auto',
              )
            "
            data-testid="chat-message-timestamp"
          >
            {{ formattedTimestamp }}
          </span>
        </div>

        <!-- body -->
        <div
          v-if="props.pending"
          class="flex items-center gap-1 py-1"
          data-testid="chat-message-pending"
          aria-label="Typing"
        >
          <span class="size-1.5 animate-bounce rounded-full bg-current opacity-60" />
          <span
            class="size-1.5 animate-bounce rounded-full bg-current opacity-60 [animation-delay:150ms]"
          />
          <span
            class="size-1.5 animate-bounce rounded-full bg-current opacity-60 [animation-delay:300ms]"
          />
        </div>
        <div
          v-else-if="props.allowHtml && props.message"
          class="whitespace-pre-wrap leading-snug"
          data-testid="chat-message-body"
          v-html="props.message"
        />
        <div
          v-else-if="props.message"
          class="whitespace-pre-wrap leading-snug"
          data-testid="chat-message-body"
        >
          {{ props.message }}
        </div>
      </div>

      <!-- error -->
      <p
        v-if="props.error"
        class="text-xs text-destructive"
        data-testid="chat-message-error"
      >
        {{ props.error }}
      </p>

      <!-- actions -->
      <div
        v-if="props.actions && props.actions.length > 0"
        :class="
          cn(
            'flex flex-wrap gap-1',
            isEnd ? 'justify-end' : 'justify-start',
          )
        "
        data-testid="chat-message-actions"
      >
        <Button
          v-for="action in props.actions"
          :key="action.id"
          type="button"
          variant="ghost"
          size="sm"
          :data-action-id="action.id"
          @click="onAction(action.id)"
        >
          <component
            :is="action.icon ? ActionIcons[action.icon] : null"
            v-if="action.icon && ActionIcons[action.icon]"
          />
          <span>{{ action.label }}</span>
        </Button>
      </div>
    </div>

    <!-- right-side avatar -->
    <Avatar
      v-if="isEnd"
      size="sm"
      class="mb-1"
      data-testid="chat-message-avatar"
    >
      <AvatarImage
        v-if="props.authorAvatarUrl"
        :src="props.authorAvatarUrl"
        :alt="props.authorName ?? props.role"
      />
      <AvatarFallback>{{ derivedAvatarFallback }}</AvatarFallback>
    </Avatar>
  </div>
</template>
