import { useMemo, useState } from 'react'
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts'

import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
import type { ChartConfig } from '@/components/ui/chart'
import { locale, t } from '@/lib/i18n'
import { cn } from '@/lib/utils'
import type { KeywordSeries } from '@/pages/Seo/types'

export type TopMetric = 'impressions' | 'clicks' | 'position'

interface Props {
  /** El eje entero del período. Es lo que convierte un día ausente en un hueco. */
  dates: string[]
  series: KeywordSeries[]
  metric: TopMetric
}

/**
 * Cuánto se apaga una línea que no es la resaltada.
 *
 * No se apagan del todo: la forma del conjunto —si todas suben o si una se
 * despega— es parte de lo que se viene a ver, y borrarlas dejaría una línea
 * suelta sin contra qué compararse.
 */
const DIMMED = 0.15

/** Con nada resaltado, las diez conviven. Al 100 % se tapan entre ellas. */
const RESTING = 0.85

/**
 * Diez colores que se distinguen entre sí.
 *
 * **No sale de `--chart-1` a `--chart-5`**, que es la paleta de gráficos del
 * producto: son cinco y son grises —croma cero, los mismos en claro y en
 * oscuro—. Se probó dibujar las diez líneas en un mismo gris y confiar todo al
 * resaltado, y no alcanza: en reposo el gráfico es una maraña donde no se puede
 * ni contar cuántas series hay.
 *
 * Es una escala **categórica**: diez matices repartidos por la rueda, con la
 * misma claridad y el mismo croma, para que ninguna línea se lea como más
 * importante que otra. La claridad está en el medio a propósito, que es lo que
 * la deja legible sobre fondo claro y sobre fondo oscuro sin dos juegos.
 *
 * Vive acá y no como variables del tema porque es de este gráfico: el día que un
 * segundo lugar necesite una escala categórica, se mueve a `app.css` con su
 * versión para cada tema. Antes de eso serían diez variables globales con un
 * solo consumidor.
 *
 * En orden: rojo, naranja, oliva, verde, turquesa, celeste, azul, violeta,
 * magenta y un marrón apagado para cerrar. Los nombres van acá y no al lado de
 * cada valor porque un comentario en castellano compartiendo renglón con código
 * es lo que el guard del catálogo confunde con texto de pantalla.
 */
const SERIES_COLORS = [
  'oklch(0.62 0.19 25)',
  'oklch(0.70 0.16 60)',
  'oklch(0.72 0.14 100)',
  'oklch(0.68 0.16 145)',
  'oklch(0.68 0.12 190)',
  'oklch(0.63 0.14 230)',
  'oklch(0.55 0.19 265)',
  'oklch(0.58 0.19 300)',
  'oklch(0.62 0.19 340)',
  'oklch(0.50 0.07 40)',
] as const

/**
 * El recorrido de las diez consultas con más impresiones, una métrica por vez.
 *
 * **El color no distingue las líneas, y no es una limitación que se sufre.** La
 * paleta de gráficos del producto son cinco grises —croma cero, los mismos en
 * claro y en oscuro—, así que diez colores distintos exigirían romper el
 * lenguaje visual entero para dar diez identidades que igual no se pueden seguir
 * con la vista: diez trazos se cruzan y ninguno se lee. Lo que hace legible al
 * conjunto es **el resaltado**, que muestra una por vez, que es como se mira de
 * verdad.
 *
 * **El eje de posición va invertido.** En Google el 1 es el mejor lugar, así que
 * con el eje natural una mejora se dibujaría como una caída.
 */
/**
 * «24 ago». Mes y día, sin año.
 *
 * Los veintiocho días caen siempre en el mismo tramo corto, así que el año es
 * una cifra que se repite idéntica en cada marca y no distingue nada. La fecha
 * entera vive en el globo.
 *
 * `timeZone: 'UTC'` **no es un detalle**: Google fecha en horario del Pacífico y
 * éstos son sus días. Sin fijarlo, el navegador los interpreta en la zona de
 * quien mira y el eje se corre un día, que es justo lo que haría que el gráfico
 * no coincida con Search Console.
 */
function shortDate(value: string): string {
  return new Intl.DateTimeFormat(locale(), {
    month: 'short',
    day: 'numeric',
    timeZone: 'UTC',
  }).format(new Date(value))
}

export function TopKeywordsChart({ dates, series, metric }: Props) {
  const [highlighted, setHighlighted] = useState<string | null>(null)

  /*
    Recharts quiere un arreglo plano de puntos donde cada serie es una clave, así
    que el pivoteo se hace acá.

    Las claves son `k0`…`k9` y **no la consulta**: una búsqueda puede traer
    puntos, comillas o barras, y eso como clave de objeto es un problema
    esperando. El nombre vive en `series[i].query`, que es de donde sale el
    rótulo.
  */
  const data = useMemo(() => {
    const byDate = series.map((entry) => new Map(entry.points.map((point) => [point.date, point])))

    return dates.map((date) => {
      const row: Record<string, string | number | null> = { date }
      byDate.forEach((points, index) => {
        // `null` y no cero para el día que Google no reportó: un cero dice «no
        // apareciste» y lo que pasó es «no sabemos».
        row[`k${index}`] = points.get(date)?.[metric] ?? null
      })
      return row
    })
  }, [dates, series, metric])

  /*
    El rótulo de cada serie es la consulta, que es un dato y no texto de
    pantalla: por eso va literal y no por el catálogo.

    El color sale de la posición en la lista, que es estable: llegan ordenadas
    por impresiones desde el servidor y nada las reordena, así que la línea y su
    entrada en la leyenda tienen siempre el mismo.
  */
  const config = useMemo<ChartConfig>(
    () =>
      Object.fromEntries(
        series.map((entry, index) => [
          `k${index}`,
          { label: entry.query, color: SERIES_COLORS[index % SERIES_COLORS.length] },
        ])
      ),
    [series]
  )

  return (
    <div className="flex flex-col gap-4">
      <ChartContainer config={config} className="aspect-auto h-72 w-full">
        <LineChart data={data} accessibilityLayer margin={{ left: 4, right: 4 }}>
          <CartesianGrid vertical={false} />
          <XAxis
            dataKey="date"
            tickLine={false}
            axisLine={false}
            tickMargin={8}
            minTickGap={32}
            tickFormatter={shortDate}
          />
          <YAxis
            // Invertido sólo para la posición. Las otras dos son cuentas y su
            // eje natural es el correcto.
            reversed={metric === 'position'}
            tickLine={false}
            axisLine={false}
            width={44}
            allowDecimals={false}
          />
          {/* El globo fecha igual que el eje: dos formatos para la misma fecha
              en la misma pantalla obligan a traducir de una a la otra. */}
          <ChartTooltip
            content={<ChartTooltipContent labelFormatter={(label) => shortDate(String(label))} />}
          />

          {series.map((entry, index) => {
            const active = highlighted === entry.query

            return (
              <Line
                key={entry.query}
                dataKey={`k${index}`}
                type="monotone"
                stroke={`var(--color-k${index})`}
                strokeWidth={active ? 2.5 : 1.5}
                strokeOpacity={highlighted === null ? RESTING : active ? 1 : DIMMED}
                dot={false}
                // Un día sin dato deja un hueco y no una recta: Google omite las
                // consultas poco frecuentes, así que unir los extremos dibujaría
                // una continuidad que nadie observó.
                connectNulls={false}
                // Sin animación: diez líneas redibujándose en cada cambio de
                // métrica es medio segundo de trazos moviéndose sin informar.
                isAnimationActive={false}
              />
            )
          })}
        </LineChart>
      </ChartContainer>

      <Legend series={series} highlighted={highlighted} onHighlight={setHighlighted} />
    </div>
  )
}

/**
 * La leyenda, que además de nombrar **es el control**.
 *
 * El trazo de color une cada rótulo con su línea, que es lo que hace una leyenda
 * corriente. Lo que agrega ésta es el resaltado: con diez trazos cruzándose, el
 * color solo alcanza para saber cuál es cuál pero no para **seguir** una de
 * punta a punta, y apuntarle apaga las otras nueve.
 *
 * Son botones y no `<span>` con `onMouseEnter`: se llega con el tabulador y el
 * foco resalta igual que el puntero, así que la lectura del gráfico no depende
 * de tener un mouse.
 */
function Legend({
  series,
  highlighted,
  onHighlight,
}: {
  series: KeywordSeries[]
  highlighted: string | null
  onHighlight: (query: string | null) => void
}) {
  return (
    <ul className="flex flex-wrap gap-x-3 gap-y-1.5">
      {series.map((entry, index) => {
        const active = highlighted === entry.query

        return (
          <li key={entry.query}>
            <button
              type="button"
              className={cn(
                'focus-visible:ring-ring flex items-center gap-1.5 rounded px-1 py-0.5 text-xs',
                'focus-visible:ring-2 focus-visible:outline-none',
                active ? 'text-foreground font-medium' : 'text-muted-foreground'
              )}
              // El clic fija el resaltado, para poder mirar el gráfico sin
              // sostener el puntero encima de la leyenda. Volver a apretar
              // suelta.
              onClick={() => onHighlight(active ? null : entry.query)}
              onMouseEnter={() => onHighlight(entry.query)}
              onMouseLeave={() => onHighlight(null)}
              onFocus={() => onHighlight(entry.query)}
              onBlur={() => onHighlight(null)}
              aria-pressed={active}
            >
              {/*
                Un trazo y no un cuadrado: es el mismo dibujo que la línea que
                nombra, y así el ojo lo empareja sin traducir de una forma a la
                otra. Engorda al resaltar, que es lo que hace la línea.
              */}
              <span
                aria-hidden
                className={cn('w-4 shrink-0 rounded-full transition-all', active ? 'h-1' : 'h-0.5')}
                style={{ backgroundColor: SERIES_COLORS[index % SERIES_COLORS.length] }}
              />
              <span translate="no">{entry.query}</span>
            </button>
          </li>
        )
      })}

      {/* Qué hace la leyenda, para quien no lo deduce pasando el puntero. */}
      <li className="text-muted-foreground/70 self-center text-xs">
        {t('seo.top.legendHint')}
      </li>
    </ul>
  )
}
