<template>
  <div :class="mergedGroupClass">
    <label
      v-if="label"
      :for="editorId"
      :class="mergedLabelClass"
    >
      {{ label }}<MalioRequiredMark v-if="required" />
    </label>

    <!-- Mode lecture seule (rendu uniquement) -->
    <div
      v-if="!editable"
      :id="editorId"
      :class="mergedReadonlyClass"
    >
      <EditorContent :editor="editor" />
    </div>

    <!-- Mode éditable -->
    <div
      v-else
      :id="editorId"
      :class="mergedEditorWrapperClass"
      :aria-required="required || undefined"
      @click="focusEditor"
    >
      <div
        class="flex flex-wrap items-center gap-0.5 border-b border-m-border bg-m-bg p-1"
        @click.stop
      >
        <button
          v-for="btn in toolbarButtons"
          :key="btn.key"
          type="button"
          class="flex h-8 w-8 items-center justify-center rounded text-m-text transition-colors hover:bg-m-primary/10 disabled:cursor-not-allowed disabled:opacity-40"
          :class="btn.isActive() ? 'bg-m-primary/15 text-m-primary' : ''"
          :title="btn.title"
          :disabled="disabled || readonly"
          :aria-label="btn.title"
          :aria-pressed="btn.isActive()"
          @mousedown.prevent
          @click="btn.action()"
        >
          <IconifyIcon :icon="btn.icon" :width="18" :height="18" />
        </button>

        <span class="mx-1 h-5 w-px bg-m-border" aria-hidden="true" />

        <div class="relative">
          <button
            type="button"
            class="flex h-8 w-8 flex-col items-center justify-center rounded text-m-text transition-colors hover:bg-m-primary/10 disabled:cursor-not-allowed disabled:opacity-40"
            :class="colorPickerOpen ? 'bg-m-primary/15 text-m-primary' : ''"
            title="Couleur du texte"
            aria-label="Couleur du texte"
            :aria-expanded="colorPickerOpen"
            :disabled="disabled || readonly"
            @mousedown.prevent
            @click="toggleColorPicker"
          >
            <IconifyIcon icon="mdi:format-color-text" :width="18" :height="18" />
            <span
              class="-mt-0.5 block h-1 w-4 rounded-sm"
              :style="{ backgroundColor: currentTextColor ?? 'transparent' }"
              aria-hidden="true"
            />
          </button>
          <div
            v-if="colorPickerOpen"
            class="absolute left-0 top-full z-10 mt-1 flex w-44 flex-col gap-2 rounded-md border border-m-border bg-white p-2 shadow-lg"
            role="dialog"
            aria-label="Palette couleur du texte"
          >
            <div class="grid grid-cols-4 gap-1">
              <button
                v-for="swatch in textColorSwatches"
                :key="swatch.value"
                type="button"
                class="h-7 w-7 rounded border border-m-border transition-transform hover:scale-110"
                :class="currentTextColor === swatch.value ? 'ring-2 ring-m-primary ring-offset-1' : ''"
                :style="{ backgroundColor: swatch.value }"
                :title="swatch.label"
                :aria-label="swatch.label"
                @mousedown.prevent
                @click="applyTextColor(swatch.value)"
              />
            </div>
            <button
              type="button"
              class="flex items-center justify-center gap-1 rounded border border-m-border px-2 py-1 text-xs text-m-text transition-colors hover:bg-m-bg"
              @mousedown.prevent
              @click="applyTextColor(null)"
            >
              <IconifyIcon icon="mdi:format-color-marker-cancel" :width="14" :height="14" />
              Aucune couleur
            </button>
          </div>
        </div>

        <div class="relative">
          <button
            type="button"
            class="flex h-8 w-8 flex-col items-center justify-center rounded text-m-text transition-colors hover:bg-m-primary/10 disabled:cursor-not-allowed disabled:opacity-40"
            :class="highlightPickerOpen ? 'bg-m-primary/15 text-m-primary' : ''"
            title="Surlignage"
            aria-label="Surlignage"
            :aria-expanded="highlightPickerOpen"
            :disabled="disabled || readonly"
            @mousedown.prevent
            @click="toggleHighlightPicker"
          >
            <IconifyIcon icon="mdi:marker" :width="18" :height="18" />
            <span
              class="-mt-0.5 block h-1 w-4 rounded-sm"
              :style="{ backgroundColor: currentHighlightColor ?? 'transparent' }"
              aria-hidden="true"
            />
          </button>
          <div
            v-if="highlightPickerOpen"
            class="absolute left-0 top-full z-10 mt-1 flex w-44 flex-col gap-2 rounded-md border border-m-border bg-white p-2 shadow-lg"
            role="dialog"
            aria-label="Palette de surlignage"
          >
            <div class="grid grid-cols-4 gap-1">
              <button
                v-for="swatch in highlightSwatches"
                :key="swatch.value"
                type="button"
                class="h-7 w-7 rounded border border-m-border transition-transform hover:scale-110"
                :class="currentHighlightColor === swatch.value ? 'ring-2 ring-m-primary ring-offset-1' : ''"
                :style="{ backgroundColor: swatch.value }"
                :title="swatch.label"
                :aria-label="swatch.label"
                @mousedown.prevent
                @click="applyHighlight(swatch.value)"
              />
            </div>
            <button
              type="button"
              class="flex items-center justify-center gap-1 rounded border border-m-border px-2 py-1 text-xs text-m-text transition-colors hover:bg-m-bg"
              @mousedown.prevent
              @click="applyHighlight(null)"
            >
              <IconifyIcon icon="mdi:format-color-marker-cancel" :width="14" :height="14" />
              Aucun surlignage
            </button>
          </div>
        </div>

        <span class="mx-1 h-5 w-px bg-m-border" aria-hidden="true" />

        <button
          type="button"
          class="flex h-8 w-8 items-center justify-center rounded text-m-text transition-colors hover:bg-m-primary/10 disabled:cursor-not-allowed disabled:opacity-40"
          title="Annuler"
          aria-label="Annuler"
          :disabled="disabled || readonly || !editor?.can().undo()"
          @mousedown.prevent
          @click="editor?.chain().focus().undo().run()"
        >
          <IconifyIcon icon="mdi:undo" :width="18" :height="18" />
        </button>
        <button
          type="button"
          class="flex h-8 w-8 items-center justify-center rounded text-m-text transition-colors hover:bg-m-primary/10 disabled:cursor-not-allowed disabled:opacity-40"
          title="Rétablir"
          aria-label="Rétablir"
          :disabled="disabled || readonly || !editor?.can().redo()"
          @mousedown.prevent
          @click="editor?.chain().focus().redo().run()"
        >
          <IconifyIcon icon="mdi:redo" :width="18" :height="18" />
        </button>
      </div>

      <EditorContent
        :editor="editor"
        class="malio-rich-text flex flex-1 cursor-text"
        :style="{ minHeight }"
        :aria-invalid="hasError || undefined"
        :aria-describedby="describedBy"
      />
    </div>

    <p
      :id="`${editorId}-describedby`"
      :class="[
        hasError
          ? 'text-m-danger'
          : hasSuccess
            ? 'text-m-success'
            : 'text-m-muted',
        'mt-1 text-xs ml-[2px] min-h-[1rem]',
      ]"
    >
      {{ error || success || hint }}
    </p>
  </div>
</template>

<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, shallowRef, useId, watch } from 'vue'
import { Icon as IconifyIcon } from '@iconify/vue'
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Placeholder from '@tiptap/extension-placeholder'
import { TextStyle } from '@tiptap/extension-text-style'
import Color from '@tiptap/extension-color'
import Highlight from '@tiptap/extension-highlight'
import { Markdown } from 'tiptap-markdown'
import { twMerge } from 'tailwind-merge'
import MalioRequiredMark from '../shared/RequiredMark.vue'

defineOptions({ name: 'MalioInputRichText', inheritAttrs: false })

type OutputFormat = 'markdown' | 'html'

const props = withDefaults(
  defineProps<{
    id?: string
    label?: string
    modelValue?: string | null | undefined
    placeholder?: string
    minHeight?: string
    editable?: boolean
    disabled?: boolean
    readonly?: boolean
    hint?: string
    error?: string
    success?: string
    outputFormat?: OutputFormat
    groupClass?: string
    labelClass?: string
    editorClass?: string
    required?: boolean
  }>(),
  {
    id: '',
    label: '',
    modelValue: undefined,
    placeholder: '',
    minHeight: '160px',
    editable: true,
    disabled: false,
    readonly: false,
    hint: '',
    error: '',
    success: '',
    outputFormat: 'html',
    groupClass: '',
    labelClass: '',
    editorClass: '',
    required: false,
  },
)

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

const generatedId = useId()
const editor = shallowRef<Editor>()
const isFocused = shallowRef(false)

const editorId = computed(() => props.id?.toString() || `malio-input-rich-text-${generatedId}`)
const hasError = computed(() => !!props.error)
const hasSuccess = computed(() => !!props.success && !hasError.value)
const isInteractionLocked = computed(() => props.disabled || props.readonly)

const describedBy = computed(() =>
  hasError.value || hasSuccess.value || props.hint ? `${editorId.value}-describedby` : undefined,
)

const mergedGroupClass = computed(() => twMerge('w-full', props.groupClass))

const mergedLabelClass = computed(() =>
  twMerge(
    'mb-1 block text-sm font-medium',
    hasError.value
      ? 'text-m-danger'
      : hasSuccess.value
        ? 'text-m-success'
        : props.disabled
          ? 'text-m-muted'
          : isFocused.value
            ? 'text-m-primary'
            : 'text-m-text',
    props.labelClass,
  ),
)

const mergedEditorWrapperClass = computed(() =>
  twMerge(
    'rich-text-wrapper flex flex-col overflow-hidden rounded-md border bg-white transition-colors',
    hasError.value
      ? 'border-m-danger focus-within:border-m-danger'
      : hasSuccess.value
        ? 'border-m-success focus-within:border-m-success'
        : isFocused.value
          ? 'border-m-primary'
          : 'border-m-muted hover:border-m-text/60',
    props.disabled ? 'cursor-not-allowed bg-m-bg/50 opacity-70' : '',
    props.editorClass,
  ),
)

const mergedReadonlyClass = computed(() =>
  twMerge(
    'malio-rich-text prose prose-sm max-w-none rounded-md border border-m-border bg-white p-3',
    'prose-headings:font-semibold prose-a:text-m-primary',
    'prose-code:rounded prose-code:bg-m-bg prose-code:px-1.5 prose-code:py-0.5 prose-code:before:content-none prose-code:after:content-none',
    'prose-pre:bg-m-text prose-pre:text-white',
    props.editorClass,
  ),
)

const focusEditor = () => {
  if (isInteractionLocked.value) return
  closePickers()
  editor.value?.commands.focus()
}

const htmlPattern = /<\/?[a-z][\s\S]*>/i

const normalizeEditorInput = (value: string | null | undefined): string => {
  const content = (value ?? '').replace(/\r\n?/g, '\n')
  if (htmlPattern.test(content)) return content
  return content.split('\n').join('\n\n').replace(/\n{3,}/g, '\n\n')
}

const promptForLink = () => {
  if (!editor.value) return
  const previous = editor.value.getAttributes('link').href as string | undefined
  const url = window.prompt('URL du lien (vide pour retirer)', previous ?? '')
  if (url === null) return
  if (url === '') {
    editor.value.chain().focus().extendMarkRange('link').unsetLink().run()
    return
  }
  editor.value.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
}

type ColorSwatch = { label: string; value: string }

const textColorSwatches: ColorSwatch[] = [
  { label: 'Rouge', value: '#bf2600' },
  { label: 'Orange', value: '#ff8b00' },
  { label: 'Jaune', value: '#ffc400' },
  { label: 'Vert', value: '#00875a' },
  { label: 'Turquoise', value: '#00a3bf' },
  { label: 'Bleu', value: '#0747a6' },
  { label: 'Violet', value: '#5243aa' },
  { label: 'Gris', value: '#42526e' },
]

const highlightSwatches: ColorSwatch[] = [
  { label: 'Rouge', value: '#fdd0c8' },
  { label: 'Orange', value: '#ffe2c2' },
  { label: 'Jaune', value: '#fff0b3' },
  { label: 'Vert', value: '#c6edd0' },
  { label: 'Turquoise', value: '#c1ecf0' },
  { label: 'Bleu', value: '#cce0ff' },
  { label: 'Violet', value: '#dfd8fa' },
  { label: 'Gris', value: '#dfe1e6' },
]

const colorPickerOpen = ref(false)
const highlightPickerOpen = ref(false)

const closePickers = () => {
  colorPickerOpen.value = false
  highlightPickerOpen.value = false
}

const toggleColorPicker = () => {
  highlightPickerOpen.value = false
  colorPickerOpen.value = !colorPickerOpen.value
}

const toggleHighlightPicker = () => {
  colorPickerOpen.value = false
  highlightPickerOpen.value = !highlightPickerOpen.value
}

const applyTextColor = (value: string | null) => {
  if (!editor.value) return
  if (value === null) {
    editor.value.chain().focus().unsetColor().run()
  } else {
    editor.value.chain().focus().setColor(value).run()
  }
  colorPickerOpen.value = false
}

const applyHighlight = (value: string | null) => {
  if (!editor.value) return
  if (value === null) {
    editor.value.chain().focus().unsetHighlight().run()
  } else {
    editor.value.chain().focus().setHighlight({ color: value }).run()
  }
  highlightPickerOpen.value = false
}

const currentTextColor = computed(() => {
  const attrs = editor.value?.getAttributes('textStyle') as { color?: string } | undefined
  return attrs?.color ?? null
})

const currentHighlightColor = computed(() => {
  const attrs = editor.value?.getAttributes('highlight') as { color?: string } | undefined
  return attrs?.color ?? null
})

const toolbarButtons = computed(() => {
  const e = editor.value
  return [
    { key: 'bold', icon: 'mdi:format-bold', title: 'Gras', isActive: () => !!e?.isActive('bold'), action: () => e?.chain().focus().toggleBold().run() },
    { key: 'italic', icon: 'mdi:format-italic', title: 'Italique', isActive: () => !!e?.isActive('italic'), action: () => e?.chain().focus().toggleItalic().run() },
    { key: 'strike', icon: 'mdi:format-strikethrough', title: 'Barré', isActive: () => !!e?.isActive('strike'), action: () => e?.chain().focus().toggleStrike().run() },
    { key: 'h2', icon: 'mdi:format-header-2', title: 'Titre H2', isActive: () => !!e?.isActive('heading', { level: 2 }), action: () => e?.chain().focus().toggleHeading({ level: 2 }).run() },
    { key: 'h3', icon: 'mdi:format-header-3', title: 'Titre H3', isActive: () => !!e?.isActive('heading', { level: 3 }), action: () => e?.chain().focus().toggleHeading({ level: 3 }).run() },
    { key: 'bulletList', icon: 'mdi:format-list-bulleted', title: 'Liste à puces', isActive: () => !!e?.isActive('bulletList'), action: () => e?.chain().focus().toggleBulletList().run() },
    { key: 'orderedList', icon: 'mdi:format-list-numbered', title: 'Liste numérotée', isActive: () => !!e?.isActive('orderedList'), action: () => e?.chain().focus().toggleOrderedList().run() },
    { key: 'blockquote', icon: 'mdi:format-quote-close', title: 'Citation', isActive: () => !!e?.isActive('blockquote'), action: () => e?.chain().focus().toggleBlockquote().run() },
    { key: 'code', icon: 'mdi:code-tags', title: 'Code inline', isActive: () => !!e?.isActive('code'), action: () => e?.chain().focus().toggleCode().run() },
    { key: 'codeBlock', icon: 'mdi:code-braces-box', title: 'Bloc de code', isActive: () => !!e?.isActive('codeBlock'), action: () => e?.chain().focus().toggleCodeBlock().run() },
    { key: 'link', icon: 'mdi:link-variant', title: 'Lien', isActive: () => !!e?.isActive('link'), action: promptForLink },
  ]
})

const getCurrentValue = (): string => {
  if (!editor.value) return ''
  if (props.outputFormat === 'html') return editor.value.getHTML()
  const storage = (editor.value.storage as unknown as Record<string, { getMarkdown?: () => string } | undefined>).markdown
  return storage?.getMarkdown ? storage.getMarkdown() : editor.value.getHTML()
}

const handleDocumentMousedown = (event: MouseEvent) => {
  if (!colorPickerOpen.value && !highlightPickerOpen.value) return
  const target = event.target as Node | null
  if (!target) return
  const popovers = document.querySelectorAll(`#${editorId.value} [role="dialog"]`)
  const triggers = document.querySelectorAll(`#${editorId.value} [aria-expanded]`)
  for (const node of [...popovers, ...triggers]) {
    if (node.contains(target)) return
  }
  closePickers()
}

const handleDocumentKeydown = (event: KeyboardEvent) => {
  if (event.key === 'Escape' && (colorPickerOpen.value || highlightPickerOpen.value)) {
    closePickers()
  }
}

onMounted(() => {
  document.addEventListener('mousedown', handleDocumentMousedown)
  document.addEventListener('keydown', handleDocumentKeydown)

  editor.value = new Editor({
    content: normalizeEditorInput(props.modelValue),
    editable: props.editable && !props.disabled && !props.readonly,
    extensions: [
      StarterKit.configure({
        heading: { levels: [2, 3] },
        link: {
          openOnClick: false,
          autolink: true,
          HTMLAttributes: { rel: 'noopener noreferrer nofollow', target: '_blank' },
        },
      }),
      TextStyle,
      Color.configure({ types: ['textStyle'] }),
      Highlight.configure({ multicolor: true }),
      Placeholder.configure({
        placeholder: props.placeholder,
      }),
      Markdown.configure({
        html: true,
        tightLists: true,
        bulletListMarker: '-',
        linkify: true,
        breaks: false,
        transformPastedText: true,
        transformCopiedText: true,
      }),
    ],
    editorProps: {
      attributes: {
        class: 'prose prose-sm max-w-none w-full p-3 focus:outline-none prose-headings:font-semibold prose-a:text-m-primary prose-code:rounded prose-code:bg-m-bg prose-code:px-1.5 prose-code:py-0.5 prose-code:before:content-none prose-code:after:content-none prose-pre:bg-m-text prose-pre:text-white',
      },
    },
    onUpdate: () => {
      emit('update:modelValue', getCurrentValue())
    },
    onFocus: () => {
      isFocused.value = true
    },
    onBlur: () => {
      isFocused.value = false
    },
  })
})

onBeforeUnmount(() => {
  document.removeEventListener('mousedown', handleDocumentMousedown)
  document.removeEventListener('keydown', handleDocumentKeydown)
  editor.value?.destroy()
})

watch(() => props.modelValue, (incoming) => {
  if (!editor.value) return
  if ((incoming ?? '') === getCurrentValue()) return
  editor.value.commands.setContent(normalizeEditorInput(incoming), { emitUpdate: false })
})

watch(() => [props.editable, props.disabled, props.readonly], () => {
  editor.value?.setEditable(props.editable && !props.disabled && !props.readonly)
})
</script>
 
<style scoped>
.malio-rich-text :deep(.ProseMirror) {
  outline: none;
  flex: 1 1 auto;
  min-width: 0;
}
.malio-rich-text :deep(.ProseMirror > *:first-child) {
  margin-top: 0;
}
.malio-rich-text :deep(.ProseMirror > *:last-child) {
  margin-bottom: 0;
}
.malio-rich-text :deep(.ProseMirror p.is-editor-empty:first-child::before) {
  content: attr(data-placeholder);
  float: left;
  color: rgb(var(--m-muted));
  pointer-events: none;
  height: 0;
}
.malio-rich-text :deep(h2) {
  margin: 0.75rem 0 0.5rem;
  font-size: 1.5rem;
  line-height: 2rem;
  font-weight: 700;
  color: rgb(var(--m-text));
}
.malio-rich-text :deep(h3) {
  margin: 0.65rem 0 0.4rem;
  font-size: 1.25rem;
  line-height: 1.75rem;
  font-weight: 700;
  color: rgb(var(--m-text));
}
.malio-rich-text :deep(p) {
  margin: 0.45rem 0;
}
.malio-rich-text :deep(ul),
.malio-rich-text :deep(ol) {
  margin: 0.5rem 0;
  padding-left: 1.5rem;
}
.malio-rich-text :deep(ul) {
  list-style: disc;
}
.malio-rich-text :deep(ol) {
  list-style: decimal;
}
.malio-rich-text :deep(blockquote) {
  margin: 0.75rem 0;
  border-left: 3px solid rgb(var(--m-border));
  padding-left: 0.75rem;
  color: rgb(var(--m-muted));
}
</style>
