import { KeyRound } from 'lucide-react'
import type { ReactNode } from 'react'
import { useEffect, useRef, useState } from 'react'

import { CopyButton } from '@/components/CopyButton'
import {
  AlertDialog,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { t } from '@/lib/i18n'

/* ───────────────────────────────────────────────────────────────────────────
 * La compuerta de salida, aparte del dibujo.
 *
 * Vive en una función pura por el mismo motivo que el orden del menú de una
 * fila: es la regla que hay que poder comprobar sin una pantalla, y es la única
 * parte de esta pieza donde un descuido cuesta un dato que no vuelve a existir.
 * El diálogo se dibuja en un portal y un portal no existe fuera del navegador,
 * así que si la regla viviera adentro del componente no habría forma de
 * defenderla.
 * ─────────────────────────────────────────────────────────────────────────── */

/** Las dos caras del pie. No son dos diálogos: el contrato prohíbe anidarlos. */
export type SecretGate = 'holding' | 'leaving'

export interface SecretRevealState {
  /** El valor llegó al portapapeles. Es lo único que abre la salida de un clic. */
  copied: boolean
  gate: SecretGate
  clipboardFailed: boolean
}

/**
 * Todo lo que puede pasarle al diálogo, incluidas las tres formas de salir sin
 * querer que la modalidad no cubre por sí sola.
 */
export type SecretRevealEvent =
  /** El portapapeles aceptó el valor. */
  | 'copied'
  /** El portapapeles falló. **No** cuenta como copiado. */
  | 'copy-failed'
  /** «Ya la guardé». */
  | 'save-confirmed'
  /** «Cerrar igual», la única salida sin copiar y la única que hay que pedir dos veces. */
  | 'leave-anyway'
  /** «Volver». */
  | 'stay'
  | 'escape'
  /** El botón «Atrás» del navegador. */
  | 'back'

export const SECRET_REVEAL_START: SecretRevealState = {
  copied: false,
  gate: 'holding',
  clipboardFailed: false,
}

export interface SecretRevealTransition {
  state: SecretRevealState
  /** Si el diálogo se cierra, o sea si el valor deja de existir. */
  closes: boolean
}

/**
 * Qué hace el diálogo ante cada cosa que podría cerrarlo.
 *
 * La regla, entera: **con el valor sin copiar, ningún evento cierra el diálogo
 * salvo `leave-anyway`**, que es el segundo clic de una pregunta explícita. Con
 * el valor copiado, en cambio, cualquiera de las tres salidas cierra de una.
 *
 * Un clic si hiciste lo correcto, dos si no: la fricción cae exactamente sobre
 * el caso de pérdida y sobre ningún otro.
 *
 * `copy-failed` es el caso que parece un detalle y no lo es: si un portapapeles
 * que falló contara como copiado, la salida se abriría de un clic sobre un valor
 * que nadie tiene. Por eso deja `copied` como estaba y sólo enciende el aviso.
 */
export function nextSecretRevealState(
  state: SecretRevealState,
  event: SecretRevealEvent
): SecretRevealTransition {
  switch (event) {
    case 'copied':
      return { state: { ...state, copied: true, clipboardFailed: false }, closes: false }

    case 'copy-failed':
      return { state: { ...state, clipboardFailed: true }, closes: false }

    case 'stay':
      return { state: { ...state, gate: 'holding' }, closes: false }

    case 'leave-anyway':
      return { state, closes: true }

    case 'save-confirmed':
    case 'escape':
    case 'back':
      if (state.copied) return { state, closes: true }
      return { state: { ...state, gate: 'leaving' }, closes: false }
  }
}

interface Props {
  open: boolean
  /** «Esta es tu clave». */
  title: string
  /** Por qué hay que guardarlo ahora. Es la única explicación que va a haber. */
  description: string
  /**
   * El valor en claro.
   *
   * Llega del estado de React de quien lo pidió y **nunca** de las props de
   * Inertia (RT-06): una prop de Inertia queda guardada en el historial del
   * navegador, y el botón «Atrás» volvería a mostrar un secreto que el contrato
   * promete irrepetible.
   */
  value: string
  /** Cómo se nombra el dato: «la clave». Sale en el botón de copiar. */
  valueLabel: string
  /** Con qué queda listado después, para reconocerlo sin el valor. */
  footnote?: ReactNode
  onDismiss: () => void
}

/**
 * `C-1B-9` · «Este dato existe una sola vez».
 *
 * Es el patrón y no «la clave de API»: si mañana hay un secreto de webhook, es
 * este mismo diálogo. Lo irreversible **no es crear el dato: es cerrar esta
 * pantalla**, y el costo de equivocarse recae entero sobre la persona y no se
 * puede deshacer desde ningún lado. Por eso va sobre `AlertDialog` —la misma
 * primitiva que `ConfirmDestructive`, por las mismas tres razones: el clic
 * afuera no cierra, el foco queda atrapado y al cerrarse vuelve al control que
 * lo abrió— y no sobre un `Sheet`, que cierra con clic afuera y con `Escape`,
 * que son las dos formas de perder el dato sin decidirlo.
 *
 * Tampoco tiene dirección propia, y ése es un **descarte activo** del árbol de
 * decisión: una URL implica que se puede volver a pedir, y RT-06 dice que no se
 * vuelve a pedir nunca. Una página `/api-keys/<id>/new` sería un contrato roto
 * escrito en la barra de direcciones.
 *
 * Las cinco puertas por las que el valor se podía perder sin decidirlo, y qué
 * tapa cada una:
 *
 * 1. **Clickear otra cosa** —una entrada del menú, una fila de la lista—: no se
 *    puede, porque esto es modal de verdad. Era el accidente más probable, y el
 *    `<div role="alertdialog">` que había antes no lo impedía: le prometía al
 *    lector de pantalla una modalidad que no existía.
 * 2. **El botón de cerrar**: pasa por la compuerta de `nextSecretRevealState`.
 * 3. **`Escape`**: por la misma compuerta. La salida propia de la primitiva se
 *    frena siempre —el mismo mecanismo que `ConfirmDestructive` usa mientras
 *    corre una acción— y quién cierra lo decide la compuerta, no Radix.
 * 4. **El botón «Atrás» del navegador**: ver `useHistoryGuard`.
 * 5. **Recargar y cerrar la pestaña**: `beforeunload` mientras el valor no se
 *    copió. El texto lo pone el navegador y no se puede personalizar; alcanza
 *    igual, porque lo que hace falta es la pausa, no el mensaje.
 *
 * Y la sexta, que no es una salida sino una mentira: **si el portapapeles
 * falla**. `CopyButton` avisa el resultado hacia afuera y acá se dice con
 * palabras, sin salir del estado «sin copiar»: creer que se copió y cerrar
 * pierde el dato igual. El `<code select-all>` existe justamente para ese caso.
 */
export function SecretRevealDialog({
  open,
  title,
  description,
  value,
  valueLabel,
  footnote,
  onDismiss,
}: Props) {
  const [state, setState] = useState<SecretRevealState>(SECRET_REVEAL_START)

  const valueRow = useRef<HTMLDivElement>(null)
  const backToSafety = useRef<HTMLButtonElement>(null)
  const send = useRef<(event: SecretRevealEvent) => boolean>(() => false)

  const leave = useHistoryGuard({
    open,
    onBack: () => send.current('back'),
    onLeft: onDismiss,
  })

  /** Devuelve si el diálogo se cierra, que es lo que la guardia necesita saber. */
  const dispatch = (event: SecretRevealEvent): boolean => {
    const next = nextSecretRevealState(state, event)
    setState(next.state)
    if (next.closes) leave()
    return next.closes
  }

  useEffect(() => {
    send.current = dispatch
  })

  // Cada apertura empieza de cero: el diálogo se reusa para el secreto
  // siguiente, y heredar el «ya la copiaste» del anterior abriría la salida de
  // un clic sobre un valor que nadie copió.
  useEffect(() => {
    if (open) setState(SECRET_REVEAL_START)
  }, [open])

  // Recargar o cerrar la pestaña con el valor sin copiar pide confirmación al
  // navegador. Se arma sólo mientras hace falta: un `beforeunload` que sobrevive
  // al momento en que sirve es de los que enseñan a ignorarlos.
  useEffect(() => {
    if (!open || state.copied) return

    const hold = (event: BeforeUnloadEvent) => event.preventDefault()
    window.addEventListener('beforeunload', hold)
    return () => window.removeEventListener('beforeunload', hold)
  }, [open, state.copied])

  // Al abrirse la compuerta el foco cae en «Volver» y no en «Cerrar igual»: es
  // la misma decisión que `ConfirmDestructive` toma con «Cancelar», porque la
  // tecla siguiente no puede ser la que pierde el dato.
  useEffect(() => {
    if (state.gate === 'leaving') backToSafety.current?.focus()
  }, [state.gate])

  return (
    <AlertDialog open={open}>
      <AlertDialogContent
        className="data-[size=default]:max-w-[calc(100%-2rem)] data-[size=default]:sm:max-w-3xl"
        onEscapeKeyDown={(event) => {
          event.preventDefault()
          dispatch('escape')
        }}
        onOpenAutoFocus={(event) => {
          // La primitiva lleva el foco a «Cancelar», que acá no existe: sin
          // esto el foco se queda afuera del diálogo y la modalidad protege la
          // pantalla pero no a quien navega con teclado. El destino correcto es
          // el botón de copiar, que es lo único que hay que hacer.
          event.preventDefault()
          valueRow.current?.querySelector('button')?.focus()
        }}
      >
        <AlertDialogHeader>
          <AlertDialogMedia>
            <KeyRound aria-hidden />
          </AlertDialogMedia>
          <AlertDialogTitle>{title}</AlertDialogTitle>
          <AlertDialogDescription className="text-pretty">
            {description}
          </AlertDialogDescription>
        </AlertDialogHeader>

        <div ref={valueRow} className="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center">
          {/*
            Texto seleccionable en monoespaciada. **Nunca un `input
            type="text"` «para que se pueda seleccionar»**, ni un campo
            deshabilitado, ni una imagen: `select-all` ya lo resuelve, y un campo
            de formulario con un secreto adentro es superficie que el navegador
            se puede ofrecer a recordar.
          */}
          <code
            translate="no"
            title={value}
            className="bg-background block min-w-0 flex-1 truncate whitespace-nowrap rounded border px-2 py-1.5 font-mono text-sm select-all"
          >
            {value}
          </code>
          <div className="shrink-0">
            <CopyButton
              value={value}
              label={t('common.copy')}
              ariaLabel={t('secret.copy', { what: valueLabel })}
              className="min-w-24"
              onCopy={(success) => dispatch(success ? 'copied' : 'copy-failed')}
            />
          </div>
        </div>

        {state.clipboardFailed ? (
          <p role="alert" className="text-destructive text-sm text-pretty">
            {t('common.copy.failedDetail')}
          </p>
        ) : null}

        {state.gate === 'leaving' ? (
          <AlertDialogFooter>
            <p role="alert" className="text-sm text-pretty sm:mr-auto sm:self-center">
              {t('secret.leaving')}
            </p>
            <Button ref={backToSafety} variant="outline" onClick={() => dispatch('stay')}>
              {t('secret.back')}
            </Button>
            <Button variant="destructive" onClick={() => dispatch('leave-anyway')}>
              {t('secret.closeAnyway')}
            </Button>
          </AlertDialogFooter>
        ) : (
          <AlertDialogFooter>
            {footnote ? (
              <p className="text-muted-foreground text-xs text-pretty sm:mr-auto sm:self-center">
                {footnote}
              </p>
            ) : null}

            {state.copied ? (
              <Button onClick={() => dispatch('save-confirmed')}>{t('secret.saved')}</Button>
            ) : (
              <>
                <span className="text-muted-foreground text-xs sm:self-center">
                  {t('secret.notCopiedYet')}
                </span>
                {/*
                  `outline` mientras no se copió: el botón que sale de la única
                  pantalla donde vive el dato no puede ser el más llamativo de
                  las dos cosas que hay para hacer.
                */}
                <Button variant="outline" onClick={() => dispatch('save-confirmed')}>
                  {t('secret.saved')}
                </Button>
              </>
            )}
          </AlertDialogFooter>
        )}
      </AlertDialogContent>
    </AlertDialog>
  )
}

/**
 * Dos entradas de historial, y no una.
 *
 * Una sola no alcanza, y el motivo es de Inertia y no del diálogo: cuando el
 * navegador vuelve a una entrada cuyo estado es una página de Inertia, el
 * cliente la rehidrata con `preserveState: false`, o sea **remonta el
 * componente de la página**. Todo lo que viva en el estado de React se pierde, y
 * acá lo que vive en el estado de React es el secreto. Con una entrada sola, el
 * primer «Atrás» aterriza justo en esa entrada y el dato desaparece sin que
 * nadie lo haya decidido, que es exactamente lo que esta pieza existe para
 * impedir.
 *
 * Con dos, la entrada que queda **debajo** de la actual es una nuestra, con
 * estado nulo. Ante un estado nulo el cliente de Inertia no rehidrata nada: sólo
 * repone su propio estado en esa entrada. La página no se remonta, el diálogo
 * sigue en pie y «Atrás» pasa por la compuerta como cualquier otra salida. Por
 * eso cada vez que se consume una se reponen dos: la de abajo puede haber dejado
 * de ser nula.
 */
function pushGuard(): void {
  window.history.pushState(null, '')
  window.history.pushState(null, '')
}

interface GuardProps {
  open: boolean
  /** Alguien apretó «Atrás» con el diálogo abierto. Devuelve si eso lo cerró. */
  onBack: () => boolean
  /** El historial ya volvió a donde estaba: recién ahora se avisa hacia afuera. */
  onLeft: () => void
}

function useHistoryGuard({ open, onBack, onLeft }: GuardProps): () => void {
  /** ¿La guardia sigue puesta? */
  const guarding = useRef(false)
  /** El `popstate` que viene lo pedimos nosotros al cerrar. */
  const leavingOnPurpose = useRef(false)

  // Lo que el manejador necesita saber cambia en cada render, y el efecto que lo
  // instala depende sólo de `open`: si dependiera del estado del diálogo, cada
  // copia empujaría otro par de entradas al historial.
  const latest = useRef({ onBack, onLeft })
  useEffect(() => {
    latest.current = { onBack, onLeft }
  })

  useEffect(() => {
    if (!open) return

    leavingOnPurpose.current = false
    pushGuard()
    guarding.current = true

    const onPopState = () => {
      guarding.current = false

      // Salida pedida desde un botón: el diálogo ya decidió cerrarse y lo único
      // que faltaba era devolver el historial a donde estaba.
      if (leavingOnPurpose.current) {
        leavingOnPurpose.current = false
        latest.current.onLeft()
        return
      }

      // «Atrás» de verdad: lo resuelve la compuerta. Si el valor ya estaba
      // copiado, la compuerta cierra y no hay nada que volver a guardar; si no,
      // la guardia se repone para que el siguiente no se lleve la página.
      if (latest.current.onBack()) return

      pushGuard()
      guarding.current = true
    }

    window.addEventListener('popstate', onPopState)
    return () => window.removeEventListener('popstate', onPopState)
  }, [open])

  /**
   * Cerrar por decisión propia.
   *
   * Se vuelve una entrada atrás —a la nuestra, la de estado nulo— y recién en
   * ese aterrizaje se avisa hacia afuera. El orden importa: quien usa esta pieza
   * suele navegar al cerrar, y una navegación disparada mientras el
   * `history.back()` está en vuelo deja el historial en un lugar que no eligió
   * nadie.
   */
  return () => {
    if (!guarding.current) {
      onLeft()
      return
    }
    if (leavingOnPurpose.current) return

    leavingOnPurpose.current = true
    window.history.back()
  }
}
