import React, {
  useCallback,
  useEffect,
  useState,
  useImperativeHandle,
  forwardRef,
  useRef,
} from 'react';
import clsx from 'clsx';
import {
  $getSelection,
  $isRangeSelection,
  $isElementNode,
  $createParagraphNode,
  $getRoot,
  $getNodeByKey,
  $createTextNode,
  $createRangeSelection,
  $setSelection,
  type LexicalEditor,
} from 'lexical';
import { $getNearestNodeOfType } from '@lexical/utils';
import { LinkNode } from '@lexical/link';
import { ListNode, ListItemNode } from '@lexical/list';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { type InitialConfigType, LexicalComposer } from '@lexical/react/LexicalComposer';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
import { $generateNodesFromDOM } from '@lexical/html';
import CleanHtmlOnChangePlugin from '../utilities/lexicalPlugins/CleanHtmlOnChangePlugin.js';
import ClearTriggerPlugin from '../utilities/lexicalPlugins/ClearTriggerPlugin.js';
import EditorRefPlugin from '../utilities/lexicalPlugins/EditorRefPlugin.js';
import InitialTextPlugin from '../utilities/lexicalPlugins/InitialTextPlugin.js';
import SetTextPlugin from '../utilities/lexicalPlugins/SetTextPlugin.js';
import TextEditorToolbar from './TextEditorToolbar.js';
import './TextEditor.css';

export interface BlockSelection {
  anchorKey: string;
  anchorOffset: number;
  anchorType: 'text' | 'element';
  focusKey: string;
  focusOffset: number;
  focusType: 'text' | 'element';
  text: string;
}

export interface TextEditorRef {
  clear: () => void;
  setText: (value: string) => void;
  getText: () => string;
  selectCurrentBlock: () => BlockSelection | null;
  replaceSelection: (selection: BlockSelection, value: string) => void;
  isEmpty: () => boolean;
  isCurrentBlockEmpty: () => boolean;
}

interface TextEditorProps {
  floatingMenu?: React.ReactNode;
  overlay?: React.ReactNode;
  labelId?: string;
  initialText?: string;
  onChange?: (text: string) => void;
}

const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(({ floatingMenu = <></>, overlay = <>

    </>, labelId, initialText, onChange }, ref) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const lexicalEditorRef = useRef<LexicalEditor | null>(null);

  const [clearTrigger, setClearTrigger] = useState(0);
  const [setTextTrigger, setSetTextTrigger] = useState<{
    value: string;
    timestamp: number;
  } | null>(null);

  const [currentHtml, setCurrentHtml] = useState<string>('');

  const sentinelRef = useRef<HTMLDivElement>(null);
  const toolbarRef = useRef<HTMLDivElement>(null);
  const [isToolbarSticky, setIsToolbarSticky] = useState(false);
  const [toolbarSize, setToolbarSize] = useState<{
    width: number;
    height: number;
  } | null>(null);

  // Watch sentinel to toggle sticky toolbar.
  useEffect(() => {
    const sentinel = sentinelRef.current;
    if (!sentinel) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        setIsToolbarSticky(!entry?.isIntersecting);
      },
      { threshold: 0 },
    );

    observer.observe(sentinel);
    return () => {
      observer.disconnect();
    };
  }, []);

  // Get toolbar size.
  useEffect(() => {
    const container = containerRef.current;
    const toolbar = toolbarRef.current;
    if (!container || !toolbar) return;

    const updateSize = () => {
      const width = container.getBoundingClientRect().width;
      const height = toolbar.getBoundingClientRect().height;
      setToolbarSize({ width, height });
    };

    updateSize();
    window.addEventListener('resize', updateSize);
    return () => {
      window.removeEventListener('resize', updateSize);
    };
  }, []);

  const floatingMenuRef = useRef<HTMLDivElement>(null);
  const [floatingMenuTop, setFloatingMenuTop] = useState<number>(0);

  // Set floating menu position on cursor change.
  useEffect(() => {
    const editor = lexicalEditorRef.current;
    if (!editor) return;

    return editor.registerUpdateListener(() => {
      const nativeSelection = window.getSelection();
      if (!nativeSelection?.rangeCount) return;

      const contentContainer = containerRef.current?.querySelector(
        '.text-editor__content-container',
      );
      if (!contentContainer) return;

      const range = nativeSelection.getRangeAt(0);
      const rect = range.getBoundingClientRect();
      const contentContainerRect = contentContainer.getBoundingClientRect();

      // On empty lines, the range rect has zero dimensions, so don't move the menu unless the text
      // area is completely empty.
      if (rect.height === 0 && rect.width === 0) {
        editor.getEditorState().read(() => {
          const root = $getRoot();
          const textContent = root.getTextContent().trim();
          if (textContent.length === 0) {
            setFloatingMenuTop(0);
            return;
          }
        });
        return;
      }

      setFloatingMenuTop(rect.top - contentContainerRect.top);
    });
  }, []);

  useImperativeHandle(ref, () => ({
    clear: () => {
      setClearTrigger((prev) => prev + 1);
    },
    setText: (value: string) => {
      setSetTextTrigger({ value, timestamp: Date.now() });
    },
    getText: () => {
      return currentHtml;
    },
    selectCurrentBlock: () => {
      const editor = lexicalEditorRef.current;
      if (!editor) return null;

      let result: BlockSelection | null = null;

      editor.update(() => {
        const selection = $getSelection();
        if (!$isRangeSelection(selection)) {
          return;
        }

        // If text is already selected, return that text
        if (!selection.isCollapsed()) {
          result = {
            anchorKey: selection.anchor.key,
            anchorOffset: selection.anchor.offset,
            anchorType: selection.anchor.type,
            focusKey: selection.focus.key,
            focusOffset: selection.focus.offset,
            focusType: selection.focus.type,
            text: selection.getTextContent(),
          };
          return;
        }

        const anchorNode = selection.anchor.getNode();
        // Get the top-level element (paragraph, heading, list, quote)
        const topLevelElement =
          anchorNode.getKey() === 'root' ? anchorNode : anchorNode.getTopLevelElementOrThrow();

        // For list items, get the parent list node
        const listNode = $getNearestNodeOfType(anchorNode, ListNode);
        const blockToSelect = listNode || topLevelElement;

        // Select the entire block
        if ($isElementNode(blockToSelect)) {
          blockToSelect.select(0, blockToSelect.getChildrenSize());
          const newSelection = $getSelection();
          if ($isRangeSelection(newSelection)) {
            result = {
              anchorKey: newSelection.anchor.key,
              anchorOffset: newSelection.anchor.offset,
              anchorType: newSelection.anchor.type,
              focusKey: newSelection.focus.key,
              focusOffset: newSelection.focus.offset,
              focusType: newSelection.focus.type,
              text: blockToSelect.getTextContent(),
            };
          }
        }
      });

      return result;
    },
    replaceSelection: (blockSelection: BlockSelection, value: string) => {
      const editor = lexicalEditorRef.current;
      if (!editor) return;

      editor.update(() => {
        // Get the anchor node to check if we're in a list
        const anchorNode = $getNodeByKey(blockSelection.anchorKey);
        if (!anchorNode) return;

        // Check if we're inside a list
        const listNode = $getNearestNodeOfType(anchorNode, ListNode);

        // Check if it's HTML
        const isHTML = /<[a-z][\s\S]*>/i.test(value);

        let nodesToInsert;
        if (isHTML) {
          const parser = new DOMParser();
          const dom = parser.parseFromString(value, 'text/html');
          nodesToInsert = $generateNodesFromDOM(editor, dom);
        } else {
          // For plain text, create a paragraph with the text.
          const paragraph = $createParagraphNode();
          paragraph.append($createTextNode(value));
          nodesToInsert = [paragraph];
        }

        if (listNode) {
          // If we're in a list, insert the new nodes after the list and remove the list.
          for (const node of nodesToInsert) {
            listNode.insertBefore(node);
          }
          listNode.remove();
        } else {
          // Restore the selection.
          const selection = $createRangeSelection();
          selection.anchor.set(
            blockSelection.anchorKey,
            blockSelection.anchorOffset,
            blockSelection.anchorType,
          );
          selection.focus.set(
            blockSelection.focusKey,
            blockSelection.focusOffset,
            blockSelection.focusType,
          );
          $setSelection(selection);

          // Delete the selected content and insert new nodes.
          selection.removeText();
          selection.insertNodes(nodesToInsert);
        }
      });
    },
    isEmpty: () => {
      const editor = lexicalEditorRef.current;
      if (!editor) return true;

      let empty = true;
      editor.getEditorState().read(() => {
        const root = $getRoot();
        const textContent = root.getTextContent().trim();
        empty = textContent.length === 0;
      });

      return empty;
    },
    isCurrentBlockEmpty: () => {
      const editor = lexicalEditorRef.current;
      if (!editor) return true;

      let empty = true;
      editor.getEditorState().read(() => {
        const selection = $getSelection();
        if (!$isRangeSelection(selection)) {
          return;
        }

        const anchorNode = selection.anchor.getNode();
        const topLevelElement =
          anchorNode.getKey() === 'root' ? anchorNode : anchorNode.getTopLevelElementOrThrow();

        const listNode = $getNearestNodeOfType(anchorNode, ListNode);
        const block = listNode || topLevelElement;

        const textContent = block.getTextContent().trim();
        empty = textContent.length === 0;
      });

      return empty;
    },
  }));

  const initialConfig: InitialConfigType = {
    namespace: 'ConductorEditor',
    onError: (error: Error) => console.error(error),
    nodes: [HeadingNode, QuoteNode, LinkNode, ListNode, ListItemNode],
  };

  const handleHtmlChange = useCallback(
    (html: string) => {
      setCurrentHtml(html);
      if (onChange) {
        onChange(html);
      }
    },
    [onChange],
  );

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <div className="text-editor" ref={containerRef}>
        <div
          className={clsx('text-editor__toolbar', {
            'text-editor__toolbar--is-sticky': isToolbarSticky,
          })}
          style={isToolbarSticky && toolbarSize?.width ? { width: toolbarSize.width } : undefined}
          ref={toolbarRef}
        >
          <TextEditorToolbar />
        </div>
        <div className="text-editor__content-container">
          <div className="text-editor__content-sentinel" ref={sentinelRef} />
          {floatingMenu && (
            <div
              className="text-editor__floating-menu"
              ref={floatingMenuRef}
              style={{
                top: `max(0px, calc(${floatingMenuTop}px - var(--spacing-1) - 3px))`,
                position: 'absolute',
              }}
            >
              {floatingMenu}
            </div>
          )}
          <RichTextPlugin
            contentEditable={
              <ContentEditable className="text-editor__content" aria-labelledby={labelId} />
            }
            ErrorBoundary={LexicalErrorBoundary}
          />
          {overlay && <div className="text-editor__overlay">{overlay}</div>}
        </div>
        <HistoryPlugin />
        <ListPlugin />
        <LinkPlugin />
        <CleanHtmlOnChangePlugin onChange={handleHtmlChange} />
        <InitialTextPlugin initialText={initialText} />
        <ClearTriggerPlugin clearTrigger={clearTrigger} />
        <SetTextPlugin setTextTrigger={setTextTrigger} />
        <EditorRefPlugin editorRef={lexicalEditorRef} />
      </div>
    </LexicalComposer>
  );
});

TextEditor.displayName = 'TextEditor';

export default TextEditor;
