/**
 * Las sesiones abiertas de la cuenta, como pestaña del perfil.
 *
 * Era una pantalla propia. Se mudó acá adentro porque la pregunta que la trae
 * —«¿desde dónde estoy adentro?»— es de la misma familia que «¿con qué correo
 * entro?»: las dos son de quien entra y no de una herramienta.
 *
 * Y pasó a la tabla del navegador. Es la excepción declarada a RT-09: las
 * sesiones abiertas de una cuenta están acotadas por naturaleza —una por
 * dispositivo— así que llegan enteras, y a cambio se buscan y se ordenan sin ir
 * al servidor. La pregunta que trae hasta acá es «¿hay algo que no reconozco?»,
 * y eso se contesta escribiendo media dirección IP en un campo, no paginando.
 */

import { Link, router, usePage } from '@inertiajs/react'
import { MonitorCheck, MonitorSmartphone } from 'lucide-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'

import { addressParam, addressWith } from '@/components/address'
import { ClientDataTable, createClientDataTableColumns } from '@/components/ClientDataTable'
import type { ClientDataTableColumn, RowActionsFor } from '@/components/ClientDataTable'
import { ConfirmDestructive } from '@/components/ConfirmDestructive'
import { CopyButton } from '@/components/CopyButton'
import { DangerZone } from '@/components/DangerZone'
import { DataTimestamp } from '@/components/DataTimestamp'
import { DetailSheet } from '@/components/DetailSheet'
import { DescriptionList } from '@/components/LabelValue'
import type { RowActionConfirm } from '@/components/RowActions'
import { Section } from '@/components/Section'
import { StatusBadge } from '@/components/StatusBadge'
import { Button } from '@/components/ui/button'
import { Item, ItemActions, ItemContent, ItemDescription, ItemTitle } from '@/components/ui/item'
import { formatAbsolute } from '@/lib/format'
import { t } from '@/lib/i18n'
import { route } from '@/lib/routes'

export interface SessionInfo {
  id: string
  is_current: boolean
  ip_address: string | null
  user_agent: string | null
  browser: string | null
  operating_system: string | null
  last_activity_at: string
  created_at: string
}

interface Props {
  /** Todas las abiertas de la cuenta, no una página. */
  sessions: SessionInfo[]
  /**
   * Cuántas hay además de la actual.
   *
   * No se cuenta acá aunque las filas estén todas: el buscador de la tabla
   * cambia lo que se ve, y la confirmación del cierre masivo lleva la cantidad
   * en el título —esa cantidad es la de la cuenta, no la de lo que quedó a la
   * vista después de escribir tres letras—.
   */
  others_count: number
  /** Cuántas cerró la acción anterior. Nulo cuando se llegó sin cerrar nada. */
  closed_count: number | null
}

/** Qué dice la dirección cuando hay un detalle abierto. */
const DETAIL_PARAM = 'session'

/**
 * Cómo se nombra un dispositivo, sin inventarle un nombre.
 *
 * El servidor devuelve nulo cuando la cadena del agente no coincide con nada
 * que sepa reconocer, y acá tampoco se completa. Un nombre plausible pero
 * equivocado es peor que ninguno: es exactamente el dato que se mira para
 * decidir si la sesión es propia antes de cerrarla, y equivocarse cuesta o
 * dejar entrar a un intruso o echarse a uno mismo.
 *
 * Las cuatro formas son cuatro claves y no una oración armada por pedazos: en
 * inglés el orden es «Firefox on Windows» y no todos los idiomas lo ponen igual.
 */
function describe(session: SessionInfo): string {
  if (session.browser && session.operating_system) {
    return t('sessions.device.known', {
      browser: session.browser,
      system: session.operating_system,
    })
  }
  if (session.browser) return t('sessions.device.browserOnly', { browser: session.browser })
  if (session.operating_system) {
    return t('sessions.device.systemOnly', { system: session.operating_system })
  }
  return t('sessions.device.unknown')
}

/** Cuántas se cerraron, dicho en la pantalla de destino (RT-11). */
function closedResult(closed: number): string {
  if (closed === 0) return t('sessions.closed.none')
  return t('sessions.closed', { count: closed })
}

/**
 * Lo que dice la confirmación de cerrar **una** sesión.
 *
 * Sale acá afuera porque el cierre se pide desde dos lugares —el menú de la fila
 * y el pie del panel de detalle— y son la misma acción: dos textos distintos
 * para lo mismo es cómo una pantalla empieza a contradecirse.
 */
function closeConfirm(session: SessionInfo, timeZone: string): RowActionConfirm {
  const date = formatAbsolute(session.created_at, timeZone)

  return {
    title: t('sessions.close.title', { device: describe(session) }),
    consequence: session.ip_address
      ? t('sessions.close.consequenceWithAddress', { date, address: session.ip_address })
      : t('sessions.close.consequence', { date }),
    confirmLabel: t('sessions.close.action'),
    busyLabel: t('sessions.close.busy'),
  }
}

const columnHelper = createClientDataTableColumns<SessionInfo>()

/**
 * Una sesión por fila, con los cuatro datos que permiten decidir si es propia
 * (FR-059).
 *
 * La fila actual queda marcada, **sin casilla y con la celda de acciones
 * vacía**. Es la señal más fuerte de todas: la única que no se puede cerrar es
 * la única sin controles. Un «Cerrar» apagado en su lugar sería ofrecer algo que
 * no existe (RT-07), y una frase de cinco palabras en una columna alineada a la
 * derecha compite con veinte controles —esa explicación vive en el panel de
 * detalle, donde hay lugar para explicar—.
 *
 * Cada columna declara su `label`: es el nombre con el que el menú de columnas
 * la ofrece, y sin él quedaría fuera del menú, porque lo único que restaría para
 * nombrarla es su identificador en inglés.
 *
 * `enableGlobalFilter` dice qué mira el buscador. Miran el dispositivo y la
 * dirección, que son los dos datos con los que se reconoce una sesión; las dos
 * fechas no, porque adentro son marcas ISO y buscar «2026» traería todo.
 */
function sessionColumns(
  detailHref: (session: SessionInfo) => string
): ClientDataTableColumn<SessionInfo>[] {
  return [
    columnHelper.accessor((row) => describe(row), {
      id: 'device',
      header: t('sessions.column.device'),
      meta: { label: t('sessions.column.device') },
      cell: (info) => {
        const session = info.row.original

        return (
          <div className="flex flex-col items-start gap-1">
            <Link
              href={detailHref(session)}
              preserveScroll
              preserveState
              className="font-medium underline-offset-4 hover:underline"
            >
              {info.getValue()}
            </Link>

            {/*
              La marca es icono y palabra dentro de la fila, no un color de
              fondo: un lector de pantalla no anuncia el color, y es el único
              dato de la pantalla que no puede pasar desapercibido.
            */}
            {session.is_current ? (
              <StatusBadge tone="neutral" icon={MonitorCheck}>
                {t('sessions.thisSession')}
              </StatusBadge>
            ) : null}
          </div>
        )
      },
    }),
    /*
      «Dirección de origen» y nunca «Ubicación»: se muestra la dirección tal
      como llegó. Traducirla a una ciudad sería presentar una inferencia como
      dato cierto.
    */
    columnHelper.accessor((row) => row.ip_address ?? '', {
      id: 'address',
      header: t('sessions.column.address'),
      meta: { label: t('sessions.column.address') },
      cell: (info) =>
        info.row.original.ip_address ? (
          <code translate="no" className="font-mono text-xs wrap-anywhere">
            {info.row.original.ip_address}
          </code>
        ) : (
          <span className="text-muted-foreground">{t('sessions.noAddress')}</span>
        ),
    }),
    columnHelper.accessor('last_activity_at', {
      id: 'activity',
      header: t('sessions.column.activity'),
      meta: { label: t('sessions.column.activity') },
      enableGlobalFilter: false,
      cell: (info) => <DataTimestamp value={info.getValue()} />,
    }),
    columnHelper.accessor('created_at', {
      id: 'created',
      header: t('sessions.column.created'),
      meta: { label: t('sessions.column.created') },
      enableGlobalFilter: false,
      cell: (info) => <DataTimestamp value={info.getValue()} />,
    }),
  ]
}

/**
 * Qué ofrece hacer cada fila.
 *
 * La sesión en curso devuelve `null`: es la única que no se puede cerrar, así
 * que es la única sin menú y sin casilla. Es la señal más fuerte de todas, y más
 * clara que un «Cerrar» apagado, que sería ofrecer algo que no existe (RT-07).
 * La explicación de por qué vive en el panel de detalle, donde hay lugar para
 * explicar.
 *
 * Los rótulos: «ver los detalles» usa el del registro, porque es el verbo a
 * secas y tiene que decir lo mismo que en las otras tablas; «cerrar» lo pisa,
 * porque acá no se cierra cualquier cosa sino una sesión.
 */
function sessionActions(
  session: SessionInfo,
  detailHref: (session: SessionInfo) => string,
  timeZone: string,
  closingId: string | null,
  close: (session: SessionInfo) => void
): RowActionsFor | null {
  if (session.is_current) return null

  return {
    // Nombre único por fila: veintiún disparadores llamados «Acciones» obligan
    // a recorrer la tabla celda por celda para saber cuál es cuál.
    label: t('sessions.row.actions', {
      device: describe(session),
      date: formatAbsolute(session.created_at, timeZone),
    }),
    actions: {
      detail: { href: detailHref(session) },
      close: {
        label: t('sessions.close.action'),
        onSelect: () => close(session),
        confirm: { ...closeConfirm(session, timeZone), busy: closingId === session.id },
      },
    },
  }
}

export function SessionsSection({ sessions, others_count, closed_count }: Props) {
  const { props, url } = usePage()
  const timeZone = props.app.display_timezone

  /*
    La confirmación del cierre nunca se monta adentro de la fila: al confirmar,
    la fila desaparece de la lista que devuelve el servidor, y un diálogo montado
    adentro se desmontaría con ella mientras todavía está abierto, dejando el
    foco en ningún lado. En el menú de la fila eso lo resuelve `RowActions`, que
    lo hospeda afuera; el del panel de detalle vive acá, al nivel de la página,
    por el mismo motivo.
  */
  const [toClose, setToClose] = useState<SessionInfo | null>(null)
  const [closingId, setClosingId] = useState<string | null>(null)
  const [closingOthers, setClosingOthers] = useState(false)
  const [closingSelected, setClosingSelected] = useState(false)

  const announcement = useRef<HTMLParagraphElement>(null)

  // `closed_count` sólo llega en la respuesta a un cierre, así que esto corre
  // una vez por acción: sin mover el foco, quien navega con teclado vuelve al
  // principio del documento y tiene que buscar si pasó algo (RT-17).
  useEffect(() => {
    if (closed_count !== null) announcement.current?.focus()
  }, [closed_count])

  // Estable a propósito: viaja adentro de las columnas, que se rearman sólo
  // cuando cambian los datos. Una identidad nueva en cada render invalidaría los
  // modelos de la tabla en cada tecla del buscador.
  const closeOne = useCallback((session: SessionInfo) => {
    router.post(
      route('session.close', { session_id: session.id }),
      {},
      {
        preserveScroll: true,
        onStart: () => setClosingId(session.id),
        onFinish: () => setClosingId(null),
      }
    )
  }, [])

  const closeOthers = () => {
    router.post(
      route('sessions.close_others'),
      {},
      {
        preserveScroll: true,
        onStart: () => setClosingOthers(true),
        onFinish: () => setClosingOthers(false),
      }
    )
  }

  const closeSelected = (ids: string[], clear: () => void) => {
    router.post(
      route('sessions.close_selected'),
      { session_ids: ids },
      {
        preserveScroll: true,
        onStart: () => setClosingSelected(true),
        onFinish: () => setClosingSelected(false),
        // Se vacía al saber que el servidor cerró, y no al empezar: la selección
        // es estado de ids que sobrevive a que las filas desaparezcan, y
        // limpiarla antes dejaría la pantalla afirmando un cierre que pudo no
        // haber ocurrido.
        onSuccess: clear,
      }
    )
  }

  const detailHref = useCallback(
    (session: SessionInfo) => addressWith(url, DETAIL_PARAM, session.id),
    [url]
  )

  // Sólo dependen de la dirección: lo que cambia con cada cierre —`closingId`—
  // vive en el menú de la fila, que no es una columna y por eso ya no obliga a
  // rearmar los modelos de la tabla en cada clic.
  const columns = useMemo(() => sessionColumns(detailHref), [detailHref])

  /*
    Lo que hay abierto lo dice la dirección, y lo que se muestra sale de las
    filas que ya están en la pantalla. Si la sesión no está entre ellas —porque
    se cerró, o porque el enlace quedó viejo— el panel no se abre: mostrar un
    detalle que la lista no tiene sería afirmar que esa sesión sigue viva.
  */
  const openId = addressParam(url, DETAIL_PARAM)
  const openSession = openId ? (sessions.find((row) => row.id === openId) ?? null) : null

  const onlyCurrent = sessions.length === 1 && others_count === 0

  return (
    <>
      {/*
        Que haya una sola sesión se dice con palabras y no se deduce de que la
        tabla tenga un renglón: la pregunta que trae hasta acá es «¿hay alguien
        más adentro?», y una lista corta no la contesta sola.
      */}
      {/*
        El título no se dibuja porque la pestaña ya dice «Sesiones» a dos
        renglones de acá, y repetirlo es un encabezado que no agrega ninguna
        información. Existe igual como nombre de la región: escondido no es lo
        mismo que ausente.
      */}
      <Section
        title={{ text: t('sessions.title'), isHidden: true }}
        description={onlyCurrent ? t('sessions.onlyCurrent') : t('sessions.description')}
      >
        {/*
          El resultado es la respuesta a lo último que se hizo y es enfocable.
          Llega por navegación completa de Inertia y no por actualización
          parcial, así que la región viva no necesita preexistir: se rinde ya con
          su texto y sin reservar alto.
        */}
        {closed_count !== null ? (
          <p
            ref={announcement}
            role="status"
            aria-live="polite"
            tabIndex={-1}
            className="focus-visible:ring-ring rounded text-sm focus-visible:ring-2 focus-visible:outline-none"
          >
            {closedResult(closed_count)}
          </p>
        ) : null}

        <ClientDataTable
          data={sessions}
          columns={columns}
          rowId={(row) => row.id}
          unit={t('sessions.unit')}
          unitSingular={t('sessions.unitSingular')}
          caption={t('sessions.caption')}
          sort={{ column: 'activity', descending: true }}
          rowActions={(session) =>
            sessionActions(session, detailHref, timeZone, closingId, closeOne)
          }
          search={{
            label: t('sessions.search.label'),
            placeholder: t('sessions.search.placeholder'),
          }}
          empty={{
            icon: MonitorSmartphone,
            title: t('sessions.empty.title'),
            description: t('sessions.empty.description'),
          }}
          selection={{
            // La sesión en curso es la única que no se puede cerrar, así que es
            // la única sin casilla. Es la misma señal que ya da la columna de
            // acciones, dicha dos veces en la misma fila y no dos veces
            // distinto.
            canSelect: (session) => !session.is_current,
            allLabel: t('sessions.select.all'),
            rowLabel: (session) =>
              t('sessions.select.row', {
                device: describe(session),
                date: formatAbsolute(session.created_at, timeZone),
              }),
            actions: ({ ids, clear }) => (
              <ConfirmDestructive
                title={t('sessions.closeSelected.title', { count: ids.length })}
                consequence={t('sessions.closeSelected.consequence')}
                confirmLabel={t('sessions.closeSelected.action', { count: ids.length })}
                busyLabel={t('sessions.close.busy')}
                busy={closingSelected}
                onConfirm={() => closeSelected(ids, clear)}
                trigger={
                  <Button variant="destructive" size="sm">
                    {t('sessions.closeSelected.action', { count: ids.length })}
                  </Button>
                }
              />
            ),
          }}
        />
      </Section>

      <DetailSheet
        paramName={DETAIL_PARAM}
        openKey={openSession ? openSession.id : null}
        title={openSession ? describe(openSession) : ''}
        // `lg` por la cadena del agente declarado, que es el motivo por el que
        // este panel existe: es larga, monoespaciada y no se puede angostar sin
        // partirla en una escalera de renglones de tres palabras.
        size="lg"
        action={
          openSession && !openSession.is_current ? (
            <Button variant="destructive" onClick={() => setToClose(openSession)}>
              {t('sessions.close.action')}
            </Button>
          ) : undefined
        }
      >
        {openSession ? <SessionDetail session={openSession} timeZone={timeZone} /> : null}
      </DetailSheet>

      {/*
        Al pie y nunca en el slot del encabezado, que es una línea de controles
        inocuos a un píxel del disparador de la barra lateral. Cuando no hay
        otras sesiones la zona **no se rinde**: nada apagado, nada con
        «Próximamente» (RT-07).

        Convive con «cerrar las seleccionadas» y no la reemplaza: ésta habla de
        la cuenta entera —sin importar qué dejó a la vista el buscador— y la otra
        de lo que se marcó a mano.
      */}
      {others_count > 0 ? (
        <DangerZone title={t('sessions.closeOthers.zone')}>
          <Item variant="muted">
            <ItemContent>
              <ItemTitle>{t('sessions.closeOthers.action', { count: others_count })}</ItemTitle>
              <ItemDescription className="line-clamp-none text-pretty">
                {t('sessions.closeOthers.description')}
              </ItemDescription>
            </ItemContent>
            <ItemActions>
              <ConfirmDestructive
                title={t('sessions.closeOthers.title', { count: others_count })}
                consequence={t('sessions.closeOthers.description')}
                confirmLabel={t('sessions.closeOthers.confirm', { count: others_count })}
                busyLabel={t('sessions.close.busy')}
                busy={closingOthers}
                onConfirm={closeOthers}
                trigger={
                  <Button variant="destructive">
                    {t('sessions.closeOthers.action', { count: others_count })}
                  </Button>
                }
              />
            </ItemActions>
          </Item>
        </DangerZone>
      ) : null}

      {toClose ? (
        <ConfirmDestructive
          open
          onOpenChange={(isOpen) => {
            if (!isOpen) setToClose(null)
          }}
          {...closeConfirm(toClose, timeZone)}
          busy={closingId === toClose.id}
          onConfirm={() => closeOne(toClose)}
        />
      ) : null}
    </>
  )
}

/**
 * Lo que no entra en una fila: la cadena declarada entera y las fechas en
 * absoluto.
 *
 * Los veinte `<details>` nativos que había adentro de la tabla llevaban cada
 * fila a 57 px y obligaban a abrir veinte cosas para comparar dos. Acá hay lugar
 * para explicar, y es también donde vive la frase que antes competía con
 * veintiún controles en la columna de acciones.
 */
function SessionDetail({ session, timeZone }: { session: SessionInfo; timeZone: string }) {
  return (
    <div className="flex min-w-0 flex-col gap-4">
      {session.is_current ? (
        <p className="text-sm text-pretty">{t('sessions.currentHint')}</p>
      ) : null}

      <DescriptionList
        columns={2}
        items={[
          {
            term: t('sessions.detail.browser'),
            value: session.browser ?? t('common.notIdentified'),
          },
          {
            term: t('sessions.detail.system'),
            value: session.operating_system ?? t('common.notIdentified'),
          },
          {
            term: t('sessions.detail.address'),
            value: session.ip_address ?? t('sessions.noAddress'),
          },
          {
            term: t('sessions.detail.activity'),
            value: formatAbsolute(session.last_activity_at, timeZone),
          },
          { term: t('sessions.detail.created'), value: formatAbsolute(session.created_at, timeZone) },
        ]}
      />

      {/*
        Cuando no pudimos nombrar el navegador queda la cadena declarada, sin
        interpretar. Es fea y puede ser larguísima, pero es lo único que queda
        para reconocer el dispositivo, y esconderla dejaría un detalle que no
        dice nada.
      */}
      {session.user_agent ? (
        <div className="flex min-w-0 flex-col gap-2">
          <h3 className="text-sm font-medium">{t('sessions.detail.userAgent')}</h3>
          <code
            translate="no"
            className="bg-muted/60 block min-w-0 rounded p-2 font-mono text-xs wrap-anywhere select-all"
          >
            {session.user_agent}
          </code>
          <div>
            <CopyButton value={session.user_agent} label={t('sessions.detail.copyAgent')} />
          </div>
        </div>
      ) : null}
    </div>
  )
}
