import { Link, useForm, usePage } from '@inertiajs/react'
import { Bell, BellOff, Clock, Globe, Plus, Search } from 'lucide-react'
import type { ReactNode } from 'react'
import { useMemo } from 'react'

import { AccessStateBadge, accessStateHelp, accessStateLabel } from '@/components/AccessStateBadge'
import { DataTable, createDataTableColumns, tableUrl } from '@/components/DataTable'
import type { DataTableColumn, TablePage } from '@/components/DataTable'
import { DataTimestamp, TimezoneFootnote } from '@/components/DataTimestamp'
import { ServerErrorNotice } from '@/components/ServerErrorNotice'
import { AppLayout } from '@/layouts/AppLayout'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { formatNumber } from '@/lib/format'
import { t } from '@/lib/i18n'
import type { TranslationKey } from '@/lib/i18n'
import { route } from '@/lib/routes'

/**
 * Resumen de cobertura de una fila (FR-062).
 *
 * `known_urls` sobre `total_urls` es el par que hace honesto a todo lo demás:
 * «980 indexadas» sobre un sitio del que se consultó una cuarta parte insinúa
 * una cobertura que no existe. Las URLs sin consultar quedan **fuera** de
 * `known_urls` y por lo tanto de cualquier proporción (R-B).
 */
interface Coverage {
  total_urls: number
  known_urls: number
  indexed: number
  not_indexed: number
  /** Nulo mientras no haya corrido ningún ciclo. Sin él no hay reparto que mostrar (R-A). */
  last_cycle_at: string | null
}

interface Domain {
  id: string
  hostname: string
  property_type: string
  property_uri: string
  access_state: string
  access_checked_at: string | null
  access_error: string | null
  access_error_code: string | null
  is_operational: boolean
  daily_inspection_budget: number
  manual_reserve: number
  notifications_enabled: boolean
  /** Nulo cuando el dominio todavía no tiene ninguna URL descubierta. */
  coverage: Coverage | null
}

interface Props {
  domains: TablePage<Domain>
  access_states: { value: string; label: string }[]
  /** Dominios de la cuenta sin filtrar. Separa «no hay ninguno» de «el filtro no encontró». */
  total: number
  /** Hostname recién dado de alta, si venimos de la redirección del alta. */
  created: string | null
}

/**
 * Los dos estados desde los que volver a comprobar el acceso tiene sentido.
 *
 * Revocado y suspendido son detenciones administrativas: comprobarlos no los
 * destraba —la máquina de estados ni siquiera admite el paso a operativo— así
 * que ofrecer el botón sería ofrecer algo que falla siempre.
 */
const CHECKABLE_STATES = ['AWAITING_ACCESS', 'OPERATIONAL', 'ACCESS_LOST']

const columnHelper = createDataTableColumns<Domain>()

/**
 * Las columnas del listado.
 *
 * Se arman con `canOperate` adentro porque esa condición cambia lo que dice
 * cada fila: con la credencial de la cuenta caída, ningún dominio se presenta
 * como operativo y ninguna fila ofrece una acción que llame a Google (RT-18).
 * Es un dato de la cuenta, no de la fila, y por eso entra por acá.
 *
 * Los identificadores tienen que coincidir con las claves que el servidor
 * declara ordenables en `DOMAINS`; si no coinciden, el encabezado ofrecería un
 * orden que el servidor descarta en silencio.
 */
function domainColumns(canOperate: boolean): DataTableColumn<Domain>[] {
  return [
    columnHelper.accessor('hostname', {
      id: 'domain',
      header: t('domains.column.domain'),
      cell: (info) => {
        const domain = info.row.original
        return (
          <div className="space-y-0.5">
            {/*
              El enlace es el nombre del dominio y no la fila entera: así un
              lector de pantalla anuncia un destino con sentido en vez de
              repetir cinco celdas como si fueran un botón. Lleva a la ficha,
              que es desde donde se llega al resto; antes llevaba directo a la
              cobertura, que es una de las tres vistas hijas.
            */}
            <Link
              href={route('domain.show', { domain_id: domain.id })}
              className="focus-visible:ring-ring rounded font-medium hover:underline focus-visible:ring-2 focus-visible:outline-none"
            >
              <span translate="no">{domain.hostname}</span>
            </Link>
            <p className="text-muted-foreground text-xs">
              {t(`propertyShape.${domain.property_type}.title` as TranslationKey)}
            </p>
          </div>
        )
      },
    }),

    columnHelper.accessor('access_state', {
      id: 'access',
      header: t('domains.column.access'),
      cell: (info) => <AccessCell domain={info.row.original} canOperate={canOperate} />,
    }),

    columnHelper.accessor('access_checked_at', {
      id: 'checked',
      // La fecha va en su propia columna y no en un globo: tiene que sobrevivir
      // a una captura de pantalla y a una exportación (RT-02).
      header: t('domains.column.checked'),
      cell: (info) => (
        <DataTimestamp
          value={info.row.original.access_checked_at}
          emptyLabel={t('domains.notChecked')}
          className="text-sm"
        />
      ),
    }),

    columnHelper.accessor('coverage', {
      id: 'coverage',
      // El encabezado nombra el denominador antes que los conteos, igual que la
      // celda: es lo que evita leer «980 indexadas» como una cobertura total.
      header: t('domains.column.coverage'),
      cell: (info) => <CoverageCell domain={info.row.original} />,
    }),

    columnHelper.accessor('notifications_enabled', {
      id: 'notifications',
      header: t('domains.column.notifications'),
      cell: (info) =>
        info.row.original.notifications_enabled ? (
          <span className="flex items-center gap-1.5 text-sm">
            <Bell className="size-3.5 shrink-0" aria-hidden />
            {t('domains.notifications.on')}
          </span>
        ) : (
          <span className="text-muted-foreground flex items-center gap-1.5 text-sm">
            <BellOff className="size-3.5 shrink-0" aria-hidden />
            {t('domains.notifications.off')}
          </span>
        ),
    }),
  ]
}

export default function DomainsIndex({ domains, access_states, total, created }: Props) {
  const { account, errors } = usePage().props
  const canOperate = account?.can_operate ?? false
  const columns = useMemo(() => domainColumns(canOperate), [canOperate])

  const filter = domains.filters.access_state ?? ''
  const caption = filter
    ? t('domains.caption.filtered', { state: accessStateLabel(filter) })
    : t('domains.title')

  return (
    <AppLayout
      title={t('domains.title')}
      description={t('domains.description')}
      actions={<AddDomainAction canOperate={canOperate} />}
    >
      {/*
        El resultado de comprobar el acceso desde una fila vuelve acá (RT-11).
        El único motivo por el que la acción no llega a Google es que la
        credencial de la cuenta no sirva, así que el código es conocido y el
        aviso ofrece la pantalla que lo resuelve en vez de la acción neutra.
      */}
      {errors.check ? (
        <ServerErrorNotice
          code={errors.check.code}
          message=""
          action={
            <Button asChild size="sm" variant="outline">
              <Link href={route('settings')}>{t('domains.reviewConnection')}</Link>
            </Button>
          }
        />
      ) : null}

      {created ? <DomainAddedNotice hostname={created} domains={domains} /> : null}

      {/*
        Sin credencial comprobada el alta no puede terminar, así que se dice acá
        y el botón de arriba lleva a la configuración. No hay un segundo botón:
        el aviso de la cuenta ya está encima con su acción, y dos controles al
        mismo destino se leen como dos problemas distintos (RT-18).
      */}
      {canOperate ? null : (
        <Alert>
          <Globe />
          <AlertTitle>{t('domains.needsConnection.title')}</AlertTitle>
          <AlertDescription>{t('domains.needsConnection.body')}</AlertDescription>
        </Alert>
      )}

      {total > 0 ? (
        <AccessStateFilter domains={domains} states={access_states} current={filter} />
      ) : null}

      <DataTable
        data={domains}
        columns={columns}
        unit={t('domains.unit')}
        unitSingular={t('domains.unitSingular')}
        caption={caption}
        rowId={(row) => row.id}
        empty={
          filter
            ? {
                icon: Search,
                title: t('domains.empty.filtered.title'),
                description: t('domains.empty.filtered.description'),
                action: (
                  <Button asChild variant="outline">
                    <Link href={tableUrl(domains, { access_state: null, page: null })} preserveScroll>
                      {t('domains.empty.filtered.action')}
                    </Link>
                  </Button>
                ),
              }
            : {
                icon: Globe,
                title: t('domains.empty.title'),
                description: t('domains.empty.description'),
                action: <AddDomainAction canOperate={canOperate} />,
              }
        }
      />

      {/*
        Fija y debajo de la tabla, no en un aviso que se cierra: la confusión que
        evita —que las URLs sin consultar no son URLs sin indexar— vuelve cada
        vez que se mira esta columna, así que el texto también (R-B).
      */}
      <p className="text-muted-foreground text-sm text-pretty">{t('domains.coverageFootnote')}</p>

      <TimezoneFootnote />
    </AppLayout>
  )
}

/**
 * La acción primaria, o el paso previo que la habilita.
 *
 * Con la credencial sin comprobar, el alta redirige a la configuración desde el
 * servidor. El botón dice adónde lleva de verdad en vez de prometer un
 * formulario que no se va a ver: no se ofrece deshabilitado, se reemplaza
 * (RT-18).
 */
function AddDomainAction({ canOperate }: { canOperate: boolean }) {
  if (!canOperate) {
    return (
      <Button asChild>
        <Link href={route('settings')}>{t('domains.goToSettings')}</Link>
      </Button>
    )
  }

  return (
    <Button asChild>
      <Link href={route('domain.new')}>
        <Plus aria-hidden />
        {t('domains.add')}
      </Link>
    </Button>
  )
}

/**
 * Lo que pasó recién y lo que falta ahora (RT-11).
 *
 * No es un mensaje que se desvanece porque lo que dice es una instrucción, no
 * una palmada: el dominio quedó dado de alta y todavía no sirve para nada hasta
 * que se lo autorice en Search Console. Desaparece solo al primer clic en la
 * tabla, que es cuando ya dejó de ser la novedad de la pantalla.
 */
function DomainAddedNotice({ hostname, domains }: { hostname: string; domains: TablePage<Domain> }) {
  const added = domains.rows.find((row) => row.hostname === hostname)

  return (
    <Alert role="status">
      <Globe />
      <AlertTitle>
        {t('domains.added.title')} <span translate="no">{hostname}</span>
      </AlertTitle>
      <AlertDescription className="flex flex-col items-start gap-3">
        <span>{t('domains.added.body')}</span>
        {added ? (
          <Button asChild size="sm" variant="outline">
            <Link href={route('domain.show', { domain_id: added.id })}>
              {t('domains.added.action')}
            </Link>
          </Button>
        ) : null}
      </AlertDescription>
    </Alert>
  )
}

/**
 * El filtro por estado de acceso, como enlaces de verdad (RT-09).
 *
 * Cada opción tiene dirección propia, así que se abre en otra pestaña, se copia
 * y se guarda; y como el estado vive en la querystring, el botón Atrás funciona
 * y recargar no lo pierde. Eso es también lo que hace que
 * `/domains?access_state=ACCESS_LOST` —la dirección a la que entra el aviso de
 * la cuenta— llegue con el filtro puesto y a la vista.
 *
 * Se arma pisando una clave sobre la dirección que devolvió el servidor:
 * reconstruirla a mano haría reaparecer en el primer clic un filtro que el
 * servidor había descartado.
 */
function AccessStateFilter({
  domains,
  states,
  current,
}: {
  domains: TablePage<Domain>
  states: Props['access_states']
  current: string
}) {
  return (
    <section aria-labelledby="filter-state" className="space-y-2">
      <p id="filter-state" className="text-muted-foreground text-xs">
        {t('domains.filter.label')}
      </p>
      {/*
        Grupo con nombre y no `fieldset`: son enlaces, no controles de
        formulario, y `fieldset` le prometería a un lector de pantalla campos
        que no hay.
      */}
      <div role="group" aria-labelledby="filter-state" className="flex flex-wrap gap-1.5">
        <FilterOption domains={domains} value="" active={current === ''}>
          {t('table.all')}
        </FilterOption>
        {states.map((state) => (
          <FilterOption
            key={state.value}
            domains={domains}
            value={state.value}
            active={current === state.value}
          >
            {accessStateLabel(state.value) || state.label}
          </FilterOption>
        ))}
      </div>
    </section>
  )
}

function FilterOption({
  domains,
  value,
  active,
  children,
}: {
  domains: TablePage<Domain>
  value: string
  active: boolean
  children: ReactNode
}) {
  return (
    <Button asChild size="sm" variant={active ? 'default' : 'outline'}>
      <Link
        href={tableUrl(domains, { access_state: value || null, page: null })}
        preserveScroll
        preserveState
        aria-current={active ? 'true' : undefined}
      >
        {children}
      </Link>
    </Button>
  )
}

/**
 * El estado del acceso: qué significa y qué se puede hacer.
 *
 * Es la columna que dispara la acción, así que lleva el badge, su explicación
 * de una línea y —sólo cuando comprobar tiene sentido— el botón que la vuelve a
 * intentar, cuyo resultado aterriza en esta misma fila (RT-11).
 *
 * El motivo del último fallo no se muestra acá: es un texto largo de Google que
 * en una celda empuja el resto de la tabla, y la salida de cada código vive en
 * la ficha, a un clic del nombre del dominio.
 */
function AccessCell({ domain, canOperate }: { domain: Domain; canOperate: boolean }) {
  return (
    <div className="space-y-1.5">
      <AccessStateBadge state={domain.access_state} canOperate={canOperate} />
      {/*
        Una sola explicación por celda. Sin credencial, la del estado —«falta
        autorizar la cuenta de servicio»— manda a Search Console a resolver algo
        que ahí no se arregla, y sumarle abajo un segundo aviso deja dos motivos
        distintos para el mismo renglón (RT-18). La acción que sí lo resuelve
        está arriba, en el aviso de la cuenta: cincuenta filas con el mismo
        botón no la arreglan más rápido.
      */}
      <p className="text-muted-foreground max-w-56 text-xs text-pretty">
        {accessStateHelp(domain.access_state, canOperate) || accessStateLabel(domain.access_state)}
      </p>

      {canOperate && CHECKABLE_STATES.includes(domain.access_state) ? (
        <CheckAccessButton domain={domain} />
      ) : null}
    </div>
  )
}

/**
 * Volver a preguntarle a Google por esta propiedad.
 *
 * Se deshabilita **sólo** el botón que la disparó y cambia su texto mientras
 * tanto (RT-12): el resto de la tabla sigue usable, y quien la aprieta ve que
 * pasó algo sin que se le mueva la pantalla.
 */
function CheckAccessButton({ domain }: { domain: Domain }) {
  const checkForm = useForm({})

  return (
    <Button
      size="sm"
      variant="outline"
      onClick={() =>
        checkForm.post(route('domain.check', { domain_id: domain.id }), {
          preserveScroll: true,
          preserveState: true,
        })
      }
      disabled={checkForm.processing}
      aria-busy={checkForm.processing}
    >
      {checkForm.processing ? t('credentialError.checking') : t('domains.checkAccess')}
      <span className="sr-only">{t('domains.ofDomain', { hostname: domain.hostname })}</span>
    </Button>
  )
}

/**
 * El resumen de cobertura de la fila (FR-062, H-07).
 *
 * Tres formas distintas para tres situaciones distintas, y ninguna de las tres
 * es una cifra sola:
 *
 * - Sin URLs descubiertas todavía no hay nada que contar, y el siguiente paso
 *   son los sitemaps.
 * - Sin ninguna URL con dato se dice eso, no ceros: «0 indexadas» se lee como
 *   una afirmación sobre Google que no podemos hacer.
 * - Con datos, el denominador va **delante** de los conteos y en la misma
 *   celda, sin pasar el mouse por ningún lado (RT-03).
 *
 * Va todo en un solo párrafo para que un lector de pantalla lo lea como una
 * frase con su denominador incluido, y no como cuatro números sueltos.
 */
function CoverageCell({ domain }: { domain: Domain }) {
  const summary = domain.coverage

  if (summary === null) {
    return (
      <p className="text-muted-foreground max-w-64 text-sm text-pretty">
        {t('domains.coverage.noUrls')}{' '}
        <Link
          href={route('sitemaps', { domain_id: domain.id })}
          className="hover:text-foreground focus-visible:ring-ring rounded underline underline-offset-3 focus-visible:ring-2 focus-visible:outline-none"
        >
          {t('domains.coverage.seeSitemaps')}
          <span className="sr-only">{t('domains.ofDomain', { hostname: domain.hostname })}</span>
        </Link>
      </p>
    )
  }

  const urlLabel = t('coverageSummary.urls', { count: summary.total_urls })

  /*
    Sin ninguna URL con dato no hay denominador, así que no hay reparto: el
    bloque toma la familia visual de «sin dato» —neutra y con reloj, nunca
    verde, roja ni ámbar (RT-03)— y dice cuántas URLs conocemos, que es lo único
    cierto en ese momento. La condición mira `known_urls` y no la fecha del
    ciclo porque un ciclo que corrió y no obtuvo ningún estado deja las mismas
    cero URLs con dato.
  */
  if (summary.known_urls === 0) {
    return (
      <p className="text-muted-foreground max-w-64 text-sm text-pretty">
        <Clock className="mr-1 inline size-3.5 align-[-0.15em]" aria-hidden />
        {t('domains.coverage.noData')} ·{' '}
        <span className="tabular-nums">{formatNumber(summary.total_urls)}</span>
        &nbsp;{t('domains.coverage.discovered', { unit: urlLabel })}
        {summary.last_cycle_at ? (
          <>
            {` · ${t('domains.coverage.lastCycle')} `}
            <DataTimestamp value={summary.last_cycle_at} />
          </>
        ) : null}
      </p>
    )
  }

  return (
    <p className="max-w-72 text-sm text-pretty">
      <span className="font-medium tabular-nums">
        {t('domains.coverage.withData', {
          known: formatNumber(summary.known_urls),
          total: formatNumber(summary.total_urls),
          unit: urlLabel,
        })}
      </span>
      <span className="text-muted-foreground">
        {' · '}
        <span className="tabular-nums">{formatNumber(summary.indexed)}</span>{' '}
        {t('domains.coverage.indexed')}
        {' · '}
        <span className="tabular-nums">{formatNumber(summary.not_indexed)}</span>{' '}
        {t('domains.coverage.notIndexed')}
        {` · ${t('domains.coverage.lastCycle')} `}
        <DataTimestamp value={summary.last_cycle_at} />
      </span>
    </p>
  )
}
