'use client'

import * as React from 'react'
import type { ContentEditorSelectionRequest } from '@admin/utils/editor/content-editor-selection-request'
import type { MarkdownHeadingLevel } from '@admin/utils/editor/markdown-heading-level'
import type { MarkdownListType } from '@admin/utils/editor/markdown-list-type'
import type { ViewUpdate } from '@codemirror/view'
import type { QueryClient } from '@tanstack/react-query'
import type { ReactCodeMirrorRef } from '@uiw/react-codemirror'
import type { Root } from 'react-dom/client'
import { MediaField } from '@admin/components/custom/media-field'
import { applyCursorChange } from '@admin/utils/editor/apply-cursor-change'
import { applyCursorChangeIfAvailable } from '@admin/utils/editor/apply-cursor-change-if-available'
import { createMarkdownTableText } from '@admin/utils/editor/create-markdown-table-text'
import { createMarkdownTextFromUrl } from '@admin/utils/editor/create-markdown-text-from-url'
import { findMarkdownLinkRange } from '@admin/utils/editor/find-markdown-link-range'
import { formatMarkdownBlockquoteLine } from '@admin/utils/editor/format-markdown-blockquote-line'
import { formatMarkdownHeadingLine } from '@admin/utils/editor/format-markdown-heading-line'
import { formatMarkdownListLine } from '@admin/utils/editor/format-markdown-list-line'
import { getMarkdownHeadingFallback } from '@admin/utils/editor/get-markdown-heading-fallback'
import { getMarkdownListFallback } from '@admin/utils/editor/get-markdown-list-fallback'
import { insertComponentAtCursor } from '@admin/utils/editor/insert-component-at-cursor'
import { insertMarkdownTextAtPosition } from '@admin/utils/editor/insert-markdown-text-at-position'
import { insertTextAtCursor } from '@admin/utils/editor/insert-text-at-cursor'
import { parseContentEditorMathInput } from '@admin/utils/editor/parse-content-editor-math-input'
import { removeHighlightSpanAtCursor } from '@admin/utils/editor/remove-highlight-span-at-cursor'
import { removeMarkdownLinkAtCursor } from '@admin/utils/editor/remove-markdown-link-at-cursor'
import { sanitizeUrl } from '@admin/utils/editor/sanitize-url'
import { setMarkdownLinkAtCursor } from '@admin/utils/editor/set-markdown-link-at-cursor'
import { transformSelectedMarkdownLines } from '@admin/utils/editor/transform-selected-markdown-lines'
import { redo, redoDepth, undo, undoDepth } from '@codemirror/commands'
import { markdown } from '@codemirror/lang-markdown'
import { Decoration, EditorView, WidgetType } from '@codemirror/view'
import { QueryClientProvider, useQueryClient } from '@tanstack/react-query'
import { githubLightInit } from '@uiw/codemirror-theme-github'
import { NuqsAdapter } from 'nuqs/adapters/react'
import { createRoot } from 'react-dom/client'

const ADMIN_MONO_FONT_FAMILY = 'var(--font-geist-mono), ui-monospace, SFMono-Regular, monospace'
const editorTheme = githubLightInit({ settings: { fontFamily: ADMIN_MONO_FONT_FAMILY } })

type MarkdownMediaGalleryWidgetProps = {
  widgetId: number
  disabled: boolean
  queryClient: QueryClient
  onChange: (url: string) => void
  onRemove: () => void
}

const markdownMediaGalleryRoots = new WeakMap<HTMLElement, Root>()

class MarkdownMediaGalleryWidget extends WidgetType {
  constructor(private readonly props: MarkdownMediaGalleryWidgetProps) {
    super()
  }

  eq(other: WidgetType) {
    if (!(other instanceof MarkdownMediaGalleryWidget)) return false

    return (
      this.props.widgetId === other.props.widgetId &&
      this.props.disabled === other.props.disabled &&
      this.props.queryClient === other.props.queryClient &&
      this.props.onChange === other.props.onChange &&
      this.props.onRemove === other.props.onRemove
    )
  }

  toDOM() {
    const container = document.createElement('div')
    container.className = 'cm-markdown-media-gallery'
    const root = createRoot(container)
    markdownMediaGalleryRoots.set(container, root)
    root.render(
      <QueryClientProvider client={this.props.queryClient}>
        <NuqsAdapter>
          <MediaField
            value=""
            valueMode="url"
            accept="image/*"
            disabled={this.props.disabled}
            isRemovable
            onChange={this.props.onChange}
            onRemove={this.props.onRemove}
          />
        </NuqsAdapter>
      </QueryClientProvider>
    )
    return container
  }

  updateDOM(container: HTMLElement) {
    const root = markdownMediaGalleryRoots.get(container)
    if (!root) return false

    root.render(
      <QueryClientProvider client={this.props.queryClient}>
        <NuqsAdapter>
          <MediaField
            value=""
            valueMode="url"
            accept="image/*"
            disabled={this.props.disabled}
            isRemovable
            onChange={this.props.onChange}
            onRemove={this.props.onRemove}
          />
        </NuqsAdapter>
      </QueryClientProvider>
    )
    return true
  }

  destroy(container: HTMLElement) {
    const root = markdownMediaGalleryRoots.get(container)
    markdownMediaGalleryRoots.delete(container)
    if (root) {
      queueMicrotask(() => root.unmount())
    }
  }

  ignoreEvent() {
    return false
  }
}

export interface UseContentEditorSourceModeOptions {
  value: string
  onChange?: (value: string) => void
  disabled: boolean
  selectionRequest?: ContentEditorSelectionRequest | null
  onSelectionChange?: (offset: number) => void
}

export function useContentEditorSourceMode({
  value,
  onChange,
  disabled,
  selectionRequest,
  onSelectionChange
}: UseContentEditorSourceModeOptions) {
  const editorRef = React.useRef<ReactCodeMirrorRef>(null)
  const queryClient = useQueryClient()
  const mediaGalleryAnchorRef = React.useRef<number | null>(null)
  const [mediaGalleryAnchor, setMediaGalleryAnchor] = React.useState<{
    id: number
    pos: number
  } | null>(null)
  const [markdownLinkUrl, setMarkdownLinkUrl] = React.useState<string | null>(null)
  const [isMarkdownLinkActive, setIsMarkdownLinkActive] = React.useState(false)
  const [canUndoMarkdownChange, setCanUndoMarkdownChange] = React.useState(false)
  const [canRedoMarkdownChange, setCanRedoMarkdownChange] = React.useState(false)
  const valueRef = React.useRef(value)
  valueRef.current = value
  const onChangeRef = React.useRef(onChange)
  onChangeRef.current = onChange

  const handleMediaSelected = React.useCallback(
    (url: string) => {
      if (disabled || !url) return

      const view = editorRef.current?.view ?? null
      const currentValue = view?.state.doc.toString() ?? valueRef.current ?? ''
      const markdownImages = createMarkdownTextFromUrl(url)
      const requestedPos =
        mediaGalleryAnchorRef.current ?? view?.state.selection.main.from ?? currentValue.length
      const { newValue, newCursorPos } = insertMarkdownTextAtPosition(
        currentValue,
        markdownImages,
        requestedPos
      )

      if (view) {
        view.dispatch({
          changes: {
            from: newCursorPos - markdownImages.length,
            insert: markdownImages
          },
          selection: { anchor: newCursorPos }
        })
      }

      onChangeRef.current?.(newValue)
      mediaGalleryAnchorRef.current = null
      setMediaGalleryAnchor(null)
      if (view) {
        applyCursorChange(view, newCursorPos)
      }
    },
    [disabled]
  )

  const handleMediaGalleryRemove = React.useCallback(() => {
    const view = editorRef.current?.view ?? null
    const cursorPos = mediaGalleryAnchorRef.current ?? view?.state.selection.main.from ?? 0

    mediaGalleryAnchorRef.current = null
    setMediaGalleryAnchor(null)
    if (view) {
      applyCursorChange(view, cursorPos)
    }
  }, [])

  const getCurrentMarkdownLink = React.useCallback(() => {
    const view = editorRef.current?.view ?? null
    if (!view) return null

    const markdown = view.state.doc.toString()
    const { from, to } = view.state.selection.main

    return findMarkdownLinkRange(markdown, from, to)
  }, [])

  const syncMarkdownLinkState = React.useCallback((view: EditorView | null) => {
    if (!view) {
      setIsMarkdownLinkActive(false)
      return
    }

    const markdown = view.state.doc.toString()
    const { from, to } = view.state.selection.main
    const activeLink = findMarkdownLinkRange(markdown, from, to)

    setIsMarkdownLinkActive(Boolean(activeLink))
    if (activeLink) {
      setMarkdownLinkUrl(activeLink.url)
    }
  }, [])

  const undoMarkdownChange = React.useCallback(() => {
    const view = editorRef.current?.view
    if (view) undo(view)
  }, [])

  const redoMarkdownChange = React.useCallback(() => {
    const view = editorRef.current?.view
    if (view) redo(view)
  }, [])

  const focusSourceSelection = React.useCallback(
    (view: EditorView, offset: number) => {
      const selectionOffset = Math.max(0, Math.min(offset, view.state.doc.length))

      view.focus()
      view.dispatch({
        selection: { anchor: selectionOffset },
        effects: EditorView.scrollIntoView(selectionOffset, {
          y: 'center',
          yMargin: 48
        })
      })
      onSelectionChange?.(selectionOffset)
    },
    [onSelectionChange]
  )

  const handleCreateEditor = React.useCallback(
    (view: EditorView) => {
      if (!selectionRequest) return
      focusSourceSelection(view, selectionRequest.offset)
    },
    [focusSourceSelection, selectionRequest]
  )

  React.useEffect(() => {
    const view = editorRef.current?.view
    if (!view || !selectionRequest) return
    focusSourceSelection(view, selectionRequest.offset)
  }, [focusSourceSelection, selectionRequest])

  const openMediaGalleryAtCursor = React.useCallback(() => {
    if (disabled) return

    const view = editorRef.current?.view ?? null
    const pos = view?.state.selection.main.from ?? (value || '').length
    mediaGalleryAnchorRef.current = pos
    setMediaGalleryAnchor((current) => ({
      id: (current?.id ?? 0) + 1,
      pos
    }))
  }, [disabled, value])

  const handleMarkdownLinkPopoverOpenChange = React.useCallback(
    (isOpen: boolean) => {
      if (!isOpen) return

      const activeLink = getCurrentMarkdownLink()
      setMarkdownLinkUrl(activeLink?.url ?? '')
      setIsMarkdownLinkActive(Boolean(activeLink))
    },
    [getCurrentMarkdownLink]
  )

  const handleEditorChange = React.useCallback(
    (nextValue: string, viewUpdate: ViewUpdate) => {
      const anchor = mediaGalleryAnchorRef.current

      if (anchor !== null && viewUpdate.docChanged) {
        const mappedAnchor = viewUpdate.changes.mapPos(anchor)
        mediaGalleryAnchorRef.current = mappedAnchor
        setMediaGalleryAnchor((current) => (current ? { ...current, pos: mappedAnchor } : current))
      }

      onSelectionChange?.(viewUpdate.state.selection.main.from)
      onChange?.(nextValue)
    },
    [onChange, onSelectionChange]
  )

  const handleEditorUpdate = React.useCallback(
    (viewUpdate: ViewUpdate) => {
      setCanUndoMarkdownChange(undoDepth(viewUpdate.state) > 0)
      setCanRedoMarkdownChange(redoDepth(viewUpdate.state) > 0)

      if (!viewUpdate.selectionSet) return
      syncMarkdownLinkState(viewUpdate.view)
      onSelectionChange?.(viewUpdate.state.selection.main.from)
    },
    [onSelectionChange, syncMarkdownLinkState]
  )

  const mediaGalleryExtensions = React.useMemo(() => {
    if (!mediaGalleryAnchor) return []

    return [
      EditorView.decorations.of(
        Decoration.set([
          Decoration.widget({
            widget: new MarkdownMediaGalleryWidget({
              widgetId: mediaGalleryAnchor.id,
              disabled,
              queryClient,
              onChange: handleMediaSelected,
              onRemove: handleMediaGalleryRemove
            }),
            block: true,
            side: 1
          }).range(mediaGalleryAnchor.pos)
        ])
      )
    ]
  }, [disabled, handleMediaGalleryRemove, handleMediaSelected, mediaGalleryAnchor, queryClient])

  const codeMirrorExtensions = React.useMemo(
    () => [
      markdown(),
      EditorView.lineWrapping,
      EditorView.updateListener.of(handleEditorUpdate),
      EditorView.theme({
        '&': {
          backgroundColor: 'var(--background)',
          color: 'var(--foreground)',
          display: 'flex',
          flex: '1',
          fontFamily: ADMIN_MONO_FONT_FAMILY,
          fontSize: '0.875rem',
          fontWeight: '400',
          lineHeight: '1.6',
          minHeight: '0',
          overflow: 'hidden'
        },
        '& .cm-content': {
          backgroundColor: 'transparent',
          caretColor: 'var(--foreground)',
          color: 'var(--foreground)',
          fontFamily: ADMIN_MONO_FONT_FAMILY,
          minHeight: '100%',
          padding: '1.5rem'
        },
        '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, & .cm-selectionLayer .cm-selectionBackground':
          {
            background: 'color-mix(in oklab, var(--primary) 20%, transparent) !important'
          },
        '& .cm-line': {
          padding: '0'
        },
        '& .cm-placeholder': {
          color: 'var(--muted-foreground)'
        },
        '& .cm-gutters': {
          backgroundColor: 'var(--background)',
          borderColor: 'var(--border)',
          color: 'var(--muted-foreground)',
          fontFamily: ADMIN_MONO_FONT_FAMILY
        },
        '& .cm-scroller': {
          backgroundColor: 'var(--background)',
          flex: '1',
          fontFamily: ADMIN_MONO_FONT_FAMILY,
          lineHeight: '1.6',
          minHeight: '0',
          overflow: 'auto'
        }
      }),
      ...mediaGalleryExtensions
    ],
    [handleEditorUpdate, mediaGalleryExtensions]
  )

  const insertInline = React.useCallback(
    (before: string, after: string, placeholder: string) => {
      if (disabled) return

      const view = editorRef.current?.view ?? null
      const currentValue = value || ''
      const { newValue, newCursorPos } = insertTextAtCursor(view, currentValue, {
        before,
        after,
        placeholder
      })

      onChange?.(newValue)
      applyCursorChangeIfAvailable(view, newCursorPos)
    },
    [disabled, value, onChange]
  )

  const insertBlock = React.useCallback(
    (block: string) => {
      if (disabled) return

      const view = editorRef.current?.view ?? null
      const currentValue = value || ''
      const { newValue, newCursorPos } = insertComponentAtCursor(view, currentValue, block)

      onChange?.(newValue)
      applyCursorChangeIfAvailable(view, newCursorPos)
    },
    [disabled, value, onChange]
  )

  const insertCodeBlock = React.useCallback(() => {
    insertInline('```\n', '\n```', 'code')
  }, [insertInline])

  const insertDivider = React.useCallback(() => {
    insertBlock('---')
  }, [insertBlock])

  const insertTable = React.useCallback(
    ({ cols, rows }: { cols: number; rows: number }) => {
      if (disabled) return

      const view = editorRef.current?.view ?? null
      const currentValue = view?.state.doc.toString() ?? valueRef.current ?? ''
      const table = createMarkdownTableText(rows, cols)
      const { newValue, newCursorPos } = insertComponentAtCursor(view, currentValue, table)

      onChangeRef.current?.(newValue)
      applyCursorChangeIfAvailable(view, newCursorPos)
    },
    [disabled]
  )

  const insertMath = React.useCallback(
    (input: string) => {
      if (disabled) return false

      const math = parseContentEditorMathInput(input)
      if (!math) return false

      const view = editorRef.current?.view ?? null
      const currentValue = view?.state.doc.toString() ?? valueRef.current ?? ''
      const insertValue = math.kind === 'display' ? `\n${math.markdown}\n` : math.markdown

      if (view) {
        const { from, to } = view.state.selection.main

        view.dispatch({
          changes: { from, to, insert: insertValue },
          selection: { anchor: from + insertValue.length }
        })

        onChangeRef.current?.(currentValue.slice(0, from) + insertValue + currentValue.slice(to))
        applyCursorChangeIfAvailable(view, from + insertValue.length)
        return true
      }

      onChangeRef.current?.(currentValue + insertValue)
      return true
    },
    [disabled]
  )

  const applyBold = React.useCallback(() => {
    insertInline('**', '**', 'bold text')
  }, [insertInline])

  const applyItalic = React.useCallback(() => {
    insertInline('_', '_', 'italic text')
  }, [insertInline])

  const applyStrike = React.useCallback(() => {
    insertInline('~~', '~~', 'strikethrough')
  }, [insertInline])

  const applyCode = React.useCallback(() => {
    insertInline('`', '`', 'code')
  }, [insertInline])

  const setMarkdownLink = React.useCallback(() => {
    if (disabled || !markdownLinkUrl) return

    const view = editorRef.current?.view ?? null
    const currentValue = view?.state.doc.toString() ?? valueRef.current ?? ''
    const { newValue, newCursorPos } = setMarkdownLinkAtCursor(view, currentValue, markdownLinkUrl)

    onChangeRef.current?.(newValue)
    setMarkdownLinkUrl('')
    setIsMarkdownLinkActive(true)
    applyCursorChangeIfAvailable(view, newCursorPos)
  }, [disabled, markdownLinkUrl])

  const removeMarkdownLink = React.useCallback(() => {
    if (disabled) return

    const view = editorRef.current?.view ?? null
    const currentValue = view?.state.doc.toString() ?? valueRef.current ?? ''
    const { newValue, newCursorPos } = removeMarkdownLinkAtCursor(view, currentValue)

    onChangeRef.current?.(newValue)
    setMarkdownLinkUrl('')
    setIsMarkdownLinkActive(false)
    applyCursorChangeIfAvailable(view, newCursorPos)
  }, [disabled])

  const openMarkdownLink = React.useCallback(
    (target: string = '_blank', features: string = 'noopener,noreferrer') => {
      const url = markdownLinkUrl || getCurrentMarkdownLink()?.url
      if (!url) return

      const safeUrl = sanitizeUrl(url, window.location.href)
      if (safeUrl !== '#') {
        window.open(safeUrl, target, features)
      }
    },
    [getCurrentMarkdownLink, markdownLinkUrl]
  )

  const applyUnderline = React.useCallback(() => {
    insertInline('<u>', '</u>', 'underlined text')
  }, [insertInline])

  const applyHighlight = React.useCallback(
    (color: string) => {
      insertInline(`<span data-highlight-color="${color}">`, '</span>', 'highlighted text')
    },
    [insertInline]
  )

  const removeHighlight = React.useCallback(() => {
    if (disabled) return

    const view = editorRef.current?.view ?? null
    const currentValue = value || ''
    const { newValue, newCursorPos } = removeHighlightSpanAtCursor(view, currentValue)

    onChange?.(newValue)
    applyCursorChangeIfAvailable(view, newCursorPos)
  }, [disabled, value, onChange])

  const transformLines = React.useCallback(
    (transformLine: (line: string, index: number) => string, fallbackLine: string) => {
      if (disabled) return

      const view = editorRef.current?.view ?? null
      const { newValue, newCursorPos } = transformSelectedMarkdownLines(
        view,
        value || '',
        transformLine,
        fallbackLine
      )

      onChange?.(newValue)
      applyCursorChangeIfAvailable(view, newCursorPos)
    },
    [disabled, value, onChange]
  )

  const applyHeading = React.useCallback(
    (level: MarkdownHeadingLevel) => {
      transformLines(
        (line) => formatMarkdownHeadingLine(line, level),
        getMarkdownHeadingFallback(level)
      )
    },
    [transformLines]
  )

  const applyList = React.useCallback(
    (type: MarkdownListType) => {
      transformLines(
        (line, index) => formatMarkdownListLine(line, index, type),
        getMarkdownListFallback(type)
      )
    },
    [transformLines]
  )

  const applyBlockquote = React.useCallback(() => {
    transformLines(formatMarkdownBlockquoteLine, '> Quote')
  }, [transformLines])

  return {
    applyBlockquote,
    applyBold,
    applyCode,
    applyHeading,
    applyItalic,
    applyList,
    applyHighlight,
    applyStrike,
    applyUnderline,
    canRedoMarkdownChange,
    canUndoMarkdownChange,
    codeMirrorExtensions,
    editorRef,
    editorTheme,
    handleCreateEditor,
    handleEditorChange,
    handleMarkdownLinkPopoverOpenChange,
    isMarkdownLinkActive,
    insertCodeBlock,
    insertDivider,
    insertMath,
    insertTable,
    markdownLinkUrl: markdownLinkUrl || '',
    openMediaGalleryAtCursor,
    openMarkdownLink,
    redoMarkdownChange,
    removeHighlight,
    removeMarkdownLink,
    setMarkdownLink,
    setMarkdownLinkUrl,
    undoMarkdownChange
  }
}
