import { Link } from '@inertiajs/react'
import {
  ArrowUpRight,
  Ban,
  Copy,
  Download,
  List,
  MoreVertical,
  PanelLeftOpen,
  Pencil,
  RotateCw,
  Trash2,
  XCircle,
} from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import type { ComponentProps } from 'react'
import { useEffect, useId, useSyncExternalStore } from 'react'
import { createRoot } from 'react-dom/client'
import type { Root } from 'react-dom/client'

import { ConfirmDestructive } from '@/components/ConfirmDestructive'
import { Button } from '@/components/ui/button'
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { t } from '@/lib/i18n'
import type { TranslationKey } from '@/lib/i18n'

/**
 * Las acciones que una fila puede ofrecer. **El juego es cerrado.**
 *
 * Una vista no elige el icono de «ver el detalle» ni dónde cae en el menú:
 * elige si esa fila lo tiene y qué hace. Es la diferencia entre un menú que se
 * ve igual en las nueve tablas del producto y nueve menús que se parecen. El
 * caso que esto evita es el de dos pantallas que ofrecen lo mismo con dibujos
 * distintos, que obliga a leer cada entrada en vez de reconocerla.
 *
 * Si hace falta una acción que no está, se agrega **acá** y queda disponible
 * para todas. Eso es a propósito: sumar una fila a esta tabla es una decisión
 * de producto, y escribir un icono suelto en una vista no lo era.
 */
export type RowActionKind =
  | 'detail'
  | 'open'
  | 'list'
  | 'copy'
  | 'download'
  | 'rename'
  | 'retry'
  | 'revoke'
  | 'close'
  | 'delete'

interface RowActionSpec {
  icon: LucideIcon
  /** El rótulo por omisión, como clave: el catálogo todavía no está cuando esto se arma. */
  label: TranslationKey
  /** Lo que destruye va último, separado y siempre detrás de una confirmación. */
  destructive?: boolean
}

/**
 * El registro, en el orden en que las entradas aparecen en el menú.
 *
 * El orden es del registro y no de la vista: dos tablas que ofrecen «ver» y
 * «abrir» las tienen que ofrecer en la misma posición, o el menú deja de
 * reconocerse de un vistazo y hay que leerlo entero cada vez.
 */
const ROW_ACTIONS: Record<RowActionKind, RowActionSpec> = {
  detail: { icon: PanelLeftOpen, label: 'rowAction.detail' },
  open: { icon: ArrowUpRight, label: 'rowAction.open' },
  list: { icon: List, label: 'rowAction.list' },
  copy: { icon: Copy, label: 'rowAction.copy' },
  download: { icon: Download, label: 'rowAction.download' },
  rename: { icon: Pencil, label: 'rowAction.rename' },
  retry: { icon: RotateCw, label: 'rowAction.retry' },
  revoke: { icon: Ban, label: 'rowAction.revoke', destructive: true },
  close: { icon: XCircle, label: 'rowAction.close', destructive: true },
  delete: { icon: Trash2, label: 'rowAction.delete', destructive: true },
}

const ROW_ACTION_ORDER = Object.keys(ROW_ACTIONS) as RowActionKind[]

/** Lo que la vista aporta de una acción: qué hace, y nada sobre cómo se ve. */
export interface RowActionConfig {
  /**
   * Destino. Se rinde como `<Link>` de verdad, así que Cmd+clic, el clic del
   * medio y «abrir en otra pestaña» funcionan como en cualquier enlace.
   */
  href?: string
  /** Lo que hace, cuando no hay destino. Excluyente con `href`. */
  onSelect?: () => void
  /**
   * Reemplaza el rótulo del registro. Sólo para cuando tiene que **nombrar el
   * objeto** —«Ver todos los lotes de ejemplo.com»—; si es el verbo a secas, se
   * deja el del registro y así las nueve tablas dicen lo mismo.
   */
  label?: string
  disabled?: boolean
  /** Obligatoria en las destructivas: nunca se destruye sin confirmar. */
  confirm?: RowActionConfirm
}

/** Qué ofrece esta fila. Lo que no está, no se dibuja. */
export type RowActionMap = Partial<Record<RowActionKind, RowActionConfig>>

/**
 * Lo que `ConfirmDestructive` necesita saber, menos lo que maneja el menú.
 *
 * `onConfirm` sale de `onSelect` de la acción destructiva, y la apertura la
 * maneja el anfitrión: pedirlas de nuevo acá sería ofrecer dos formas de abrir
 * el mismo diálogo.
 */
export type RowActionConfirm = Omit<
  ComponentProps<typeof ConfirmDestructive>,
  'onConfirm' | 'trigger' | 'open' | 'onOpenChange'
>

/** Una entrada ya resuelta: lo del registro más lo que aportó la vista. */
export interface RowActionItem extends RowActionConfig {
  action: RowActionKind
  label: string
  icon: LucideIcon
}

/**
 * El orden del menú, resuelto aparte del dibujo.
 *
 * Es una regla del contrato y no una decisión de cada vista: lo de leer arriba,
 * lo que destruye último y separado. Sale acá afuera porque es lo que se puede
 * comprobar sin abrir el menú.
 */
export type RowActionEntry =
  | { kind: 'action'; item: RowActionItem }
  | { kind: 'separator' }
  | { kind: 'destructive'; item: RowActionItem }

/**
 * Las entradas que le corresponden a una fila.
 *
 * **Una acción declarada sin nada que hacer no se dibuja.** Sin destino y sin
 * manejador no lleva a ningún lado ni hace nada, y una entrada de menú inerte
 * es peor que su ausencia: se elige, no pasa nada, y no queda claro si falló o
 * si nunca hizo algo (RT-07). Así una vista puede armar el mapa de una sola
 * forma para todas sus filas y dejar en blanco lo que esa fila no ofrece.
 */
export function rowActionEntries(actions: RowActionMap): RowActionEntry[] {
  const items = ROW_ACTION_ORDER.filter((action) => {
    const config = actions[action]
    return Boolean(config) && Boolean(config?.href || config?.onSelect)
  }).map((action) => ({
    ...actions[action],
    action,
    label: actions[action]?.label ?? t(ROW_ACTIONS[action].label),
    icon: ROW_ACTIONS[action].icon,
  }))

  const entries: RowActionEntry[] = []

  for (const item of items) {
    if (!ROW_ACTIONS[item.action].destructive) entries.push({ kind: 'action', item })
  }

  const destructive = items.filter((item) => ROW_ACTIONS[item.action].destructive)

  if (destructive.length > 0) {
    // Sin nada arriba, el separador sería una línea debajo de la nada: existe
    // para marcar que lo que sigue es de otra clase, y con una sola clase no hay
    // dos que separar.
    if (entries.length > 0) entries.push({ kind: 'separator' })
    for (const item of destructive) entries.push({ kind: 'destructive', item })
  }

  return entries
}

/* ───────────────────────────────────────────────────────────────────────────
 * El anfitrión de la confirmación, fuera de la fila.
 *
 * Este es el conocimiento que estaba escrito en un comentario de la pantalla de
 * sesiones y que le costaba una copia a cada vista: **al confirmar, la fila
 * desaparece de la respuesta del servidor**. Un `ConfirmDestructive` montado
 * adentro de la fila se desmonta con ella mientras todavía está abierto, y
 * entonces no corre ninguna de las cosas que pasan al cerrarse: el foco no
 * vuelve a ningún lado, y el bloqueo que Radix le pone al `body` mientras hay
 * un modal abierto —`pointer-events: none`, el scroll trabado— se queda puesto
 * sobre una página que ya no tiene quién lo saque.
 *
 * Por eso el diálogo no vive en el árbol de la fila sino en una raíz de React
 * propia, colgada del `body`: nada de lo que pase en la tabla la puede
 * desmontar. La fila sólo **pide** la confirmación y le sigue pasando el estado
 * de su acción mientras exista; si desaparece antes de que termine, el
 * anfitrión cierra el diálogo por el camino normal.
 *
 * Lo que el componente compartido no puede hacer es decidir adónde va el foco
 * después: eso lo sabe la vista, que es la que tiene el mensaje con el
 * resultado (RT-17).
 * ─────────────────────────────────────────────────────────────────────────── */

interface PendingConfirm {
  /** Quién la pidió. Sólo su dueño la actualiza o la retira. */
  owner: string
  /** Cuál de sus acciones destructivas, para las filas que ofrecen más de una. */
  action: RowActionKind
  confirm: RowActionConfirm
  onConfirm: () => void
}

/**
 * `pending` sobrevive al cierre a propósito: `AlertDialog` necesita seguir
 * montado con `open` en falso para correr su animación de salida y devolver el
 * `body` a la normalidad. Se reemplaza recién cuando alguien pide otra.
 */
let state: { open: boolean; pending: PendingConfirm | null } = { open: false, pending: null }

const listeners = new Set<() => void>()

function subscribe(listener: () => void): () => void {
  listeners.add(listener)
  return () => listeners.delete(listener)
}

function publish(next: typeof state) {
  state = next
  for (const listener of listeners) listener()
}

function DestructiveConfirmHost() {
  const current = useSyncExternalStore(
    subscribe,
    () => state,
    () => state
  )

  if (!current.pending) return null

  return (
    <ConfirmDestructive
      {...current.pending.confirm}
      open={current.open}
      onOpenChange={(isOpen) => {
        if (!isOpen) publish({ open: false, pending: current.pending })
      }}
      onConfirm={current.pending.onConfirm}
    />
  )
}

let host: Root | null = null

function mountHost() {
  if (host || typeof document === 'undefined') return
  const container = document.createElement('div')
  container.dataset.slot = 'row-actions-confirm'
  document.body.append(container)
  host = createRoot(container)
  host.render(<DestructiveConfirmHost />)
}

interface Props {
  /**
   * El nombre accesible del disparador, que **nombra la fila**: «Acciones de la
   * clave “CI de producción”». Veinte botones llamados «Acciones» obligan a
   * recorrer la tabla celda por celda para saber cuál es cuál.
   */
  label: string
  /** Qué ofrece esta fila, del juego cerrado. Lo que no está, no se dibuja. */
  actions: RowActionMap
}

/**
 * `C-1B-1` · El menú de acciones de la fila.
 *
 * Reemplaza los botones sueltos repartidos por las filas —veintiún «Cerrar» en
 * la lista de sesiones— por un solo disparador con nombre propio. Tres cosas
 * viajan con la pieza y no se vuelven a decidir por vista:
 *
 * - **El icono, el rótulo por omisión y la posición salen del registro de
 *   arriba.** La vista dice qué acciones tiene la fila y qué hacen; no elige el
 *   dibujo. Una acción declarada sin destino ni manejador no se dibuja.
 * - **Lo destructivo va último, detrás de un `DropdownMenuSeparator`**, y
 *   siempre a través de `ConfirmDestructive`. Nunca sin confirmar.
 * - **La confirmación no se monta adentro de la fila** (ver arriba el porqué,
 *   que es el motivo de que esta pieza exista y no sea un `DropdownMenu` a
 *   mano).
 *
 * El disparador va **al principio de la fila, junto a la casilla**, y no al
 * final: en una tabla ancha el final está detrás de un desplazamiento
 * horizontal, así que llegar a lo que se puede hacer con una fila costaba
 * arrastrar la tabla. Quién lo pone ahí es `ClientDataTable`, que arma la
 * columna sola.
 *
 * La fila **no** es clickeable entera: el detalle se abre desde el `<Link>` de
 * la primera celda con texto, y todo lo demás desde acá.
 */
export function RowActions({ label, actions }: Props) {
  const owner = useId()
  const entries = rowActionEntries(actions)

  // Mientras la confirmación abierta sea suya, la fila le sigue pasando lo que
  // cambia —`busy`, sobre todo—. Sin dependencias a propósito: el objeto
  // `confirm` se escribe en línea en la vista, así que su identidad cambia en
  // cada render y una lista de dependencias sólo daría la ilusión de control.
  useEffect(() => {
    if (!state.open || state.pending?.owner !== owner) return

    const current = entries.find(
      (entry) => entry.kind === 'destructive' && entry.item.action === state.pending?.action
    )
    if (current?.kind !== 'destructive' || !current.item.confirm) return

    publish({
      open: true,
      pending: {
        owner,
        action: current.item.action,
        confirm: current.item.confirm,
        onConfirm: () => current.item.onSelect?.(),
      },
    })
  })

  // La fila se fue con la confirmación abierta. Se cierra por el camino normal
  // —el anfitrión sigue montado, así que Radix corre su salida y le devuelve el
  // `body` a la página— en vez de desaparecer de golpe y dejarla inerte.
  useEffect(
    () => () => {
      if (state.open && state.pending?.owner === owner) {
        publish({ open: false, pending: state.pending })
      }
    },
    [owner]
  )

  const askToConfirm = (item: RowActionItem) => {
    if (!item.confirm) return
    mountHost()
    publish({
      open: true,
      pending: {
        owner,
        action: item.action,
        confirm: item.confirm,
        onConfirm: () => item.onSelect?.(),
      },
    })
  }

  // Una fila sin nada que ofrecer no dibuja un botón que abre un menú vacío.
  if (entries.length === 0) return null

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        {/*
          Puntos verticales y no horizontales: es la primera celda de la fila y
          queda pegado a la casilla, así que un control ancho compite con ella;
          y los tres puntos apilados son la convención de «más cosas de este
          renglón» en cualquier lista.
        */}
        <Button variant="ghost" size="icon-sm">
          <MoreVertical aria-hidden />
          <span className="sr-only">{label}</span>
        </Button>
      </DropdownMenuTrigger>

      <DropdownMenuContent align="start" className="w-auto min-w-44">
        {entries.map((entry) =>
          entry.kind === 'separator' ? (
            // Hay como mucho uno, así que su nombre alcanza como identidad.
            <DropdownMenuSeparator key="separator" />
          ) : entry.kind === 'destructive' ? (
            <DropdownMenuItem
              key={entry.item.action}
              variant="destructive"
              disabled={entry.item.disabled}
              onSelect={() => askToConfirm(entry.item)}
            >
              <MenuLabel item={entry.item} />
            </DropdownMenuItem>
          ) : (
            <MenuAction key={entry.item.action} item={entry.item} />
          )
        )}
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

/** El dibujo de una entrada: icono primero, palabra después, como todo acá. */
function MenuLabel({ item }: { item: RowActionItem }) {
  const Icon = item.icon

  return (
    <>
      <Icon aria-hidden />
      {item.label}
    </>
  )
}

/** Un ítem de lectura: enlace si lleva a algún lado, acción si no. */
function MenuAction({ item }: { item: RowActionItem }) {
  if (item.href) {
    return (
      <DropdownMenuItem asChild disabled={item.disabled}>
        <Link href={item.href}>
          <MenuLabel item={item} />
        </Link>
      </DropdownMenuItem>
    )
  }

  return (
    <DropdownMenuItem disabled={item.disabled} onSelect={() => item.onSelect?.()}>
      <MenuLabel item={item} />
    </DropdownMenuItem>
  )
}
