import { Link, router, useForm, usePage } from "@inertiajs/react";
import {
  CircleCheck,
  CircleX,
  CornerDownRight,
  Equal,
  FileSearchCorner,
  FolderTree,
  Hourglass,
  LoaderCircle,
  Plus,
  RefreshCw,
  TriangleAlert,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { FormEvent, ReactNode } from "react";
import { useMemo } from "react";

import { accessStateLabel } from "@/components/AccessStateBadge";
import { addressParam, addressWith } from "@/components/address";
import { BatchProgress } from "@/components/BatchProgress";
import { BatchStateBadge } from "@/components/BatchStateBadge";
import {
  ClientDataTable,
  createClientDataTableColumns,
} from "@/components/ClientDataTable";
import type {
  ClientDataTableColumn,
  ClientDataTableFacetedFilter,
} from "@/components/ClientDataTable";
import { DataTimestamp, TimezoneFootnote } from "@/components/DataTimestamp";
import { EmptyState } from "@/components/EmptyState";
import { FieldError, fieldErrorProps } from "@/components/FieldError";
import { FormDialog } from "@/components/FormDialog";
import { MetricCard } from "@/components/MetricCard";
import { SectionCard } from "@/components/SectionCard";
import { ServerErrorNotice } from "@/components/ServerErrorNotice";
import { StatusBadge } from "@/components/StatusBadge";
import type { StatusTone } from "@/components/StatusBadge";
import { useBatchPolling } from "@/hooks/useBatchPolling";
import { AppLayout } from "@/layouts/AppLayout";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { faultText } from "@/lib/errors";
import type { FieldFault } from "@/lib/errors";
import { batchDuration, batchOriginLabel } from "@/lib/batch";
import { formatNumber } from "@/lib/format";
import { t } from "@/lib/i18n";
import type { TranslationKey } from "@/lib/i18n";
import { route } from "@/lib/routes";

interface Sitemap {
  id: string;
  location: string;
  kind: string;
  source: string;
  parent_location: string | null;
  depth: number;
  url_count: number;
  last_read_at: string | null;
  last_submitted_at: string | null;
  last_submit_result: string | null;
  last_error: string | null;
  last_error_code: string | null;
  foreign_url_count: number;
  needs_submit: boolean;
  reading: boolean;
}

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

interface Batch {
  id: string;
  state: string;
  origin: string;
  total_items: number;
  processed_items: number;
  failed_items: number;
  quota_consumed: number;
  started_at: string | null;
  finished_at: string | null;
  duration_seconds: number | null;
  summary: {
    sitemaps_read?: number;
    sitemaps_submitted?: number;
    sitemaps_unchanged?: number;
    new_urls?: number;
    seen_urls?: number;
    foreign_urls?: number;
    errors?: string[];
  };
  is_terminal: boolean;
}

interface Props {
  domain: {
    id: string;
    hostname: string;
    access_state: string;
    is_operational: boolean;
  };
  sitemaps: Sitemap[];
  options: { kind: Option[]; source: Option[]; result: Option[] };
  last_sync: Batch | null;
}

const TAB_PARAM = "tab";
const LIST_TAB = "sitemaps";
const SYNC_TAB = "sync";
const NEW_SITEMAP_PARAM = "new";

/**
 * Qué significa cada resultado de envío, con icono y palabra (RT-04).
 *
 * `SKIPPED_UNCHANGED` **no** es una falla y no se dibuja como tal: no reenviar
 * lo que no cambió es el comportamiento correcto y ahorra cupo del dominio
 * (FR-026). Pintarlo con el mismo gris apagado que «Falló» empuja a forzar
 * envíos inútiles, que es exactamente lo que el mecanismo evita. Por eso lleva
 * el color del texto normal y un icono de igualdad —no una tilde, que sugeriría
 * que acabamos de mandar algo—, mientras que la falla sí es destructiva.
 */
/**
 * Los tres desenlaces de un envío, cada uno con su tono.
 *
 * Iban como texto suelto con un ícono al lado y el mismo color para dos de los
 * tres: «Sin cambios desde el último envío» aparecía con un `=` gris que no
 * decía si eso era bueno, malo o algo que atender. Ahora son badges de la paleta
 * cerrada, que es como se dice un estado en el resto del producto.
 *
 * **Saltear un envío no es una falla**: no reenviar un sitemap idéntico ahorra
 * cupo del dominio, así que va en `neutral` y no en `attention`. Tratarlo como
 * un problema empuja a forzar envíos que no cambian nada.
 */
const SUBMIT_RESULTS: Record<string, { icon: LucideIcon; tone: StatusTone }> = {
  OK: { icon: CircleCheck, tone: "positive" },
  SKIPPED_UNCHANGED: { icon: Equal, tone: "neutral" },
  FAILED: { icon: CircleX, tone: "critical" },
};

/**
 * Los tres motivos por los que una fila puede estar en falta (RT-08).
 *
 * El texto concreto —qué código respondió el servidor, qué etiqueta trajo el
 * XML— lo redacta el servidor y se muestra tal cual; acá vive sólo el
 * encabezado, que es lo que dice **dónde** se arregla: la dirección, el archivo
 * del sitio o la propiedad en Search Console. Un código que no esté en el mapa
 * muestra el mensaje del servidor y nada más: inventarle una causa mandaría a
 * revisar lo que puede estar perfecto.
 */
const ERROR_REASONS: readonly string[] = [
  "UNREACHABLE",
  "MALFORMED",
  "SUBMIT_FAILED",
];

/**
 * Qué ofrecer ante cada error de la sincronización.
 *
 * Los dos códigos que trae esta acción se resuelven en pantallas distintas, y
 * un aviso que sólo describe el problema deja a la persona buscando dónde
 * seguir. El resto cae en la acción neutra de `C-13`.
 */
const ERROR_ACTIONS: Record<
  string,
  { label: () => string; href: (domainId: string) => string }
> = {
  CREDENTIAL_NOT_READY: {
    label: () => t("domains.reviewConnection"),
    href: () => route("settings"),
  },
  DOMAIN_NOT_OPERATIONAL: {
    label: () => t("batches.seeDomain"),
    href: (domainId) => route("domain.show", { domain_id: domainId }),
  },
};

const columnHelper = createClientDataTableColumns<Sitemap>();

/**
 * Las columnas de la tabla.
 *
 * Se arman con el nombre del dominio adentro porque el aviso de URLs ajenas lo
 * nombra —«no pertenecen a ejemplo.com» dice qué revisar; «no pertenecen a este
 * dominio» no dice nada— y ese dato es de la página, no de la fila. La vista las
 * memoriza una sola vez: una identidad nueva en cada render invalidaría los
 * modelos de TanStack.
 *
 * Los identificadores también nombran filtros y columnas escondibles dentro de
 * la tabla local; por eso son estables aunque cambie el texto visible.
 */
function sitemapColumns(hostname: string): ClientDataTableColumn<Sitemap>[] {
  return [
    columnHelper.accessor("location", {
      id: "location",
      header: t("sitemaps.column.location"),
      meta: { label: t("sitemaps.column.location") },
      cell: (info) => (
        <SitemapLocation sitemap={info.row.original} hostname={hostname} />
      ),
    }),
    columnHelper.accessor("kind", {
      id: "kind",
      header: t("batches.column.kind"),
      meta: { label: t("batches.column.kind") },
      filterFn: "includesSelected",
      enableGlobalFilter: false,
      cell: (info) => {
        const sitemap = info.row.original;
        return (
          <Badge variant="outline">
            {sitemap.kind === "INDEX" ? <FolderTree aria-hidden /> : null}
            {t(`sitemapKind.${sitemap.kind}` as TranslationKey)}
          </Badge>
        );
      },
    }),
    columnHelper.accessor("source", {
      id: "source",
      header: t("sitemaps.filter.source"),
      meta: { label: t("sitemaps.filter.source") },
      filterFn: "includesSelected",
      enableGlobalFilter: false,
      cell: (info) =>
        t(`sitemapSource.${info.getValue()}` as TranslationKey),
    }),
    columnHelper.accessor("url_count", {
      id: "urls",
      // «Declaradas» y no «indexadas»: es lo que dice el archivo, no lo que
      // Google hizo con él. Un índice declara sitemaps, no URLs, así que su
      // cifra se lee distinto y la celda lo aclara.
      header: t("sitemaps.column.urls"),
      meta: { label: t("sitemaps.column.urls"), numeric: true },
      enableGlobalFilter: false,
      cell: (info) => {
        const sitemap = info.row.original;
        return (
          <span className="tabular-nums">
            {formatNumber(sitemap.url_count)}
            {sitemap.kind === "INDEX" ? (
              <span className="text-muted-foreground block text-xs">
                {t("sitemaps.declaredUnit")}
              </span>
            ) : null}
          </span>
        );
      },
    }),
    columnHelper.accessor("last_read_at", {
      id: "read",
      header: t("dashboard.panel.sitemaps.lastRead"),
      meta: { label: t("dashboard.panel.sitemaps.lastRead") },
      enableGlobalFilter: false,
      cell: (info) => {
        const sitemap = info.row.original;
        if (sitemap.reading) {
          return (
            <span className="flex items-center gap-1.5 text-sm">
              <LoaderCircle
                className="size-3.5 animate-spin motion-reduce:animate-none"
                aria-hidden
              />
              Leyendo…
            </span>
          );
        }
        return (
          <DataTimestamp
            value={sitemap.last_read_at}
            emptyLabel={t("sitemaps.notReadYet")}
          />
        );
      },
    }),
    columnHelper.accessor("last_submitted_at", {
      id: "submitted",
      // La pregunta que trae a esta pantalla es «¿Google tiene lo último que
      // publiqué?», así que el resultado y su fecha van juntos y en la misma
      // celda: separarlos obliga a cruzar dos columnas para responderla.
      header: t("sitemaps.column.lastSubmit"),
      meta: { label: t("sitemaps.column.lastSubmit") },
      filterFn: "includesSelected",
      enableGlobalFilter: false,
      cell: (info) => <SubmitResult sitemap={info.row.original} />,
    }),
    // Columna de estado para el filtro; se esconde porque su resultado ya se
    // explica, junto con la fecha, en «Último envío».
    columnHelper.accessor((row) => row.last_submit_result ?? "", {
      id: "result",
      header: t("sitemaps.filter.result"),
      filterFn: "includesSelected",
      enableGlobalFilter: false,
    }),
  ];
}

export default function SitemapsIndex({
  domain,
  sitemaps,
  options,
  last_sync,
}: Props) {
  const page = usePage();
  const { errors, account } = page.props;
  const { url } = page;
  const sync = useForm({});
  const tableColumns = useMemo(
    () => sitemapColumns(domain.hostname),
    [domain.hostname],
  );
  const tableFilters = useMemo<ClientDataTableFacetedFilter[]>(
    () => [
      {
        column: "kind",
        title: t("sitemaps.filter.kind"),
        options: options.kind,
        selection: "multiple",
      },
      {
        column: "source",
        title: t("sitemaps.filter.source"),
        options: options.source,
        selection: "multiple",
      },
      {
        column: "result",
        title: t("sitemaps.filter.result"),
        options: options.result,
        selection: "single",
      },
    ],
    [options],
  );
  const tab = addressParam(url, TAB_PARAM) === SYNC_TAB ? SYNC_TAB : LIST_TAB;
  const newSitemapUrl = addressWith(url, NEW_SITEMAP_PARAM, "");

  // Único mecanismo de actualización del producto (RT-13): mientras el lote no
  // llegue a un estado terminal y la pestaña esté visible, se vuelven a pedir
  // las dos props que cambian. Ninguna vista escribe su propio reloj.
  const syncing = useBatchPolling({
    states: [last_sync?.state],
    only: ["sitemaps", "last_sync"],
  });

  const canOperate = account?.can_operate ?? false;
  return (
    <AppLayout
      title={t("sitemaps.title", { hostname: domain.hostname })}
      description={t("sitemaps.description")}
      actions={
        <div className="flex flex-wrap gap-2">
          <SyncAction
            domain={domain}
            canOperate={canOperate}
            syncing={syncing}
            processing={sync.processing}
            onSync={() =>
              sync.post(route("domain.sync", { domain_id: domain.id }), {
                preserveScroll: true,
              })
            }
          />
          <Button asChild >
            <Link href={newSitemapUrl} preserveScroll>
              <Plus aria-hidden />
              {t("sitemaps.new")}
            </Link>
          </Button>
        </div>
      }
    >
      <Tabs
        className="gap-4"
        value={tab}
        onValueChange={(next) =>
          router.get(
            addressWith(url, TAB_PARAM, next),
            {},
            { preserveScroll: true, preserveState: true },
          )
        }
      >
        <TabsList>
          <TabsTrigger value={LIST_TAB}>{t("sitemaps.tab.list")}</TabsTrigger>
          <TabsTrigger value={SYNC_TAB}>{t("sitemaps.tab.sync")}</TabsTrigger>
        </TabsList>

        <TabsContent value={LIST_TAB} className="flex flex-col gap-6">
          <ClientDataTable
            data={sitemaps}
            columns={tableColumns}
            rowId={(row) => row.id}
            unit={t("sitemaps.unit")}
            unitSingular={t("sitemaps.unitSingular")}
            caption={t("sitemaps.caption", { hostname: domain.hostname })}
            search={{
              label: t("sitemaps.searchLabel"),
              placeholder: t("sitemaps.searchPlaceholder"),
            }}
            facetedFilters={tableFilters}
            hiddenColumns={["result"]}
            empty={{
              icon: FolderTree,
              title: t("sitemaps.empty.title"),
              description: t("sitemaps.empty.description"),
              action: (
                <Button asChild>
                  <Link href={newSitemapUrl} preserveScroll>
                    {t("sitemaps.empty.action")}
                  </Link>
                </Button>
              ),
            }}
          />

          {/*
        La aclaración es fija y va debajo de la tabla, no en un aviso que se
        cierra (RT-05): la confusión que evita —enviar el sitemap no es que
        Google lo procese, y menos que indexe sus URLs— vuelve cada vez que se
        mira esta pantalla, así que el texto también.
      */}
          <div className="text-muted-foreground max-w-7xl space-y-2 text-sm">
            <p>{t("sitemaps.footnote.submit")}</p>
            <p>{t("sitemaps.footnote.quota")}</p>
          </div>
        </TabsContent>

        <TabsContent value={SYNC_TAB} className="flex flex-col gap-6">
          {errors.sync ? (
            <SyncError
              code={errors.sync_code ?? "INTERNAL_ERROR"}
              message={errors.sync}
              domainId={domain.id}
            />
          ) : null}

          {canOperate && !domain.is_operational ? (
            <Alert>
              <TriangleAlert />
              <AlertTitle>{t("sitemaps.notSyncing.title")}</AlertTitle>
              <AlertDescription className="flex flex-col items-start gap-3">
                <span>
                  {t("sitemaps.notSyncing.body", {
                    state: accessStateLabel(domain.access_state),
                  })}
                </span>
                <Button asChild size="sm" variant="outline">
                  <Link href={route("domain.show", { domain_id: domain.id })}>
                    {t("batches.seeDomain")}
                  </Link>
                </Button>
              </AlertDescription>
            </Alert>
          ) : null}

          {last_sync ? (
            <LastSync batch={last_sync} />
          ) : (
            <EmptyState
              icon={Hourglass}
              title={t("sitemaps.sync.empty.title")}
              description={t("sitemaps.sync.empty.description")}
            />
          )}
        </TabsContent>
      </Tabs>

      <TimezoneFootnote />
      <NewSitemapDialog domainId={domain.id} errors={errors} />
    </AppLayout>
  );
}

/**
 * El error de la sincronización, con la pantalla que lo resuelve (RT-08).
 *
 * Los dos códigos que esta acción produce se arreglan en lugares distintos, y
 * un aviso que sólo describe el problema deja a la persona buscando dónde
 * seguir. Un código que no esté en el mapa cae en la acción neutra de `C-13`:
 * nunca se le inventa una causa.
 */
function SyncError({
  code,
  message,
  domainId,
}: {
  code: string;
  message: string;
  domainId: string;
}) {
  const action = ERROR_ACTIONS[code];

  return (
    <ServerErrorNotice
      code={code}
      message={message}
      action={
        action ? (
          <Button asChild size="sm" variant="outline">
            <Link href={action.href(domainId)}>{action.label()}</Link>
          </Button>
        ) : undefined
      }
    />
  );
}

/**
 * El botón de sincronizar, o lo que hay que resolver antes (RT-18).
 *
 * Sincronizar habla con Google. Mientras la cuenta no pueda operar o el dominio
 * no tenga el acceso confirmado, la acción no se ofrece deshabilitada con un
 * globo que explique: se **reemplaza** por la que destraba el problema. Un botón
 * apagado deja a la persona mirándolo sin saber qué hacer, y peor, sugiere que
 * el problema está acá.
 */
function SyncAction({
  domain,
  canOperate,
  syncing,
  processing,
  onSync,
}: {
  domain: Props["domain"];
  canOperate: boolean;
  syncing: boolean;
  processing: boolean;
  onSync: () => void;
}) {
  if (!canOperate) {
    return (
      <Button asChild variant="outline">
        <Link href={route("settings")}>{t("domains.reviewConnection")}</Link>
      </Button>
    );
  }

  if (!domain.is_operational) {
    return (
      <Button asChild variant="outline">
        <Link href={route("domain.show", { domain_id: domain.id })}>
          {t("batch.action.checkAccess")}
        </Link>
      </Button>
    );
  }

  return (
    <Button variant="outline"
      onClick={onSync}
      // Se deshabilita sólo mientras hay una corrida en vuelo, y el motivo está
      // a la vista en el bloque de arriba: pedir otra no la apura, encola
      // trabajo repetido y gasta cupo dos veces.
      disabled={processing || syncing}
      aria-busy={processing}
    >
      <RefreshCw aria-hidden />
      {processing ? t("sitemaps.syncing") : t("sitemaps.syncNow")}
    </Button>
  );
}

/**
 * Lo que hizo la última corrida (RT-13, C-07, C-17).
 *
 * El estado inicial nunca es «listo»: mientras el lote no sea terminal la
 * pantalla dice que está en curso y sigue consultando. El avance sale de las
 * cifras y no del estado, así que un lote parcial se ve parcial —la barra no
 * toca el borde— y se distingue de uno terminado sin leer los números.
 */
function LastSync({ batch }: { batch: Batch }) {
  const summary = batch.summary ?? {};
  const errors = summary.errors ?? [];

  return (
    <div className="flex flex-col gap-6">
      <SectionCard
        className="gap-0"
        title={{ text: t("sitemaps.latestRun"), level: "h2" }}
        actions={
          <Tooltip>
            <TooltipTrigger asChild>
              <Button asChild size="icon-sm">
                <Link
                  href={route("batch.show", { batch_id: batch.id })}
                  aria-label={t("batch.action.seeDetail")}
                >
                  <FileSearchCorner aria-hidden />
                </Link>
              </Button>
            </TooltipTrigger>
            <TooltipContent>{t("batch.action.seeDetail")}</TooltipContent>
          </Tooltip>
        }
      >
        <div role="status" className="flex flex-wrap items-center gap-3">
          <BatchStateBadge state={batch.state} />
          <span className="text-muted-foreground text-sm">
            {batch.is_terminal
              ? t("dashboard.panel.activity.finished")
              : t("dashboard.panel.activity.started")}{" "}
            <DataTimestamp
              value={batch.is_terminal ? batch.finished_at : batch.started_at}
              emptyLabel={
                batch.is_terminal ? t("sitemaps.notRecorded") : t("time.justNow")
              }
            />
          </span>
        </div>

        <dl className="grid gap-x-8 gap-y-3 text-sm sm:grid-cols-3">
          <RunDetail
            label={t("sitemaps.run.origin")}
            value={batchOriginLabel(batch.origin)}
          />
          <RunDetail
            label={t("batches.column.duration")}
            value={batchDuration(batch.duration_seconds)}
          />
          <RunDetail
            label={t("sitemaps.run.batch")}
            value={
              <span translate="no" className="font-mono">
                {batch.id.slice(0, 8)}
              </span>
            }
          />
        </dl>
      </SectionCard>

      <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
        <MetricCard
          label={t("sitemaps.stat.read")}
          value={formatNumber(summary.sitemaps_read ?? 0)}
        />
        <MetricCard
          label={t("sitemaps.stat.submitted")}
          value={formatNumber(summary.sitemaps_submitted ?? 0)}
        />
        <MetricCard
          label={t("sitemaps.stat.unchanged")}
          value={formatNumber(summary.sitemaps_unchanged ?? 0)}
        />
        <MetricCard
          label={t("sitemaps.stat.newUrls")}
          value={formatNumber(summary.new_urls ?? 0)}
        />
      </div>

      <SectionCard title={{ text: t("sitemaps.processing"), level: "h2" }}>
        <BatchProgress
          total={batch.total_items}
          processed={batch.processed_items}
          failed={batch.failed_items}
          state={batch.state}
        />
        <p className="text-muted-foreground text-sm">
          {batch.quota_consumed === 0
            ? t("sitemaps.quota.none")
            : t("sitemaps.quota.used", {
                count: batch.quota_consumed,
                total: formatNumber(batch.quota_consumed),
              })}
        </p>
      </SectionCard>

      <SyncOutcome batch={batch} errors={errors} />
    </div>
  );
}

function RunDetail({ label, value }: { label: string; value: ReactNode }) {
  return (
    <div className="min-w-0">
      <dt className="text-muted-foreground text-xs">{label}</dt>
      <dd className="mt-0.5 truncate font-medium">{value}</dd>
    </div>
  );
}

function SyncOutcome({
  batch,
  errors,
}: {
  batch: Batch;
  errors: string[];
}) {
  const isSuccessful = batch.state === "COMPLETED" && errors.length === 0;
  const isRunning = !batch.is_terminal;
  const OutcomeIcon = isSuccessful
    ? CircleCheck
    : isRunning
      ? Hourglass
      : batch.state === "FAILED"
        ? CircleX
        : TriangleAlert;

  const description = isSuccessful
    ? batch.total_items === 0
      ? t("sitemaps.outcome.nothing")
      : t("sitemaps.outcome.success", {
          count: batch.total_items,
          total: formatNumber(batch.total_items),
        })
    : isRunning
      ? t("sitemaps.outcome.running")
      : t("sitemaps.outcome.incomplete");

  return (
    <SectionCard title={{ text: t("sitemaps.outcome"), level: "h2" }}>
      <div className="flex items-start gap-3">
        <OutcomeIcon
          aria-hidden
          className={
            batch.state === "FAILED"
              ? "text-destructive mt-0.5 size-4 shrink-0"
              : "text-muted-foreground mt-0.5 size-4 shrink-0"
          }
        />
        <p className="text-sm text-pretty">{description}</p>
      </div>

      {errors.length > 0 ? (
        <div className="space-y-1.5">
          <p className="text-sm font-medium">{t("sitemaps.unprocessed")}</p>
          <ul className="text-muted-foreground list-inside list-disc space-y-1 text-sm">
            {errors.map((error) => (
              <li key={error} className="wrap-anywhere">
                {error}
              </li>
            ))}
          </ul>
        </div>
      ) : null}
    </SectionCard>
  );
}

/**
 * El alta breve vive fuera de la vista habitual y se abre desde la dirección.
 *
 * Registrar no lee ni llama a Google, así que sigue disponible aunque la cuenta
 * no pueda operar: dejar anotada la dirección ahora es trabajo que no hay que
 * repetir cuando la conexión se arregle.
 */
function NewSitemapDialog({
  domainId,
  errors,
}: {
  domainId: string;
  errors: Record<string, FieldFault>;
}) {
  const form = useForm({ location: "" });

  const submit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    form.post(route("sitemap.create", { domain_id: domainId }), {
      // `preserveState` es lo que conserva lo escrito cuando el servidor
      // rechaza la dirección: sin él la respuesta vuelve con el campo vacío y
      // hay que tipear de nuevo lo que ya estaba bien salvo por un detalle
      // (RT-17).
      preserveState: true,
      preserveScroll: true,
    });
  };

  return (
    <FormDialog
      param={NEW_SITEMAP_PARAM}
      title={t("sitemaps.register.title")}
      description={t("sitemaps.register.description")}
      submitLabel={t("sitemaps.register.submit")}
      pendingLabel={t("sitemaps.register.busy")}
      pending={form.processing}
      onSubmit={submit}
    >
      <Field data-invalid={errors.location ? true : undefined}>
        <FieldLabel htmlFor="location">
          {t("sitemaps.register.label")}
        </FieldLabel>
        <Input
          id="location"
          name="location"
          type="text"
          inputMode="url"
          autoComplete="off"
          spellCheck={false}
          autoCapitalize="none"
          placeholder={t("sitemaps.register.placeholder")}
          value={form.data.location}
          onChange={(event) => form.setData("location", event.target.value)}
          {...fieldErrorProps("error-location", faultText(errors.location))}
        />
        <FieldError id="error-location" message={faultText(errors.location)} />
      </Field>
    </FormDialog>
  );
}

/**
 * La ubicación del archivo y de quién depende.
 *
 * La sangría dice la profundidad, que es una propiedad de la fila y es cierta
 * en cualquier orden; la contigüidad con el índice, en cambio, sólo existe en
 * el orden del árbol. Por eso la pertenencia se escribe además con palabras:
 * un lector de pantalla no anuncia el margen izquierdo, y con la tabla ordenada
 * por cantidad de URLs el índice de una fila puede quedar treinta renglones más
 * arriba.
 */
function SitemapLocation({
  sitemap,
  hostname,
}: {
  sitemap: Sitemap;
  hostname: string;
}) {
  const errorId = `sitemap-error-${sitemap.id}`;
  const reason =
    sitemap.last_error_code && ERROR_REASONS.includes(sitemap.last_error_code)
      ? t(`sitemaps.error.${sitemap.last_error_code}` as TranslationKey)
      : undefined;
  // El fallo del envío se cuenta en la columna del envío, junto al resultado que
  // explica. Acá queda lo que le pasó al archivo: no se pudo bajar, o se bajó y
  // no era un sitemap.
  const readError =
    sitemap.last_error_code === "SUBMIT_FAILED" ? null : sitemap.last_error;
  const hasNotice = Boolean(readError) || sitemap.foreign_url_count > 0;

  return (
    <div
      className="space-y-1"
      style={{ paddingInlineStart: `${Math.min(sitemap.depth, 3) * 1.25}rem` }}
    >
      {/*
        `select-all` para que un clic tome la dirección entera: son direcciones
        largas y lo que se hace con ellas es copiarlas y pegarlas en otro lado.
        Nunca se recortan con puntos suspensivos (RT-09).
      */}
      <span
        translate="no"
        className="block font-medium select-all"
        aria-describedby={hasNotice ? errorId : undefined}
      >
        {sitemap.location}
      </span>

      {sitemap.parent_location ? (
        <p className="text-muted-foreground flex items-start gap-1 text-xs">
          <CornerDownRight className="mt-0.5 size-3 shrink-0" aria-hidden />
          <span>
            Declarado por{" "}
            <span translate="no" className="select-all">
              {sitemap.parent_location}
            </span>
          </span>
        </p>
      ) : null}

      {hasNotice ? (
        <div id={errorId} className="space-y-1.5 pt-0.5">
          {readError ? (
            <p className="text-destructive flex items-start gap-1.5 text-xs">
              <CircleX className="mt-0.5 size-3.5 shrink-0" aria-hidden />
              <span>
                {reason ? (
                  <span className="font-medium">{reason}. </span>
                ) : null}
                {readError}
              </span>
            </p>
          ) : null}

          {/*
            Las URLs ajenas no son una falla del sitemap: se leyó bien y se
            envió bien, y sólo parte de lo que declara es de otro sitio. Va en
            tratamiento de advertencia y no de error, porque tratarlo como falla
            escondería que el resto sí se registró.
          */}
          {sitemap.foreign_url_count > 0 ? (
            <p className="flex items-start gap-1.5 text-xs text-amber-700 dark:text-amber-300">
              <TriangleAlert className="mt-0.5 size-3.5 shrink-0" aria-hidden />
              {/*
                La oración se parte en dos claves porque el hostname va en el
                medio y no se traduce: es un identificador, y `translate="no"`
                lo protege de un traductor del navegador que le cambie una letra.
              */}
              <span>
                {t("sitemaps.foreignUrls.before", {
                  total: formatNumber(sitemap.foreign_url_count),
                })}{" "}
                <span translate="no" className="font-medium">
                  {hostname}
                </span>
                {t("sitemaps.foreignUrls.after")}
              </span>
            </p>
          ) : null}
        </div>
      ) : null}
    </div>
  );
}

/**
 * Qué pasó con el último envío, y cuándo llegó el último que sí salió.
 *
 * Son dos datos distintos y los dos hacen falta: «falló» sin la fecha del
 * último envío correcto deja sin responder qué tiene Google hoy, que es la
 * pregunta de esta pantalla.
 */
function SubmitResult({ sitemap }: { sitemap: Sitemap }) {
  const result = sitemap.last_submit_result;
  const definition = result ? SUBMIT_RESULTS[result] : undefined;
  const errorId = `submit-${sitemap.id}`;
  const submitError =
    sitemap.last_error_code === "SUBMIT_FAILED" ? sitemap.last_error : null;

  if (!definition) {
    return (
      <span className="text-muted-foreground flex items-center gap-1.5 text-sm">
        <Hourglass className="size-3.5 shrink-0" aria-hidden />
        {/* Lo único que sabemos es que todavía no se lo mandamos: nada acá
            puede sonar a que Google lo tiene en alguna cola. */}
        {t("sitemaps.notSubmitted")}
      </span>
    );
  }

  return (
    <div className="space-y-1">
      <span aria-describedby={submitError ? errorId : undefined}>
        <StatusBadge
          tone={definition.tone}
          icon={definition.icon}
          // La explicación de qué significa el desenlace va al globo, que es
          // donde el resto del producto pone la ayuda de un badge (RT-02). La
          // celda queda con la etiqueta y su color, que es lo que se lee de un
          // vistazo en una tabla.
          help={t(`sitemaps.submit.${result}.help` as TranslationKey)}
        >
          {t(`sitemaps.submit.${result}` as TranslationKey)}
        </StatusBadge>
      </span>

      {/* La fecha del último envío correcto queda **fuera** del badge y a la
          vista: es el otro dato que la pregunta de esta pantalla necesita, y
          adentro de un globo dejaría de poder leerse de un vistazo. */}
      {result === "OK" ? (
        <p className="text-muted-foreground text-xs">
          <DataTimestamp
            value={sitemap.last_submitted_at}
            emptyLabel={t("time.noDate")}
          />
        </p>
      ) : null}

      {submitError ? (
        <p id={errorId} className="text-destructive text-xs">
          {submitError}
        </p>
      ) : null}

      {/*
        Sin esta línea, «Sin cambios desde el último envío» mentiría en el único
        caso en que importa: cuando el archivo sí cambió y el envío no llegó a
        salir —falló, o no quedaba cupo— el resultado guardado sigue siendo el
        anterior, y quien lee la fila creería que Google tiene lo último.
      */}
      {sitemap.needs_submit ? (
        <p className="text-muted-foreground text-xs">
          {t("sitemaps.changedAfterSubmit")}
        </p>
      ) : null}

      {/*
        Que el último intento haya fallado no borra lo que Google recibió antes.
        Sin esta línea, «Falló» dejaría sin responder qué tiene Google hoy, que
        es la única pregunta que trae a esta pantalla.
      */}
      {result !== "OK" ? (
        <p className="text-muted-foreground text-xs">
          {sitemap.last_submitted_at ? (
            <>
              {t("sitemaps.lastGoodSubmit")}{" "}
              <DataTimestamp
                value={sitemap.last_submitted_at}
                emptyLabel={t("time.never")}
              />
            </>
          ) : (
            t("sitemaps.neverSubmitted")
          )}
        </p>
      ) : null}
    </div>
  );
}
