import {
    createContext,
    useCallback,
    useContext,
    useEffect,
    useMemo,
    useRef,
    useState,
} from 'react'
import { useBlocker } from 'react-router-dom'

import { useDialogue } from '@/lib/dialogues/useDialogue'

import type { FC, PropsWithChildren } from 'react'

type UnsavedChangesSource = {
    dirty:    boolean
    discard:  () => void | Promise<void> }

type UseUnsavedChangesGuardOptions = {
    dirty:      boolean
    onDiscard?: () => void | Promise<void> }

type UnsavedChangesGuardContextValue = {
    hasUnsavedChanges:            boolean
    registerUnsavedChangesSource: (
        source: UnsavedChangesSource,
    ) => () => void
    allowNextNavigation:          () => void }

const UnsavedChangesGuardContext =
    createContext<UnsavedChangesGuardContextValue | null> (null)


export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children }) => {
  const dialogue = useDialogue ()
  const sourcesRef = useRef (new Map<symbol, UnsavedChangesSource> ())
  const bypassNextNavigationRef = useRef<symbol | null> (null)
  const [revision, setRevision] = useState (0)
  const dialogueOpenRef = useRef (false)
  const handlingBlockedTransitionRef = useRef (false)

  const registerUnsavedChangesSource = useCallback ((
      source: UnsavedChangesSource,
  ): (() => void) => {
    const sourceId = Symbol ('unsaved-changes-source')
    sourcesRef.current.set (sourceId, source)
    setRevision (current => current + 1)

    return () => {
      if (!(sourcesRef.current.delete (sourceId)))
        return

      setRevision (current => current + 1)
    }
  }, [])

  const allowNextNavigation = useCallback (() => {
    const token = Symbol ('allowed-navigation')
    bypassNextNavigationRef.current = token

    queueMicrotask (() => {
      if (bypassNextNavigationRef.current === token)
        bypassNextNavigationRef.current = null
    })
  }, [])

  const sources = useMemo (
      () => [...sourcesRef.current.values ()],
      [revision],
  )
  const dirtySources = useMemo (
      () => sources.filter (source => source.dirty),
      [sources],
  )
  const hasUnsavedChanges = dirtySources.length > 0
  const hasUnsavedChangesRef = useRef (hasUnsavedChanges)
  hasUnsavedChangesRef.current = hasUnsavedChanges
  const shouldBlock = useCallback (() => {
    if (bypassNextNavigationRef.current != null)
      {
        bypassNextNavigationRef.current = null
        return false
      }

    return hasUnsavedChangesRef.current
  }, [])
  const blocker = useBlocker (shouldBlock)

  const confirmDiscardChanges = useCallback (async (): Promise<boolean> => {
    if (!(hasUnsavedChanges))
      return true

    if (dialogueOpenRef.current)
      return false

    dialogueOpenRef.current = true

    try
    {
      const confirmed = await dialogue.confirm ({
          title: '変更が破棄してページ移動しますか?',
          confirmText: '変更を破棄して移動',
          variant: 'danger' })
      if (!(confirmed))
        return false

      return true
    }
    finally
    {
      dialogueOpenRef.current = false
    }
  }, [dialogue, hasUnsavedChanges])

  useEffect (() => {
    if (blocker.state !== 'blocked' || handlingBlockedTransitionRef.current)
      return

    handlingBlockedTransitionRef.current = true
    void (async () => {
      try
      {
        const confirmed = await confirmDiscardChanges ()
        if (!(confirmed))
          {
            blocker.reset ()
            return
          }

        let discardResults: Array<void | Promise<void>>
        try
        {
	  discardResults = dirtySources.map (source => source.discard ())
        }
        catch
        {
	  blocker.reset ()
	  return
        }

        blocker.proceed ()
        await Promise.allSettled (discardResults)
      }
      finally
      {
        handlingBlockedTransitionRef.current = false
      }
    }) ()
  }, [blocker, confirmDiscardChanges, dirtySources])

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

    const handleBeforeUnload = (event: BeforeUnloadEvent) => {
      event.preventDefault ()
      event.returnValue = ''
    }

    window.addEventListener ('beforeunload', handleBeforeUnload)
    return () => {
      window.removeEventListener ('beforeunload', handleBeforeUnload)
    }
  }, [hasUnsavedChanges])

  const value = useMemo<UnsavedChangesGuardContextValue> (() => ({
      hasUnsavedChanges,
      registerUnsavedChangesSource,
      allowNextNavigation,
  }), [
      allowNextNavigation,
      hasUnsavedChanges,
      registerUnsavedChangesSource,
  ])

  return (
      <UnsavedChangesGuardContext.Provider value={value}>
        {children}
      </UnsavedChangesGuardContext.Provider>)
}


export const useUnsavedChangesGuard = (
    options?: UseUnsavedChangesGuardOptions,
): UnsavedChangesGuardContextValue => {
  const context = useContext (UnsavedChangesGuardContext)

  if (context == null)
    throw new Error ('UnsavedChangesGuardProvider が必要です．')

  const { registerUnsavedChangesSource } = context

  useEffect (() => {
    if (options == null)
      return

    return registerUnsavedChangesSource ({
        dirty: options.dirty,
        discard: options.onDiscard ?? (() => undefined) })
  }, [options?.dirty, options?.onDiscard, registerUnsavedChangesSource])

  return context
}
