<script setup lang="ts">
/**
 * Props-driven rich-text editor composition. Wraps Tiptap (`@tiptap/vue-3`
 * + StarterKit) inside a FieldFrame, exposing the standard input chrome:
 * label / required / helpText / errors / hideLabel.
 *
 * The model is HTML — emits `update:modelValue` with `editor.getHTML()` on
 * every internal change.
 *
 *   <RichTextEditor v-model="body" label="Body" />
 */
import { computed, onBeforeUnmount, watch } from "vue"
import { EditorContent, useEditor } from "@tiptap/vue-3"
import type { AnyExtension } from "@tiptap/vue-3"
import StarterKit from "@tiptap/starter-kit"
import {
  Bold,
  Code,
  Heading1,
  Heading2,
  Heading3,
  Italic,
  List,
  ListOrdered,
  Minus,
  Quote,
  Redo,
  Strikethrough,
  Undo,
} from "@lucide/vue"
import { Button } from "@/components/ui/button"
import { FieldFrame } from "@/components/ui/field-frame"
import { cn } from "@/lib/utils"

export interface RichTextEditorProps {
  modelValue?: string | null
  id?: string
  name?: string
  label?: string
  hideLabel?: boolean
  placeholder?: string
  required?: boolean
  readonly?: boolean
  disabled?: boolean
  helpText?: string
  errors?: string[]
  minHeight?: string
  extensions?: unknown[]
}

const props = withDefaults(defineProps<RichTextEditorProps>(), {
  errors: () => [],
  minHeight: "12rem",
  extensions: () => [],
})

const emit = defineEmits<{
  (e: "update:modelValue", value: string): void
}>()

const editor = useEditor({
  content: props.modelValue ?? "",
  editable: !(props.disabled || props.readonly),
  extensions: [
    StarterKit,
    ...(props.extensions as AnyExtension[]),
  ],
  onUpdate: ({ editor }) => {
    emit("update:modelValue", editor.getHTML())
  },
})

watch(
  () => props.modelValue,
  (value) => {
    const instance = editor.value
    if (!instance) {
      return
    }
    const next = value ?? ""
    if (next === instance.getHTML()) {
      return
    }
    instance.commands.setContent(next, { emitUpdate: false })
  },
)

watch(
  () => [props.disabled, props.readonly] as const,
  ([disabled, readonly]) => {
    editor.value?.setEditable(!(disabled || readonly))
  },
)

onBeforeUnmount(() => {
  editor.value?.destroy()
})

const hasErrors = computed<boolean>(() => props.errors.length > 0)
const isDisabled = computed<boolean>(() => Boolean(props.disabled))
const isReadonly = computed<boolean>(() => Boolean(props.readonly))
const isInert = computed<boolean>(() => isDisabled.value || isReadonly.value)

const contentStyle = computed<Record<string, string>>(() => ({
  minHeight: props.minHeight,
}))

interface ToolbarAction {
  key: string
  label: string
  icon: typeof Bold
  isActive: () => boolean
  run: () => void
  disabled?: () => boolean
}

function active(name: string, attrs?: Record<string, unknown>): boolean {
  const instance = editor.value
  if (!instance) {
    return false
  }
  return attrs ? instance.isActive(name, attrs) : instance.isActive(name)
}

const toolbarActions = computed<ToolbarAction[]>(() => [
  {
    key: "bold",
    label: "Bold",
    icon: Bold,
    isActive: () => active("bold"),
    run: () => editor.value?.chain().focus().toggleBold().run(),
  },
  {
    key: "italic",
    label: "Italic",
    icon: Italic,
    isActive: () => active("italic"),
    run: () => editor.value?.chain().focus().toggleItalic().run(),
  },
  {
    key: "strike",
    label: "Strikethrough",
    icon: Strikethrough,
    isActive: () => active("strike"),
    run: () => editor.value?.chain().focus().toggleStrike().run(),
  },
  {
    key: "code",
    label: "Inline code",
    icon: Code,
    isActive: () => active("code"),
    run: () => editor.value?.chain().focus().toggleCode().run(),
  },
  {
    key: "h1",
    label: "Heading 1",
    icon: Heading1,
    isActive: () => active("heading", { level: 1 }),
    run: () =>
      editor.value?.chain().focus().toggleHeading({ level: 1 }).run(),
  },
  {
    key: "h2",
    label: "Heading 2",
    icon: Heading2,
    isActive: () => active("heading", { level: 2 }),
    run: () =>
      editor.value?.chain().focus().toggleHeading({ level: 2 }).run(),
  },
  {
    key: "h3",
    label: "Heading 3",
    icon: Heading3,
    isActive: () => active("heading", { level: 3 }),
    run: () =>
      editor.value?.chain().focus().toggleHeading({ level: 3 }).run(),
  },
  {
    key: "bullet-list",
    label: "Bullet list",
    icon: List,
    isActive: () => active("bulletList"),
    run: () => editor.value?.chain().focus().toggleBulletList().run(),
  },
  {
    key: "ordered-list",
    label: "Ordered list",
    icon: ListOrdered,
    isActive: () => active("orderedList"),
    run: () => editor.value?.chain().focus().toggleOrderedList().run(),
  },
  {
    key: "blockquote",
    label: "Blockquote",
    icon: Quote,
    isActive: () => active("blockquote"),
    run: () => editor.value?.chain().focus().toggleBlockquote().run(),
  },
  {
    key: "horizontal-rule",
    label: "Horizontal rule",
    icon: Minus,
    isActive: () => false,
    run: () => editor.value?.chain().focus().setHorizontalRule().run(),
  },
  {
    key: "undo",
    label: "Undo",
    icon: Undo,
    isActive: () => false,
    run: () => editor.value?.chain().focus().undo().run(),
    disabled: () => !editor.value?.can().chain().focus().undo().run(),
  },
  {
    key: "redo",
    label: "Redo",
    icon: Redo,
    isActive: () => false,
    run: () => editor.value?.chain().focus().redo().run(),
    disabled: () => !editor.value?.can().chain().focus().redo().run(),
  },
])
</script>

<template>
  <FieldFrame
    :label="label"
    :required="required"
    :hide-label="hideLabel"
    :for="id"
    :help-text="helpText"
    :errors="errors"
    data-testid="rich-text-editor"
  >
    <div
      :class="
        cn(
          'rounded-md border border-input bg-background shadow-sm overflow-hidden',
          hasErrors ? 'border-destructive ring-1 ring-destructive' : '',
          isDisabled ? 'opacity-60' : '',
        )
      "
      :data-element-name="name ?? undefined"
    >
      <div
        class="flex flex-wrap items-center gap-1 border-b bg-muted/30 px-2 py-1"
        :class="isInert ? 'pointer-events-none opacity-50' : ''"
        role="toolbar"
        aria-label="Formatting"
        data-testid="rich-text-editor-toolbar"
      >
        <Button
          v-for="action in toolbarActions"
          :key="action.key"
          type="button"
          size="icon-sm"
          :variant="action.isActive() ? 'secondary' : 'ghost'"
          :disabled="isInert || (action.disabled ? action.disabled() : false)"
          :aria-label="action.label"
          :title="action.label"
          :data-testid="`rich-text-editor-toolbar-${action.key}`"
          :aria-pressed="action.isActive()"
          @click="action.run()"
        >
          <component :is="action.icon" />
        </Button>
      </div>

      <EditorContent
        v-if="editor"
        :editor="editor"
        :style="contentStyle"
        class="prose prose-sm max-w-none px-3 py-2 focus-within:outline-none dark:prose-invert [&_.ProseMirror]:min-h-full [&_.ProseMirror]:outline-none"
        data-testid="rich-text-editor-content"
      />
    </div>
  </FieldFrame>
</template>
