import { Check, Copy, TriangleAlert } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import { useEffect, useState } from 'react'
import type { ReactNode } from 'react'

import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { t } from '@/lib/i18n'
import type { TranslationKey } from '@/lib/i18n'
import { cn } from '@/lib/utils'

interface Props {
  value: string
  /**
   * La palabra que se lee en el botón. **Opcional, y su ausencia cambia la pieza.**
   *
   * Con rótulo, el botón se dibuja entero y no lleva globo: repetir en un globo
   * lo que ya está escrito al lado es ruido que además tapa la pantalla.
   *
   * Sin rótulo, el botón es sólo el icono y **el globo pasa a ser obligatorio**:
   * es lo único que nombra la acción y lo único que puede decir el resultado,
   * porque no hay un renglón donde escribirlo.
   */
  label?: string
  /**
   * Cómo se llama la acción cuando el rótulo no alcanza para ubicarla.
   *
   * Sirve para nombrar **qué** se copia —«Copiar la dirección de la cuenta de
   * servicio»— cuando hay varios de estos en la misma pantalla. Sin rótulo, es
   * además lo que dice el globo.
   */
  ariaLabel?: string
  className?: string
  /**
   * Qué pasó con el portapapeles, para quien necesite saberlo.
   *
   * Lo usa la revelación de una clave, que es el caso donde el fallo importa: si
   * la persona cree que copió y cierra el diálogo, el dato no vuelve a existir.
   */
  onCopy?: (success: boolean) => void
}

type Outcome = 'idle' | 'copied' | 'failed'

/**
 * Las tres caras del botón, cada una entera en su renglón.
 *
 * Antes esto eran tres cadenas de ternarios paralelas —una para el icono, otra
 * para la palabra, otra para el anuncio— que había que leer en tres lugares
 * distintos para saber qué muestra un estado. Con la tabla, agregar o cambiar un
 * estado es tocar una fila, y es imposible que el icono diga una cosa y la
 * palabra otra.
 *
 * `announce` es lo que oye quien no ve el cambio de icono, y por eso no repite
 * la palabra del botón: dice la consecuencia. En el fallo eso importa más que en
 * ningún lado —el valor sigue en pantalla y se puede copiar a mano—, y un
 * «No se pudo copiar» a secas dejaría eso sin decir.
 */
const FACES: Record<Outcome, { icon: LucideIcon; label: TranslationKey; announce: TranslationKey | null }> =
  {
    idle: { icon: Copy, label: 'common.copy', announce: null },
    copied: { icon: Check, label: 'common.copy.done', announce: 'common.copy.announced' },
    failed: {
      icon: TriangleAlert,
      label: 'common.copy.failed',
      announce: 'common.copy.failedDetail',
    },
  }

/**
 * Copiar un dato al portapapeles, con confirmación junto al control.
 *
 * La confirmación va acá y no en un aviso flotante (RT-11): la acción no cambia
 * la pantalla, así que el resultado tiene que aparecer donde estaba la mirada.
 * Se anuncia además por región viva, porque un cambio de icono no le dice nada
 * a quien usa un lector de pantalla.
 *
 * **El fallo se dice.** Antes se lo tragaba en silencio: el icono no cambiaba y
 * no había forma de distinguir «no lo apretaste» de «no se pudo». En la
 * revelación de una clave que existe una sola vez, esa diferencia es la clave
 * entera.
 *
 * **Con rótulo y sin rótulo son dos dibujos, no dos tamaños.** Ver `label`: el
 * globo aparece exactamente cuando no hay palabra a la vista, que es cuando hace
 * falta que algo nombre la acción.
 */
export function CopyButton({ value, label, ariaLabel, className, onCopy }: Props) {
  const [outcome, setOutcome] = useState<Outcome>('idle')
  // Si el globo está abierto por el puntero o por el foco. Sólo se usa sin
  // rótulo; con rótulo no hay globo que controlar.
  const [hovering, setHovering] = useState(false)

  const hasLabel = Boolean(label)
  const face = FACES[outcome]

  useEffect(() => {
    if (outcome === 'idle') return
    const timer = setTimeout(() => setOutcome('idle'), 2000)
    return () => clearTimeout(timer)
  }, [outcome])

  const copy = async () => {
    try {
      await navigator.clipboard.writeText(value)
      setOutcome('copied')
      onCopy?.(true)
    } catch {
      // Sin permiso de portapapeles no hay nada que hacer desde acá, pero sí hay
      // algo que decir: el valor sigue en pantalla y se puede seleccionar a
      // mano. Callarse es lo que hace que se pierda.
      setOutcome('failed')
      onCopy?.(false)
    }
  }

  /*
    Lo que dice el control en este momento.

    En reposo manda lo que escribió la vista —el rótulo, o el nombre accesible
    cuando no hay rótulo— y recién si no hay ninguno cae en «Copiar». Los otros
    dos estados son del catálogo y no se pueden reemplazar desde afuera: son la
    respuesta de la pieza, no su nombre.
  */
  const resting = label ?? ariaLabel ?? t('common.copy')
  const says = outcome === 'idle' ? resting : t(face.label)

  const button = (
    <Button
      type="button"
      variant="outline"
      // `icon-sm` y no `sm`: son la misma altura —siete— y la cuadrada es la que
      // no deja el icono nadando en el relleno horizontal de un botón con texto.
      size={hasLabel ? 'sm' : 'icon-sm'}
      onClick={copy}
      aria-invalid={outcome === 'failed' || undefined}
      // Sin rótulo, el nombre accesible es lo único que tiene el botón: un
      // control cuyo único hijo es un icono con `aria-hidden` no se anuncia. Con
      // rótulo, sólo se pisa si la vista pidió nombrar qué se copia.
      aria-label={hasLabel ? ariaLabel : says}
      className={cn(className)}
    >
      <face.icon aria-hidden />
      {hasLabel ? says : null}
    </Button>
  )

  return (
    <>
      {hasLabel ? (
        button
      ) : (
        <IconOnly outcome={outcome} says={says} open={hovering} onOpenChange={setHovering}>
          {button}
        </IconOnly>
      )}
      {/* La región viva va afuera de las dos ramas: el anuncio no depende de si
          hay rótulo, y duplicarla dejaría dos lugares donde arreglar lo mismo. */}
      <span role="status" aria-live="polite" className="sr-only">
        {face.announce ? t(face.announce) : ''}
      </span>
    </>
  )
}

/**
 * El botón sin rótulo, con su globo abierto mientras haya algo que decir.
 *
 * El globo tiene que quedarse abierto después del clic, y por omisión pasa lo
 * contrario: Radix lo cierra al apretar. Sin esto, el único lugar donde se
 * escribe «Copiado» desaparecería en el mismo gesto que lo produce, y la
 * variante sin rótulo quedaría sin manera de confirmar nada.
 *
 * Por eso la apertura se controla acá y no se deja librada al puntero: el globo
 * está abierto si alguien lo está mirando **o** si hay una respuesta fresca.
 */
function IconOnly({
  outcome,
  says,
  open,
  onOpenChange,
  children,
}: {
  outcome: Outcome
  says: string
  open: boolean
  onOpenChange: (open: boolean) => void
  children: ReactNode
}) {
  return (
    <Tooltip
      open={open || outcome !== 'idle'}
      onOpenChange={(next) => {
        // Se acepta abrir siempre; cerrar, sólo cuando no hay respuesta a la
        // vista. Es lo que hace que el clic no se lleve puesto su propio acuse.
        if (next || outcome === 'idle') onOpenChange(next)
      }}
    >
      <TooltipTrigger asChild>{children}</TooltipTrigger>
      <TooltipContent>{says}</TooltipContent>
    </Tooltip>
  )
}
