import { DndContext,
	 DragOverlay,
	 MeasuringStrategy,
	 MouseSensor,
	 TouchSensor,
	 pointerWithin,
	 useDroppable,
	 useSensor,
	 useSensors } from '@dnd-kit/core'
import { restrictToWindowEdges, snapCenterToCursor } 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 { clientAnimationTransition } from '@/lib/clientAnimation'
import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { dateString, originalCreatedAtString } from '@/lib/utils'

import type { CollisionDetection, 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 tagCollisionDetection: CollisionDetection = args => {
  return pointerWithin (args)
}

const alwaysMeasureDroppables = {
    droppable: { strategy: MeasuringStrategy.Always } } as const


const renderTagTree = (
    activeDndId:       string | undefined,
    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
	    activeDndId={activeDndId}
	    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 (
		  activeDndId,
		  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 buildFlatTagByCategory = (
    byCategory: TagByCategory,
): TagByCategory => {
  const tagsTmp = { } as TagByCategory
  const seen = new Set<number> ()

  for (const category of CATEGORIES)
    tagsTmp[category] = []

  const visit = (tag: TagWithSections) => {
    if (seen.has (tag.id))
      return

    seen.add (tag.id)
    tagsTmp[tag.category].push ({ ...tag, children: [] })

    for (const child of tag.children ?? [])
      visit (child)
  }

  for (const category of CATEGORIES)
    for (const tag of byCategory[category] ?? [])
      visit (tag)

  for (const category of CATEGORIES)
    tagsTmp[category].sort ((tagA, tagB) => tagA.name < tagB.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>)
}

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

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


type Props = {
    className?: string
    post:       Post
    sp?:        boolean }

type ActiveTagDrag = {
    dndId:       string
    tagId:      number
    nestLevel:  number }


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

  const qc = useQueryClient ()
  const behaviourSettings = useClientBehaviourSettings ()
  const tagRelationDisplay = behaviourSettings.tagRelationDisplay ?? 'grouped'
  const animationMode = behaviourSettings.animation ?? 'normal'
  const layoutTransition = clientAnimationTransition (
      animationMode,
      { normal: { duration: .2, ease: 'easeOut' as const } },
  )

  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 [activeTagDrag, setActiveTagDrag] = useState<ActiveTagDrag | null> (null)
  const [dragging, setDragging] = useState (false)
  const [saving, setSaving] = useState (false)
  const [tags, setTags] = useState (baseTags)
  const flatTagsByCategory = useMemo<TagByCategory> (
      () => buildFlatTagByCategory (tags),
      [tags],
  )
  const activeDndId = activeTagDrag?.dndId

  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 sidebarKey="post-detail-tags" className={className}>
	<TagSearch/>
	<DndContext
	    sensors={sensors}
	    collisionDetection={tagCollisionDetection}
	    measuring={alwaysMeasureDroppables}
	    onDragStart={e => {
	      if (e.active.data.current?.kind === 'tag')
		{
		  const tagId = e.active.data.current?.tagId
		  const nestLevel = e.active.data.current?.nestLevel
		  const dndId = e.active.data.current?.dndId
		  setActiveTagDrag (
		      tagId == null || nestLevel == null || dndId == null
		      ? null
		      : { dndId, tagId, nestLevel })
		}
	      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={() => {
	      setActiveTagDrag (null)
	      setDragging (false)
	      document.body.style.userSelect = ''
	      suppressClickRef.current = false
	    }}
	    onDragEnd={async e => {
	      setActiveTagDrag (null)
	      setDragging (false)
	      await onDragEnd (e)
	      document.body.style.userSelect = ''
	    }}
	    modifiers={[restrictToWindowEdges]}>
	  {CATEGORIES.map ((cat: Category) => {
	    const categoryTags =
		tagRelationDisplay === 'grouped'
		? (tags[cat] ?? [])
		: (flatTagsByCategory[cat] ?? [])

	    if (!(categoryTags.length > 0 || dragging))
	      return null

	    const sectionTitle = (
		<SubsectionTitle>
		  <motion.div
		      layoutId={animationMode === 'off'
			  ? undefined
			  : `tag-${ sp ? 'sp-' : '' }${ cat }`}
		      transition={{ layout: layoutTransition }}>
		    {CATEGORY_NAMES[cat]}
		  </motion.div>
		</SubsectionTitle>)

	    if (!(categoryTags.length > 0))
	      return (
		  <EmptyCategoryDropSection cat={cat} key={cat}>
		    {sectionTitle}
		  </EmptyCategoryDropSection>)

	    return (
		<div className="my-3" key={cat}>
		  {sectionTitle}
		  <ul>
		    {(tagRelationDisplay === 'grouped'
		      ? (tags[cat] ?? []).flatMap (tag =>
			    renderTagTree (
				activeDndId,
				tag,
				0,
				`cat-${ cat }`,
				suppressClickRef,
				undefined,
				sp,
			    ),
			)
		      : (flatTagsByCategory[cat] ?? []).map (tag => (
			  <li key={`flat-${ cat }-${ tag.id }`} className="mb-1">
			    <DraggableDroppableTagRow
				activeDndId={activeDndId}
				tag={tag}
				nestLevel={0}
				pathKey={`flat-${ cat }-${ tag.id }`}
				suppressClickRef={suppressClickRef}
				sp={sp}/>
			  </li>)))}
		    <DropSlot cat={cat}/>
		  </ul>
		</div>)
	  })}
	  {post && (
	      <motion.div
		  layoutId={animationMode === 'off'
		      ? undefined
		      : `post-info-${ sp }`}
		  transition={{ layout: layoutTransition }}>
		<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}
	      modifiers={[snapCenterToCursor]}
	      dropAnimation={animationMode === 'off' ? null : undefined}>
	    <div className="pointer-events-none">
	      {activeTagDrag != null && (() => {
		const tag = findTag (tags, activeTagDrag.tagId)
		return tag && <TagLink tag={tag} nestLevel={activeTagDrag.nestLevel}/>
	      }) ()}
	    </div>
	  </DragOverlay>
	</DndContext>
      </SidebarComponent>)
}

export default TagDetailSidebar
