import { useEffect } from 'react';
import { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import * as stylex from '@stylexjs/stylex';
import { $getRoot, $insertNodes } from 'lexical';

import type { InitialConfigType } from '@lexical/react/LexicalComposer';
import type { EditorThemeClasses, LexicalEditor } from 'lexical';
import type { RichTextEditorProps } from './types';

import { editorNodes } from './nodes';
import { ToolbarPlugin } from './plugins/ToolbarPlugin';
import { styles } from './styles';
import { isValidUrl } from './utils';

const cx = (style: stylex.StyleXStyles): string =>
  stylex.props(style).className ?? '';

// Map Lexical node types onto stylex-generated classNames so the rendered
// document matches the design tokens.
const theme: EditorThemeClasses = {
  paragraph: cx(styles.paragraph),
  link: cx(styles.link),
  text: {
    bold: cx(styles.bold),
    italic: cx(styles.italic),
    underline: cx(styles.underline),
  },
};

const buildInitialState =
  (html: string) =>
  (editor: LexicalEditor): void => {
    // DOMParser is browser-only; the editor is rendered client-side, but guard
    // defensively so SSR/serialization can't throw.
    if (typeof window === 'undefined') return;

    const dom = new DOMParser().parseFromString(html, 'text/html');
    const nodes = $generateNodesFromDOM(editor, dom);
    $getRoot().clear();
    $getRoot().select();
    $insertNodes(nodes);
  };

export const RichTextEditor = ({
  defaultValue,
  placeholder = '',
  onChange,
  isDisabled = false,
  testId,
}: RichTextEditorProps) => {
  const initialConfig: InitialConfigType = {
    namespace: 'RichTextEditor',
    theme,
    nodes: editorNodes,
    editable: !isDisabled,
    editorState: defaultValue ? buildInitialState(defaultValue) : undefined,
    onError: (error) => {
      // eslint-disable-next-line no-console
      console.error('[RichTextEditor]', error);
    },
  };

  const handleChange = (editor: LexicalEditor) => {
    editor.read(() => {
      onChange?.($generateHtmlFromNodes(editor, null));
    });
  };

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <div
        {...stylex.props(styles.root, isDisabled && styles.disabled)}
        data-testid={testId}
      >
        <div {...stylex.props(styles.editorContainer)}>
          <RichTextPlugin
            contentEditable={
              <ContentEditable
                {...stylex.props(styles.contentEditable)}
                aria-label={placeholder || 'Rich text editor'}
              />
            }
            placeholder={
              placeholder ? (
                <div {...stylex.props(styles.placeholder)}>{placeholder}</div>
              ) : null
            }
            ErrorBoundary={LexicalErrorBoundary}
          />
        </div>
        <ToolbarPlugin isDisabled={isDisabled} />
      </div>
      <HistoryPlugin />
      <LinkPlugin validateUrl={isValidUrl} />
      {onChange && (
        <OnChangePlugin
          ignoreSelectionChange
          onChange={(_editorState, editor) => handleChange(editor)}
        />
      )}
      <DisabledSync isDisabled={isDisabled} />
    </LexicalComposer>
  );
};

/**
 * Keeps the editor's editable state in sync when `isDisabled` changes after
 * mount (initialConfig.editable only applies on first render).
 */
const DisabledSync = ({ isDisabled }: { isDisabled: boolean }) => {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    editor.setEditable(!isDisabled);
  }, [editor, isDisabled]);
  return null;
};
