import { router, useForm, usePage } from '@inertiajs/react'
import { Monitor, Moon, Sun } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import type { FormEvent } from 'react'
import { useEffect, useRef } from 'react'

import { addressParam, addressWith } from '@/components/address'
import { FieldError, fieldErrorProps } from '@/components/FieldError'
import { SectionCard } from '@/components/SectionCard'
import { SessionsSection } from '@/components/SessionsSection'
import type { SessionInfo } from '@/components/SessionsSection'
import { TaskColumn } from '@/components/TaskColumn'
import { TimezoneFootnote } from '@/components/DataTimestamp'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { AppLayout } from '@/layouts/AppLayout'
import { faultText } from '@/lib/errors'
import { t } from '@/lib/i18n'
import type { Locale } from '@/lib/i18n'
import { route } from '@/lib/routes'
import { applyTheme } from '@/lib/theme'
import type { ThemePreference } from '@/lib/theme'
import { cn } from '@/lib/utils'

interface Props {
  email: string
  /** Todas las abiertas de la cuenta: la tabla de sesiones busca y pagina en el navegador. */
  sessions: SessionInfo[]
  others_count: number
  closed_count: number | null
  /** Qué guardó la acción anterior, para anunciarlo. Nulo cuando se llegó sin guardar nada. */
  saved: 'preferences' | 'email' | 'password' | null
}

/** Qué dice la dirección cuando hay una pestaña elegida. */
const TAB_PARAM = 'tab'

/** Las dos pestañas. La primera es la de omisión, así que no necesita decirse. */
type Tab = 'general' | 'sessions'

const SESSIONS_TAB: Tab = 'sessions'

/** El id del rótulo del tema, que no es un `<label>` porque no hay campo al cual atarlo. */
const THEME_LABEL_ID = 'theme-label'

/** Los idiomas, en el orden en el que se ofrecen. El fuente primero. */
const LANGUAGES: { value: Locale; label: 'profile.preferences.language.en' | 'profile.preferences.language.es-ar' }[] = [
  { value: 'en', label: 'profile.preferences.language.en' },
  { value: 'es-ar', label: 'profile.preferences.language.es-ar' },
]

/**
 * Los tres temas, cada uno con su dibujo. «Seguir al sistema» va último porque
 * es el de fábrica.
 *
 * Guarda claves y no oraciones: este mapa se arma cuando se importa el módulo, y
 * para entonces el catálogo todavía no está instalado.
 */
const THEMES: {
  value: ThemePreference
  icon: LucideIcon
  label:
    | 'profile.preferences.theme.light'
    | 'profile.preferences.theme.dark'
    | 'profile.preferences.theme.system'
}[] = [
  { value: 'light', icon: Sun, label: 'profile.preferences.theme.light' },
  { value: 'dark', icon: Moon, label: 'profile.preferences.theme.dark' },
  { value: 'system', icon: Monitor, label: 'profile.preferences.theme.system' },
]

/**
 * Todo lo que es de la cuenta, en dos pestañas.
 *
 * **La pestaña vive en la dirección y no en un estado de React.** Las cinco
 * acciones de esta pantalla —guardar preferencias, correo y contraseña, cerrar
 * una sesión y cerrar las demás— redirigen todas al perfil, así que con la
 * pestaña en memoria cerrar una sesión devolvería a la primera y el resultado se
 * anunciaría en la pestaña que no se está mirando. De paso queda enlazable y el
 * botón Atrás del navegador vuelve a la pestaña anterior, que es lo que espera
 * quien lo aprieta.
 */
export default function Profile({ email, sessions, others_count, closed_count, saved }: Props) {
  const { props, url } = usePage()
  const preferences = props.preferences
  const tab: Tab = addressParam(url, TAB_PARAM) === SESSIONS_TAB ? SESSIONS_TAB : 'general'

  return (
    <AppLayout
      title={t('profile.title')}
      titleAccessory={<RoleBadge role={props.auth?.role ?? 'VIEWER'} />}
      description={t('profile.description')}
    >
      <Tabs
        value={tab}
        onValueChange={(next) => {
          router.get(
            addressWith(url, TAB_PARAM, next),
            {},
            { preserveScroll: true, preserveState: true }
          )
        }}
      >
        <TabsList>
          <TabsTrigger value="general">{t('profile.tab.general')}</TabsTrigger>
          <TabsTrigger value={SESSIONS_TAB}>{t('profile.tab.sessions')}</TabsTrigger>
        </TabsList>

        {/*
          Columna angosta: las tres son pantallas de tarea —se completa un
          formulario— y una tarjeta de mil trescientos píxeles con un campo de
          correo adentro deja el control perdido en el medio de la nada.
        */}
        <TabsContent value="general">
          <TaskColumn className="max-w-3xl">
            <PreferencesForm
              language={preferences.language}
              theme={preferences.theme}
              saved={saved === 'preferences'}
            />

            <EmailForm email={email} saved={saved === 'email'} />

            <PasswordForm saved={saved === 'password'} />
          </TaskColumn>
        </TabsContent>

        {/* Las sesiones son datos y usan el ancho completo. */}
        <TabsContent value={SESSIONS_TAB}>
          <div className="flex flex-col gap-6">
            <SessionsSection
              sessions={sessions}
              others_count={others_count}
              closed_count={closed_count}
            />

            <TimezoneFootnote />
          </div>
        </TabsContent>
      </Tabs>
    </AppLayout>
  )
}

function RoleBadge({ role }: { role: 'SUPER_ADMIN' | 'ADMIN' | 'VIEWER' }) {
  const styles = {
    SUPER_ADMIN: 'border-black bg-black text-white dark:border-white dark:bg-white dark:text-black',
    ADMIN: 'border-blue-600 bg-blue-600 text-white dark:border-blue-500 dark:bg-blue-500',
    VIEWER:
      'border-slate-300 bg-slate-100 text-slate-800 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100',
  }
  const labels = {
    SUPER_ADMIN: t('profile.role.superAdmin'),
    ADMIN: t('profile.role.admin'),
    VIEWER: t('profile.role.viewer'),
  }

  return (
    <Badge variant="outline" className={styles[role]}>
      {labels[role]}
    </Badge>
  )
}

/**
 * Idioma y tema.
 *
 * **El tema se aplica al elegirlo y el idioma recarga la página.** No es una
 * inconsistencia: son dos cosas distintas. El tema es una clase en el documento
 * y cambia sin que nada más se entere; el idioma cambia cada palabra que ya está
 * dibujada, y el catálogo nuevo viaja aparte. Una navegación de Inertia dibuja
 * la pantalla antes de que llegue el catálogo, así que el idioma pide una carga
 * completa —que además deja el `lang` del documento en su valor nuevo, que es lo
 * que lee un lector de pantalla—.
 */
function PreferencesForm({
  language,
  theme,
  saved,
}: {
  language: Locale
  theme: ThemePreference
  saved: boolean
}) {
  const form = useForm({ language, theme })

  const save = (next: { language: Locale; theme: ThemePreference }) => {
    const changesLanguage = next.language !== language

    // El tema se pinta ya, sin esperar la respuesta. Es una clase en el
    // documento, y el resultado de la elección es justamente lo que se está
    // eligiendo: esperar el viaje al servidor deja la pantalla igual medio
    // segundo después del clic, que alcanza para pensar que el control no anda.
    // La cuenta sigue siendo la única fuente de verdad —cuando llega la
    // respuesta, el armazón repinta con lo que quedó guardado—, así que un
    // guardado que falla se deshace solo.
    applyTheme(next.theme)

    router.post(route('profile.preferences'), next, {
      preserveScroll: true,
      // Cambiar de idioma no navega: recarga. El catálogo se elige en el
      // arranque, y una navegación llegaría con la pantalla ya dibujada.
      onSuccess: () => {
        if (changesLanguage) window.location.assign(route('profile'))
      },
    })
  }

  return (
    <SectionCard
      title={{ text: t('profile.preferences.title') }}
      description={t('profile.preferences.description')}
      footer={saved ? <SavedNotice message={t('profile.preferences.saved')} /> : undefined}
    >
      <div className="grid gap-2">
        <Label htmlFor="language">{t('profile.preferences.language.label')}</Label>
        <Select
          value={form.data.language}
          onValueChange={(value) => {
            const next = { ...form.data, language: value as Locale }
            form.setData(next)
            save(next)
          }}
        >
          <SelectTrigger id="language" className="w-full">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            {LANGUAGES.map((option) => (
              <SelectItem key={option.value} value={option.value}>
                {t(option.label)}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
        <p className="text-muted-foreground text-sm">
          {t('profile.preferences.language.description')}
        </p>
      </div>

      <div className="grid gap-2">
        {/*
          Un `<span>` y no un `Label`: el grupo de conmutadores no tiene ningún
          campo con `id` al cual apuntar, así que un `<label for=…>` quedaría
          huérfano. Radix lo rinde como `radiogroup`, y a un grupo se le pone
          nombre con `aria-labelledby`, nunca con un `<label>`.
        */}
        <span id={THEME_LABEL_ID} className="text-sm leading-none font-medium">
          {t('profile.preferences.theme.label')}
        </span>

        {/*
          Los tres a la vista y pegados entre sí: se comparan entre ellos y el
          resultado se ve en el acto, así que tenerlos los tres delante es parte
          de la elección. Un desplegable escondería dos de las tres.
        */}
        <ToggleGroup
          type="single"
          variant="outline"
          spacing={0}
          value={form.data.theme}
          onValueChange={(value) => {
            // Un grupo de conmutadores deja apagar el que está prendido y
            // devuelve cadena vacía. «Sin tema» no es una de las tres opciones,
            // así que ese valor no se guarda: el que estaba sigue estando.
            if (!value) return

            const next = { ...form.data, theme: value as ThemePreference }
            form.setData(next)
            save(next)
          }}
          aria-labelledby={THEME_LABEL_ID}
        >
          {THEMES.map((option) => (
            <ToggleGroupItem key={option.value} value={option.value}>
              <option.icon aria-hidden />
              {t(option.label)}
            </ToggleGroupItem>
          ))}
        </ToggleGroup>

        <p className="text-muted-foreground text-sm">
          {t('profile.preferences.theme.description')}
        </p>
      </div>
    </SectionCard>
  )
}

/** La dirección con la que se ingresa. */
function EmailForm({ email, saved }: { email: string; saved: boolean }) {
  const { errors } = usePage().props
  const form = useForm({ email })
  const emailField = useRef<HTMLInputElement>(null)

  // El foco vuelve al campo con error al recibir la respuesta: sin esto, quien
  // navega con teclado tiene que recorrer el formulario para saber qué salió
  // mal (RT-17).
  useEffect(() => {
    if (errors.email) emailField.current?.focus()
  }, [errors.email])

  const submit = (event: FormEvent) => {
    event.preventDefault()
    form.post(route('profile.email'), { preserveScroll: true })
  }

  return (
    // El formulario envuelve a la tarjeta y no al revés: el campo vive en el
    // contenido y el botón en el pie, que son dos regiones distintas, y un
    // `<form>` adentro de una de las dos dejaría al botón fuera de su alcance.
    <form onSubmit={submit} noValidate>
      <SectionCard
        title={{ text: t('profile.account.title') }}
        description={t('profile.account.description')}
        footer={
          <div className="flex w-full flex-wrap items-center justify-end gap-3">
            {saved ? (
              <SavedNotice message={t('profile.account.saved')} className="mr-auto" />
            ) : null}

            <Button type="submit" disabled={form.processing} aria-busy={form.processing}>
              {form.processing ? t('common.saving') : t('profile.account.email.save')}
            </Button>
          </div>
        }
      >
        <div className="grid gap-2">
          <Label htmlFor="email">{t('profile.account.email.label')}</Label>
          <Input
            id="email"
            name="email"
            ref={emailField}
            type="email"
            autoComplete="email"
            spellCheck={false}
            autoCapitalize="none"
            value={form.data.email}
            onChange={(event) => form.setData('email', event.target.value)}
            {...fieldErrorProps('error-email', faultText(errors.email))}
          />
          <FieldError id="error-email" message={faultText(errors.email)} />
        </div>
      </SectionCard>
    </form>
  )
}

/**
 * El cambio de contraseña.
 *
 * Pide la actual además de la nueva: sin eso, una sesión ajena que quedó abierta
 * en una máquina prestada alcanza para quedarse con la cuenta.
 */
function PasswordForm({ saved }: { saved: boolean }) {
  const { errors } = usePage().props
  const form = useForm({ current_password: '', new_password: '', confirm_password: '' })
  const currentField = useRef<HTMLInputElement>(null)

  useEffect(() => {
    if (errors.current_password) currentField.current?.focus()
  }, [errors.current_password])

  // Los campos se vacían al guardar: dejar la contraseña escrita en tres
  // controles después de haberla cambiado no sirve para nada y queda a la vista
  // de quien pase por atrás.
  useEffect(() => {
    if (saved) form.reset()
    // La dependencia es sólo `saved`: reaccionar a `form` la volvería a limpiar
    // en cada tecla.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [saved])

  const submit = (event: FormEvent) => {
    event.preventDefault()
    form.post(route('profile.password'), { preserveScroll: true })
  }

  return (
    <form onSubmit={submit} noValidate>
      <SectionCard
        title={{ text: t('profile.password.title') }}
        description={t('profile.password.description')}
        footer={
          <div className="flex w-full flex-wrap items-center justify-end gap-3">
            {saved ? (
              <SavedNotice message={t('profile.password.saved')} className="mr-auto" />
            ) : null}

            <Button type="submit" disabled={form.processing} aria-busy={form.processing}>
              {form.processing ? t('profile.password.submitting') : t('profile.password.submit')}
            </Button>
          </div>
        }
      >
        <div className="grid gap-2">
          <Label htmlFor="current_password">{t('profile.password.current.label')}</Label>
          <Input
            id="current_password"
            name="current_password"
            ref={currentField}
            type="password"
            autoComplete="current-password"
            value={form.data.current_password}
            onChange={(event) => form.setData('current_password', event.target.value)}
            {...fieldErrorProps('error-current', faultText(errors.current_password))}
          />
          <FieldError id="error-current" message={faultText(errors.current_password)} />
        </div>

        <div className="grid gap-2">
          <Label htmlFor="new_password">{t('profile.password.new.label')}</Label>
          <Input
            id="new_password"
            name="new_password"
            type="password"
            autoComplete="new-password"
            value={form.data.new_password}
            onChange={(event) => form.setData('new_password', event.target.value)}
            {...fieldErrorProps('error-new', faultText(errors.new_password))}
          />
          <FieldError id="error-new" message={faultText(errors.new_password)} />
        </div>

        <div className="grid gap-2">
          <Label htmlFor="confirm_password">{t('profile.password.confirm.label')}</Label>
          <Input
            id="confirm_password"
            name="confirm_password"
            type="password"
            autoComplete="new-password"
            value={form.data.confirm_password}
            onChange={(event) => form.setData('confirm_password', event.target.value)}
            {...fieldErrorProps('error-confirm', faultText(errors.confirm_password))}
          />
          <FieldError id="error-confirm" message={faultText(errors.confirm_password)} />
        </div>
      </SectionCard>
    </form>
  )
}

/**
 * Lo que se guardó, anunciado donde se guardó.
 *
 * Es enfocable y vive en el pie de su propia tarjeta: las tres acciones del
 * perfil vuelven a la misma dirección, así que un aviso arriba de todo dejaría
 * sin saber cuál de las tres respondió (RT-11).
 *
 * Se monta sólo cuando hay algo que anunciar, así que el efecto que mueve el
 * foco corre una vez por acción; y la tarjeta que lo hospeda no rinde su pie
 * cuando no está, que es lo que evita una franja vacía debajo del formulario.
 */
function SavedNotice({ message, className }: { message: string; className?: string }) {
  const announcement = useRef<HTMLParagraphElement>(null)

  useEffect(() => {
    announcement.current?.focus()
  }, [])

  return (
    <p
      ref={announcement}
      role="status"
      aria-live="polite"
      tabIndex={-1}
      className={cn(
        'focus-visible:ring-ring text-muted-foreground rounded text-sm focus-visible:ring-2 focus-visible:outline-none',
        className
      )}
    >
      {message}
    </p>
  )
}
