import { DndContext,
	 DragOverlay,
	 MouseSensor,
	 TouchSensor,
	 useDroppable,
	 useSensor,
	 useSensors } from '@dnd-kit/core'
import { restrictToWindowEdges } from '@dnd-kit/modifiers'
import { useQueryClient } from '@tanstack/react-query'
import { motion } from 'framer-motion'
import { useEffect, useMemo, 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, CATEGORY_NAMES } from '@/consts'
import { apiDelete, apiGet, apiPatch, apiPost } from '@/lib/api'
import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { dateString, originalCreatedAtString } from '@/lib/utils'

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

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

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


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

  const self = (
      <li key={key} className="mb-1">
	<DraggableDroppableTagRow
	    tag={tag}
	    nestLevel={nestLevel}
	    pathKey={key}
	    parentTagId={parentTagId}
	    suppressClickRef={suppressClickRef}
	    sp={sp}/>
      </li>)

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


const isDescendant = (
    root:     TagWithSections,
    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,
): TagWithSections | undefined => {
  const walk = (nodes: TagWithSections[]): TagWithSections | 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; sp?: boolean }


const TagDetailSidebar: FC<Props> = ({ post, sp }) => {
  sp = Boolean (sp)

  const qc = useQueryClient ()

  const baseTags = useMemo<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 ((tagA: TagWithSections, tagB: TagWithSections) => tagA.name < tagB.name ? -1 : 1)

    return tagsTmp
  }, [post])

  const [activeTagId, setActiveTagId] = useState<number | null> (null)
  const [dragging, setDragging] = useState (false)
  const [saving, setSaving] = useState (false)
  const [tags, setTags] = useState (baseTags)

  const suppressClickRef = useRef (false)

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

  const reloadTags = async (): Promise<void> => {
    setTags (buildTagByCategory (await apiGet<Post> (`/posts/${ post.id }`)))
    qc.invalidateQueries ({ queryKey: postsKeys.root })
    qc.invalidateQueries ({ queryKey: tagsKeys.root })
  }

  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)
    }
  }

  useEffect (() => {
    setTags (baseTags)
  }, [baseTags])

  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]}>
	  {CATEGORIES.map ((cat: Category) => ((tags[cat] ?? []).length > 0 || dragging) && (
	      <div className="my-3" key={cat}>
		<SubsectionTitle>
		  <motion.div
		      layoutId={`tag-${ sp ? 'sp-' : '' }${ cat }`}
		      transition={{ layout: { duration: .2, ease: 'easeOut' } }}>
		    {CATEGORY_NAMES[cat]}
		  </motion.div>
		</SubsectionTitle>

		<ul>
		  {(tags[cat] ?? []).flatMap (tag => (
		      renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined, sp)))}
		  <DropSlot cat={cat}/>
		</ul>
	      </div>))}
	  {post && (
	      <motion.div
		  layoutId={`post-info-${ sp }`}
		  transition={{ layout: { duration: .2, ease: 'easeOut' } }}>
		<SectionTitle>情報</SectionTitle>
		<ul>
		  <li>Id.: {post.id}</li>
		  <li>
		    <>耕作者: </>
		    {post.uploadedUser
		     ? (
			<PrefetchLink to={`/users/${ post.uploadedUser.id }`}>
			  {post.uploadedUser.name || '名もなきニジラー'}
			</PrefetchLink>)
		     : 'bot操作'}
		  </li>
		  <li>耕作日時: {dateString (post.createdAt)}</li>
		  <li>
		    <>リンク: </>
		    <a
			className="break-all"
			href={post.url}
			target="_blank"
			rel="noopener noreferrer nofollow">
		      {post.url}
		    </a>
		  </li>
		  <li>
		    <>オリジナルの投稿日時: </>
		    {originalCreatedAtString (post.originalCreatedFrom,
					      post.originalCreatedBefore)}
		  </li>
		  <li>
		    <PrefetchLink to={`/posts/changes?id=${ post.id }`}>
		      履歴
		    </PrefetchLink>
		  </li>
		</ul>
	      </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>)
}

export default TagDetailSidebar
