import { InfiniteScroll, Link, router, usePage } from "@inertiajs/react";
import {
  Check,
  CheckCheck,
  Circle,
  Inbox,
  Info,
  ListFilter,
  TriangleAlert,
} from "lucide-react";
import { useEffect, useState } from "react";

import type { TablePage } from "@/components/DataTable";
import { tableUrl } from "@/components/DataTable";
import { DataTimestamp } from "@/components/DataTimestamp";
import { EmptyState } from "@/components/EmptyState";
import { FilterBar } from "@/components/FilterBar";
import { FilterSelect } from "@/components/FilterSelect";
import { PageIntro } from "@/components/PageIntro";
import { Section } from "@/components/Section";
import { StatusBadge } from "@/components/StatusBadge";
import { AppLayout } from "@/layouts/AppLayout";
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { filterValue } from "@/lib/filters";
import { formatNumber } from "@/lib/format";
import { locale, t } from "@/lib/i18n";
import { storedAction, storedBody, storedTitle } from "@/lib/notices";
import { route } from "@/lib/routes";
import { cn } from "@/lib/utils";

interface NotificationRow {
  id: string;
  kind: string;
  level: string;
  /**
   * La frase que le corresponde, en el catálogo.
   *
   * `LEGACY` es la de los avisos escritos antes de que el producto hablara dos
   * idiomas: llevan su texto adentro de `params` y se muestran tal cual
   * quedaron. Rehacerlos sería reescribir el historial.
   */
  text_key: string;
  params: Record<string, string | number>;
  /** Ruta ya resuelta por el servidor: el destino de un aviso viejo no se recalcula. */
  action_path: string;
  /** Nulo en los avisos de nivel cuenta, que no cuelgan de ninguna propiedad. */
  hostname: string | null;
  created_at: string;
  read_at: string | null;
}

interface Option {
  value: string;
  label: string;
}

interface Props {
  notifications: TablePage<NotificationRow>;
  /** Sin leer de toda la cuenta, no de la página ni del filtro vigente. */
  unread: number;
  /**
   * Cuántos de los sin leer piden una acción, también de la cuenta entera.
   *
   * Llega como prop de la pantalla y no desde las compartidas porque acá la
   * clave `notifications` es la tabla paginada y tapa a la compartida del mismo
   * nombre. Es el mismo conteo: lo que cambia es por dónde entra.
   */
  unread_action: number;
  /** Avisos de la cuenta sin ningún recorte. Separa «no hay ninguno» de «el filtro no encontró». */
  total: number;
  only_unread: boolean;
  options: {
    kind: Option[];
    level: Option[];
  };
}

/**
 * El nivel que pide algo de la persona. El otro es para saber.
 *
 * Es el código del modelo, que es con lo que viaja cada fila. Para meterlo en
 * una dirección hay que pasarlo por `filterValue()`: ahí va la forma pública.
 */
const ACTION_LEVEL = "ACTION";

/**
 * Por qué esta vista es una lista y no `C-11 DataTable`.
 *
 * El servidor manda la tabla paginada, así que `DataTable` entraría sin
 * discusión; lo que no entra es el aviso. Un aviso es un bloque —título, cuerpo
 * de dos renglones y su acción— y no un juego de celdas comparables: puesto en
 * columnas queda una tabla de una sola columna útil con un párrafo adentro, y
 * las tres cosas que `DataTable` aporta se caen solas. El orden lo declara el
 * servidor con una única clave, que además es la de omisión y que la vista no
 * ofrece cambiar, así que no hay `aria-sort` que ganar; la comparación entre
 * filas —el motivo por el que una tabla existe— no aplica cuando cada renglón
 * dice algo distinto; y la distinción entre leído y sin leer necesita tratar el
 * bloque entero, no teñir una celda.
 *
 * Lo que sí se conserva de RT-09 es todo lo que importa: el paginado sigue
 * siendo del servidor, el total está siempre a la vista, y filtros, recorte y
 * página viajan en la cadena de consulta como enlaces de verdad. Los enlaces se
 * arman con `tableUrl()`, que está exportado justamente para las vistas con
 * controles propios: pisan una clave sobre la dirección que el servidor ya
 * saneó en vez de reconstruirla a mano.
 *
 * La lista se agrupa **por día**, dentro del recorte vigente. El servidor ofrece
 * un solo orden y no deja cambiarlo, la pregunta que trae acá —«qué pasó
 * mientras no miraba»— es temporal, y un encabezado de día es un renglón que
 * convierte cincuenta filas indistinguibles en cinco o seis grupos que el ojo
 * puede saltar. Un grupo puede quedar partido entre dos páginas: la página 2
 * arranca con su propio encabezado y eso es honesto, no un defecto.
 */
export default function Notifications({
  notifications,
  unread,
  unread_action,
  total,
  only_unread,
  options,
}: Props) {
  const { app } = usePage().props;
  const timeZone = app.display_timezone;

  const [markingAll, setMarkingAll] = useState(false);
  const filtering = useFiltering();

  // El recorte de sin leer ya viene dentro de `query`: el servidor lo agrega
  // porque no es un filtro declarado y, sin él ahí, cualquier enlace armado
  // sobre esta dirección lo abandonaría al primer clic.
  const view = notifications;

  const kindFilter = view.filters.kind ?? "";
  const levelFilter = view.filters.level ?? "";
  const hasFilters = kindFilter !== "" || levelFilter !== "";

  const markAllRead = () => {
    router.post(
      route("notifications.read_all"),
      {},
      {
        preserveScroll: true,
        onStart: () => setMarkingAll(true),
        onFinish: () => setMarkingAll(false),
      },
    );
  };

  return (
    <AppLayout
      title={t("notifications.title")}
      description={t("notifications.description")}
      actions={
        /*
          Sólo existe si hay algo que marcar (RT-07). No se deja deshabilitada
          con un globo que explique por qué: en cero no hay nada que explicar.

          Tampoco pide confirmación. Marcar leído no destruye nada —los avisos
          quedan guardados y se pueden volver a leer— así que no entra en la
          lista de acciones destructivas de RT-10, y un diálogo acá educaría a
          confirmar sin leer justo donde después sí hace falta.

          El globo dice por qué el número no se vacía solo. Es la pregunta que
          aparece justo acá, y por eso la regla dejó de explicarse arriba de todo
          como si fuera un manual.
        */
        unread > 0 ? (
          <Tooltip>
            <TooltipTrigger asChild>
              <Button
                variant="outline"
                size="sm"
                onClick={markAllRead}
                disabled={markingAll}
                aria-busy={markingAll || undefined}
              >
                <CheckCheck data-icon="inline-start" aria-hidden />
                {markingAll ? t("notifications.marking") : t("notifications.markAllRead")}
                <span className="sr-only">
                  {t('notifications.unreadCount', { total: formatNumber(unread) })}
                </span>
              </Button>
            </TooltipTrigger>
            <TooltipContent>{t('notifications.markAllRead.note')}</TooltipContent>
          </Tooltip>
        ) : null
      }
    >
      {total > 0 ? (
        <section className="flex gap-4 justify-between items-center -mb-4">
          {/* Filter Bar */}
          <FilterBar
            /*
              Sin línea de auditoría, a propósito. La escribe `FilterBar` para las
              listas cuyos ejes están plegados, donde un recorte heredado de un
              enlace guardado no se ve; acá los tres ejes están a la vista con el
              suyo marcado —hasta el plegado, que dice su valor en el disparador—,
              así que la línea sería la tercera vez que se escribe el mismo
              recorte en la misma pantalla. Para el lector de pantalla sigue
              estando, en el encabezado de la lista.
            */
            clearHref={
              hasFilters
                ? tableUrl(view, { kind: null, level: null, page: null })
                : undefined
            }
          >
            {/*
              El recorte va primero y separado de los filtros: es la pregunta que
              trae a esta pantalla —«¿qué me falta mirar?»— y es la dirección a la
              que apunta el menú, no una faceta más del listado. Va como eje de dos
              opciones porque eso es lo que es: sin recorte, o con él.
            */}
            <FilterSelect
              label={t("notifications.filter.read")}
              param="unread"
              table={{
                ...view,
                filters: { ...view.filters, unread: only_unread ? "1" : "" },
              }}
              allLabel={t("notifications.filter.all")}
              options={[
                { value: "1", label: t("notifications.filter.unread"), count: unread },
              ]}
            />

            {/*
              Con buscador declarado y no deducido del largo: los tipos de aviso
              los define el producto y la lista crece cada vez que aparece uno
              nuevo. Hoy son nueve; el umbral automático los tomaría por poco.
            */}
            <FilterSelect
              label={t("batches.filter.kind")}
              param="kind"
              table={view}
              allLabel={t("table.all")}
              options={options.kind}
              searchable
            />

            <FilterSelect
              label={t("notifications.filter.level")}
              param="level"
              table={view}
              allLabel={t("table.all")}
              options={options.level}
            />
          </FilterBar>
          {/* UnreadLine */}
          {total > 0 && (
            <UnreadLine
              unread={unread}
              unreadAction={unread_action}
              total={total}
              view={view}
            />
          )}
        </section>
      ) : null}

      {view.rows.length === 0 && !filtering ? (
        <EmptyList
          view={view}
          total={total}
          onlyUnread={only_unread}
          hasFilters={hasFilters}
        />
      ) : (
        <>
          {/*
            El encabezado nombra el recorte vigente y queda sólo para lectores de
            pantalla: en la pantalla eso ya lo dicen los controles de arriba,
            marcados con `aria-current`.
          */}
          <Section
            title={{
              text: listCaption(
                only_unread,
                labelOf(options.kind, kindFilter),
                labelOf(options.level, levelFilter),
              ),
              isHidden: true,
            }}
          >
            {filtering ? (
              <LoadingList rows={view.rows.length} />
            ) : (
              /*
                La lista se desplaza dentro de sí misma. Sin tope, cincuenta
                avisos empujan los filtros fuera de la pantalla y para cambiar un
                recorte hay que volver arriba.

                Y no se pagina: la tanda siguiente se **agrega** al final. Acá se
                mira hacia abajo buscando desde cuándo pasa algo, y paginar
                obliga a recordar qué había en la página anterior. El botón queda
                al pie de la lista, que es donde se termina de leer.
              */
              <InfiniteScroll
                data="notifications"
                manual
                className="max-h-[60vh] overflow-y-auto pr-1"
                next={({ fetch, loading, hasMore }) =>
                  hasMore ? (
                    <div className="flex justify-center pt-3">
                      <Button
                        variant="outline"
                        size="sm"
                        onClick={fetch}
                        disabled={loading}
                        aria-busy={loading || undefined}
                      >
                        {loading ? t('notifications.loading') : t('notifications.loadMore')}
                      </Button>
                    </div>
                  ) : null
                }
              >
                <div className="flex flex-col gap-4">
                  {groupByDay(view.rows, timeZone).map((group) => (
                    <section key={group.day} className="flex flex-col gap-1">
                      <h3 className="text-muted-foreground text-xs font-medium">
                        {dayHeading(group.day, timeZone)}
                      </h3>

                      {/*
                        Cada aviso es una caja. `AccordionItem` viene con una
                        línea divisoria y nada más —shadcn no trae variante con
                        caja—, así que la caja son las clases de `Card`: el
                        mismo borde y el mismo radio que el resto del producto,
                        para que una lista de avisos no parezca de otra
                        aplicación.
                      */}
                      <Accordion
                        type="multiple"
                        className="rounded-3xl border shadow bg-card mb-3 overflow-hidden"
                      >
                        {group.items.map((item) => (
                          <NotificationEntry
                            key={item.id}
                            item={item}
                            levelLabel={labelOf(options.level, item.level)}
                            kindLabel={labelOf(options.kind, item.kind)}
                          />
                        ))}
                      </Accordion>
                    </section>
                  ))}
                </div>
              </InfiniteScroll>
            )}
          </Section>
        </>
      )}
    </AppLayout>
  );
}

/**
 * La única línea que hace falta leer al entrar.
 *
 * Contesta la pregunta que trae acá y que hoy hay que contar a mano bajando la
 * lista: cuántos piden una acción, no cuántos hay. Por volumen manda el aviso
 * informativo —uno por lote, por dominio y por día—, así que «12 sin leer» no
 * dice si adentro hay algo roto.
 *
 * Los dos conteos salen de la cuenta entera y no de las cincuenta filas que se
 * listan: derivarlos de la página daría una cifra falsa en cuanto haya una
 * segunda, y falsa justo hacia abajo.
 */
function UnreadLine({
  unread,
  unreadAction,
  total,
  view,
}: {
  unread: number;
  unreadAction: number;
  /** Avisos de la cuenta **sin ningún recorte**: es el denominador del «sin leer». */
  total: number;
  view: TablePage<NotificationRow>;
}) {
  if (unread === 0) {
    return (
      <PageIntro
        badge={
          <StatusBadge tone="positive" icon={CheckCheck}>
            {t("notifications.nothingUnread")}
          </StatusBadge>
        }
      />
    );
  }

  return (
    <PageIntro
      badge={
        unreadAction > 0 ? (
          <StatusBadge tone="attention" icon={TriangleAlert}>
            {t("notifications.needAction", {
              count: unreadAction,
              total: formatNumber(unreadAction),
            })}
          </StatusBadge>
        ) : (
          <StatusBadge tone="neutral" icon={Info}>
            {t("notifications.noneNeedAction")}
          </StatusBadge>
        )
      }
      items={[
        // El denominador es la cuenta entera y no lo que quedó del recorte: con
        // un filtro puesto, «9 sin leer de 3» es una cifra que no cierra. Cuántos
        // quedaron con el recorte vigente lo dice la línea de conteo de la lista.
        <span className="tabular-nums">
          {t("notifications.unreadOf", {
            unread: formatNumber(unread),
            total: formatNumber(total),
          })}
        </span>,
        unreadAction > 0 ? (
          <Link
            href={tableUrl(view, {
              unread: 1,
              level: filterValue(ACTION_LEVEL),
              page: null,
            })}
            preserveScroll
            preserveState
            className="focus-visible:ring-ring rounded underline underline-offset-4 focus-visible:ring-2 focus-visible:outline-none"
          >
            {t("notifications.onlyNeedingAction")}
          </Link>
        ) : null,
      ]}
    />
  );
}

/**
 * Un aviso.
 *
 * Lo que pide una acción y lo que sólo informa se distinguen por tres señales
 * independientes del color (RT-04): **la forma del control** —un botón lleno
 * contra un enlace de texto—, el badge con su icono y su palabra, y la cuenta de
 * la primera línea. Un botón lleno contra un texto subrayado se distingue de
 * reojo y a cualquier tamaño; dos botones de peso parecido, no.
 *
 * Leído y sin leer bajan a **una** señal visual —el relleno del ítem— más el
 * badge «Sin leer» para la escala de grises y el lector de pantalla. Se fueron
 * el filo de 4 px y el `font-semibold` del título: cuatro señales para la
 * distinción menos consecuente y una sola para la más consecuente era el
 * problema al revés. Y el peso del título no cambia al marcarlo, porque el
 * título no cambia de importancia.
 *
 * La acción del aviso y «Marcar como leída» son dos controles distintos a
 * propósito. Seguir el enlace lleva a mirar el problema, que no es lo mismo que
 * darlo por visto: si el enlace marcara leído de paso, un clic exploratorio
 * borraría la única señal de que quedaba algo pendiente.
 */
function NotificationEntry({
  item,
  levelLabel,
  kindLabel,
}: {
  item: NotificationRow;
  levelLabel: string;
  kindLabel: string;
}) {
  const [marking, setMarking] = useState(false);

  const isRead = item.read_at !== null;
  const needsAction = item.level === ACTION_LEVEL;

  const title = storedTitle(item);
  const body = storedBody(item);
  const actionLabel = storedAction(item);
  const hasAction = Boolean(item.action_path && actionLabel);

  const markRead = () => {
    router.post(
      route("notification.read", { notification_id: item.id }),
      {},
      {
        // El servidor vuelve a esta misma dirección con su filtro, así que sólo
        // hay que sostener el scroll: la fila se redibuja donde estaba.
        preserveScroll: true,
        onStart: () => setMarking(true),
        onFinish: () => setMarking(false),
      },
    );
  };

  return (
    <AccordionItem value={item.id} className="not-last:border">
      <AccordionTrigger className="items-center gap-3 px-4 hover:no-underline hover:bg-muted transition-colors duration-200 cursor-pointer rounded-t-3xl rounded-b-none">
        {/*
          El nivel se dibuja con icono y palabra, y la palabra es la que mandó el
          servidor: si mañana cambia el rótulo, cambia en un solo lugar.

          **El color sólo mientras no se leyó.** Un aviso ya leído sigue diciendo
          qué era, pero deja de reclamar la vista: teñir la lista entera hace que
          el ámbar deje de significar nada. Y lo informativo no tiene tono propio
          porque los cinco tonos son un juego cerrado: no hay un azul de «para
          saber», y agregarlo sería inventar una familia para no revisar qué
          significa el estado.
        */}
        <StatusBadge
          tone={needsAction && !isRead ? "attention" : "neutral"}
          icon={needsAction ? TriangleAlert : Info}
          className={cn("shrink-0", isRead && "text-muted-foreground")}
        >
          {levelLabel}
        </StatusBadge>

        <div className="flex min-w-0 flex-col gap-0.5 w-full">
          <p className="flex min-w-0 items-center gap-2 w-full">
            {/*
              Sin leer se dice con **forma** y no con color (RT-04): un punto
              lleno antes del título, que sobrevive a la escala de grises. El
              texto accesible lo dice con palabras.
            */}
            {isRead ? null : (
              <>
                <Circle aria-hidden className="size-2 shrink-0 fill-current" />
                <span className="sr-only">{t("notifications.unreadPrefix")}</span>
              </>
            )}
            <span className={cn("truncate")}>{title}</span>
          </p>

          <p className="text-muted-foreground text-xs font-normal w-full flex items-center gap-1">
            <span>{kindLabel}</span>
            {" · "}
            <DataTimestamp value={item.created_at} />
            {isRead ? (
              <div className="ml-auto inline-flex items-center gap-1">
                <Check className="size-3.5" aria-hidden />
                <span className="items-center text-muted-foreground text-xs font-normal">
                  {t("notifications.readAt")} <DataTimestamp value={item.read_at} />
                </span>
              </div>
            ) : null}
          </p>
        </div>
      </AccordionTrigger>

      {/*
        El `className` de `AccordionContent` cae sobre un div que Radix mide y
        al que le fija el alto; meterle un `flex` ahí lo hace pelear con esa
        medida y la apertura sale a los tropezones. La disposición va en un hijo
        propio, y arriba la línea que separa el encabezado del cuerpo.
      */}
      <AccordionContent className="border-t px-4 pt-3">
        <div className="flex flex-col items-start gap-1">
          {body && <span className="text-pretty">{body}</span>}

          <span className="text-muted-foreground flex flex-wrap items-center gap-x-2 text-xs">
            {item.hostname && !title.includes(item.hostname) && (
              <span translate="no" className="wrap-anywhere">
                {item.hostname}
              </span>
            )}
          </span>

          {/*
          Las dos acciones van acá adentro y no en el renglón, que es lo que hace
          que la lista cerrada sea una lista y no una pared de controles. Siguen
          siendo dos a propósito: seguir el enlace lleva a mirar el problema, que
          no es lo mismo que darlo por visto. Si el enlace marcara leído de paso,
          un clic exploratorio borraría la única señal de que quedaba algo
          pendiente.
        */}
          <span className="flex flex-wrap items-center gap-2">
            {hasAction ? (
              <Link href={item.action_path} className="no-underline! hover:underline!">
                {actionLabel}
              </Link>
            ) : null}

            {isRead ? null : (
              <Button
                size="sm"
                variant="ghost"
                onClick={markRead}
                disabled={marking}
                aria-busy={marking || undefined}
              >
                <Check aria-hidden />
                {marking ? t("notifications.marking") : t("notifications.markRead")}
                <span className="sr-only"> — {title}</span>
              </Button>
            )}
          </span>
        </div>
      </AccordionContent>
    </AccordionItem>
  );
}

/**
 * El esqueleto de la lista mientras llega el recorte nuevo.
 *
 * Conserva la altura de lo que había: sin esto, cambiar un filtro deja la lista
 * vieja quieta hasta que llega la respuesta y después salta a otra cosa, y no
 * hay forma de saber si algo estaba pasando (contrato §3.9).
 */
function LoadingList({ rows }: { rows: number }) {
  return (
    <div className="flex flex-col gap-2" aria-hidden>
      {Array.from({ length: Math.max(rows, 3) }, (_unused, index) => (
        <Skeleton key={index} className="h-19 w-full" />
      ))}
    </div>
  );
}

/**
 * Si hay una navegación de lectura en curso sobre esta pantalla.
 *
 * Sólo los `GET`: marcar un aviso como leído es un `POST` que vuelve a la misma
 * dirección, y su confirmación es el redibujado en el lugar (RT-08). Meterlo acá
 * convertiría cada tilde en un parpadeo de la lista entera.
 */
function useFiltering(): boolean {
  const [filtering, setFiltering] = useState(false);

  useEffect(() => {
    const started = router.on("start", (event) => {
      if (event.detail.visit.method === "get") setFiltering(true);
    });
    const finished = router.on("finish", () => setFiltering(false));

    return () => {
      started();
      finished();
    };
  }, []);

  return filtering;
}

/**
 * La etiqueta que mandó el servidor, o el valor crudo si llegó uno que no declaró.
 *
 * Acepta las dos formas del mismo valor y por eso normaliza antes de comparar:
 * el recorte vigente llega como forma pública —sale de la dirección— y cada fila
 * trae su código. Comparar sin normalizar hacía que la etiqueta no se encontrara
 * y saliera `EXPORT_READY` en pantalla.
 */
function labelOf(options: Option[], value: string): string {
  if (!value) return "";
  const wanted = filterValue(value);
  return options.find((option) => option.value === wanted)?.label ?? value;
}

/** Qué lista es, con su recorte y sus filtros nombrados. */
function listCaption(
  onlyUnread: boolean,
  kindLabel: string,
  levelLabel: string,
): string {
  const parts = [
    onlyUnread ? t("notifications.caption.unread") : t("notifications.caption.all"),
  ];
  if (kindLabel) parts.push(t("notifications.caption.ofKind", { label: kindLabel }));
  if (levelLabel) parts.push(t("notifications.caption.ofLevel", { label: levelLabel }));
  return t("notifications.caption", { parts: parts.join(" ") });
}

/* ── La agrupación por día ────────────────────────────────────────────────── */

export interface DayGroup<T> {
  /** El día como fecha de calendario, `AAAA-MM-DD`, en la zona de la cuenta. */
  day: string;
  items: T[];
}

/**
 * Parte la página en días, respetando el orden que mandó el servidor.
 *
 * El corte es el día **en la zona de la cuenta** y no en la del navegador: es la
 * misma con la que se cortan el cupo y las series del tablero, y dos cortes
 * distintos harían que un aviso de las once de la noche apareciera bajo «Hoy»
 * en una pantalla y bajo «Ayer» en otra.
 *
 * No reordena ni junta días separados: si el servidor devolvió las filas del más
 * nuevo al más viejo, los grupos salen en ese mismo orden y cada uno con lo suyo.
 */
export function groupByDay<T extends { created_at: string }>(
  rows: T[],
  timeZone: string,
): DayGroup<T>[] {
  const groups: DayGroup<T>[] = [];

  for (const row of rows) {
    const day = dayKey(row.created_at, timeZone);
    const last = groups[groups.length - 1];

    if (last && last.day === day) last.items.push(row);
    else groups.push({ day, items: [row] });
  }

  return groups;
}

/** El día de un instante, en la zona de la cuenta. `en-CA` sólo porque emite `AAAA-MM-DD`. */
function dayKey(iso: string, timeZone: string): string {
  return new Intl.DateTimeFormat("en-CA", {
    timeZone,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  }).format(new Date(iso));
}

/**
 * El rótulo del grupo: «Hoy», «Ayer», o el día con su nombre.
 *
 * `Hoy` y `Ayer` son los dos únicos relativos. A partir de ahí el nombre del día
 * ubica mejor que «hace 4 días», que obliga a contar con los dedos.
 */
export function dayHeading(day: string, timeZone: string): string {
  const today = dayKey(new Date().toISOString(), timeZone);
  const yesterday = dayKey(
    new Date(Date.now() - 86_400_000).toISOString(),
    timeZone,
  );

  if (day === today) return t("time.today");
  if (day === yesterday) return t("time.yesterday");

  const [year, month, dayOfMonth] = day.split("-").map(Number);

  return new Intl.DateTimeFormat(locale(), {
    weekday: "long",
    day: "numeric",
    month: "long",
    // El día ya viene cortado en la zona de la cuenta, así que se formatea en
    // UTC: volver a convertirlo correría la mitad del calendario un día atrás.
    timeZone: "UTC",
  }).format(
    new Date(Date.UTC(year || 1970, (month || 1) - 1, dayOfMonth || 1)),
  );
}

/**
 * Los tres vacíos de esta pantalla, que no son el mismo.
 *
 * «No tenés ningún aviso» describe una cuenta nueva y explica qué va a
 * aparecer. «No te queda nada sin leer» es una buena noticia y así se dice: no
 * falta nada, se terminó de leer. Y «el filtro no encontró» es un recorte que
 * se puede deshacer, así que trae el control para deshacerlo.
 */
function EmptyList({
  view,
  total,
  onlyUnread,
  hasFilters,
}: {
  view: TablePage<NotificationRow>;
  total: number;
  onlyUnread: boolean;
  hasFilters: boolean;
}) {
  if (total === 0) {
    return (
      <EmptyState
        icon={Inbox}
        title={t("notifications.empty.title")}
        description={t("notifications.empty.description")}
      />
    );
  }

  if (hasFilters) {
    return (
      <EmptyState
        icon={ListFilter}
        title={t("notifications.empty.filtered.title")}
        description={t("notifications.empty.filtered.description")}
        action={
          <Button asChild variant="outline">
            <Link
              href={tableUrl(view, { kind: null, level: null, page: null })}
              preserveScroll
            >
              {t("batches.clearFilters")}
            </Link>
          </Button>
        }
      />
    );
  }

  if (onlyUnread) {
    return (
      <EmptyState
        icon={CheckCheck}
        title={t("notifications.empty.unread.title")}
        description={t("notifications.empty.unread.description")}
        action={
          <Button asChild variant="outline">
            <Link href={route("notifications")}>{t("notifications.seeAll")}</Link>
          </Button>
        }
      />
    );
  }

  return (
    <EmptyState
      icon={Inbox}
      title={t("notifications.empty.none.title")}
      description={t("notifications.empty.none.description")}
    />
  );
}
