import { ChevronLeft, ChevronRight } from 'lucide-react'
import { RadioGroup as RadioGroupPrimitive } from 'radix-ui'
import { useEffect, useRef, useState, type TouchEvent } from 'react'

import { Button } from '@/components/ui/button'
import { t } from '@/lib/i18n'
import { cn } from '@/lib/utils'

/** Una tarjeta: su valor, su foto y cómo se dice, ya resuelto por quien llama. */
export interface CardItem {
  value: string
  image: string
  label: string
  description?: string
}

interface Props {
  /** Las tarjetas vigentes, en el orden del catálogo. */
  items: CardItem[]
  value: string
  onChange: (value: string) => void
  /** El id del grupo: es adonde salta el foco cuando falta contestar. */
  id: string
  /** Ids del rótulo y del error, para que el grupo se nombre y se explique. */
  labelledBy: string
  describedBy?: string
  invalid?: boolean
  /** Cómo se reparten las tarjetas. Por omisión, dos columnas. */
  className?: string
}

/**
 * Hasta cuántas tarjetas van todas a la vista. Más que esto y el grupo se
 * pagina: catorce acabados de pileta de una vez son siete filas de fotos que
 * empujan el resto del formulario fuera de la pantalla.
 */
const VISIBLE_AT_ONCE = 8
/** Filas por página: la página sigue a las columnas que entran en el ancho. */
const PAGE_ROWS = 2
/** Cuánto hay que arrastrar el dedo para que cuente como pasar de página. */
const SWIPE_DISTANCE = 40

// Cuatro columnas, y menos a medida que se achica la pantalla. Con un
// `auto-fit` las tarjetas se estiraban: un campo de dos opciones se repartía
// el ancho entero entre las dos y quedaban dos fotos enormes.
const GRID = 'grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4'

/**
 * Las columnas que dibuja `GRID` con el ancho actual. Tiene que coincidir con
 * sus cortes (`md` y `lg` de Tailwind): de esto sale cuántas tarjetas entran en
 * una página, y una página que no llena sus dos filas deja un hueco a la vista.
 */
function useColumns(): number {
  const [columns, setColumns] = useState(4)

  useEffect(() => {
    const large = window.matchMedia('(min-width: 64rem)')
    const medium = window.matchMedia('(min-width: 48rem)')
    const update = () => setColumns(large.matches ? 4 : medium.matches ? 3 : 2)
    update()
    large.addEventListener('change', update)
    medium.addEventListener('change', update)
    return () => {
      large.removeEventListener('change', update)
      medium.removeEventListener('change', update)
    }
  }, [])

  return columns
}

function chunk<T>(list: T[], size: number): T[][] {
  const pages: T[][] = []
  for (let start = 0; start < list.length; start += size) {
    pages.push(list.slice(start, start + size))
  }
  return pages
}

/**
 * Un grupo de opciones como tarjetas con foto, de a una.
 *
 * Es un grupo de radios y no una fila de botones: semánticamente se elige una
 * opción entre varias, y las flechas del teclado recorren el grupo como en
 * cualquier radio. Cada tarjeta es el propio radio, con su foto, su rótulo y su
 * aclaración.
 *
 * Quien llama resuelve los textos, porque las dos familias que lo usan los
 * buscan en lugares distintos: la opción de un campo por `service.field.value`
 * y el servicio por su propio código.
 */
export function OptionCards(props: Props) {
  return props.items.length > VISIBLE_AT_ONCE ? <PagedCards {...props} /> : <AllCards {...props} />
}

function AllCards({
  items,
  value,
  onChange,
  id,
  labelledBy,
  describedBy,
  invalid,
  className,
}: Props) {
  return (
    <RadioGroupPrimitive.Root
      id={id}
      tabIndex={-1}
      value={value}
      onValueChange={onChange}
      aria-labelledby={labelledBy}
      aria-describedby={describedBy}
      aria-invalid={invalid || undefined}
      className={cn(GRID, className)}
    >
      {items.map((item) => (
        <Card key={item.value} item={item} invalid={invalid} />
      ))}
    </RadioGroupPrimitive.Root>
  )
}

/**
 * Las tarjetas de a dos filas por página, con flechas y puntos debajo.
 *
 * Todas las páginas están en el DOM, una al lado de la otra, y se corre la
 * tira: el grupo sigue siendo uno solo, así que las flechas del teclado lo
 * recorren entero y un lector de pantalla oye todas las opciones. Cuando el
 * foco cae en una tarjeta de otra página, la página la sigue.
 *
 * `className` no aplica acá: las columnas de la página tienen que ser las que
 * `useColumns` sabe contar.
 */
function PagedCards({ items, value, onChange, id, labelledBy, describedBy, invalid }: Props) {
  const columns = useColumns()
  const size = columns * PAGE_ROWS
  const pages = chunk(items, size)
  const chosen = items.findIndex((item) => item.value === value)
  const chosenPage = chosen < 0 ? 0 : Math.floor(chosen / size)
  const [page, setPage] = useState(chosenPage)
  const touchStart = useRef<number | null>(null)

  // Con otro ancho cambia cuántas entran por página: se vuelve a la de la
  // opción elegida, que es la que la persona estaba mirando.
  useEffect(() => setPage(chosenPage), [size, chosenPage])

  const current = Math.min(page, pages.length - 1)
  const go = (target: number) => setPage(Math.max(0, Math.min(target, pages.length - 1)))

  const onTouchStart = (event: TouchEvent) => {
    touchStart.current = event.touches[0]?.clientX ?? null
  }
  const onTouchEnd = (event: TouchEvent) => {
    const start = touchStart.current
    touchStart.current = null
    const end = event.changedTouches[0]?.clientX
    if (start === null || end === undefined || Math.abs(end - start) < SWIPE_DISTANCE) return
    go(end < start ? current + 1 : current - 1)
  }

  return (
    <div className="flex flex-col gap-3">
      <RadioGroupPrimitive.Root
        id={id}
        tabIndex={-1}
        value={value}
        onValueChange={onChange}
        aria-labelledby={labelledBy}
        aria-describedby={describedBy}
        aria-invalid={invalid || undefined}
        // `clip` y no `hidden`: con `hidden` el navegador desplaza la caja
        // para mostrar una tarjeta enfocada de otra página, y la tira queda
        // corrida dos veces. El relleno deja ver el anillo de foco de las
        // tarjetas del borde.
        className="-mx-1 overflow-x-clip px-1"
        onTouchStart={onTouchStart}
        onTouchEnd={onTouchEnd}
      >
        <div
          // Entre páginas hay el doble del relleno: la página de al lado no
          // asoma por el margen que deja ver el anillo.
          className="flex gap-2 transition-transform duration-300 ease-out motion-reduce:transition-none"
          style={{ transform: `translateX(calc(${-current} * (100% + 0.5rem)))` }}
        >
          {pages.map((pageItems, index) => (
            <div
              key={pageItems[0]?.value ?? index}
              className={cn(GRID, 'w-full shrink-0 content-start')}
              onFocus={() => setPage(index)}
            >
              {pageItems.map((item) => (
                <Card key={item.value} item={item} invalid={invalid} />
              ))}
            </div>
          ))}
        </div>
      </RadioGroupPrimitive.Root>
      <div className="flex items-center justify-between gap-3">
        <div className="flex items-center gap-1.5">
          {pages.map((pageItems, index) => (
            <button
              key={pageItems[0]?.value ?? index}
              type="button"
              aria-label={t('ai.cards.goToPage', { page: index + 1, pages: pages.length })}
              aria-current={index === current || undefined}
              onClick={() => go(index)}
              className={cn(
                'size-2 rounded-full transition-colors',
                'focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none',
                index === current ? 'bg-foreground' : 'bg-foreground/20 hover:bg-foreground/40'
              )}
            />
          ))}
        </div>
        <div className="flex items-center gap-1">
          {/* Cuántas hay a la vista y cuántas en total: el recorte se dice. */}
          <span className="text-muted-foreground mr-2 text-xs tabular-nums">
            {t('ai.cards.shown', {
              shown: pages[current]?.length ?? 0,
              total: items.length,
            })}
          </span>
          <Button
            type="button"
            variant="outline"
            size="icon-sm"
            aria-label={t('ai.cards.previous')}
            disabled={current === 0}
            onClick={() => go(current - 1)}
          >
            <ChevronLeft />
          </Button>
          <Button
            type="button"
            variant="outline"
            size="icon-sm"
            aria-label={t('ai.cards.next')}
            disabled={current === pages.length - 1}
            onClick={() => go(current + 1)}
          >
            <ChevronRight />
          </Button>
        </div>
      </div>
    </div>
  )
}

function Card({ item, invalid }: { item: CardItem; invalid?: boolean }) {
  return (
    <RadioGroupPrimitive.Item
      value={item.value}
      className={cn(
        'bg-card text-card-foreground group/option flex flex-col overflow-hidden rounded-lg border text-left transition-colors',
        'hover:border-foreground/40 focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none',
        // Elegida: el pie en gris y el mismo borde que al pasar el puntero.
        // Sin anillo ni borde oscuro: la foto ya es lo que llama la atención.
        'data-[state=checked]:border-foreground/40 data-[state=checked]:bg-muted',
        invalid && 'border-destructive'
      )}
    >
      <img
        src={item.image}
        alt=""
        loading="lazy"
        className="bg-muted aspect-video w-full object-cover"
      />
      <span className="flex flex-col gap-0.5 px-3 py-2">
        <span className="font-medium">{item.label}</span>
        {item.description ? (
          <span className="text-muted-foreground text-xs">{item.description}</span>
        ) : null}
      </span>
    </RadioGroupPrimitive.Item>
  )
}
