"use client";

import { useState, type CSSProperties } from "react";
import type {
  Block,
  TextBlockProps,
  ButtonBlockProps,
  ImageBlockProps,
  VideoBlockProps,
  SectionBlockProps,
  DividerBlockProps,
  ListBlockProps,
  ListItemNode,
  FormBlockProps,
  FormInputBlockProps,
  FormSelectBlockProps,
  FormCheckboxBlockProps,
  FormRadioBlockProps,
  FormSelectOption,
  FormRadioOption,
  FormCheckboxOption,
} from "@/features/editor/state/editorStore";
import {
  normalizeVideoSourceUrl,
  resolveVideoPlatform,
  buildVideoEmbedUrl,
  computeVideoPublicTokens,
} from "@/features/editor/utils/videoHelpers";
import { computeButtonPublicTokens, computeButtonPbTokens } from "@/features/editor/utils/buttonHelpers";
import { computeTextPublicTokens } from "@/features/editor/utils/textHelpers";
import { computeDividerPublicTokens } from "@/features/editor/utils/dividerHelpers";
import { computeImagePublicTokens } from "@/features/editor/utils/imageHelpers";
import { computeListPublicTokens } from "@/features/editor/utils/listHelpers";
import { computeSectionPublicTokens } from "@/features/editor/utils/sectionHelpers";
import {
  computeFormInputPublicTokens,
  computeFormSelectPublicTokens,
  computeFormCheckboxPublicTokens,
  computeFormRadioPublicTokens,
  computeFormControllerPublicTokens,
} from "@/features/editor/utils/formHelpers";

// 섹션 레이아웃/스타일 토큰을 실제 Tailwind 클래스 조합으로 변환하는 유틸
// (에디터/퍼블릭 렌더러에서 동일한 규칙을 재사용하기 위해 export 한다.)
export function getSectionLayoutConfig(props: SectionBlockProps) {
  const backgroundClass =
    props.background === "muted"
      ? "bg-slate-900"
      : props.background === "primary"
      ? "bg-sky-900"
      : "bg-slate-950";

  // 섹션 수직 패딩: builder.css 의 pb-section-py-sm/md/lg (1rem/1.5rem/2.5rem)에 대응
  // - sm  → 1rem  → py-4
  // - md  → 1.5rem → py-6
  // - lg  → 2.5rem → py-10
  const paddingYClass =
    props.paddingY === "sm" ? "py-4" : props.paddingY === "lg" ? "py-10" : "py-6";

  const maxWidthClass =
    props.maxWidthMode === "narrow"
      ? "max-w-3xl"
      : props.maxWidthMode === "wide"
      ? "max-w-6xl"
      : "max-w-5xl";

  const gapXClass =
    props.gapX === "sm" ? "gap-4" : props.gapX === "lg" ? "gap-10" : "gap-8";

  const alignItemsClass =
    props.alignItems === "center"
      ? "items-center"
      : props.alignItems === "bottom"
      ? "items-end"
      : "items-start";

  return {
    backgroundClass,
    paddingYClass,
    maxWidthClass,
    gapXClass,
    alignItemsClass,
  } as const;
}

// 에디터 크롬 없이 실제 랜딩 페이지처럼 블록들을 렌더링하는 컴포넌트
interface PublicPageRendererProps {
  blocks: Block[];
}

const pxToEm = (px: number, base = 16) => `${px / base}em`;

const convertPxStringToEm = (value?: string | null) => {
  if (!value) return null;
  const match = value.trim().match(/-?\d+(?:\.\d+)?/);
  if (!match) return null;
  const px = parseFloat(match[0]);
  if (!Number.isFinite(px)) return null;
  return pxToEm(px);
};

export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
  const sectionBlocks = blocks.filter((b) => b.type === "section");
  const rootBlocks = blocks.filter((b) => !b.sectionId && b.type !== "section");

  const [formStatus, setFormStatus] = useState<"idle" | "submitting" | "success" | "error">("idle");
  const [formMessage, setFormMessage] = useState<string>("");

  const renderBlock = (block: Block) => {
    if ((block as any).type === "form") {
      return null;
    }

    if (block.type === "text") {
      const props = block.props as TextBlockProps;
      const tokens = computeTextPublicTokens(props);
      const textStyle: CSSProperties = { ...tokens.styleOverrides };

      return (
        <p
          key={block.id}
          className={`${tokens.alignClass} ${tokens.sizeClass} text-slate-50 leading-relaxed whitespace-pre-wrap`}
          style={textStyle}
        >
          {props.text}
        </p>
      );
    }

    if (block.type === "formInput") {
      const props = block.props as FormInputBlockProps;
      const tokens = computeFormInputPublicTokens(props);

      const baseInputClass =
        "rounded border px-3 py-2 text-xs outline-none focus:border-sky-500";

      return (
        <label
          key={block.id}
          data-testid="preview-form-input-wrapper"
          className={`${tokens.wrapperLayoutClass} text-xs text-slate-200`}
          style={tokens.wrapperStyle}
        >
          <span>{props.label}</span>
          {props.inputType === "textarea" ? (
            <textarea
              data-testid="preview-form-input"
              className={`${baseInputClass} ${tokens.widthClass}`}
              style={tokens.inputStyle}
              placeholder={props.placeholder || props.label}
            />
          ) : (
            <input
              data-testid="preview-form-input"
              className={`${baseInputClass} ${tokens.widthClass}`}
              style={tokens.inputStyle}
              type={props.inputType === "email" ? "email" : "text"}
              placeholder={props.placeholder || props.label}
            />
          )}
        </label>
      );
    }

    if (block.type === "formSelect") {
      const props = block.props as FormSelectBlockProps;
      const tokens = computeFormSelectPublicTokens(props);

      return (
        <label key={block.id} className="flex flex-col gap-1 text-xs text-slate-200">
          <span>{props.label}</span>
          <div
            data-testid="preview-form-select"
            className={`${tokens.widthClass}`}
            style={tokens.wrapperStyle}
          >
            <select
              className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-xs outline-none focus:border-sky-500"
              style={tokens.selectStyle}
              defaultValue={props.options[0]?.value ?? ""}
            >
              {props.options.map((opt) => (
                <option key={opt.value} value={opt.value}>
                  {opt.label}
                </option>
              ))}
            </select>
          </div>
        </label>
      );
    }

    if (block.type === "formCheckbox") {
      const props = block.props as FormCheckboxBlockProps;
      const tokens = computeFormCheckboxPublicTokens(props);
      const groupLabelMode = props.groupLabelMode ?? "text";

      return (
        <div
          key={block.id}
          data-testid="preview-form-checkbox-group"
          className={["flex flex-col gap-1", tokens.textSizeClass, "text-slate-200", tokens.widthClass]
            .filter(Boolean)
            .join(" ")}
          style={tokens.groupStyle}
        >
          {groupLabelMode === "image" && props.groupLabelImageUrl ? (
            // eslint-disable-next-line @next/next/no-img-element
            <img
              src={props.groupLabelImageUrl}
              alt={props.groupLabel || "폼 그룹 타이틀"}
              className="inline-block max-w-full h-auto"
              style={tokens.groupTextStyle}
            />
          ) : (
            <span style={tokens.groupTextStyle}>{props.groupLabel}</span>
          )}
          <div className="flex flex-col" data-testid="preview-form-checkbox-options" style={tokens.optionsStyle}>
            {props.options.map((opt) => (
              <label
                key={opt.value}
                data-testid="preview-form-checkbox-option-container"
                className="inline-flex items-center gap-1"
                style={tokens.optionContainerStyle}
              >
                <input
                  type="checkbox"
                  className="h-4 w-4 rounded border-slate-700 bg-slate-900 text-sky-500"
                />
                {opt.labelImageUrl ? (
                  // eslint-disable-next-line @next/next/no-img-element
                  <img
                    src={opt.labelImageUrl}
                    alt={opt.label || props.groupLabel || "체크박스 옵션"}
                    className="inline-block max-w-full h-auto"
                    style={tokens.optionTextStyle}
                  />
                ) : (
                  <span data-testid="preview-form-checkbox-option" style={tokens.optionTextStyle}>
                    {opt.label}
                  </span>
                )}
              </label>
            ))}
          </div>
        </div>
      );
    }

    if (block.type === "formRadio") {
      const props = block.props as FormRadioBlockProps;
      const tokens = computeFormRadioPublicTokens(props);
      const groupLabelMode = props.groupLabelMode ?? "text";

      return (
        <div
          key={block.id}
          data-testid="preview-form-radio-group"
          className={["flex flex-col gap-1", tokens.textSizeClass, "text-slate-200", tokens.widthClass]
            .filter(Boolean)
            .join(" ")}
          style={tokens.groupStyle}
        >
          {groupLabelMode === "image" && props.groupLabelImageUrl ? (
            // eslint-disable-next-line @next/next/no-img-element
            <img
              src={props.groupLabelImageUrl}
              alt={props.groupLabel || "폼 그룹 타이틀"}
              className="inline-block max-w-full h-auto"
              style={tokens.groupTextStyle}
            />
          ) : (
            <span style={tokens.groupTextStyle}>{props.groupLabel}</span>
          )}
          <div className="flex flex-col" data-testid="preview-form-radio-options" style={tokens.optionsStyle}>
            {props.options.map((opt) => (
              <label
                key={opt.value}
                className="inline-flex items-center gap-1"
                style={tokens.optionContainerStyle}
              >
                <input
                  type="radio"
                  className="h-4 w-4 border-slate-700 bg-slate-900 text-sky-500"
                  name={props.formFieldName}
                />
                {opt.labelImageUrl ? (
                  // eslint-disable-next-line @next/next/no-img-element
                  <img
                    src={opt.labelImageUrl}
                    alt={opt.label || props.groupLabel || "라디오 옵션"}
                    className="inline-block max-w-full h-auto"
                    style={tokens.optionTextStyle}
                  />
                ) : (
                  <span data-testid="preview-form-radio-option" style={tokens.optionTextStyle}>
                    {opt.label}
                  </span>
                )}
              </label>
            ))}
          </div>
        </div>
      );
    }

    if (block.type === "button") {
      const props = block.props as ButtonBlockProps;

      const pbTokens = computeButtonPbTokens({
        align: props.align ?? "left",
        size: props.size ?? "md",
        variant: props.variant ?? "solid",
        colorPalette: props.colorPalette ?? "primary",
        borderRadius: props.borderRadius ?? "md",
        fullWidth: props.fullWidth ?? false,
        widthMode: props.widthMode ?? undefined,
        widthPx: props.widthPx ?? undefined,
        paddingX: props.paddingX ?? undefined,
        paddingY: props.paddingY ?? undefined,
        fillColorCustom: props.fillColorCustom ?? undefined,
        strokeColorCustom: props.strokeColorCustom ?? undefined,
        textColorCustom: props.textColorCustom ?? undefined,
      });

      const publicTokens = computeButtonPublicTokens(props);
      const buttonStyle: CSSProperties = { ...publicTokens.styleOverrides };

      const wrapperClass = pbTokens.alignClass;
      const buttonClassName = [
        "pb-btn-base",
        pbTokens.sizeClass,
        pbTokens.variantClass,
        pbTokens.radiusClass,
        publicTokens.widthClass,
      ]
        .filter(Boolean)
        .join(" ");

      const hasImage = typeof (props as any).imageSrc === "string" && (props as any).imageSrc.trim() !== "";
      const placement = ((props as any).imagePlacement as string | undefined) ?? "left";

      let innerContent: React.ReactNode;
      if (!hasImage) {
        innerContent = <span className="whitespace-pre-wrap">{props.label}</span>;
      } else {
        const src = (props as any).imageSrc as string;
        const altRaw = (props as any).imageAlt as string | undefined;
        const alt = altRaw && altRaw.trim() !== "" ? altRaw.trim() : props.label;

        const imgEl = (
          // eslint-disable-next-line @next/next/no-img-element
          <img src={src} alt={alt} className="inline-block max-w-full h-auto" />
        );

        const labelSpan = <span className="whitespace-pre-wrap">{props.label}</span>;
        const isVertical = placement === "top" || placement === "bottom";
        const wrapperClassNames = [
          "inline-flex",
          "items-center",
          "gap-2",
          isVertical ? "flex-col" : "",
        ]
          .filter(Boolean)
          .join(" ");

        const first = placement === "left" || placement === "top" ? imgEl : labelSpan;
        const second = placement === "left" || placement === "top" ? labelSpan : imgEl;

        innerContent = (
          <span className={wrapperClassNames}>
            {first}
            {second}
          </span>
        );
      }

      return (
        <div key={block.id} className={wrapperClass}>
          <a href={props.href} className={buttonClassName} style={buttonStyle}>
            {innerContent}
          </a>
        </div>
      );
    }

    if (block.type === "divider") {
      const props = block.props as DividerBlockProps;
      const tokens = computeDividerPublicTokens(props);

      return (
        <div
          key={block.id}
          data-testid="preview-divider"
          className={`flex w-full ${tokens.alignClass}`}
          style={{ marginTop: tokens.wrapperMarginEm, marginBottom: tokens.wrapperMarginEm }}
        >
          <div className={tokens.innerWidthClass} style={tokens.innerStyle}>
            <div
              data-testid="preview-divider-line"
              className={tokens.thicknessClass}
              style={tokens.lineStyle}
            />
          </div>
        </div>
      );
    }

    if (block.type === "list") {
      const props = block.props as ListBlockProps;
      const tokens = computeListPublicTokens(props);

      const itemsTree: ListItemNode[] =
        (props as any).itemsTree && (props as any).itemsTree.length > 0
          ? ((props as any).itemsTree as ListItemNode[])
          : props.items && props.items.length > 0
            ? props.items.map((text, index) => ({
                id: `${block.id}_item_${index + 1}`,
                text,
                children: [],
              }))
            : [
                {
                  id: `${block.id}_item_1`,
                  text: "리스트 아이템 1",
                  children: [],
                },
              ];

      const renderListNodes = (nodes: ListItemNode[], level: number): React.ReactNode => {
        const isOrderedLevel = tokens.bulletStyle === "decimal";
        const ListTag = isOrderedLevel ? "ol" : "ul";

        return (
          <ListTag
            className={`pb-list ${tokens.alignClass}`}
            style={{
              ...tokens.listStyle,
              listStyleType:
                tokens.bulletStyle === "none"
                  ? "none"
                  : tokens.bulletStyle,
            }}
          >
            {nodes.map((node, index) => (
              <li
                key={node.id}
                style={index < nodes.length - 1 ? { marginBottom: `${tokens.gapEm}em` } : undefined}
              >
                {node.text}
                {node.children && node.children.length > 0 && renderListNodes(node.children, level + 1)}
              </li>
            ))}
          </ListTag>
        );
      };

      return renderListNodes(itemsTree, 0);
    }

    if (block.type === "video") {
      const props = block.props as VideoBlockProps;
      const rawUrl = normalizeVideoSourceUrl(props.sourceUrl ?? "");

      if (!rawUrl) {
        return null;
      }

      const platform = resolveVideoPlatform(rawUrl, props.platform ?? "auto");
      const embedUrl = buildVideoEmbedUrl(rawUrl, platform, { enableVimeoEmbed: true });

      const tokens = computeVideoPublicTokens(props);

      const startTimeSec =
        typeof (props as any).startTimeSec === "number" && (props as any).startTimeSec >= 0
          ? (props as any).startTimeSec
          : null;
      const endTimeSec =
        typeof (props as any).endTimeSec === "number" && (props as any).endTimeSec >= 0
          ? (props as any).endTimeSec
          : null;

      const isEmbed = platform === "youtube" || platform === "vimeo";

      return (
        <div key={block.id} className={`w-full flex ${tokens.justifyClass}`}>
          <div>
            <div className={`pb-video-wrapper${tokens.aspectClass}`} style={tokens.wrapperStyle}>
              {isEmbed ? (
                <iframe
                  title={props.titleText && props.titleText.trim() !== "" ? props.titleText.trim() : "비디오"}
                  src={embedUrl}
                  className="pb-video-frame"
                  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
                  allowFullScreen
                />
              ) : (
                <video
                  data-testid="preview-video"
                  className="pb-video-frame"
                  src={rawUrl}
                  style={tokens.videoStyle}
                  poster={props.posterImageSrc && props.posterImageSrc.trim() !== "" ? props.posterImageSrc.trim() : undefined}
                  aria-label={props.ariaLabel && props.ariaLabel.trim() !== "" ? props.ariaLabel.trim() : undefined}
                  onLoadedMetadata={(event) => {
                    if (startTimeSec != null) {
                      const el = event.currentTarget;
                      try {
                        el.currentTime = startTimeSec;
                      } catch {
                        // ignore
                      }
                    }
                  }}
                  onTimeUpdate={(event) => {
                    if (endTimeSec != null) {
                      const el = event.currentTarget;
                      if (el.currentTime >= endTimeSec) {
                        el.pause();
                        try {
                          if (!Number.isNaN(endTimeSec)) {
                            el.currentTime = endTimeSec;
                          }
                        } catch {
                          // ignore
                        }
                      }
                    }
                  }}
                  controls={props.controls !== false}
                  autoPlay={!!props.autoplay}
                  loop={!!props.loop}
                  muted={!!props.muted}
                />
              )}
            </div>
            {props.captionText && props.captionText.trim() !== "" && (
              <p
                data-testid="preview-video-caption"
                className="mt-2 text-xs text-slate-400 hidden"
              >
                {props.captionText}
              </p>
            )}
          </div>
        </div>
      );
    }

    if (block.type === "form") {
      const props = block.props as FormBlockProps;
      const tokens = computeFormControllerPublicTokens(block, blocks);
      const fields = tokens.fields as any[];
      const hasFields = fields.length > 0;
      const submitLabelBase = tokens.submitLabel ?? (hasFields ? "폼 전송" : null);

      const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();

        const form = e.currentTarget;
        const data = new FormData(form);

        try {
          setFormStatus("submitting");
          setFormMessage("");

          const res = await fetch("/api/forms/submit", {
            method: "POST",
            body: data,
          });

          if (res.ok) {
            setFormStatus("success");
            setFormMessage(props.successMessage ?? "성공적으로 전송되었습니다.");
            form.reset();
          } else {
            setFormStatus("error");
            setFormMessage(props.errorMessage ?? "전송 중 오류가 발생했습니다.");
          }
        } catch (error) {
          console.error("[PublicPageRenderer] form submit error", error);
          setFormStatus("error");
          setFormMessage(props.errorMessage ?? "전송 중 오류가 발생했습니다.");
        }
      };

      return (
        <form
          key={block.id}
          data-testid="preview-form-controller"
          className={tokens.formClassName}
          style={tokens.formStyle}
          onSubmit={handleSubmit}
        >
          {/* 폼 설정 전체를 서버로 함께 전달하기 위한 hidden 필드 */}
          <input type="hidden" name="__config" value={JSON.stringify(props)} />
          {hasFields && (
            <div className="flex flex-col gap-2 text-xs text-slate-200">
              {fields.map((field: any) => (
                <label key={field.id} className="flex flex-col gap-1">
                  <span>{field.label}</span>
                  {field.type === "textarea" ? (
                    <textarea
                      className="w-full min-h-[100px] rounded border border-slate-700 bg-slate-900 px-3 py-2 text-xs outline-none focus:border-sky-500"
                      name={field.name}
                      placeholder={field.label}
                      required={field.required}
                    />
                  ) : field.type === "select" ? (
                    <select
                      className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-xs outline-none focus:border-sky-500"
                      name={field.name}
                      required={field.required}
                      defaultValue={field.options?.[0]?.value ?? ""}
                    >
                      {(field.options ?? []).map((opt: FormSelectOption) => (
                        <option key={opt.value} value={opt.value}>
                          {opt.label}
                        </option>
                      ))}
                    </select>
                  ) : field.type === "checkbox" ? (
                    <div className="flex flex-col gap-1">
                      {/* 그룹 타이틀: 텍스트/이미지 모드를 지원한다. alt 는 그룹 라벨 텍스트를 사용한다. */}
                      {field.groupLabelMode === "image" && field.groupLabelImageUrl ? (
                        // eslint-disable-next-line @next/next/no-img-element
                        <img
                          src={field.groupLabelImageUrl}
                          alt={field.label}
                          className="max-h-10 w-auto max-w-[200px]"
                        />
                      ) : (
                        <span>{field.label}</span>
                      )}
                      {(field.options ?? []).map((opt: FormCheckboxOption, index: number) => (
                        <label key={opt.value} className="inline-flex items-center gap-1 text-xs">
                          <input
                            className="h-4 w-4 rounded border-slate-700 bg-slate-900 text-sky-500"
                            type="checkbox"
                            name={field.name}
                            value={opt.value}
                            required={field.required && index === 0}
                          />
                          {opt.labelImageUrl ? (
                            // eslint-disable-next-line @next/next/no-img-element
                            <img
                              src={opt.labelImageUrl}
                              alt={opt.label || field.label || "체크박스 옵션"}
                              className="h-4 w-auto"
                            />
                          ) : (
                            <span>{opt.label}</span>
                          )}
                        </label>
                      ))}
                    </div>
                  ) : field.type === "radio" ? (
                    <div className="flex flex-col gap-1">
                      {field.groupLabelMode === "image" && field.groupLabelImageUrl ? (
                        // eslint-disable-next-line @next/next/no-img-element
                        <img
                          src={field.groupLabelImageUrl}
                          alt={field.label}
                          className="max-h-10 w-auto max-w-[200px] md:max-w-[300px] lg:max-w-[400px]"
                        />
                      ) : (
                        <span>{field.label}</span>
                      )}
                      {(field.options ?? []).map((opt: FormRadioOption, index: number) => (
                        <label key={opt.value} className="inline-flex items-center gap-1 text-xs">
                          <input
                            type="radio"
                            className="h-4 w-4 border-slate-700 bg-slate-900 text-sky-500"
                            name={field.name}
                            value={opt.value}
                            required={field.required && index === 0}
                          />
                          {opt.labelImageUrl ? (
                            // eslint-disable-next-line @next/next/no-img-element
                            <img
                              src={opt.labelImageUrl}
                              alt={opt.label || field.label || "라디오 옵션"}
                              className="h-5 w-auto max-w-[120px]"
                            />
                          ) : (
                            <span>{opt.label}</span>
                          )}
                        </label>
                      ))}
                    </div>
                  ) : (
                    <input
                      className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-xs outline-none focus:border-sky-500"
                      name={field.name}
                      type={field.type === "email" ? "email" : "text"}
                      placeholder={field.label}
                      required={field.required}
                    />
                  )}
                </label>
              ))}
            </div>
          )}

          {submitLabelBase != null && (
            <button
              type="submit"
              disabled={formStatus === "submitting"}
              className="inline-flex items-center justify-center rounded border border-emerald-500 bg-emerald-600 px-4 py-2 text-xs font-medium text-white hover:bg-emerald-500 disabled:opacity-60 disabled:cursor-not-allowed"
            >
              {formStatus === "submitting" ? "전송 중..." : submitLabelBase}
            </button>
          )}

          {formStatus !== "idle" && formMessage ? (
            <p
              className={`text-xs mt-1 ${
                formStatus === "success" ? "text-emerald-400" : "text-rose-400"
              }`}
            >
              {formMessage}
            </p>
          ) : null}
        </form>
      );
    }

    if (block.type === "image") {
      const props = block.props as ImageBlockProps;
      const tokens = computeImagePublicTokens(props);

      const rawSrc = typeof props.src === "string" ? props.src.trim() : "";
      const hasSrc = rawSrc !== "";

      return (
        <div key={block.id} className={`w-full flex ${tokens.alignClass}`} style={tokens.wrapperStyle}>
          {hasSrc && (
            // eslint-disable-next-line @next/next/no-img-element
            <img src={rawSrc} alt={props.alt} className="object-contain" style={tokens.imageStyle} />
          )}
        </div>
      );
    }

    return null;
  };

  const renderSection = (section: Block) => {
    const props = section.props as SectionBlockProps;

    const { backgroundClass, paddingYClass, maxWidthClass, gapXClass, alignItemsClass } =
      getSectionLayoutConfig(props);

    const columns = props.columns && props.columns.length > 0 ? props.columns : [{ id: `${section.id}_col`, span: 12 }];
    const tokens = computeSectionPublicTokens(props);

    return (
      <section
        key={section.id}
        data-testid="preview-section"
        data-section-id={section.id}
        className={`${backgroundClass} ${typeof props.paddingYPx === "number" ? "" : paddingYClass}`}
        style={tokens.sectionStyle}
      >
        {tokens.hasBackgroundVideo && tokens.backgroundVideoSrc && (
          <video
            data-testid="preview-section-bg-video"
            className="pb-section-bg-video"
            src={tokens.backgroundVideoSrc}
            autoPlay
            loop
            muted
            playsInline
          />
        )}
        <div
          data-testid="preview-section-inner"
          className={`mx-auto ${typeof props.maxWidthPx === "number" ? "" : maxWidthClass} px-4`}
          style={tokens.innerWrapperStyle}
        >
          <div
            data-testid="preview-section-columns"
            className={`flex ${typeof props.gapXPx === "number" ? "" : gapXClass} ${alignItemsClass}`}
            style={tokens.columnsContainerStyle}
          >
            {columns.map((col) => {
              const basis = `${(col.span / 12) * 100}%`;
              const columnBlocks = blocks.filter((b) => b.sectionId === section.id && b.columnId === col.id);

              return (
                <div key={col.id} className="flex flex-col gap-4" style={{ flexBasis: basis }}>
                  {columnBlocks.map((b) => (
                    <div key={b.id}>{renderBlock(b)}</div>
                  ))}
                </div>
              );
            })}
          </div>
        </div>
      </section>
    );
  };

  return (
    <div className="flex-1 flex flex-col text-slate-50">
      {/* 루트 텍스트/버튼/이미지 블록들이 있다면 페이지 상단에 노출 */}
      {rootBlocks.length > 0 && (
        <section className="py-12">
          <div className="mx-auto max-w-3xl px-4 flex flex-col gap-4">
            {rootBlocks.map((b) => (
              <div key={b.id}>{renderBlock(b)}</div>
            ))}
          </div>
        </section>
      )}

      {/* 섹션 블록들 */}
      {sectionBlocks.map((section) => renderSection(section))}
    </div>
  );
}
