"use client"

import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import type { Editor } from "@tiptap/react"
import { NodeSelection } from "@tiptap/pm/state"
import { Minus, Plus, X } from "lucide-react"
import { Icon } from "@iconify/react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Checkbox } from "@/components/ui/checkbox"
import { Slider } from "@/components/ui/slider"
import { Textarea } from "@/components/ui/textarea"
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"
import type { DocPageLayout } from "@/lib/drive/doc-page-setup"
import { mmToPx, pxToMm } from "@/lib/drive/doc-page-setup"
import { DOCS_GRAPHIC_WRAP_MARGIN_PRESETS_MM } from "@/lib/drive/docs-graphic-layout"
import {
  DOCS_GRAPHIC_RECOLOR_PRESETS,
  DOCS_GRAPHIC_WRAP_LABELS,
  buildGradientCss,
  parseGraphicAttrs,
  type DocsGraphicAttrs,
  type DocsGraphicPositionMode,
  type DocsGraphicWrap,
  type DocsGradientType,
} from "@/lib/drive/docs-graphic-types"
import {
  DOCS_GRAPHIC_POSITION_MODE_DESCRIPTIONS,
  DOCS_GRAPHIC_POSITION_MODE_SHORT_LABELS,
  DOCS_GRAPHIC_WRAP_DESCRIPTIONS,
  DOCS_GRAPHIC_WRAP_SHORT_LABELS,
  type DocsGraphicOptionsSection,
} from "@/lib/drive/docs-graphic-ui"
import {
  DocsGraphicMarginPreview,
  DocsGraphicPageAnchorPreview,
  DocsGraphicPositionModePreview,
  DocsGraphicWrapPreview,
} from "@/components/drive/richtext/docs-graphic-layout-previews"
import { startDocsGraphicCrop } from "@/lib/drive/docs-graphic-crop-bridge"
import { cn } from "@/lib/utils"

export const DOCS_GRAPHIC_OPTIONS_EVENT = "ultidocs:graphic-options-open"
export const DOCS_GRAPHIC_OPTIONS_SIDEBAR_WIDTH_PX = 320

/** Opens the graphic options sidebar (listened to by the docs workspace). */
export function openDocsGraphicOptionsSidebar(section?: DocsGraphicOptionsSection) {
  window.dispatchEvent(
    new CustomEvent(DOCS_GRAPHIC_OPTIONS_EVENT, { detail: { section } })
  )
}

const IN_FLOW_WRAP_CHOICES: DocsGraphicWrap[] = [
  "inline",
  "square",
  "tight",
  "through",
  "top-bottom",
]

const PAGE_LAYER_WRAP_CHOICES: DocsGraphicWrap[] = ["behind", "in-front"]

function wrapChoicesForPositionMode(
  positionMode: DocsGraphicPositionMode
): DocsGraphicWrap[] {
  if (positionMode === "move-with-text") return IN_FLOW_WRAP_CHOICES
  return PAGE_LAYER_WRAP_CHOICES
}

const QUICK_LAYOUTS: { id: string; label: string; h: 0 | 0.5 | 1; v: 0 | 0.5 | 1 }[] = [
  { id: "tl", label: "En haut à gauche", h: 0, v: 0 },
  { id: "tc", label: "En haut au centre", h: 0.5, v: 0 },
  { id: "tr", label: "En haut à droite", h: 1, v: 0 },
  { id: "ml", label: "Au milieu à gauche", h: 0, v: 0.5 },
  { id: "mc", label: "Au centre", h: 0.5, v: 0.5 },
  { id: "mr", label: "Au milieu à droite", h: 1, v: 0.5 },
  { id: "bl", label: "En bas à gauche", h: 0, v: 1 },
  { id: "bc", label: "En bas au centre", h: 0.5, v: 1 },
  { id: "br", label: "En bas à droite", h: 1, v: 1 },
]

const GRADIENT_ANGLE_PRESETS: { angle: number; label: string; title: string }[] = [
  { angle: 0, label: "→", title: "Gauche à droite" },
  { angle: 90, label: "↓", title: "Haut vers le bas" },
  { angle: 180, label: "←", title: "Droite à gauche" },
  { angle: 270, label: "↑", title: "Bas vers le haut" },
]

function pxToCm(px: number): number {
  return pxToMm(px) / 10
}

function cmToPx(cm: number): number {
  return mmToPx(cm * 10)
}

function roundTo(value: number, decimals: number): number {
  const f = 10 ** decimals
  return Math.round(value * f) / f
}

function readSelectedGraphic(editor: Editor | null) {
  if (!editor) return null
  const name = editor.isActive("docsInlineGraphic")
    ? "docsInlineGraphic"
    : editor.isActive("docsGraphic")
      ? "docsGraphic"
      : null
  if (!name) return null
  return parseGraphicAttrs(editor.getAttributes(name) as Record<string, unknown>)
}

function readNaturalSize(): { width: number; height: number } | null {
  const img = document.querySelector(
    ".docs-graphic--selected img, .ProseMirror-selectednode .docs-graphic img"
  ) as HTMLImageElement | null
  if (!img || !img.naturalWidth || !img.naturalHeight) return null
  return { width: img.naturalWidth, height: img.naturalHeight }
}

/**
 * Numeric field with commit-on-blur/Enter so typing isn't interrupted by
 * editor transactions re-feeding the value.
 */
function NumberField({
  label,
  value,
  unit,
  step = 0.01,
  min,
  max,
  decimals = 2,
  onCommit,
  disabled,
  showSteppers = true,
}: {
  label: string
  value: number
  unit: string
  step?: number
  min?: number
  max?: number
  decimals?: number
  onCommit: (value: number) => void
  disabled?: boolean
  showSteppers?: boolean
}) {
  const display = String(roundTo(value, decimals))
  const [draft, setDraft] = useState(display)
  const [focused, setFocused] = useState(false)

  useEffect(() => {
    if (!focused) setDraft(display)
  }, [display, focused])

  const clamp = (parsed: number) => {
    let next = parsed
    if (min != null) next = Math.max(min, next)
    if (max != null) next = Math.min(max, next)
    return next
  }

  const commit = () => {
    const parsed = Number.parseFloat(draft.replace(",", "."))
    if (!Number.isFinite(parsed)) {
      setDraft(display)
      return
    }
    const next = clamp(parsed)
    onCommit(next)
    setDraft(String(roundTo(next, decimals)))
  }

  const stepBy = (delta: number) => {
    const next = clamp(value + delta)
    onCommit(next)
    setDraft(String(roundTo(next, decimals)))
  }

  const handleStepperPointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {
    event.preventDefault()
  }

  return (
    <div className="flex min-w-0 flex-1 flex-col gap-1">
      <span className="text-xs text-muted-foreground">
        {label} <span className="text-[10px]">({unit})</span>
      </span>
      <div className="flex items-center gap-1">
        {showSteppers ? (
          <Button
            type="button"
            variant="outline"
            size="icon"
            className="docs-graphic-options-stepper size-8 shrink-0"
            disabled={disabled}
            aria-label={`Diminuer ${label}`}
            onPointerDown={handleStepperPointerDown}
            onClick={() => stepBy(-step)}
          >
            <Minus className="size-3.5" />
          </Button>
        ) : null}
        <Input
          type="text"
          inputMode="decimal"
          value={draft}
          disabled={disabled}
          className="docs-graphic-options-number-input h-8 min-w-0 flex-1 px-1 text-center text-sm tabular-nums"
          onFocus={() => setFocused(true)}
          onChange={(event) => setDraft(event.target.value)}
          onBlur={() => {
            setFocused(false)
            commit()
          }}
          onKeyDown={(event) => {
            if (event.key === "Enter") {
              event.preventDefault()
              commit()
              return
            }
            if (event.key === "ArrowUp") {
              event.preventDefault()
              stepBy(event.shiftKey ? step * 10 : step)
              return
            }
            if (event.key === "ArrowDown") {
              event.preventDefault()
              stepBy(event.shiftKey ? -step * 10 : -step)
            }
          }}
        />
        {showSteppers ? (
          <Button
            type="button"
            variant="outline"
            size="icon"
            className="docs-graphic-options-stepper size-8 shrink-0"
            disabled={disabled}
            aria-label={`Augmenter ${label}`}
            onPointerDown={handleStepperPointerDown}
            onClick={() => stepBy(step)}
          >
            <Plus className="size-3.5" />
          </Button>
        ) : null}
      </div>
    </div>
  )
}

function AdjustmentSlider({
  label,
  value,
  min,
  max,
  onChange,
}: {
  label: string
  value: number
  min: number
  max: number
  onChange: (value: number) => void
}) {
  return (
    <div className="flex flex-col gap-1.5">
      <div className="flex items-center justify-between">
        <span className="text-xs text-muted-foreground">{label}</span>
        <span className="text-xs tabular-nums text-muted-foreground">{value}</span>
      </div>
      <Slider
        value={[value]}
        min={min}
        max={max}
        step={1}
        onValueChange={(values) => onChange(values[0] ?? value)}
      />
    </div>
  )
}

function DocsGraphicOptionsSidebarInner({
  editor,
  pageLayout,
  open,
  focusSection,
  onClose,
}: {
  editor: Editor | null
  pageLayout: DocPageLayout
  open: boolean
  focusSection?: DocsGraphicOptionsSection | null
  onClose: () => void
}) {
  const [, setTick] = useState(0)
  const graphicSelectionPos = useRef<number | null>(null)
  const [expandedSections, setExpandedSections] = useState<string[]>([
    "size",
    "position",
    "wrap",
    "margin",
  ])

  const refresh = useCallback(() => setTick((t) => t + 1), [])

  useEffect(() => {
    if (!open || !focusSection) return
    setExpandedSections((prev) =>
      prev.includes(focusSection) ? prev : [...prev, focusSection]
    )
    requestAnimationFrame(() => {
      document
        .querySelector(`[data-graphic-section="${focusSection}"]`)
        ?.scrollIntoView({ block: "nearest", behavior: "smooth" })
    })
  }, [focusSection, open])

  useEffect(() => {
    if (!editor) return
    editor.on("selectionUpdate", refresh)
    editor.on("transaction", refresh)
    return () => {
      editor.off("selectionUpdate", refresh)
      editor.off("transaction", refresh)
    }
  }, [editor, refresh])

  const attrs = readSelectedGraphic(editor)
  const isImage = attrs?.graphicType === "image"
  const isGradient = attrs?.graphicType === "gradient"

  useEffect(() => {
    if (!open || !isGradient) return
    setExpandedSections((prev) => (prev.includes("gradient") ? prev : [...prev, "gradient"]))
  }, [open, isGradient])

  useEffect(() => {
    if (!editor) return
    const { selection } = editor.state
    if (!(selection instanceof NodeSelection)) return
    const node = selection.node
    if (node.type.name !== "docsGraphic" && node.type.name !== "docsInlineGraphic") return
    graphicSelectionPos.current = selection.from
  }, [editor, attrs])
  const natural = useMemo(
    () => (open && isImage ? readNaturalSize() : null),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [open, isImage, attrs?.src]
  )

  if (!open) return null

  const apply = (command: () => boolean | void) => {
    if (!editor) return
    command()
  }

  const update = (patch: Partial<DocsGraphicAttrs>) => {
    if (!editor) return
    const chain = editor.chain()
    const pos = graphicSelectionPos.current
    if (pos != null) chain.setNodeSelection(pos)
    chain.updateDocsGraphic(patch).run()
  }

  const updateGradient = (
    patch: Partial<
      Pick<
        DocsGraphicAttrs,
        "gradientType" | "gradientAngle" | "gradientColor1" | "gradientColor2"
      >
    >
  ) => {
    if (!attrs) return
    const gradientType = patch.gradientType ?? attrs.gradientType
    const gradientAngle = patch.gradientAngle ?? attrs.gradientAngle
    const gradientColor1 = patch.gradientColor1 ?? attrs.gradientColor1
    const gradientColor2 = patch.gradientColor2 ?? attrs.gradientColor2
    update({
      ...patch,
      gradientCss: buildGradientCss(
        gradientAngle,
        gradientColor1,
        gradientColor2,
        gradientType
      ),
    })
  }

  const lockAspect = attrs?.lockAspectRatio !== false

  const setWidth = (cm: number) => {
    if (!attrs) return
    const width = Math.max(8, cmToPx(cm))
    const patch: Partial<DocsGraphicAttrs> = { width }
    if (lockAspect && attrs.width > 0) {
      patch.height = Math.max(8, attrs.height * (width / attrs.width))
    }
    update(patch)
  }

  const setHeight = (cm: number) => {
    if (!attrs) return
    const height = Math.max(8, cmToPx(cm))
    const patch: Partial<DocsGraphicAttrs> = { height }
    if (lockAspect && attrs.height > 0) {
      patch.width = Math.max(8, attrs.width * (height / attrs.height))
    }
    update(patch)
  }

  const setScaleWidth = (pct: number) => {
    if (!attrs || !natural) return
    const width = Math.max(8, (natural.width * pct) / 100)
    const patch: Partial<DocsGraphicAttrs> = { width }
    if (lockAspect) patch.height = Math.max(8, (natural.height * pct) / 100)
    update(patch)
  }

  const setScaleHeight = (pct: number) => {
    if (!attrs || !natural) return
    const height = Math.max(8, (natural.height * pct) / 100)
    const patch: Partial<DocsGraphicAttrs> = { height }
    if (lockAspect) patch.width = Math.max(8, (natural.width * pct) / 100)
    update(patch)
  }

  const applyQuickLayout = (h: number, v: number) => {
    if (!attrs) return
    const margins = pageLayout.marginsPx
    const innerW = pageLayout.widthPx - margins.left - margins.right
    const innerH = pageLayout.heightPx - margins.top - margins.bottom
    update({
      positionMode: "fixed-on-page",
      pageX: margins.left + (innerW - attrs.width) * h,
      pageY: margins.top + (innerH - attrs.height) * v,
    })
  }

  const sidebar = (
    <aside
      className="docs-graphic-options-sidebar flex h-full shrink-0 flex-col border-l border-border bg-background"
      style={{ width: DOCS_GRAPHIC_OPTIONS_SIDEBAR_WIDTH_PX }}
      aria-label="Options du graphique"
      onMouseDownCapture={(event) => {
        const target = event.target as HTMLElement
        if (target.closest("input, textarea, button, [role=slider]")) return
        event.preventDefault()
      }}
    >
      <div className="flex items-center justify-between border-b border-border px-4 py-2.5">
        <h2 className="text-sm font-medium">
          {isImage
            ? "Options de l'image"
            : isGradient
              ? "Options du dégradé"
              : "Options du graphique"}
        </h2>
        <Button
          type="button"
          variant="ghost"
          size="icon"
          className="size-7"
          aria-label="Fermer"
          onClick={onClose}
        >
          <X className="size-4" />
        </Button>
      </div>

      {!attrs ? (
        <p className="px-4 py-6 text-sm text-muted-foreground">
          Sélectionnez une image ou un graphisme pour afficher ses options.
        </p>
      ) : (
        <div className="docs-graphic-options-sidebar__scroll min-h-0 flex-1 overflow-y-auto">
          <Accordion
            type="multiple"
            value={expandedSections}
            onValueChange={setExpandedSections}
            className="px-1"
          >
            <AccordionItem value="size" data-graphic-section="size">
              <AccordionTrigger className="px-3 py-3 text-sm">
                Taille et rotation
              </AccordionTrigger>
              <AccordionContent className="flex flex-col gap-3 px-3">
                <div className="flex gap-2">
                  <NumberField
                    label="Largeur"
                    unit="cm"
                    value={pxToCm(attrs.width)}
                    min={0.2}
                    step={0.1}
                    onCommit={setWidth}
                  />
                  <NumberField
                    label="Hauteur"
                    unit="cm"
                    value={pxToCm(attrs.height)}
                    min={0.2}
                    step={0.1}
                    onCommit={setHeight}
                  />
                </div>
                {natural ? (
                  <div className="flex gap-2">
                    <NumberField
                      label="Échelle de largeur"
                      unit="%"
                      value={(attrs.width / natural.width) * 100}
                      min={1}
                      decimals={0}
                      step={1}
                      onCommit={setScaleWidth}
                    />
                    <NumberField
                      label="Échelle de hauteur"
                      unit="%"
                      value={(attrs.height / natural.height) * 100}
                      min={1}
                      decimals={0}
                      step={1}
                      onCommit={setScaleHeight}
                    />
                  </div>
                ) : null}
                <label className="flex items-center gap-2 text-sm">
                  <Checkbox
                    checked={lockAspect}
                    onCheckedChange={(checked) =>
                      update({ lockAspectRatio: checked === true })
                    }
                  />
                  Verrouiller le format
                </label>
                {isImage ? (
                  <div className="flex flex-col gap-2">
                    <span className="text-xs text-muted-foreground">Ajustement dans le cadre</span>
                    <div className="grid grid-cols-3 gap-2">
                      {(
                        [
                          { fit: "contain" as const, label: "Contenir" },
                          { fit: "cover" as const, label: "Remplir" },
                          { fit: "crop" as const, label: "Recadrer" },
                        ] as const
                      ).map(({ fit, label }) => (
                        <button
                          key={fit}
                          type="button"
                          className={cn(
                            "rounded-md border px-2 py-1.5 text-xs font-medium",
                            attrs.imageFit === fit
                              ? "border-primary bg-primary/5 text-primary"
                              : "border-border hover:border-muted-foreground/40 hover:bg-accent/30"
                          )}
                          onClick={() => {
                            if (fit === "crop") {
                              apply(() => {
                                editor.chain().updateDocsGraphic({ imageFit: "crop" }).run()
                                startDocsGraphicCrop()
                              })
                              return
                            }
                            update({ imageFit: fit })
                          }}
                        >
                          {label}
                        </button>
                      ))}
                    </div>
                    {attrs.imageFit === "crop" ? (
                      <Button
                        type="button"
                        variant="outline"
                        size="sm"
                        className="h-8 w-full text-xs"
                        onClick={() => apply(() => startDocsGraphicCrop())}
                      >
                        Ajuster le recadrage
                      </Button>
                    ) : null}
                    {attrs.imageFit === "cover" ? (
                      <div className="flex flex-col gap-1.5">
                        <span className="text-xs text-muted-foreground">Point focal</span>
                        <div className="grid w-fit grid-cols-3 gap-1.5">
                          {QUICK_LAYOUTS.map((layout) => (
                            <button
                              key={layout.id}
                              type="button"
                              className="rounded-md"
                              aria-label={layout.label}
                              title={layout.label}
                              onClick={() =>
                                update({
                                  imageFitAnchorH: layout.h,
                                  imageFitAnchorV: layout.v,
                                })
                              }
                            >
                              <DocsGraphicPageAnchorPreview
                                h={layout.h}
                                v={layout.v}
                                active={
                                  attrs.imageFitAnchorH === layout.h &&
                                  attrs.imageFitAnchorV === layout.v
                                }
                              />
                            </button>
                          ))}
                        </div>
                      </div>
                    ) : null}
                  </div>
                ) : null}
                <div className="flex items-end gap-2">
                  <NumberField
                    label="Angle de rotation"
                    unit="°"
                    value={attrs.rotationDeg}
                    decimals={0}
                    step={1}
                    onCommit={(deg) => update({ rotationDeg: ((deg % 360) + 360) % 360 })}
                  />
                  <Button
                    type="button"
                    variant="outline"
                    size="sm"
                    className="h-8 shrink-0 gap-1.5"
                    onClick={() =>
                      update({ rotationDeg: (attrs.rotationDeg + 90) % 360 })
                    }
                  >
                    <Icon icon="material-symbols:rotate-90-degrees-cw-outline" className="size-4" />
                    90°
                  </Button>
                </div>
              </AccordionContent>
            </AccordionItem>

            {isGradient ? (
              <AccordionItem value="gradient" data-graphic-section="gradient">
                <AccordionTrigger className="px-3 py-3 text-sm">Dégradé</AccordionTrigger>
                <AccordionContent className="flex flex-col gap-3 px-3">
                  <div
                    className="h-16 w-full rounded-md border border-border"
                    style={{ background: attrs.gradientCss }}
                    aria-hidden
                  />
                  <div className="flex flex-col gap-1.5">
                    <span className="text-xs text-muted-foreground">Type</span>
                    <div className="grid grid-cols-2 gap-2">
                      {(
                        [
                          { type: "linear" as const, label: "Linéaire" },
                          { type: "radial" as const, label: "Radial" },
                        ] satisfies { type: DocsGradientType; label: string }[]
                      ).map(({ type, label }) => (
                        <button
                          key={type}
                          type="button"
                          className={cn(
                            "rounded-md border px-2 py-1.5 text-xs font-medium",
                            attrs.gradientType === type
                              ? "border-primary bg-primary/5 text-primary"
                              : "border-border hover:border-muted-foreground/40 hover:bg-accent/30"
                          )}
                          onClick={() => updateGradient({ gradientType: type })}
                        >
                          {label}
                        </button>
                      ))}
                    </div>
                  </div>
                  <div className="grid grid-cols-2 gap-2">
                    <label className="flex flex-col gap-1">
                      <span className="text-xs text-muted-foreground">Couleur 1</span>
                      <Input
                        type="color"
                        className="h-9 cursor-pointer p-1"
                        value={attrs.gradientColor1}
                        onChange={(event) =>
                          updateGradient({ gradientColor1: event.target.value })
                        }
                      />
                    </label>
                    <label className="flex flex-col gap-1">
                      <span className="text-xs text-muted-foreground">Couleur 2</span>
                      <Input
                        type="color"
                        className="h-9 cursor-pointer p-1"
                        value={attrs.gradientColor2}
                        onChange={(event) =>
                          updateGradient({ gradientColor2: event.target.value })
                        }
                      />
                    </label>
                  </div>
                  {attrs.gradientType === "linear" ? (
                    <>
                      <div className="flex flex-col gap-1.5">
                        <span className="text-xs text-muted-foreground">Orientation</span>
                        <div className="grid grid-cols-4 gap-1.5">
                          {GRADIENT_ANGLE_PRESETS.map(({ angle, label, title }) => (
                            <button
                              key={angle}
                              type="button"
                              className={cn(
                                "rounded-md border px-2 py-1.5 text-sm font-medium",
                                attrs.gradientAngle === angle
                                  ? "border-primary bg-primary/5 text-primary"
                                  : "border-border hover:border-muted-foreground/40 hover:bg-accent/30"
                              )}
                              aria-label={title}
                              title={title}
                              onClick={() => updateGradient({ gradientAngle: angle })}
                            >
                              {label}
                            </button>
                          ))}
                        </div>
                      </div>
                      <NumberField
                        label="Angle"
                        unit="°"
                        value={attrs.gradientAngle}
                        decimals={0}
                        step={1}
                        min={0}
                        max={360}
                        onCommit={(deg) =>
                          updateGradient({ gradientAngle: ((deg % 360) + 360) % 360 })
                        }
                      />
                    </>
                  ) : (
                    <p className="text-xs text-muted-foreground">
                      Le dégradé radial part du centre vers les bords.
                    </p>
                  )}
                </AccordionContent>
              </AccordionItem>
            ) : null}

            <AccordionItem value="position" data-graphic-section="position">
              <AccordionTrigger className="px-3 py-3 text-sm">Position</AccordionTrigger>
              <AccordionContent className="flex flex-col gap-3 px-3">
                <div className="flex flex-col gap-2">
                  {(["move-with-text", "fixed-on-page"] as DocsGraphicPositionMode[]).map(
                    (mode) => (
                      <button
                        key={mode}
                        type="button"
                        className={cn(
                          "docs-graphic-option-card flex w-full flex-col gap-2 rounded-lg border p-2 text-left",
                          attrs.positionMode === mode
                            ? "border-primary bg-primary/5 ring-1 ring-primary/30"
                            : "border-border hover:border-muted-foreground/40 hover:bg-accent/30"
                        )}
                        onClick={() =>
                          apply(() => editor.chain().setDocsGraphicPositionMode(mode).run())
                        }
                      >
                        <DocsGraphicPositionModePreview mode={mode} />
                        <span className="text-sm font-medium">
                          {DOCS_GRAPHIC_POSITION_MODE_SHORT_LABELS[mode]}
                        </span>
                        <span className="text-xs leading-snug text-muted-foreground">
                          {DOCS_GRAPHIC_POSITION_MODE_DESCRIPTIONS[mode]}
                        </span>
                      </button>
                    )
                  )}
                </div>
                {attrs.positionMode === "fixed-on-page" ? (
                  <>
                    <div className="flex flex-col gap-2">
                      <span className="text-xs text-muted-foreground">
                        Placer l&apos;image sur la page
                      </span>
                      <div className="grid w-fit grid-cols-3 gap-1.5">
                        {QUICK_LAYOUTS.map((layout) => (
                          <button
                            key={layout.id}
                            type="button"
                            className="rounded-md"
                            aria-label={layout.label}
                            title={layout.label}
                            onClick={() => applyQuickLayout(layout.h, layout.v)}
                          >
                            <DocsGraphicPageAnchorPreview h={layout.h} v={layout.v} />
                          </button>
                        ))}
                      </div>
                    </div>
                    <div className="flex gap-2">
                      <NumberField
                        label="X"
                        unit="cm"
                        value={pxToCm(attrs.pageX)}
                        step={0.1}
                        onCommit={(cm) => update({ pageX: cmToPx(cm) })}
                      />
                      <NumberField
                        label="Y"
                        unit="cm"
                        value={pxToCm(attrs.pageY)}
                        step={0.1}
                        onCommit={(cm) => update({ pageY: cmToPx(cm) })}
                      />
                    </div>
                    <p className="text-xs text-muted-foreground">
                      Distance depuis l&apos;angle supérieur gauche de la page.
                    </p>
                  </>
                ) : null}
              </AccordionContent>
            </AccordionItem>

            {wrapChoicesForPositionMode(attrs.positionMode).length > 0 ? (
            <AccordionItem value="wrap" data-graphic-section="wrap">
              <AccordionTrigger className="px-3 py-3 text-sm">
                {attrs.positionMode === "move-with-text"
                  ? "Habillage du texte"
                  : "Calque par rapport au texte"}
              </AccordionTrigger>
              <AccordionContent className="px-3">
                <div className="grid grid-cols-2 gap-2">
                  {wrapChoicesForPositionMode(attrs.positionMode).map((wrap) => (
                    <button
                      key={wrap}
                      type="button"
                      className={cn(
                        "docs-graphic-option-card flex flex-col gap-1.5 rounded-lg border p-2 text-left",
                        attrs.wrap === wrap
                          ? "border-primary bg-primary/5 ring-1 ring-primary/30"
                          : "border-border hover:border-muted-foreground/40 hover:bg-accent/30"
                      )}
                      aria-label={DOCS_GRAPHIC_WRAP_LABELS[wrap]}
                      title={DOCS_GRAPHIC_WRAP_LABELS[wrap]}
                      onClick={() =>
                        apply(() => editor.chain().setDocsGraphicWrap(wrap).run())
                      }
                    >
                      <DocsGraphicWrapPreview wrap={wrap} />
                      <span className="text-xs font-medium leading-tight">
                        {DOCS_GRAPHIC_WRAP_SHORT_LABELS[wrap]}
                      </span>
                    </button>
                  ))}
                </div>
              </AccordionContent>
            </AccordionItem>
            ) : null}

            <AccordionItem value="margin" data-graphic-section="margin">
              <AccordionTrigger className="px-3 py-3 text-sm">
                Marges par rapport au texte
              </AccordionTrigger>
              <AccordionContent className="flex flex-col gap-2 px-3">
                <p className="text-xs text-muted-foreground">
                  Espace entre l&apos;image et le texte qui l&apos;entoure.
                </p>
                <div className="grid grid-cols-2 gap-2">
                  {DOCS_GRAPHIC_WRAP_MARGIN_PRESETS_MM.map((mm) => (
                    <button
                      key={mm}
                      type="button"
                      className={cn(
                        "docs-graphic-option-card flex flex-col gap-2 rounded-lg border p-2 text-left",
                        attrs.wrapMarginMm === mm
                          ? "border-primary bg-primary/5 ring-1 ring-primary/30"
                          : "border-border hover:border-muted-foreground/40 hover:bg-accent/30"
                      )}
                      onClick={() =>
                        apply(() => editor.chain().setDocsGraphicWrapMargin(mm).run())
                      }
                    >
                      <DocsGraphicMarginPreview mm={mm} />
                      <span className="text-sm font-medium">Marge de {mm} mm</span>
                    </button>
                  ))}
                </div>
              </AccordionContent>
            </AccordionItem>

            {isImage ? (
              <AccordionItem value="recolor" data-graphic-section="recolor">
                <AccordionTrigger className="px-3 py-3 text-sm">
                  Recolorier
                </AccordionTrigger>
                <AccordionContent className="px-3">
                  <div className="grid grid-cols-4 gap-1.5">
                    {DOCS_GRAPHIC_RECOLOR_PRESETS.map((preset) => (
                      <button
                        key={preset.id || "none"}
                        type="button"
                        className={cn(
                          "h-12 overflow-hidden rounded-md border",
                          attrs.recolor === preset.id
                            ? "border-primary ring-2 ring-primary/40"
                            : "border-border hover:border-muted-foreground/60"
                        )}
                        aria-label={preset.label}
                        title={preset.label}
                        onClick={() => update({ recolor: preset.id })}
                      >
                        {attrs.src ? (
                          // eslint-disable-next-line @next/next/no-img-element
                          <img
                            src={attrs.src}
                            alt=""
                            className="h-full w-full object-cover"
                            style={{ filter: preset.filter || undefined }}
                          />
                        ) : (
                          <span className="block h-full w-full bg-muted" />
                        )}
                      </button>
                    ))}
                  </div>
                </AccordionContent>
              </AccordionItem>
            ) : null}

            <AccordionItem value="adjust" data-graphic-section="adjust">
              <AccordionTrigger className="px-3 py-3 text-sm">
                Ajustements
              </AccordionTrigger>
              <AccordionContent className="flex flex-col gap-4 px-3">
                <AdjustmentSlider
                  label="Transparence"
                  value={Math.round((1 - attrs.opacity) * 100)}
                  min={0}
                  max={100}
                  onChange={(value) => update({ opacity: 1 - value / 100 })}
                />
                {isImage ? (
                  <>
                    <AdjustmentSlider
                      label="Luminosité"
                      value={Math.round(attrs.brightness * 100)}
                      min={-100}
                      max={100}
                      onChange={(value) => update({ brightness: value / 100 })}
                    />
                    <AdjustmentSlider
                      label="Contraste"
                      value={Math.round(attrs.contrast * 100)}
                      min={-100}
                      max={100}
                      onChange={(value) => update({ contrast: value / 100 })}
                    />
                  </>
                ) : null}
                <Button
                  type="button"
                  variant="outline"
                  size="sm"
                  className="h-8 w-fit"
                  onClick={() =>
                    update({ opacity: 1, brightness: 0, contrast: 0, recolor: "" })
                  }
                >
                  Rétablir
                </Button>
              </AccordionContent>
            </AccordionItem>

            <AccordionItem value="alt" data-graphic-section="alt" className="border-b-0">
              <AccordionTrigger className="px-3 py-3 text-sm">
                Texte alternatif
              </AccordionTrigger>
              <AccordionContent className="flex flex-col gap-3 px-3">
                <label className="flex flex-col gap-1">
                  <span className="text-xs text-muted-foreground">Titre</span>
                  <Input
                    value={attrs.altTitle}
                    className="h-8 text-sm"
                    onChange={(event) => update({ altTitle: event.target.value })}
                  />
                </label>
                <label className="flex flex-col gap-1">
                  <span className="text-xs text-muted-foreground">Description</span>
                  <Textarea
                    value={attrs.alt}
                    rows={3}
                    className="text-sm"
                    onChange={(event) => update({ alt: event.target.value })}
                  />
                </label>
              </AccordionContent>
            </AccordionItem>
          </Accordion>
        </div>
      )}
    </aside>
  )

  return sidebar
}

export const DocsGraphicOptionsSidebar = memo(DocsGraphicOptionsSidebarInner)
