import { router, useForm, usePage } from '@inertiajs/react'
import { Download, Send, ShieldAlert } from 'lucide-react'
import { useCallback, useMemo, useState } from 'react'

import { BatchStateHeadline } from '@/components/BatchStateBadge'
import { ClientDataTable, createClientDataTableColumns } from '@/components/ClientDataTable'
import type { ClientDataTableColumn, RowActionsFor } from '@/components/ClientDataTable'
import { ConfirmDestructive } from '@/components/ConfirmDestructive'
import { CoverageStateBadge } from '@/components/CoverageStateBadge'
import { DataTimestamp } from '@/components/DataTimestamp'
import { EmptyState } from '@/components/EmptyState'
import { IndexingStateBadge } from '@/components/IndexingStateBadge'
import { DescriptionList } from '@/components/LabelValue'
import { Section } from '@/components/Section'
import { SectionCard } from '@/components/SectionCard'
import { ServerErrorNotice } from '@/components/ServerErrorNotice'
import { useBatchPolling } from '@/hooks/useBatchPolling'
import { AppLayout } from '@/layouts/AppLayout'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
} from '@/components/ui/sheet'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { 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'

/** Un pedido, tal como lo serializa `_request_props()` en el servidor. */
interface IndexingRequestRow {
  id: string
  loc: string
  state: string
  position: number
  sent_at: string | null
  response_status: number | null
  error_code: string
  has_evidence: boolean
  request_body: Record<string, unknown> | null
  response_body: Record<string, unknown> | null
  /**
   * Lo que Google dice de la dirección, en su **propia** clave.
   *
   * Viaja al lado del estado del pedido y nunca mezclado con él: son las dos
   * afirmaciones que este producto tiene que mantener separadas, y compartir
   * campo las encimaría en la pantalla igual que las encimaría en la base.
   */
  coverage_state: string
  /** El código del motivo por el que pedir esto está desaconsejado, o vacío. */
  discouraged: string
}

interface Props {
  domain: {
    id: string
    hostname: string
    is_operational: boolean
  }
  batch: {
    id: string
    state: string
    origin: string
    total_items: number
    processed_items: number
    failed_items: number
    created_at: string
    started_at: string | null
    finished_at: string | null
    is_draft: boolean
    is_running: boolean
    /** Por qué se detuvo, en código. Vacío cuando no se detuvo. */
    stopped_by: string
  }
  progress: {
    pending: number
    sent: number
    errors: number
    removed: number
  }
  requests: IndexingRequestRow[]
}

const columnHelper = createClientDataTableColumns<IndexingRequestRow>()

/** Los motivos de corte que el producto sabe explicar. Lo demás cae en la frase genérica. */
const STOP_REASONS = ['QUOTA_EXCEEDED', 'PERMISSION_DENIED', 'API_NOT_ENABLED', 'INVALID_KEY']

/** Los cinco estados desaconsejados, cada uno con su explicación en el catálogo. */
const DISCOURAGED = [
  'EXCLUDED_NOINDEX',
  'BLOCKED_ROBOTS',
  'DUPLICATE_CANONICAL',
  'REDIRECT',
  'UNKNOWN',
]

function requestColumns(): ClientDataTableColumn<IndexingRequestRow>[] {
  return [
    columnHelper.accessor('loc', {
      id: 'url',
      header: t('indexing.column.url'),
      cell: (info) => (
        // Una línea, truncada, con la dirección entera en el globo. Es el mismo
        // criterio que la tabla de cobertura: filas de alto parejo para poder
        // comparar dos estados sin recorrer la tabla entera.
        <Tooltip>
          <TooltipTrigger asChild>
            <span translate="no" className="block max-w-80 truncate">
              {info.getValue()}
            </span>
          </TooltipTrigger>
          <TooltipContent className="max-w-md">
            <span translate="no" className="wrap-anywhere">
              {info.getValue()}
            </span>
          </TooltipContent>
        </Tooltip>
      ),
    }),
    columnHelper.accessor('state', {
      id: 'state',
      header: t('indexing.column.state'),
      cell: (info) => <IndexingStateBadge state={info.getValue()} />,
    }),
    columnHelper.accessor('coverage_state', {
      id: 'coverage',
      header: t('indexing.column.coverage'),
      cell: (info) =>
        info.getValue() ? (
          // Sin fecha de obtención a propósito: acá el dato de cobertura es
          // contexto para decidir si el pedido tiene sentido, y no la
          // afirmación que la pantalla sostiene. La fecha vive en la tabla de
          // cobertura, que es donde ese estado **es** el dato (RT-02).
          <CoverageStateBadge state={info.getValue()} fetchedAt={null} />
        ) : (
          <span className="text-muted-foreground text-sm">—</span>
        ),
    }),
    columnHelper.numeric('response_status', {
      id: 'response',
      header: t('indexing.column.response'),
      cell: (info) => {
        const row = info.row.original
        const status = info.getValue()
        if (status === null && !row.error_code) {
          return <span className="text-muted-foreground text-sm">—</span>
        }
        return (
          <span className="tabular-nums">
            {/* El código de Google al lado del HTTP: un 403 solo no dice si
                falta el permiso o si la API no está habilitada, y son dos
                arreglos en dos pantallas distintas. */}
            {status === null ? '—' : status}
            {row.error_code ? (
              <span className="text-muted-foreground ml-2 text-xs">{row.error_code}</span>
            ) : null}
          </span>
        )
      },
    }),
    columnHelper.accessor('sent_at', {
      id: 'sent',
      header: t('indexing.column.sent'),
      cell: (info) =>
        info.getValue() ? (
          <DataTimestamp value={info.getValue() as string} />
        ) : (
          <span className="text-muted-foreground text-sm">—</span>
        ),
    }),
  ]
}

//: La pestaña que abre. La otra es la lista larga, que no puede estar encima de
//: las cifras cuando alguien llega a ver si el lote terminó.
const DEFAULT_TAB = 'batch'

export default function IndexingBatch({ domain, batch, progress, requests }: Props) {
  const { errors } = usePage().props
  const run = useForm({})
  const resume = useForm({})
  const discard = useForm({})

  /*
    Qué pedido tiene abierta la ficha de evidencia.

    A diferencia del panel de cobertura, éste **no** vive en la dirección: lo
    que muestra ya viajó entero en las props, así que abrirlo no es una
    navegación y enlazarlo no traería nada que no esté acá. Guardar el id en la
    dirección sólo agregaría una entrada al historial por cada vez que alguien
    mira un cuerpo JSON.
  */
  const [openRequest, setOpenRequest] = useState<string | null>(null)
  const opened = requests.find((item) => item.id === openRequest) ?? null

  /*
    Esta pantalla no se actualizaba sola, y era la única del producto con
    trabajo en curso que no lo hacía.

    El efecto: se confirmaba el borrador, el envío salía y arrancaba, y acá no
    pasaba nada —ni progreso, ni cambio de estado, ni el aviso al terminar—.
    Quien lo miraba concluía que se había roto; en realidad el trabajo corría
    entero y la pantalla se enteraba recién al recargarla a mano.

    Se recargan las tres props que el envío mueve, y no la pantalla entera: el
    dominio no cambia mientras el lote corre.
  */
  useBatchPolling({
    states: [batch.state],
    only: ['batch', 'progress', 'requests'],
  })


  const tableColumns = useMemo(() => requestColumns(), [])

  const changeRequest = useCallback(
    (requestId: string, action: 'remove' | 'restore' | 'clear') => {
      router.post(
        route('indexing.request', { batch_id: batch.id, request_id: requestId }),
        { action },
        { preserveScroll: true }
      )
    },
    [batch.id]
  )

  const rowActions = useCallback(
    (row: IndexingRequestRow): RowActionsFor => {
      const actions: RowActionsFor['actions'] = {}

      if (row.has_evidence) {
        actions.detail = {
          onSelect: () => setOpenRequest(row.id),
          label: t('indexing.row.evidence'),
        }
      }

      if (batch.is_draft) {
        if (row.state === 'REMOVED') {
          actions.retry = {
            onSelect: () => changeRequest(row.id, 'restore'),
            label: t('indexing.row.restore'),
          }
        } else {
          actions.close = {
            onSelect: () => changeRequest(row.id, 'remove'),
            label: t('indexing.row.remove'),
            confirm: {
              title: t('indexing.row.remove.confirm.title', { url: row.loc }),
              consequence: t('indexing.row.remove.confirm.consequence'),
              confirmLabel: t('indexing.row.remove.confirm.action'),
            },
          }
        }
      }

      // Limpiar el error devuelve la fila a la cola **sin borrar la
      // evidencia**: lo que se limpia es lo que frena el lote, no el rastro.
      if (row.state === 'ERROR' && !batch.is_running) {
        actions.retry = {
          onSelect: () => changeRequest(row.id, 'clear'),
          label: t('indexing.row.clear'),
        }
      }

      return {
        label: t('indexing.row.actions', { url: row.loc }),
        actions,
      }
    },
    [batch.is_draft, batch.is_running, changeRequest]
  )

  const discouragedCount = requests.filter(
    (item) => item.state !== 'REMOVED' && DISCOURAGED.includes(item.discouraged)
  ).length

  const stopReason = STOP_REASONS.includes(batch.stopped_by)
    ? t(`indexing.stopped.${batch.stopped_by}` as TranslationKey)
    : batch.stopped_by
      ? t('indexing.stopped.generic')
      : ''

  const canResume = !batch.is_draft && !batch.is_running && progress.pending > 0

  return (
    <AppLayout
      title={t('indexing.title', { hostname: domain.hostname })}
      description={t('indexing.description')}
      actions={
        <div className="flex flex-wrap gap-2">
          {batch.is_draft ? (
            <>
              <ConfirmDestructive
                title={t('indexing.delete.confirm.title')}
                consequence={t('indexing.delete.confirm.consequence')}
                confirmLabel={t('indexing.delete.confirm.action')}
                irreversible
                busy={discard.processing}
                busyLabel={t('indexing.delete.busy')}
                onConfirm={() =>
                  discard.post(route('indexing.delete', { batch_id: batch.id }), {
                    preserveScroll: true,
                  })
                }
                trigger={<Button variant="outline">{t('indexing.delete')}</Button>}
              />
              <Button
                disabled={!domain.is_operational || progress.pending === 0 || run.processing}
                onClick={() =>
                  run.post(route('indexing.run', { batch_id: batch.id }), { preserveScroll: true })
                }
              >
                <Send aria-hidden />
                {run.processing ? t('indexing.run.busy') : t('indexing.run')}
              </Button>
            </>
          ) : null}

          {canResume ? (
            <Button
              disabled={!domain.is_operational || resume.processing}
              onClick={() =>
                resume.post(route('indexing.resume', { batch_id: batch.id }), {
                  preserveScroll: true,
                })
              }
            >
              <Send aria-hidden />
              {resume.processing ? t('indexing.resume.busy') : t('indexing.resume')}
            </Button>
          ) : null}

          {/* La evidencia se puede bajar en cualquier estado que ya tenga algo
              adentro. Es el entregable, así que no espera a que el lote
              termine: un lote detenido es justamente el que hay que mostrar. */}
          {progress.sent + progress.errors > 0 ? (
            <Button variant="outline" asChild>
              <a href={route('indexing.evidence', { batch_id: batch.id })}>
                <Download aria-hidden />
                {t('indexing.evidence.download')}
              </a>
            </Button>
          ) : null}
        </div>
      }
    >
      {errors.indexing ? (
        <ServerErrorNotice code={errors.indexing.code} message="" />
      ) : null}

      {/*
        Sin estado propio y sin tocar la dirección: las dos pestañas muestran
        props que ya viajaron enteras en la carga, así que cambiar de pestaña no
        es una navegación y no hay nada que ir a buscar. `Tabs` guarda cuál está
        abierta y el sondeo —que recarga props sin remontar— no la pierde.
      */}
      <Tabs defaultValue={DEFAULT_TAB}>
        <TabsList>
          <TabsTrigger value="batch">{t('indexing.tab.batch')}</TabsTrigger>
          <TabsTrigger value="addresses">{t('indexing.tab.addresses')}</TabsTrigger>
        </TabsList>

        <TabsContent value="batch" className="flex flex-col gap-4">
          <BatchTab
            batch={batch}
            progress={progress}
            stopReason={stopReason}
            discouragedCount={discouragedCount}
          />
        </TabsContent>

        <TabsContent value="addresses" className="flex flex-col gap-4">
          {/*
            La tabla va en una `Section` y no en una `SectionCard`: ya tiene
            borde propio, y meterla en una caja la deja con dos marcos
            concéntricos que no separan nada.
          */}
          <Section
            title={{ text: t('indexing.urls.title'), isHidden: false }}
            description={t('indexing.urls.description')}
          >
            <ClientDataTable
              data={requests}
              columns={tableColumns}
              rowId={(row) => row.id}
              unit={t('indexing.unit.other')}
              unitSingular={t('indexing.unit.one')}
              caption={t('indexing.urls.caption', { hostname: domain.hostname })}
              sort={{ column: 'url' }}
              pageSize={50}
              search={{
                label: t('indexing.searchLabel'),
                placeholder: t('indexing.searchPlaceholder'),
              }}
              rowActions={rowActions}
              empty={{
                icon: Send,
                title: t('indexing.empty.title'),
                description: t('indexing.empty.description'),
              }}
            />
          </Section>
        </TabsContent>
      </Tabs>

      <EvidencePanel request={opened} onClose={() => setOpenRequest(null)} />
    </AppLayout>
  )
}

/**
 * Dónde está el lote, de dónde salió y qué se le puede prometer a quien mira.
 *
 * La advertencia vive **acá adentro** y no arriba de las pestañas. Es tentador
 * dejarla fija sobre las dos, pero entonces la pantalla arranca con dos bloques
 * de prosa antes de la primera cifra, y una advertencia que se ve siempre deja
 * de verse. Va en la pestaña que se abre primero, junto al estado que califica.
 */
function BatchTab({
  batch,
  progress,
  stopReason,
  discouragedCount,
}: {
  batch: Props['batch']
  progress: Props['progress']
  stopReason: string
  discouragedCount: number
}) {
  return (
    <>
      {/*
        Es la afirmación que sostiene todo lo demás. Sin ella, una lista de
        veinte direcciones con el badge «Enviada» se lee como veinte páginas
        indexadas, que es exactamente lo contrario de lo que esta pantalla puede
        probar.
      */}
      <Alert>
        <ShieldAlert />
        <AlertTitle>{t('indexing.disclaimer.title')}</AlertTitle>
        <AlertDescription>{t('indexing.disclaimer.body')}</AlertDescription>
      </Alert>

      <SectionCard
        title={{ text: t('indexing.batch.heading') }}
        description={t('indexing.batch.description')}
      >
        {/* De dónde salió el lote y cuándo: importa una vez, al llegar, y
            después es ruido. Iba como dos pares más de la misma lista que las
            cuatro cifras, con el mismo peso que ellas, y son otra clase de
            dato. */}
        <p className="text-muted-foreground -mt-2 text-xs italic">
          {t('indexing.batch.provenance', {
            origin: batchOriginLabel(batch.origin),
          })}{' '}
          <DataTimestamp value={batch.created_at} />
        </p>

        <BatchStateHeadline state={batch.state} reason={stopReason || null} />

        <Counters
          items={[
            { term: t('indexing.progress.pending'), value: progress.pending },
            { term: t('indexing.progress.sent'), value: progress.sent },
            { term: t('indexing.progress.errors'), value: progress.errors },
            { term: t('indexing.progress.removed'), value: progress.removed },
          ]}
        />
      </SectionCard>

      {discouragedCount > 0 ? (
        <Alert>
          <ShieldAlert />
          <AlertTitle>
            {t('indexing.discouraged.title', {
              count: discouragedCount,
              total: formatNumber(discouragedCount),
            })}
          </AlertTitle>
          <AlertDescription>{t('indexing.discouraged.body')}</AlertDescription>
        </Alert>
      ) : null}
    </>
  )
}

/**
 * Las cifras del lote, una al lado de la otra.
 *
 * Eran seis pares de una lista de descripción a dos columnas, con las cifras
 * alineadas contra el margen derecho: para aparear cada número con su rótulo
 * había que cruzar un hueco con la vista, y las cuatro que importan competían
 * en peso con dos datos de procedencia que se leen una sola vez.
 *
 * Acá la cifra manda —grande, arriba— y el rótulo la nombra abajo. Sigue siendo
 * una `<dl>`: un lector de pantalla anuncia los cuatro pares y salta de término
 * en término, que es lo que se perdería con una fila de `<div>`.
 */
function Counters({ items }: { items: { term: string; value: number }[] }) {
  return (
    <dl className="flex flex-wrap gap-x-10 gap-y-4">
      {items.map((item) => (
        <div key={item.term} className="min-w-0">
          <dd className="text-2xl font-semibold tabular-nums">{formatNumber(item.value)}</dd>
          <dt className="text-muted-foreground text-xs">{item.term}</dt>
        </div>
      ))}
    </dl>
  )
}

function EvidencePanel({
  request,
  onClose,
}: {
  request: IndexingRequestRow | null
  onClose: () => void
}) {
  return (
    <Sheet open={request !== null} onOpenChange={(open) => (open ? null : onClose())}>
      <SheetContent className="w-full gap-0 overflow-y-auto sm:max-w-xl">
        <SheetHeader>
          <SheetTitle>{t('indexing.evidence.title')}</SheetTitle>
          <SheetDescription>{t('indexing.evidence.description')}</SheetDescription>
        </SheetHeader>

        {request === null ? null : (
          <div className="flex flex-col gap-6 px-4 pb-6">
            <p translate="no" className="wrap-anywhere text-sm font-medium">
              {request.loc}
            </p>

            <DescriptionList
              items={[
                {
                  term: t('indexing.evidence.status'),
                  value: request.response_status === null ? '—' : String(request.response_status),
                },
                {
                  term: t('indexing.evidence.sentAt'),
                  value: request.sent_at ? <DataTimestamp value={request.sent_at} /> : '—',
                },
              ]}
            />

            <EvidenceBlock title={t('indexing.evidence.request')} body={request.request_body} />
            <EvidenceBlock title={t('indexing.evidence.response')} body={request.response_body} />
          </div>
        )}
      </SheetContent>
    </Sheet>
  )
}

function EvidenceBlock({ title, body }: { title: string; body: Record<string, unknown> | null }) {
  if (!body) {
    return (
      <EmptyState
        icon={Send}
        title={title}
        description={t('indexing.evidence.none')}
      />
    )
  }

  return (
    <section className="flex flex-col gap-2">
      <h3 className="text-sm font-medium">{title}</h3>
      {/* Desborde propio y horizontal: un cuerpo JSON con una línea larga no
          puede hacer que la página entera se desplace de costado. */}
      <pre
        translate="no"
        className="bg-muted overflow-x-auto rounded-md p-3 text-xs leading-relaxed"
      >
        {JSON.stringify(body, null, 2)}
      </pre>
    </section>
  )
}
