import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Check, LoaderCircle, Pencil, X } from 'lucide-react'
import { Fragment, useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'

import TagLink from '@/components/TagLink'
import SortHeader from '@/components/SortHeader'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination'
import TextArea from '@/components/common/TextArea'
import { useDialogue } from '@/components/dialogues/DialogueProvider'
import MainArea from '@/components/layout/MainArea'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { apiPut } from '@/lib/api'
import { extractValidationError } from '@/lib/apiErrors'
import { clientScrollBehaviour } from '@/lib/clientAnimation'
import { tagsKeys } from '@/lib/queryKeys'
import { fetchNicoTags } from '@/lib/tags'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn, dateString, inputClass } from '@/lib/utils'
import { canEditContent } from '@/lib/users'

import type { FC, FormEvent } from 'react'

import type { FetchNicoTagsOrder, FetchNicoTagsOrderField, NicoTag, Tag, User } from '@/types'

type LinkStatus = 'all' | 'linked' | 'unlinked'
type Props = { user: User | null }


const setIf = (qs: URLSearchParams, key: string, value: string) => {
  const trimmed = value.trim ()
  if (trimmed)
    qs.set (key, trimmed)
}


const NicoTagListPage: FC<Props> = ({ user }) => {
  const behaviourSettings = useClientBehaviourSettings ()
  const animationMode = behaviourSettings.animation ?? 'normal'
  const dialogue = useDialogue ()
  const location = useLocation ()
  const navigate = useNavigate ()
  const queryClient = useQueryClient ()
  const query = useMemo (() => new URLSearchParams (location.search), [location.search])

  const page = Number (query.get ('page') ?? 1)
  const limit = Number (query.get ('limit') ?? 20)
  const qName = query.get ('name') ?? ''
  const qLinkedTag = query.get ('linked_tag') ?? ''
  const qLinkStatus = (query.get ('link_status') || 'all') as LinkStatus
  const order = (query.get ('order') || 'updated_at:desc') as FetchNicoTagsOrder

  const [editingId, setEditingId] = useState<number | null> (null)
  const [errorsByTagId, setErrorsByTagId] = useState<Record<number, string[]>> ({ })
  const [linkStatus, setLinkStatus] = useState<LinkStatus> ('all')
  const [linkedTag, setLinkedTag] = useState ('')
  const [name, setName] = useState ('')
  const [rawTags, setRawTags] = useState<Record<number, string>> ({ })
  const [savingId, setSavingId] = useState<number | null> (null)

  const keys = {
      name: qName, linkedTag: qLinkedTag, linkStatus: qLinkStatus, page, limit, order }
  const { data, isError, isLoading: loading } = useQuery ({
      queryKey: tagsKeys.nicoIndex (keys),
      queryFn:  () => fetchNicoTags (keys) })
  const nicoTags = data?.tags ?? []
  const count = data?.count ?? 0

  const editable = canEditContent (user)
  const totalPages = Math.ceil (count / limit)

  const handleSearch = (e: FormEvent) => {
    e.preventDefault ()

    const qs = new URLSearchParams ()
    setIf (qs, 'name', name)
    setIf (qs, 'linked_tag', linkedTag)
    if (linkStatus !== 'all')
      qs.set ('link_status', linkStatus)
    qs.set ('page', '1')
    qs.set ('limit', String (limit))
    qs.set ('order', order)
    navigate (`${ location.pathname }?${ qs.toString () }`)
  }

  const defaultDirection = {
      name:       'asc',
      created_at: 'desc',
      updated_at: 'desc'} as const

  const beginEdit = async (tag: NicoTag) => {
    const editingTag = nicoTags.find (tag => tag.id === editingId)
    const editingValue = editingTag?.linkedTags.map (tag => tag.name).join (' ') ?? ''
    const editingChanged = editingId != null && rawTags[editingId] !== editingValue

    if (editingId != null && editingId !== tag.id && editingChanged
	&& !(await dialogue.confirm ({
	  title:       '編集中の内容を破棄しますか?',
	  confirmText: '破棄',
	  variant:     'danger'})))
      return

    setEditingId (tag.id)
    setRawTags (rawTags => ({
	...rawTags,
	[tag.id]: tag.linkedTags.map (linkedTag => linkedTag.name).join (' ')}))
    setErrorsByTagId (errors => ({ ...errors, [tag.id]: [] }))
  }

  const cancelEdit = (tag: NicoTag) => {
    setEditingId (null)
    setRawTags (rawTags => ({
	...rawTags,
	[tag.id]: tag.linkedTags.map (linkedTag => linkedTag.name).join (' ')}))
    setErrorsByTagId (errors => ({ ...errors, [tag.id]: [] }))
  }

  const saveLinks = async (id: number) => {
    const formData = new FormData
    formData.append ('tags', rawTags[id] ?? '')
    setSavingId (id)

    try
    {
      await apiPut<Tag[]> (`/tags/nico/${ id }`, formData,
			   { headers: { 'Content-Type': 'multipart/form-data' } })
      setErrorsByTagId (errors => ({ ...errors, [id]: [] }))
      setEditingId (null)
      await queryClient.invalidateQueries ({ queryKey: tagsKeys.nicoRoot })
      toast ({ description: '連携を更新しました．' })
    }
    catch (e)
    {
      const validationError = extractValidationError<'tags'> (e)
      setErrorsByTagId (errors => ({
	  ...errors,
	  [id]: validationError?.fieldErrors.tags
	      ?? validationError?.baseErrors
	      ?? ['更新できませんでした．']}))
      toast ({ title: '更新失敗', description: '入力内容を確認してください．' })
    }
    finally
    {
      setSavingId (null)
    }
  }

  useEffect (() => {
    setName (qName)
    setLinkedTag (qLinkedTag)
    setLinkStatus (qLinkStatus)
    setEditingId (null)

    document.querySelector ('table')?.scrollIntoView ({
	behavior: clientScrollBehaviour (animationMode) })
  }, [animationMode, location.search, qLinkedTag, qLinkStatus, qName])

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

    setRawTags (Object.fromEntries (data.tags.map (tag => [
      tag.id,
      tag.linkedTags.map (linkedTag => linkedTag.name).join (' ')])))
  }, [data])

  useEffect (() => {
    if (isError)
      toast ({ title: '読込失敗', description: 'ニコニコ連携を読み込めませんでした．' })
  }, [isError])

  return (
      <MainArea>
	<Helmet>
	  <title>ニコニコ連携 | {SITE_TITLE}</title>
	</Helmet>

	<div className="max-w-xl">
	  <PageTitle>ニコニコ連携</PageTitle>
	  <p className="mb-4 text-sm text-gray-600 dark:text-gray-300">
	    ニコニコタグを広場のタグへ結び付けます．
	  </p>

	  <form onSubmit={handleSearch} className="space-y-2">
	    <FormField label="ニコニコタグ">
	      {({ invalid }) => (
		  <input
		      type="text"
		      aria-label="ニコニコタグ"
		      value={name}
		      onChange={e => setName (e.target.value)}
		      className={inputClass (invalid)}/>)}
	    </FormField>

	    <FormField label="連携タグ">
	      {({ invalid }) => (
		  <input
		      type="text"
		      aria-label="連携タグ"
		      value={linkedTag}
		      onChange={e => setLinkedTag (e.target.value)}
		      className={inputClass (invalid)}/>)}
	    </FormField>

	    <FormField label="連携状態">
	      {({ invalid }) => (
		  <select
		      aria-label="連携状態"
		      value={linkStatus}
		      onChange={e => setLinkStatus (e.target.value as LinkStatus)}
		      className={inputClass (invalid)}>
		    <option value="all">すべて</option>
		    <option value="linked">連携済み</option>
		    <option value="unlinked">未連携</option>
		  </select>)}
	    </FormField>

	    <div className="py-3">
	      <button
		  type="submit"
		  className="rounded bg-blue-500 px-4 py-2 text-white">
		検索
	      </button>
	    </div>
	  </form>
	</div>

	{loading
	 ? 'Loading...'
	 : (
	    <div className="mt-6">
	      <div className="mb-3 flex items-baseline justify-between gap-4">
		<h2 className="text-lg font-bold">検索結果</h2>
		<span className="text-sm text-gray-500 dark:text-gray-400">{count} 件</span>
	      </div>

	      {nicoTags.length > 0
	       ? (
		  <div className="overflow-x-auto">
		    <table className="w-full min-w-[800px] table-fixed border-collapse">
		      <colgroup>
			<col className="w-64"/>
			<col className="w-[48rem]"/>
			<col className="w-56"/>
			<col className="w-56"/>
			{editable && <col className="w-24"/>}
		      </colgroup>
		      <thead className="border-b-2 border-black dark:border-white">
			<tr>
			  <th className="p-2 text-left whitespace-nowrap">
			    <SortHeader<FetchNicoTagsOrderField>
				by="name"
				label="ニコニコタグ"
				currentOrder={order}
				defaultDirection={defaultDirection}/>
			  </th>
			  <th className="p-2 text-left">連携タグ</th>
			  <th className="p-2 text-left whitespace-nowrap">
			    <SortHeader<FetchNicoTagsOrderField>
				by="created_at"
				label="最初に記載された日時"
				currentOrder={order}
				defaultDirection={defaultDirection}/>
			  </th>
			  <th className="p-2 text-left whitespace-nowrap">
			    <SortHeader<FetchNicoTagsOrderField>
				by="updated_at"
				label="最近記載された日時"
				currentOrder={order}
				defaultDirection={defaultDirection}/>
			  </th>
			  {editable && <th className="p-2"></th>}
			</tr>
		      </thead>
		      <tbody>
			{nicoTags.map (tag => {
			  const isEditing = editingId === tag.id

			  return (
			      <Fragment key={tag.id}>
			    <tr
				key={tag.id}
				className={cn (
				    'border-b border-gray-200 dark:border-gray-700',
				    isEditing
				    ? 'bg-rose-50 dark:bg-rose-950/30'
				    : 'even:bg-gray-100 dark:even:bg-gray-800')}>
			      <td className="p-2 align-top font-semibold">
				<TagLink tag={tag} withWiki={false} withCount={false}/>
			      </td>
			      <td className="p-2 align-top">
				{tag.linkedTags.map ((linkedTag, i) => (
				    <span key={linkedTag.id}>
				      {i > 0 && ' '}
				      <TagLink
					  tag={linkedTag}
					  linkFlg={false}
					  withCount={false}/>
				    </span>))}
			      </td>
			      <td className="p-2 align-top whitespace-nowrap">
				{dateString (tag.createdAt)}
			      </td>
			      <td className="p-2 align-top whitespace-nowrap">
				{tag.recentPostTagCreatedAt && dateString (tag.recentPostTagCreatedAt)}
			      </td>
			      {editable && (
				  <td className="p-2 text-right align-top">
				    {!(isEditing) && (
					<button
					    type="button"
					    onClick={() => beginEdit (tag)}
					    className="inline-flex items-center gap-1 text-sm text-blue-700
						       hover:underline dark:text-blue-300">
					  <Pencil className="size-3.5"/>
					  編集
					</button>)}
				  </td>)}
			    </tr>
			    {isEditing && (
				<tr key={`${ tag.id }-edit`}
				    className="border-b border-rose-200 bg-rose-50 dark:border-rose-900
					       dark:bg-rose-950/30">
				  <td colSpan={editable ? 5 : 4} className="p-3">
				    <div className="space-y-2">
				      <label htmlFor={`nico-links-${ tag.id }`}
					     className="block text-sm font-semibold">
					連携する広場タグ
				      </label>
				      <TextArea
					  id={`nico-links-${ tag.id }`}
					  value={rawTags[tag.id] ?? ''}
					  invalid={(errorsByTagId[tag.id] ?? []).length > 0}
					  className="min-h-24 resize-y"
					  placeholder="タグ名を空白または改行で区切って入力"
					  onChange={e => setRawTags (rawTags => ({
					      ...rawTags,
					      [tag.id]: e.target.value}))}/>
				      <FieldError messages={errorsByTagId[tag.id]}/>
				      <div className="flex justify-end gap-2">
					<button
					    type="button"
					    disabled={savingId === tag.id}
					    onClick={() => cancelEdit (tag)}
					    className="inline-flex items-center gap-1 rounded border
						       border-gray-300 px-3 py-1.5 text-sm
						       disabled:opacity-50 dark:border-gray-700">
					  <X className="size-3.5"/>
					  取消
					</button>
					<button
					    type="button"
					    disabled={savingId === tag.id}
					    onClick={() => saveLinks (tag.id)}
					    className="inline-flex items-center gap-1 rounded bg-rose-700
						       px-3 py-1.5 text-sm text-white disabled:opacity-50">
					  {savingId === tag.id
					   ? <LoaderCircle className="size-3.5 animate-spin"/>
					   : <Check className="size-3.5"/>}
					  保存
					</button>
				      </div>
				    </div>
				  </td>
				</tr>)}
			      </Fragment>)
			})}
		      </tbody>
		    </table>
		  </div>)
	       : <p>結果ないよ（笑）</p>}

	      <Pagination page={page} totalPages={totalPages}/>
	    </div>)}
      </MainArea>)
}

export default NicoTagListPage
