'use client'

/**
 * CodeMirror-based Markdown Editor with XSS Protection
 * 
 * Security Features:
 * - rehype-sanitize: Sanitizes HTML output to prevent XSS attacks
 * - Custom schema: Allows KaTeX math elements while blocking dangerous content
 * - Protocol filtering: Only allows safe URL protocols (http, https, mailto)
 * - Safe error handling: Error messages don't include user content
 * - KaTeX integration: Math rendering is processed before sanitization
 */

import { useState, useEffect, useRef, useCallback } from 'react'
//import { basicSetup } from 'codemirror'
import { EditorView } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { markdown } from '@codemirror/lang-markdown'
import CodeMirrorToolbar from '~/components/CodeMirrorToolbar'
import PreviewRenderer from '~/components/PreviewRenderer'
import { processMarkdown } from '~/lib/markdownProcessor'

import { history, historyKeymap } from "@codemirror/commands"
import { defaultKeymap } from "@codemirror/commands"
import { foldGutter, foldKeymap } from "@codemirror/language"
import { indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching } from "@codemirror/language"
import { highlightActiveLine, highlightActiveLineGutter, keymap, drawSelection } from "@codemirror/view"
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search"
import { lintKeymap } from "@codemirror/lint"
import { logger } from '~/lib/logger'

// Import KaTeX CSS
import 'katex/dist/katex.min.css'

export const basicSetupNoLineNumbers = [
  history(),
  foldGutter(),
  drawSelection(),
  indentOnInput(),
  syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
  bracketMatching(),
  highlightActiveLine(),
  highlightActiveLineGutter(),
  highlightSelectionMatches(),
  keymap.of([
    ...defaultKeymap,
    ...searchKeymap,
    ...historyKeymap,
    ...foldKeymap,
    ...lintKeymap
  ])
]

interface EditorProps {
  initialValue?: string
  onChange?: (value: string) => void
  displayMode?: 'preview' | 'split' | 'editor'
  showToolbar?: boolean
}

// Hook to detect the active theme by watching the `dark` class on <html>,
// which is what applyTheme() toggles. This correctly reflects explicit
// user overrides, not just the OS-level prefers-color-scheme.
const useSystemTheme = () => {
  // Read the DOM directly so the initial render uses the correct theme.
  // The inline script in root.tsx applies the `dark` class synchronously
  // before React mounts, so this lazy initializer always sees the right value.
  const [theme, setTheme] = useState<'light' | 'dark'>(() =>
    document.documentElement.classList.contains('dark') ? 'dark' : 'light'
  )

  useEffect(() => {
    const isDark = () => document.documentElement.classList.contains('dark')

    const observer = new MutationObserver(() => {
      setTheme(isDark() ? 'dark' : 'light')
    })
    observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
    return () => observer.disconnect()
  }, [])

  return theme
}

const CodeMirrorMarkdownEditor: React.FC<EditorProps> = ({
  displayMode = 'split', // 'split' | 'editor' | preview'
  showToolbar = true,
  initialValue = '',
  onChange,
}) => {
  const [markdownContent, setMarkdownContent] = useState(initialValue)
  const [html, setHtml] = useState('')
  const [isProcessing, setIsProcessing] = useState(false)
  const [viewMode, setViewMode] = useState<'split' | 'editor' | 'preview'>(displayMode)
  const [imageError, setImageError] = useState<string | null>(null)
  const imageErrorTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const systemTheme = useSystemTheme()
  const processGenRef = useRef(0)
  const editorParentRef = useRef<HTMLDivElement | null>(null)
  const previewRef = useRef<HTMLDivElement | null>(null)
  const viewRef = useRef<EditorView | null>(null)
  const isScrollingEditor = useRef(false)
  const isScrollingPreview = useRef(false)

  // Scroll synchronization function
  const syncPreviewScroll = useCallback(() => {
    if (!viewRef.current || !previewRef.current || isScrollingPreview.current) return
    
    const editor = viewRef.current
    const preview = previewRef.current
    
    // Get editor scroll info
    const editorElement = editor.scrollDOM
    const scrollTop = editorElement.scrollTop
    const scrollHeight = editorElement.scrollHeight - editorElement.clientHeight
    
    // Calculate scroll percentage
    const scrollPercentage = scrollHeight > 0 ? scrollTop / scrollHeight : 0
    
    // Apply to preview
    const previewScrollHeight = preview.scrollHeight - preview.clientHeight
    const targetScrollTop = Math.max(0, scrollPercentage * previewScrollHeight)
    
    // Set flag to prevent reverse sync
    isScrollingEditor.current = true
    preview.scrollTop = targetScrollTop
    // Reset flag after a short delay
    setTimeout(() => {
      isScrollingEditor.current = false
    }, 50)
  }, [])

  function handleImageError(msg: string) {
    setImageError(msg)
    if (imageErrorTimerRef.current) clearTimeout(imageErrorTimerRef.current)
    imageErrorTimerRef.current = setTimeout(() => setImageError(null), 4000)
  }

  // Process markdown to HTML using shared processor.
  // processGenRef is a generation counter that guards against stale async results.
  // Two calls can be in-flight simultaneously — most commonly on page load when
  // useSystemTheme's initial render uses the correct theme but a theme-change
  // effect can still fire and kick off a second call. Shiki loads each theme's
  // highlighter lazily and concurrently, so the earlier call can resolve last
  // and overwrite the correct HTML. Stamping each call with the current counter
  // and discarding any result whose stamp no longer matches ensures only the
  // most recently dispatched call wins.
  const processMarkdownContent = useCallback(async (markdownText: string) => {
    const gen = ++processGenRef.current
    setIsProcessing(true)
    try {
      const htmlString = await processMarkdown(markdownText, systemTheme)
      if (gen !== processGenRef.current) return  // a newer call is already in flight; discard
      setHtml(htmlString)
    } catch (error) {
      if (gen !== processGenRef.current) return
      logger.error({ error }, 'Error processing Markdown.')
      // Use safe HTML for error message - no user content included
      setHtml('<div style="color: red; padding: 1rem; border: 1px solid red; border-radius: 4px; background: rgba(255,0,0,0.1);">⚠️ Error processing markdown. Please check your syntax.</div>')
    } finally {
      if (gen === processGenRef.current) setIsProcessing(false)
    }
  }, [systemTheme])

  // Initial processing and reprocess when theme changes
  useEffect(() => {
    processMarkdownContent(markdownContent)
  }, [markdownContent, processMarkdownContent])

  // Initialize CodeMirror view
  useEffect(() => {
    if (viewRef.current) return // Don't recreate if already exists

    // Create scroll synchronization extension
    const scrollSyncExtension = EditorView.domEventHandlers({
      scroll: (event, view) => { // eslint-disable-line @typescript-eslint/no-unused-vars
        // Check viewMode at runtime instead of capturing it in closure
        requestAnimationFrame(() => {
          // Get current view mode from state
          const currentViewMode = document.querySelector('[data-view-mode]')?.getAttribute('data-view-mode')
          if (currentViewMode === 'split' && !isScrollingPreview.current) {
            syncPreviewScroll()
          }
        })
        return false
      }
    })

    // Create the EditorState
    const state = EditorState.create({
      doc: markdownContent,
      extensions: [
        displayMode === 'preview' ? EditorState.readOnly.of(true) : [],
        basicSetupNoLineNumbers, // basicSetup replacement without line numbers
        markdown(),
        EditorView.lineWrapping,
        scrollSyncExtension,
        EditorView.updateListener.of((update) => {
          if (update.docChanged) {
            const value = update.state.doc.toString()
            setMarkdownContent(value)
            onChange?.(value)
            processMarkdownContent(value)
          }
        }),
        EditorView.theme({
          '&': { height: '100%' },
          '.cm-scroller': { 
            fontFamily: 'inherit',
            fontSize: 'inherit',
            overflow: 'auto'
          },
          '.cm-editor': {
            height: '100%'
          },
          '.cm-focused': {
            outline: 'none'
          },
          /* Make the caret/cursor match the current text color (better visibility in dark mode) */
          '.cm-content': {
            caretColor: 'currentColor'
          },
          '.cm-cursor': {
            borderLeftColor: 'currentColor'
          },
          '.cm-dropCursor': {
            borderLeftColor: 'currentColor'
          },
          /* Improve text visibility in selections */
          '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
            background: '#2563eb !important', /* bright blue */
          },
          '.cm-selectionBackground, .cm-content ::selection': {
            color: '#ffffff !important' /* white text for contrast */
          }
        })
      ]
    })

    // Create the EditorView without parent initially
    const view = new EditorView({ state })
    viewRef.current = view

    // Add direct scroll listener as backup
    const scrollElement = view.scrollDOM
    const handleScroll = () => {
      requestAnimationFrame(() => {
        const currentViewMode = document.querySelector('[data-view-mode]')?.getAttribute('data-view-mode')
        if (currentViewMode === 'split' && !isScrollingPreview.current) {
          syncPreviewScroll()
        }
      })
    }
    
    scrollElement.addEventListener('scroll', handleScroll, { passive: true })

    // Cleanup on unmount
    return () => {
      scrollElement.removeEventListener('scroll', handleScroll)
      view.destroy()
      viewRef.current = null
      if (imageErrorTimerRef.current) clearTimeout(imageErrorTimerRef.current)
    }
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  // Attach editor to DOM when container is available
  useEffect(() => {
    if (!viewRef.current || !editorParentRef.current) return
    
    // Only attach if not already attached
    if (editorParentRef.current.children.length === 0) {
      editorParentRef.current.appendChild(viewRef.current.dom)
    }
  }, [viewMode]) // Re-attach when view mode changes


  // Sync editor content when value prop changes
  /* useEffect(() => {
    if (viewRef.current && value !== undefined) {
      const currentValue = viewRef.current.state.doc.toString()
      if (currentValue !== value) {
        viewRef.current.dispatch({
          changes: {
            from: 0,
            to: viewRef.current.state.doc.length,
            insert: value
          }
        })
      }
    }
  }, [value]) */

  return (
    <div className="flex flex-col h-full min-h-0" data-view-mode={viewMode}>
      {/* Toolbar and Processing Header */}
      <div className="flex-shrink-0">
        {isProcessing && (
          <div className="bg-gray-100 dark:bg-gray-800 px-4 py-2">
            <div className="text-xs text-gray-500">Processing...</div>
          </div>
        )}
        {showToolbar && (
          <CodeMirrorToolbar
            editor={viewRef.current}
            viewMode={viewMode}
            onViewModeChange={setViewMode}
            onImageError={handleImageError}
          />
        )}
        {imageError && (
          <div className="flex items-center justify-between gap-3 px-4 py-2 bg-red-50 dark:bg-red-950/40 border-b border-red-200 dark:border-red-800">
            <p className="text-sm text-red-600 dark:text-red-400">{imageError}</p>
            <button
              type="button"
              onClick={() => setImageError(null)}
              className="shrink-0 text-red-400 dark:text-red-500 hover:text-red-600 dark:hover:text-red-300 transition-colors"
              aria-label="Dismiss"
            >
              <svg className="size-4" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75">
                <path d="M4 4l8 8M12 4l-8 8" strokeLinecap="round" />
              </svg>
            </button>
          </div>
        )}
      </div>
      
      {/* Content Area */}
      <div className="flex-1 flex min-h-0">
        {/* Editor Panel */}
        {(viewMode === 'split' || viewMode === 'editor') && (
          <div className={viewMode === 'split' ? 'w-1/2 border-r flex flex-col' : 'w-full flex flex-col'}>
            <div ref={editorParentRef} className="flex-1 text-base overflow-auto bg-white dark:bg-gray-900" style={{ minHeight: 0 }} />
          </div>
        )}

        {/* Preview Panel */}
        {(viewMode === 'split' || viewMode === 'preview') && (
          <div className={viewMode === 'split' ? 'w-1/2 flex flex-col' : 'w-full flex flex-col'}>
            <div 
              ref={previewRef}
              className="flex-1 overflow-auto p-4 bg-white dark:bg-gray-900"
              onScroll={(e) => {
                // Only sync if in split mode
                if (viewMode !== 'split') return
                
                // Prevent infinite scroll loops
                if (isScrollingEditor.current) {
                  isScrollingEditor.current = false
                  return
                }
                
                // Sync editor scroll when preview is scrolled
                if (!viewRef.current) return
                
                const preview = e.currentTarget
                const editor = viewRef.current
                const editorElement = editor.scrollDOM
                
                const scrollTop = preview.scrollTop
                const scrollHeight = preview.scrollHeight - preview.clientHeight
                const scrollPercentage = scrollHeight > 0 ? scrollTop / scrollHeight : 0
                
                const editorScrollHeight = editorElement.scrollHeight - editorElement.clientHeight
                const targetScrollTop = Math.max(0, scrollPercentage * editorScrollHeight)
                
                // Set flag to prevent editor from syncing back
                isScrollingPreview.current = true
                editorElement.scrollTop = targetScrollTop
                
                // Reset flag after a short delay
                setTimeout(() => {
                  isScrollingPreview.current = false
                }, 50)
              }}
            >
              <PreviewRenderer htmlContent={html} />
            </div>
          </div>
        )}
      </div>
    </div>
  )
}

export default CodeMirrorMarkdownEditor