import { router, usePage } from '@inertiajs/react'
import { ChevronDown } from 'lucide-react'
import { useEffect, useState } from 'react'
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'

import { Section } from '@/components/Section'
import { Button } from '@/components/ui/button'
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from '@/components/ui/collapsible'
import {
  ChartContainer,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
  type ChartConfig,
} from '@/components/ui/chart'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { formatNumber } from '@/lib/format'
import { locale, t } from '@/lib/i18n'

/**
 * La actividad de la cuenta en el tiempo, dentro del panel de «Actividad» (V0).
 *
 * Es el mismo gráfico de área del bloque `dashboard-01` —su `ChartContainer`,
 * sus gradientes, el `ToggleGroup` de rangos y el `Select` para pantallas
 * angostas—; lo que cambió es de dónde salen los datos, qué se afirma con ellos
 * y **dónde vive**. Ya no está en la primera pantalla: un gráfico es detalle, y
 * el detalle es bajo demanda. No se borró del producto, se mudó al panel del
 * aspecto que explica.
 *
 * Tampoco lleva `Card` propia. Una `Section` con exactamente una `Card` adentro
 * es un marco doble, y el título de la tarjeta ni siquiera era un encabezado:
 * `CardTitle` rinde un `<div>`, así que este bloque no tenía etiqueta semántica
 * ninguna.
 *
 * **El gráfico no mide resultados.** Consultar más URLs no indexa más URLs. Las
 * dos series miden cosas distintas y por eso van juntas: las consultas son
 * esfuerzo —cuántas veces le preguntamos a Google por una URL— y los cambios son
 * hallazgo —cuántas contestaron algo distinto de lo que ya sabíamos—. Una sola de
 * las dos se malinterpreta: las consultas solas parecen resultado y los cambios
 * solos no dicen sobre cuánto trabajo salieron. Eso se dice **antes** del
 * dibujo y no al pie: leerlo después de haber mirado el gráfico llega tarde,
 * porque la interpretación ya se hizo.
 *
 * Las series **no se apilan**, a diferencia del demo. Apilarlas dibujaría una
 * suma —consultas más cambios— que no cuenta nada: un cambio de estado es el
 * resultado de una consulta, no otra unidad de trabajo al lado.
 */

export interface ActivityPoint {
  /** Día de calendario en `AAAA-MM-DD`, ya cortado en la zona de presentación. */
  date: string
  queries: number
  changes: number
}

export interface Activity {
  days: number
  /** Los rangos que ofrece el servidor. Un valor fuera de la lista lo ignora. */
  ranges: number[]
  /** Un punto por día, con los días sin actividad en cero y no salteados. */
  series: ActivityPoint[]
  total_queries: number
  total_changes: number
  /** Lotes que arrancaron hoy. Es la cifra del aspecto, no una serie del gráfico. */
  batches_today: number
}

/**
 * Las dos series y sus colores.
 *
 * Los rótulos se resuelven en el render y no acá: esto se arma al importar el
 * módulo, que es antes de que el catálogo esté instalado.
 */
function chartConfigOf(): ChartConfig {
  return {
    queries: {
      label: t('activity.series.queries'),
      color: 'var(--primary)',
    },
    changes: {
      label: t('activity.series.changes'),
      color: 'var(--chart-2)',
    },
  }
}

/** Eje y tabla: día y mes alcanzan, el año lo da el rango. */
function shortDay(): Intl.DateTimeFormat {
  return new Intl.DateTimeFormat(locale(), {
    day: 'numeric',
    month: 'short',
    timeZone: 'UTC',
  })
}

/** Globo del gráfico: ahí sí conviene el día completo con su año. */
function longDay(): Intl.DateTimeFormat {
  return new Intl.DateTimeFormat(locale(), {
    weekday: 'long',
    day: 'numeric',
    month: 'long',
    year: 'numeric',
    timeZone: 'UTC',
  })
}

export function ChartAreaInteractive({ activity }: { activity: Activity }) {
  const { url } = usePage()
  const reducedMotion = useReducedMotion()
  const range = String(activity.days)
  const hasActivity = activity.total_queries > 0 || activity.total_changes > 0

  /*
    El rango elegido viaja en la cadena de consulta y no en el estado del
    componente: es lo que el servidor lee para armar la serie, y además hace que
    el rango sobreviva a recargar y que el enlace se pueda compartir (RT-09).

    Se pisa `days` sobre la dirección vigente en vez de armar una nueva: el
    gráfico vive dentro del panel de actividad, que también vive en la dirección
    (`?panel=activity`). Reconstruyéndola desde la ruta, cambiar el rango cerraba
    el panel donde estaba el gráfico que se acababa de tocar.
  */
  const changeRange = (value: string) => {
    if (!value || value === range) return

    const [path, search] = url.split('?')
    const query = new URLSearchParams(search)
    // `days` es el nombre con el que el servidor lee el rango: no se traduce.
    query.set('days', value)

    router.get(
      `${path}?${query.toString()}`,
      {},
      { preserveScroll: true, preserveState: true, only: ['activity'] }
    )
  }

  return (
    <Section
      title={{ text: t('activity.title') }}
      description={`${periodSummary(activity)} ${t('activity.howToRead')}`}
      actions={
        <>
          <ToggleGroup
            type="single"
            value={range}
            onValueChange={changeRange}
            variant="outline"
            size="sm"
            aria-label={t('activity.rangeLabel')}
            className="hidden sm:flex"
          >
            {activity.ranges.map((days) => (
              <ToggleGroupItem key={days} value={String(days)}>
                {t('activity.range.short', { days })}
              </ToggleGroupItem>
            ))}
          </ToggleGroup>
          {/* La misma decisión en angosto, no otro control: es el mismo eje
              con la presentación que entra en el ancho que hay. */}
          <Select value={range} onValueChange={changeRange}>
            <SelectTrigger
              className="flex w-40 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate sm:hidden"
              size="sm"
              aria-label={t('activity.rangeLabel')}
            >
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {activity.ranges.map((days) => (
                <SelectItem key={days} value={String(days)}>
                  {t('activity.range.long', { days })}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </>
      }
    >
      <div className="flex flex-col gap-3">
        {hasActivity ? (
          <ChartContainer config={chartConfigOf()} className="aspect-auto h-62.5 w-full">
            <AreaChart data={activity.series} accessibilityLayer>
              <defs>
                <linearGradient id="fillQueries" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="5%" stopColor="var(--color-queries)" stopOpacity={0.6} />
                  <stop offset="95%" stopColor="var(--color-queries)" stopOpacity={0.05} />
                </linearGradient>
                <linearGradient id="fillChanges" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="5%" stopColor="var(--color-changes)" stopOpacity={0.5} />
                  <stop offset="95%" stopColor="var(--color-changes)" stopOpacity={0.05} />
                </linearGradient>
              </defs>
              <CartesianGrid vertical={false} />
              <XAxis
                dataKey="date"
                tickLine={false}
                axisLine={false}
                tickMargin={8}
                minTickGap={32}
                tickFormatter={(value: string) => shortDay().format(asDate(value))}
              />
              <YAxis
                tickLine={false}
                axisLine={false}
                width={48}
                allowDecimals={false}
                tickFormatter={(value: number) => formatNumber(value)}
              />
              <ChartTooltip
                cursor={false}
                content={
                  <ChartTooltipContent
                    indicator="dot"
                    labelFormatter={(value) => longDay().format(asDate(String(value)))}
                    formatter={(value, name) => (
                      <div className="flex flex-1 items-center justify-between gap-4 leading-none">
                        <span className="text-muted-foreground">
                          {chartConfigOf()[name as keyof ReturnType<typeof chartConfigOf>]?.label ??
                            name}
                        </span>
                        <span className="text-foreground font-mono font-medium tabular-nums">
                          {formatNumber(Number(value))}
                        </span>
                      </div>
                    )}
                  />
                }
              />
              <ChartLegend content={<ChartLegendContent />} />
              {/*
                Las consultas van primero y los cambios encima: los cambios son
                siempre el subconjunto más chico, y dibujarlos abajo los dejaría
                tapados por el área de las consultas justo en los días de más
                trabajo.
              */}
              <Area
                dataKey="queries"
                type="monotone"
                fill="url(#fillQueries)"
                stroke="var(--color-queries)"
                isAnimationActive={!reducedMotion}
              />
              <Area
                dataKey="changes"
                type="monotone"
                fill="url(#fillChanges)"
                stroke="var(--color-changes)"
                // La raya discontinua distingue las dos series en escala de
                // grises: la paleta del producto es monocroma y el color solo no
                // comunica nada (RT-04).
                strokeDasharray="5 3"
                isAnimationActive={!reducedMotion}
              />
            </AreaChart>
          </ChartContainer>
        ) : (
          <p className="text-muted-foreground text-sm text-pretty">
            {t('activity.empty', { days: activity.days })}
          </p>
        )}

        {/*
          El equivalente textual del dibujo. Un SVG sin equivalente no comunica
          nada a quien no lo ve, y acá la tabla es además la forma de leer una
          cifra exacta sin perseguir el globo con el mouse.
        */}
        {hasActivity ? <ActivityTable activity={activity} /> : null}
      </div>
    </Section>
  )
}

/**
 * La tabla equivalente, plegada.
 *
 * Va plegada y no oculta a lector de pantalla: quien no puede leer el gráfico no
 * es sólo quien no lo ve, también es quien necesita la cifra exacta de un día.
 *
 * Sobre `Collapsible` y no sobre un `<details>` a mano: es un solo bloque
 * secundario que se pliega, que es exactamente para lo que existe la primitiva.
 * El `<details>` dibujado a mano sólo sobrevive en el detalle técnico de un
 * error del servidor.
 */
function ActivityTable({ activity }: { activity: Activity }) {
  return (
    <Collapsible className="group/collapsible w-full">
      <CollapsibleTrigger asChild>
        <Button variant="link" size="sm" className="px-0">
          {t('activity.seeTable')}
          <ChevronDown
            data-icon="inline-end"
            aria-hidden
            className="transition-transform duration-200 group-data-[state=open]/collapsible:rotate-180 motion-reduce:transition-none"
          />
        </Button>
      </CollapsibleTrigger>

      <CollapsibleContent className="mt-3 max-h-64 overflow-y-auto overscroll-contain rounded-lg border">
        <table className="w-full text-sm">
          <caption className="sr-only">{t('activity.caption', { days: activity.days })}</caption>
          <thead className="bg-muted/50 sticky top-0">
            <tr className="text-muted-foreground text-xs">
              <th scope="col" className="px-3 py-2 text-left font-medium">
                {t('activity.column.day')}
              </th>
              <th scope="col" className="px-3 py-2 text-right font-medium">
                {t('activity.series.queries')}
              </th>
              <th scope="col" className="px-3 py-2 text-right font-medium">
                {t('activity.series.changes')}
              </th>
            </tr>
          </thead>
          <tbody>
            {activity.series.map((point) => (
              <tr key={point.date} className="border-border/60 border-t">
                <th scope="row" className="px-3 py-1.5 text-left font-normal whitespace-nowrap">
                  <time dateTime={point.date}>{shortDay().format(asDate(point.date))}</time>
                </th>
                <td className="px-3 py-1.5 text-right tabular-nums">
                  {formatNumber(point.queries)}
                </td>
                <td className="px-3 py-1.5 text-right tabular-nums">
                  {formatNumber(point.changes)}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </CollapsibleContent>
    </Collapsible>
  )
}

/**
 * Qué se hizo en el período, en una frase.
 *
 * Es el resumen en texto de lo que dibuja el gráfico: sin él, la única forma de
 * saber cuánto trabajo hubo es estimar el área a ojo.
 */
function periodSummary(activity: Activity): string {
  return t('activity.summary', {
    days: activity.days,
    queries: formatNumber(activity.total_queries),
    changes: formatNumber(activity.total_changes),
  })
}

/**
 * El día viaja como fecha de calendario, no como instante.
 *
 * `AAAA-MM-DD` se interpreta en UTC y se formatea en UTC a propósito: el
 * servidor ya cortó cada día en la zona de presentación, así que volver a
 * convertirlo correría medio calendario un día hacia atrás.
 */
function asDate(day: string): Date {
  const [year, month, dayOfMonth] = day.split('-').map(Number)
  return new Date(Date.UTC(year || 1970, (month || 1) - 1, dayOfMonth || 1))
}

/**
 * Si el sistema pide menos movimiento, el gráfico no se dibuja animándose.
 *
 * Recharts anima por defecto y no mira la preferencia del sistema, así que hay
 * que pasársela (RT-14). El valor inicial se lee al montar para que la primera
 * pintura ya salga quieta: leerlo recién en el efecto dejaría pasar justo la
 * animación de entrada, que es la que molesta.
 */
function useReducedMotion(): boolean {
  const [reduced, setReduced] = useState(
    () => window.matchMedia('(prefers-reduced-motion: reduce)').matches
  )

  useEffect(() => {
    const query = window.matchMedia('(prefers-reduced-motion: reduce)')
    const onChange = () => setReduced(query.matches)
    query.addEventListener('change', onChange)
    return () => query.removeEventListener('change', onChange)
  }, [])

  return reduced
}
