import { Link, usePage } from '@inertiajs/react'
import { ChevronRight, Clock, FileText, Globe, TriangleAlert } from 'lucide-react'
import { useState } from 'react'

import { AccessStateBadge, accessStateLabel } from '@/components/AccessStateBadge'
import { addressParam } from '@/components/address'
import { BatchProgress } from '@/components/BatchProgress'
import { BatchStateBadge } from '@/components/BatchStateBadge'
import { ChartAreaInteractive } from '@/components/chart-area-interactive'
import type { Activity } from '@/components/chart-area-interactive'
import { CodeChip } from '@/components/CodeChip'
import { coverageStateLabel } from '@/components/CoverageStateBadge'
import { DataTimestamp, TimezoneFootnote } from '@/components/DataTimestamp'
import { DetailSheet } from '@/components/DetailSheet'
import { EmptyState } from '@/components/EmptyState'
import { NoticeItem } from '@/components/NoticeItem'
import { QuotaMeter } from '@/components/QuotaMeter'
import { RowActions } from '@/components/RowActions'
import { Section } from '@/components/Section'
import { SectionCards, PANEL_KEYS } from '@/components/section-cards'
import type {
  CoverageOverview,
  DomainsSummary,
  LastCycle,
  PanelKey,
  SitemapsSummary,
  TodayQuota,
} from '@/components/section-cards'
import { StatusBadge } from '@/components/StatusBadge'
import { Button } from '@/components/ui/button'
import { ItemGroup } from '@/components/ui/item'
import { useBatchPolling } from '@/hooks/useBatchPolling'
import { AppLayout } from '@/layouts/AppLayout'
import { formatNumber } from '@/lib/format'
import { cn } from '@/lib/utils'
import { t } from '@/lib/i18n'
import type { TranslationKey } from '@/lib/i18n'
import { route } from '@/lib/routes'
import {
  batchKindLabel,
  batchOriginLabel,
  batchPath,
  batchesPath,
  shortBatchName,
} from '@/lib/batch'

/**
 * V0 · la pantalla de inicio.
 *
 * Contesta las dos preguntas con las que alguien abre la plataforma —«¿hay algo
 * roto?» y «¿cuánto sabemos hoy de mis sitios?»— y en ese orden: una cobertura
 * preciosa sobre un dominio que perdió el acceso hace tres días es una foto
 * vieja presentada como si fuera de hoy.
 *
 * Es **una grilla de seis aspectos y nada más**. Antes era una línea de estado,
 * cuatro tarjetas, un gráfico y una lista de lotes: cuatro preguntas con el
 * mismo peso visual, así que el ojo no tenía por dónde empezar. Ahora cada
 * aspecto dice su cifra y ofrece **una** salida hacia el detalle; el gráfico y
 * los últimos lotes no se borraron, se mudaron al panel de «Actividad», que es
 * el aspecto que explican.
 *
 * Lo que abre panel y lo que navega no es una preferencia: navega lo que ya es
 * una pantalla —dominios, avisos—, porque duplicarlo en un panel sería mantener
 * dos diseños de lo mismo. El panel es para los agregados de cuenta que hoy sólo
 * existen por dominio.
 *
 * Las tres reglas que gobiernan lo que se puede afirmar acá son las mismas que
 * gobiernan `apps/web/dashboard.py`: ninguna cifra sin su denominador y su
 * fecha, `UNKNOWN` fuera de todo porcentaje, y lo congelado contado aparte de lo
 * que sigue vivo.
 */

interface PendingDomain {
  id: string
  hostname: string
  access_state: string
  access_checked_at: string | null
}

interface FailedBatch {
  id: string
  hostname: string
  domain_id: string
  kind: string
  finished_at: string | null
}

interface RecentBatch {
  id: string
  hostname: string
  domain_id: string
  kind: string
  origin: string
  state: string
  total_items: number
  processed_items: number
  failed_items: number
  created_at: string | null
  started_at: string | null
  finished_at: string | null
  is_terminal: boolean
}

interface AttentionItems {
  /** Nulo cuando la credencial de la cuenta sirve. Un objeto siempre presente obligaría a mirar adentro. */
  credential: { has_credential: boolean; status: string | null; error_code: string | null } | null
  domains: PendingDomain[]
  failed_batches: FailedBatch[]
}

interface Props {
  domains: DomainsSummary
  attention: AttentionItems
  coverage: CoverageOverview
  quota: TodayQuota
  sitemaps: SitemapsSummary
  activity: Activity
  last_batches: RecentBatch[]
  last_cycle: LastCycle | null
  has_domains: boolean
}

export default function Dashboard({
  domains,
  attention,
  coverage,
  quota,
  sitemaps,
  activity,
  last_batches,
  last_cycle,
  has_domains,
}: Props) {
  // `active_work` viaja compartido: el indicador de actividad vive en la barra y
  // se dibuja en todas las pantallas. Acá se lee para el sondeo, que sí es de
  // esta vista.
  const { account, active_work } = usePage().props
  const { url } = usePage()
  const canOperate = account?.can_operate ?? false

  // Único mecanismo de actualización del producto (RT-13). Se mira la cuenta
  // entera y no los cinco lotes que se listan: el que está corriendo puede no
  // estar entre los más recientes, y el tablero quedaría quieto justo mientras
  // hay trabajo en curso.
  // Con la conexión caída ningún lote puede avanzar, así que sondear cada cinco
  // segundos es pedirle al servidor, para siempre, un cambio que no puede
  // ocurrir. La lista vacía apaga el reloj.
  useBatchPolling({ states: canOperate ? active_work.map((work) => work.state) : [] })

  if (!has_domains) {
    return (
      <AppLayout title={t('dashboard.title')} description={t('dashboard.description.empty')}>
        <EmptyState
          icon={Globe}
          title={t('dashboard.empty.title')}
          description={t('dashboard.empty.description')}
          action={
            /*
              Un botón y un enlace, no dos botones. Dos controles del mismo peso
              obligan a elegir entre dos caminos sin saber en qué se diferencian,
              y acá uno de los dos es claramente el que sigue.
            */
            <div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-2">
              <Button asChild>
                <Link href={route('domain.new')}>{t('dashboard.empty.addFirst')}</Link>
              </Button>
              <Button asChild variant="link">
                <Link href={route('onboarding')}>{t('dashboard.empty.resumeTour')}</Link>
              </Button>
            </div>
          }
        />
      </AppLayout>
    )
  }

  const hasUrls = coverage.total > 0

  return (
    /*
      El encabezado no lleva ninguna acción, y está bien que no lleve.

      Acá vivía «Agregar dominio», que era el único control visible al llegar:
      la pantalla le ofrecía agregar un segundo dominio a alguien que tenía el
      primero esperando autorización. Un tablero contesta «¿qué pasó?», y el
      alta pertenece a la pantalla donde viven los dominios, a un clic de la
      tarjeta que los cuenta. Un encabezado sin acciones no es un encabezado
      incompleto: es una pantalla que no está pidiendo nada.
    */
    <AppLayout title={t('dashboard.title')} description={t('dashboard.description')}>
      {hasUrls ? (
        <SectionCards
          domains={domains}
          coverage={coverage}
          quota={quota}
          sitemaps={sitemaps}
          activity={activity}
          lastCycle={last_cycle}
          panelHref={(panel) => panelHref(url, panel)}
        />
      ) : (
        <NoUrlsYet domains={domains} />
      )}

      <TimezoneFootnote />

      <AspectPanel
        openKey={openPanel(url)}
        attention={attention}
        canOperate={canOperate}
        coverage={coverage}
        quota={quota}
        sitemaps={sitemaps}
        activity={activity}
        batches={last_batches}
      />
    </AppLayout>
  )
}

/**
 * El enlace que lleva un nombre de dominio adentro.
 *
 * `wrap-anywhere` no es cosmética: un hostname es una sola palabra sin espacios
 * y `website.patiospoolsanddriveways.test` no entra en los 576 px de un panel.
 * Sin esto empuja el panel entero hacia el costado y aparece una barra de
 * desplazamiento horizontal sobre contenido que sí entraba.
 */
const HOSTNAME_LINK =
  'focus-visible:ring-ring rounded wrap-anywhere underline-offset-4 hover:underline ' +
  'focus-visible:ring-2 focus-visible:outline-none'

/* ── La dirección: qué panel está abierto ─────────────────────────────────── */

/** El aspecto abierto, o nulo. Un nombre que no existe se lee como «ninguno». */
export function openPanel(address: string): PanelKey | null {
  const value = addressParam(address, 'panel')
  return PANEL_KEYS.includes(value as PanelKey) ? (value as PanelKey) : null
}

/**
 * La dirección que abre un aspecto, conservando lo que ya hubiera puesto.
 *
 * El rango del gráfico viaja como `?days=`, así que reconstruir la dirección
 * desde la ruta le devolvería el rango por omisión a alguien que acaba de elegir
 * noventa días.
 */
export function panelHref(address: string, panel: PanelKey): string {
  const [path, search] = address.split('?')
  const query = new URLSearchParams(search)
  query.set('panel', panel)
  return `${path}?${query.toString()}`
}

/**
 * La lista completa de lo que hay que resolver, con el motivo de cada cosa y
 * dónde se resuelve.
 *
 * Es una lista de verdad y no una fila de puntos de colores: el motivo tiene que
 * poder leerse, y en escala de grises un punto ámbar y uno rojo son el mismo
 * punto (RT-04).
 */
function AttentionList({
  attention,
  canOperate,
}: {
  attention: AttentionItems
  canOperate: boolean
}) {
  return (
      <ItemGroup className="gap-2">
        {/*
          La credencial de la cuenta no se lista acá. Ya la nombra
          `AccountNotices`, arriba de todo y en todas las pantallas, con su
          botón: repetirla sería el mismo hecho dos veces con dos redacciones. El
          microcopy que decía «el aviso de arriba lleva a donde se resuelve» era
          la confesión de que este ítem sobraba.
        */}
        {attention.domains.map((domain) => (
          <NoticeItem
            key={domain.id}
            badge={<AccessStateBadge state={domain.access_state} canOperate={canOperate} />}
            title={
              <Link
                href={route('domain.show', { domain_id: domain.id })}
                className={HOSTNAME_LINK}
                translate="no"
              >
                {domain.hostname}
              </Link>
            }
            description={domainReason(domain.access_state)}
            meta={
              <>
                {t('dashboard.attention.lastCheck')}{' '}
                <DataTimestamp value={domain.access_checked_at} emptyLabel={t('time.never')} />
              </>
            }
            action={
              /*
                Enlaces y no botones: el único botón lo lleva la tarjeta de
                estado, que es la que pide la acción. Una lista de cinco botones
                rellenos no tiene un primero, que es justo lo que hace falta
                cuando hay varias cosas rotas a la vez.
              */
              <Button asChild size="sm" variant="link">
                <Link href={route('domain.show', { domain_id: domain.id })}>
                  {t('account.review')}
                  {/* Nombre único por control: cinco «Revisar» obligan a
                      recorrer la lista para saber cuál es cuál. Empieza por el
                      texto visible, así que el comando de voz lo alcanza. */}
                  <span className="sr-only"> — {domain.hostname}</span>
                </Link>
              </Button>
            }
          />
        ))}

        {attention.failed_batches.map((batch) => (
          <NoticeItem
            key={batch.id}
            badge={<BatchStateBadge state="FAILED" />}
            title={
              <Link
                href={batchPath(batch.id)}
                className={HOSTNAME_LINK}
              >
                {batchKindLabel(batch.kind)} {t('dashboard.attention.on')}{' '}
                <span translate="no">{batch.hostname}</span>
              </Link>
            }
            description={t('dashboard.attention.batchFailed')}
            meta={
              <>
                {t('dashboard.attention.failedAt')}{' '}
                <DataTimestamp value={batch.finished_at} emptyLabel={t('time.noDate')} />
              </>
            }
            action={
              <Button asChild size="sm" variant="link">
                <Link href={batchPath(batch.id)}>
                  {t('dashboard.attention.seeBatch')}
                  <span className="sr-only">
                    {' '}
                    — {batchKindLabel(batch.kind)} {t('dashboard.attention.on')} {batch.hostname}
                  </span>
                </Link>
              </Button>
            }
          />
        ))}
      </ItemGroup>
  )
}

/**
 * El titular dice **qué** hay que resolver y en **una** oración.
 *
 * Acumular una frase por familia producía «La conexión con Google dejó de
 * funcionar. 1 dominio necesita tu atención.»: dos problemas de dos naturalezas
 * pegados, y el primero ya dicho por el aviso del armazón con su botón. Ahora
 * nombra lo que la tarjeta cuenta —dominios y lotes, que son cosas de tus
 * sitios— y nada más.
 *
 * Sigue diciendo **qué** y no «cuántas cosas»: «3 cosas para resolver» obliga a
 * abrir la lista para saber si lo que falla es un permiso o una tanda de
 * trabajo, que se resuelven en pantallas distintas.
 */
export function attentionHeadline(attention: AttentionItems): string {
  const parts: string[] = []

  const domainCount = attention.domains.length
  if (domainCount > 0) {
    parts.push(
      t('dashboard.attention.domains', { count: domainCount, total: formatNumber(domainCount) })
    )
  }

  const batchCount = attention.failed_batches.length
  if (batchCount > 0) {
    parts.push(
      t('dashboard.attention.batches', { count: batchCount, total: formatNumber(batchCount) })
    )
  }

  if (parts.length === 0) return t('dashboard.attention.allGood')

  // La lista se une con «y» y el verbo concuerda con el total, no con el último
  // elemento: son dos cosas del idioma y por eso las dos salen del catálogo.
  return t('dashboard.attention.headline', {
    count: domainCount + batchCount,
    items: parts.join(t('dashboard.attention.and')),
  })
}

function domainReason(state: string): string {
  if (state === 'AWAITING_ACCESS') return t('dashboard.attention.awaiting')
  if (state === 'ACCESS_LOST') return t('dashboard.attention.lost')
  return t('dashboard.attention.state', { label: accessStateLabel(state) })
}

/**
 * Hay dominios pero todavía no hay URLs: la grilla no se rinde.
 *
 * Seis tarjetas en cero son seis bloques diciendo «todavía nada», y una de ellas
 * dedicada a explicar por qué no hay cifra. No es quitar información: no hay
 * ninguna. Lo que sí hay es un paso siguiente, y es uno solo.
 */
function NoUrlsYet({ domains }: { domains: DomainsSummary }) {
  return (
    <EmptyState
      icon={FileText}
      title={t('dashboard.noUrls.title')}
      description={t('dashboard.noUrls.description')}
      action={
        <Button asChild>
          {/*
            Con el sitio conectado, el botón lleva a sus sitemaps, que es donde
            se resuelve no tener URLs. Sin sitio no hay sitemaps que registrar y
            lo que falta es un paso antes: conectarlo, en la conexión. Antes
            este camino llevaba al listado de dominios, que salió del menú.
          */}
          {domains.only_id ? (
            <Link href={route('sitemaps', { domain_id: domains.only_id })}>
              {t('dashboard.noUrls.registerSitemap')}
            </Link>
          ) : (
            <Link href={route('settings')}>{t('dashboard.domains.connect')}</Link>
          )}
        </Button>
      }
    />
  )
}

/* ── Los cinco paneles del tablero ────────────────────────────────────────── */

/**
 * El título de cada panel, buscado por su clave.
 *
 * Es una función y no un mapa constante: el mapa se armaría al importar el
 * módulo, que es antes de que el catálogo esté instalado.
 */
function panelTitle(panel: PanelKey): string {
  return t(`dashboard.panel.${panel}` as TranslationKey)
}

/**
 * El detalle de lo que no tiene pantalla propia.
 *
 * Cuatro son agregados de cuenta que en el producto sólo existen por dominio
 * —cobertura, cupo, actividad, sitemaps—: no hay adónde navegar, así que el
 * panel es lo que hay. El quinto es la lista de lo que necesita atención, que
 * tampoco es una pantalla: es un recorte de tres tablas distintas.
 *
 * **Panel y no diálogo, y esto no se rediscute.** Lo que se abre es una lista, y
 * el contrato prohíbe explícitamente meter una tabla o una lista adentro de un
 * `Dialog`: para volver a mirar el tablero habría que cerrarlo. Y si esta lista
 * abriera de una forma distinta a los otros cuatro paneles de la misma pantalla,
 * volveríamos a tener dos maneras de hacer lo mismo en el mismo lugar, que es
 * exactamente lo que este rediseño está sacando.
 *
 * Lo abierto vive en la dirección (`?panel=coverage`), así que el panel se puede
 * enlazar y el botón Atrás lo cierra. Un solo `DetailSheet` para los cinco y no
 * cinco montados a la vez: sólo hay uno abierto por definición, y cinco
 * superficies compitiendo por el mismo parámetro es la forma de que un día se
 * abran dos.
 */
function AspectPanel({
  openKey,
  attention,
  canOperate,
  coverage,
  quota,
  sitemaps,
  activity,
  batches,
}: {
  openKey: PanelKey | null
  attention: AttentionItems
  canOperate: boolean
  coverage: CoverageOverview
  quota: TodayQuota
  sitemaps: SitemapsSummary
  activity: Activity
  batches: RecentBatch[]
}) {
  return (
    <DetailSheet
      paramName="panel"
      openKey={openKey}
      title={openKey ? panelTitle(openKey) : ''}
      description={
        openKey ? panelDescription(openKey, coverage, quota, sitemaps, attention) : undefined
      }
      /*
        El ancho lo decide el panel y no los cinco juntos.

        `lg` está porque los que listan lotes y avisos ponen un hostname —una
        sola palabra sin espacios, de hasta treinta y cinco caracteres— al lado
        de un estado y una fecha, y en un panel angosto ese nombre, que es el
        dato que la persona vino a leer, se parte en cuatro renglones.

        El de cobertura no tiene ese problema: son tres columnas cortas y una
        lista de un dominio. A 768 px queda una franja vacía del ancho de media
        pantalla al lado de tres cifras.
      */
      size={openKey === 'coverage' ? 'default' : 'lg'}
    >
      {openKey === 'attention' ? (
        <AttentionList attention={attention} canOperate={canOperate} />
      ) : null}
      {openKey === 'coverage' ? <CoveragePanel coverage={coverage} /> : null}
      {openKey === 'quota' ? <QuotaPanel quota={quota} /> : null}
      {openKey === 'activity' ? <ActivityPanel activity={activity} batches={batches} /> : null}
      {openKey === 'sitemaps' ? <SitemapsPanel sitemaps={sitemaps} /> : null}
    </DetailSheet>
  )
}

/** La bajada de cada panel: dice qué se está mirando y qué queda afuera. */
function panelDescription(
  panel: PanelKey,
  coverage: CoverageOverview,
  _quota: TodayQuota,
  sitemaps: SitemapsSummary,
  attention: AttentionItems
): string {
  if (panel === 'attention') return attentionHeadline(attention)

  if (panel === 'coverage') {
    return coverage.without_data > 0
      ? t('dashboard.panel.coverage.description.partial', {
          withData: formatNumber(coverage.with_data),
          withoutData: formatNumber(coverage.without_data),
        })
      : t('dashboard.panel.coverage.description', {
          withData: formatNumber(coverage.with_data),
        })
  }

  if (panel === 'sitemaps') {
    return t('dashboard.panel.sitemaps.description', {
      count: sitemaps.total,
      total: formatNumber(sitemaps.total),
    })
  }

  return t(`dashboard.panel.${panel}.description` as TranslationKey)
}

/**
 * El reparto por estado de la cuenta, y el enlace a la cobertura de cada
 * dominio.
 *
 * Todo porcentaje va sobre las que **tienen dato** y lo dice en el encabezado de
 * la columna. `UNKNOWN` no entra en ninguno: significa «todavía no le
 * preguntamos a Google», y meterlo adentro lo convertiría en «no está
 * indexada», que es una afirmación sobre Google que no podemos hacer (R-B). Por
 * eso viaja aparte, con su reloj y sin porcentaje.
 */
/**
 * Una fila del reparto, que además puede decir **cuáles**.
 *
 * El reparto decía «3 descubiertas y sin rastrear» y ahí se terminaba: para
 * saber cuáles había que salir a la tabla de cobertura y rearmar el filtro a
 * mano. Acá la cifra se abre donde se lee, que es la regla del repo.
 *
 * Las direcciones sólo viajan para los estados que no son «indexada»: son las
 * que alguien mira, y una fila de doscientas treinta y ocho correctas no se
 * despliega nunca. Sin ellas la fila queda como estaba, sin flecha, en vez de
 * ofrecer un desplegable vacío.
 */
function StateRow({
  state,
  count,
  withData,
  urls,
}: {
  state: string
  count: number
  withData: number
  urls: string[]
}) {
  const [open, setOpen] = useState(false)
  const hidden = Math.max(count - urls.length, 0)

  return (
    <>
      <tr className="border-border/60 border-t">
        <td className="py-1.5 pr-2">
          {urls.length > 0 ? (
            <button
              type="button"
              onClick={() => setOpen((was: boolean) => !was)}
              aria-expanded={open}
              className="focus-visible:ring-ring flex items-center gap-1 rounded text-left underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:outline-none"
            >
              <ChevronRight
                className={cn('size-3.5 transition-transform', open && 'rotate-90')}
                aria-hidden
              />
              {coverageStateLabel(state)}
            </button>
          ) : (
            coverageStateLabel(state)
          )}
        </td>
        <td className="py-1.5 text-right tabular-nums">{formatNumber(count)}</td>
        <td className="py-1.5 text-right tabular-nums">
          {formatNumber(percentage(count, withData))}&nbsp;%
        </td>
      </tr>

      {open ? (
        <tr className="border-border/60 border-t">
          {/* Ocupa el ancho entero: las direcciones no son una celda de ninguna
              de las tres columnas, son el detalle de la fila de arriba. */}
          <td colSpan={3} className="py-2">
            <ul className="space-y-1">
              {urls.map((loc) => (
                <li key={loc}>
                  <CodeChip value={loc} copy />
                </li>
              ))}
            </ul>
            {hidden > 0 ? (
              <p className="text-muted-foreground pt-2 text-sm text-pretty">
                {t('urlList.truncated', { count: hidden, total: formatNumber(hidden) })}
              </p>
            ) : null}
          </td>
        </tr>
      ) : null}
    </>
  )
}

function CoveragePanel({ coverage }: { coverage: CoverageOverview }) {
  const rows = Object.entries(coverage.by_state)
    .filter(([state, count]) => state !== 'UNKNOWN' && count > 0)
    .sort((first, second) => second[1] - first[1])

  return (
    <div className="flex flex-col gap-6">
      <Section title={{ text: t('dashboard.panel.byState') }}>
        {rows.length > 0 ? (
          <table className="w-full text-sm">
            <caption className="sr-only">
              {t('coverageSummary.table.caption', {
                withData: formatNumber(coverage.with_data),
                notQueried: formatNumber(coverage.without_data),
              })}
            </caption>
            <thead>
              <tr className="text-muted-foreground text-xs">
                <th scope="col" className="py-1 text-left font-medium">
                  {t('coverageSummary.table.state')}
                </th>
                <th scope="col" className="py-1 text-right font-medium">
                  {t('coverageSummary.table.urls')}
                </th>
                <th scope="col" className="py-1 text-right font-medium whitespace-nowrap">
                  {t('coverageSummary.table.share', { total: formatNumber(coverage.with_data) })}
                </th>
              </tr>
            </thead>
            <tbody>
              {rows.map(([state, count]) => (
                <StateRow
                  key={state}
                  state={state}
                  count={count}
                  withData={coverage.with_data}
                  urls={coverage.urls_by_state[state] ?? []}
                />
              ))}
            </tbody>
          </table>
        ) : (
          <p className="text-muted-foreground text-sm text-pretty">
            {t('dashboard.panel.coverage.noneQueried')}
          </p>
        )}

        {coverage.without_data > 0 ? (
          <p className="text-muted-foreground flex items-start gap-2 text-sm text-pretty">
            <Clock className="mt-0.5 size-4 shrink-0" aria-hidden />
            {t('dashboard.panel.coverage.notQueried', {
              count: coverage.without_data,
              total: formatNumber(coverage.without_data),
            })}
          </p>
        ) : null}
      </Section>

      <Section title={{ text: t('dashboard.panel.byDomain') }}>
        <ItemGroup className="gap-2">
          {coverage.domains.map((domain) => (
            <NoticeItem
              key={domain.id}
              title={
                <Link
                  href={route('coverage', { domain_id: domain.id })}
                  className={HOSTNAME_LINK}
                  translate="no"
                >
                  {domain.hostname}
                </Link>
              }
              description={t('dashboard.panel.coverage.domain', {
                indexed: formatNumber(domain.indexed),
                total: formatNumber(domain.total),
                withData: formatNumber(domain.with_data),
              })}
              meta={
                <>
                  {t('dashboard.panel.coverage.lastQuery')}{' '}
                  <DataTimestamp value={domain.last_checked_at} emptyLabel={t('time.never')} />
                </>
              }
            />
          ))}
        </ItemGroup>
      </Section>

      {coverage.frozen_domains > 0 ? (
        /*
          Lo congelado se cuenta aparte de lo que sigue vivo. Un dominio sin
          acceso no aporta datos nuevos desde que lo perdió, así que sumarlo en
          silencio al resto presenta una foto vieja como si fuera de hoy (R-F).
        */
        <p className="text-muted-foreground text-sm text-pretty">
          {t('dashboard.panel.coverage.frozen', {
            count: coverage.frozen_domains,
            total: formatNumber(coverage.frozen_domains),
          })}
        </p>
      ) : null}
    </div>
  )
}

function percentage(part: number, total: number): number {
  if (!total) return 0
  return Math.round((part / total) * 1000) / 10
}

/**
 * El cupo del día, abierto por propiedad y con sus dos bolsillos.
 *
 * Una propiedad por `Section`, y adentro el medidor de siempre. El cupo es de
 * **cada** propiedad en Search Console y no un saldo común: la suma sirve para
 * la tarjeta, pero lo que dice qué se puede consultar hoy es la fila de cada
 * dominio.
 */
function QuotaPanel({ quota }: { quota: TodayQuota }) {
  if (quota.domains.length === 0) {
    return (
      <EmptyState
        icon={Clock}
        title={t('dashboard.panel.quota.empty.title')}
        description={t('dashboard.panel.quota.empty.description')}
      />
    )
  }

  return (
    <div className="flex flex-col gap-6">
      {quota.domains.map((domain) => (
        // `wrap-anywhere` se hereda hasta el encabezado: un hostname es una sola
        // palabra sin espacios y en 576 px de panel empuja todo hacia el costado.
        <Section
          key={domain.id}
          // `h3`: estas secciones viven dentro del panel, que ya puso su `h2`.
          title={{ text: domain.hostname, level: 'h3' }}
          className="wrap-anywhere"
        >
          <QuotaMeter
            date={quota.date}
            limitTotal={domain.limit_total}
            manualReserve={domain.manual_reserve}
            usedAutomatic={domain.used_automatic}
            usedManual={domain.used_manual}
          />
        </Section>
      ))}
    </div>
  )
}

/**
 * La actividad de la cuenta: el gráfico y lo último que corrió.
 *
 * Los dos vivían en la primera pantalla y se mudaron acá enteros. El gráfico
 * conserva sus dos series sin apilar y su tabla equivalente; la lista conserva
 * su barra de avance, que sale de las cifras y nunca del estado, así que un lote
 * cortado por cupo se ve cortado aunque haya terminado (R-C).
 *
 * Cinco filas y sin paginar: es un vistazo, no el historial. Para eso está la
 * lista por dominio, que pagina.
 */
function ActivityPanel({ activity, batches }: { activity: Activity; batches: RecentBatch[] }) {
  return (
    <div className="flex flex-col gap-6">
      <ChartAreaInteractive activity={activity} />

      <Section title={{ text: t('dashboard.panel.activity.lastBatches') }}>
        {batches.length === 0 ? (
          <p className="text-muted-foreground text-sm text-pretty">
            {t('dashboard.panel.activity.noBatches')}
          </p>
        ) : (
          <ItemGroup className="gap-2">
            {batches.map((batch) => (
              <NoticeItem
                key={batch.id}
                badge={<BatchStateBadge state={batch.state} />}
                title={
                  <Link href={batchPath(batch.id)} className={HOSTNAME_LINK}>
                    {batchKindLabel(batch.kind)} {t('dashboard.attention.on')}{' '}
                    <span translate="no">{batch.hostname}</span>
                  </Link>
                }
                meta={
                  <div className="flex flex-col gap-1.5">
                    <span>
                      {batchOriginLabel(batch.origin)} · {t('batches.batchWord')}{' '}
                      <span translate="no" className="font-mono">
                        {shortBatchName(batch.id)}
                      </span>{' '}
                      ·{' '}
                      {batch.is_terminal ? (
                        <>
                          {t('dashboard.panel.activity.finished')}{' '}
                          <DataTimestamp value={batch.finished_at} emptyLabel={t('time.noDate')} />
                        </>
                      ) : batch.started_at ? (
                        <>
                          {t('dashboard.panel.activity.started')}{' '}
                          <DataTimestamp value={batch.started_at} />
                        </>
                      ) : (
                        <>
                          {t('dashboard.panel.activity.queuedSince')}{' '}
                          <DataTimestamp value={batch.created_at} emptyLabel={t('time.noDate')} />
                        </>
                      )}
                    </span>
                    <BatchProgress
                      total={batch.total_items}
                      processed={batch.processed_items}
                      failed={batch.failed_items}
                      state={batch.state}
                    />
                  </div>
                }
                /*
                  Dos destinos por fila son un menú. El segundo enlace escrito
                  entero —«Ver todos los lotes de ejemplo.com»— costaba un
                  renglón por fila sin agregar ningún destino nuevo.
                */
                menu={
                  <RowActions
                    label={t('dashboard.panel.activity.rowActions', {
                      name: shortBatchName(batch.id),
                      hostname: batch.hostname,
                    })}
                    /*
                      Los dos rótulos se pisan porque los dos nombran el objeto:
                      «Abrir» a secas no distingue el lote de la lista de lotes
                      del dominio, que es justamente la diferencia entre las dos
                      entradas. El icono y la posición los sigue poniendo el
                      registro de `RowActions`.
                    */
                    actions={{
                      open: {
                        label: t('dashboard.attention.seeBatch'),
                        href: batchPath(batch.id),
                      },
                      list: {
                        label: t('dashboard.panel.activity.seeAllBatches', {
                          hostname: batch.hostname,
                        }),
                        href: batchesPath(batch.domain_id),
                      },
                    }}
                  />
                }
              />
            ))}
          </ItemGroup>
        )}
      </Section>
    </div>
  )
}

/**
 * Los sitemaps de la cuenta, por dominio y con su última lectura.
 *
 * Los dominios sin ningún sitemap también aparecen, con su cero: es el único
 * aspecto donde el cero es la respuesta que se buscaba, porque una cuenta sin
 * URLs descubiertas casi siempre es una cuenta a la que le falta registrar un
 * sitemap.
 *
 * La fecha es la de la última lectura nuestra del archivo, no la del último
 * envío a Google: son dos hechos distintos y el que explica de cuándo son las
 * URLs es el primero.
 */
function SitemapsPanel({ sitemaps }: { sitemaps: SitemapsSummary }) {
  return (
    <Section title={{ text: t('dashboard.panel.byDomain'), isHidden: true }}>
      <ItemGroup className="gap-2">
        {sitemaps.domains.map((domain) => (
          <NoticeItem
            key={domain.id}
            badge={
              domain.total > 0 ? (
                <StatusBadge tone="positive" icon={FileText}>
                  {t('dashboard.panel.sitemaps.count', {
                    count: domain.total,
                    total: formatNumber(domain.total),
                  })}
                </StatusBadge>
              ) : (
                <StatusBadge tone="attention" icon={TriangleAlert}>
                  {t('dashboard.panel.sitemaps.none')}
                </StatusBadge>
              )
            }
            title={
              <Link
                href={route('sitemaps', { domain_id: domain.id })}
                className={HOSTNAME_LINK}
                translate="no"
              >
                {domain.hostname}
              </Link>
            }
            description={
              domain.total > 0 ? undefined : t('dashboard.panel.sitemaps.noneDescription')
            }
            meta={
              <>
                {t('dashboard.panel.sitemaps.lastRead')}{' '}
                <DataTimestamp value={domain.last_read_at} emptyLabel={t('time.never')} />
              </>
            }
          />
        ))}
      </ItemGroup>
    </Section>
  )
}
