"use client";

import cx from "classnames";
import React, {
  FunctionComponent,
  ReactNode,
  useEffect,
  useId,
  useRef,
  useState,
} from "react";
import { createPortal } from "react-dom";
import reactElementToJSXString from "react-element-to-jsx-string";
import breakpoints from "@/styles/export/breakpoint";
import { Button } from "../Button";
import { Card } from "../Card";
import { Dropdown, DropdownItem } from "../Dropdown";
import { Icon } from "../Icon";
import CodeExample from "./CodeExample";
import { getElementDisplayName } from "./getElementDisplayName";
import PreviewTitleBar from "./PreviewTitleBar";

import "./styles/style.scss";

const CLASS_ROOT = "preview";

// Helper function to generate JSX string from React elements (client-side fallback)
const generateJSXString = (
  children: ReactNode,
  options?: Record<string, any>,
): string => {
  const defaultOptions = {
    showDefaultProps: false,
    showFunctions: true,
    functionValue: (fn: any) => fn.name || "Function",
    displayName: getElementDisplayName,
    filterProps: ["mdxType", "originalType"],
    useBooleanShorthandSyntax: false,
    sortProps: false,
    ...options,
  };

  try {
    if (!children) {
      return "// No children provided";
    }

    if (Array.isArray(children)) {
      return children
        .filter(Boolean) // Filter out null/undefined children
        .map((child) => {
          try {
            // Additional validation for React elements
            if (!React.isValidElement(child)) {
              return "// Invalid React element";
            }
            return reactElementToJSXString(child as any, defaultOptions);
          } catch (error) {
            console.warn("Failed to generate JSX string for child:", error);
            return "// Unable to generate JSX for this element";
          }
        })
        .join("\n");
    }

    // Additional validation for React elements
    if (!React.isValidElement(children)) {
      return "// Invalid React element";
    }
    return reactElementToJSXString(children as any, defaultOptions);
  } catch (error) {
    console.warn("Failed to generate JSX string:", error);
    return "// Unable to generate JSX code";
  }
};

interface PreviewBackground {
  value: string;
  label: string;
}

interface PreviewRenderContext {
  bgTheme: string;
  bgThemeValue: string;
  themeClass: string;
  isDarkMode: boolean;
}

interface PreviewBreakpoint {
  key: string;
  label: string;
  width: number | null;
}

const PREVIEW_BREAKPOINTS: PreviewBreakpoint[] = [
  { key: "auto", label: "Auto", width: null },
  ...Object.entries(breakpoints).map(([key, value]) => {
    const tokenWidth = Number.parseInt(value, 10);
    // `xs` is 0 in tokens (mobile-first min-width), but preview needs a real viewport size.
    const width = key === "xs" ? 320 : tokenWidth;

    return {
      key,
      label: `${key.toUpperCase()} (${width}px)`,
      width,
    };
  }),
];

const IFRAME_VIEWPORT_GUTTER = 16;
const getScreenLikeMinHeight = (width: number | null): number => {
  if (width === null) {
    return 1;
  }

  if (width <= 480) {
    return 700;
  }

  if (width <= 768) {
    return 640;
  }

  return 560;
};

interface PreviewViewportFrameProps {
  children: ReactNode;
  className?: string;
  minHeight?: number;
}

const PreviewViewportFrame: React.FC<PreviewViewportFrameProps> = ({
  children,
  className,
  minHeight = 1,
}) => {
  const iframeRef = useRef<HTMLIFrameElement | null>(null);
  const [iframeRoot, setIframeRoot] = useState<HTMLElement | null>(null);
  const [height, setHeight] = useState(1);

  useEffect(() => {
    const iframe = iframeRef.current;
    if (!iframe) return;

    const doc = iframe.contentDocument;
    if (!doc) return;

    doc.open();
    doc.write(
      `<!doctype html><html><head></head><body><div id="preview-iframe-root"></div></body></html>`,
    );
    doc.close();

    // Mirror parent styles so components inside iframe have the same look.
    const stylesToClone = document.querySelectorAll(
      'link[rel="stylesheet"], style',
    );
    stylesToClone.forEach((styleNode) => {
      const clonedNode = styleNode.cloneNode(true);
      doc.head.appendChild(clonedNode);
    });

    doc.body.style.margin = "0";
    doc.body.style.padding = "0";
    doc.body.style.background = "transparent";
    doc.body.style.overflow = "visible";
    doc.documentElement.style.margin = "0";
    doc.documentElement.style.padding = "0";
    doc.documentElement.style.overflow = "visible";

    iframe.setAttribute("scrolling", "no");

    const root = doc.getElementById("preview-iframe-root");
    if (!root) return;

    root.style.width = "100%";
    root.style.minHeight = "1px";
    root.style.boxSizing = "border-box";
    root.style.paddingInline = `${IFRAME_VIEWPORT_GUTTER}px`;
    root.style.overflow = "visible";
    setIframeRoot(root);
  }, []);

  useEffect(() => {
    if (!iframeRoot) return;

    const doc = iframeRoot.ownerDocument;
    const frameWindow = doc.defaultView;

    const updateHeight = () => {
      const rootRect = iframeRoot.getBoundingClientRect();

      // Include absolutely positioned descendants that may not contribute to
      // scrollHeight/offsetHeight (e.g. sliders/popovers in docs previews).
      let maxDescendantBottom = rootRect.bottom;
      iframeRoot.querySelectorAll("*").forEach((node) => {
        const nodeRect = node.getBoundingClientRect();
        if (nodeRect.bottom > maxDescendantBottom) {
          maxDescendantBottom = nodeRect.bottom;
        }
      });

      const nextHeight = Math.max(
        Math.ceil(iframeRoot.getBoundingClientRect().height),
        iframeRoot.scrollHeight,
        Math.ceil(maxDescendantBottom - rootRect.top),
        minHeight,
        1,
      );
      // Update only when there is a meaningful delta to avoid feedback loops.
      setHeight((prevHeight) =>
        Math.abs(prevHeight - nextHeight) <= 1 ? prevHeight : nextHeight,
      );
    };

    updateHeight();

    const rafId = frameWindow?.requestAnimationFrame(updateHeight);
    const timeoutId = frameWindow?.setTimeout(updateHeight, 100);
    const resizeObserver =
      typeof ResizeObserver !== "undefined"
        ? new ResizeObserver(updateHeight)
        : null;
    resizeObserver?.observe(iframeRoot);

    const mutationObserver =
      typeof MutationObserver !== "undefined"
        ? new MutationObserver(updateHeight)
        : null;
    mutationObserver?.observe(iframeRoot, {
      subtree: true,
      childList: true,
      attributes: true,
      characterData: true,
    });

    frameWindow?.addEventListener("resize", updateHeight);

    return () => {
      if (typeof rafId === "number" && frameWindow) {
        frameWindow.cancelAnimationFrame(rafId);
      }
      if (typeof timeoutId === "number" && frameWindow) {
        frameWindow.clearTimeout(timeoutId);
      }
      resizeObserver?.disconnect();
      mutationObserver?.disconnect();
      frameWindow?.removeEventListener("resize", updateHeight);
    };
  }, [children, iframeRoot, minHeight]);

  return (
    <>
      <iframe
        ref={iframeRef}
        title="Preview viewport"
        className={cx("previewViewportFrame", className)}
        style={{ height }}
      />
      {iframeRoot && createPortal(children, iframeRoot)}
    </>
  );
};

interface PreviewProps {
  isDark?: boolean;
  bgTheme?: string | boolean;
  bgThemeColors?: Record<string, string>;
  bgThemeExcludedColors?: string[];
  children?: ReactNode | ((context: PreviewRenderContext) => ReactNode);
  className?: string;
  code?: ReactNode | string | ((context: PreviewRenderContext) => ReactNode);
  codeJSXOptions?: Record<string, any>;
  codeTypes?: string[];
  enableFullscreen?: boolean;
  hasCodePreview?: boolean;
  hasMultipleCodeRoots?: boolean;
  html?: string;
  jsxCode?: string; // Pre-generated JSX string (from server component)
  disableBreakpoints?: boolean;
  showBreakpointControls?: boolean;
  [key: string]: any;
}

const getBackgroundsAsArray = (
  previewBackgrounds: Record<string, string>,
  excludedColors: string[] = [],
): PreviewBackground[] =>
  Object.keys(previewBackgrounds).reduce<PreviewBackground[]>((acc, name) => {
    if (!excludedColors.includes(name)) {
      acc.push({
        value: previewBackgrounds[name],
        label: name,
      });
    }
    return acc;
  }, []);

// Map names to allowed background/surface classes
const getBgClassFromName = (name: string): string => {
  const normalized = name.toLowerCase().replace(/\s+/g, "-");

  const allowedBackgrounds = [
    "background-primary",
    "background-secondary",
    "background-contrast",
    "background-accent",
    "background-accent1-blog",
    "background-accent2-blog",
    "background-none",
    "surface-primary",
    "surface-secondary",
    "surface-tertiary",
    "surface-subtle",
    "surface-moderate",
    "surface-contrast",
    "surface-accent",
  ];

  // Return the class if it's in the allowed list, otherwise default to background-primary
  return allowedBackgrounds.includes(normalized)
    ? normalized
    : "background-primary";
};

const PreviewGenerator: FunctionComponent<PreviewProps> = ({
  bgTheme = "background-primary",
  bgThemeColors = {
    "Background Primary": "background-primary",
    "Background Secondary": "background-secondary",
    "Background Contrast": "background-contrast",
    "Background Accent": "background-accent",
    "Background Accent1 Blog": "background-accent1-blog",
    "Background Accent2 Blog": "background-accent2-blog",
    "Surface Primary": "surface-primary",
    "Surface Secondary": "surface-secondary",
    "Surface Tertiary": "surface-tertiary",
    "Surface Subtle": "surface-subtle",
    "Surface Moderate": "surface-moderate",
    "Surface Contrast": "surface-contrast",
    "Surface Accent": "surface-accent",
  },
  bgThemeExcludedColors = [],
  children,
  className,
  code,
  codeJSXOptions,
  codeTypes = ["html", "jsx"],
  enableFullscreen = true,
  hasCodePreview = true,
  hasMultipleCodeRoots = false,
  html,
  jsxCode,
  isDark = false,
  disableBreakpoints = false,
  showBreakpointControls = true,
  ...other
}) => {
  const [isCodeShown, setIsCodeShown] = useState(false);
  const [isFullscreen, setIsFullscreen] = useState(false);
  const [isDarkMode, setIsDarkMode] = useState(isDark);
  // Ensure unique dropdown id per Preview instance to avoid id collisions (SSR-safe)
  const backgroundDropdownId = `background-select-${useId()}`;
  const breakpointDropdownId = `breakpoint-select-${useId()}`;
  const [previewBreakpoint, setPreviewBreakpoint] = useState<PreviewBreakpoint>(
    PREVIEW_BREAKPOINTS[0],
  );
  const [previewBackground, setPreviewBackground] = useState<PreviewBackground>(
    () => {
      if (bgThemeColors && typeof bgTheme === "string") {
        const bgColorsOptions = getBackgroundsAsArray(
          bgThemeColors,
          bgThemeExcludedColors,
        );

        // If bgTheme is a string, try to find matching label in provided options
        if (bgThemeColors[bgTheme]) {
          return {
            label: bgTheme,
            value: bgThemeColors[bgTheme],
          };
        }

        const matchingOption = bgColorsOptions.find(
          (option) => option.label.toLowerCase() === bgTheme.toLowerCase(),
        );

        if (matchingOption) {
          return matchingOption;
        }

        if (bgColorsOptions.length > 0) {
          return bgColorsOptions[0];
        }

        return {
          label: bgTheme,
          value: bgThemeColors[bgTheme] || "background-primary",
        };
      }
      return { label: "Background Primary", value: "background-primary" };
    },
  );

  function handleToggleCode() {
    setIsCodeShown(!isCodeShown);
  }

  function handlePreviewBackground(previewBackground: PreviewBackground) {
    setPreviewBackground(previewBackground);
  }

  function handleToggleFullscreen() {
    setIsFullscreen(!isFullscreen);
  }

  function handleToggleDarkMode() {
    setIsDarkMode(!isDarkMode);
  }

  function handlePreviewBreakpoint(previewBreakpoint: PreviewBreakpoint) {
    setPreviewBreakpoint(previewBreakpoint);
  }

  const themeClass = isDarkMode ? "is-dark" : "is-light";
  const bgClass = getBgClassFromName(previewBackground.label);
  const applyThemeClassToJsxCode = (value?: string): string | undefined => {
    if (!value) return value;

    const trimmedValue = value.trim();
    if (!trimmedValue) return trimmedValue;

    if (hasMultipleCodeRoots) {
      return `<div className="${themeClass}">\n${trimmedValue}\n</div>`;
    }

    return trimmedValue.replace(
      /^<([A-Za-z][\w.]*)((?:\s+[\s\S]*?)?)\s*(\/?)>/,
      (_, tagName: string, rawAttrs = "", selfClosing = "") => {
        const attrs = rawAttrs || "";

        if (/\sclassName=/.test(attrs)) {
          return `<${tagName}${attrs.replace(
            /className=(['"])(.*?)\1/,
            (_match: string, quote: string, className: string) =>
              `className=${quote}${className} ${themeClass}${quote}`,
          )}${selfClosing ? " /" : ""}>`;
        }

        return `<${tagName}${attrs}\n  className="${themeClass}"${selfClosing ? " /" : ""}>`;
      },
    );
  };

  const classes = cx(CLASS_ROOT, themeClass, bgClass, className);
  const wrapperClasses = cx("previewWrapper", {
    isFullscreen: isFullscreen,
  });

  const actions: ReactNode[] = [];

  if (bgTheme && bgThemeColors) {
    const bgColorsOptions = getBackgroundsAsArray(
      bgThemeColors,
      bgThemeExcludedColors,
    );

    // Add theme toggle button
    actions.push(
      <Button key="theme-toggle" onClick={handleToggleDarkMode} type="ghost">
        <Icon
          name={isDarkMode ? "sleep" : "flashlight"}
          size="medium"
          fill={"currentColor"}
          style={{ marginRight: "0.5rem" }}
        />
        {isDarkMode ? (
          <code className="small bold">{".is-dark"}</code>
        ) : (
          <code className="small bold">{".is-light"}</code>
        )}
      </Button>,
    );

    if (bgColorsOptions.length) {
      actions.push(
        <Dropdown
          id={backgroundDropdownId}
          button={{
            children: previewBackground.label,
            iconName: "chevron-down",
            type: "ghost",
          }}
        >
          {bgColorsOptions.map((option: any) => (
            <DropdownItem
              key={option.value}
              onClick={() => handlePreviewBackground(option)}
            >
              {option.label}
            </DropdownItem>
          ))}
        </Dropdown>,
      );
    }
  }

  if (showBreakpointControls && !disableBreakpoints) {
    actions.push(
      <Dropdown
        key="breakpoint-select"
        id={breakpointDropdownId}
        button={{
          children: `Viewport: ${previewBreakpoint.label}`,
          iconName: "chevron-down",
          type: "ghost",
        }}
      >
        {PREVIEW_BREAKPOINTS.map((option) => (
          <DropdownItem
            key={option.key}
            onClick={() => handlePreviewBreakpoint(option)}
          >
            {option.label}
          </DropdownItem>
        ))}
      </Dropdown>,
    );
  }

  if (hasCodePreview) {
    actions.push(
      <Button key="code-toggle" onClick={handleToggleCode} type="ghost">
        <Icon
          name={isCodeShown ? "chevron-up" : "chevron-down"}
          size="medium"
          fill={"currentColor"}
          style={{ marginRight: "0.5rem" }}
        />
        {isCodeShown ? " HIDE CODE" : "SHOW CODE"}
      </Button>,
    );
  }

  if (enableFullscreen) {
    actions.push(
      <Button
        key="fullscreen-toggle"
        onClick={handleToggleFullscreen}
        type="ghost"
      >
        {isFullscreen ? "MINIMIZE" : "FULLSCREEN"}
      </Button>,
    );
  }

  const renderAsFunctionContext = {
    bgTheme: previewBackground.label,
    bgThemeValue: previewBackground.value,
    themeClass: themeClass, // Add theme class to context
    isDarkMode: isDarkMode, // Add dark mode state to context
  };

  const childrenToRender =
    typeof children === "function"
      ? children(renderAsFunctionContext)
      : children;

  const wrapRenderedComponentWithTheme = (component: ReactNode) => {
    if (!component) return component;

    // Keep wrapper shape stable across SSR/hydration and avoid cloning RSC children.
    return <div className={cx(themeClass)}>{component}</div>;
  };

  const wrapCodeWithTheme = (component: ReactNode) => {
    if (!component) return component;

    const normalizedChildren = React.Children.toArray(component);

    if (
      normalizedChildren.length === 1 &&
      React.isValidElement(normalizedChildren[0])
    ) {
      const element = normalizedChildren[0] as React.ReactElement<any>;
      const props = element.props as { className?: string };

      return React.cloneElement(element, {
        ...props,
        className: cx(props.className, themeClass),
      });
    }

    return <div className={cx(themeClass)}>{component}</div>;
  };

  const toRender = childrenToRender ? (
    wrapRenderedComponentWithTheme(childrenToRender)
  ) : html ? (
    <div
      className={cx(themeClass)}
      dangerouslySetInnerHTML={{ __html: html || "" }}
    />
  ) : null;

  // For code generation, use the same wrapping logic so it stays in sync
  const codeContent =
    (code && typeof code === "function"
      ? code(renderAsFunctionContext)
      : code) ||
    (typeof children === "function"
      ? children(renderAsFunctionContext)
      : children) || // Use children, handle function case
    html;

  // Apply the exact same wrapping logic for code display
  const toCode = codeContent ? wrapCodeWithTheme(codeContent) : codeContent;

  // Generate JSX string for the JSX tab
  // Use pre-generated JSX if provided (from server), otherwise generate client-side
  const generatedJSXCode =
    typeof code === "string"
      ? code
      : applyThemeClassToJsxCode(jsxCode) ||
        (toCode ? generateJSXString(toCode, codeJSXOptions) : "");

  // Use toCode for HTML rendering
  const codeForHTML = codeContent;

  const previewViewportWidth = disableBreakpoints
    ? null
    : previewBreakpoint.width;
  const previewViewportMinHeight = getScreenLikeMinHeight(previewViewportWidth);
  const shouldUseIframeViewport = previewViewportWidth !== null;

  return (
    <div className={wrapperClasses}>
      <PreviewTitleBar actions={actions} />
      <Card className={classes} {...other}>
        <div
          className={cx("previewLiveViewportArea", {
            hasFixedViewport: shouldUseIframeViewport,
          })}
        >
          <div
            className={cx("previewLiveViewport", {
              isAuto: previewViewportWidth === null,
            })}
            style={{
              width:
                previewViewportWidth === null
                  ? "100%"
                  : `${previewViewportWidth}px`,
            }}
          >
            {!shouldUseIframeViewport ? (
              <div className="previewLive">{toRender}</div>
            ) : (
              <PreviewViewportFrame
                key={previewBreakpoint.key}
                className={themeClass}
                minHeight={previewViewportMinHeight}
              >
                <div className="previewLive">{toRender}</div>
              </PreviewViewportFrame>
            )}
          </div>
        </div>
      </Card>
      {isCodeShown && hasCodePreview && toCode && (
        <Card className="codeExample">
          <CodeExample
            codeJSXOptions={codeJSXOptions}
            codeTypes={codeTypes}
            htmlThemeClass={themeClass}
            jsxCode={generatedJSXCode}
            htmlCode={codeForHTML}
          >
            {typeof code === "string" ? undefined : toCode}
          </CodeExample>
        </Card>
      )}
    </div>
  );
};

export { PreviewGenerator };
