import { Link } from '@inertiajs/react'
import { Check, ChevronsUpDown } from 'lucide-react'
import { useId, useState } from 'react'

import { tableUrl } from '@/components/DataTable'
import type { TablePage } from '@/components/DataTable'
import { Button } from '@/components/ui/button'
import {
  Command,
  CommandEmpty,
  CommandInput,
  CommandItem,
  CommandList,
} from '@/components/ui/command'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { formatNumber } from '@/lib/format'
import { t } from '@/lib/i18n'
import { cn } from '@/lib/utils'

export interface FilterOption {
  value: string
  label: string
  /** Cuántas filas quedan si se elige. Un eje sin cifras se elige a ciegas. */
  count?: number
}

interface Props {
  /** El nombre del eje: «Estado informado por Google». Va arriba y se ve. */
  label: string
  /** La clave del filtro en la dirección, tal como la declara la tabla. */
  param: string
  /** La página que devolvió `table_props()`: de ahí sale la dirección de cada opción. */
  table: TablePage<unknown>
  /** Cómo se llama «sin filtrar» en este eje: «Todas», «Cualquiera». */
  allLabel: string
  options: FilterOption[]
  /**
   * Si la lista lleva buscador.
   *
   * Por omisión se prende sola pasadas las {@link SEARCH_THRESHOLD} opciones,
   * pero se declara a mano cuando el eje **puede crecer**: los estados de un
   * lote son cinco y van a seguir siendo cinco, mientras que una lista de
   * dominios empieza en dos y no tiene techo. Lo primero es un dato de la
   * pantalla; lo segundo, del producto, y sólo lo sabe quien usa la pieza.
   */
  searchable?: boolean
  /** Otro parámetro que este eje apaga al elegirse, por preguntar lo mismo. */
  clears?: string
}

/** A partir de cuántas opciones una lista deja de recorrerse con la vista. */
const SEARCH_THRESHOLD = 8

/**
 * La dirección de una opción del eje.
 *
 * Exportada porque es la regla que hay que poder comprobar: cada opción tiene
 * dirección propia, el valor vacío saca el parámetro en vez de mandarlo vacío, y
 * elegir vuelve a la página 1 —la página 7 de otro conjunto de filas no
 * significa nada—. Desde el marcado no se alcanza: las opciones viven adentro
 * del popover, que sin abrir no se dibuja.
 *
 * `page` sin prefijo: los ejes de filtro son de la lista de la página, que es la
 * que no lo lleva.
 */
export function optionHref(
  table: TablePage<unknown>,
  param: string,
  value: string,
  clears?: string
): string {
  const changes: Record<string, string | null> = {
    [param]: value || null,
    page: null,
  }
  if (clears) changes[clears] = null

  return tableUrl(table, changes)
}

/**
 * Un eje de filtro como desplegable de **una** opción.
 *
 * Reemplaza a la fila de botones. Una fila no escala: cada opción ocupa lugar en
 * primer plano tenga o no algo que ver con lo que se está mirando, y con nueve
 * tipos de aviso —uno de treinta y cinco caracteres— la región de filtros pesa
 * más que la tabla que filtra. Acá el eje mide siempre lo mismo, diga lo que
 * diga: una etiqueta y un disparador con lo que está puesto.
 *
 * Es el de selección **única**. El de selección múltiple es otra pieza: comparte
 * la forma pero no la semántica —varios valores en la dirección, casillas en vez
 * de marca, y un disparador que resume en vez de nombrar—.
 *
 * **Las opciones siguen siendo enlaces de verdad.** El recorte vive en la
 * dirección, así que cada opción tiene dirección propia y se abre en otra
 * pestaña, se copia y se guarda. Por eso `CommandItem` va con `asChild` sobre un
 * `<Link>` y no con un `onSelect` que navegue: un manejador de clic no se puede
 * copiar ni abrir en otra pestaña, y el botón Atrás no lo deshace.
 *
 * Elegir una opción vuelve a la página 1: la página 7 de otro conjunto de filas
 * no significa nada.
 */
export function FilterSelect({
  label,
  param,
  table,
  allLabel,
  options,
  searchable,
  clears,
}: Props) {
  const labelId = useId()
  const [open, setOpen] = useState(false)

  const current = table.filters[param] ?? ''
  const selected = options.find((option) => option.value === current)
  const withSearch = searchable ?? options.length >= SEARCH_THRESHOLD

  // «Todas» es la ausencia del parámetro, así que entra en la lista como una
  // opción más con valor vacío: elegirla es la forma de sacar el recorte.
  const all: FilterOption = { value: '', label: allLabel }

  return (
    <div className="flex flex-col gap-1.5">
      <span id={labelId} className="text-muted-foreground text-xs">
        {label}
      </span>

      <Popover open={open} onOpenChange={setOpen}>
        <PopoverTrigger asChild>
          <Button
            variant="outline"
            size="sm"
            role="combobox"
            aria-expanded={open}
            aria-labelledby={labelId}
            className="justify-between gap-2 font-normal min-w-25"
          >
            {/* El disparador dice lo que está puesto, no el nombre del eje: un
                filtro que sólo se ve al desplegar es un filtro que se olvida
                puesto. El nombre ya está arriba, en la etiqueta. */}
            <span className="truncate">{selected ? selected.label : allLabel}</span>
            <ChevronsUpDown aria-hidden className="size-3.5 shrink-0 opacity-50" />
          </Button>
        </PopoverTrigger>

        <PopoverContent align="start" className="w-64 p-2">
          <Command>
            {withSearch ? <CommandInput placeholder={label} /> : null}
            <CommandList>
              <CommandEmpty>{t('table.noMatchingOption')}</CommandEmpty>
              {[all, ...options].map((option) => (
                <OptionLink
                  key={option.value}
                  option={option}
                  table={table}
                  param={param}
                  current={current}
                  clears={clears}
                  onNavigate={() => setOpen(false)}
                />
              ))}
            </CommandList>
          </Command>
        </PopoverContent>
      </Popover>
    </div>
  )
}

function OptionLink({
  option,
  table,
  param,
  current,
  clears,
  onNavigate,
}: {
  option: FilterOption
  table: TablePage<unknown>
  param: string
  current: string
  clears?: string
  onNavigate: () => void
}) {
  const active = current === option.value

  return (
    <CommandItem
      asChild
      // Lo que busca el buscador es la etiqueta. Sin esto, `cmdk` indexa el
      // `value`, que son códigos —`URL_INSPECTION`, `ACCESS_LOST`— y escribir el
      // nombre que se ve en pantalla no encontraría nada.
      value={option.label}
      data-checked={active || undefined}
    >
      <Link
        href={optionHref(table, param, option.value, clears)}
        preserveScroll
        preserveState
        aria-current={active ? 'true' : undefined}
        onClick={onNavigate}
      >
        <span className="truncate">{option.label}</span>
        {option.count === undefined ? null : (
          <span className="text-muted-foreground text-xs tabular-nums">
            {formatNumber(option.count)}
          </span>
        )}
        {/* La marca va dentro del enlace porque el enlace es el único hijo de la
            ranura. `ml-auto` la empuja al borde tenga o no cifra al lado. */}
        <Check aria-hidden className={cn('ml-auto size-4', active ? 'opacity-100' : 'opacity-0')} />
      </Link>
    </CommandItem>
  )
}
