import { useEffect, useMemo, useReducer, useRef, type ReactNode } from 'react'
import { EditorContent, useEditor, type Editor } from '@tiptap/react'
import { DefaultToolbar, type RichTextEditorToolbarState } from './DefaultToolbar'
import {
  createDefaultExtensions,
  type EditorExtensionConfig,
} from './extensions'

export interface RichTextEditorProps {
  value: string
  onChange: (next: string) => void
  dir?: 'ltr' | 'rtl'
  'aria-label': string                   // a11y HARD FLOOR — REQUIRED: editor sets role=textbox, so an accessible name is mandatory (WCAG 4.1.2)
  'aria-describedby'?: string
  extensions?: EditorExtensionConfig
  readOnly?: boolean
  className?: string
  renderToolbar?: (state: RichTextEditorToolbarState) => ReactNode
}

function useToolbarState(editor: Editor | null): RichTextEditorToolbarState {
  const [, rerender] = useReducer((n: number) => n + 1, 0)

  useEffect(() => {
    if (!editor) return
    const update = () => rerender()
    editor.on('selectionUpdate', update)
    editor.on('transaction', update)
    return () => {
      editor.off('selectionUpdate', update)
      editor.off('transaction', update)
    }
  }, [editor])

  return {
    editor,
    isBold: editor?.isActive('bold') ?? false,
    isItalic: editor?.isActive('italic') ?? false,
    isH2: editor?.isActive('heading', { level: 2 }) ?? false,
    isBullet: editor?.isActive('bulletList') ?? false,
  }
}

function sameOptionValue(left: unknown, right: unknown, seen = new WeakMap<object, object>()): boolean {
  if (Object.is(left, right)) return true
  if (typeof left !== 'object' || left === null || typeof right !== 'object' || right === null) return false
  if (Object.getPrototypeOf(left) !== Object.getPrototypeOf(right)) return false
  if (seen.get(left) === right) return true
  seen.set(left, right)
  const leftKeys = Object.keys(left as Record<string, unknown>)
  const rightKeys = Object.keys(right as Record<string, unknown>)
  if (leftKeys.length !== rightKeys.length) return false
  return leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key)
    && sameOptionValue(
      (left as Record<string, unknown>)[key],
      (right as Record<string, unknown>)[key],
      seen,
    ))
}

function sameExtensionImplementation(
  left: object,
  right: object,
): boolean {
  const leftKeys = Object.keys(left)
  const rightKeys = Object.keys(right)
  if (leftKeys.length !== rightKeys.length) return false

  return leftKeys.every((key) => {
    if (!Object.prototype.hasOwnProperty.call(right, key)) return false
    const leftValue = Reflect.get(left, key) as unknown
    const rightValue = Reflect.get(right, key) as unknown
    if (typeof leftValue === 'function' || typeof rightValue === 'function') {
      return typeof leftValue === 'function'
        && typeof rightValue === 'function'
        && Function.prototype.toString.call(leftValue) === Function.prototype.toString.call(rightValue)
    }
    return sameOptionValue(leftValue, rightValue)
  })
}

function sameExtensionConfig(
  left: EditorExtensionConfig | undefined,
  right: EditorExtensionConfig | undefined,
): boolean {
  if (left === right) return true
  if (!left || !right || left.extensions.length !== right.extensions.length) return false
  return left.extensions.every((extension, index) => {
    const candidate = right.extensions[index]
    return candidate !== undefined
      && extension.name === candidate.name
      && sameOptionValue(extension.options, candidate.options)
      && sameExtensionImplementation(extension.config, candidate.config)
  })
}

function useStableExtensionConfig(config: EditorExtensionConfig | undefined): EditorExtensionConfig | undefined {
  const stable = useRef(config)
  if (!sameExtensionConfig(stable.current, config)) stable.current = config
  return stable.current
}

export function RichTextEditor({
  value,
  onChange,
  dir = 'ltr',
  'aria-label': ariaLabel,
  'aria-describedby': ariaDescribedBy,
  extensions,
  readOnly = false,
  className,
  renderToolbar,
}: RichTextEditorProps) {
  const stableExtensions = useStableExtensionConfig(extensions)
  const resolvedExtensions = useMemo(
    () => stableExtensions?.extensions ?? createDefaultExtensions(),
    [stableExtensions],
  )
  const onChangeRef = useRef(onChange)
  const synchronizingRef = useRef(false)
  const lastEmittedRef = useRef<string | null>(null)
  const previousValueRef = useRef(value)
  const [documentRevision, resetDocumentState] = useReducer((revision: number) => revision + 1, 0)
  onChangeRef.current = onChange

  const editor = useEditor(
    {
      extensions: resolvedExtensions,
      content: value,
      editable: !readOnly,
      immediatelyRender: false,
      onUpdate: ({ editor: ed }) => {
        if (synchronizingRef.current) return
        const nextHtml = ed.getHTML()
        lastEmittedRef.current = nextHtml
        onChangeRef.current(nextHtml)
      },
      editorProps: {
        attributes: {
          dir,
          role: 'textbox',
          'aria-multiline': 'true',
          'aria-label': ariaLabel,
          ...(ariaDescribedBy ? { 'aria-describedby': ariaDescribedBy } : {}),
          'aria-readonly': String(readOnly),
        },
      },
    },
    [resolvedExtensions, documentRevision],
  )

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

    const lastEmitted = lastEmittedRef.current
    const propChanged = value !== previousValueRef.current
    const acceptedUserEcho = lastEmitted !== null
      && value === lastEmitted
      && value === editor.getHTML()
    previousValueRef.current = value

    if (acceptedUserEcho) {
      lastEmittedRef.current = null
      return
    }
    if (lastEmitted === null && !propChanged) return

    lastEmittedRef.current = null
    synchronizingRef.current = true
    try {
      editor.chain().setMeta('addToHistory', false).setContent(value, { emitUpdate: false }).run()
    } finally {
      synchronizingRef.current = false
    }

    resetDocumentState()
  })

  useEffect(() => {
    if (!editor) return
    editor.setEditable(!readOnly, false)
  }, [editor, readOnly])

  useEffect(() => {
    if (!editor) return
    const root = editor.view.dom as HTMLElement
    root.setAttribute('dir', dir)
    root.setAttribute('aria-label', ariaLabel)
    root.setAttribute('aria-readonly', String(readOnly))
    if (ariaDescribedBy) root.setAttribute('aria-describedby', ariaDescribedBy)
    else root.removeAttribute('aria-describedby')
  }, [editor, dir, ariaLabel, ariaDescribedBy, readOnly])

  const toolbarState = useToolbarState(editor)

  return (
    <div className={className}>
      {renderToolbar ? renderToolbar(toolbarState) : <DefaultToolbar {...toolbarState} />}
      <EditorContent editor={editor} />
    </div>
  )
}
