<?php
// components/Ui/PerformanceDashboard.php
namespace Components\Ui;

use Components\Base;
use Components\Data\DataRegistry;
use Components\Support\FlowHtmlCache;
use Components\Router;
use Components\Newsletter\PageConfig;

class PerformanceDashboard
{
    public const CONTEXT_TRIGGER = 'trigger';
    public const CONTEXT_TRIGGER_TYPE_REVIEW = 'trigger_type_review';
    public const CONTEXT_NEWSLETTER = PageConfig::MODE_NEWSLETTER;
    public const CONTEXT_AUTOMATION = PageConfig::MODE_AUTOMATION;
    public const CONTEXT_AUDIENCE = 'audience';

    /**
     * Get the metrics definition for a given context.
     *
     * @param string $context
     * @return array
     */
    public static function getMetricsDefinition(string $context = self::CONTEXT_TRIGGER): array
    {
        switch ($context) {
            case self::CONTEXT_AUDIENCE:
                return [
                    'total' => ['label' => Base::lng('MAILBEEZ_PERFORMANCE_TOTAL', 'Subscribers'), 'type' => 'number'],
                ];
            case self::CONTEXT_TRIGGER_TYPE_REVIEW:
                return [
                    'lastSent' => ['label' => Base::lng('MAILBEEZ_PERFORMANCE_LAST_SENT', 'Last Sent'), 'type' => 'timeago'],
                    'sent' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_SENT', 'Sent'), 'type' => 'number', 'dotColor' => 'bg-event-sent/70'],
                    'opened' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_OPENED', 'Opened'), 'type' => 'number', 'dotColor' => 'bg-event-opened/70'],
                    'clicked' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_CLICKED', 'Clicked'), 'type' => 'number', 'dotColor' => 'bg-event-clicked/70'],
                    'orders' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_ORDERED', 'Orders'), 'type' => 'number', 'dotColor' => 'bg-event-ordered/70'],
                    'revenue' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_REVENUE', 'Revenue'), 'type' => 'currency', 'dotColor' => 'bg-event-ordered/70'],
                    'revenuePerEmail' => ['label' => Base::lng('MAILBEEZ_PERFORMANCE_REVENUE_PER_EMAIL', 'per Email'), 'type' => 'currency', 'dotColor' => 'bg-event-ordered/70'],
                    'deliveredRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_DELIVERED', 'Delivered'), 'type' => 'percentage', 'dotColor' => 'bg-event-sent/70'],
                    'openRate' => ['label' => Base::lng('MAILBEEZ_PERFORMANCE_OPEN_RATE', 'Opened'), 'type' => 'percentage', 'dotColor' => 'bg-event-opened/70'],
                    'clickRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_CLICKED', 'Clicked'), 'type' => 'percentage', 'dotColor' => 'bg-event-clicked/70'],
                    'orderRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_ORDERED', 'Ordered'), 'type' => 'percentage', 'dotColor' => 'bg-event-ordered/70'],
                    'bounceRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_BOUNCED', 'Bounces'), 'type' => 'percentage', 'dotColor' => 'bg-event-bounced/70'],
                    'mobileRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_MOBILE', 'Mobile'), 'type' => 'percentage', 'dotColor' => 'bg-event-opened/70'],
                ];
            case self::CONTEXT_NEWSLETTER:
                return [
                    'lastSent' => ['label' => Base::lng('MAILBEEZ_PERFORMANCE_LAST_SENT', 'Last Sent'), 'type' => 'timeago'],
                    'sent' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_SENT', 'Sent'), 'type' => 'number', 'dotColor' => 'bg-event-sent/70'],
                    'openRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_OPENED', 'Opened'), 'type' => 'percentage', 'dotColor' => 'bg-event-opened/70'],
                    'clickRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_CLICKED', 'Clicked'), 'type' => 'percentage', 'dotColor' => 'bg-event-clicked/70'],
                    'orderRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_ORDERED', 'Ordered'), 'type' => 'percentage', 'dotColor' => 'bg-event-ordered/70'],
                    'revenue' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_REVENUE', 'Revenue'), 'type' => 'currency', 'dotColor' => 'bg-event-ordered/70'],
                    'revenuePerEmail' => ['label' => Base::lng('MAILBEEZ_PERFORMANCE_REVENUE_PER_EMAIL', 'per Email'), 'type' => 'currency', 'dotColor' => 'bg-event-ordered/70'],
                    'deliveredRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_DELIVERED', 'Delivered'), 'type' => 'percentage', 'dotColor' => 'bg-event-sent/70'],
                    'bounceRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_BOUNCED', 'Bounces'), 'type' => 'percentage', 'dotColor' => 'bg-event-bounced/70'],
                    'mobileRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_MOBILE', 'Mobile'), 'type' => 'percentage', 'dotColor' => 'bg-event-opened/70'],
                ];
            case self::CONTEXT_AUTOMATION:
            case self::CONTEXT_TRIGGER:
            default:
                return [
                    'lastSent' => ['label' => Base::lng('MAILBEEZ_PERFORMANCE_LAST_SENT', 'Last Sent'), 'type' => 'timeago'],
                    'sent' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_SENT', 'Sent'), 'type' => 'number', 'dotColor' => 'bg-event-sent/70'],
                    'orders' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_ORDERED', 'Orders'), 'type' => 'number', 'dotColor' => 'bg-event-ordered/70'],
                    'revenue' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_REVENUE', 'Revenue'), 'type' => 'currency', 'dotColor' => 'bg-event-ordered/70'],
                    'revenuePerEmail' => ['label' => Base::lng('MAILBEEZ_PERFORMANCE_REVENUE_PER_EMAIL', 'per Email'), 'type' => 'currency', 'dotColor' => 'bg-event-ordered/70'],
                    'deliveredRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_DELIVERED', 'Delivered'), 'type' => 'percentage', 'dotColor' => 'bg-event-sent/70'],
                    'openRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_OPENED', 'Opened'), 'type' => 'percentage', 'dotColor' => 'bg-event-opened/70'],
                    'clickRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_CLICKED', 'Clicked'), 'type' => 'percentage', 'dotColor' => 'bg-event-clicked/70'],
                    'orderRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_ORDERED', 'Ordered'), 'type' => 'percentage', 'dotColor' => 'bg-event-ordered/70'],
                    'bounceRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_BOUNCED', 'Bounced'), 'type' => 'percentage', 'dotColor' => 'bg-event-bounced/70'],
                    'mobileRate' => ['label' => Base::lng('MAILBEEZ_STATSBAR_LABEL_MOBILE', 'Mobile'), 'type' => 'percentage', 'dotColor' => 'bg-event-opened/70'],
                ];
        }
    }

    /**
     * Get performance data for a module or newsletter item.
     *
     * @param string $modKey
     * @param int    $itemId
     * @param string $context
     * @return array
     */
    public static function getPerformanceData(string $modKey = '', int $itemId = 0, string $context = self::CONTEXT_TRIGGER, array $options = []): array
    {
        switch ($context) {
            case self::CONTEXT_AUDIENCE:
                $perfData = self::getAudienceData($modKey, $itemId, $options);
                break;
            case self::CONTEXT_NEWSLETTER:
                $perfData = self::getNewsletterData($modKey, $itemId, $options);
                break;
            case self::CONTEXT_AUTOMATION:
                $perfData = self::getAutomationData($modKey, $itemId, $options);
                break;
            case self::CONTEXT_TRIGGER_TYPE_REVIEW:
                $perfData = self::getTriggerTypeReviewData($modKey, $itemId, $options);
                break;
            case self::CONTEXT_TRIGGER:
            default:
                $perfData = self::getTriggerData($modKey, $itemId, $options);
                break;
        }

        if (isset($options['metrics_data']) && is_array($options['metrics_data'])) {
            $perfData = array_merge($perfData, $options['metrics_data']);
        }

        if (isset($perfData['revenue']) && isset($perfData['sent']) && $perfData['sent'] > 0) {
            $perfData['revenuePerEmail'] = (float)$perfData['revenue'] / $perfData['sent'];
        }

        if (isset($perfData['sent'])) {
            $perfData['orderRate'] = $perfData['sent'] > 0 ? ((int)($perfData['orders'] ?? 0) / (int)$perfData['sent']) * 100 : 0;
            $perfData['deliveredRate'] = 100 - (float)($perfData['bounceRate'] ?? 0);

            // Derive absolute counts so the rate rings can show the underlying number
            // (e.g. delivered/bounced emails) instead of repeating the percentage.
            $sentInt = (int)$perfData['sent'];
            $perfData['bounced'] = (int)round($sentInt * (float)($perfData['bounceRate'] ?? 0) / 100);
            $perfData['delivered'] = max(0, $sentInt - $perfData['bounced']);
        }
        if (isset($perfData['opened'])) {
            $perfData['mobile'] = (int)round((int)$perfData['opened'] * (float)($perfData['mobileRate'] ?? 0) / 100);
        }

        // Map each rate metric to the source key holding its absolute count.
        $countSourceMap = [
            'deliveredRate' => 'delivered',
            'openRate' => 'opened',
            'clickRate' => 'clicked',
            'orderRate' => 'orders',
            'bounceRate' => 'bounced',
            'mobileRate' => 'mobile',
        ];

        $definition = self::getMetricsDefinition($context);

        $metrics = [];
        foreach ($definition as $key => $config) {
            $value = $perfData[$key] ?? 0;
            $hasCount = isset($countSourceMap[$key]) && array_key_exists($countSourceMap[$key], $perfData);
            $countRaw = $hasCount ? (int)$perfData[$countSourceMap[$key]] : null;
            $metrics[] = [
                'key' => $key,
                'isFiller' => false,
                'label' => $config['label'],
                'value' => self::formatValue($value, $config['type']),
                'raw' => $value,
                'count' => $hasCount ? self::formatValue($countRaw, 'number') : null,
                'countRaw' => $countRaw,
                'color' => $config['color'] ?? 'text-primary',
                'type' => $config['type'],
                'dotColor' => $config['dotColor'] ?? '',
            ];
        }

        // Carry the "last update of data" timestamp as a non-card helper metric so the
        // dashboard can show when the underlying data last changed (not just last sent).
        if (array_key_exists('lastUpdate', $perfData)) {
            $metrics[] = [
                'key' => 'lastUpdate',
                'isFiller' => false,
                'label' => Base::lng('MAILBEEZ_PERFORMANCE_LAST_UPDATE', 'Last Update'),
                'value' => self::formatValue($perfData['lastUpdate'], 'timeago'),
                'raw' => $perfData['lastUpdate'],
                'count' => null,
                'countRaw' => null,
                'color' => 'text-primary',
                'type' => 'timeago',
                'dotColor' => '',
            ];
        }

        return $metrics;
    }

    public static function getTriggerData(string $modKey, int $itemId, array $options = []): array
    {
        return DataRegistry::analytics()->getModulePerformanceData($modKey, $itemId, $options);
    }

    public static function getTriggerTypeReviewData(string $modKey, int $itemId, array $options = []): array
    {
        return DataRegistry::analytics()->getModulePerformanceData($modKey, $itemId, $options);
    }

    public static function getNewsletterData(string $modKey, int $itemId, array $options = []): array
    {
        return DataRegistry::analytics()->getModulePerformanceData($modKey, $itemId, $options);
    }

    public static function getAutomationData(string $modKey, int $itemId, array $options = []): array
    {
        return DataRegistry::analytics()->getModulePerformanceData($modKey, $itemId, $options);
    }

    public static function getAudienceData(string $modKey, int $itemId, array $options = []): array
    {
        $forceRefresh = !empty($options['refresh']);
        return DataRegistry::analytics()->getAudiencePerformanceData($itemId, $forceRefresh);
    }

    public static function timeFrameSelector(string $context = self::CONTEXT_TRIGGER) {
        $storeName = self::getStoreName($context);
        $saveUrl = \Components\Router::url('save_module_config', ['partial' => 1, 'module' => 'ui', 'context' => $context]);
        $timeframeItems = [
                ['label' => '1d', 'value' => '1day'],
                ['label' => '7d', 'value' => '7days'],
                ['label' => '14d', 'value' => '14days'],
                ['label' => '30d', 'value' => '30days'],
                ['label' => 'MTD', 'value' => 'MTD'],
                ['label' => 'YTD', 'value' => 'YTD'],
                ['label' => 'All', 'value' => 'all'],
        ];

        $timeframeSelector = \Components\Ui\TabsGroup::render($timeframeItems, [
                'model' => '$store.' . $storeName . '.performanceTimeframe',
                'size' => 'sm',
                'attrs' => [
                        'x-show' => '$store.' . $storeName . ' && $store.' . $storeName . '.viewMode === "performance"',
                        'x-cloak' => true,
                        'class' => 'ml-4',
                        '@click' => '$nextTick(() => htmx.ajax(\'POST\', \'' . $saveUrl . '\', { values: { performance_timeframe: $store.' . $storeName . '.performanceTimeframe }, swap: \'none\' }))'
                ]
        ]);
        return $timeframeSelector;
    }

    public static function getStoreName(string $context): string
    {
        switch ($context) {
            case self::CONTEXT_AUDIENCE:
                return 'audience';
            case self::CONTEXT_NEWSLETTER:
                return 'newsletter';
            case self::CONTEXT_AUTOMATION:
            case self::CONTEXT_TRIGGER:
            case self::CONTEXT_TRIGGER_TYPE_REVIEW:
            default:
                return 'trigger';
        }
    }

    public static function getTimeframeLabel(string $timeframe): string
    {
        switch ($timeframe) {
            case '1day':   return Base::lng('MAILBEEZ_PERFORMANCE_1D', 'Last 24 hours');
            case '7days':  return Base::lng('MAILBEEZ_PERFORMANCE_7D', 'Last 7 days');
            case '14days': return Base::lng('MAILBEEZ_PERFORMANCE_14D', 'Last 14 days');
            case '30days': return Base::lng('MAILBEEZ_PERFORMANCE_30D', 'Last 30 days');
            case 'MTD':    return Base::lng('MAILBEEZ_PERFORMANCE_MTD', 'Month to Date');
            case 'YTD':    return Base::lng('MAILBEEZ_PERFORMANCE_YTD', 'Year to Date');
            case 'all':    return Base::lng('MAILBEEZ_PERFORMANCE_ALL', 'All time');
            default:       return $timeframe;
        }
    }

    /**
     * Resolve timeframe slugs into date ranges.
     *
     * @param string $timeframe
     * @return array [start, end]
     */
    public static function resolveTimeframe(string $timeframe): array
    {
        $end = date('Y-m-d 23:59:59');
        switch ($timeframe) {
            case '1day':   $start = date('Y-m-d 00:00:00', strtotime('-1 day')); break;
            case '7days':  $start = date('Y-m-d 00:00:00', strtotime('-6 days')); break;
            case '14days': $start = date('Y-m-d 00:00:00', strtotime('-13 days')); break;
            case '30days': $start = date('Y-m-d 00:00:00', strtotime('-29 days')); break;
            case 'MTD':    $start = date('Y-m-01 00:00:00'); break;
            case 'YTD':    $start = date('Y-01-01 00:00:00'); break;
            case 'all':
            default:       $start = (string)mh_cfg('MAILBEEZ_ANALYTICS_BEGIN_OF_TIME', '2000-01-01 00:00:00'); break;
        }
        return [$start, $end];
    }

    /**
     * Lightweight read of the "age of data" timestamp used to seed the badge on
     * lazy (deferred) panels. Must NOT trigger any expensive recompute; it only
     * reads the existing cache entry. Returns an ATOM-formatted string or ''.
     */
    protected static function getLazyLastUpdate(string $context, int $itemId): string
    {
        try {
            if ($context === self::CONTEXT_AUDIENCE && $itemId > 0) {
                $audience = \Components\Models\Audience::find($itemId);
                if ($audience) {
                    $rc = (array)$audience->getResultCountCache(\Components\Models\Audience::RESULT_COUNT_CACHE_TTL);
                    $ts = isset($rc['ts']) ? (int)$rc['ts'] : 0;
                    if ($ts > 0) {
                        return (new \DateTime('@' . $ts))->format(\DateTime::ATOM);
                    }
                }
            }
        } catch (\Throwable $e) { /* ignore cache/lookup issues */ }
        return '';
    }

    public static function getEmptyMetrics(string $context = self::CONTEXT_TRIGGER): array
    {
        $definition = self::getMetricsDefinition($context);

        $metrics = [];
        foreach ($definition as $key => $config) {
            $metrics[] = [
                'key' => $key,
                'isFiller' => false,
                'label' => $config['label'],
                'value' => self::formatValue(0, $config['type']),
                'raw' => 0,
                'count' => null,
                'countRaw' => null,
                'color' => $config['color'] ?? 'text-primary',
                'type' => $config['type'],
                'dotColor' => $config['dotColor'] ?? '',
            ];
        }

        return $metrics;
    }

    /**
     * Format a metric value based on its type.
     *
     * @param mixed  $value
     * @param string $type
     * @return string
     */
    protected static function formatValue($value, string $type): string
    {
        switch ($type) {
            case 'currency':
                if (function_exists('mh_price')) {
                    $val = mh_price((float)$value);
                    return html_entity_decode($val, ENT_QUOTES | ENT_HTML5, 'UTF-8');
                }
                return number_format((float)$value, 2) . ' EUR';
            case 'percentage':
                return number_format((float)$value, 1) . '%';
            case 'timeago':
                return $value ? (string)$value : '';
            case 'number':
            default:
                return (string)$value;
        }
    }

    /**
     * Render the timeframe label.
     *
     * @param string $initialTimeframeLabel
     * @param string $containerClasses
     * @param string $extraAttrs
     * @return string
     */
    public static function renderTimeframeLabel(string $initialTimeframeLabel, string $containerClasses = 'absolute bottom-1 right-1', string $extraAttrs = ''): string
    {
        ob_start();
        ?>
        <div class="<?= $containerClasses ?> pointer-events-none border border-divider rounded-full px-1.5 py-0.5 flex items-center gap-1 bg-white/70 dark:bg-zinc-800/50" <?= $extraAttrs ?>>
            <span class="shrink-0 [&_svg]:size-3 text-secondary/40"><?= Base::SVG_OUTLINE_CALENDAR ?></span>
            <span class="text-[10px] text-secondary/60 whitespace-nowrap" x-text="timeframeLabel"><?= Base::h($initialTimeframeLabel) ?></span>
        </div>
        <?php
        return (string)ob_get_clean();
    }

    /**
     * Render the Performance micro-dashboard.
     *
     * @param string $modKey
     * @param int    $itemId
     * @param array  $options
     * @return string
     */
    public static function render(string $modKey = '', int $itemId = 0, array $options = []): string
    {
        $context = $options['context'] ?? null;
        $item = null;
        if ($context === null) {
            if ($itemId > 0) {
                $item = \Components\Models\NewsletterItem::find($itemId);
                $context = $item ? $item->getPageMode() : self::CONTEXT_NEWSLETTER;
            } else {
                $context = self::CONTEXT_TRIGGER;
            }
        }

        $useCache = $options['use_cache'] ?? true;
        $refresh = $options['refresh'] ?? (mh_get('refresh') === '1');
        // Keep the resolved refresh flag in $options so downstream data providers
        // (e.g. audience result count) can force a recompute instead of serving cache.
        $options['refresh'] = $refresh;
        $lazy = $options['lazy'] ?? false;
        $format = $options['format'] ?? mh_get('format');

        $ownerType = FlowHtmlCache::OWNER_MODULE_PERFORMANCE;
        if ($context === self::CONTEXT_AUDIENCE) {
            $ownerType = FlowHtmlCache::OWNER_AUDIENCE_PERFORMANCE;
        } elseif ($itemId > 0 || $context === self::CONTEXT_NEWSLETTER || $context === self::CONTEXT_AUTOMATION) {
            $ownerType = FlowHtmlCache::OWNER_NEWSLETTER_ITEM_PERFORMANCE;
        }

        $ownerId = $itemId > 0 ? $itemId : $modKey;

        $timeframe = $options['timeframe'] ?? (string)mh_get('timeframe', '');
        if ($timeframe === '') {
            $timeframe = \Components\ConfigStore::getPerformanceTimeframe($context);
        }
        $options['timeframe'] = $timeframe;

        // Partition cache by timeframe to allow instant switching
        $ownerId .= '_' . $timeframe;

        // If context is provided and not default, include it in ownerId to avoid cache collisions
        if ($context !== self::CONTEXT_TRIGGER) {
            $ownerId .= '_' . $context;
        }

        $lang = $_SESSION['language'] ?? 'default';

        $metrics = null;
        $triggerRefresh = false;

        if ($useCache && !$refresh) {
            $cached = FlowHtmlCache::get($ownerType, $ownerId, $lang);
            if (is_string($cached) && $cached !== '') {
                $metrics = json_decode($cached, true);
            }
        }

        if ($metrics === null) {
            if ($lazy && !$refresh) {
                $metrics = self::getEmptyMetrics($context);
                $triggerRefresh = true;

                // Even though the heavy metric compute is deferred to the on-intersect
                // refresh, surface the age of the already-cached data now so the
                // "last update" badge reflects the true data age (read cheaply from
                // the DB cache) instead of falling back to the page-load moment.
                $seededLastUpdate = self::getLazyLastUpdate($context, $itemId);
                if ($seededLastUpdate !== '') {
                    $metrics[] = [
                        'key' => 'lastUpdate',
                        'isFiller' => false,
                        'label' => Base::lng('MAILBEEZ_PERFORMANCE_LAST_UPDATE', 'Last Update'),
                        'value' => self::formatValue($seededLastUpdate, 'timeago'),
                        'raw' => $seededLastUpdate,
                        'count' => null,
                        'countRaw' => null,
                        'color' => 'text-primary',
                        'type' => 'timeago',
                        'dotColor' => '',
                    ];
                }
            } else {
                // Some legacy layers may emit HTML comments during computation; suppress incidental output
                ob_start();
                try {
                    $metrics = self::getPerformanceData($modKey, $itemId, $context, $options);
                } finally {
                    ob_end_clean();
                }

                if ($useCache) {
                    FlowHtmlCache::set($ownerType, $ownerId, $lang, json_encode($metrics));
                }
            }
        }

        // Pull the "last update of data" helper metric out of the list so it never renders
        // as a card and does not affect column/filler calculations.
        $lastUpdate = '';
        if (is_array($metrics)) {
            foreach ($metrics as $mi => $m) {
                if (($m['key'] ?? '') === 'lastUpdate') {
                    $lastUpdate = $m['raw'] ?? '';
                    unset($metrics[$mi]);
                }
            }
            $metrics = array_values($metrics);
        }

        // For audience panels the metrics list usually comes from the cached JSON
        // (FlowHtmlCache OWNER_AUDIENCE_PERFORMANCE). Older cache entries — or those
        // whose value was empty when first stored — carry no "lastUpdate" helper
        // metric, so the badge would fall back to the page-load moment even though a
        // valid timestamp exists in the DB result-count cache. Seed it cheaply here
        // (cache read only, no recompute) so the badge always reflects the real data
        // age regardless of which render branch produced $metrics.
        if (($lastUpdate === '' || $lastUpdate === null) && $context === self::CONTEXT_AUDIENCE && $itemId > 0) {
            $lastUpdate = self::getLazyLastUpdate($context, $itemId);
        }

        $realCount = count($metrics);
        $xlCols = max(1, $realCount);
        $lgCols = min(4, $xlCols);
        $smCols = 2;

        $totalForLg = ceil($realCount / $lgCols) * $lgCols;
        $totalForSm = ceil($realCount / $smCols) * $smCols;
        $maxTotal = max($totalForLg, $totalForSm);

        for ($i = $realCount; $i < $maxTotal; $i++) {
            $showSm = ($i < $totalForSm);
            $showLg = ($i < $totalForLg);
/*
            $class = 'hidden';
            if ($showSm && $showLg) $class = 'hidden sm:flex xl:hidden';
            elseif ($showSm)        $class = 'hidden sm:flex lg:hidden';
            elseif ($showLg)        $class = 'hidden lg:flex xl:hidden';
*/
            $metrics[] = [
                'isFiller' => true,
                'label' => 'filler-' . $i,
                'value' => '',
                'color' => '',
                'type' => 'filler',
//                'fillerClass' => $class,
            ];
        }

        if ($format === 'json') {
            // Ensure no incidental output (e.g. legacy comments) pollutes the JSON response
            if (ob_get_level() > 0) {
                ob_clean();
            }
            return json_encode([
                'metrics' => $metrics,
                'timeframeLabel' => self::getTimeframeLabel($timeframe),
                'lastUpdate' => $lastUpdate,
            ]);
        }

        $viewMode = $options['view_mode'] ?? 'compact'; // 'compact'; // compact | grid

        $refreshUrl = $options['refresh_url'] ?? '';
        if ($refreshUrl === '') {
            $params = ['partial' => 1, 'view_mode' => 'performance', 'refresh' => 1, 'format' => 'json'];
            if ($timeframe !== '') {
                $params['timeframe'] = $timeframe;
            }
            if ($context !== self::CONTEXT_TRIGGER) {
                $params['context'] = $context;
            }

            if ($itemId > 0) {
                $params['id'] = $itemId;
                $routeName = ($context === self::CONTEXT_AUDIENCE) ? 'audience_list_flow' : 'newsletter_item_flow';
                $refreshUrl = Router::url($routeName, $params, true);
            } elseif ($modKey !== '') {
                $params['module'] = $modKey;
                $refreshUrl = Router::url('trigger_module', $params, true);
            }
        }
        $refreshUrl = Router::makeAbsolute($refreshUrl);

        $analyticsUrl = '';
        if (class_exists('Components\\Legacy\\LegacyRouter')) {
            /** @var \Components\Legacy\LegacyRouter $router */
            $router = 'Components\\Legacy\\LegacyRouter';
            if (in_array($context, [self::CONTEXT_TRIGGER, self::CONTEXT_TRIGGER_TYPE_REVIEW]) && $modKey !== '') {
                $analyticsUrl = $router::url('reportAnalyticsAdvanced', ['selected_module' => $modKey]);
            } elseif (in_array($context, [self::CONTEXT_NEWSLETTER, self::CONTEXT_AUTOMATION]) && $itemId > 0) {
                $item = $item ?? \Components\Models\NewsletterItem::find($itemId);
                if ($item) {
                    $analyticsUrl = $router::url('reportAnalyticsAdvanced', [
                        'selected_module' => 'mb_newsletter',
                        'selected_campaign' => $item->newsletter_campaign_id,
                        'selected_campaign_item' => $itemId,
                    ]);
                }
            }
        }

        $commonData = [
            'modKey' => $modKey,
            'itemId' => $itemId,
            'context' => $context,
            'metrics' => $metrics,
            'timeframe' => $timeframe,
            'triggerRefresh' => $triggerRefresh,
            'options' => $options,
            'item' => $item,
            'refreshUrl' => $refreshUrl,
            'analyticsUrl' => $analyticsUrl,
            'lastUpdate' => $lastUpdate,
            'storeName' => self::getStoreName($context),
        ];

        if ($viewMode === 'grid' || $context === self::CONTEXT_AUDIENCE) {
            return self::renderGrid($commonData);
        }

        return self::renderCompact($commonData);
    }

    /**
     * Render the Performance compact dashboard.
     *
     * @param array $data
     * @return string
     */
    public static function renderCompact(array $data): string
    {
        $metrics = $data['metrics'];
        $refreshUrl = $data['refreshUrl'];
        $triggerRefresh = $data['triggerRefresh'];
        $storeName = $data['storeName'];
        $timeframe = $data['timeframe'];
        $context = $data['context'];
        $analyticsUrl = $data['analyticsUrl'];
        $options = $data['options'];

        $initialTimeframeLabel = self::getTimeframeLabel($timeframe);
        $title = $options['title'] ?? '';
        if ($title === true) {
            $title = $initialTimeframeLabel;
        }

        $radialRadius = 44;
        $radialSize = 48;
        $radialStroke = 10;
        $rowHeight = 'h-12';
        $classesLabel = 'text-secondary text-[10px] font-medium leading-tight';
        $classesRadialValue = 'absolute inset-0 flex items-center justify-center text-zinc-600 font-medium text-[10px]';

        $metricsJson = json_encode($metrics);

        ob_start();
        ?>
        <div class="relative group/perf bg-white dark:bg-zinc-800/50 rounded-xl border border-divider/50 p-6 px-8" x-data="{
            refreshUrl: '<?= Base::h($refreshUrl); ?>',
            metrics: <?= Base::h($metricsJson, false); ?>,
            timeframeLabel: '<?= Base::h($initialTimeframeLabel); ?>',
            lastUpdate: '<?= Base::h($data['lastUpdate'] ?? ''); ?>',
            lastRefresh: new Date(),
            refreshing: false,
            isDirty: <?= $triggerRefresh ? 'true' : 'false' ?>,
            isVisible: false,
            get store() {
                return $store['<?= $storeName ?>'];
            },
            get timeframe() {
                // Fall back to the server-rendered timeframe when the store has no
                // performanceTimeframe value (e.g. the audience store only seeds
                // viewMode). Without this guard the timeframe would read as
                // undefined and falsely mark the panel dirty, forcing an
                // unnecessary refresh on every navigation even when the cache is valid.
                return (this.store && this.store.performanceTimeframe) ? this.store.performanceTimeframe : '<?= $timeframe ?>';
            },
            getMetric(key) {
                return this.metrics.find(m => m.key === key) || { value: 0, raw: 0 };
            },
            async refresh() {
                if (this.refreshing) return;
                if (!this.refreshUrl) return;

                this.refreshing = true;
                const self = this;
                // The actual network work. Routed through a low-priority, single
                // in-flight queue so many expanded/visible panels don't fire at once.
                const task = async function () {
                    // Skip if the panel was removed (e.g. page switch / list re-render).
                    if (!self.$el || !self.$el.isConnected) return;
                    const tf = self.timeframe;
                    const u = new URL(self.refreshUrl, window.location.origin);
                    if (tf) {
                        u.searchParams.set('timeframe', tf);
                    }
                    // Hint the browser to fetch at low priority (Fetch Priority
                    // API). These lazy, on-intersect metric refreshes must never
                    // contend with navigation or user-initiated requests, which
                    // otherwise default to High in the network panel.
                    const res = await fetch(u.toString(), { priority: 'low' });
                    const json = await res.json();
                    self.metrics = json.metrics;
                    self.timeframeLabel = json.timeframeLabel;
                    self.lastUpdate = json.lastUpdate;
                    self.lastRefresh = new Date();
                    self.isDirty = false;
                };
                try {
                    if (window.BeezUI && typeof window.BeezUI.queuePerformanceRefresh === 'function') {
                        await window.BeezUI.queuePerformanceRefresh(task);
                    } else {
                        await task();
                    }
                } catch (e) { console.error('Performance refresh failed', e); }
                this.refreshing = false;
            }
        }"
        x-init="
            $watch('timeframe', (val) => {
                isDirty = true;
                if (isVisible) refresh();
            });
            if (timeframe !== '<?= $timeframe ?>') {
                isDirty = true;
            }
            $nextTick(() => {
                if (isDirty && $el.offsetParent !== null) {
                    isVisible = true;
                    refresh();
                }
            });
        "
        x-intersect="isVisible = true; if (isDirty) refresh();"
        x-intersect:leave="isVisible = false"
        x-show="typeof localViewMode === 'undefined' || localViewMode === 'performance'"
        >
            <div class="absolute top-0 right-0 flex items-start justify-between mb-2">
                <button type="button"
                        class="ml-auto inline-flex items-center gap-1 rounded-bl-md rounded-tr-xl px-2 py-1 text-xs font-normal text-secondary hover:text-primary hover:bg-muted"
                        title="<?= Base::h(Base::lng('UI_REFRESH', 'Refresh')); ?>"
                        @click.stop="refresh()">
                    <!-- Show the age of the underlying data (lastUpdate). Falls back to
                         the page-load/refresh moment only when no data timestamp exists. -->
                    <span x-timeago.seconds="lastUpdate || lastRefresh"
                          class="whitespace-nowrap opacity-70"
                          title="<?= Base::h(Base::lng('MAILBEEZ_PERFORMANCE_LAST_UPDATE', 'Last Update')); ?>"></span>
                    <span :class="refreshing ? 'animate-spin' : ''">
                        <?= Base::SVG_REFRESH; ?>
                    </span>
                </button>
            </div>

            <div class="flex justify-center-safe items-start gap-1">
                <!-- Mobile (Optional) -->
                <template x-if="getMetric('mobileRate').key">
                    <div class="flex items-start">
                        <div class="flex flex-col items-center">
                            <div class="<?= $rowHeight; ?> flex items-center">
                                <?php
                                echo \Components\RadialProgress::render(0, [
                                    'radius' => $radialRadius,
                                    'size' => $radialSize,
                                    'strokeWidth' => $radialStroke,
                                    'colorClass' => 'text-event-opened',
                                    'trackColor' => 'text-event-opened/20',
                                    'labelClass' => $classesRadialValue,
                                    'alpineKey' => "getMetric('mobileRate').raw",
                                ]);
                                ?>
                            </div>
                            <div class="<?= $classesLabel ?> mt-1 flex items-center justify-center">
                                <span class="[&_svg]:size-3 mr-1"><?= Base::SVG_DEVICE_MOBILE ?></span>
                                <span x-text="getMetric('mobileRate').label"></span>
                            </div>
                            <div class="<?= $classesLabel ?>" x-text="getMetric('mobileRate').count ?? getMetric('mobileRate').value"></div>
                        </div>
                        <div class="flex items-center <?= $rowHeight; ?> px-4">
                            <div class="h-10 border-r border-divider"></div>
                        </div>
                    </div>
                </template>

                <!-- Sent -->
                <div class="min-w-36 flex flex-col text-center justify-center mr-4">
                    <div class="text-xl tracking-tight font-medium flex items-center justify-center text-primary leading-none <?= $rowHeight; ?> t-digit-group"
                         x-data="animatedNumber(getMetric('sent').raw)"
                         x-effect="update(getMetric('sent').raw)"
                         :class="isAnimating && 'is-animating'">
                        <template x-for="(digit, index) in digits" :key="index">
                            <span class="t-digit" :data-stagger="index" x-text="digit"></span>
                        </template>
                    </div>
                    <div class="<?= $classesLabel ?> mt-1" x-text="getMetric('sent').label"></div>
                    <div class="<?= $classesLabel ?>" :title="getMetric('lastSent').label">
                        <template x-if="getMetric('lastSent').value">
                            <time :datetime="getMetric('lastSent').value" x-data="{ date: new Date(getMetric('lastSent').value) }" x-timeago="date" class="whitespace-nowrap"></time>
                        </template>
                        <template x-if="!getMetric('lastSent').value">
                            <span>&nbsp;</span>
                        </template>
                    </div>
                </div>

                <!-- Arrows and Radials -->
                <?php
                $steps = [
                    ['key' => 'deliveredRate', 'color' => 'text-event-sent/70', 'bg' => 'text-event-sent/20'],
                    ['key' => 'openRate', 'color' => 'text-event-opened/70', 'bg' => 'text-event-opened/20'],
                    ['key' => 'clickRate', 'color' => 'text-event-clicked/70', 'bg' => 'text-event-clicked/20'],
                    ['key' => 'orderRate', 'color' => 'text-event-ordered/70', 'bg' => 'text-event-ordered/20'],
                ];
                foreach ($steps as $step):
                ?>
                <template x-if="getMetric('<?= $step['key'] ?>').key">
                    <div class="flex items-start gap-1">
                        <div class="text-2xl text-zinc-300 flex items-center justify-center <?= $rowHeight; ?>">
                            <?= Base::SVG_CHEVRON_RIGHT; ?>
                        </div>
                        <div class="flex flex-col items-center">
                            <div class="<?= $rowHeight; ?> flex items-center">
                                <?php
                                echo \Components\RadialProgress::render(0, [
                                    'radius' => $radialRadius,
                                    'size' => $radialSize,
                                    'strokeWidth' => $radialStroke,
                                    'colorClass' => $step['color'],
                                    'trackColor' => $step['bg'],
                                    'labelClass' => $classesRadialValue,
                                    'alpineKey' => "getMetric('{$step['key']}').raw",
                                ]);
                                ?>
                            </div>
                            <div class="<?= $classesLabel ?> mt-1" x-text="getMetric('<?= $step['key'] ?>').label"></div>
                            <div class="<?= $classesLabel ?>" x-text="getMetric('<?= $step['key'] ?>').count ?? getMetric('<?= $step['key'] ?>').value"></div>
                        </div>
                    </div>
                </template>
                <?php endforeach; ?>

                <div class="text-2xl text-zinc-300 flex items-center justify-center <?= $rowHeight; ?> ml-2">
                    <?= Base::SVG_CHEVRON_RIGHT; ?>
                </div>

                <!-- Revenue -->
                <div class="min-w-36 flex flex-col text-center justify-center ml-4">
                    <div class="text-xl tracking-tight font-medium flex items-center justify-center text-primary leading-none <?= $rowHeight; ?> t-digit-group"
                         x-data="animatedNumber(getMetric('revenue').value)"
                         x-effect="update(getMetric('revenue').value)"
                         :class="isAnimating && 'is-animating'">
                         <template x-for="(digit, index) in digits" :key="index">
                            <span class="t-digit" :data-stagger="index" x-text="digit"></span>
                        </template>
                    </div>
                    <div class="<?= $classesLabel ?> mt-1" x-text="getMetric('revenue').label"></div>
                    <div class="<?= $classesLabel ?>">&nbsp;</div>
                </div>

                <!-- Revenue per Email -->
                <template x-if="getMetric('revenuePerEmail').key">
                    <div class="min-w-36 flex flex-col text-center justify-center ml-4">
                        <div class="text pt-0.5 tracking-tight font-medium flex items-center justify-center text-primary leading-none <?= $rowHeight; ?> t-digit-group"
                             x-data="animatedNumber(getMetric('revenuePerEmail').value)"
                             x-effect="update(getMetric('revenuePerEmail').value)"
                             :class="isAnimating && 'is-animating'">
                             <template x-for="(digit, index) in digits" :key="index">
                                <span class="t-digit" :data-stagger="index" x-text="digit"></span>
                            </template>
                        </div>
                        <div class="<?= $classesLabel ?> mt-1" x-text="getMetric('revenuePerEmail').label"></div>
                        <div class="<?= $classesLabel ?>">&nbsp;</div>
                    </div>
                </template>
            </div>
            <?php if ($title && $context !== self::CONTEXT_AUDIENCE): ?>
                <?= self::renderTimeframeLabel($initialTimeframeLabel, 'absolute bottom-2 right-2'); ?>
            <?php endif; ?>
        </div>
        <?php
        return (string)ob_get_clean();
    }

    /**
     * Render the Performance grid dashboard.
     *
     * @param array $data
     * @return string
     */
    public static function renderGrid(array $data): string
    {
        $metrics = $data['metrics'];
        $refreshUrl = $data['refreshUrl'];
        $triggerRefresh = $data['triggerRefresh'];
        $storeName = $data['storeName'];
        $timeframe = $data['timeframe'];
        $context = $data['context'];
        $analyticsUrl = $data['analyticsUrl'];
        $options = $data['options'];
        $itemId = (int)($data['itemId'] ?? 0);

        $realCount = count($metrics);
        $xlCols = max(1, $realCount);
        $lgCols = min(4, $xlCols);
        $smCols = 2;

        $totalForLg = ceil($realCount / $lgCols) * $lgCols;
        $totalForSm = ceil($realCount / $smCols) * $smCols;
        $maxTotal = max($totalForLg, $totalForSm);

        for ($i = $realCount; $i < $maxTotal; $i++) {
            $metrics[] = [
                'isFiller' => true,
                'label' => 'filler-' . $i,
                'value' => '',
                'color' => '',
                'type' => 'filler',
            ];
        }

        $metricsJson = json_encode($metrics);
        $initialTimeframeLabel = self::getTimeframeLabel($timeframe);
        $title = $options['title'] ?? '';
        if ($title === true) {
            $title = $initialTimeframeLabel;
        }

        // External refresh listeners: when the underlying data of this panel changes
        // elsewhere (e.g. an audience segmentation is edited, which recomputes the
        // subscriber count), the legacy/save flow emits a body/window event such as
        // "audience_lists:counts:refresh" carrying the affected item id. We reuse that
        // existing count-anchor contract here so the performance metrics stay in sync
        // instead of showing stale numbers until the next intersect/navigation.
        $externalRefreshAttrs = '';
        if ($itemId > 0 && $context === self::CONTEXT_AUDIENCE) {
            $idJs = (string)$itemId;
            // Mark dirty (so a deferred/hidden panel refreshes once it becomes visible)
            // and refresh immediately when already visible. Scope to the matching id so
            // unrelated audiences are not refreshed.
            $handler = "if (\$event.detail && String(\$event.detail.id) === '" . $idJs . "') { isDirty = true; if (isVisible) refresh(); }";
            $externalRefreshAttrs = ' @audience_lists:counts:refresh.window="' . Base::h($handler, false) . '"';
        }

        ob_start();
        ?>
        <div class="relative group/perf"<?= $externalRefreshAttrs ?> x-data="{
            refreshUrl: '<?= Base::h($refreshUrl); ?>',
            metrics: <?= Base::h($metricsJson, false); ?>,
            timeframeLabel: '<?= Base::h($initialTimeframeLabel); ?>',
            lastUpdate: '<?= Base::h($data['lastUpdate'] ?? ''); ?>',
            lastRefresh: new Date(),
            refreshing: false,
            isDirty: <?= $triggerRefresh ? 'true' : 'false' ?>,
            isVisible: false,
            get store() {
                return $store['<?= $storeName ?>'];
            },
            get timeframe() {
                // Fall back to the server-rendered timeframe when the store has no
                // performanceTimeframe value (e.g. the audience store only seeds
                // viewMode). Without this guard the timeframe would read as
                // undefined and falsely mark the panel dirty, forcing an
                // unnecessary refresh on every navigation even when the cache is valid.
                return (this.store && this.store.performanceTimeframe) ? this.store.performanceTimeframe : '<?= $timeframe ?>';
            },
            async refresh() {
                if (this.refreshing) return;
                if (!this.refreshUrl) return;

                this.refreshing = true;
                const self = this;
                // Routed through a low-priority, single in-flight queue so many
                // expanded/visible panels don't fire requests all at once.
                const task = async function () {
                    if (!self.$el || !self.$el.isConnected) return;
                    const tf = self.timeframe;
                    const u = new URL(self.refreshUrl, window.location.origin);
                    if (tf) {
                        u.searchParams.set('timeframe', tf);
                    }
                    // Hint the browser to fetch at low priority (Fetch Priority
                    // API). These lazy, on-intersect metric refreshes must never
                    // contend with navigation or user-initiated requests, which
                    // otherwise default to High in the network panel.
                    const res = await fetch(u.toString(), { priority: 'low' });
                    const json = await res.json();
                    self.metrics = json.metrics;
                    self.timeframeLabel = json.timeframeLabel;
                    self.lastUpdate = json.lastUpdate;
                    self.lastRefresh = new Date();
                    self.isDirty = false;
                };
                try {
                    if (window.BeezUI && typeof window.BeezUI.queuePerformanceRefresh === 'function') {
                        await window.BeezUI.queuePerformanceRefresh(task);
                    } else {
                        await task();
                    }
                } catch (e) { console.error('Performance refresh failed', e); }
                this.refreshing = false;
            }
        }"
        x-init="
            $watch('timeframe', (val) => {
                isDirty = true;
                if (isVisible) refresh();
            });
            if (timeframe !== '<?= $timeframe ?>') {
                isDirty = true;
            }
            $nextTick(() => {
                if (isDirty && $el.offsetParent !== null) {
                    isVisible = true;
                    refresh();
                }
            });
        "
        x-intersect="isVisible = true; if (isDirty) refresh();"
        x-intersect:leave="isVisible = false"
        x-show="typeof localViewMode === 'undefined' || localViewMode === 'performance'"
        >
            <?php if ($refreshUrl): ?>
                <div class="absolute top-0 right-0 z-20 flex items-start justify-between mb-2">
                    <button type="button"
                            class="ml-auto inline-flex items-center gap-1 rounded-bl-md rounded-tr-xl px-2 py-1 text-xs font-normal text-secondary hover:text-primary hover:bg-muted"
                            title="<?= Base::h(Base::lng('UI_REFRESH', 'Refresh')); ?>"
                            aria-label="<?= Base::h(Base::lng('UI_REFRESH', 'Refresh')); ?>"
                            @click.stop="refresh()">
                        <!-- Show the age of the underlying data (lastUpdate). Falls back to
                             the page-load/refresh moment only when no data timestamp exists. -->
                        <span x-timeago.seconds="lastUpdate || lastRefresh"
                              class="whitespace-nowrap opacity-70"
                              title="<?= Base::h(Base::lng('MAILBEEZ_PERFORMANCE_LAST_UPDATE', 'Last Update')); ?>"></span>
                        <span :class="refreshing ? 'animate-spin' : ''">
                            <?= Base::SVG_REFRESH; ?>
                        </span>
                    </button>
                </div>
            <?php endif; ?>

            <dl class="grid grid-cols-1 gap-px overflow-hidden rounded-xl border border-divider/50 bg-divider/50 sm:grid-cols-2 lg:grid-cols-<?= $lgCols; ?> xl:grid-cols-<?= $xlCols; ?>">
                <template x-for="(metric, idx) in metrics" :key="metric.label || 'filler-' + idx">
                    <div class="contents">
                        <?php
                        $commonClass = 'p-4 bg-white dark:bg-zinc-800/50 flex flex-col text-left appearance-none w-full border-none transition-colors relative';
                        $cardAttrs = [
                            'class' => $commonClass,
                            ':class' => "metric.isFiller ? metric.fillerClass : '" . ($analyticsUrl ? 'cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-800/80 group/card' : '') . "'",
                        ];
                        if ($analyticsUrl) {
                            $cardAttrs[':command'] = "!metric.isFiller ? 'show-modal' : null";
                            $cardAttrs[':commandfor'] = "!metric.isFiller ? 'config-modal' : null";
                            $cardAttrs[':data-title'] = "!metric.isFiller ? '" . addslashes(Base::lng('MAILBEEZ_BUTTON_ANALYTICS_LABEL', 'Analytics')) . "' : null";
                            $cardAttrs[':data-size'] = "!metric.isFiller ? (['" . self::CONTEXT_TRIGGER . "', '" . self::CONTEXT_TRIGGER_TYPE_REVIEW . "'].includes('" . $context . "') ? 'full' : 'full-margin') : null";
                            $cardAttrs[':data-url'] = "!metric.isFiller ? '" . $analyticsUrl . "' : null";
                            $cardAttrs['hx-target'] = '#config-modal-body';
                            $cardAttrs['hx-swap'] = 'innerHTML';
                        }
                        ?>
                        <?php if ($analyticsUrl): ?>
                        <button type="button" <?= Base::attrs($cardAttrs); ?> :disabled="metric.isFiller">
                        <?php else: ?>
                        <div <?= Base::attrs($cardAttrs); ?>>
                        <?php endif; ?>
                            <div class="contents" x-show="!metric.isFiller">
                                <dt class="text-xs text-secondary/70 uppercase font-medium mb-1 line-clamp-1 flex items-center" :title="metric.label">
                                    <template x-if="metric.dotColor">
                                        <span class="inline-block w-2 h-2 rounded-full mr-1.5" :class="metric.dotColor"></span>
                                    </template>
                                    <span x-text="metric.label"></span>
                                </dt>
                                <dd class="text-lg font-bold" :class="metric.color">
                                    <template x-if="metric.type === 'timeago'">
                                        <span>
                                            <template x-if="metric.value">
                                                <time :datetime="metric.value" x-data="{ date: new Date(metric.value) }" x-timeago="date" class="whitespace-nowrap"></time>
                                            </template>
                                            <template x-if="!metric.value">
                                                <span>-</span>
                                            </template>
                                        </span>
                                    </template>

                                    <template x-if="metric.type !== 'timeago'">
                                        <span x-text="metric.value"></span>
                                    </template>
                                </dd>
                            </div>
                            <div x-show="metric.isFiller">&nbsp;</div>
                            <?php if ($title && $context !== self::CONTEXT_AUDIENCE): ?>
                                <?= self::renderTimeframeLabel($initialTimeframeLabel, 'absolute bottom-1 right-1', 'x-show="idx === metrics.length - 1"'); ?>
                            <?php endif; ?>
                        <?php if ($analyticsUrl): ?>
                        </button>
                        <?php else: ?>
                        </div>
                        <?php endif; ?>
                    </div>
                </template>
            </dl>
        </div>
        <?php
        return (string)ob_get_clean();
    }
}
