import { Link } from '@inertiajs/react'
import { AlertCircleIcon } from 'lucide-react'
import { useEffect, useState } from 'react'

import { Skeleton } from '@/components/ui/skeleton'
import { splitAddress } from '@/lib/address'
import { formatNumber } from '@/lib/format'
import { t } from '@/lib/i18n'
import { route } from '@/lib/routes'
import { cn } from '@/lib/utils'
import type { Metrics } from '@/pages/Seo/types'

type CannibalizedPage = Metrics & { page: string }

interface Props {
  /** La consulta cuyas URLs se piden. Es lo único que identifica al pedido. */
  query: string
  /** El host del sitio, para recortar el origen que todas las filas repiten. */
  hostname: string
}

/** Cuántos esqueletos se dibujan mientras carga. Cuatro es el largo corriente. */
const SKELETON_ROWS = 4

/**
 * A partir de cuántas URLs la lista se acota y se desplaza por dentro.
 *
 * Umbral y no altura fija: una consulta con tres páginas se dibuja entera y sin
 * barra, y sólo la de dieciocho —que existe en el sitio real— se convierte en
 * una región desplazable en vez de empujar las demás filas fuera de la vista.
 */
const SCROLL_AFTER = 8

/**
 * Las URLs que se pisan por una consulta, **pedidas al abrir la fila**.
 *
 * Es el detalle de una fila de la tabla de canibalización, y se carga por red en
 * vez de venir en el props de la pantalla. El motivo es de escala: la pantalla
 * lista decenas de consultas afectadas, cada una con su lista de páginas, y de
 * todas ésas alguien abre dos o tres. Mandarlas todas era traer cientos de filas
 * para dibujar unas pocas.
 *
 * **No guarda lo que trajo.** La tabla desmonta este componente al cerrar la
 * fila, así que reabrirla vuelve a pedir. Es a propósito: una caché acá crecería
 * con cada fila que alguien abrió en la sesión, y el dato que evita repetir es
 * de una pantalla que se mira una vez.
 *
 * `fetch` y no una biblioteca: no hay cliente HTTP en el proyecto, y las tres
 * pantallas que ya piden algo por red lo hacen así. En un GET ni siquiera hace
 * falta el token CSRF; `credentials` sí, porque la vista pide sesión.
 */
export function CannibalizationPages({ query, hostname }: Props) {
  const [pages, setPages] = useState<CannibalizedPage[] | null>(null)
  const [failed, setFailed] = useState(false)

  useEffect(() => {
    // Se cancela al desmontar: cerrar la fila mientras el pedido está en vuelo
    // dejaría un `setState` sobre un componente que ya no existe.
    const controller = new AbortController()

    setPages(null)
    setFailed(false)

    fetch(`${route('seo.cannibalization.pages')}?q=${encodeURIComponent(query)}`, {
      credentials: 'same-origin',
      headers: { Accept: 'application/json' },
      signal: controller.signal,
    })
      .then((response) => {
        if (!response.ok) throw new Error(String(response.status))
        return response.json()
      })
      .then((body) => setPages(body.pages))
      .catch((error: unknown) => {
        // Abortar es lo que hace esta pantalla al cerrar una fila, no un fallo.
        if (error instanceof DOMException && error.name === 'AbortError') return
        setFailed(true)
      })

    return () => controller.abort()
  }, [query])

  if (failed) {
    return (
      <Frame>
        {/*
          El error se dice acá adentro y no como un aviso de la pantalla: lo que
          falló es el detalle de una fila, y un cartel arriba de todo haría
          pensar que falló la tabla entera.
        */}
        <p
          role="alert"
          className="text-muted-foreground flex items-center gap-2 px-4 py-3 text-sm text-pretty"
        >
          <AlertCircleIcon aria-hidden className="size-4 shrink-0" />
          {t('seo.cannibalization.pages.failed')}
        </p>
      </Frame>
    )
  }

  if (pages === null) {
    return (
      <Frame>
        {/*
          El encabezado se dibuja de una y no como esqueleto: no depende de lo
          que traiga la red, y un hueco gris donde ya se sabe qué va a decir es
          un salto de más al llegar la respuesta (RT-12).
        */}
        <ul className="divide-border/60 divide-y">
          <ListHeader />
          {Array.from({ length: SKELETON_ROWS }, (_unused, index) => (
            <PageSkeleton key={index} />
          ))}
        </ul>
      </Frame>
    )
  }

  return (
    <Frame>
      {/*
        Ordenadas por CTR, que lo decide el servidor: la pregunta de esta lista
        es cuál conservar, y la que más se clickea es la que quien busca prefiere
        cuando se la ofrecen. Las impresiones desempatan, o una página con dos
        apariciones y un clic se pondría arriba de la que se lleva el tráfico.
      */}
      <ul
        className={cn(
          'divide-border/60 divide-y',
          pages.length > SCROLL_AFTER && 'max-h-80 overflow-y-auto overscroll-contain'
        )}
      >
        <ListHeader />
        {pages.map((page) => (
          <PageRow key={page.page} page={page} hostname={hostname} />
        ))}
      </ul>
    </Frame>
  )
}

/**
 * El fondo del detalle, que lo separa de las filas.
 *
 * `bg-muted/40` y no un borde: la fila de detalle ocupa el ancho entero de la
 * tabla y un borde alrededor la haría leerse como una caja adentro de otra. Un
 * fondo apenas distinto dice «esto cuelga de la fila de arriba» sin dibujar
 * nada.
 */
function Frame({ children }: { children: React.ReactNode }) {
  // Sin relleno horizontal: el encabezado de la lista lleva fondo lleno y tiene
  // que llegar a los bordes del detalle. Una barra de color con márgenes a los
  // costados se lee como un botón ancho y no como un encabezado. El relleno lo
  // pone cada renglón, que es lo que además los alinea con esa barra.
  return <div className="bg-muted/40 pb-2">{children}</div>
}

/**
 * El renglón que nombra las columnas.
 *
 * Con encabezado propio, **los rótulos salen de cada renglón**. Antes cada fila
 * repetía «impresiones», «Posición» y «CTR», y dieciocho filas eran cincuenta y
 * cuatro palabras dibujadas para decir tres cosas: la lista se leía como un
 * bloque de texto en vez de como una columna de cifras.
 *
 * Fondo lleno para que se separe **de sus propios renglones y del encabezado de
 * la tabla que la contiene**, que es blanco y está unos píxeles más arriba. Dos
 * encabezados del mismo color, uno adentro del otro, se leen como uno solo
 * partido al medio.
 *
 * `sticky`: cuando la lista se acota y se desplaza, el encabezado se queda. Sin
 * eso, la consulta con dieciocho páginas pierde los nombres de columna en el
 * primer scroll, que es justo cuando más se necesitan.
 */
function ListHeader() {
  return (
    <li className="bg-primary text-primary-foreground sticky top-0 z-10 flex items-center gap-x-4 px-4 py-2 text-xs font-medium">
      <span className="w-24 shrink-0">{t('seo.column.impressionsClicks')}</span>
      <span className="min-w-0 flex-1">{t('seo.column.page')}</span>
      <span className="w-14 shrink-0 text-right">{t('seo.column.position')}</span>
      <span className="w-14 shrink-0 text-right">{t('seo.column.ctr')}</span>
    </li>
  )
}

/**
 * Una URL que compite, en un renglón.
 *
 * Era una grilla de tarjetas y ahora es una lista, que es lo que corresponde: la
 * tarjeta es una caja para agrupar cosas distintas, y acá las cuatro o dieciocho
 * URLs son lo mismo repetido. Cuatro cajas con borde para cuatro renglones
 * hacían ver el marco antes que el dato.
 *
 * La anatomía es la del listado de movimientos: **el bloque de la izquierda
 * identifica el peso**, el del medio dice de qué es, y la derecha trae lo que se
 * compara entre filas. Los nombres de cada cosa los pone `ListHeader`.
 */
function PageRow({ page, hostname }: { page: CannibalizedPage; hostname: string }) {
  const { origin, rest } = splitAddress(page.page, hostname)

  return (
    <li className="flex flex-wrap items-center gap-x-4 gap-y-2 px-4 py-2.5">
      {/*
        Las dos cifras una al lado de la otra y no una debajo de la otra: los
        clics se leen **contra** las impresiones —cuántas de las veces que
        apareció entró alguien— y esa comparación se hace de un vistazo en
        horizontal. Apiladas, la de abajo se leía como una etiqueta de la de
        arriba.

        Las impresiones grandes hacen de ancla: son la cifra que dice cuánto pesa
        esta URL en el problema. Los clics chicos al lado, pero en el color del
        texto y no apagados: es un dato, no una acotación.
      */}
      <p className="flex w-24 shrink-0 items-baseline gap-1.5">
        <span className="text-lg leading-none font-semibold tabular-nums">
          {formatNumber(page.impressions)}
        </span>
        <span className="text-xs tabular-nums">{formatNumber(page.clicks)}</span>
      </p>

      {/*
        El origen se repite en todos los renglones, así que no distingue nada:
        gastaría el ancho que necesita el camino, que es lo único que cambia. Va
        en `sr-only` para que la dirección se siga leyendo entera, con la misma
        anatomía que la celda de cobertura.

        La portada es el único caso donde lo visible no es un trozo de la
        dirección: su camino es `/`, que solo se lee como un renglón a medio
        cargar. Ahí el `sr-only` lleva la dirección entera y el rótulo se esconde
        del lector, que si no anunciaría «https://… Portada», que no es ninguna
        dirección.
      */}
      <Link
        href={`${route('seo.page')}?url=${encodeURIComponent(page.page)}`}
        className="min-w-0 flex-1 truncate text-sm font-medium hover:underline"
        title={page.page}
        translate="no"
      >
        {rest === '/' ? (
          <>
            <span className="sr-only">{page.page}</span>
            {/* La misma clave que la tabla de cobertura: es el mismo concepto y
                el mismo texto, y una entrada duplicada en el catálogo se
                desincroniza en la primera corrección. */}
            <span aria-hidden>{t('coverage.url.home')}</span>
          </>
        ) : (
          <>
            <span className="sr-only">{origin}</span>
            {/* Sin `aria-hidden`: los dos trozos se leen seguidos y forman la
                dirección entera. */}
            <span>{rest}</span>
          </>
        )}
      </Link>

      <p className="w-14 shrink-0 text-right text-sm tabular-nums">
        {page.position.toFixed(1)}
      </p>
      <p className="w-14 shrink-0 text-right text-sm tabular-nums">
        {(page.ctr * 100).toFixed(1)}%
      </p>
    </li>
  )
}

/** El hueco de un renglón, con su misma anatomía para que nada se corra al llegar (RT-12). */
function PageSkeleton() {
  return (
    <li className="flex items-center gap-x-4 px-4 py-2.5" aria-hidden>
      <div className="flex w-24 shrink-0 items-baseline gap-1.5">
        <Skeleton className="h-5 w-8" />
        <Skeleton className="h-3 w-5" />
      </div>
      <Skeleton className="h-4 min-w-0 flex-1" />
      <Skeleton className="h-4 w-14 shrink-0" />
      <Skeleton className="h-4 w-14 shrink-0" />
    </li>
  )
}
