import {
  BrushIcon,
  CheckIcon,
  ChevronDownIcon,
  EraserIcon,
  LassoIcon,
  PencilIcon,
  Redo2Icon,
  Trash2Icon,
  Undo2Icon,
  XIcon,
} from 'lucide-react'
import { useEffect, useId, useRef, useState } from 'react'
import type { ReactElement, ReactNode } from 'react'
import { HexColorInput, HexColorPicker } from 'react-colorful'

import { FieldError } from '@/components/FieldError'
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import { Label } from '@/components/ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Separator } from '@/components/ui/separator'
import { Slider } from '@/components/ui/slider'
import { Textarea } from '@/components/ui/textarea'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { formatNumber } from '@/lib/format'
import { t } from '@/lib/i18n'
import type { TranslationKey } from '@/lib/i18n'
import { cn } from '@/lib/utils'

import { MarkupCanvas } from './MarkupCanvas'
import type { MarkupCanvasHandle, MarkupCanvasState, MarkupTool } from './MarkupCanvas'
import type { MarkupConfig, SavedMarkup } from './types'

/** Los grosores del pincel, como fracción del ancho de la foto. */
const SIZES = { small: 0.008, medium: 0.018, large: 0.035 } as const
type SizeName = keyof typeof SIZES

const TOOLS: { value: MarkupTool; icon: typeof BrushIcon }[] = [
  { value: 'brush', icon: BrushIcon },
  { value: 'lasso', icon: LassoIcon },
  { value: 'eraser', icon: EraserIcon },
]

const IDLE_CANVAS: MarkupCanvasState = {
  canUndo: false,
  canRedo: false,
  colors: [],
  baseCleared: false,
}

/** Cuántos colores recientes se muestran en la barra. */
const RECENT_COUNT = 3
/**
 * Dónde se recuerdan los recientes entre una foto y otra. Es una comodidad de
 * quien dibuja, en su navegador: no es del pedido ni de la compañía.
 */
const RECENT_KEY = 'ai.markup.recentColors'
const HEX = /^#[0-9a-f]{6}$/

/** El anillo de todos los colores: el botón que abre el selector a mano. */
const SPECTRUM = 'conic-gradient(#ef4444, #facc15, #22c55e, #06b6d4, #3b82f6, #d946ef, #ef4444)'

function loadRecent(defaults: string[]): string[] {
  try {
    const stored: unknown = JSON.parse(window.localStorage.getItem(RECENT_KEY) ?? 'null')
    if (
      Array.isArray(stored) &&
      stored.length === RECENT_COUNT &&
      stored.every((item) => typeof item === 'string' && HEX.test(item))
    ) {
      return stored
    }
  } catch {
    // Sin almacenamiento —navegación privada, o dibujando en Node— arranca
    // con los de siempre.
  }
  return defaults.slice(0, RECENT_COUNT)
}

function storeRecent(colors: string[]) {
  try {
    window.localStorage.setItem(RECENT_KEY, JSON.stringify(colors))
  } catch {
    // Recordarlos es una comodidad: si no se puede, no pasa nada.
  }
}

/** Un color como lo lee la persona: el hex en mayúscula. */
function colorText(hex: string): string {
  return hex.toUpperCase()
}

/**
 * Un tooltip de shadcn sobre un control de la barra.
 *
 * El disparador es un envoltorio y no el control mismo, por dos motivos. El
 * tooltip escribe su propio `data-state` en el elemento que dispara, y sobre
 * un ítem del toggle group pisaba el `data-state="on"` que pinta al activo: la
 * herramienta elegida no se veía elegida. Y un botón apagado no recibe el
 * puntero, así que sin envoltorio «Rehacer» sin nada que rehacer no diría qué
 * es.
 */
function Hint({ label, children }: { label: string; children: ReactElement }) {
  return (
    <Tooltip>
      <TooltipTrigger asChild>
        <span className="inline-flex">{children}</span>
      </TooltipTrigger>
      <TooltipContent>{label}</TooltipContent>
    </Tooltip>
  )
}

/** El tamaño de la goma: del 0,4 % al 10 % del ancho de la foto, en un slider de 1 a 100. */
function eraserFraction(value: number): number {
  return 0.004 + (value / 100) * 0.096
}

/**
 * Los cursores del lienzo: el mismo dibujo que el ícono de la herramienta
 * elegida, para saber con qué se está por pintar sin mirar la barra.
 *
 * Los trazos son los de lucide (`lucide-react/dist/esm/icons/brush.mjs` y
 * `lasso.mjs`), con un halo blanco debajo para que se vean sobre cielo y sobre
 * césped por igual. Llevan el **color en uso**: el SVG se arma en cada dibujado,
 * así que cambiar de color cambia el cursor en el acto.
 */
const BRUSH_TIP =
  '<path d="M6.5 21A3.5 3.5 0 1 0 3 17.5a2.62 2.62 0 0 1-.708 1.792A1 1 0 0 0 3 21z"/>'
const BRUSH_HANDLE =
  '<path d="m11 10 3 3"/>' +
  '<path d="M9.969 17.031 21.378 5.624a1 1 0 0 0-3.002-3.002L6.967 14.031"/>'
const LASSO =
  '<path d="M3.704 14.467a10 8 0 1 1 3.115 2.375"/>' +
  '<path d="M7 22a5 5 0 0 1-2-3.994"/>' +
  '<circle cx="5" cy="16" r="2"/>'

function svgCursor(svg: string, hotX: number, hotY: number): string {
  return `url("data:image/svg+xml,${encodeURIComponent(svg)}") ${hotX} ${hotY}, crosshair`
}

function iconSvg(body: string): string {
  return (
    '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" ' +
    `fill="none" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`
  )
}

/** El pincel con la cerda cargada del color en uso. El punto que pinta es su punta. */
function brushCursor(color: string): string {
  const svg = iconSvg(
    `<g stroke="white" stroke-width="4">${BRUSH_TIP}${BRUSH_HANDLE}</g>` +
      // El mango relleno de blanco: el trazo de lucide es abierto y el relleno
      // lo cierra por la base, justo donde empieza la cerda.
      `<g fill="white" stroke="black" stroke-width="2">${BRUSH_HANDLE}</g>` +
      `<g fill="${color}" stroke="black" stroke-width="1.5">${BRUSH_TIP}</g>`
  )
  return svgCursor(svg, 3, 24)
}

/** El lazo con la línea del color en uso. El punto que dibuja es el nudo. */
function lassoCursor(color: string): string {
  const svg = iconSvg(
    `<g stroke="white" stroke-width="4.5">${LASSO}</g>` +
      `<g stroke="${color}" stroke-width="2.5">${LASSO}</g>`
  )
  return svgCursor(svg, 6, 19)
}

/**
 * La goma como cursor es un círculo del tamaño que borra. Los navegadores no
 * dibujan cursores de más de 128 px: por encima de eso queda el círculo tope.
 */
function eraserCursor(diameter: number): string {
  const size = Math.round(Math.min(128, Math.max(8, diameter)))
  const radius = size / 2 - 1.5
  const svg =
    `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}">` +
    `<circle cx="${size / 2}" cy="${size / 2}" r="${radius}" fill="none" stroke="white" stroke-width="3"/>` +
    `<circle cx="${size / 2}" cy="${size / 2}" r="${radius}" fill="none" stroke="black" stroke-width="1"/>` +
    '</svg>'
  return svgCursor(svg, Math.round(size / 2), Math.round(size / 2))
}


interface Props {
  photoUrl: string
  photoAlt: string
  /** Los puntos de las áreas detectadas. Se esconden mientras se dibuja. */
  marks: ReactNode
  /**
   * Si se está dibujando. Lo decide la tarjeta de la foto, porque el botón que
   * abre el modo vive en su encabezado.
   */
  editing: boolean
  onEditingChange: (editing: boolean) => void
  config: MarkupConfig
  /** Las marcas guardadas, o nada. */
  saved: SavedMarkup | null
  onSave: (markup: SavedMarkup) => void
  onRemove: () => void
  /** Lo que el servidor rechazó de las marcas, ya traducido. */
  error?: string
}

/**
 * El editor de marcas sobre la foto (D5 de la task `ai-image-annotation-layer`).
 *
 * La foto, la barra de herramientas y la nota viven en el mismo contenedor. La
 * foto va arriba en el eje Z y las otras dos **escondidas detrás de ella**
 * todo el tiempo: se montan una vez y nunca se desmontan ni se rearman. Lo
 * único que cambia React es `data-markup` en el contenedor; el resto lo hace
 * el CSS con `:has()` (`frontend/styles/app.css`, «El editor de marcas»):
 * atenúa lo que rodea a la foto, saca la barra flotando hacia arriba y la nota
 * hacia abajo, y al guardar o cancelar las devuelve detrás de la foto.
 *
 * Escondidas llevan `inert`: si no, el tabulador entraría en botones que no se
 * ven.
 *
 * «Guardar» es de la pantalla y no del servidor: las marcas y la nota viajan
 * con el pedido, al generar. La nota es obligatoria, porque un color no
 * significa nada hasta que alguien dice qué va ahí (D1).
 */
export function PhotoMarkup({
  photoUrl,
  photoAlt,
  marks,
  editing,
  onEditingChange,
  config,
  saved,
  onSave,
  onRemove,
  error,
}: Props) {
  const canvas = useRef<MarkupCanvasHandle>(null)
  const container = useRef<HTMLDivElement>(null)
  const noteId = useId()
  const errorId = useId()
  const [size, setSize] = useState<{ width: number; height: number } | null>(null)
  const [tool, setTool] = useState<MarkupTool>('lasso')
  // El primer reciente es el color en uso: elegir otro lo trae al frente.
  const [recent, setRecent] = useState(() => loadRecent(config.default_colors))
  const color = recent[0]
  const [pickerOpen, setPickerOpen] = useState(false)
  const [picked, setPicked] = useState(color)
  const [thickness, setThickness] = useState<SizeName>('medium')
  const [eraserSize, setEraserSize] = useState(25)
  const [eraserOpen, setEraserOpen] = useState(false)
  // Mientras se arrastra el slider de la goma, su tamaño se ve en el centro de
  // la foto; al soltar, desaparece.
  const [previewing, setPreviewing] = useState(false)
  const photo = useRef<HTMLImageElement>(null)

  // El soltar puede caer fuera del slider, y ahí Radix no avisa: se escucha en
  // la ventana mientras dura el arrastre.
  useEffect(() => {
    if (!previewing) return
    const stop = () => setPreviewing(false)
    window.addEventListener('pointerup', stop)
    window.addEventListener('pointercancel', stop)
    return () => {
      window.removeEventListener('pointerup', stop)
      window.removeEventListener('pointercancel', stop)
    }
  }, [previewing])

  // El diámetro de la goma como se ve en pantalla: la fracción es de la foto
  // real, y la foto se muestra escalada.
  const eraserPixels = eraserFraction(eraserSize) * (photo.current?.clientWidth ?? 0)
  const cursor =
    tool === 'eraser'
      ? eraserCursor(eraserPixels)
      : tool === 'brush'
        ? brushCursor(color)
        : lassoCursor(color)
  const [note, setNote] = useState('')
  const [canvasState, setCanvasState] = useState<MarkupCanvasState>(IDLE_CANVAS)

  /**
   * Usar el color elegido a mano: entra primero entre los recientes y empuja
   * al último afuera. Si ya estaba, sólo sube al frente.
   */
  const bringToFront = (hex: string) => {
    const next = [hex, ...recent.filter((item) => item !== hex)].slice(0, RECENT_COUNT)
    setRecent(next)
    storeRecent(next)
  }

  const applyCustom = () => {
    const hex = picked.toLowerCase()
    if (!HEX.test(hex)) return
    bringToFront(hex)
    setPickerOpen(false)
  }

  const keepsBase = saved !== null && !canvasState.baseCleared
  const hasPaint = keepsBase || canvasState.colors.length > 0
  const canSave = hasPaint && note.trim() !== ''

  // Al abrir, la nota arranca con la guardada, y la foto va al centro de la
  // ventana: la barra sale por arriba y la nota por abajo, y así las dos
  // quedan a la vista.
  const savedNote = saved?.note ?? ''
  useEffect(() => {
    if (!editing) return
    setNote(savedNote)
    container.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
    // Sólo al abrir: lo que se escribe después no se pisa con la guardada.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [editing])

  const open = () => onEditingChange(true)

  const cancel = () => {
    canvas.current?.reset()
    onEditingChange(false)
  }

  const save = () => {
    const image = canvas.current?.exportLayer() ?? null
    onEditingChange(false)
    // Se borró todo con la goma: no queda nada que mandar.
    if (image === null) {
      onRemove()
      return
    }
    onSave({ image, note: note.trim() })
  }

  // Esc cancela, como en cualquier edición que ocupa el primer plano. Con el
  // selector de color o el de la goma abiertos, el Esc es de ellos: los cierra
  // y nada más.
  const cancelRef = useRef(cancel)
  cancelRef.current = cancel
  const pickerOpenRef = useRef(pickerOpen)
  pickerOpenRef.current = pickerOpen || eraserOpen
  useEffect(() => {
    if (!editing) return
    const onKey = (event: KeyboardEvent) => {
      if (event.key === 'Escape' && !pickerOpenRef.current) cancelRef.current()
    }
    document.addEventListener('keydown', onKey)
    return () => document.removeEventListener('keydown', onKey)
  }, [editing])

  return (
    <div className="flex flex-col gap-3">
      <div
        ref={container}
        className="photo-markup mx-auto w-fit max-w-full"
        data-markup={editing ? 'editing' : 'idle'}
      >
        <div
          className="photo-markup-toolbar bg-popover text-popover-foreground flex flex-wrap items-center gap-2 rounded-lg p-2 shadow-lg ring-1 ring-foreground/10"
          role="toolbar"
          aria-label={t('ai.markup.toolbar')}
          inert={!editing}
        >
          <ToggleGroup
            type="single"
            size="lg"
            value={tool}
            onValueChange={(value) => value && setTool(value as MarkupTool)}
            aria-label={t('ai.markup.tool.label')}
          >
            {TOOLS.filter(({ value }) => value !== 'eraser').map(({ value, icon: Icon }) => {
              const label = t(`ai.markup.tool.${value}` as TranslationKey)
              return (
                <Hint key={value} label={label}>
                  <ToggleGroupItem value={value} aria-label={label}>
                    <Icon aria-hidden className="size-5" />
                  </ToggleGroupItem>
                </Hint>
              )
            })}

            {/*
              La goma y su tamaño son un solo botón partido, del mismo alto que
              el resto. Abrir el tamaño elige la goma: nadie ajusta el tamaño de
              una herramienta que no va a usar. Las esquinas se ajustan a mano
              porque los hijos del grupo son los envoltorios de los tooltips.
            */}
            <ButtonGroup>
              <Hint label={t('ai.markup.tool.eraser')}>
                <ToggleGroupItem
                  value="eraser"
                  aria-label={t('ai.markup.tool.eraser')}
                  className="rounded-r-none"
                >
                  <EraserIcon aria-hidden className="size-5" />
                </ToggleGroupItem>
              </Hint>
              <Popover
                open={eraserOpen}
                onOpenChange={(open) => {
                  if (open) setTool('eraser')
                  setEraserOpen(open)
                }}
              >
                <Hint label={t('ai.markup.eraser.size')}>
                  <PopoverTrigger asChild>
                    <Button
                      type="button"
                      variant="ghost"
                      aria-label={t('ai.markup.eraser.size')}
                      className="h-9 w-6 rounded-l-none px-0"
                    >
                      <ChevronDownIcon aria-hidden className="size-4" />
                    </Button>
                  </PopoverTrigger>
                </Hint>
                <PopoverContent className="w-56" sideOffset={10}>
                  {/*
                    Sólo el slider. El tamaño se ve sobre la foto mientras se
                    arrastra (`previewing`): es ahí donde va a borrar.
                  */}
                  <Slider
                    min={1}
                    max={100}
                    value={[eraserSize]}
                    onPointerDown={() => setPreviewing(true)}
                    onValueChange={([value]) => setEraserSize(value)}
                    onValueCommit={() => setPreviewing(false)}
                    aria-label={t('ai.markup.eraser.size')}
                  />
                </PopoverContent>
              </Popover>
            </ButtonGroup>
          </ToggleGroup>

          <Separator orientation="vertical" className="h-8" />

          {/*
            Tres recientes y un botón para elegir cualquier otro. El primero es
            siempre el color en uso: elegir otro, de los recientes o a mano, lo
            trae al frente y empuja al último.
          */}
          <div className="flex items-center gap-1">
            <ToggleGroup
              type="single"
              size="lg"
              value={color}
              onValueChange={(value) => value && bringToFront(value)}
              aria-label={t('ai.markup.color.label')}
            >
              {recent.map((hex, index) => {
                const label =
                  index === 0
                    ? t('ai.markup.color.current', { color: colorText(hex) })
                    : t('ai.markup.color.swatch', { color: colorText(hex) })
                return (
                  <Hint key={hex} label={label}>
                    <ToggleGroupItem value={hex} aria-label={label}>
                      <span
                        aria-hidden
                        className="size-6 rounded-full ring-1 ring-foreground/20"
                        style={{ backgroundColor: hex }}
                      />
                    </ToggleGroupItem>
                  </Hint>
                )
              })}
            </ToggleGroup>

            <Popover
              open={pickerOpen}
              onOpenChange={(open) => {
                // Abre con el color que se está usando, para ajustarlo en vez
                // de arrancar de cero.
                if (open) setPicked(color)
                setPickerOpen(open)
              }}
            >
              <Hint label={t('ai.markup.color.custom')}>
                <PopoverTrigger asChild>
                  <Button
                    type="button"
                    variant="ghost"
                    size="icon-lg"
                    aria-label={t('ai.markup.color.custom')}
                  >
                    <span
                      aria-hidden
                      className="size-6 rounded-full ring-1 ring-foreground/20"
                      style={{ background: SPECTRUM }}
                    />
                  </Button>
                </PopoverTrigger>
              </Hint>
              <PopoverContent className="w-64" sideOffset={10}>
                <div className="flex flex-col gap-3">
                  {/*
                    Al ancho del popover: la librería trae 200 px fijos, y un
                    estilo en línea es lo único que le gana a su hoja inyectada.
                  */}
                  <HexColorPicker color={picked} onChange={setPicked} style={{ width: '100%' }} />
                  {/* Una sola fila del mismo alto: muestra, hex y confirmar. */}
                  <div className="flex items-center gap-2">
                    <span
                      aria-hidden
                      className="size-9 shrink-0 rounded-md ring-1 ring-foreground/20"
                      style={{ backgroundColor: picked }}
                    />
                    <HexColorInput
                      prefixed
                      color={picked}
                      onChange={setPicked}
                      aria-label={t('ai.markup.color.hex')}
                      className="border-input bg-background h-9 w-full min-w-0 rounded-md border px-2 font-mono text-sm uppercase"
                    />
                    <Hint label={t('ai.markup.color.use')}>
                      <Button
                        type="button"
                        size="icon-lg"
                        aria-label={t('ai.markup.color.use')}
                        onClick={applyCustom}
                      >
                        <CheckIcon aria-hidden className="size-5" />
                      </Button>
                    </Hint>
                  </div>
                </div>
              </PopoverContent>
            </Popover>
          </div>

          <Separator orientation="vertical" className="h-8" />

          <ToggleGroup
            type="single"
            size="lg"
            value={thickness}
            onValueChange={(value) => value && setThickness(value as SizeName)}
            aria-label={t('ai.markup.size.label')}
          >
            {(Object.keys(SIZES) as SizeName[]).map((name) => {
              const label = t(`ai.markup.size.${name}` as TranslationKey)
              return (
                <Hint key={name} label={label}>
                  <ToggleGroupItem value={name} aria-label={label}>
                    <span
                      aria-hidden
                      className={cn(
                        'bg-foreground rounded-full',
                        name === 'small' && 'size-2',
                        name === 'medium' && 'size-3.5',
                        name === 'large' && 'size-5'
                      )}
                    />
                  </ToggleGroupItem>
                </Hint>
              )
            })}
          </ToggleGroup>

          <Separator orientation="vertical" className="h-8" />

          <div className="flex items-center gap-1">
            {(
              [
                ['ai.markup.undo', Undo2Icon, canvasState.canUndo, () => canvas.current?.undo()],
                ['ai.markup.redo', Redo2Icon, canvasState.canRedo, () => canvas.current?.redo()],
                ['ai.markup.clear', Trash2Icon, hasPaint, () => canvas.current?.clear()],
              ] as const
            ).map(([key, Icon, enabled, action]) => (
              <Hint key={key} label={t(key)}>
                <Button
                  type="button"
                  variant="ghost"
                  size="icon-lg"
                  aria-label={t(key)}
                  disabled={!enabled}
                  onClick={action}
                >
                  <Icon aria-hidden className="size-5" />
                </Button>
              </Hint>
            ))}
          </div>
        </div>

        {/*
          La foto, arriba de todo en el eje Z. El contenedor se ajusta a la caja
          de la imagen —`w-fit`— y la imagen entra entera: los puntos de las
          áreas van en porcentaje y el lienzo cubre exactamente la foto.
        */}
        <div className="photo-markup-stage relative">
          <img
            ref={photo}
            src={photoUrl}
            alt={photoAlt}
            className="bg-muted block max-h-128 w-auto max-w-full rounded-md border"
            onLoad={(event) =>
              setSize({
                width: event.currentTarget.naturalWidth,
                height: event.currentTarget.naturalHeight,
              })
            }
          />
          {size ? (
            <MarkupCanvas
              ref={canvas}
              width={size.width}
              height={size.height}
              base={saved?.image ?? null}
              active={editing}
              tool={tool}
              color={color}
              // La goma tiene su propio tamaño; los grosores son del pincel y
              // del contorno del lazo.
              size={tool === 'eraser' ? eraserFraction(eraserSize) : SIZES[thickness]}
              cursor={cursor}
              label={t('ai.markup.canvas')}
              onChange={setCanvasState}
            />
          ) : null}
          {previewing ? (
            <span
              aria-hidden
              className="pointer-events-none absolute top-1/2 left-1/2 z-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-white/15 ring-1 ring-black/40"
              style={{ width: eraserPixels, height: eraserPixels }}
            />
          ) : null}
          <div className="photo-markup-points">{marks}</div>
        </div>

        <div
          className="photo-markup-note bg-popover text-popover-foreground flex flex-col gap-2 rounded-lg p-3 shadow-lg ring-1 ring-foreground/10"
          inert={!editing}
        >
          <Label htmlFor={noteId}>{t('ai.markup.note.label')}</Label>
          <Textarea
            id={noteId}
            value={note}
            rows={3}
            maxLength={config.max_note_length}
            placeholder={t('ai.markup.note.placeholder')}
            onChange={(event) => setNote(event.target.value)}
          />
          <div className="flex flex-wrap items-center justify-between gap-2">
            <p className="text-muted-foreground text-xs">
              {canSave
                ? t('ai.notes.length', {
                    used: formatNumber(note.length),
                    limit: formatNumber(config.max_note_length),
                  })
                : hasPaint
                  ? t('ai.markup.note.required')
                  : t('ai.markup.note.paintFirst')}
            </p>
            <div className="flex gap-2">
              <Button type="button" variant="ghost" size="sm" onClick={cancel}>
                <XIcon aria-hidden />
                {t('common.cancel')}
              </Button>
              <Button type="button" size="sm" disabled={!canSave} onClick={save}>
                <CheckIcon aria-hidden />
                {t('common.save')}
              </Button>
            </div>
          </div>
        </div>
      </div>

      {/*
        Guardadas, las marcas siguen sobre la foto y la nota queda a la vista,
        de sólo lectura: es lo que va a viajar con el pedido.
      */}
      {saved ? (
        // `group` y no `<section>`: una sección con nombre es un punto de
        // referencia, y éste es un detalle de la tarjeta de la foto.
        <div
          role="group"
          aria-labelledby="markup-note-title"
          className="bg-muted/50 flex flex-col gap-2 rounded-md p-3"
        >
          <div className="flex flex-wrap items-center justify-between gap-2">
            <h3 id="markup-note-title" className="font-medium">
              {t('ai.markup.saved.title')}
            </h3>
            <div className="flex gap-1">
              <Button type="button" variant="ghost" size="sm" onClick={open}>
                <PencilIcon aria-hidden />
                {t('ai.markup.editAgain')}
              </Button>
              <Button type="button" variant="ghost" size="sm" onClick={onRemove}>
                <Trash2Icon aria-hidden />
                {t('ai.markup.remove')}
              </Button>
            </div>
          </div>
          <p className="whitespace-pre-line">{saved.note}</p>
        </div>
      ) : null}
      <FieldError id={errorId} message={error} />
    </div>
  )
}
