import { router } from '@inertiajs/react'
import { CircleAlert } from 'lucide-react'
import type { ReactNode } from 'react'

import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { hasTranslation, t } from '@/lib/i18n'
import type { TranslationKey } from '@/lib/i18n'

/**
 * Códigos que emite el servidor y que sabemos explicar (RT-08).
 *
 * Están los de `apps/core/errors.py`, que puede devolver cualquier vista. Cada
 * vista suma los suyos por `codes` en vez de reescribir estos: si cada pantalla
 * tradujera `PROVIDER_UNAVAILABLE` a su manera, el mismo fallo se explicaría
 * distinto en cada lugar y la persona no podría reconocerlo dos veces.
 *
 * Lo que se dice de cada uno vive en el catálogo, bajo `serverError.<CODE>.title`
 * y `.explanation`. Ningún texto de ahí culpa a la credencial ni afirma una
 * causa que no sabemos: un código traducido de más es peor que uno sin traducir,
 * porque manda a revisar lo que no está roto.
 */
const ERROR_CODES: readonly string[] = [
  'UNAUTHENTICATED',
  'PERMISSION_DENIED',
  'NOT_FOUND',
  'VALIDATION_FAILED',
  'CONFLICT',
  'QUOTA_EXHAUSTED',
  'RATE_LIMITED',
  'CREDENTIAL_NOT_READY',
  'DOMAIN_NOT_OPERATIONAL',
  'PROVIDER_UNAVAILABLE',
  'INTERNAL_ERROR',
]

interface Props {
  /** Código estable del contrato: `{error: {code, message, details}}`. */
  code: string
  /** Mensaje del servidor. Es lo único que se muestra ante un código sin mapear. */
  message: string
  /** Lo que el cliente necesita para reaccionar. Va al detalle técnico, plegado. */
  details?: Record<string, unknown> | null
  /**
   * El control que resuelve o sale del error. Sin él queda la acción neutra,
   * porque un aviso que sólo describe el problema deja a la persona buscando
   * dónde seguir (RT-08).
   */
  action?: ReactNode
  /**
   * Códigos propios de la vista, que se suman a los de arriba.
   *
   * Lo que viaja es el **código** y no su texto: el texto de cualquier código,
   * venga de acá o de una vista, sale del catálogo. Así una pantalla no puede
   * escribir su propia versión de algo que ya está dicho.
   */
  codes?: readonly string[]
  /**
   * Prefijo del catálogo para los textos de esta pantalla.
   *
   * Existe porque a veces la misma falla se explica mejor sabiendo dónde pasó:
   * un `PERMISSION_DENIED` comprobando el acceso a una propiedad se resuelve en
   * Search Console y conviene decirlo así, mientras que el genérico no puede
   * suponerlo. Con ámbito se busca primero `serverError.<scope>.<CODE>` y, si no
   * está, se usa el texto común: es una precisión opcional, no una copia.
   */
  scope?: string
  className?: string
}

/**
 * Error estructurado del servidor, traducido a algo accionable (RT-08).
 *
 * El `message` del servidor nunca se descarta: cuando el código está mapeado
 * queda como detalle secundario, porque suele traer el dato concreto —qué
 * campo, cuánto cupo, qué propiedad— que la explicación genérica no puede
 * tener. Viaja en inglés, que es el idioma del contrato de la API.
 */
export function ServerErrorNotice({
  code,
  message,
  details,
  action,
  codes,
  scope,
  className,
}: Props) {
  const known = ERROR_CODES.includes(code) || (codes?.includes(code) ?? false)
  const fromServer = message?.trim() ?? ''

  // El ámbito de la pantalla primero, el texto común después. El catálogo manda
  // sobre la lista de códigos: uno declarado sin texto propio se trata como no
  // mapeado, en vez de dibujar una clave cruda.
  const prefix =
    scope && hasTranslation(`serverError.${scope}.${code}.title`)
      ? `serverError.${scope}.${code}`
      : `serverError.${code}`
  const explained = known && hasTranslation(`${prefix}.title`)

  const title = explained ? t(`${prefix}.title` as TranslationKey) : t('serverError.unmapped')
  const body = explained ? t(`${prefix}.explanation` as TranslationKey) : fromServer
  const serverDetail = explained && fromServer && fromServer !== body ? fromServer : null

  // Sólo los valores planos: un objeto anidado impreso con `String()` sale como
  // «[object Object]», que no le sirve a nadie.
  const technical = Object.entries(details ?? {}).filter(
    ([, value]) => value !== null && value !== undefined && typeof value !== 'object'
  )

  return (
    <Alert variant="critical" className={className}>
      <CircleAlert />
      <AlertTitle>{title}</AlertTitle>
      <AlertDescription className="flex flex-col items-start gap-3">
        <span>{body || t('serverError.noDetail')}</span>

        {serverDetail ? <span className="text-xs">{serverDetail}</span> : null}

        {/*
          La acción neutra no es un adorno: recargar es lo único que se puede
          ofrecer sin saber qué falló, y es siempre correcto. La vista la pisa
          con la suya cuando conoce el código.
        */}
        {action ?? (
          <Button size="sm" variant="outline" onClick={() => router.reload()}>
            {t('serverError.reload')}
          </Button>
        )}

        {/*
          El código y sus datos van plegados y no ocultos: para quien integra la
          API por su cuenta son la mitad del diagnóstico, y para el resto son
          ruido que no tiene por qué leer.
        */}
        <details className="text-xs">
          <summary className="cursor-pointer underline underline-offset-3">
            {t('serverError.technical')}
          </summary>
          <dl className="mt-1.5 grid gap-1 font-mono">
            <div className="flex flex-wrap gap-x-2">
              <dt>{t('serverError.code')}</dt>
              <dd>{code}</dd>
            </div>
            {technical.map(([key, value]) => (
              <div key={key} className="flex flex-wrap gap-x-2">
                <dt>{key}</dt>
                <dd className="break-all">{String(value)}</dd>
              </div>
            ))}
          </dl>
        </details>
      </AlertDescription>
    </Alert>
  )
}
