import { useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'

import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
import Label from '@/components/common/Label'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import TagLink from '@/components/TagLink'
import { useDialogue } from '@/components/dialogues/DialogueProvider'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import BehaviourSettingsSection from '@/components/users/BehaviourSettingsSection'
import InheritDialogue from '@/components/users/InheritDialogue'
import KeyboardSettingsSection from '@/components/users/KeyboardSettingsSection'
import UserCodeDialogue from '@/components/users/UserCodeDialogue'
import { CATEGORY_NAMES, CATEGORIES } from '@/consts'
import { SITE_TITLE } from '@/config'
import { apiPut } from '@/lib/api'
import {
    applyThemeSelection,
    DEFAULT_USER_SETTINGS,
    deriveUserThemeModeFromSlotSelection,
    fetchUserSettings,
    fetchUserThemeSlots,
    getClientActiveThemeSlot,
    getDefaultUserThemeSlot,
    hasStoredClientThemeSelection,
    isUserThemeSlotSelection,
    normaliseThemeSlotSelectionAgainstSlots,
    normaliseUserThemeSlots,
    setCachedUserThemeSlots,
    setClientActiveThemeSlot,
    resolveThemeFromSlotSelection,
    updateUserSettings,
    upsertUserThemeSlot,
    USER_THEME_SLOT_SELECTIONS,
} from '@/lib/settings'
import { useKeyboardShortcutSettings } from '@/lib/useKeyboardShortcuts'
import { cn, inputClass } from '@/lib/utils'
import { useValidationErrors } from '@/lib/useValidationErrors'

import type { CSSProperties, Dispatch, FC, KeyboardEvent, SetStateAction } from 'react'

import type { Category, Tag, User } from '@/types'
import type {
    BuiltinThemeId,
    ClientTheme,
    ThemeSlotSelection,
    ThemeTokens,
    ThemeTagColours,
    UserSettings,
    UserThemeSlotMap,
    UserThemeSlotNo,
    UserThemeSlotSelection,
} from '@/lib/settings'

type Props = {
    user:     User | null
    setUser:  Dispatch<SetStateAction<User | null>> }

type UserFormField = 'name'
type SettingsFormField = 'theme'
type SettingsTab = 'account' | 'theme' | 'keyboard' | 'behavior'
type ThemeTokenKey = Exclude<keyof ThemeTokens, 'tagColours'>

type TabSpec = {
    id:     SettingsTab
    label:  string }

type AccountSectionProps = {
    user:             User
    name:             string
    onNameChange:     (value: string) => void
    onInheritOpen:    () => void
    onUserCodeOpen:   () => void
    onUserSubmit:     () => void
    userBaseErrors:   string[]
    userFieldErrors:  Partial<Record<UserFormField, string[]>> }

type ThemeSelectionItem = {
    id:         ThemeSlotSelection
    label:      string
    editable:   boolean
    baseTheme?: BuiltinThemeId
    slotNo?:    UserThemeSlotNo }

type ThemeTokenFieldSpec = {
    key:    ThemeTokenKey
    label:  string }

type ThemeTokenGroupSpec = {
    title:   string
    fields:  ThemeTokenFieldSpec[] }

type ThemeSectionProps = {
    draftActiveThemeSlot:    ThemeSlotSelection
    draftThemeSlots:         UserThemeSlotMap
    editingThemeSlot:        UserThemeSlotSelection | null
    selectedTheme:           ClientTheme
    selectedThemeLabel:      string
    hasUnsavedChanges:       boolean
    settingsBaseErrors:      string[]
    settingsFieldErrors:     Partial<Record<SettingsFormField, string[]>>
    onSelectThemeSlot:       (selection: ThemeSlotSelection) => void
    onEditThemeSlot:         (selection: UserThemeSlotSelection) => void
    onEnsureEditableTheme:   () => void
    onThemeTokenChange:      (key: ThemeTokenKey, value: string) => void
    onTagColourChange:       (category: Category, colour: string) => void
    onResetThemeSlot:        (selection: UserThemeSlotSelection) => void
    onThemeSubmit:           () => void
    onThemeDiscard:          () => void }

const tabs: TabSpec[] = [
    { id: 'account', label: 'アカウント' },
    { id: 'theme', label: 'テーマ' },
    { id: 'keyboard', label: 'キーボード' },
    { id: 'behavior', label: '動作' } ]

const sectionClassName =
    'space-y-4 rounded-xl border border-border bg-background/80 p-4'

const themeSelections: ThemeSelectionItem[] = [
    { id: 'system', label: 'システム設定', editable: false },
    { id: 'builtin:light', label: '標準ライト', editable: false },
    { id: 'builtin:dark', label: '標準ダーク', editable: false },
    { id: 'user:light:1', label: 'ライト 1', editable: true, baseTheme: 'light', slotNo: 1 },
    { id: 'user:light:2', label: 'ライト 2', editable: true, baseTheme: 'light', slotNo: 2 },
    { id: 'user:light:3', label: 'ライト 3', editable: true, baseTheme: 'light', slotNo: 3 },
    { id: 'user:dark:1', label: 'ダーク 1', editable: true, baseTheme: 'dark', slotNo: 1 },
    { id: 'user:dark:2', label: 'ダーク 2', editable: true, baseTheme: 'dark', slotNo: 2 },
    { id: 'user:dark:3', label: 'ダーク 3', editable: true, baseTheme: 'dark', slotNo: 3 },
]

const themeTokenGroups: ThemeTokenGroupSpec[] = [
    { title: '基本色',
      fields: [
	  { key: 'background', label: '背景' },
	  { key: 'foreground', label: '文字' },
	  { key: 'primary', label: '主ボタン' },
	  { key: 'primaryForeground', label: '主ボタン文字' },
	  { key: 'accent', label: 'アクセント' },
	  { key: 'accentForeground', label: 'アクセント文字' },
      ] },
    { title: 'パネル・カード',
      fields: [
	  { key: 'card', label: 'カード背景' },
	  { key: 'cardForeground', label: 'カード文字' },
	  { key: 'popover', label: 'ポップアップ背景' },
	  { key: 'popoverForeground', label: 'ポップアップ文字' },
	  { key: 'muted', label: '薄い背景' },
	  { key: 'mutedForeground', label: '薄い文字' },
      ] },
    { title: '入力欄・境界線',
      fields: [
	  { key: 'border', label: '境界線' },
	  { key: 'input', label: '入力欄' },
	  { key: 'ring', label: 'フォーカスリング' },
      ] },
    { title: 'リンク',
      fields: [
	  { key: 'link', label: 'リンク' },
      ] },
    { title: '上部ナビ',
      fields: [
	  { key: 'topNavBackground', label: '背景' },
	  { key: 'topNavForeground', label: '文字' },
	  { key: 'topNavBorder', label: '境界線' },
	  { key: 'topNavLink', label: 'リンク' },
	  { key: 'topNavLinkHover', label: 'リンク hover' },
	  { key: 'topNavActiveBackground', label: '選択中背景' },
	  { key: 'topNavActiveForeground', label: '選択中文字' },
	  { key: 'topNavSubmenuBackground', label: 'サブメニュー背景' },
	  { key: 'topNavSubmenuForeground', label: 'サブメニュー文字' },
      ] },
]

const previewTag = (
    id:         number,
    name:       string,
    category:   Category,
    postCount:  number,
    hasWiki:    boolean,
    options?:   { hasDeerjikists?: boolean },
): Tag => ({
    id,
    name,
    category,
    deprecatedAt: null,
    aliases: [],
    parents: [],
    postCount,
    createdAt: '',
    updatedAt: '',
    hasWiki,
    materialId: null,
    hasDeerjikists: options?.hasDeerjikists ?? false,
    children: [] })

const previewTags: Record<Category, Tag> = {
    deerjikist: previewTag (1, 'にじらー', 'deerjikist', 12, true, { hasDeerjikists: true }),
    meme: previewTag (2, 'ミーム', 'meme', 34, true),
    character: previewTag (3, 'キャラクター', 'character', 56, false),
    general: previewTag (4, '一般', 'general', 78, true),
    material: previewTag (5, '素材', 'material', 90, false),
    meta: previewTag (6, 'メタ', 'meta', 21, true),
    nico: previewTag (7, 'nico:sm9', 'nico', 43, false) }

const parseTab = (search: string): SettingsTab => {
  const tab = new URLSearchParams (search).get ('tab')
  return (tabs.some (candidate => candidate.id === tab)
	  ? (tab as SettingsTab)
	  : 'account')
}

const tabButtonId = (tab: SettingsTab): string => `settings-tab-${ tab }`
const tabPanelId = (tab: SettingsTab): string => `settings-panel-${ tab }`

const themePreviewStyle = (tagColours: ThemeTagColours): CSSProperties =>
    Object.fromEntries (
	CATEGORIES.flatMap (category => [
	    [`--tag-colour-${ category }`, tagColours[category]],
	    [`--tag-colour-${ category }-hover`, tagColours[category]]])) as CSSProperties

const themeLabelBySelection = (selection: ThemeSlotSelection): string =>
    themeSelections.find (item => item.id === selection)?.label ?? 'システム設定'

const ensureCompleteThemeSlots = (
    themeSlots: UserThemeSlotMap,
): UserThemeSlotMap =>
    USER_THEME_SLOT_SELECTIONS.reduce<UserThemeSlotMap> (
	(complete, selection) => {
	  const [_, baseTheme, slotNo] = selection.split (':')
	  complete[selection] =
	      themeSlots[selection]
	      ?? getDefaultUserThemeSlot (
		  baseTheme as BuiltinThemeId,
		  Number (slotNo) as UserThemeSlotNo,
	      )
	  return complete
	},
	{ ...themeSlots },
    )

const serialiseThemeDraft = (
    activeThemeSlot: ThemeSlotSelection,
    themeSlots:      UserThemeSlotMap,
): string =>
    JSON.stringify ({
	activeThemeSlot,
	themeSlots: USER_THEME_SLOT_SELECTIONS.map (selection => ({
	    selection,
	    tokens: themeSlots[selection]?.tokens,
	})),
    })

const themeToSelection = (
    theme: UserSettings['theme'],
): ThemeSlotSelection =>
    theme === 'system'
    ? 'system'
    : `builtin:${ theme }`

const themeRowBaseLabel = (
    selection: ThemeSlotSelection,
): string =>
    selection === 'system'
    ? '端末設定に従います'
    : themeLabelBySelection (selection)


const formatHslComponent = (value: number): string =>
    Number.isInteger (value) ? String (value) : value.toFixed (1).replace (/\.0$/, '')


const hslTokenToHex = (token: string): string => {
  const match =
      token.match (
	  /^(-?\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/,
      )
  if (match == null)
    return '#000000'

  const hue = Number (match[1]) / 360
  const saturation = Number (match[2]) / 100
  const lightness = Number (match[3]) / 100

  if (saturation === 0)
    {
      const channel = Math.round (lightness * 255)
      const hex = channel.toString (16).padStart (2, '0')
      return `#${ hex }${ hex }${ hex }`
    }

  const hueToRgb = (
      p: number,
      q: number,
      t: number,
  ): number => {
    let adjusted = t

    if (adjusted < 0)
      adjusted += 1
    if (adjusted > 1)
      adjusted -= 1
    if (adjusted < 1 / 6)
      return p + (q - p) * 6 * adjusted
    if (adjusted < 1 / 2)
      return q
    if (adjusted < 2 / 3)
      return p + (q - p) * (2 / 3 - adjusted) * 6
    return p
  }

  const q =
      lightness < .5
      ? lightness * (1 + saturation)
      : lightness + saturation - lightness * saturation
  const p = 2 * lightness - q
  const red = Math.round (hueToRgb (p, q, hue + 1 / 3) * 255)
  const green = Math.round (hueToRgb (p, q, hue) * 255)
  const blue = Math.round (hueToRgb (p, q, hue - 1 / 3) * 255)

  return `#${ [red, green, blue]
      .map (channel => channel.toString (16).padStart (2, '0'))
      .join ('') }`
}


const hexToHslToken = (hex: string): string => {
  const red = parseInt (hex.slice (1, 3), 16) / 255
  const green = parseInt (hex.slice (3, 5), 16) / 255
  const blue = parseInt (hex.slice (5, 7), 16) / 255
  const max = Math.max (red, green, blue)
  const min = Math.min (red, green, blue)
  const lightness = (max + min) / 2

  if (max === min)
    return `0 0% ${ formatHslComponent (lightness * 100) }%`

  const delta = max - min
  const saturation =
      lightness > .5
      ? delta / (2 - max - min)
      : delta / (max + min)

  let hue = 0
  switch (max)
  {
  case red:
    hue = (green - blue) / delta + (green < blue ? 6 : 0)
    break
  case green:
    hue = (blue - red) / delta + 2
    break
  case blue:
    hue = (red - green) / delta + 4
    break
  }

  return [
      formatHslComponent (hue * 60),
      `${ formatHslComponent (saturation * 100) }%`,
      `${ formatHslComponent (lightness * 100) }%`,
  ].join (' ')
}


const AccountSection: FC<AccountSectionProps> = (
    { name,
      onInheritOpen,
      onNameChange,
      onUserCodeOpen,
      onUserSubmit,
      user,
      userBaseErrors,
      userFieldErrors },
) => (
    <section className={sectionClassName}>
      <h2 className="text-xl font-bold">アカウント</h2>
      <FieldError messages={userBaseErrors}/>

      <FormField label="表示名" messages={userFieldErrors.name}>
	{({ describedBy, invalid }) => (
	    <>
	      <input
		  type="text"
		  aria-describedby={describedBy}
		  aria-invalid={invalid}
		  className={inputClass (invalid)}
		  value={name}
		  placeholder="名もなきニジラー"
		  onChange={ev => onNameChange (ev.target.value)}/>
	      {!(user.name) && (
		  <p className="mt-1 text-sm text-red-500">
		    名前が未設定のアカウントは 30 日間アクセスしないと削除されます!!!!
		  </p>)}
	    </>)}
      </FormField>

      <div className="flex flex-wrap gap-2">
	<Button type="button" onClick={onUserSubmit}>
	  表示名を更新
	</Button>
      </div>

      <div className="space-y-2">
	<Label>引継ぎ</Label>
	<div className="flex flex-wrap gap-2">
	  <Button
	      type="button"
	      onClick={onUserCodeOpen}
	      className="bg-gray-600 text-white"
	      disabled={!(user)}>
	    引継ぎコードを表示
	  </Button>
	  <Button
	      type="button"
	      onClick={onInheritOpen}
	      className="bg-red-600 text-white"
	      disabled={!(user)}>
	    ほかのブラウザから引継ぐ
	  </Button>
	</div>
      </div>
    </section>)

const ThemeSection: FC<ThemeSectionProps> = (
    { draftActiveThemeSlot,
      draftThemeSlots,
      editingThemeSlot,
      selectedTheme,
      selectedThemeLabel,
      hasUnsavedChanges,
      settingsBaseErrors,
      settingsFieldErrors,
      onSelectThemeSlot,
      onEditThemeSlot,
      onEnsureEditableTheme,
      onThemeTokenChange,
      onTagColourChange,
      onResetThemeSlot,
      onThemeSubmit,
      onThemeDiscard },
) => (
    <section className={sectionClassName}>
      <div className="flex flex-wrap items-start justify-between gap-3">
	<div className="space-y-1">
	  <h2 className="text-xl font-bold">テーマ</h2>
	  <p className="text-sm text-muted-foreground">
	    現在の表示: {selectedThemeLabel}
	  </p>
	  {editingThemeSlot && (
	      <p className="text-sm text-muted-foreground">
		編輯対象: {themeLabelBySelection (editingThemeSlot)}
	      </p>)}
	  {hasUnsavedChanges && (
	      <p className="text-sm text-amber-700 dark:text-amber-300">
		未保存の変更があります
	      </p>)}
	</div>

	<div className="flex flex-wrap gap-2">
	  <Button
	      type="button"
	      variant="outline"
	      disabled={!(hasUnsavedChanges)}
	      onClick={onThemeDiscard}>
	    変更を破棄
	  </Button>
	  <Button
	      type="button"
	      disabled={!(hasUnsavedChanges)}
	      onClick={onThemeSubmit}>
	    保存
	  </Button>
	</div>
      </div>

      <FieldError messages={settingsBaseErrors}/>
      <FieldError messages={settingsFieldErrors.theme ?? []}/>

      <div className="space-y-3">
	{themeSelections.map (selection => {
	    const themeSlot =
		selection.editable
		? draftThemeSlots[selection.id as UserThemeSlotSelection]
		: null
	    const isSelected = draftActiveThemeSlot === selection.id
	    const isEditing = editingThemeSlot === selection.id

	    return (
		<div
		    key={selection.id}
		    className={cn (
			'space-y-3 rounded-xl border border-border/70 bg-background/70 p-4',
			isSelected && 'ring-1 ring-primary/30',
		    )}>
		  <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
		    <div className="space-y-1">
		      <p className="text-sm font-semibold">{selection.label}</p>
		      <p className="text-sm text-muted-foreground">
			{themeRowBaseLabel (selection.id)}
		      </p>
		    </div>

		    <div className="flex flex-wrap gap-2">
		      <Button
			  type="button"
			  variant={isSelected ? 'default' : 'outline'}
			  onClick={() => onSelectThemeSlot (selection.id)}>
			選択
		      </Button>
		      {selection.editable && (
			  <>
			    <Button
				type="button"
				variant={isEditing ? 'default' : 'outline'}
				onClick={() =>
				    onEditThemeSlot (selection.id as UserThemeSlotSelection)}>
			      編集
			    </Button>
			    <Button
				type="button"
				variant="outline"
				disabled={!(hasUnsavedChanges) || !(isEditing || isSelected)}
				onClick={onThemeSubmit}>
			      保存
			    </Button>
			    <Button
				type="button"
				variant="ghost"
				onClick={() =>
				    onResetThemeSlot (selection.id as UserThemeSlotSelection)}>
			      既定に戻す
			    </Button>
			  </>)}
		    </div>
		  </div>

		  {themeSlot && (
		      <div style={themePreviewStyle (themeSlot.tokens.tagColours)}>
			<TagLink
			    tag={previewTags.general}
			    linkFlg={false}
			    truncateOnMobile
			    withWiki={false}/>
		      </div>)}
		</div>)
	})}
      </div>

      <div className="space-y-4">
	{themeTokenGroups.map (group => (
	    <div
		key={group.title}
		className="space-y-3 rounded-xl border border-border/70 bg-background/70 p-4">
	      <h3 className="text-sm font-semibold">{group.title}</h3>
	      <div className="space-y-3">
		{group.fields.map (field => (
		    <div
			key={field.key}
			className="flex flex-col gap-3 rounded-lg border border-border/70 px-3 py-3
				   sm:flex-row sm:items-center sm:justify-between">
		      <div className="space-y-1">
			<p className="text-sm font-medium">{field.label}</p>
		      </div>
		      <div className="flex flex-wrap items-center gap-3">
			<input
			    type="color"
			    value={hslTokenToHex (selectedTheme.tokens[field.key])}
			    onPointerDown={onEnsureEditableTheme}
			    onFocus={onEnsureEditableTheme}
			    onChange={ev =>
				onThemeTokenChange (field.key, hexToHslToken (ev.target.value))}
			    className="h-10 w-16 cursor-pointer rounded border border-border
				       bg-transparent p-1"/>
		      </div>
		    </div>))}
	      </div>
	    </div>))}

	<div className="space-y-3 rounded-xl border border-border/70 bg-background/70 p-4">
	  <div className="flex flex-wrap items-center justify-between gap-2">
	    <div>
	      <h3 className="font-medium">タグ</h3>
	    </div>
	  </div>

	  <div className="space-y-3">
	    {CATEGORIES.map (category => (
		<div
		    key={category}
		    className="flex flex-col gap-3 rounded-lg border border-border/70 px-3 py-3
			       sm:flex-row sm:items-center sm:justify-between">
		  <div className="space-y-2">
		    <p className="text-sm font-medium">{CATEGORY_NAMES[category]}</p>
		    <div style={themePreviewStyle (selectedTheme.tokens.tagColours)}>
		      <TagLink
			  tag={previewTags[category]}
			  linkFlg={false}
			  truncateOnMobile
			  withWiki={false}/>
		    </div>
		  </div>

		  <div className="flex flex-wrap items-center gap-3">
		    <input
			type="color"
			value={selectedTheme.tokens.tagColours[category]}
			onPointerDown={onEnsureEditableTheme}
			onFocus={onEnsureEditableTheme}
			onChange={ev => onTagColourChange (category, ev.target.value)}
			className="h-10 w-16 cursor-pointer rounded border border-border
				   bg-transparent p-1"/>
		  </div>
		</div>))}
	  </div>
	</div>
      </div>
    </section>)


const SettingPage: FC<Props> = ({ user, setUser }) => {
  const location = useLocation ()
  const navigate = useNavigate ()
  const dialogue = useDialogue ()

  const [inheritVsbl, setInheritVsbl] = useState (false)
  const [loading, setLoading] = useState (true)
  const [name, setName] = useState ('')
  const [settingsError, setSettingsError] = useState<string | null> (null)
  const [, setDraftSettings] = useState<UserSettings> (DEFAULT_USER_SETTINGS)
  const [savedActiveThemeSlot, setSavedActiveThemeSlot] =
      useState<ThemeSlotSelection> (getClientActiveThemeSlot ())
  const [draftActiveThemeSlot, setDraftActiveThemeSlot] =
      useState<ThemeSlotSelection> (getClientActiveThemeSlot ())
  const [savedThemeSlots, setSavedThemeSlots] =
      useState<UserThemeSlotMap> (() => ensureCompleteThemeSlots ({ }))
  const [draftThemeSlots, setDraftThemeSlots] =
      useState<UserThemeSlotMap> (() => ensureCompleteThemeSlots ({ }))
  const [editingThemeSlot, setEditingThemeSlot] =
      useState<UserThemeSlotSelection | null> (null)
  const [userCodeVsbl, setUserCodeVsbl] = useState (false)
  const { conflictActionIds } = useKeyboardShortcutSettings ()
  const { baseErrors: userBaseErrors,
	  fieldErrors: userFieldErrors,
	  clearValidationErrors: clearUserValidationErrors,
	  applyValidationError: applyUserValidationError } = useValidationErrors<UserFormField> ()
  const { baseErrors: settingsBaseErrors,
	  fieldErrors: settingsFieldErrors,
	  clearValidationErrors: clearSettingsValidationErrors,
	  applyValidationError: applySettingsValidationError } =
      useValidationErrors<SettingsFormField> ()

  const activeTab = useMemo<SettingsTab> (() => parseTab (location.search), [location.search])
  const selectedTheme = useMemo<ClientTheme> (
      () => {
	  const resolvedTheme =
	      resolveThemeFromSlotSelection (draftActiveThemeSlot, draftThemeSlots)
	  return {
	      id: resolvedTheme.baseTheme,
	      name: themeLabelBySelection (draftActiveThemeSlot),
	      builtin: resolvedTheme.builtin,
	      baseTheme: resolvedTheme.baseTheme,
	      tokens: resolvedTheme.tokens,
	  }
      },
      [draftActiveThemeSlot, draftThemeSlots],
  )
  const hasUnsavedThemeChanges = useMemo (
      () =>
	  serialiseThemeDraft (draftActiveThemeSlot, draftThemeSlots)
	  !== serialiseThemeDraft (savedActiveThemeSlot, savedThemeSlots),
      [draftActiveThemeSlot, draftThemeSlots, savedActiveThemeSlot, savedThemeSlots],
  )

  const setActiveTab = (tab: SettingsTab) => {
    const params = new URLSearchParams (location.search)
    params.set ('tab', tab)
    navigate (
	`${ location.pathname }?${ params.toString () }`,
	{ replace: true })
  }

  const settingsTabErrors = useMemo<Record<SettingsTab, boolean>> (
      () => (
	  { account: Boolean (userFieldErrors.name?.length),
	    theme: Boolean (settingsFieldErrors.theme?.length),
	    keyboard: conflictActionIds.size > 0,
	    behavior: false }),
      [conflictActionIds.size, settingsFieldErrors.theme, userFieldErrors.name],
  )

  const applyDraftThemeSelection = (
      activeThemeSlot: ThemeSlotSelection,
      themeSlots:      UserThemeSlotMap,
  ) => {
    setDraftActiveThemeSlot (activeThemeSlot)
    setDraftThemeSlots (themeSlots)
    setDraftSettings (current => ({
	...current,
	theme: deriveUserThemeModeFromSlotSelection (activeThemeSlot),
    }))
    applyThemeSelection (activeThemeSlot, themeSlots)
  }

  const confirmThemeSlotSwitch = async (
      onConfirmed: (themeSlots: UserThemeSlotMap) => void,
  ): Promise<void> => {
    if (!(hasUnsavedThemeChanges))
      {
	onConfirmed (draftThemeSlots)
	return
      }

    const confirmed = await dialogue.confirm ({
	title: '未保存の変更があります',
	description: 'このまま切り替えると、保存していない変更は失われます。',
	confirmText: '変更を破棄して切り替える',
	cancelText: 'このまま編輯する',
    })

    if (!(confirmed))
      return

    setDraftActiveThemeSlot (savedActiveThemeSlot)
    setDraftThemeSlots (savedThemeSlots)
    setEditingThemeSlot (null)
    onConfirmed (savedThemeSlots)
  }

  const ensureEditableThemeSlot = async (): Promise<UserThemeSlotSelection | null> => {
    if (editingThemeSlot != null)
      return editingThemeSlot
    if (isUserThemeSlotSelection (draftActiveThemeSlot))
      {
	setEditingThemeSlot (draftActiveThemeSlot)
	return draftActiveThemeSlot
      }

    const choice = await dialogue.choice<UserThemeSlotSelection> ({
	title: '保存先を選択してください',
	description: '色を変更する前に、保存先のテーマ枠を選択してください。',
	cancelText: '取消',
	choices: themeSelections
	    .filter (selection => selection.editable)
	    .map (selection => ({
		value: selection.id as UserThemeSlotSelection,
		label: selection.label,
	    })),
    })

    if (choice == null)
      return null

    setEditingThemeSlot (choice)
    applyDraftThemeSelection (choice, draftThemeSlots)
    return choice
  }

  const updateDraftThemeSlot = (
      selection: UserThemeSlotSelection,
      updater:   (tokens: ThemeTokens) => ThemeTokens,
      options?:  { activeThemeSlot?: ThemeSlotSelection },
  ) => {
    const currentThemeSlot =
	draftThemeSlots[selection]
	?? ensureCompleteThemeSlots ({ })[selection]
    const nextThemeSlots = {
	...draftThemeSlots,
	[selection]: {
	    ...currentThemeSlot,
	    tokens: updater (currentThemeSlot.tokens),
	},
    }

    applyDraftThemeSelection (
	options?.activeThemeSlot ?? draftActiveThemeSlot,
	nextThemeSlots,
    )
  }

  const handleTabKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
    const currentIndex = tabs.findIndex (tab => tab.id === activeTab)

    switch (event.key)
    {
    case 'ArrowDown':
    case 'ArrowRight':
      event.preventDefault ()
      setActiveTab (tabs[(currentIndex + 1) % tabs.length].id)
      break

    case 'ArrowUp':
    case 'ArrowLeft':
      event.preventDefault ()
      setActiveTab (tabs[(currentIndex - 1 + tabs.length) % tabs.length].id)
      break

    case 'Home':
      event.preventDefault ()
      setActiveTab (tabs[0].id)
      break

    case 'End':
      event.preventDefault ()
      setActiveTab (tabs[tabs.length - 1].id)
      break
    }
  }

  const handleUserSubmit = async () => {
    if (!(user))
      return

    clearUserValidationErrors ()

    const formData = new FormData
    formData.append ('name', name)

    try
    {
      const data = await apiPut<User> (
	  `/users/${ user.id }`, formData,
	  { headers: { 'Content-Type': 'multipart/form-data' } },
      )
      setUser (currentUser => ({ ...currentUser, ...data }))
      toast ({ title: '表示名を更新しました．' })
    }
    catch (submitError)
    {
      applyUserValidationError (submitError)
      toast ({ title: '表示名を更新できませんでした．' })
    }
  }

  const handleThemeSubmit = async () => {
    clearSettingsValidationErrors ()

    try
    {
      let nextSavedThemeSlots = { ...savedThemeSlots }

      for (const selection of USER_THEME_SLOT_SELECTIONS)
	{
	  const draftThemeSlot = draftThemeSlots[selection]
	  const savedThemeSlot = savedThemeSlots[selection]
	  const requiresPersist =
	      selection === draftActiveThemeSlot
	      && isUserThemeSlotSelection (draftActiveThemeSlot)
	      && savedThemeSlot?.createdAt == null

	  if (
	      JSON.stringify (draftThemeSlot?.tokens)
	      === JSON.stringify (savedThemeSlot?.tokens)
	      && !(requiresPersist)
	  )
	    continue

	  const persisted = await upsertUserThemeSlot (
	      draftThemeSlot?.baseTheme ?? selection.split (':')[1] as BuiltinThemeId,
	      draftThemeSlot?.slotNo ?? Number (selection.split (':')[2]) as UserThemeSlotNo,
	      draftThemeSlot?.tokens ?? getDefaultUserThemeSlot (
		  selection.split (':')[1] as BuiltinThemeId,
		  Number (selection.split (':')[2]) as UserThemeSlotNo,
	      ).tokens,
	  )
	  nextSavedThemeSlots = {
	      ...nextSavedThemeSlots,
	      [selection]: persisted,
	  }
	}

      const themeMode =
	  deriveUserThemeModeFromSlotSelection (draftActiveThemeSlot)
      const data = await updateUserSettings ({ theme: themeMode })
      const completeThemeSlots = ensureCompleteThemeSlots (nextSavedThemeSlots)

      setDraftSettings ({ ...data, theme: themeMode })
      setSavedActiveThemeSlot (draftActiveThemeSlot)
      setSavedThemeSlots (completeThemeSlots)
      setDraftThemeSlots (completeThemeSlots)
      setEditingThemeSlot (
	  isUserThemeSlotSelection (draftActiveThemeSlot)
	  ? draftActiveThemeSlot
	  : null,
      )
      setClientActiveThemeSlot (draftActiveThemeSlot)
      setCachedUserThemeSlots (completeThemeSlots)
      applyThemeSelection (draftActiveThemeSlot, completeThemeSlots)
      toast ({ title: 'テーマ設定を保存しました．' })
    }
    catch (submitError)
    {
      applySettingsValidationError (submitError)
      toast ({ title: 'テーマ設定を保存できませんでした．' })
    }
  }

  const handleThemeDiscard = () => {
    setDraftActiveThemeSlot (savedActiveThemeSlot)
    setDraftThemeSlots (savedThemeSlots)
    setEditingThemeSlot (
	isUserThemeSlotSelection (savedActiveThemeSlot)
	? savedActiveThemeSlot
	: null,
    )
    applyThemeSelection (savedActiveThemeSlot, savedThemeSlots)
  }

  useEffect (() => {
    if (!user)
      return

    setName (user.name ?? '')
  }, [user])

  useEffect (() => {
    if (!(user))
      return

    let cancelled = false

    void (async () => {
      setLoading (true)

      try
      {
	const [settings, themeSlots] = await Promise.all ([
	    fetchUserSettings (),
	    fetchUserThemeSlots (),
	])
	if (cancelled)
	  return

	const storedThemeSlots = normaliseUserThemeSlots (themeSlots)
	const completeThemeSlots = ensureCompleteThemeSlots (storedThemeSlots)
	const initialSelection = (
	    hasStoredClientThemeSelection ()
	    ? getClientActiveThemeSlot ()
	    : themeToSelection (settings.theme)
	)
	const nextSelection = normaliseThemeSlotSelectionAgainstSlots (
	    initialSelection,
	    storedThemeSlots,
	)
	const themeMode = deriveUserThemeModeFromSlotSelection (nextSelection)

	setDraftSettings ({ ...settings, theme: themeMode })
	setSavedActiveThemeSlot (nextSelection)
	setDraftActiveThemeSlot (nextSelection)
	setSavedThemeSlots (completeThemeSlots)
	setDraftThemeSlots (completeThemeSlots)
	setEditingThemeSlot (
	    isUserThemeSlotSelection (nextSelection)
	    ? nextSelection
	    : null,
	)
	setCachedUserThemeSlots (completeThemeSlots)
	applyThemeSelection (nextSelection, completeThemeSlots)
	if (nextSelection !== initialSelection)
	  setClientActiveThemeSlot (nextSelection)
	setSettingsError (null)
      }
      catch
      {
	if (cancelled)
	  return

	setSettingsError ('設定の読込みに失敗しました．')
      }
      finally
      {
	if (!(cancelled))
	  setLoading (false)
      }
    }) ()

    return () => {
      cancelled = true
    }
  }, [user])

  useEffect (() => {
    if (userFieldErrors.name?.length)
      setActiveTab ('account')
  }, [location.pathname, location.search, userFieldErrors.name])

  useEffect (() => {
    if (settingsFieldErrors.theme?.length)
      setActiveTab ('theme')
  }, [location.pathname, location.search, settingsFieldErrors.theme])

  useEffect (() => {
    applyThemeSelection (draftActiveThemeSlot, draftThemeSlots)

    return () => {
      applyThemeSelection (savedActiveThemeSlot, savedThemeSlots)
    }
  }, [draftActiveThemeSlot, draftThemeSlots, savedActiveThemeSlot, savedThemeSlots])

  const accountSectionProps: AccountSectionProps | null =
      user
      ? {
	  user,
	  name,
	  onNameChange: setName,
	  onInheritOpen: () => setInheritVsbl (true),
	  onUserCodeOpen: () => setUserCodeVsbl (true),
	  onUserSubmit: handleUserSubmit,
	  userBaseErrors,
	  userFieldErrors,
      }
      : null

  const themeSectionProps: ThemeSectionProps = {
      draftActiveThemeSlot,
      draftThemeSlots,
      editingThemeSlot,
      selectedTheme,
      selectedThemeLabel: themeLabelBySelection (draftActiveThemeSlot),
      hasUnsavedChanges: hasUnsavedThemeChanges,
      settingsBaseErrors,
      settingsFieldErrors,
      onSelectThemeSlot: selection => {
	  void confirmThemeSlotSwitch (themeSlots => {
	      setEditingThemeSlot (null)
	      applyDraftThemeSelection (selection, themeSlots)
	  })
      },
      onEditThemeSlot: selection => {
	  void confirmThemeSlotSwitch (themeSlots => {
	      setEditingThemeSlot (selection)
	      applyDraftThemeSelection (selection, themeSlots)
	  })
      },
      onEnsureEditableTheme: () => {
	  void ensureEditableThemeSlot ()
      },
      onThemeTokenChange: (key, value) => {
	  void (async () => {
	      const editableThemeSlot = await ensureEditableThemeSlot ()
	      if (editableThemeSlot == null)
		return

	      updateDraftThemeSlot (editableThemeSlot, tokens => ({
		  ...tokens,
		  [key]: value,
	      }), { activeThemeSlot: editableThemeSlot })
	  }) ()
      },
      onTagColourChange: (category, colour) => {
	  void (async () => {
	      const editableThemeSlot = await ensureEditableThemeSlot ()
	      if (editableThemeSlot == null)
		return

	      updateDraftThemeSlot (editableThemeSlot, tokens => ({
		  ...tokens,
		  tagColours: {
		      ...tokens.tagColours,
		      [category]: colour,
		  },
	      }), { activeThemeSlot: editableThemeSlot })
	  }) ()
      },
      onResetThemeSlot: selection => {
	  const baseTheme = draftThemeSlots[selection]?.baseTheme ?? selection.split (':')[1] as BuiltinThemeId
	  updateDraftThemeSlot (
	      selection,
	      () => getDefaultUserThemeSlot (
		  baseTheme,
		  draftThemeSlots[selection]?.slotNo
		      ?? Number (selection.split (':')[2]) as UserThemeSlotNo,
	      ).tokens,
	  )
      },
      onThemeSubmit: handleThemeSubmit,
      onThemeDiscard: handleThemeDiscard,
  }

  return (
      <MainArea>
	<Helmet>
	  <meta name="robots" content="noindex"/>
	  <title>設定 | {SITE_TITLE}</title>
	</Helmet>

	<div className="mx-auto max-w-xl space-y-4 p-4 md:hidden">
	  <PageTitle>設定</PageTitle>
	  <FieldError messages={settingsError ? [settingsError] : []}/>
	  <div
	      role="tablist"
	      aria-label="設定区分"
	      aria-orientation="horizontal"
	      className="-mx-4 flex gap-2 overflow-x-auto px-4 pb-1">
	    {tabs.map (tab => (
		<button
		    key={tab.id}
		    id={tabButtonId (tab.id)}
		    type="button"
		    role="tab"
		    aria-selected={activeTab === tab.id}
		    aria-controls={tabPanelId (tab.id)}
		    className={cn (
			'shrink-0 rounded-full border px-3 py-2 text-sm font-medium',
			(activeTab === tab.id
			 ? ['border-slate-900 bg-slate-900 text-white',
			    'dark:border-slate-100 dark:bg-slate-100 dark:text-slate-900']
			 : ['border-border bg-background text-foreground']),
		    )}
		    onClick={() => setActiveTab (tab.id)}>
		  {tab.label}
		  {settingsTabErrors[tab.id] ? ' !' : ''}
		</button>))}
	  </div>
	  {loading ? 'Loading...' : (
	      <div
		  id={tabPanelId (activeTab)}
		  role="tabpanel"
		  aria-labelledby={tabButtonId (activeTab)}>
		{accountSectionProps && activeTab === 'account' && (
		    <AccountSection {...accountSectionProps}/>)}
		{activeTab === 'theme' && <ThemeSection {...themeSectionProps}/>}
		{activeTab === 'keyboard' && (
		    <KeyboardSettingsSection sectionClassName={sectionClassName}/>)}
		{activeTab === 'behavior' && (
		    <BehaviourSettingsSection sectionClassName={sectionClassName}/>)}
	      </div>)}
	</div>

	<div className="hidden md:flex md:justify-center md:px-4">
	  <div className="grid w-full max-w-[58rem] grid-cols-[12rem_minmax(0,1fr)] gap-6">
	    <div
		role="tablist"
		aria-label="設定区分"
		aria-orientation="vertical"
		className="sticky top-4 flex h-fit flex-col gap-2">
	      {tabs.map (tab => (
		  <button
		      key={tab.id}
		      id={tabButtonId (tab.id)}
		      type="button"
		      role="tab"
		      aria-selected={activeTab === tab.id}
		      aria-controls={tabPanelId (tab.id)}
		      tabIndex={activeTab === tab.id ? 0 : -1}
		      className={cn (
			  'rounded-xl border px-4 py-3 text-left text-sm font-medium',
			  (activeTab === tab.id
			   ? ['border-slate-900 bg-slate-900 text-white',
			      'dark:border-slate-100 dark:bg-slate-100 dark:text-slate-900']
			   : ['border-slate-300 bg-white text-slate-700',
			      'dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100']),
		      )}
		      onClick={() => setActiveTab (tab.id)}
		      onKeyDown={handleTabKeyDown}>
		    {tab.label}
		    {settingsTabErrors[tab.id] ? ' ⚠' : ''}
		  </button>))}
	    </div>

	    <div className="w-full max-w-xl justify-self-center space-y-4">
	      <PageTitle>設定</PageTitle>
	      <FieldError messages={settingsError ? [settingsError] : []}/>
	      {loading ? 'Loading...' : (
		  <div
		      id={tabPanelId (activeTab)}
		      role="tabpanel"
		      aria-labelledby={tabButtonId (activeTab)}>
		    {activeTab === 'account' && accountSectionProps && (
			<AccountSection {...accountSectionProps}/>)}
		    {activeTab === 'theme' && <ThemeSection {...themeSectionProps}/>}
		    {activeTab === 'keyboard' && (
			<KeyboardSettingsSection sectionClassName={sectionClassName}/>)}
		    {activeTab === 'behavior' && (
			<BehaviourSettingsSection sectionClassName={sectionClassName}/>)}
		  </div>)}
	    </div>
	  </div>
	</div>

	<UserCodeDialogue
	    visible={userCodeVsbl}
	    onVisibleChange={setUserCodeVsbl}
	    user={user}
	    setUser={setUser}/>

	<InheritDialogue
	    visible={inheritVsbl}
	    onVisibleChange={setInheritVsbl}
	    setUser={setUser}/>
      </MainArea>)
}

export default SettingPage
