<?php
// components/CampaignFlow/StepCard.php
namespace Components\CampaignFlow;

use Components\Base;
use Components\Support\Palette;

class StepCard extends Base
{
    /**
     * Render a visual step card
     * $step keys:
     * - id (string)
     * - title (string) legacy: rendered as the step content (secondary line)
     * - subtitle (string) legacy: rendered as the step type (primary line)
     *   New keys also supported:
     *   - stepType|steptype (string): primary line (step type)
     *   - content (string): secondary line (step content)
     * - icon (string) raw HTML (emoji or SVG markup)
     * - color (string) Tailwind utility classes applied to the icon wrapper, e.g. "bg-blue-100 text-blue-600".
     *           Backwards compatible: a simple color token like "blue"|"indigo"|"yellow"|"green"|"gray" still works.
     * - disabled (bool) optional: when true, card is shown with a visually disabled style and aria-disabled.
     * - markers (array) optional: list of marker objects describing top-right indicators
     *     marker object: ['color' => '<Tailwind classes or any CSS color>', 'label' => 'Tooltip text', 'icon' => '<raw SVG or emoji>']
     *   Also supported: 'marker' as a single object or string color for backwards compatibility.
     * - icon_indicators (array) optional: small colored dots overlaid on the module icon itself (not the card). Each item supports:
     *     ['color' => '<Tailwind bg-* classes, any CSS color, or palette token (blue|indigo|orange|yellow|green|gray|purple|red|pink|brand)>', 'label' => 'Tooltip text', 'pos' => 'tr|tl|br|bl']
     *   Also supported: 'icon_indicator' as a single object for convenience. When a palette token is used, the dot uses the palette's text tone converted to a bg tone (e.g., text-green-600 -> bg-green-600), falling back to the palette bg tone.
     * - ring (string) optional: ring color override for the step wrapper. Accepts a Tailwind utility (e.g., "ring-orange-500"),
     *   or a palette token (blue|indigo|orange|yellow|green|gray|purple|red|pink|brand). When omitted, defaults to "ring-flow".
     *   Alias: 'ring_color'.
     * - href (string) optional: when provided, wraps the card with <a href="..."> making it clickable.
     * - attrs (array) optional: extra HTML attributes on the clickable wrapper (button or anchor) to support modal or htmx triggers.
     * - hover_icon (string) optional: override the hover icon. 'click' for SVG_CHEVRON_RIGHT, 'menu' for SVG_ELLIPSIS_HORIZONTAL_CIRCLE_OUTLINE.
     * - dropdown (array) optional: configuration for a Tailwind Plus Elements dropdown rendered as an options button.
     *     - trigger: ['label' => string, 'icon' => string, 'class' => string, 'attrs' => array]
     *     - items: list of menu item arrays: ['label' => string, 'attrs' => array, 'href' => string, 'icon' => string, 'divider' => bool]
     */
    public static function render(array $step, array $opts = []): string
    {
        $size = $opts['size'] ?? 'md'; // xs|sm|md|lg
        $editable = (bool)($opts['editable'] ?? false);

        // Resolve potential language keys, then escape
        // Legacy mapping:
        // - subtitle -> stepType (primary)
        // - title    -> content  (secondary)
        $stepTypeRaw = $step['stepType'] ?? ($step['steptype'] ?? ($step['subtitle'] ?? ''));
        $contentRaw = $step['content'] ?? ($step['title'] ?? '');

        $stepType = self::h(self::lng($stepTypeRaw, (string)$stepTypeRaw));
        $content = self::h(self::lng($contentRaw, (string)$contentRaw));
        $icon = $step['icon'] ?? self::SVG_MAIL;
        $isDisabled = (bool)($step['disabled'] ?? false);
        // Accept both 'color' (new) and legacy '_color'
        $color = trim((string)($step['color'] ?? ($step['_color'] ?? 'gray')));

        // Size classes
        // Base wrapper (ring color is appended later via $ringCls)
        $wrapBase = 'relative flex items-center bg-white ring shrink-0 text-left';
        $sizes = [
            'xs' => ['wrap' => 'rounded-md p-1 pr-3 space-x-1 leading-none', 'iconWrap' => 'rounded-sm size-6 p-1', 'labelWrap' => '', 'title' => 'hidden', 'subtitle' => 'text-xs', 'markerWrap' => 'top-0.5 right-0.5', 'markerSize' => 'size-3'],
            'sm' => ['wrap' => 'rounded-lg p-2 pr-5 space-x-2 leading-none', 'iconWrap' => 'rounded-md size-8 p-1.5', 'labelWrap' => 'h-8 ', 'title' => 'text-xs', 'subtitle' => 'text-[11px]', 'markerWrap' => 'top-1 right-1', 'markerSize' => 'size-3'],
            'md' => ['wrap' => 'rounded-xl p-2 pr-5 space-x-3 leading-none', 'iconWrap' => 'rounded-lg size-10 p-2', 'labelWrap' => 'h-10', 'title' => 'text-sm', 'subtitle' => 'text-xs', 'markerWrap' => 'top-1 right-1', 'markerSize' => 'size-4'],
            'lg' => ['wrap' => 'rounded-2xl p-2.5 pr-6 space-x-4 leading-none', 'iconWrap' => 'rounded-xl size-12 p-3', 'labelWrap' => 'h-12', 'title' => 'text-base', 'subtitle' => 'text-sm', 'markerWrap' => 'top-1.5 right-1.5', 'markerSize' => 'size-5'],
        ];
        $sz = $sizes[$size] ?? $sizes['md'];

        // Special visual treatment for the Trigger/Source steps: rounded-full wrapper and icon container
        $variant = isset($step['variant']) ? (string)$step['variant'] : '';
        $isTrigger = (($step['id'] ?? '') === 'trigger') || ($variant === 'trigger');
        $isSource = (($step['id'] ?? '') === 'source') || ($variant === 'source');
        $isRound = $isTrigger || $isSource;
        if ($isRound) {
            // Replace any rounded-* with rounded-full in wrapper and iconWrap size presets
            foreach (['wrap', 'iconWrap'] as $rk) {
                if (!empty($sz[$rk]) && is_string($sz[$rk])) {
                    $sz[$rk] = preg_replace('/\brounded-[^\s]+/i', 'rounded-full', $sz[$rk]);
                    if ($sz[$rk] === null) { $sz[$rk] = 'rounded-full'; }
                }
            }
        }

        // Shared color palette map
        $paletteMap = Palette::map();

        // If this is the special Trigger card and no explicit color provided, prefer green palette by default
        $hasExplicitColor = array_key_exists('color', $step) || array_key_exists('_color', $step);
        if (($isTrigger ?? false) && !$hasExplicitColor) {
            $color = 'green';
        }
        // If Source card without explicit color, prefer green as well
        if (($isSource ?? false) && !$hasExplicitColor) {
            $color = 'green';
        }

        // Resolve wrapper color classes from token or pass-through utilities
        $colorClasses = Palette::classes($color, 'gray');

        // Determine ring color override (default 'ring-flow').
        // Accepts Tailwind ring utility (e.g., 'ring-orange-500') or a palette token (e.g., 'orange').
        $isHighlighted = (bool)($step['highlight'] ?? false);
        $highlightLabel = $step['highlight_label'] ?? '';

        $ringCls = $isHighlighted ? 'ring-brand-500 ring-inset ring-2' : 'ring-flow';
        if (isset($step['ring']) || isset($step['ring_color'])) {
            $ringValue = trim((string)($step['ring'] ?? $step['ring_color'] ?? ''));
            if ($ringValue !== '') {
                $isUtility = (bool)(preg_match('/\b(ring|border)-/i', $ringValue) || strpos($ringValue, ' ') !== false);
                if ($isUtility) {
                    $ringCls = $ringValue;
                } else {
                    if (isset($paletteMap[$ringValue]['ring'])) {
                        $ringCls = (string)$paletteMap[$ringValue]['ring'];
                    }
                }
            }
        }

        // Normalize markers input
        $markers = [];
        if (isset($step['markers'])) {
            $m = $step['markers'];
            if (is_array($m) && isset($m['color'])) {
                $markers = [$m];
            } elseif (is_array($m)) {
                foreach ($m as $it) {
                    if (is_string($it)) {
                        $markers[] = ['color' => $it];
                    } elseif (is_array($it)) {
                        $markers[] = $it;
                    }
                }
            }
        } elseif (isset($step['marker'])) {
            $m = $step['marker'];
            if (is_string($m)) {
                $markers = [['color' => $m]];
            } elseif (is_array($m)) {
                $markers = [$m];
            }
        }

        // Normalize icon indicators input (colored dots on the icon itself)
        $iconIndicators = [];
        if (isset($step['icon_indicators'])) {
            $ii = $step['icon_indicators'];
            if (is_array($ii) && isset($ii['color'])) {
                $iconIndicators = [$ii];
            } elseif (is_array($ii)) {
                foreach ($ii as $it) {
                    if (is_string($it)) {
                        $iconIndicators[] = ['color' => $it];
                    } elseif (is_array($it)) {
                        $iconIndicators[] = $it;
                    }
                }
            }
        } elseif (isset($step['icon_indicator'])) {
            $ii = $step['icon_indicator'];
            if (is_string($ii)) {
                $iconIndicators = [['color' => $ii]];
            } elseif (is_array($ii)) {
                $iconIndicators = [$ii];
            }
        }

        $highlightRingHtml = '';
        if ($isHighlighted) {
            $rounding = 'rounded-xl';
            if ($isRound) {
                $rounding = 'rounded-full';
            } elseif ($size === 'xs') {
                $rounding = 'rounded-md';
            } elseif ($size === 'sm') {
                $rounding = 'rounded-xl';
            } elseif ($size === 'lg') {
                $rounding = 'rounded-2xl';
            }
            $highlightRingHtml = <<<HTML
<div class="absolute -inset-1 !m-0 {$rounding} ring-2 ring-orange-300/70 animate-pulse pointer-events-none"></div>
HTML;
        }

        $wrapCls = trim($wrapBase . ' ' . $ringCls . ' ' . $sz['wrap'] . ' ' . ($opts['class'] ?? ''));
        if ($isDisabled) {
            $wrapCls .= ' ' . self::DISABLED_CLASSES;
        }
        // Mouseover feedback for interactive cards
        // Add only when the step is clickable (href/attrs/dropdown) and not disabled
        $willHaveDropdown = !empty($step['dropdown']) && is_array($step['dropdown']);
        $hasAttrs = !empty($step['attrs']) && is_array($step['attrs']);
        $hasHref = isset($step['href']) && (string)$step['href'] !== '';
        $isInteractive = !$isDisabled && ($hasHref || $hasAttrs || $willHaveDropdown);
        $hoverIconHtml = '';
        if ($isInteractive) {
            $wrapCls .= ' ' . self::HOVER_CLICKABLE . ' group';

            $hoverIconType = $step['hover_icon'] ?? ($willHaveDropdown ? 'menu' : 'click');
            $hoverIcon = self::SVG_CHEVRON_RIGHT;
            if ($hoverIconType === 'menu') {
                $hoverIcon = self::SVG_ELLIPSIS_HORIZONTAL_CIRCLE_OUTLINE;
            } elseif ($hoverIconType === 'view') {
                $hoverIcon = self::SVG_EYE;
            }

            $hoverIconHtml = <<<HTML
<div class="absolute right-1.5 top-1/2 -translate-y-1/2 !m-0 [&>svg]:size-4! text-secondary opacity-30 group-hover:opacity-100 transition-opacity pointer-events-none">
    {$hoverIcon}
</div>
HTML;
        }

        $iconWrapCls = 'relative flex items-center justify-center ' . $sz['iconWrap'] . ' '  . trim($colorClasses);

        // Typography mapping (refactor):
        // - stepType (from legacy subtitle) is the stronger primary line (old title styling)
        // - content (from legacy title) is the softer secondary line (old subtitle styling)
        // XS special-case keeps the compact behavior (only one visible line):
        // - show stepType
        // - hide content
        $stepTypeSizeCls = $sz['title'];
        $contentSizeCls = $sz['subtitle'];
        if ($size === 'xs') {
            $stepTypeSizeCls = $sz['subtitle'];
            $contentSizeCls = $sz['title'];
        }

        // Optional edit affordance (lg only)
        $editBtn = '';
        if ($editable && $size === 'lg') {
            $editBtn = <<<HTML
<button type="button" class="ml-2 rounded p-1 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-300" title="Configure step" aria-label="Configure step">
    <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06A2 2 0 1 1 7.04 3.3l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c0 .69.28 1.35.78 1.82.5.47 1.18.74 1.88.74H21a2 2 0 1 1 0 4h-.09c-.69 0-1.35.28-1.82.78Z"/></svg>
</button>
HTML;
        }

        $iconHtml = is_string($icon) ? $icon : '';

        $size = $sz['labelWrap'] ;

        $isContentHidden = (stripos($contentSizeCls, 'hidden') !== false);
        $labelDivCls = $isContentHidden ? 'min-w-0 ' : 'flex flex-col justify-between min-w-0';
        $labelDivCls .=  ' ' . $size;

        // Clickable wrapper support
        $href = isset($step['href']) ? (string)$step['href'] : '';
        $extraAttrs = is_array($step['attrs'] ?? null) ? $step['attrs'] : [];
        $hasDropdown = !empty($step['dropdown']) && is_array($step['dropdown']);
        // Avoid nested interactive elements: if a dropdown is present, force a non-interactive wrapper (div)
        $outerTag = ($href !== '' && !$hasDropdown) ? 'a' : ((!empty($extraAttrs) && !$hasDropdown) ? 'button' : 'div');
        $outerAttrs = ['class' => $wrapCls];
        if (!empty($step['id'])) {
            $outerAttrs['id'] = $step['id'];
        }
        if ($href !== '' && $outerTag === 'a') {
            $outerAttrs['href'] = $href;
        }
        if ($outerTag === 'button') {
            $outerAttrs['type'] = $outerAttrs['type'] ?? 'button';
        }
        if ($isDisabled) {
            $outerAttrs['aria-disabled'] = 'true';
            if ($outerTag === 'button') {
                $outerAttrs['disabled'] = true;
            }
        }
        // Merge additional attributes (e.g., command, hx-*)
        foreach ($extraAttrs as $k => $v) {
            $outerAttrs[$k] = $v;
        }
        $outerAttrStr = self::attrs($outerAttrs);

        $iconWrapEsc = self::h($iconWrapCls);
        $stepTypeHtml = '';
        if ($stepType !== '') {
            $stepTypeClsEsc = self::h(trim($stepTypeSizeCls . ' font-medium'));
            $stepTypeHtml = <<<HTML
<p class="text-primary max-w-48 truncate leading-snug {$stepTypeClsEsc}">{$stepType}</p>
HTML;
        }
        $contentHtml = '';
        if ($content !== '') {
            $contentClsEsc = self::h($contentSizeCls);
            $contentHtml = <<<HTML
<p class="text-secondary max-w-48 truncate leading-snug {$contentClsEsc}">{$content}</p>
HTML;
        }

        // Build markers HTML if any
        $markersHtml = '';
        if (!empty($markers)) {
            $mWrapPos = self::h('absolute !m-0 ' . $sz['markerWrap']);
            $mItems = '';
            foreach ($markers as $mk) {
                $mColorRaw = trim((string)($mk['color'] ?? ''));
                $mSizeCls = $sz['markerSize'];
                $titleAttr = '';
                $rawLabel = isset($mk['label']) ? (string)$mk['label'] : '';
                if ($rawLabel !== '') {
                    $titleAttr = ' title="' . self::h($rawLabel) . '" aria-label="' . self::h($rawLabel) . '"';
                }

                // Determine whether we show a text label inside the marker
                $hasLabel = ($rawLabel !== '');
                $hasIcon  = (!empty($mk['icon']) && is_string($mk['icon']));

                // Base class for the marker container
                $mClass = 'inline-flex items-center rounded-sm';

                // Compute dimensions: when a label is present we render a small pill; otherwise a fixed square
                $hCls = 'h-4'; $pxCls = 'px-1'; $textSizeCls = 'text-[10px]'; $gapCls = 'gap-0.5';
                if (preg_match('/size-(\d+)/', $mSizeCls, $mmSz)) {
                    $szNum = (int)$mmSz[1];
                    if ($szNum <= 3) { $hCls = 'h-3'; $pxCls = 'px-0.5'; $textSizeCls = 'text-[9px]'; $gapCls = 'gap-0.5'; }
                    elseif ($szNum === 4) { $hCls = 'h-4'; $pxCls = 'px-1'; $textSizeCls = 'text-[10px]'; $gapCls = 'gap-0.5'; }
                    else { $hCls = 'h-5'; $pxCls = 'px-1.5'; $textSizeCls = 'text-[11px]'; $gapCls = 'gap-1'; }
                }

                if ($hasLabel) {
                    // Small pill with text
                    $mClass .= ' ' . $hCls . ' ' . $pxCls . ' ' . $textSizeCls . ' ' . $gapCls;
                } else {
                    // Compact square
                    $mClass .= ' justify-center ' . $mSizeCls;
                }

                // Determine background/text color via palette, tailwind utilities, or raw CSS color
                $mIsUtility = $mColorRaw !== '' && (preg_match('/\b(bg|text|from|to|via|fill|stroke|ring|border)-/i', $mColorRaw) || strpos($mColorRaw, ' ') !== false);
                $mStyle = '';
                if ($mColorRaw === '') {
                    $mClass .= ' bg-flow';
                } elseif ($mIsUtility) {
                    $mClass .= ' ' . $mColorRaw;
                } elseif (isset($paletteMap[$mColorRaw])) {
                    // Use palette mapping: light bg + readable text (e.g., green -> bg-green-100 text-green-600)
                    $pal = $paletteMap[$mColorRaw];
                    $mClass .= ' ' . ($pal['bg'] ?? '') . ' ' . ($pal['text'] ?? '');
                } else {
                    // Raw CSS color value (e.g., #ff9900)
                    $mStyle = ' style="background-color:' . self::h($mColorRaw) . '"';
                }

                // Optional icon inside marker
                $markerIconHtml = '';
                if ($hasIcon) {
                    $markerIconHtml = (string)$mk['icon'];
                    // Derive a slightly smaller icon size from the marker size (e.g., size-5 -> size-4)
                    $iconSize = $mSizeCls;
                    if (preg_match('/size-(\d+)/', $mSizeCls, $mm)) {
                        $n = max(1, ((int)$mm[1]) - 1);
                        $iconSize = 'size-' . $n;
                    }
                    // Size the icon: use CSS for SVG, or wrap in a span for emoji
                    if (strpos($markerIconHtml, '<svg') !== false) {
                        // Use CSS to size the SVG instead of modifying the raw SVG string
                        $mClass .= ' [&_svg]:' . $iconSize;
                    } else {
                        // Emoji or text: scale using a span
                        $iconSizeEsc = self::h($iconSize);
                        $markerIconHtml = <<<HTML
<span class=" {$iconSizeEsc} leading-none flex items-center justify-center">{$markerIconHtml}</span>
HTML;
                    }
                }

                // Optional inline label output
                $labelHtml = '';
                if ($hasLabel) {
                    $labelEsc = self::h($rawLabel, false);
                    $labelHtml = '<span class="leading-none">' . $labelEsc . '</span>';
                }

                $mClassEsc = self::h($mClass);
                $mItems .= <<<HTML
<div class=" p-0.5 {$mClassEsc}"{$mStyle}{$titleAttr}>{$markerIconHtml}{$labelHtml}</div>
HTML;
            }
            $markersHtml = <<<HTML
<div class="{$mWrapPos} flex items-center gap-1">{$mItems}</div>
HTML;
        }

        // Build icon indicators HTML (dots on the icon itself)
        $iconIndicatorsHtml = '';
        if (!empty($iconIndicators)) {
            // derive a dot size slightly smaller than the card marker size
            $dotSize = $sz['markerSize'];
            if (preg_match('/size-(\d+)/', $dotSize, $mm)) {
                $n = max(1, ((int)$mm[1]) - 1);
                $dotSize = 'size-' . $n;
            }
            foreach ($iconIndicators as $ii) {
                $iColorRaw = trim((string)($ii['color'] ?? ''));
                $pos = strtolower((string)($ii['pos'] ?? 'tr'));
                $titleAttr = '';
                if (!empty($ii['label'])) {
                    $titleAttr = ' title="' . self::h((string)$ii['label']) . '" aria-label="' . self::h((string)$ii['label']) . '"';
                }
                // Position the indicator inside the icon wrapper (on the SVG), offset by the wrapper padding
                $offset = '2';
                if (preg_match('/\bp-([0-9]+(?:\.[0-9]+)?)/', $sz['iconWrap'], $mPad)) { $offset = (string)($mPad[1] - 0.5); }
                $posCls = 'top-' . $offset . ' right-' . $offset;
                if ($pos === 'tl') { $posCls = 'top-' . $offset . ' left-' . $offset; }
                elseif ($pos === 'br') { $posCls = 'bottom-' . $offset . ' right-' . $offset; }
                elseif ($pos === 'bl') { $posCls = 'bottom-' . $offset . ' left-' . $offset; }

                $baseCls = 'absolute block transform rounded-full  dark:ring-gray-900 ' . $dotSize . ' ' . $posCls;
                $style = '';
                if ($iColorRaw === '') {
                    $baseCls .= ' bg-red-500';
                } else {
                    $isUtility = preg_match('/\b(bg|text|from|to|via|fill|stroke|ring|border)-/i', $iColorRaw) || strpos($iColorRaw, ' ') !== false;
                    if ($isUtility) {
                        $baseCls .= ' ' . $iColorRaw;
                    } else {
                        // Try palette token first
                        if (isset($paletteMap[$iColorRaw])) {
                            $dotClsFromPalette = Palette::dotBg($iColorRaw, 'gray');
                            if ($dotClsFromPalette !== '') {
                                $baseCls .= ' ' . $dotClsFromPalette;
                            } else {
                                $baseCls .= ' bg-red-500';
                            }
                        } else {
                            // Treat as raw CSS color (e.g., #ff9900)
                            $style = ' style="background-color:' . self::h($iColorRaw) . '"';
                        }
                    }
                }
                $baseClsEsc = self::h($baseCls);
                $iconIndicatorsHtml .= <<<HTML
<span class="{$baseClsEsc}"{$style}{$titleAttr}></span>
HTML;
            }
        }

        // Optional dropdown menu � collect config; actual rendering delegated to Components\Dropdown
        $dropdownWrapStart = '';
        $dropdownWrapEnd = '';
        $useDropdown = !empty($step['dropdown']) && is_array($step['dropdown']);
        $dropdownItems = [];
        $dropdownTriggerAttrs = [];
        if ($useDropdown) {
            $dd = $step['dropdown'];
            $tr = is_array($dd['trigger'] ?? null) ? $dd['trigger'] : [];
            $dropdownItems = is_array($dd['items'] ?? null) ? $dd['items'] : [];
            $dropdownTriggerAttrs = is_array($tr['attrs'] ?? null) ? $tr['attrs'] : [];
            // Do not modify $outerTag/$outerAttrs here; Dropdown will handle trigger/button wrapping
        }

        $highlightLabelHtml = '';
        if ($highlightLabel !== '') {
            $labelEsc = self::h(self::lng($highlightLabel, (string)$highlightLabel));
            $highlightLabelHtml = <<<HTML
<div class="absolute -top-8 left-1/2 -translate-x-1/2 !m-0 text-brand-800 font-bold text-xs whitespace-nowrap">
    {$labelEsc}
</div>
HTML;
        }

        // Assemble card inner content
        $cardInner = <<<HTML
  $highlightLabelHtml
  $highlightRingHtml
  <div class="$iconWrapEsc">$iconHtml$iconIndicatorsHtml</div>
  <div class="$labelDivCls">
    $stepTypeHtml
    $contentHtml
  </div>
  $editBtn
  $markersHtml
  $hoverIconHtml
HTML;

        // Final outer structure � delegate dropdown rendering to Components\Dropdown when configured
        if ($useDropdown) {
            $triggerAttrs = ['class' => $wrapCls, 'type' => 'button'];
            if (!empty($step['id'])) {
                $triggerAttrs['id'] = $step['id'];
            }
            if ($isDisabled) { $triggerAttrs['aria-disabled'] = 'true'; $triggerAttrs['disabled'] = true; }
            foreach ($dropdownTriggerAttrs as $k => $v) { $triggerAttrs[$k] = $v; }
            $html = \Components\Dropdown::render([
                'trigger_tag' => 'button',
                'trigger_attrs' => $triggerAttrs,
                'trigger_html' => $cardInner,
                'items' => $dropdownItems,
                'anchor' => 'bottom start',
                'popover' => 'manual',
                'wrapper_class' => 'inline-flex relative',
            ]);
            return $html;
        }

        $html = <<<HTML
<{$outerTag}{$outerAttrStr}>
$cardInner</{$outerTag}>
HTML;
        return $html;
    }
}
