<?php

namespace FluentCrm\App\Services;
/**
 * Gutenberg Block Parser for Email
 * Converts Gutenberg blocks to email-compatible HTML
 */

use FluentCrm\App\Services\BlockRender\BlockEditorHelper;
use FluentCrm\App\Services\BlockRender\CartProduct;
use FluentCrm\App\Services\BlockRender\CartProducts;
use FluentCrm\App\Services\BlockRender\WooProduct;
use FluentCrm\App\Services\BlockRender\WooProducts;
use FluentCrm\Framework\Support\Arr;

class GutenbergEmailParser
{

    private $inlineStyles = [];

    private $childCss = '';

    private static $rssRenderCache = [];

    private $autoPaddedElements = [
        'core/heading',
        'core/paragraph',
        'core/list'
    ];

    /**
     * Parse Gutenberg blocks and convert to email HTML
     *
     * @param string $content The post content with Gutenberg blocks
     * @return string Email-compatible HTML
     */
    public function parse($content)
    {
        $this->inlineStyles = [];
        $this->childCss = '';

        // Parse blocks using WordPress function if available
        if (function_exists('parse_blocks')) {
            $blocks = parse_blocks($content);
        } else {
            // Fallback: use custom parser
            $blocks = $this->parseBlocksManually($content);
        }

        $blockHtmls = $this->renderBlocks($blocks);

        $css = $this->generateInlineStyles();

        $css = BlockEditorHelper::replaceStyleSlugsWithValues($css);

        return $css . $blockHtmls;
    }


    private function collectInlineStyles($id, $attrs, $blockName = '')
    {
        if (!$attrs) {
            return;
        }

        $idSelector = '#' . $id;

        $styleAttr = [];

        // Handling the variable css attribuytes
        if ($fontSize = Arr::get($attrs, 'fontSize')) {
            $styleAttr['font-size'] = $this->resolveFontSizeValue($fontSize);
        }

        // Some blocks (notably list) may only store preset font size as className.
        if (empty($styleAttr['font-size']) && ($className = Arr::get($attrs, 'className'))) {
            if (preg_match('/has-([a-z0-9-]+)-font-size/i', (string)$className, $matches)) {
                $styleAttr['font-size'] = $this->resolveFontSizeValue($matches[1]);
            }
        }

        if ($height = Arr::get($attrs, 'height')) {
            $styleAttr['height'] = $height;
        }

        if ($textColor = Arr::get($attrs, 'textColor')) {
            $styleAttr['color'] = 'var(--fcom--color--' . $textColor . ')';
        }

        if ($backgroundColor = Arr::get($attrs, 'backgroundColor')) {
            $colorValue = 'var(--fcom--color--' . $backgroundColor . ')';
            if ($blockName === 'core/separator') {
                $styleAttr['color'] = $colorValue;
            } else {
                $styleAttr['background-color'] = $colorValue;
            }
        }

        $textAlign = Arr::get($attrs, 'align');
        if ($blockName === 'core/button') {
            $textAlign = Arr::get($attrs, 'textAlign');
        }

        if ($textAlign) {
            $styleAttr['text-align'] = $this->getDirectionalTextAlign($textAlign);
        }

        $className = (string)Arr::get($attrs, 'className', '');
        if (preg_match('/has-([a-z0-9-]+)-font-family/i', $className, $fontFamilyMatch)) {
            $resolvedFontFamily = $this->resolveFontFamilyValue($fontFamilyMatch[1]);
            if ($resolvedFontFamily) {
                $styleAttr['font-family'] = $resolvedFontFamily;
            }
        }
        if (empty($styleAttr['font-family']) && ($fontFamily = Arr::get($attrs, 'fontFamily'))) {
            $resolvedFontFamily = $this->resolveFontFamilyValue($fontFamily);
            if ($resolvedFontFamily) {
                $styleAttr['font-family'] = $resolvedFontFamily;
            }
        }

        $style = Arr::get($attrs, 'style', []);

        $childCss = '';
        $hadPadding = false;

        if ($style && is_array($style)) {
            if ($linkColor = Arr::get($style, 'elements.link.color.text')) {
                $childCss .= $idSelector . ' a { color: ' . $this->transformToCssVar($linkColor) . '; }';
            }

            if ($color = Arr::get($style, 'color.text')) {
                $styleAttr['color'] = $color;
            }

            if ($bgColor = Arr::get($style, 'color.background')) {
                if ($blockName === 'core/separator') {
                    $styleAttr['color'] = $bgColor;
                } else {
                    $styleAttr['background-color'] = $bgColor;
                }
            }

            if ($paddings = Arr::get($style, 'spacing.padding', [])) {
                foreach ($paddings as $paddingType => $padding) {
                    if (!$padding) {
                        continue;
                    }
                    $hadPadding = true;
                    $styleAttr['padding-' . $paddingType] = $this->transformToCssVar($padding);
                }
            }

            if ($margins = Arr::get($style, 'spacing.margin', [])) {
                foreach ($margins as $marginType => $margin) {
                    if (!$margin) {
                        continue;
                    }
                    $styleAttr['margin-' . $marginType] = $this->transformToCssVar($margin);
                }
            }

            if ($borderConfig = Arr::get($style, 'border', [])) {
                $width = Arr::get($borderConfig, 'width', '0px');
                $color = Arr::get($borderConfig, 'color', '');

                if (!$color) {
                    if ($borderColor = Arr::get($attrs, 'borderColor', '')) {
                        $color = 'var(--fcom--color--' . $borderColor . ')';
                    }
                }

                $borderStyle = Arr::get($borderConfig, 'style', 'solid');

                if ($width || $color) {
                    $styleAttr['border'] = trim($width . ' ' . $borderStyle . ' ' . $color);
                }

                foreach (['top', 'right', 'bottom', 'left'] as $borderSide) {
                    $sideConfig = Arr::get($borderConfig, $borderSide, []);
                    if (!$sideConfig || !is_array($sideConfig)) {
                        continue;
                    }

                    $sideWidth = $this->normalizeCssSize(Arr::get($sideConfig, 'width', ''));
                    $sideStyle = Arr::get($sideConfig, 'style', 'solid');
                    $sideColor = Arr::get($sideConfig, 'color', '');

                    if ($sideColor && strpos($sideColor, 'var:') === 0) {
                        $sideColor = $this->transformToCssVar($sideColor);
                    } elseif ($sideColor && strpos($sideColor, '#') !== 0 && strpos($sideColor, 'rgb') !== 0 && strpos($sideColor, 'hsl') !== 0 && strpos($sideColor, 'var(') !== 0) {
                        $sideColor = 'var(--fcom--color--' . $sideColor . ')';
                    }

                    if (!$sideWidth && !$sideColor) {
                        continue;
                    }

                    $styleAttr['border-' . $borderSide] = trim($sideWidth . ' ' . $sideStyle . ' ' . $sideColor);
                }

                $borderRadius = Arr::get($borderConfig, 'radius', []);

                if (is_string($borderRadius) || is_numeric($borderRadius)) {
                    $styleAttr['border-radius'] = $this->normalizeCssSize($borderRadius);
                } elseif ($borderRadius) {
                    $radiusTypeMap = [
                        'topLeft'     => 'top-left',
                        'topRight'    => 'top-right',
                        'bottomLeft'  => 'bottom-left',
                        'bottomRight' => 'bottom-right'
                    ];

                    foreach ($borderRadius as $radiusType => $radius) {
                        if (!$radius || !isset($radiusTypeMap[$radiusType])) {
                            continue;
                        }
                        $styleAttr['border-' . $radiusTypeMap[$radiusType] . '-radius'] = $radius;
                    }
                }
            }

            if ($minHeight = Arr::get($style, 'dimensions.minHeight')) {
                $styleAttr['min-height'] = $this->normalizeCssSize($minHeight);
            }

            $typoGraphis = Arr::get($style, 'typography', []);

            foreach ($typoGraphis as $typographyType => $typoValue) {
                if (!$typoValue || !is_string($typoValue)) {
                    continue;
                }

                $cssProperty = $this->camelCaseToKebabCase($typographyType);

                // Normalize font-size slugs/tokens to a valid CSS size.
                if ($cssProperty === 'font-size') {
                    if (strpos($typoValue, 'var:') === 0) {
                        $typoValue = $this->transformToCssVar($typoValue);
                    }
                    $typoValue = $this->resolveFontSizeValue($typoValue);
                } else if ($cssProperty === 'font-family') {
                    $typoValue = $this->resolveFontFamilyValue($typoValue);
                } else if (strpos($typoValue, 'var:') === 0) {
                    // Convert Gutenberg var:preset|... tokens to CSS vars.
                    $typoValue = $this->transformToCssVar($typoValue);
                }

                $styleAttr[$cssProperty] = $typoValue;
            }
        }

        if ($blockName === 'core/list') {
            if (!empty($styleAttr['font-size'])) {
                $size = $styleAttr['font-size'];
                $childCss .= $idSelector . ' li, ' . $idSelector . ' li p { font-size: ' . $size . '; }';
            }
            if (!empty($styleAttr['line-height'])) {
                $lineHeight = $styleAttr['line-height'];
                $childCss .= $idSelector . ' li, ' . $idSelector . ' li p { line-height: ' . $lineHeight . '; }';
            }
        } else if ($blockName === 'core/button') {
            $className = (string)Arr::get($attrs, 'className', '');
            $isOutline = strpos($className, 'is-style-outline') !== false;

            // Keep button text centered by default unless explicitly set.
            if (empty($styleAttr['text-align'])) {
                $styleAttr['text-align'] = 'center';
            }

            if (empty($styleAttr['line-height'])) {
                $styleAttr['line-height'] = '1.5';
            }

            if ($isOutline) {
                $backgroundColor = Arr::get($styleAttr, 'background-color');
                $hasExplicitBackground = !empty($backgroundColor);

                if (!$hasExplicitBackground) {
                    $styleAttr['background-color'] = 'transparent';
                }

                if (empty($styleAttr['border'])) {
                    $borderWidth = Arr::get($attrs, 'style.border.width', '2px');
                    $borderStyle = Arr::get($attrs, 'style.border.style', 'solid');
                    $borderColor = Arr::get($attrs, 'style.border.color', 'currentColor');

                    if (is_numeric($borderWidth)) {
                        $borderWidth .= 'px';
                    }
                    if (is_string($borderColor) && strpos($borderColor, 'var:') === 0) {
                        $borderColor = $this->transformToCssVar($borderColor);
                    }

                    $styleAttr['border'] = trim($borderWidth . ' ' . $borderStyle . ' ' . $borderColor);
                }
            }
        } else if ($blockName === 'core/buttons') {
            $justifyContent = Arr::get($attrs, 'layout.justifyContent', 'left');
            $alignMap = [
                'left'          => 'left',
                'center'        => 'center',
                'right'         => 'right',
                'space-between' => 'start',
                'space-around'  => 'center',
                'space-evenly'  => 'center'
            ];

            if (isset($alignMap[(string)$justifyContent])) {
                $styleAttr['text-align'] = $this->getDirectionalTextAlign($alignMap[(string)$justifyContent]);
            } else if (empty($styleAttr['text-align'])) {
                $styleAttr['text-align'] = $this->getDirectionalTextAlign('left');
            }
        }

        if (!empty($styleAttr['background-color']) && !$hadPadding && in_array($blockName, $this->autoPaddedElements)) {
            $styleAttr['padding'] = '20px';
        }

        $this->inlineStyles[$idSelector] = $styleAttr;
        $this->childCss .= $childCss . ' ';
    }

    private function setDefaultBlockStyles($id, $styles = [])
    {
        $idSelector = '#' . $id;

        if (!isset($this->inlineStyles[$idSelector])) {
            $this->inlineStyles[$idSelector] = $styles;
            return;
        }

        foreach ($styles as $property => $value) {
            if (!isset($this->inlineStyles[$idSelector][$property])) {
                $this->inlineStyles[$idSelector][$property] = $value;
            }
        }

        return;
    }

    private function getDirectionalTextAlign($align, $default = 'left')
    {
        $align = strtolower(trim((string)$align));
        if ($align === '') {
            $align = strtolower((string)$default);
        }

        if ($align === 'start') {
            $align = 'left';
        } else if ($align === 'end') {
            $align = 'right';
        }

        if (!in_array($align, ['left', 'center', 'right', 'justify'], true)) {
            $align = strtolower((string)$default);
        }

        if (fluentcrm_is_rtl()) {
            if ($align === 'left') {
                return 'right';
            }

            if ($align === 'right') {
                return 'left';
            }
        }

        return $align;
    }

    private function camelCaseToKebabCase($string)
    {
        return strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $string));
    }

    private function transformToCssVar($cssProperty)
    {
        if (strpos($cssProperty, 'var:') === 0) {
            $parts = explode('|', substr($cssProperty, 4));
            if (count($parts) === 3) {
                return 'var(--wp--' . str_replace('|', '--', $parts[0] . '--' . $parts[1] . '--' . $parts[2]) . ')';
            }
        }

        return $cssProperty;
    }

    private function normalizeCssSize($value)
    {
        if ($value === null || $value === '') {
            return '';
        }

        $value = trim((string)$value);

        if (strpos($value, 'var:') === 0) {
            return $this->transformToCssVar($value);
        }

        return $value;
    }

    private function normalizeHtmlWidthAttribute($width)
    {
        $width = trim((string)$width);

        if ($width === '') {
            return '';
        }

        if (substr($width, -2) === 'px') {
            return preg_replace('/[^0-9.]/', '', $width);
        }

        return $width;
    }

    private function generateInlineStyles()
    {

        $css = '<style type="text/css">';

        foreach ($this->inlineStyles as $selector => $styles) {
            $css .= $selector . ' { ';
            foreach ($styles as $property => $value) {
                $css .= $property . ': ' . $value . '; ';
            }
            $css .= '} ';
        }

        if ($this->childCss) {
            $css .= $this->childCss;
        }

        $css .= '</style>';

        return $css;
    }

    /**
     * Manual block parser (fallback if parse_blocks not available)
     */
    private function parseBlocksManually($content)
    {
        $blocks = [];
        $pattern = '/<!--\s+wp:([a-z][a-z0-9_-]*\/)?([a-z][a-z0-9_-]*)\s+(\{.*?\})?\s+(\/)?-->/';

        preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE);

        $lastOffset = 0;
        foreach ($matches[0] as $index => $match) {
            $blockName = ($matches[1][$index][0] ?? '') . $matches[2][$index][0];
            $attrs = $matches[3][$index][0] ?? '{}';
            $isSelfClosing = !empty($matches[4][$index][0]);

            $blockStart = $match[1] + strlen($match[0]);

            // Find closing tag if not self-closing
            if (!$isSelfClosing) {
                $closingPattern = '/<!--\s+\/wp:' . preg_quote($blockName, '/') . '\s+-->/';
                if (preg_match($closingPattern, $content, $closeMatch, PREG_OFFSET_CAPTURE, $blockStart)) {
                    $innerHTML = substr($content, $blockStart, $closeMatch[0][1] - $blockStart);
                    $lastOffset = $closeMatch[0][1] + strlen($closeMatch[0][0]);
                } else {
                    $innerHTML = '';
                }
            } else {
                $innerHTML = '';
            }

            $blocks[] = [
                'blockName'   => $blockName,
                'attrs'       => json_decode($attrs, true) ?? [],
                'innerHTML'   => trim($innerHTML),
                'innerBlocks' => []
            ];
        }

        return $blocks;
    }

    /**
     * Render blocks to email HTML
     */
    private function renderBlocks($blocks, $nested = false)
    {
        $html = '';

        foreach ($blocks as $block) {
            if (empty($block['blockName'])) {
                // Classic content or unrecognized block
                if (!empty($block['innerHTML'])) {
                    $html .= $this->wrapInTable($block['innerHTML']);
                }
                continue;
            }

            $html .= $this->renderBlock($block, $nested);
        }

        return $html;
    }

    /**
     * Render individual block
     */
    private function renderBlock($block, $isNested = false)
    {
        $isInvisible = isset($block['attrs']['metadata']['blockVisibility']) && $block['attrs']['metadata']['blockVisibility'] === false;
        if ($isInvisible) {
            return '';
        }

        $blockName = $block['blockName'];
        $attrs = $block['attrs'] ?? [];
        $innerHTML = $block['innerHTML'] ?? '';
        $innerBlocks = $block['innerBlocks'] ?? [];

        // Per-block conditional visibility (conditional-content has its own handler)
        if ($blockName !== 'fluent-crm/conditional-content') {
            if (!$this->checkBlockConditionVisibility($attrs)) {
                return '';
            }
        }

        // create an unique element ID for blocks that don't have one, to help with styling if needed
        $elementId = uniqid('block-', false);
        $this->collectInlineStyles($elementId, $attrs, $blockName);

        $attrs['elem_id'] = $elementId;
        $attrs['is_root'] = !$isNested;

        // For blocks with innerContent array, reconstruct innerHTML
        if (empty($innerHTML) && !empty($block['innerContent'])) {
            $innerHTML = implode('', array_filter($block['innerContent'], 'is_string'));
        }

        // Handle different block types
        switch ($blockName) {
            case 'core/block':
                return $this->renderSyncedPattern($attrs, $isNested);

            case 'core/paragraph': // done
                return $this->renderParagraph($innerHTML, $attrs);

            case 'core/heading': // done
                return $this->renderHeading($innerHTML, $attrs);

            case 'core/image': // done
                return $this->renderImage($attrs, $innerHTML);

            case 'core/list': // done
                return $this->renderList($innerHTML, $innerBlocks, $attrs);

            case 'core/list-item': // done
                return $this->renderListItem($innerHTML, $attrs);

            case 'core/quote': // done
                return $this->renderQuote($innerHTML, $innerBlocks, $attrs);

            case 'core/button': // done
                return $this->renderButton($innerHTML, $attrs);

            case 'core/buttons': // done
                return $this->renderButtons($innerBlocks, $attrs);

            case 'core/columns': // done
                return $this->renderColumns($innerBlocks, $attrs);

            case 'core/column': // done
                return $this->renderColumn($innerBlocks, $attrs, $innerHTML);

            case 'core/separator': // partially done
                return $this->renderSeparator($innerHTML, $attrs);

            case 'core/spacer': // done
                return $this->renderSpacer($innerHTML, $attrs);

            case 'core/group': // done
                return $this->renderGroup($innerBlocks, $attrs, $innerHTML);

            case 'core/row':
                return $this->renderRow($innerBlocks, $attrs, $innerHTML);

            case 'core/table': // done
                return $this->renderTable($innerHTML, $attrs);

            case 'core/rss': // done
                return $this->renderRss($attrs);

            case 'fluentcrm/woo-product':
            case 'fluent-crm/woo-product':
                return $this->renderWooProductBlock($block, $attrs, $innerHTML);

            case 'fluent-crm/cart-product':
                return $this->renderCartProductBlock($block, $attrs, $innerHTML);

            case 'fluent-crm/latest-posts': // partially done
                return $this->renderLatestPostsBlock($block, $attrs);

            case 'fluent-crm/woo-products': // partially done
                return $this->renderProductsBlock($block, $attrs, $blockName);

            case 'fluent-crm/cart-products': // partially done
                return $this->renderCartProductsBlock($block, $attrs);

            case 'fluent-crm/conditional-content': // done
            case 'fluentcrm/conditional-group': // done
                return $this->renderConditionalGroupBlock($innerBlocks, $attrs, $innerHTML);

            case 'core/freeform': // done
            case 'core/html': // done
                // Classic editor content - render as-is with email-safe wrapper
                return $this->wrapInTable($innerHTML, $attrs);

            case 'core/preformatted': // done
            case 'core/code': // done
            case 'core/verse': // done
                return $this->renderCodeBlock($innerHTML, $attrs);
            case 'core/pullquote': // done
                return $this->renderPullQuote($innerHTML, $attrs);
            case 'core/embed':
            case 'core/video':
            case 'core/audio':
                // For video/audio embeds in email, show a linked thumbnail or text
                $url = $attrs['url'] ?? '';
                if (!empty($url)) {
                    $linkText = __('Click here to view media content', 'fluent-crm');
                    return $this->wrapInTable("<p style=\"text-align: center; padding: 20px; background: #f0f0f0;\"><a href=\"{$url}\" style=\"color: #0073aa; text-decoration: underline;\">{$linkText}</a></p>", $attrs);
                }
                return '';
            default:
                // Fallback for unrecognized blocks
                if (!empty($innerHTML)) {
                    $attrs['td_id'] = $attrs['elem_id'] ?? '';
                    return $this->wrapInTable($innerHTML, $attrs);
                }
                if (!empty($innerBlocks)) {
                    return $this->renderBlocks($innerBlocks, true);
                }
                return '';
        }
    }

    /**
     * Render paragraph block
     */
    private function renderParagraph($content, $attrs)
    {
        $content = trim($content);

        // Skip empty paragraphs
        if (empty($content) || $content === '<p></p>') {
            return '';
        }

        // Extract content if it's wrapped in <p> tags
        if (preg_match('/<p[^>]*>(.*?)<\/p>/s', $content, $matches)) {
            $innerContent = $matches[1];
        } else {
            $innerContent = $content;
        }

        $elementId = $attrs['elem_id'] ?? '';

        return $this->wrapInTable("<p id=\"{$elementId}\">{$innerContent}</p>", $attrs);
    }

    /**
     * Render heading block
     */
    private function renderHeading($content, $attrs)
    {
        $level = $attrs['level'] ?? 2;

        // Extract content if it's wrapped in heading tags
        if (preg_match('/<h[1-6][^>]*>(.*?)<\/h[1-6]>/s', $content, $matches)) {
            $innerContent = $matches[1];
        } else {
            $innerContent = $content;
        }
        // find the existing classnames from $content and preserve them in the new heading tag
        $className = '';
        if (preg_match('/<h[1-6][^>]*class=["\']([^"\']*)["\'][^>]*>/s', $content, $matches)) {
            $className = $matches[1];
        }

        $elemId = $attrs['elem_id'] ?? '';

        return $this->wrapInTable("<h{$level} class='$className' id='$elemId'>{$innerContent}</h{$level}>", $attrs);
    }

    /**
     * Render image block
     */
    private function renderImage($attrs, $innerHTML = '')
    {

        // get the content between <figure> tags if exists, as sometimes image block can have that wrapper in innerHTML
        if (preg_match('/<figure[^>]*>(.*?)<\/figure>/s', $innerHTML, $matches)) {
            $innerHTML = $matches[1];
        }

        $id = $attrs['elem_id'] ?? '';

        // add the id to the <img tag if exists in innerHTML
        if ($id && preg_match('/<img[^>]*>/s', $innerHTML, $matches)) {
            $imgTag = $matches[0];
            // add id attribute to img tag
            if (strpos($imgTag, 'id=') === false) {
                $newImgTag = str_replace('<img', '<img id="' . $id . '"', $imgTag);
                $innerHTML = str_replace($imgTag, $newImgTag, $innerHTML);
            }
        }

        $align = Arr::get($attrs, 'align', 'left');

        $class = 'fc_image';

        $class .= ' fc_align_' . $align;

        $html = '<div class="' . $class . '">' . $innerHTML . '</div>';

        return $this->wrapInTable($html, $attrs);
    }

    /**
     * Render list block
     */
    private function renderList($content, $innerBlocks, $attrs)
    {
        $ordered = $attrs['ordered'] ?? false;
        $tag = $ordered ? 'ol' : 'ul';

        $id = $attrs['elem_id'] ?? '';

        $classes = ['fc_list_item'];
        if ($attrsClass = trim((string)Arr::get($attrs, 'className', ''))) {
            $classes = array_merge($classes, preg_split('/\s+/', $attrsClass));
        }
        if (preg_match('/<' . $tag . '[^>]*class=["\']([^"\']+)["\']/i', $content, $matches)) {
            $classes = array_merge($classes, preg_split('/\s+/', trim((string)$matches[1])));
        }
        $classes = array_filter(array_unique($classes));
        $classAttr = implode(' ', $classes);

        $fontSizeValue = '';
        if ($fontSize = Arr::get($attrs, 'fontSize')) {
            $fontSizeValue = $this->resolveFontSizeValue($fontSize);
        } elseif (preg_match('/has-([a-z0-9-]+)-font-size/i', $classAttr, $matches)) {
            $fontSizeValue = $this->resolveFontSizeValue($matches[1]);
        }

        $lineHeightValue = Arr::get($attrs, 'style.typography.lineHeight', '');
        if (is_string($lineHeightValue) && strpos($lineHeightValue, 'var:') === 0) {
            $lineHeightValue = $this->transformToCssVar($lineHeightValue);
        }
        if (!is_string($lineHeightValue)) {
            $lineHeightValue = '';
        }

        $listItems = '';
        // If we have innerBlocks, render them
        if (!empty($innerBlocks)) {
            $listItems = '';
            foreach ($innerBlocks as $block) {
                if ($block['blockName'] === 'core/list-item') {
                    $listItems .= $this->renderListItem($this->buildListItemContent($block), $block['attrs'] ?? []);
                }
            }
        }

        if (!$listItems) {
            return '';
        }

        $inlineStyle = '';
        if ($fontSizeValue) {
            $inlineStyle .= 'font-size:' . $fontSizeValue . ';';
            if ($id) {
                $this->childCss .= '#' . $id . ' li, #' . $id . ' li p { font-size: ' . $fontSizeValue . '; } ';
            }
        }
        if ($lineHeightValue) {
            $inlineStyle .= 'line-height:' . $lineHeightValue . ';';
            if ($id) {
                $this->childCss .= '#' . $id . ' li, #' . $id . ' li p { line-height: ' . $lineHeightValue . '; } ';
            }
        }

        $styleAttr = $inlineStyle ? " style=\"{$inlineStyle}\"" : '';

        return $this->wrapInTable("<{$tag} class='{$classAttr}' id='$id'{$styleAttr}>{$listItems}</{$tag}>", $attrs);
    }

    /**
     * Render list item
     */
    private function renderListItem($content, $attrs)
    {
        if (!$this->checkBlockConditionVisibility($attrs)) {
            return '';
        }

        $fontSizeValue = '';
        $fontFamilyValue = '';

        // Explicit typography style on list item takes highest priority.
        $styleFontSize = Arr::get($attrs, 'style.typography.fontSize', '');
        if (is_string($styleFontSize) && $styleFontSize !== '') {
            if (strpos($styleFontSize, 'var:') === 0) {
                $styleFontSize = $this->transformToCssVar($styleFontSize);
            }
            $fontSizeValue = $this->resolveFontSizeValue($styleFontSize);
        }

        // Preset slug from attrs (e.g. fc-small).
        if (!$fontSizeValue && ($fontSize = Arr::get($attrs, 'fontSize'))) {
            $fontSizeValue = $this->resolveFontSizeValue($fontSize);
        }

        // Fallback: detect preset class directly from li markup.
        if (!$fontSizeValue && is_string($content) && preg_match('/has-([a-z0-9-]+)-font-size/i', $content, $matches)) {
            $fontSizeValue = $this->resolveFontSizeValue($matches[1]);
        }

        // Explicit typography style on list item takes highest priority.
        $styleFontFamily = Arr::get($attrs, 'style.typography.fontFamily', '');
        if (is_string($styleFontFamily) && $styleFontFamily !== '') {
            $fontFamilyValue = $this->resolveFontFamilyValue($styleFontFamily);
        }

        // Preset slug or raw stack from attrs.
        if (!$fontFamilyValue && ($fontFamily = Arr::get($attrs, 'fontFamily'))) {
            $fontFamilyValue = $this->resolveFontFamilyValue($fontFamily);
        }

        // Fallback: detect font-family preset class from li markup.
        if (!$fontFamilyValue && is_string($content) && preg_match('/has-([a-z0-9-]+)-font-family/i', $content, $matches)) {
            $fontFamilyValue = $this->resolveFontFamilyValue($matches[1]);
        }

        if (!$fontSizeValue && !$fontFamilyValue) {
            return $content;
        }

        // Ensure list item typography is inline for email clients.
        $content = preg_replace_callback('/<li\b([^>]*)>/i', function ($matches) use ($fontSizeValue, $fontFamilyValue) {
            $attrs = $matches[1];
            $styleParts = [];
            if ($fontSizeValue) {
                $styleParts[] = 'font-size:' . $fontSizeValue;
            }
            if ($fontFamilyValue) {
                $styleParts[] = 'font-family:' . $fontFamilyValue;
            }
            $appendedStyles = implode(';', $styleParts);

            if (preg_match('/\sstyle=(["\'])(.*?)\1/i', $attrs, $styleMatch)) {
                $existingStyle = rtrim(trim($styleMatch[2]), ';');
                $updatedStyle = $existingStyle . ';' . $appendedStyles;
                return str_replace($styleMatch[0], ' style="' . esc_attr($updatedStyle) . '"', $matches[0]);
            }

            return '<li' . $attrs . ' style="' . esc_attr($appendedStyles) . '">';
        }, $content, 1);

        return $content;
    }

    /**
     * Build a list item's inner HTML, re-inserting nested list blocks.
     *
     * The block parser strips nested core/list blocks out of a list item's
     * innerHTML, leaving null placeholders in innerContent. Without this
     * reconstruction, every nested list level is lost from the sent email.
     *
     * @param array $block Parsed core/list-item block.
     * @return string
     */
    private function buildListItemContent($block)
    {
        $innerBlocks = $block['innerBlocks'] ?? [];
        $innerContent = $block['innerContent'] ?? [];

        if (empty($innerBlocks) || empty($innerContent)) {
            return $block['innerHTML'] ?? '';
        }

        $content = '';
        $blockIndex = 0;
        foreach ($innerContent as $chunk) {
            if (is_string($chunk)) {
                $content .= $chunk;
                continue;
            }

            // A null chunk marks the position of the next inner block.
            $innerBlock = isset($innerBlocks[$blockIndex]) ? $innerBlocks[$blockIndex] : null;
            $blockIndex++;

            if ($innerBlock && $innerBlock['blockName'] === 'core/list') {
                $content .= $this->renderNestedList($innerBlock);
            }
        }

        return $content;
    }

    /**
     * Render a nested list as a bare <ul>/<ol>, without the table wrapper
     * used for root-level lists (a table is not valid inside an <li>).
     *
     * @param array $listBlock Parsed core/list block nested inside a list item.
     * @return string
     */
    private function renderNestedList($listBlock)
    {
        $attrs = $listBlock['attrs'] ?? [];

        if (!$this->checkBlockConditionVisibility($attrs)) {
            return '';
        }

        $tag = !empty($attrs['ordered']) ? 'ol' : 'ul';

        $listItems = '';
        foreach (($listBlock['innerBlocks'] ?? []) as $item) {
            if ($item['blockName'] === 'core/list-item') {
                $listItems .= $this->renderListItem($this->buildListItemContent($item), $item['attrs'] ?? []);
            }
        }

        if (!$listItems) {
            return '';
        }

        $classes = ['fc_list_item'];
        if ($attrsClass = trim((string)Arr::get($attrs, 'className', ''))) {
            $classes = array_merge($classes, preg_split('/\s+/', $attrsClass));
        }
        if (preg_match('/<' . $tag . '[^>]*class=["\']([^"\']+)["\']/i', (string)($listBlock['innerHTML'] ?? ''), $matches)) {
            $classes = array_merge($classes, preg_split('/\s+/', trim((string)$matches[1])));
        }
        $classAttr = implode(' ', array_filter(array_unique($classes)));

        return "<{$tag} class='{$classAttr}'>{$listItems}</{$tag}>";
    }

    /**
     * Render the core RSS block with email-safe markup.
     *
     * @param array $attrs Block attributes.
     * @return string
     */
    private function renderRss($attrs)
    {
        $feedUrl = esc_url_raw((string)Arr::get($attrs, 'feedURL', ''));
        if (!$feedUrl || !$this->isSafeRssFeedUrl($feedUrl)) {
            return '';
        }

        $rssCacheKey = md5(wp_json_encode([
            'feed_url'       => $feedUrl,
            'items_to_show'  => (int)Arr::get($attrs, 'itemsToShow', 5),
            'display_date'   => !empty($attrs['displayDate']),
            'display_author' => !empty($attrs['displayAuthor']),
            'display_excerpt' => !empty($attrs['displayExcerpt']),
            'excerpt_length' => (int)Arr::get($attrs, 'excerptLength', 55),
            'open_new_tab'   => !empty($attrs['openInNewTab']),
            'rel'            => (string)Arr::get($attrs, 'rel', '')
        ]));

        if (isset(self::$rssRenderCache[$rssCacheKey])) {
            return $this->wrapRssHtmlWithCurrentBlock(self::$rssRenderCache[$rssCacheKey], $attrs);
        }

        if (!function_exists('fetch_feed') && defined('ABSPATH') && defined('WPINC')) {
            require_once ABSPATH . WPINC . '/feed.php';
        }

        if (!function_exists('fetch_feed')) {
            return '';
        }

        $rssRequestArgsFilter = function ($requestArgs, $url) use ($feedUrl) {
            if ($url === $feedUrl) {
                $requestArgs['timeout'] = 5;
                $requestArgs['redirection'] = 3;
                $requestArgs['reject_unsafe_urls'] = true;
            }

            return $requestArgs;
        };

        add_filter('http_request_args', $rssRequestArgsFilter, 10, 2);
        $rss = fetch_feed($feedUrl);
        remove_filter('http_request_args', $rssRequestArgsFilter, 10);

        if (is_wp_error($rss) || !$rss || !method_exists($rss, 'get_item_quantity')) {
            return '';
        }

        $itemsToShow = max(1, min(20, (int)Arr::get($attrs, 'itemsToShow', 5)));
        $quantity = $rss->get_item_quantity($itemsToShow);
        if (!$quantity) {
            return '';
        }

        $items = $rss->get_items(0, $quantity);
        if (!$items) {
            return '';
        }

        $listItems = '';
        $displayDate = !empty($attrs['displayDate']);
        $displayAuthor = !empty($attrs['displayAuthor']);
        $displayExcerpt = !empty($attrs['displayExcerpt']);
        $excerptLength = max(1, (int)Arr::get($attrs, 'excerptLength', 55));
        $openInNewTab = !empty($attrs['openInNewTab']);
        $rel = trim((string)Arr::get($attrs, 'rel', ''));

        $linkAttrs = '';
        if ($openInNewTab) {
            $linkAttrs .= ' target="_blank"';
        }
        if ($rel !== '') {
            $linkAttrs .= ' rel="' . esc_attr($rel) . '"';
        }

        foreach ($items as $item) {
            $title = trim(wp_strip_all_tags(html_entity_decode((string)$item->get_title(), ENT_QUOTES, get_option('blog_charset'))));
            if ($title === '') {
                $title = __('(no title)', 'fluent-crm');
            }

            $link = esc_url((string)$item->get_link());
            $titleHtml = $link
                ? '<a href="' . $link . '"' . $linkAttrs . '>' . esc_html($title) . '</a>'
                : esc_html($title);

            $metaHtml = '';

            if ($displayDate) {
                $timestamp = $item->get_date('U');
                if ($timestamp) {
                    $gmtOffset = get_option('gmt_offset');
                    $timestamp += (int)((float)$gmtOffset * HOUR_IN_SECONDS);
                    $metaHtml .= '<span class="wp-block-rss__item-publish-date" style="display:block;font-size:13px;color:#6b7280;">' .
                        esc_html(date_i18n(get_option('date_format'), $timestamp)) .
                        '</span>';
                }
            }

            if ($displayAuthor) {
                $author = $item->get_author();
                if (is_object($author) && method_exists($author, 'get_name')) {
                    $authorName = trim(wp_strip_all_tags((string)$author->get_name()));
                    if ($authorName !== '') {
                        $metaHtml .= '<span class="wp-block-rss__item-author" style="display:block;font-size:13px;color:#6b7280;">' .
                            sprintf(
                                /* translators: %s: author name. */
                                esc_html__('by %s', 'fluent-crm'),
                                esc_html($authorName)
                            ) .
                            '</span>';
                    }
                }
            }

            $excerptHtml = '';
            if ($displayExcerpt) {
                $description = html_entity_decode((string)$item->get_description(), ENT_QUOTES, get_option('blog_charset'));
                $description = trim(wp_strip_all_tags($description));
                if ($description !== '') {
                    $excerptHtml = '<div class="wp-block-rss__item-excerpt" style="margin-top:6px;">' .
                        esc_html(wp_trim_words($description, $excerptLength, ' [...]')) .
                        '</div>';
                }
            }

            $listItems .= '<div class="wp-block-rss__item" style="margin-bottom:12px;">' .
                '<div class="wp-block-rss__item-title">' . $titleHtml . '</div>' .
                $metaHtml .
                $excerptHtml .
                '</div>';
        }

        if (!$listItems) {
            return '';
        }

        self::$rssRenderCache[$rssCacheKey] = $listItems;

        return $this->wrapRssHtmlWithCurrentBlock($listItems, $attrs);
    }

    /**
     * Validate RSS feed URLs before the server fetches remote content.
     *
     * @param string $url Feed URL.
     * @return bool
     */
    private function isSafeRssFeedUrl($url)
    {
        $parsed = wp_parse_url($url);
        if (!$parsed || empty($parsed['host'])) {
            return false;
        }

        $scheme = strtolower(isset($parsed['scheme']) ? $parsed['scheme'] : '');
        if (!in_array($scheme, ['http', 'https'], true)) {
            return false;
        }

        $host = trim($parsed['host'], '[]');
        if (filter_var($host, FILTER_VALIDATE_IP)) {
            $ip = $host;
        } else {
            $ip = gethostbyname($host);
            if ($ip === $host && !filter_var($ip, FILTER_VALIDATE_IP)) {
                return false;
            }
        }

        $isSafe = (bool)filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);

        /**
         * Allow site owners to enforce custom RSS feed source policies.
         *
         * @param bool   $isSafe Whether the feed URL resolves to a public IP.
         * @param string $url    Feed URL.
         * @param string $host   Parsed URL host.
         * @param string $ip     Resolved host IP.
         */
        return (bool)apply_filters('fluent_crm/rss_block_is_safe_feed_url', $isSafe, $url, $host, $ip);
    }

    /**
     * Wrap cached RSS list items with the current block id and table attributes.
     *
     * @param string $listItems Rendered RSS item markup.
     * @param array  $attrs Block attributes.
     * @return string
     */
    private function wrapRssHtmlWithCurrentBlock($listItems, $attrs)
    {
        $elementId = esc_attr(Arr::get($attrs, 'elem_id', ''));
        $html = '<div id="' . $elementId . '" class="wp-block-rss" style="padding:0;margin:0;">' . $listItems . '</div>';

        return $this->wrapInTable($html, $attrs);
    }

    /**
     * Render quote block
     */
    private function renderQuote($content, $innerBlocks, $attrs)
    {

        $borderColor = BlockEditorHelper::getBorderColor($attrs, '#4f46e5');

        $quoteSide = fluentcrm_is_rtl() ? 'right' : 'left';
        $styles = "border-{$quoteSide}: 4px solid " . $borderColor . '; padding-' . $quoteSide . ': 20px;';

        if (!isset($attrs['style']['spacing']['margin']['bottom'])) {
            $styles .= ' margin-bottom: 20px;';
        }

        $html = '';
        if ($innerBlocks) {
            $html = $this->renderBlocks($innerBlocks, true);
        }

        if (!$html) {
            return '';
        }

        // get <cite> content if exists in $content
        if (preg_match('/<cite[^>]*>(.*?)<\/cite>/s', $content, $matches)) {
            $cite = '<div style="font-style: italic; font-size: 90%;">' . $matches[1] . '</div>';
            $html .= $cite;
        }

        $id = $attrs['elem_id'] ?? '';

        return $this->wrapInTable("<div id='$id' style=\"{$styles}\">{$html}</div>", $attrs);
    }

    /**
     * Render buttons container
     */
    private function renderButtons($innerBlocks, $attrs)
    {
        $layout = $attrs['layout'] ?? [];
        $justifyContent = $layout['justifyContent'] ?? 'left';
        $orientation = strtolower((string)($layout['orientation'] ?? 'horizontal'));
        $isVertical = ($orientation === 'vertical');

        $alignMap = [
            'left'          => 'left',
            'center'        => 'center',
            'right'         => 'right',
            'space-between' => 'start', // For email, align to start in the active direction
            'space-around'  => 'center', // For email, we'll center and let spacing
        ];

        $textAlign = $this->getDirectionalTextAlign($alignMap[$justifyContent] ?? 'left');

        $tableStyles = "width: 100%; border-collapse: collapse; text-align: {$textAlign};";

        $elementId = $attrs['elem_id'] ?? '';

        unset($attrs['elem_id']);

        $innerHtml = <<<HTML
        <table role="presentation" style="$tableStyles" align="$textAlign" class="fc_buttons_wrap" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td id="$elementId" class="fc_buttons_inner">
        HTML;


        $buttonsHtml = '';

        foreach ($innerBlocks as $button) {
            if ($button['blockName'] === 'core/button') {
                $buttonElementId = uniqid('block-', false);
                $button['attrs']['elem_id'] = $buttonElementId;
                $bttonHtml = $this->renderButton($button['innerHTML'], $button['attrs'] ?? []);
                if (!$bttonHtml) {
                    continue;
                }

                if ($isVertical) {
                    $buttonsHtml .= '<div style="display:block;">' . $bttonHtml . '</div>';
                } else {
                    $buttonsHtml .= $bttonHtml;
                }

                $this->collectInlineStyles($buttonElementId, $button['attrs'], 'core/button');

            }
        }

        if (!$buttonsHtml) {
            return '';
        }

        $innerHtml .= $buttonsHtml;

        $innerHtml .= "</td></tr></table>";

        return $this->wrapInTable($innerHtml, $attrs);
    }

    /**
     * Render button block
     */
    private function renderButton($content, $attrs)
    {
        if (!$this->checkBlockConditionVisibility($attrs)) {
            return '';
        }

        // Extract URL and text from content
        $url = '#';
        $text = '';

        if (!empty($content)) {
            // Try to extract from anchor tag
            if (preg_match('/<a[^>]*href=["\']([^"\']*)["\'][^>]*>(.*?)<\/a>/s', $content, $matches)) {
                $url = $matches[1];
                $rawText = $matches[2];
                // Remove all HTML tags but keep the text
                $text = trim(preg_replace('/<[^>]*>/', '', $rawText));
            }
        }

        // Fallback to attrs if extraction failed
        if ($url === '#' && !empty($attrs['url'])) {
            $url = $attrs['url'];
        }
        if (!empty($attrs['text'])) {
            $text = $attrs['text'];
        }

        if (!$text) {
            return '';
        }

        $elementId = $attrs['elem_id'] ?? '';

        $styles = '';

        if ($width = Arr::get($attrs, 'width')) {
            $styles .= "min-width: {$width}%;";
        }

        $class = 'fc_button';
        $className = (string)Arr::get($attrs, 'className', '');
        if ($className) {
            $class .= ' ' . $className;
        }

        return "<a class='" . esc_attr($class) . "' id=\"" . esc_attr($elementId) . "\" style='" . esc_attr($styles) . "' href=\"" . $this->escapeButtonUrl($url) . "\">" . esc_html($text) . "</a>";
    }

    /**
     * Escape button URLs without stripping smartcodes before the parser phase.
     *
     * Campaign smartcodes are parsed after Gutenberg blocks are rendered, so running
     * esc_url() on a smartcode URL here removes the curly braces and makes the later
     * parser miss it. Keep only smartcode-bearing safe-protocol URLs intact with
     * esc_attr(); static URLs and unsafe protocols still go through esc_url().
     */
    private function escapeButtonUrl($url)
    {
        $url = (string)$url;

        if (preg_match('/^http:\/\/(\{\{[^{}\r\n]+}}|##[^#\r\n]+##)$/i', trim($url), $matches)) {
            $url = $matches[1];
        }

        if (!preg_match('/(\{\{[^{}\r\n]+}}|##[^#\r\n]+##)/', $url)) {
            return esc_url($url);
        }

        if (!preg_match('/^([a-z][a-z0-9+.-]*):/i', ltrim($url), $matches)) {
            return esc_attr($url);
        }

        $protocol = strtolower($matches[1]);

        if (in_array($protocol, ['http', 'https', 'mailto', 'tel'], true)) {
            return esc_attr($url);
        }

        return esc_url($url);
    }

    /**
     * Render legacy Woo single product block.
     */
    private function renderWooProductBlock($block, $attrs, $innerHTML)
    {
        if (!defined('WC_PLUGIN_FILE')) {
            return '';
        }

        $buttonText = !empty($attrs['buttonText']) ? $attrs['buttonText'] : __('Buy Now', 'fluent-crm');
        $buttonUrl = '#';

        if (!empty($attrs['productId']) && function_exists('wc_get_product')) {
            $product = wc_get_product((int)$attrs['productId']);
            if ($product && method_exists($product, 'get_permalink')) {
                $buttonUrl = $product->get_permalink();
            }
        }

        if (!empty($innerHTML)) {
            if (preg_match('/<a[^>]*href=["\']([^"\']+)["\'][^>]*>/i', $innerHTML, $urlMatch)) {
                $buttonUrl = $urlMatch[1];
            }
            if (preg_match('/<a[^>]*>(.*?)<\/a>/is', $innerHTML, $textMatch)) {
                $parsedText = trim(wp_strip_all_tags($textMatch[1]));
                if ($parsedText) {
                    $buttonText = $parsedText;
                }
            }
        }

        $buttonBlock = $this->getFirstButtonBlock($block);
        $buttonAttrs = Arr::get($buttonBlock, 'attrs', []);
        $buttonInnerHTML = Arr::get($buttonBlock, 'innerHTML', '');

        if (!empty($buttonInnerHTML)) {
            if (preg_match('/<a[^>]*href=["\']([^"\']+)["\'][^>]*>/i', $buttonInnerHTML, $urlMatch)) {
                $buttonUrl = $urlMatch[1];
            }

            if (preg_match('/<a[^>]*>(.*?)<\/a>/is', $buttonInnerHTML, $textMatch)) {
                $parsedText = trim(wp_strip_all_tags($textMatch[1]));
                if ($parsedText) {
                    $buttonText = $parsedText;
                }
            }
        }

        if (!empty($buttonAttrs['url'])) {
            $buttonUrl = $buttonAttrs['url'];
        }

        if (!empty($buttonAttrs['text'])) {
            $buttonText = $buttonAttrs['text'];
        }

        $buttonElementId = uniqid('block-', false);
        $buttonAttrs['elem_id'] = $buttonElementId;

        $buttonHtml = $this->renderButton(
            '<a href="' . esc_url($buttonUrl) . '">' . esc_html($buttonText) . '</a>',
            $buttonAttrs
        );

        if (!$buttonHtml) {
            $buttonHtml = '<a href="' . esc_url($buttonUrl) . '">' . esc_html($buttonText) . '</a>';
        } else {
            $this->collectInlineStyles($buttonElementId, $buttonAttrs, 'core/button');
        }

        $html = WooProduct::renderProduct($buttonHtml, [
            'blockName' => 'fluentcrm/woo-product',
            'attrs'     => $attrs
        ]);

        if (!$html) {
            return '';
        }

        $attrs['td_id'] = $attrs['elem_id'] ?? '';
        return $this->wrapInTable($html, $attrs);
    }

    /**
     * Find the first nested Gutenberg button block inside product blocks.
     */
    private function getFirstButtonBlock($block)
    {
        $innerBlocks = Arr::get($block, 'innerBlocks', []);

        foreach ($innerBlocks as $innerBlock) {
            if (Arr::get($innerBlock, 'blockName') === 'core/button') {
                return $innerBlock;
            }

            $buttonBlock = $this->getFirstButtonBlock($innerBlock);
            if ($buttonBlock) {
                return $buttonBlock;
            }
        }

        return [];
    }

    /**
     * Render FluentCart single product block.
     */
    private function renderCartProductBlock($block, $attrs, $innerHTML)
    {
        if (!defined('FLUENTCART_VERSION')) {
            return '';
        }

        $buttonText = !empty($attrs['buttonText']) ? $attrs['buttonText'] : __('Buy Now', 'fluent-crm');
        $buttonUrl = '#';

        if (!empty($attrs['productId'])) {
            $productPermalink = get_permalink((int)$attrs['productId']);
            if ($productPermalink) {
                $buttonUrl = $productPermalink;
            }
        }

        if (!empty($innerHTML)) {
            if (preg_match('/<a[^>]*href=["\']([^"\']+)["\'][^>]*>/i', $innerHTML, $urlMatch)) {
                $buttonUrl = $urlMatch[1];
            }
            if (preg_match('/<a[^>]*>(.*?)<\/a>/is', $innerHTML, $textMatch)) {
                $parsedText = trim(wp_strip_all_tags($textMatch[1]));
                if ($parsedText) {
                    $buttonText = $parsedText;
                }
            }
        }

        $buttonAttrs = Arr::get($this->getFirstButtonBlock($block), 'attrs', []);
        $buttonElementId = uniqid('block-', false);
        $buttonAttrs['elem_id'] = $buttonElementId;

        $buttonHtml = $this->renderButton(
            '<a href="' . esc_url($buttonUrl) . '">' . esc_html($buttonText) . '</a>',
            $buttonAttrs
        );

        if (!$buttonHtml) {
            $buttonHtml = '<a href="' . esc_url($buttonUrl) . '">' . esc_html($buttonText) . '</a>';
        } else {
            $this->collectInlineStyles($buttonElementId, $buttonAttrs, 'core/button');
        }

        $html = CartProduct::renderProduct($buttonHtml, [
            'blockName' => 'fluent-crm/cart-product',
            'attrs'     => $attrs
        ]);

        if (!$html) {
            return '';
        }

        $attrs['td_id'] = $attrs['elem_id'] ?? '';
        return $this->wrapInTable($html, $attrs);
    }

    /**
     * Render legacy latest posts block from FluentCampaign (if available).
     */
    private function renderLatestPostsBlock($block, $attrs)
    {
        if (!class_exists('\FluentCampaign\App\Services\PostParser\LatestPost')) {
            return '';
        }

        try {
            $html = \FluentCampaign\App\Services\PostParser\LatestPost::renderPosts('', [
                'blockName' => 'fluent-crm/latest-posts',
                'attrs'     => $attrs,
                'innerHTML' => isset($block['innerHTML']) ? $block['innerHTML'] : ''
            ]);

            $attrs['elem_id'] = $attrs['elem_id'] ?? '';
            return $this->wrapInTable($html, $attrs);

        } catch (\Throwable $e) {
            return '';
        }
    }

    /**
     * Render product listing blocks via dedicated renderer.
     */
    private function renderProductsBlock($block, $attrs, $blockName)
    {
        $html = WooProducts::renderProducts('', [
            'blockName' => $blockName,
            'attrs'     => $attrs,
            'innerHTML' => isset($block['innerHTML']) ? $block['innerHTML'] : ''
        ]);

        if (!$html) {
            return '';
        }

        $attrs['td_id'] = $attrs['elem_id'] ?? '';
        return $this->wrapInTable($html, $attrs);

    }

    private function renderCartProductsBlock($block, $attrs)
    {
        $html = CartProducts::renderProducts('', [
            'blockName' => 'fluent-crm/cart-products',
            'attrs'     => $attrs,
            'innerHTML' => isset($block['innerHTML']) ? $block['innerHTML'] : ''
        ]);

        if (!$html) {
            return '';
        }

        $attrs['td_id'] = $attrs['elem_id'] ?? '';
        return $this->wrapInTable($html, $attrs);
    }

    /**
     * Render conditional group block based on subscriber tags.
     */
    private function renderConditionalGroupBlock($innerBlocks, $attrs, $innerHTML)
    {
        $content = '';
        if (!empty($innerBlocks)) {
            $content = $this->renderBlocks($innerBlocks, true);
        } elseif (!empty($innerHTML)) {
            $content = $innerHTML;
        }

        if (!$content) {
            return '';
        }

        $content = $this->wrapInTable($content, $attrs);

        $subscriber = BlockParserHelper::getSubscriber();
        if (!$subscriber) {
            $subscriber = apply_filters('fluent_crm/get_current_block_condition_subscriber', $subscriber);
        }

        // Keep content visible in preview/test contexts when subscriber is unknown.
        if (!$subscriber) {
            return $content;
        }

        $tagIds = isset($attrs['tag_ids']) && is_array($attrs['tag_ids']) ? $attrs['tag_ids'] : [];
        if (!$tagIds) {
            return '';
        }

        $checkType = $this->normalizeConditionalCheckType(isset($attrs['condition_type']) ? $attrs['condition_type'] : 'show_if_tag_exist');
        $tagMatched = method_exists($subscriber, 'hasAnyTagId') ? $subscriber->hasAnyTagId($tagIds) : false;

        if ($checkType === 'show_if_tag_exist') {
            return $tagMatched ? $content : '';
        }

        if ($checkType === 'show_if_tag_not_exist') {
            return $tagMatched ? '' : $content;
        }

        return '';
    }


    /**
     * Render a synced pattern (core/block) by looking up its content from fc_meta
     * and recursively rendering the contained blocks.
     */
    private static $syncedPatternCache = [];

    private function renderSyncedPattern($attrs, $nested = false)
    {
        $ref = isset($attrs['ref']) ? (int) $attrs['ref'] : 0;
        if (!$ref) {
            return '';
        }

        if (!isset(self::$syncedPatternCache[$ref])) {
            $pattern = \FluentCrm\App\Models\Meta::where('object_type', 'email_pattern')
                ->where('id', $ref)
                ->first();

            self::$syncedPatternCache[$ref] = ($pattern && !empty($pattern->value['content']))
                ? $pattern->value['content']
                : '';
        }

        $content = self::$syncedPatternCache[$ref];
        if (!$content) {
            return '';
        }

        $blocks = parse_blocks($content);
        if (empty($blocks)) {
            return '';
        }

        return $this->renderBlocks($blocks, $nested);
    }

    private function renderCodeBlock($innerHTML, $attrs)
    {
        $code = '';
        // get the content between <pre> tags if exists, as sometimes code block can have that wrapper in innerHTML
        if (preg_match('/<pre[^>]*>(.*?)<\/pre>/s', $innerHTML, $matches)) {
            $code = $matches[1];
        }


        if (!$code) {
            return '';
        }

        $elementId = $attrs['elem_id'] ?? '';

        if (!isset($this->inlineStyles['#' . $elementId]['background-color'])) {
            $this->inlineStyles['#' . $elementId]['background-color'] = '#e5e7eb';
        }

        return $this->wrapInTable("<pre id=\"{$elementId}\" style=\"font-family: monospace; padding: 15px; overflow-x: auto; text-wrap: auto; width: 100%; border-radius: 4px;\">{$code}</pre>", $attrs);

    }

    private function renderPullQuote($innerHTML, $attrs)
    {
        $html = '';
        // get the content between <pre> tags if exists, as sometimes code block can have that wrapper in innerHTML
        if (preg_match('/<blockquote[^>]*>(.*?)<\/blockquote>/s', $innerHTML, $matches)) {
            $html = $matches[1];
        }


        if (!$html) {
            return '';
        }

        $elementId = $attrs['elem_id'] ?? '';
        $elementSelector = $elementId ? ('#' . $elementId) : '';
        $existingStyles = $elementSelector ? ($this->inlineStyles[$elementSelector] ?? []) : [];

        $hasBlockFontSize = !empty(Arr::get($existingStyles, 'font-size')) ||
            !empty(Arr::get($attrs, 'fontSize')) ||
            !empty(Arr::get($attrs, 'style.typography.fontSize'));

        // Pullquote defaults for title paragraph:
        // - Always ensure margin + line-height when missing.
        // - Add 25px font-size only when no font-size is configured.
        $hasParagraphFontSize = preg_match('/<p[^>]*(?:style=["\'][^"\']*font-size\s*:|class=["\'][^"\']*has-[a-z0-9-]+-font-size)/i', $html);
        $needsDefaultTitleFontSize = (!$hasBlockFontSize && !$hasParagraphFontSize);

        $firstPApplied = false;
        $html = preg_replace_callback('/<p([^>]*)>/i', function ($matches) use (&$firstPApplied, $needsDefaultTitleFontSize) {
            if ($firstPApplied) {
                return $matches[0];
            }
            $firstPApplied = true;

            $attrs = $matches[1];
            if (preg_match('/style=(["\'])(.*?)\1/i', $attrs, $styleMatch)) {
                $quote = $styleMatch[1];
                $styleValue = rtrim($styleMatch[2], ';');

                if ($needsDefaultTitleFontSize && !preg_match('/font-size\s*:/i', $styleValue)) {
                    $styleValue .= '; font-size: 25px';
                }
                if (!preg_match('/(?:^|;)\s*margin\s*:/i', $styleValue)) {
                    $styleValue .= '; margin: 0 0 1em 0';
                }
                if (!preg_match('/line-height\s*:/i', $styleValue)) {
                    $styleValue .= '; line-height: 1.6';
                }

                $styleValue = trim($styleValue, " ;") . ';';
                $attrs = preg_replace('/style=(["\'])(.*?)\1/i', 'style=' . $quote . $styleValue . $quote, $attrs, 1);
                return '<p' . $attrs . '>';
            }

            $styleParts = [];
            if ($needsDefaultTitleFontSize) {
                $styleParts[] = 'font-size: 25px';
            }
            $styleParts[] = 'margin: 0 0 1em 0';
            $styleParts[] = 'line-height: 1.6';

            return '<p' . $attrs . ' style="' . implode('; ', $styleParts) . ';">';
        }, $html, 1);

        if (!$hasBlockFontSize && !preg_match('/<cite[^>]*(?:style=["\'][^"\']*font-size\s*:|class=["\'][^"\']*has-[a-z0-9-]+-font-size)/i', $html)) {
            $html = preg_replace_callback('/<cite([^>]*)>/i', function ($matches) {
                $attrs = $matches[1];
                if (preg_match('/style=(["\'])(.*?)\1/i', $attrs, $styleMatch)) {
                    $quote = $styleMatch[1];
                    $styleValue = rtrim($styleMatch[2], ';') . '; font-size: 16px;';
                    $attrs = preg_replace('/style=(["\'])(.*?)\1/i', 'style=' . $quote . $styleValue . $quote, $attrs, 1);
                    return '<cite' . $attrs . '>';
                }

                return '<cite' . $attrs . ' style="font-size: 16px;">';
            }, $html, 1);
        }

        $styles = 'width: 100%;';

        $defaultStyles = [
            'padding-left'   => '20px',
            'padding-right'  => '20px',
            'padding-bottom' => '20px',
            'padding-top'    => '20px',
            'margin-bottom'  => '20px',
            'text-align'     => $this->getDirectionalTextAlign(Arr::get($attrs, 'textAlign', 'left'))
        ];

        if ($elementId) {
            $hasCustomBorder = isset($existingStyles['border'])
                || isset($existingStyles['border-left'])
                || isset($existingStyles['border-right'])
                || isset($existingStyles['border-top'])
                || isset($existingStyles['border-bottom']);

            if (!$hasCustomBorder) {
                $defaultStyles['border-left'] = '4px solid #e5e7eb';
                $defaultStyles['border-right'] = '4px solid #e5e7eb';
                $defaultStyles['border-bottom'] = '4px solid #e5e7eb';
                $defaultStyles['border-top'] = '4px solid #e5e7eb';
            }

            $this->setDefaultBlockStyles($elementId, $defaultStyles);
        }

        return $this->wrapInTable("<div class='fc_pull_qoute' id=\"{$elementId}\" style=\"$styles\">{$html}</div>", $attrs);

    }

    /**
     * Check per-block conditional visibility attributes.
     * Returns true if the block should be shown, false if hidden.
     */
    private function checkBlockConditionVisibility($attrs)
    {
        $conditionType = isset($attrs['fcrmConditionType']) ? $attrs['fcrmConditionType'] : '';
        $conditionType = $this->normalizeConditionalCheckType($conditionType);

        if (empty($conditionType)) {
            return true;
        }

        $subscriber = BlockParserHelper::getSubscriber();

        if (!$subscriber) {
            $subscriber = apply_filters('fluent_crm/get_current_block_condition_subscriber', $subscriber);
        }

        if (!$subscriber) {
            return true;
        }

        $tagIds = isset($attrs['fcrmTagIds']) && is_array($attrs['fcrmTagIds']) ? $attrs['fcrmTagIds'] : [];
        if (empty($tagIds)) {
            return true;
        }

        $tagMatched = $subscriber->hasAnyTagId($tagIds);

        if ($conditionType === 'show_if_tag_exist') {
            return $tagMatched;
        }

        if ($conditionType === 'show_if_tag_not_exist') {
            return !$tagMatched;
        }

        return true;
    }

    /**
     * Keep backward compatibility with legacy conditional values.
     */
    private function normalizeConditionalCheckType($checkType)
    {
        $checkType = trim((string)$checkType);
        if ($checkType === '') {
            return '';
        }

        $map = [
            // v2 legacy values
            'show_if_tag_exists'     => 'show_if_tag_exist',
            'show_if_tag_not_exists' => 'show_if_tag_not_exist',
        ];

        return Arr::get($map, $checkType, $checkType);
    }

    /**
     * Render columns block
     */
    private function renderColumns($innerBlocks, $attrs)
    {
        if (empty($innerBlocks)) {
            return '';
        }

        $hasBlockGap = isset($attrs['style']['spacing']['blockGap']['left']);
        $blockGap = 20;

        if ($hasBlockGap) {
            $blockGap = (int)str_replace('px', '', $attrs['style']['spacing']['blockGap']['left']);
        }

        $id = $attrs['elem_id'] ?? '';

        $verticalAlignment = Arr::get($attrs, 'verticalAlignment', 'top');

        $columnCount = count($innerBlocks);
        $columnWidth = floor(100 / $columnCount);
        $hasExplicitColumnWidths = false;

        foreach ($innerBlocks as $innerBlock) {
            if (!empty(Arr::get($innerBlock, 'attrs.width'))) {
                $hasExplicitColumnWidths = true;
                break;
            }
        }

        $isMobileStackable = Arr::get($attrs, 'isStackedOnMobile', true);

        $tableClass = 'fc_columns';

        if ($isMobileStackable) {
            $tableClass .= ' fc_columns_stack_mobile';
        }

        $columnTableStyles = [
            'width'            => '100%',
            'border-collapse'  => 'collapse',
            'border-spacing'   => '0'
        ];

        if ($margins = Arr::get($attrs, 'style.spacing.margin', [])) {
            foreach ($margins as $marginType => $marginValue) {
                if (!$marginValue) {
                    continue;
                }

                $columnTableStyles['margin-' . $marginType] = $this->normalizeCssSize($marginValue);
            }
        }

        $columnsHtml = '<table class="' . $tableClass . '" role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="' . BlockEditorHelper::renderStyles($columnTableStyles) . '"><tr>';

        foreach ($innerBlocks as $index => $column) {

            $blockName = $column['blockName'] ?? '';

            if ($blockName === 'core/column') {
                $elementId = uniqid('block-', false);
                $this->collectInlineStyles($elementId, $column['attrs'] ?? [], 'core/column');

                $columnVerticalAlignment = Arr::get($column, 'attrs.verticalAlignment', $verticalAlignment);
                $styles = [
                    'vertical-align' => $columnVerticalAlignment,
                ];

                $columnWidthValue = $this->normalizeCssSize(Arr::get($column, 'attrs.width', ''));
                $widthAttr = '';

                if ($columnWidthValue) {
                    $styles['width'] = $columnWidthValue;

                    if (substr($columnWidthValue, -2) === 'px') {
                        $styles['max-width'] = $columnWidthValue;
                    }

                    $widthAttr = $this->normalizeHtmlWidthAttribute($columnWidthValue);
                } elseif (!$hasExplicitColumnWidths) {
                    $styles['width'] = $columnWidth . '%';
                    $widthAttr = $columnWidth . '%';
                }

                $padding = $blockGap / 2;

                $styles['padding-left'] = $padding . 'px';
                $styles['padding-right'] = $padding . 'px';

                $widthMarkup = $widthAttr ? ' width="' . esc_attr($widthAttr) . '"' : '';
                $columnsHtml .= '<td class="fc_column"' . $widthMarkup . ' style="' . BlockEditorHelper::renderStyles($styles) . '">';

                $column['attrs']['elem_id'] = $elementId;

                $columnsHtml .= $this->renderColumn($column['innerBlocks'], $column['attrs'], $column['innerHTML']);
                $columnsHtml .= '</td>';
            } else {
                $columnsHtml .= '<td width="' . $columnWidth . '%" style="vertical-align: ' . $verticalAlignment . '; padding: 0 10px;">';
                $columnsHtml .= $this->renderBlock($column, true);
                $columnsHtml .= '</td>';
            }


        }

        $columnsHtml .= '</tr></table>';

        $attrs['td_id'] = $id;

        return $this->wrapInTable($columnsHtml, $attrs);
    }

    /**
     * Render column block
     */
    private function renderColumn($innerBlocks, $attrs, $innerHTML)
    {
        $html = '';

        if (!empty($innerBlocks)) {
            $html = $this->renderBlocks($innerBlocks, true);
        } elseif (!empty($innerHTML)) {
            $html = $innerHTML;
        }

        $attrs['td_id'] = $attrs['elem_id'] ?? '';

        return $this->wrapInTable($html, $attrs);
    }

    /**
     * Render cover block
     */
    private function renderCover($innerBlocks, $attrs, $innerHTML)
    {
        $url = $attrs['url'] ?? '';
        $dimRatio = $attrs['dimRatio'] ?? 50;
        $overlayColor = $attrs['overlayColor'] ?? '';
        $style = $attrs['style'] ?? [];
        $contentPosition = $attrs['contentPosition'] ?? 'center center';
        $minHeight = $attrs['minHeight'] ?? '';
        $minHeightUnit = $attrs['minHeightUnit'] ?? 'px';

        // Extract image URL from innerHTML if not in attrs
        if (empty($url) && !empty($innerHTML)) {
            if (preg_match('/src=["\']([^"\']+)["\']/', $innerHTML, $matches)) {
                $url = $matches[1];
            }
        }

        $opacity = $dimRatio / 100;

        // Parse content position (e.g., "top center", "center center", "bottom left")
        $verticalAlign = 'center';
        $textAlign = 'center';

        if (!empty($contentPosition)) {
            $positions = explode(' ', $contentPosition);
            if (count($positions) >= 2) {
                $verticalAlign = $positions[0]; // top, center, bottom
                $textAlign = $positions[1]; // left, center, right
            } elseif (count($positions) === 1) {
                $verticalAlign = $positions[0];
            }
        }

        $textAlign = $this->getDirectionalTextAlign($textAlign, 'center');

        // Map vertical alignment to table cell vertical-align
        $vAlignStyle = $verticalAlign === 'top' ? 'top' : ($verticalAlign === 'bottom' ? 'bottom' : 'middle');

        // Determine minimum height
        $minHeightValue = $minHeight ? $minHeight . $minHeightUnit : '300px';

        // Render inner content
        $innerContent = '';
        if (!empty($innerBlocks)) {
            foreach ($innerBlocks as $block) {
                $innerContent .= $this->renderBlock($block);
            }
        }

        // For email, create a table-based layout with background image
        if ($url) {
            $html = '<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0; background-image: url(\'' . $url . '\'); background-size: cover; background-position: center; min-height: ' . $minHeightValue . ';">';
            $html .= '<tr>';
            $html .= '<td style="padding: 40px 20px; vertical-align: ' . $vAlignStyle . '; text-align: ' . $textAlign . '; background-color: rgba(0,0,0,' . $opacity . '); color: #ffffff; min-height: ' . $minHeightValue . ';">';
            $html .= $innerContent;
            $html .= '</td>';
            $html .= '</tr>';
            $html .= '</table>';
            return $html;
        }

        return $this->wrapInTable($innerContent);
    }

    /**
     * Render separator block
     */
    private function renderSeparator($innerHTML, $attrs)
    {
        $class = 'fc_separator';
        if ($className = Arr::get($attrs, 'className')) {
            $class .= ' ' . $className;
        }

        $id = $attrs['elem_id'] ?? '';
        $separatorColor = Arr::get($attrs, 'style.color.background', '');

        if (!$separatorColor) {
            $separatorColor = Arr::get($attrs, 'backgroundColor', '');
        }

        if ($separatorColor && strpos($separatorColor, '#') !== 0 && strpos($separatorColor, 'rgb') !== 0 && strpos($separatorColor, 'hsl') !== 0 && strpos($separatorColor, 'var(') !== 0 && strpos($separatorColor, 'var:') !== 0) {
            $separatorColor = 'var(--fcom--color--' . $separatorColor . ')';
        }

        if ($separatorColor && strpos($separatorColor, 'var:') === 0) {
            $separatorColor = $this->transformToCssVar($separatorColor);
        }

        if ($separatorColor && strpos($separatorColor, 'var(') === 0) {
            $separatorColor = BlockEditorHelper::replaceStyleSlugsWithValues($separatorColor);
        }

        if (!$separatorColor) {
            $separatorColor = '#d1d5db';
        }

        $escapedId = esc_attr($id);
        $escapedClass = esc_attr($class);
        $escapedSeparatorColor = esc_attr($separatorColor);

        $isDots = strpos($class, 'is-style-dots') !== false;
        $hasWideAlignmentClass = strpos($class, 'alignwide') !== false || strpos($class, 'alignfull') !== false;
        $isWide = strpos($class, 'is-style-wide') !== false || $hasWideAlignmentClass || in_array(Arr::get($attrs, 'align', ''), ['wide', 'full'], true);

        if ($isDots) {
            $separator = "<div id='$escapedId' class='$escapedClass' style='border:none;text-align:center;line-height:1;color:$escapedSeparatorColor;font-size:24px;letter-spacing:10px;'>&middot;&middot;&middot;</div>";
            return $this->wrapInTable($separator, $attrs);
        }

        $lineWidth = $isWide ? '100%' : '100px';
        $separator = "<hr id='$escapedId' class='$escapedClass' style='margin-left: auto;margin-right: auto;border:none;height:2px;line-height:2px;font-size:0;background-color:$escapedSeparatorColor;width:$lineWidth;' />";

        return $this->wrapInTable($separator, $attrs);
    }

    /**
     * Render spacer block
     */
    private function renderSpacer($innerHtml, $attrs)
    {
        $id = $attrs['elem_id'] ?? '';

        return $this->wrapInTable("<div id='$id'>&nbsp;</div>", $attrs);
    }

    /**
     * Render group block
     */
    private function renderGroup($innerBlocks, $attrs, $innerHTML)
    {
        $layoutType = strtolower((string)Arr::get($attrs, 'layout.type', ''));
        if ($layoutType === 'flex') {
            return $this->renderRow($innerBlocks, $attrs, $innerHTML);
        }

        if (!empty($innerBlocks)) {
            $content = $this->renderBlocks($innerBlocks, true);
        } elseif (!empty($innerHTML)) {
            $content = $innerHTML;
        } else {
            return '';
        }

        $id = $attrs['elem_id'] ?? '';
        $disableBottomSpacing = !empty($attrs['fcrmDisableBottomSpacing']);
        $groupClasses = ['fc_group'];

        if (!empty($attrs['is_root'])) {
            $groupClasses[] = 'fc_group_root';
        }

        if ($disableBottomSpacing) {
            $groupClasses[] = 'fcrm-no-bottom-spacing';
        }

        $contentSize = $this->normalizeCssSize(Arr::get($attrs, 'layout.contentSize', ''));
        if ($contentSize) {
            $innerStyles = [
                'max-width'    => $contentSize,
                'margin-left'  => 'auto',
                'margin-right' => 'auto'
            ];

            $content = "<div class='fc_group_content' style='" . BlockEditorHelper::renderStyles($innerStyles) . "'>{$content}</div>";
        }

        return $this->wrapInTable("<div id='$id' class='" . esc_attr(implode(' ', $groupClasses)) . "'>{$content}</div>", $attrs);
    }

    /**
     * Render row block.
     */
    private function renderRow($innerBlocks, $attrs, $innerHTML)
    {
        if (empty($innerBlocks)) {
            if (!empty($innerHTML)) {
                return $this->wrapInTable($innerHTML, $attrs);
            }

            return '';
        }

        $layout = Arr::get($attrs, 'layout', []);
        $orientation = strtolower((string)Arr::get($layout, 'orientation', 'horizontal'));
        $className = strtolower((string)Arr::get($attrs, 'className', ''));
        if (strpos($className, 'is-vertical') !== false) {
            $orientation = 'vertical';
        }

        // A vertical row behaves like stacked content in email clients.
        if ($orientation === 'vertical') {
            $content = !empty($innerBlocks) ? $this->renderBlocks($innerBlocks, true) : $innerHTML;
            if (!$content) {
                return '';
            }

            $id = $attrs['elem_id'] ?? '';
            $disableBottomSpacing = !empty($attrs['fcrmDisableBottomSpacing']);
            $groupClasses = ['fc_group'];
            if (!empty($attrs['is_root'])) {
                $groupClasses[] = 'fc_group_root';
            }
            if ($disableBottomSpacing) {
                $groupClasses[] = 'fcrm-no-bottom-spacing';
            }

            $contentSize = $this->normalizeCssSize(Arr::get($attrs, 'layout.contentSize', ''));
            if ($contentSize) {
                $innerStyles = [
                    'max-width'    => $contentSize,
                    'margin-left'  => 'auto',
                    'margin-right' => 'auto'
                ];
                $content = "<div class='fc_group_content' style='" . BlockEditorHelper::renderStyles($innerStyles) . "'>{$content}</div>";
            }

            return $this->wrapInTable("<div id='$id' class='" . esc_attr(implode(' ', $groupClasses)) . "'>{$content}</div>", $attrs);
        }

        $justifyContent = strtolower((string)Arr::get($layout, 'justifyContent', 'left'));
        $justifyMode = 'left';
        if (in_array($justifyContent, ['right', 'end'], true)) {
            $justifyMode = 'right';
        } elseif ($justifyContent === 'center') {
            $justifyMode = 'center';
        } elseif ($justifyContent === 'space-between') {
            $justifyMode = 'space-between';
        }

        $rowGap = Arr::get($attrs, 'style.spacing.blockGap', '');
        if (is_array($rowGap)) {
            $rowGap = Arr::get($rowGap, 'left', Arr::get($rowGap, 'horizontal', ''));
        }
        $rowGap = $this->normalizeCssSize($rowGap);
        if (!$rowGap) {
            $rowGap = '20px';
        }

        $id = $attrs['elem_id'] ?? '';
        $blockCount = count($innerBlocks);

        $renderRowChild = function ($innerBlock) {
            $childBlock = $innerBlock;
            $childAttrs = (array)Arr::get($childBlock, 'attrs', []);
            // Row children must remain content-width for predictable alignment.
            $childAttrs['fcrmTableWidth'] = 'auto';
            $childBlock['attrs'] = $childAttrs;

            return $this->renderBlock($childBlock, true);
        };

        if ($justifyMode === 'space-between') {
            $tableStyles = [
                'width'           => '100%',
                'border-collapse' => 'separate',
                'border-spacing'  => '0'
            ];
            $rowHtml = '<table id="' . esc_attr($id) . '" class="fc_row" role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="' . BlockEditorHelper::renderStyles($tableStyles) . '"><tr>';

            $startAlign = $this->getDirectionalTextAlign('left');
            $endAlign = $this->getDirectionalTextAlign('right');

            foreach ($innerBlocks as $index => $innerBlock) {
                $cellStyles = [
                    'vertical-align' => 'top'
                ];

                $cellAlign = 'center';
                if ($index === 0) {
                    $cellAlign = $startAlign;
                } elseif ($index === ($blockCount - 1)) {
                    $cellAlign = $endAlign;
                }

                $rowHtml .= '<td class="fc_row_item" align="' . esc_attr($cellAlign) . '" style="' . BlockEditorHelper::renderStyles($cellStyles) . '">';
                $rowHtml .= $renderRowChild($innerBlock);
                $rowHtml .= '</td>';
            }

            $rowHtml .= '</tr></table>';
        } else {
            $outerAlign = $justifyMode === 'center'
                ? 'center'
                : ($justifyMode === 'right' ? $this->getDirectionalTextAlign('right') : $this->getDirectionalTextAlign('left'));

            $innerTableStyles = [
                'width'           => 'auto',
                'border-collapse' => 'separate',
                'border-spacing'  => '0'
            ];

            $innerHtml = '<table id="' . esc_attr($id) . '" class="fc_row" role="presentation" cellspacing="0" cellpadding="0" border="0" style="' . BlockEditorHelper::renderStyles($innerTableStyles) . '"><tr>';
            foreach ($innerBlocks as $index => $innerBlock) {
                $cellStyles = [
                    'vertical-align' => 'top'
                ];
                if ($index < ($blockCount - 1)) {
                    if (fluentcrm_is_rtl()) {
                        $cellStyles['padding-left'] = $rowGap;
                    } else {
                        $cellStyles['padding-right'] = $rowGap;
                    }
                }

                $innerHtml .= '<td class="fc_row_item" style="' . BlockEditorHelper::renderStyles($cellStyles) . '">';
                $innerHtml .= $renderRowChild($innerBlock);
                $innerHtml .= '</td>';
            }
            $innerHtml .= '</tr></table>';

            $rowHtml = '<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td align="' . esc_attr($outerAlign) . '">' . $innerHtml . '</td></tr></table>';
        }

        unset($attrs['elem_id']);

        return $this->wrapInTable($rowHtml, $attrs);
    }

    /**
     * Append inline CSS to an opening HTML tag without dropping existing styles.
     */
    private function appendInlineStyleToTag($tag, $style)
    {
        if (preg_match('/\sstyle=(["\'])(.*?)\1/i', $tag, $matches)) {
            $quote = $matches[1];
            $existingStyle = rtrim(trim($matches[2]), ';');
            $newStyle = $existingStyle ? $existingStyle . '; ' . $style : $style;

            // Use str_replace (not preg_replace) so existing style content cannot be
            // misread as a regex backreference in the replacement string.
            return str_replace($matches[0], ' style=' . $quote . $newStyle . $quote, $tag);
        }

        return preg_replace('/>$/', ' style="' . $style . '">', $tag, 1);
    }

    /**
     * Apply email-safe stripe backgrounds to table body rows.
     */
    private function applyTableStripeStyles($tableContent, $stripeColor = '#f0f0f0')
    {
        return preg_replace_callback('/<tbody\b([^>]*)>(.*?)<\/tbody>/is', function ($tbodyMatches) use ($stripeColor) {
            $rowIndex = 0;
            $tbodyContent = preg_replace_callback('/<tr\b([^>]*)>(.*?)<\/tr>/is', function ($rowMatches) use (&$rowIndex, $stripeColor) {
                $rowIndex++;

                if ($rowIndex % 2 === 0) {
                    return $rowMatches[0];
                }

                $rowContent = preg_replace_callback('/<(td|th)\b([^>]*)>/i', function ($cellMatches) use ($stripeColor) {
                    $tag = $this->appendInlineStyleToTag($cellMatches[0], 'background-color: ' . $stripeColor . ';');

                    if (stripos($tag, ' bgcolor=') === false) {
                        $tag = preg_replace('/>$/', ' bgcolor="' . $stripeColor . '">', $tag, 1);
                    }

                    return $tag;
                }, $rowMatches[2]);

                return '<tr' . $rowMatches[1] . '>' . $rowContent . '</tr>';
            }, $tbodyMatches[2]);

            return '<tbody' . $tbodyMatches[1] . '>' . $tbodyContent . '</tbody>';
        }, $tableContent);
    }

    /**
     * Render table block
     */
    private function renderTable($content, $attrs)
    {
        $borderConfig = Arr::get($attrs, 'style.border', []);
        $hasCustomBorder = !empty(Arr::get($borderConfig, 'width')) ||
            !empty(Arr::get($borderConfig, 'style')) ||
            !empty(Arr::get($borderConfig, 'color')) ||
            !empty(Arr::get($attrs, 'borderColor'));
        $className = (string)Arr::get($attrs, 'className', '');
        $isStriped = strpos($className, 'is-style-stripes') !== false;

        if ($hasCustomBorder) {
            $borderWidth = Arr::get($borderConfig, 'width', '1px');
            $borderStyle = Arr::get($borderConfig, 'style', 'solid');
            $borderColor = BlockEditorHelper::getBorderColor($attrs);
        } else {
            $borderWidth = '1px';
            $borderStyle = 'solid';
            $borderColor = '#6b7280';
        }

        $borderCss = "border: {$borderWidth} {$borderStyle} {$borderColor};";

        $styles = "width: 100%; border-collapse: collapse;";
        $cellStyles = "padding: 6px 10px;" . $borderCss;

        if (!isset($attrs['style']['spacing']['margin']['bottom'])) {
            $styles .= " margin-bottom: 20px;";
        }

        // Extract table content
        if (preg_match('/<table[^>]*>(.*?)<\/table>/s', $content, $matches)) {
            $tableContent = $matches[1];
            // Add styles to table cells
            $tableContent = preg_replace_callback('/<td\b([^>]*)>/i', function ($matches) use ($cellStyles) {
                return $this->appendInlineStyleToTag($matches[0], $cellStyles);
            }, $tableContent);
            $tableContent = preg_replace_callback('/<th\b([^>]*)>/i', function ($matches) use ($cellStyles) {
                return $this->appendInlineStyleToTag($matches[0], $cellStyles . ' font-weight: bold;');
            }, $tableContent);

            if ($isStriped) {
                $tableContent = $this->applyTableStripeStyles($tableContent);
            }
        } else {
            return '';
        }

        $id = $attrs['elem_id'] ?? '';
        $tableClass = 'fc_table';
        if ($isStriped) {
            $tableClass .= ' fc_table_striped';
        }

        // The table markup sits inside a <figure> wrapper; keep its caption too.
        $captionHtml = '';
        if (preg_match('/<figcaption[^>]*>(.*?)<\/figcaption>/s', $content, $captionMatch) && trim($captionMatch[1]) !== '') {
            $captionHtml = '<div class="wp-element-caption" style="font-size: 13px; text-align: center; margin-top: 6px;">' . $captionMatch[1] . '</div>';
        }

        return $this->wrapInTable("<table id='$id' class='{$tableClass}' style=\"{$styles}\">{$tableContent}</table>" . $captionHtml, $attrs);
    }

    /**
     * Render social links block
     */
    private function renderSocialLinks($innerBlocks, $attrs, $innerHTML)
    {
        $socialHtml = '<div style="margin: 20px 0; text-align: center;">';

        $socialLinks = [];

        // First, try to get from innerBlocks (preferred method)
        if (!empty($innerBlocks)) {
            foreach ($innerBlocks as $block) {
                if ($block['blockName'] === 'core/social-link') {
                    $blockAttrs = $block['attrs'] ?? [];
                    $blockInner = $block['innerHTML'] ?? '';

                    // Reconstruct innerHTML from innerContent if available
                    if (empty($blockInner) && !empty($block['innerContent'])) {
                        $blockInner = implode('', array_filter($block['innerContent'], 'is_string'));
                    }

                    $url = $blockAttrs['url'] ?? '';
                    $service = $blockAttrs['service'] ?? 'link';
                    $label = $blockAttrs['label'] ?? '';

                    // If URL is empty, try to extract from innerHTML
                    if (empty($url) && !empty($blockInner)) {
                        if (preg_match('/<a[^>]*href=["\']([^"\']*)["\']/', $blockInner, $urlMatch)) {
                            $url = $urlMatch[1];
                        }
                    }

                    // Extract label from aria-label or innerHTML
                    if (empty($label) && !empty($blockInner)) {
                        if (preg_match('/aria-label=["\']([^"\']*)["\']/', $blockInner, $labelMatch)) {
                            $label = $labelMatch[1];
                        }
                    }

                    // Determine service from URL if not set
                    if ($service === 'link' && !empty($url)) {
                        if (strpos($url, 'wordpress.org') !== false || strpos($url, 'wordpress.com') !== false) {
                            $service = 'wordpress';
                        } elseif (strpos($url, 'facebook.com') !== false) {
                            $service = 'facebook';
                        } elseif (strpos($url, 'github.com') !== false) {
                            $service = 'github';
                        } elseif (strpos($url, 'twitter.com') !== false || strpos($url, 'x.com') !== false) {
                            $service = 'twitter';
                        } elseif (strpos($url, 'linkedin.com') !== false) {
                            $service = 'linkedin';
                        } elseif (strpos($url, 'instagram.com') !== false) {
                            $service = 'instagram';
                        } elseif (strpos($url, 'amazon.com') !== false) {
                            $service = 'amazon';
                        }
                    }

                    if (!empty($url)) {
                        $socialLinks[] = [
                            'url'     => $url,
                            'service' => $service,
                            'label'   => $label
                        ];
                    }
                }
            }
        }

        // Fallback: Parse social links from innerHTML
        if (empty($socialLinks) && !empty($innerHTML)) {
            preg_match_all('/<li[^>]*class="[^"]*wp-social-link[^"]*"[^>]*>.*?<a[^>]*href=["\']([^"\']*)["\'][^>]*(?:aria-label=["\']([^"\']*)["\'])?[^>]*>.*?<\/a>.*?<\/li>/s', $innerHTML, $matches);

            if (!empty($matches[1])) {
                foreach ($matches[0] as $index => $match) {
                    $url = $matches[1][$index];
                    $label = $matches[2][$index] ?? '';

                    // Determine service from class or URL
                    $service = 'link';
                    if (strpos($match, 'wp-social-link-wordpress') !== false || strpos($url, 'wordpress.org') !== false || strpos($url, 'wordpress.com') !== false) {
                        $service = 'wordpress';
                    } elseif (strpos($match, 'wp-social-link-facebook') !== false || strpos($url, 'facebook.com') !== false) {
                        $service = 'facebook';
                    } elseif (strpos($match, 'wp-social-link-github') !== false || strpos($url, 'github.com') !== false) {
                        $service = 'github';
                    } elseif (strpos($match, 'wp-social-link-twitter') !== false || strpos($url, 'twitter.com') !== false || strpos($url, 'x.com') !== false) {
                        $service = 'twitter';
                    } elseif (strpos($match, 'wp-social-link-linkedin') !== false || strpos($url, 'linkedin.com') !== false) {
                        $service = 'linkedin';
                    } elseif (strpos($match, 'wp-social-link-instagram') !== false || strpos($url, 'instagram.com') !== false) {
                        $service = 'instagram';
                    } elseif (strpos($match, 'wp-social-link-amazon') !== false || strpos($url, 'amazon.com') !== false) {
                        $service = 'amazon';
                    }

                    $socialLinks[] = [
                        'url'     => $url,
                        'service' => $service,
                        'label'   => $label
                    ];
                }
            }
        }

        // Render social links using image icons or better text fallbacks
        if (!empty($socialLinks)) {
            foreach ($socialLinks as $link) {
                $iconHtml = $this->getSocialIconHtml($link['service'], $link['label']);

                $socialHtml .= '<a href="' . htmlspecialchars($link['url']) . '" style="display: inline-block; margin: 0 5px; text-decoration: none;">';
                $socialHtml .= $iconHtml;
                $socialHtml .= '</a>';
            }
        }

        $socialHtml .= '</div>';

        return $this->wrapInTable($socialHtml);
    }

    /**
     * Get social media icon HTML (with better styling)
     */
    private function getSocialIconHtml($service, $label = '')
    {
        // Get background color for each service
        $colors = [
            'facebook'  => '#1877f2',
            'twitter'   => '#1da1f2',
            'linkedin'  => '#0077b5',
            'instagram' => '#e4405f',
            'github'    => '#181717',
            'wordpress' => '#21759b',
            'amazon'    => '#ff9900',
            'link'      => '#0073aa'
        ];

        $bgColor = $colors[$service] ?? '#0073aa';
        $alt = $label ?: ucfirst($service);

        // Use service-specific icon rendering
        $iconContent = $this->getSocialIconSVG($service);

        return '<span style="display: inline-block; width: 44px; height: 44px; background-color: ' . $bgColor . '; border-radius: 50%; text-align: center; padding: 10px; box-sizing: border-box;" title="' . htmlspecialchars($alt) . '">' . $iconContent . '</span>';
    }

    /**
     * Get social media icon SVG or text representation
     */
    private function getSocialIconSVG($service)
    {
        // Simple SVG icons as inline data
        $icons = [
            'facebook'  => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>',
            'twitter'   => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>',
            'linkedin'  => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>',
            'instagram' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>',
            'github'    => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>',
            'wordpress' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M21.469 6.825c.84 1.537 1.318 3.3 1.318 5.175 0 3.979-2.156 7.456-5.363 9.325l3.295-9.527c.615-1.54.82-2.771.82-3.864 0-.405-.026-.78-.07-1.11zm-7.981.105c.647-.03 1.232-.105 1.232-.105.582-.075.514-.93-.067-.899 0 0-1.755.135-2.88.135-1.064 0-2.85-.15-2.85-.15-.585-.03-.661.855-.075.885 0 0 .54.061 1.125.09l1.68 4.605-2.37 7.08L5.354 6.9c.649-.03 1.234-.1 1.234-.1.585-.075.516-.93-.065-.896 0 0-1.746.138-2.874.138-.2 0-.438-.008-.69-.015C4.911 3.15 8.235 1.215 12 1.215c2.809 0 5.365 1.072 7.286 2.833-.046-.003-.091-.009-.141-.009-1.06 0-1.812.923-1.812 1.914 0 .89.513 1.643 1.06 2.531.411.72.89 1.643.89 2.977 0 .915-.354 1.994-.821 3.479l-1.075 3.585-3.9-11.61.001.014zM12 22.784c-1.059 0-2.081-.153-3.048-.437l3.237-9.406 3.315 9.087c.024.053.05.101.078.149-1.12.393-2.325.607-3.582.607zM1.211 12c0-1.564.336-3.05.935-4.39L7.29 21.709C3.694 19.96 1.212 16.271 1.212 12zm10.785-10.784C6.596 1.215 1.214 6.597 1.214 12s5.382 10.785 10.784 10.785S22.784 17.403 22.784 12 17.402 1.215 11.996 1.215z"/></svg>',
            'amazon'    => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M14.465 11.813c-1.75-1.297-4.056-2.017-6.121-2.017-2.766 0-5.264 1.032-7.153 2.742-.207.183-.371.322-.559.322-.188 0-.316-.128-.316-.316 0-.184.18-.404.484-.672C2.821 9.84 5.831 8.621 9.071 8.621c2.766 0 5.262 1.031 7.151 2.742.211.183.375.322.563.322.184 0 .312-.128.312-.316 0-.184-.176-.404-.484-.672zm3.438 1.227c-.133.027-.27.04-.402.04-.297 0-.559-.09-.805-.274-1.367-.992-3.164-1.547-4.93-1.547-2.465 0-4.742 1.051-6.562 2.871-.188.183-.316.304-.508.304-.188 0-.316-.12-.316-.308 0-.184.176-.402.488-.672 1.98-1.96 4.621-3.039 7.465-3.039 2.027 0 3.926.582 5.504 1.684.238.164.375.402.375.672 0 .238-.188.5-.309.524v-.003zm4.016-3.863c-.863 0-1.637.504-1.875 1.297-.231.77.13 1.617.895 1.895.691.25 1.457-.145 1.703-.836.227-.652-.012-1.391-.637-1.746-.18-.094-.387-.137-.586-.16-.164-.012-.329-.012-.5.039.004.004.004-.012.004-.016v-.012c.238-.348.54-.602.984-.707.43-.097.851-.016 1.262.16.199.082.379.211.578.32-.012.012-.027.027-.043.043-.125.133-.266.273-.398.402-.145.148-.273.297-.437.399-.035.023-.074.046-.113.05-.039.008-.102-.008-.125-.035-.098-.098-.207-.195-.309-.293-.543-.52-1.254-.742-2.055-.742-.012-.012-.012 0-.012.012zM24 17.887c-2.059 1.52-4.121 3.031-6.18 4.551-.078.059-.164.113-.258.16-.586.305-1.199.031-1.199-.559v-8.664c0-.129.047-.266.129-.383.078-.102.172-.195.277-.258 2.078-1.523 4.156-3.043 6.234-4.566.074-.054.156-.109.242-.152.375-.195.797-.094 1.016.258.094.145.117.297.117.457v8.765c0 .129-.039.262-.129.39h-.004z"/></svg>',
            'link'      => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>'
        ];

        return $icons[$service] ?? $icons['link'];
    }

    /**
     * Get color from WordPress color slug
     */
    private function getColorFromSlug($slug)
    {
        // Debug: Uncomment to see what colors your theme provides
        // $this->debugThemeColors();

        // First, try to get from FluentCRM Helper (which reads theme.json and editor-color-palette)
        static $colorMap = null;

        if ($colorMap === null) {
            $colorMap = [];

            // Get theme colors using the same method as AdminMenu.php
            if (class_exists('\FluentCrm\App\Services\Helper')) {
                $themeColors = \FluentCrm\App\Services\Helper::getThemeColorPalette();
                if (!empty($themeColors)) {
                    foreach ($themeColors as $colorData) {
                        if (isset($colorData['slug']) && isset($colorData['color'])) {
                            $colorMap[$colorData['slug']] = $colorData['color'];
                        }
                    }
                }

                // Also get theme preferences
                $themePref = \FluentCrm\App\Services\Helper::getThemePrefScheme();
                if (!empty($themePref['colors'])) {
                    foreach ($themePref['colors'] as $colorData) {
                        if (isset($colorData['slug']) && isset($colorData['color'])) {
                            $colorMap[$colorData['slug']] = $colorData['color'];
                        }
                    }
                }
            }
        }

        // Check our color map first
        if (isset($colorMap[$slug])) {
            return $colorMap[$slug];
        }

        // Try to get theme color from WordPress theme.json or global settings
        if (function_exists('wp_get_global_settings')) {
            $settings = wp_get_global_settings();
            if (!empty($settings['color']['palette']['theme'])) {
                foreach ($settings['color']['palette']['theme'] as $color) {
                    if (isset($color['slug']) && $color['slug'] === $slug && !empty($color['color'])) {
                        return $color['color'];
                    }
                }
            }
        }

        // Try WP_Theme_JSON for block themes
        if (class_exists('WP_Theme_JSON_Resolver')) {
            $theme_json = \WP_Theme_JSON_Resolver::get_merged_data();
            if ($theme_json) {
                $settings = $theme_json->get_settings();
                if (!empty($settings['color']['palette'])) {
                    foreach ($settings['color']['palette'] as $palette) {
                        if (isset($palette['slug']) && $palette['slug'] === $slug && !empty($palette['color'])) {
                            return $palette['color'];
                        }
                    }
                }
            }
        }

        // Fallback: Common WordPress and popular theme colors
        $colors = [
            // Theme palette colors (adjust these based on your active theme)
            // Twenty Twenty-Three defaults
            'theme-palette-color-1' => '#000000', // Base/Black
            'theme-palette-color-2' => '#6f42c1', // Purple
            'theme-palette-color-3' => '#007cba', // Blue
            'theme-palette-color-4' => '#16a085', // Teal
            'theme-palette-color-5' => '#e74c3c', // Red
            'theme-palette-color-6' => '#f39c12', // Orange
            'theme-palette-color-7' => '#ffffff', // White
            'theme-palette-color-8' => '#f5f5f5', // Light Gray
            'theme-palette-color-9' => '#cccccc', // Gray

            // Standard WordPress colors
            'black'                 => '#000000',
            'white'                 => '#ffffff',
            'primary'               => '#0073aa',
            'secondary'             => '#23282d',
            'tertiary'              => '#F0F0F1',

            // Common named colors
            'red'                   => '#e74c3c',
            'blue'                  => '#3498db',
            'green'                 => '#2ecc71',
            'yellow'                => '#f1c40f',
            'orange'                => '#e67e22',
            'purple'                => '#9b59b6',
            'cyan'                  => '#1abc9c',
            'vivid-red'             => '#cf2e2e',
            'vivid-orange'          => '#ff6900',
            'vivid-cyan-blue'       => '#0693e3',
            'vivid-green-cyan'      => '#00d084',
            'vivid-purple'          => '#9b51e0',
            'luminous-vivid-amber'  => '#fcb900',
            'luminous-vivid-orange' => '#ff6900',
            'light-green-cyan'      => '#7bdcb5',
            'pale-pink'             => '#f78da7',
            'pale-cyan-blue'        => '#8ed1fc',
        ];

        return $colors[$slug] ?? '#0073aa';
    }

    /**
     * Get font size from WordPress font size slug
     */
    private function getFontSizeFromSlug($slug)
    {
        static $fontSizeMap = null;

        if ($fontSizeMap === null) {
            $fontSizeMap = [];

            // Try wp_get_global_settings (WordPress 5.9+)
            if (function_exists('wp_get_global_settings')) {
                $settings = wp_get_global_settings();
                if (!empty($settings['typography']['fontSizes'])) {
                    foreach ($settings['typography']['fontSizes'] as $size) {
                        if (isset($size['slug']) && isset($size['size'])) {
                            $fontSizeMap[$size['slug']] = $size['size'];
                        }
                    }
                }
            }

            // Try WP_Theme_JSON for block themes
            if (empty($fontSizeMap) && class_exists('WP_Theme_JSON_Resolver')) {
                $theme_json = \WP_Theme_JSON_Resolver::get_merged_data();
                if ($theme_json) {
                    $settings = $theme_json->get_settings();
                    if (!empty($settings['typography']['fontSizes'])) {
                        foreach ($settings['typography']['fontSizes'] as $size) {
                            if (isset($size['slug']) && isset($size['size'])) {
                                $fontSizeMap[$size['slug']] = $size['size'];
                            }
                        }
                    }
                }
            }

            // Fallback presets
            if (empty($fontSizeMap)) {
                $fontSizeMap = [
                    'small'       => '14px',
                    'medium'      => '18px',
                    'large'       => '20px',
                    'x-large'     => '28px',
                    'extra-small' => '12px',
                    'extra-large' => '32px',
                    'huge'        => '42px'
                ];
            }
        }

        return $fontSizeMap[$slug] ?? null;
    }

    /**
     * Get font family from WordPress font family slug or return as-is
     */
    private function getFontFamilyFromSlug($fontFamily)
    {
        // If it looks like a font stack (contains comma), return as-is
        if (strpos($fontFamily, ',') !== false) {
            return $fontFamily;
        }

        static $fontFamilyMap = null;

        if ($fontFamilyMap === null) {
            $fontFamilyMap = [];

            // Try wp_get_global_settings (WordPress 5.9+)
            if (function_exists('wp_get_global_settings')) {
                $settings = wp_get_global_settings();
                if (!empty($settings['typography']['fontFamilies'])) {
                    foreach ($settings['typography']['fontFamilies'] as $family) {
                        if (isset($family['slug']) && isset($family['fontFamily'])) {
                            $fontFamilyMap[$family['slug']] = $family['fontFamily'];
                        }
                    }
                }
            }

            // Try WP_Theme_JSON for block themes
            if (empty($fontFamilyMap) && class_exists('WP_Theme_JSON_Resolver')) {
                $theme_json = \WP_Theme_JSON_Resolver::get_merged_data();
                if ($theme_json) {
                    $settings = $theme_json->get_settings();
                    if (!empty($settings['typography']['fontFamilies'])) {
                        foreach ($settings['typography']['fontFamilies'] as $family) {
                            if (isset($family['slug']) && isset($family['fontFamily'])) {
                                $fontFamilyMap[$family['slug']] = $family['fontFamily'];
                            }
                        }
                    }
                }
            }

            // Common font family presets
            $fontFamilyMap = array_merge([
                'system-ui'        => '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
                'system'           => '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
                'arial'            => 'Arial, Helvetica, sans-serif',
                'helvetica'        => '"Helvetica Neue", Helvetica, Arial, sans-serif',
                'times'            => '"Times New Roman", Times, serif',
                'times-new-roman' => '"Times New Roman", Times, serif',
                'georgia'          => 'Georgia, serif',
                'courier'          => '"Courier New", Courier, monospace',
                'courier-new'      => '"Courier New", Courier, monospace',
                'verdana'          => 'Verdana, Geneva, sans-serif',
                'tahoma'           => 'Tahoma, Geneva, sans-serif',
                'trebuchet'        => '"Trebuchet MS", Helvetica, sans-serif',
                'trebuchet-ms'     => '"Trebuchet MS", Helvetica, sans-serif',
                'lucida'           => '"Lucida Sans Unicode", "Lucida Grande", sans-serif',
                'palatino'         => '"Palatino Linotype", "Book Antiqua", Palatino, serif',
                'garamond'         => 'Garamond, serif',
                'impact'           => 'Impact, Charcoal, sans-serif',
                'comic-sans'       => '"Comic Sans MS", cursive, sans-serif',
                'comic-sans-ms'    => '"Comic Sans MS", cursive, sans-serif',
                'monospace'        => 'Monaco, "Lucida Console", Courier, monospace',
                'lato'             => '"Lato", "Helvetica Neue", Helvetica, Arial, sans-serif',
                'lora'             => '"Lora", Georgia, "Times New Roman", serif',
                'merriweather'     => '"Merriweather", Georgia, "Times New Roman", serif',
                'merriweather-sans' => '"Merriweather Sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
                'noticia-text'     => '"Noticia Text", Georgia, "Times New Roman", serif',
                'open-sans'        => '"Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
                'roboto'           => '"Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif',
                'source-sans-pro'  => '"Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif'
            ], $fontFamilyMap);
        }

        // Return mapped font family or original value
        return $fontFamilyMap[$fontFamily] ?? $fontFamily;
    }

    /**
     * Resolve Gutenberg font-family values to email-safe values.
     */
    private function resolveFontFamilyValue($fontFamilyValue)
    {
        $value = trim((string)$fontFamilyValue);
        if ($value === '') {
            return '';
        }

        if (strpos($value, 'var:preset|font-family|') === 0) {
            return $this->getFontFamilyFromSlug(substr($value, strlen('var:preset|font-family|')));
        }

        if (preg_match('/^var\\(--wp--preset--font-family--([a-z0-9-]+)\\)$/i', $value, $matches)) {
            return $this->getFontFamilyFromSlug($matches[1]);
        }

        if (strpos($value, 'var(') === 0) {
            return $value;
        }

        return $this->getFontFamilyFromSlug($value);
    }

    /**
     * Debug helper: Log all theme colors (for development only)
     * Uncomment the call in getColorFromSlug() to use
     */
    private function debugThemeColors()
    {
        static $logged = false;
        if ($logged) return;

        error_log('=== THEME COLORS DEBUG ===');

        // Check wp_get_global_settings
        if (function_exists('wp_get_global_settings')) {
            $settings = wp_get_global_settings();
            error_log('Global Settings Colors: ' . print_r($settings['color'] ?? 'none', true));
        }

        // Check WP_Theme_JSON_Resolver
        if (class_exists('WP_Theme_JSON_Resolver')) {
            $theme_json = \WP_Theme_JSON_Resolver::get_merged_data();
            if ($theme_json) {
                $settings = $theme_json->get_settings();
                error_log('Theme JSON Colors: ' . print_r($settings['color']['palette'] ?? 'none', true));
            }
        }

        $logged = true;
    }

    /**
     * Wrap content in email-safe table structure
     */
    private function wrapInTable($content, $atts = [])
    {
        if (empty(trim($content))) {
            return '';
        }

        $atts = (array)$atts;

        $align = Arr::get($atts, 'align', '');

        $tableClass = 'la-default';

        if ($align) {
            $tableClass = 'la-' . esc_attr($align);
        }

        $tableAlign = Arr::get($atts, 'fcrmTableAlign', '');
        if (!$tableAlign && in_array($align, ['left', 'center', 'right'], true)) {
            $tableAlign = $this->getDirectionalTextAlign($align);
        }

        $tableAlignMarkup = $tableAlign ? ' align="' . esc_attr($tableAlign) . '"' : '';

        $tableWidth = trim((string)Arr::get($atts, 'fcrmTableWidth', '100%'));
        $tableWidthMarkup = '';
        if ($tableWidth !== '' && strtolower($tableWidth) !== 'auto') {
            $tableWidthMarkup = ' width="' . esc_attr($tableWidth) . '"';
        }

        $tdClass = 'la-column';
        if (!empty($atts['is_root'])) {
            $tdClass = 'la-root-column';
        }

        $tdId = Arr::get($atts, 'td_id', '');

        return <<<HTML
<table role="presentation" class="$tableClass"$tableWidthMarkup$tableAlignMarkup cellspacing="0" cellpadding="0" border="0">
    <tr>
        <td id="$tdId" class="$tdClass">
            {$content}
        </td>
    </tr>
</table>
HTML;
    }

    private function resolveFontSizeValue($slugOrSize)
    {
        $value = trim((string)$slugOrSize);
        if ($value === '') {
            return '';
        }

        // If a WP preset CSS var is passed, extract its slug for proper resolution.
        if (preg_match('/^var\\(--wp--preset--font-size--([a-z0-9-]+)\\)$/i', $value, $matches)) {
            $value = $matches[1];
        } elseif (strpos($value, 'var(') === 0) {
            // Keep unknown CSS var expressions untouched.
            return $value;
        }

        // Already an explicit CSS size.
        if (preg_match('/^-?\d+(\.\d+)?(px|em|rem|%|pt|vh|vw)$/i', $value)) {
            return $value;
        }

        // Custom defaults in block editor helper (fc-small, fc-medium, etc).
        $fontPresets = BlockEditorHelper::getDefaultPreset('font-size');
        foreach ((array)$fontPresets as $preset) {
            if (!is_array($preset)) {
                continue;
            }
            if (Arr::get($preset, 'slug') === $value && Arr::get($preset, 'size')) {
                return Arr::get($preset, 'size');
            }
        }

        // Theme/WordPress presets (small, medium, large, x-large, etc).
        if (class_exists('\FluentCrm\App\Services\Helper')) {
            $themeFontSizes = Helper::getThemeFontSizes();
            foreach ((array)$themeFontSizes as $preset) {
                if (!is_array($preset)) {
                    continue;
                }
                if (Arr::get($preset, 'slug') !== $value || !Arr::get($preset, 'size')) {
                    continue;
                }

                $size = Arr::get($preset, 'size');
                if (is_numeric($size)) {
                    return $size . 'px';
                }

                return (string)$size;
            }
        }

        // Fallback to WP CSS variable naming.
        return 'var(--wp--preset--font-size--' . $value . ')';
    }

    /**
     * Generate complete email HTML with wrapper
     */
    public function generateEmailHtml($content, $title = '')
    {
        $parsedContent = $this->parse($content);

        return <<<HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>{$title}</title>
    <style type="text/css">
        body {
            margin: 0;
            padding: 0;
            font-family: Arial, Helvetica, sans-serif;
            font-size: 16px;
            line-height: 1.6;
            color: #333333;
            background-color: #f4f4f4;
        }
        table {
            border-collapse: collapse;
        }
        img {
            border: 0;
            outline: none;
            text-decoration: none;
            -ms-interpolation-mode: bicubic;
        }
        a {
            color: #0073aa;
        }
        @media only screen and (max-width: 600px) {
            .email-container {
                width: 100% !important;
            }
        }
    </style>
</head>
<body style="margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; background-color: #f4f4f4;">
    <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
        <tr>
            <td align="center" style="padding: 20px 0;">
                <table role="presentation" class="email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: #ffffff;">
                    <tr>
                        <td style="padding: 40px 30px;">
                            {$parsedContent}
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
    </table>
</body>
</html>
HTML;
    }
}

// Usage Example:
/*
$parser = new GutenbergEmailParser();

// Option 1: Parse blocks only (returns HTML fragment)
$post_content = get_post_field('post_content', $post_id);
$emailHtml = $parser->parse($post_content);

// Option 2: Generate complete email HTML with wrapper
$completeEmail = $parser->generateEmailHtml($post_content, 'Email Title');

// Send email
wp_mail($to, $subject, $completeEmail, ['Content-Type: text/html; charset=UTF-8']);
*/
