import classNames from "classnames/dedupe";
import type { RefAttributes } from "react";
import { useMemo } from "react";
import type { TextElementType, TextProps } from "../Text/Text";
import { Text, getElementType } from "../Text/Text";

export type HTMLElementType = "span" | "div";

// Block-level tags that cannot be nested inside <p>.
const BLOCK_TAG =
  /<(div|p|ul|ol|li|h[1-6]|table|tr|td|th|blockquote|pre|hr|dl|dt|dd)[\s>/]/i;
const containsBlockHTML = (text: string) => BLOCK_TAG.test(text);
const containsHTML = (text: string) =>
  /<[a-z/]/i.test(text) || /&(?:#\d+|#x[\da-f]+|[a-z]\w*);/i.test(text); // tag or entity

const buildTextClasses = (
  textProps: Omit<TextProps, "children">,
  htmlClassName?: string,
) => {
  const { variant, elementType, spacing, className } = textProps;
  const variantType = variant || getElementType(variant, elementType);
  return classNames(
    "mobius",
    "mobius-text",
    { [`--is-${variantType}`]: variantType },
    { [`--has-line-height-${spacing}`]: spacing },
    className,
    htmlClassName,
  );
};

export interface TextOrHTMLProps
  extends Omit<TextProps, "children">, RefAttributes<TextElementType> {
  /** HTML string to be rendered with dangerouslySetInnerHTML */
  text: string;
  /** Custom class name for the dangerous HTML element */
  htmlClassName?: string;
  /** HTML element type for the dangerous HTML element */
  htmlElementType?: HTMLElementType;
  /** If true, wraps the dangerous HTML element inside a Text component */
  textWrapper?: boolean;
  /** If true, auto-detects whether text is HTML or plain text to determine wrapping and element type */
  autoDetect?: boolean;
}

const TextOrHTML = ({
  ref,
  text,
  htmlClassName,
  htmlElementType,
  textWrapper = false,
  autoDetect = false,
  ...textProps
}: TextOrHTMLProps) => {
  const hasBlockContent = autoDetect && containsBlockHTML(text);

  // Memoize the dangerouslySetInnerHTML object to prevent unnecessary re-renders
  // See: https://github.com/facebook/react/issues/31660
  const dangerousHTML = useMemo(() => ({ __html: text }), [text]);

  // Non-block text with autoDetect: render directly on a Text-equivalent element,
  // avoiding unnecessary <span> nesting inside <p>.
  if (autoDetect && !hasBlockContent) {
    const { variant, spacing, elementType, className, ...domProps } = textProps;
    const Element = getElementType(variant, elementType);
    const classes = buildTextClasses(textProps, htmlClassName);

    return containsHTML(text) ? (
      <Element
        ref={ref}
        {...domProps}
        className={classes}
        dangerouslySetInnerHTML={dangerousHTML}
      />
    ) : (
      <Element ref={ref} {...domProps} className={classes}>
        {text}
      </Element>
    );
  }

  const DangerousComponent =
    htmlElementType ?? (hasBlockContent ? "div" : "span");
  const dangerousElement = (
    <DangerousComponent
      className={htmlClassName}
      dangerouslySetInnerHTML={dangerousHTML}
    />
  );

  if (textWrapper) {
    return (
      <Text ref={ref} {...textProps}>
        {dangerousElement}
      </Text>
    );
  }

  return dangerousElement;
};

TextOrHTML.displayName = "TextOrHTML";
export { TextOrHTML };
