import { DndContext,
	 DragOverlay,
	 MouseSensor,
	 TouchSensor,
	 useDroppable,
	 useSensor,
	 useSensors } from '@dnd-kit/core'
import { restrictToWindowEdges } from '@dnd-kit/modifiers'
import { AnimatePresence, motion } from 'framer-motion'
import { useEffect, useRef, useState } from 'react'

import DraggableDroppableTagRow from '@/components/DraggableDroppableTagRow'
import PrefetchLink from '@/components/PrefetchLink'
import TagLink from '@/components/TagLink'
import TagSearch from '@/components/TagSearch'
import SectionTitle from '@/components/common/SectionTitle'
import SubsectionTitle from '@/components/common/SubsectionTitle'
import SidebarComponent from '@/components/layout/SidebarComponent'
import { toast } from '@/components/ui/use-toast'
import { CATEGORIES } from '@/consts'
import { apiDelete, apiGet, apiPatch, apiPost } from '@/lib/api'

import type { DragEndEvent } from '@dnd-kit/core'
import type { FC, MutableRefObject, ReactNode } from 'react'

import type { Category, Post, Tag } from '@/types'

type TagByCategory = { [key in Category]: Tag[] }


const renderTagTree = (
    tag:              Tag,
    nestLevel:        number,
    path:             string,
    suppressClickRef: MutableRefObject<boolean>,
    parentTagId?:     number,
): ReactNode[] => {
  const key = `${ path }-${ tag.id }`

  const self = (
      <motion.li
	  key={key}
	  layout
	  transition={{ duration: .2, ease: 'easeOut' }}
	  className="mb-1">
	<DraggableDroppableTagRow
	    tag={tag}
	    nestLevel={nestLevel}
	    pathKey={key}
	    parentTagId={parentTagId}
	    suppressClickRef={suppressClickRef}/>
      </motion.li>)

  return [
      self,
      ...((tag.children
	   ?.sort ((a, b) => a.name < b.name ? -1 : 1)
	   .flatMap (child => renderTagTree (child, nestLevel + 1, key, suppressClickRef, tag.id)))
	  ?? [])]
}


const isDescendant = (
    root:     Tag,
    targetId: number,
): boolean => {
  if (!(root.children))
    return false

  for (const c of root.children)
    {
      if (c.id === targetId)
	return true
      if (isDescendant (c, targetId))
	return true
    }

  return false
}


const findTag = (
    byCat:  TagByCategory,
    id:     number,
): Tag | undefined => {
  const walk = (nodes: Tag[]): Tag | undefined => {
    for (const t of nodes)
      {
	if (t.id === id)
	  return t

	const found = t.children ? walk (t.children) : undefined
	if (found)
	  return found
      }

    return undefined
  }

  for (const cat of Object.keys (byCat) as (keyof typeof byCat)[])
    {
      const found = walk (byCat[cat] ?? [])
      if (found)
	return found
    }

  return undefined
}


const buildTagByCategory = (post: Post): TagByCategory => {
  const tagsTmp = { } as TagByCategory

  for (const tag of post.tags)
    {
      if (!(tag.category in tagsTmp))
	tagsTmp[tag.category] = []

      tagsTmp[tag.category].push (tag)
    }

  for (const cat of Object.keys (tagsTmp) as (keyof typeof tagsTmp)[])
    tagsTmp[cat].sort ((a, b) => a.name < b.name ? -1 : 1)

  return tagsTmp
}


const changeCategory = async (
    tagId:    number,
    category: Category,
): Promise<void> => {
  await apiPatch (`/tags/${ tagId }`, { category })
}


const DropSlot = ({ cat }: { cat: Category }) => {
  const { setNodeRef, isOver: over } = useDroppable ({
      id:   `slot:${ cat }`,
      data: { kind: 'slot', cat } })

  return (
      <li ref={setNodeRef} className="h-1">
	{over && <div className="h-0.5 w-full rounded bg-sky-400"/>}
      </li>)
}


type Props = { post: Post | null }


export default (({ post }: Props) => {
  const [activeTagId, setActiveTagId] = useState<number | null> (null)
  const [dragging, setDragging] = useState (false)
  const [saving, setSaving] = useState (false)
  const [tags, setTags] = useState ({ } as TagByCategory)

  const suppressClickRef = useRef (false)

  const sensors = useSensors (
      useSensor (MouseSensor, { activationConstraint: { distance: 6 } }),
      useSensor (TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } }))

  const reloadTags = async (): Promise<void> => {
    if (!(post))
      return

    setTags (buildTagByCategory (await apiGet<Post> (`/posts/${ post.id }`)))
  }

  const onDragEnd = async (e: DragEndEvent) => {
    if (saving)
      return

    const activeKind = e.active.data.current?.kind
    if (activeKind !== 'tag')
      return

    const childId: number | undefined = e.active.data.current?.tagId
    const fromParentId: number | undefined = e.active.data.current?.parentTagId

    const overKind = e.over?.data.current?.kind

    if (childId == null || !(overKind))
      return

    const child = findTag (tags, childId)

    try
    {
      setSaving (true)

      switch (overKind)
      {
      case 'tag':
	{
	  const parentId: number | undefined = e.over?.data.current?.tagId
	  if (parentId == null || childId === parentId)
	    return

	  const parent = findTag (tags, parentId)

	  if (!(child)
	      || !(parent)
	      || isDescendant (child, parentId))
	    return

	  if (fromParentId != null)
	    await apiDelete (`/tags/${ fromParentId }/children/${ childId }`)

	  await apiPost (`/tags/${ parentId }/children/${ childId }`, { })

	  await reloadTags ()
	  toast ({
	      title: '上位タグ対応追加',
	      description: `《${ child?.name }》を《${ parent?.name }》の子タグに設定しました．` })

	  break
	}

      case 'slot':
	{
	  const cat: Category | undefined = e.over?.data.current?.cat
	  if (!(cat) || !(child))
	    return

	  if (child.category !== cat)
	    await changeCategory (childId, cat)

	  if (fromParentId != null)
	    await apiDelete (`/tags/${ fromParentId }/children/${ childId }`)

	  const fromParent = fromParentId == null ? null : findTag (tags, fromParentId)

	  await reloadTags ()
	  toast ({
	      title: '上位タグ対応解除',
	      description: (
		  fromParent
		  ? `《${ child.name }》を《${ fromParent.name }》の子タグから外しました．`
		  : `《${ child.name }》のカテゴリを変更しました．`) })

	  break
	}
      }
    }
    catch
    {
      toast ({ title: '上位タグ対応失敗', description: '独裁者である必要があります．' })
    }
    finally
    {
      setSaving (false)
    }
  }

  const categoryNames: Record<Category, string> = {
      deerjikist: 'ニジラー',
      meme:       '原作・ネタ元・ミーム等',
      character:  'キャラクター',
      general:    '一般',
      material:   '素材',
      meta:       'メタタグ',
      nico:       'ニコニコタグ' }

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

    const tagsTmp = { } as TagByCategory

    for (const tag of post.tags)
      {
	if (!(tag.category in tagsTmp))
	  tagsTmp[tag.category] = []
	tagsTmp[tag.category].push (tag)
      }

    for (const cat of Object.keys (tagsTmp) as (keyof typeof tagsTmp)[])
      tagsTmp[cat].sort ((tagA: Tag, tagB: Tag) => tagA.name < tagB.name ? -1 : 1)

    setTags (tagsTmp)
  }, [post])

  return (
      <SidebarComponent>
	<TagSearch/>
	<DndContext
	    sensors={sensors}
	    onDragStart={e => {
	      if (e.active.data.current?.kind === 'tag')
		setActiveTagId (e.active.data.current?.tagId ?? null)
	      setDragging (true)
	      suppressClickRef.current = true
	      document.body.style.userSelect = 'none'
	      getSelection?.()?.removeAllRanges?.()
	      addEventListener ('click', e => {
		e.preventDefault ()
		e.stopPropagation ()
		suppressClickRef.current = false
	      }, { capture: true, once: true })
	    }}
	    onDragCancel={() => {
	      setActiveTagId (null)
	      setDragging (false)
	      document.body.style.userSelect = ''
	      suppressClickRef.current = false
	    }}
	    onDragEnd={async e => {
	      setActiveTagId (null)
	      setDragging (false)
	      await onDragEnd (e)
	      document.body.style.userSelect = ''
	    }}
	    modifiers={[restrictToWindowEdges]}>
	  <motion.div key={post?.id ?? 0} layout>
	    {CATEGORIES.map ((cat: Category) => ((tags[cat] ?? []).length > 0 || dragging) && (
		<motion.div layout className="my-3" key={cat}>
		  <SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>

		  <motion.ul layout>
		    <AnimatePresence initial={false}>
		      {(tags[cat] ?? []).flatMap (tag => (
			  renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined)))}
		      <DropSlot cat={cat}/>
		    </AnimatePresence>
		  </motion.ul>
		</motion.div>))}
	    {post && (
		<div>
		  <SectionTitle>情報</SectionTitle>
		  <ul>
		    <li>Id.: {post.id}</li>
		    {/* TODO: uploadedUser の取得を対応したらコメント外す */}
		    {/*
		    <li>
		      <>耕作者: </>
		      {post.uploadedUser
		       ? (
			  <PrefetchLink to={`/users/${ post.uploadedUser.id }`}>
			    {post.uploadedUser.name || '名もなきニジラー'}
			  </PrefetchLink>)
		       : 'bot操作'}
		    </li>
		    */}
		    <li>耕作日時: {(new Date (post.createdAt)).toLocaleString ()}</li>
		    <li>
		      <>リンク: </>
		      <a
			  className="break-all"
			  href={post.url}
			  target="_blank"
			  rel="noopener noreferrer nofollow">
			{post.url}
		      </a>
		    </li>
		    <li>
		      {/* TODO: 表示形式きしょすぎるので何とかする */}
		      <>オリジナルの投稿日時: </>
		      {!(post.originalCreatedFrom) && !(post.originalCreatedBefore)
		       ? '不明'
		       : (
			  <>
			    {post.originalCreatedFrom
			     && `${ (new Date (post.originalCreatedFrom)).toLocaleString () } 以降 `}
			    {post.originalCreatedBefore
			     && `${ (new Date (post.originalCreatedBefore)).toLocaleString () } より前`}
			  </>)}
		    </li>
		    <li>
		      <PrefetchLink to={`/posts/changes?id=${ post.id }`}>履歴</PrefetchLink>
		    </li>
		  </ul>
		</div>)}
	  </motion.div>

	  <DragOverlay adjustScale={false}>
	    <div className="pointer-events-none">
	      {activeTagId != null && (() => {
		const tag = findTag (tags, activeTagId)
		return tag && <TagLink tag={tag}/>
	      }) ()}
	    </div>
	  </DragOverlay>
	</DndContext>
      </SidebarComponent>)
}) satisfies FC<Props>
