<?php
/**
 * Dashboard data helper/service.
 *
 * Transitional module-local service for the Dashboard module.
 * This version keeps the Dashboard composer here, but delegates Weather and
 * News logic to their module-local services.
 *
 * @package Noesia_Smart_Waiter
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

if ( ! class_exists( 'Noesia_SWA_Dashboard_Data' ) ) {
    /**
     * Dashboard data helper.
     */
    class Noesia_SWA_Dashboard_Data {

        /**
         * Cached Weather service instance.
         *
         * @var Noesia_SWA_Weather_Service|null
         */
        private $weather_service = null;

        /**
         * Cached News service instance.
         *
         * @var Noesia_SWA_News_Service|null
         */
        private $news_service = null;

        /**
         * Supported dashboard language codes.
         *
         * @return array<int,string>
         */
        private function get_supported_languages() {
            return array( 'en', 'el', 'fr', 'de', 'it', 'es' );
        }

        /**
         * Resolve a generic service class name.
         *
         * Modules own the contract via filters so the Dashboard helper does not
         * hardcode Weather/News ownership into the kernel bridge.
         *
         * @param string $service_key   Logical service key.
         * @param string $default_class Backward-compatible default class.
         * @return string
         */
        private function get_service_class_name( $service_key, $default_class ) {
            $service_key   = sanitize_key( (string) $service_key );
            $default_class = is_string( $default_class ) ? $default_class : '';

            $service_class = apply_filters( 'noesia_swa_service_class', $default_class, $service_key, $this );

            return is_string( $service_class ) ? $service_class : $default_class;
        }

        /**
         * Resolve a generic service bootstrap file.
         *
         * Modules can self-announce their owner file through filters. A legacy
         * fallback is kept only for backward compatibility while old bridges
         * still exist in snapshots during migration.
         *
         * @param string $service_key  Logical service key.
         * @param string $service_class Resolved service class.
         * @return string
         */
        private function get_service_file( $service_key, $service_class ) {
            $service_key  = sanitize_key( (string) $service_key );
            $service_file = apply_filters( 'noesia_swa_service_file', '', $service_key, $service_class, $this );

            if ( is_string( $service_file ) && '' !== $service_file ) {
                return $service_file;
            }

            if ( 'weather' === $service_key ) {
                return NOESIA_SWA_PLUGIN_DIR . 'modules/free/weather/includes/class-noesia-swa-weather-service.php';
            }

            if ( 'news' === $service_key ) {
                return NOESIA_SWA_PLUGIN_DIR . 'modules/free/news/includes/class-noesia-swa-news-service.php';
            }

            return '';
        }

        /**
         * Resolve and instantiate one module-owned service.
         *
         * @param string $service_key   Logical service key.
         * @param string $cache_property Local cache property name.
         * @param string $default_class Backward-compatible default class.
         * @return object|null
         */
        private function get_service_instance( $service_key, $cache_property, $default_class ) {
            if ( ! is_string( $cache_property ) || ! property_exists( $this, $cache_property ) ) {
                return null;
            }

            $cached = $this->{$cache_property};

            if ( is_object( $cached ) ) {
                return $cached;
            }

            $service_class = $this->get_service_class_name( $service_key, $default_class );

            if ( '' === $service_class ) {
                return null;
            }

            if ( ! class_exists( $service_class ) ) {
                $service_file = $this->get_service_file( $service_key, $service_class );

                if ( is_string( $service_file ) && '' !== $service_file && file_exists( $service_file ) ) {
                    require_once $service_file;
                }
            }

            if ( class_exists( $service_class ) ) {
                $this->{$cache_property} = new $service_class();
                return $this->{$cache_property};
            }

            return null;
        }

        /**
         * Get the Weather service.
         *
         * @return object|null
         */
        private function get_weather_service() {
            return $this->get_service_instance( 'weather', 'weather_service', 'Noesia_SWA_Weather_Service' );
        }

        /**
         * Get the News service.
         *
         * @return object|null
         */
        private function get_news_service() {
            return $this->get_service_instance( 'news', 'news_service', 'Noesia_SWA_News_Service' );
        }

        /**
         * Normalize a country value into ISO alpha-2 when possible.
         *
         * @param mixed $raw Raw value.
         * @return string
         */
        public function normalize_country_code( $raw ) {
            $service = $this->get_weather_service();

            if ( $service ) {
                return $service->normalize_country_code( $raw );
            }

            return '';
        }

        /**
         * Resolve the dashboard weather language from settings.
         *
         * @param array<string,mixed> $options Plugin options.
         * @return string
         */
        public function get_weather_lang( $options ) {
            $service = $this->get_weather_service();

            if ( $service ) {
                return $service->get_weather_lang( $options );
            }

            return 'en';
        }

        /**
         * Resolve the language used by dashboard narrative widgets.
         *
         * The Settings screen stores "Dashboard Language" as news_language.
         * That key now drives News, Weather, SMART AI ALERT and Global Pulse.
         *
         * @param array<string,mixed> $options Plugin options.
         * @return string
         */
        public function get_dashboard_language( $options ) {
            $options = is_array( $options ) ? $options : array();
            $lang    = isset( $options['news_language'] ) ? sanitize_key( (string) $options['news_language'] ) : '';

            if ( in_array( $lang, $this->get_supported_languages(), true ) ) {
                return $lang;
            }

            return $this->get_weather_lang( $options );
        }

        /**
         * Whether Feel the World live providers are explicitly enabled.
         *
         * This setting is scoped to the Feel the World live layer so the admin
         * can make an explicit privacy decision before the dashboard contacts
         * remote world-data providers.
         *
         * @param array<string,mixed> $options Plugin options.
         * @return bool
         */
        public function is_world_live_data_enabled( $options ) {
            $options = is_array( $options ) ? $options : array();
            $value   = isset( $options['dashboard_world_live_data_enabled'] ) ? sanitize_key( (string) $options['dashboard_world_live_data_enabled'] ) : 'no';

            return 'yes' === $value;
        }

        /**
         * Empty earthquake snapshot for disabled or unavailable live providers.
         *
         * @return array<string,mixed>
         */
        private function get_empty_world_earthquake_snapshot() {
            return array(
                'source_label'  => 'USGS',
                'updated_label' => '—',
                'count_24h'     => 0,
                'largest_label' => '—',
                'largest_place' => __( 'Live earthquake data is currently unavailable.', 'noesia-smart-waiter' ),
                'items'         => array(),
            );
        }

        /**
         * Empty major-events snapshot for disabled or unavailable live providers.
         *
         * @return array<string,mixed>
         */
        private function get_empty_world_events_snapshot() {
            return array(
                'source_label'  => 'GDACS',
                'updated_label' => '—',
                'count'         => 0,
                'items'         => array(),
            );
        }

        /**
         * Empty ISS snapshot for disabled or unavailable live providers.
         *
         * @return array<string,mixed>
         */
        private function get_empty_world_iss_snapshot() {
            return array(
                'source_label'   => 'wheretheiss.at',
                'source_url'     => 'https://www.wheretheiss.at/',
                'updated_label'  => '—',
                'has_position'   => false,
                'lat'            => null,
                'lng'            => null,
                'altitude_label' => '—',
                'velocity_label' => '—',
                'visibility'     => '—',
                'region_label'   => __( 'Live orbit unavailable right now', 'noesia-smart-waiter' ),
            );
        }

        /**
         * Translate small dashboard weather labels.
         *
         * @param string $key  Label key.
         * @param string $lang Language code.
         * @return string
         */
        public function get_weather_ui_label( $key, $lang ) {
            $service = $this->get_weather_service();

            if ( $service ) {
                return $service->get_weather_ui_label( $key, $lang );
            }

            return (string) $key;
        }

        /**
         * Translate Open-Meteo codes using the dashboard-selected language.
         *
         * @param int    $code Weather code.
         * @param string $lang Language code.
         * @return string
         */
        public function get_weather_code_short_for_lang( $code, $lang ) {
            $service = $this->get_weather_service();

            if ( $service ) {
                return $service->get_weather_code_short_for_lang( $code, $lang );
            }

            return '';
        }

        /**
         * Format a localized day name for a forecast timestamp.
         *
         * @param string $date Date string.
         * @param string $lang Language code.
         * @return string
         */
        public function format_weather_day_name( $date, $lang ) {
            $service = $this->get_weather_service();

            if ( $service ) {
                return $service->format_weather_day_name( $date, $lang );
            }

            return '';
        }

        /**
         * Format a localized day/date string for display.
         *
         * @param string $date Date string.
         * @param string $lang Language code.
         * @return string
         */
        public function format_weather_day_date( $date, $lang ) {
            $service = $this->get_weather_service();

            if ( $service ) {
                return $service->format_weather_day_date( $date, $lang );
            }

            return '';
        }

        /**
         * Get a short weather code label (English fallback helper).
         *
         * @param int $code Weather code.
         * @return string
         */
        public function get_weather_code_short( $code ) {
            $service = $this->get_weather_service();

            if ( $service ) {
                return $service->get_weather_code_short( $code );
            }

            return '';
        }

        /**
         * Get a weather icon HTML fragment.
         *
         * @param int $code Weather code.
         * @return string
         */
        public function get_weather_icon( $code ) {
            $service = $this->get_weather_service();

            if ( $service ) {
                return $service->get_weather_icon( $code );
            }

            return '<span class="dashicons dashicons-cloud"></span>';
        }

        /**
         * Return the next notable day, localized with Dashboard Language.
         *
         * @param array<string,mixed> $options Plugin options.
         * @return array<string,string>
         */
        public function get_next_notable_day( $options = array() ) {
            $service = $this->get_weather_service();
            $lang    = $this->get_dashboard_language( $options );

            if ( $service && method_exists( $service, 'get_next_notable_day' ) ) {
                return $service->get_next_notable_day( $lang );
            }

            return array(
                'date_label' => __( '—', 'noesia-smart-waiter' ),
                'title'      => $this->get_dashboard_phrase( 'no_notable_date_available', $lang ),
            );
        }

        /**
         * Build the current weather bundle for the Dashboard.
         *
         * @param array<string,mixed> $options Plugin options.
         * @return array<string,mixed>
         */
        public function get_weather_bundle( $options ) {
            $service = $this->get_weather_service();

            if ( $service ) {
                return $service->get_weather_bundle( $options );
            }

            return array();
        }

        /**
         * Get forecast day cards for the Dashboard.
         *
         * @param array<string,mixed> $weather_bundle Weather bundle.
         * @return array<int,array<string,mixed>>
         */
        public function get_forecast_days( $weather_bundle ) {
            $service = $this->get_weather_service();

            if ( $service ) {
                return $service->get_forecast_days( $weather_bundle );
            }

            return array();
        }

        /**
         * Resolve Google News country code from language.
         *
         * @param string $news_language Language code.
         * @return string
         */
        public function get_news_country( $news_language, $options = array() ) {
            $service = $this->get_news_service();

            if ( $service ) {
                return $service->get_news_country( $news_language, $options );
            }

            return 'US';
        }

        /**
         * Translate dashboard-specific narrative strings using Settings language.
         *
         * @param string $key  Phrase key.
         * @param string $lang Language code.
         * @return string
         */
        public function get_dashboard_phrase( $key, $lang ) {
            $lang = in_array( $lang, $this->get_supported_languages(), true ) ? $lang : 'en';

            $map = array(
                'smart_ai_alert' => array(
                    'en' => 'SMART AI ALERT',
                    'el' => 'ΕΞΥΠΝΗ AI ΕΙΔΟΠΟΙΗΣΗ',
                    'fr' => 'ALERTE IA INTELLIGENTE',
                    'de' => 'SMARTER KI-ALARM',
                    'it' => 'AVVISO AI SMART',
                    'es' => 'ALERTA IA INTELIGENTE',
                ),
                'business_pulse' => array(
                    'en' => 'Business Pulse',
                    'el' => 'Επιχειρησιακός Παλμός',
                    'fr' => 'Pouls de l’Entreprise',
                    'de' => 'Geschäfts-Puls',
                    'it' => 'Business Pulse',
                    'es' => 'Pulso del Negocio',
                ),
                'live_operational_status' => array(
                    'en' => 'Live Operational Status:',
                    'el' => 'Ζωντανή Επιχειρησιακή Κατάσταση:',
                    'fr' => 'Statut Opérationnel en Direct :',
                    'de' => 'Live-Betriebsstatus:',
                    'it' => 'Stato Operativo in Tempo Reale:',
                    'es' => 'Estado Operativo en Vivo:',
                ),
                'business_pulse_intro' => array(
                    'en' => 'Real-time monitoring of system health, activity and local conditions.',
                    'el' => 'Παρακολούθηση σε πραγματικό χρόνο της υγείας του συστήματος, της δραστηριότητας και των τοπικών συνθηκών.',
                    'fr' => 'Surveillance en temps réel de la santé du système, de l’activité et des conditions locales.',
                    'de' => 'Echtzeitüberwachung von Systemzustand, Aktivität und lokalen Bedingungen.',
                    'it' => 'Monitoraggio in tempo reale di stato del sistema, attività e condizioni locali.',
                    'es' => 'Monitoreo en tiempo real del estado del sistema, la actividad y las condiciones locales.',
                ),
                'local_conditions' => array(
                    'en' => 'Local Conditions',
                    'el' => 'Τοπικές Συνθήκες',
                    'fr' => 'Conditions Locales',
                    'de' => 'Lokale Bedingungen',
                    'it' => 'Condizioni Locali',
                    'es' => 'Condiciones Locales',
                ),
                'system_health' => array(
                    'en' => 'System Health',
                    'el' => 'Υγεία Συστήματος',
                    'fr' => 'Santé du Système',
                    'de' => 'Systemzustand',
                    'it' => 'Stato del Sistema',
                    'es' => 'Salud del Sistema',
                ),
                'daily_activity' => array(
                    'en' => 'Daily Activity',
                    'el' => 'Ημερήσια Δραστηριότητα',
                    'fr' => 'Activité Quotidienne',
                    'de' => 'Tagesaktivität',
                    'it' => 'Attività Giornaliera',
                    'es' => 'Actividad Diaria',
                ),
                'live_orders' => array(
                    'en' => 'Live Orders',
                    'el' => 'Ζωντανές Παραγγελίες',
                    'fr' => 'Commandes en Direct',
                    'de' => 'Live-Bestellungen',
                    'it' => 'Ordini in Tempo Reale',
                    'es' => 'Pedidos en Vivo',
                ),
                'steady_service_window' => array(
                    'en' => 'Steady service window',
                    'el' => 'Σταθερό παράθυρο εξυπηρέτησης',
                    'fr' => 'Fenêtre de service stable',
                    'de' => 'Stabiles Servicefenster',
                    'it' => 'Finestra di servizio stabile',
                    'es' => 'Ventana de servicio estable',
                ),
                'steady_service_window_text' => array(
                    'en' => 'Conditions look balanced. Keep your usual service flow and stay ready for standard requests.',
                    'el' => 'Οι συνθήκες φαίνονται ισορροπημένες. Κράτησε τη συνήθη ροή εξυπηρέτησης και μείνε έτοιμος για κανονικά αιτήματα.',
                    'fr' => 'Les conditions semblent équilibrées. Gardez votre flux de service habituel et restez prêt pour les demandes standard.',
                    'de' => 'Die Bedingungen wirken ausgeglichen. Behalte den normalen Servicefluss bei und bleibe bereit für Standardanfragen.',
                    'it' => 'Le condizioni sembrano equilibrate. Mantieni il normale flusso di servizio e resta pronto per le richieste standard.',
                    'es' => 'Las condiciones parecen equilibradas. Mantén tu flujo habitual de servicio y prepárate para solicitudes estándar.',
                ),
                'weather_sync_pending' => array(
                    'en' => 'Weather sync pending',
                    'el' => 'Εκκρεμεί συγχρονισμός καιρού',
                    'fr' => 'Synchronisation météo en attente',
                    'de' => 'Wetter-Synchronisierung ausstehend',
                    'it' => 'Sincronizzazione meteo in attesa',
                    'es' => 'Sincronización del clima pendiente',
                ),
                'weather_sync_pending_text' => array(
                    'en' => 'Weather data is temporarily unavailable. Keep a flexible service plan and rely on live floor activity.',
                    'el' => 'Τα δεδομένα καιρού δεν είναι προσωρινά διαθέσιμα. Κράτησε ευέλικτο πλάνο εξυπηρέτησης και βασίσου στη ζωντανή δραστηριότητα του χώρου.',
                    'fr' => 'Les données météo sont temporairement indisponibles. Gardez un plan de service flexible et appuyez-vous sur l’activité en salle.',
                    'de' => 'Wetterdaten sind vorübergehend nicht verfügbar. Halte den Serviceplan flexibel und stütze dich auf die Live-Aktivität im Raum.',
                    'it' => 'I dati meteo sono temporaneamente non disponibili. Mantieni un piano di servizio flessibile e affidati all’attività reale della sala.',
                    'es' => 'Los datos del clima no están disponibles temporalmente. Mantén un plan de servicio flexible y apóyate en la actividad en vivo del local.',
                ),
                'storm_watch' => array(
                    'en' => 'Storm watch',
                    'el' => 'Επιφυλακή καταιγίδας',
                    'fr' => 'Vigilance tempête',
                    'de' => 'Sturmwarnung',
                    'it' => 'Allerta tempesta',
                    'es' => 'Vigilancia por tormenta',
                ),
                'storm_watch_text' => array(
                    'en' => 'Storm conditions are possible. Prioritize indoor seating, protect outdoor service points, and keep staff movement efficient.',
                    'el' => 'Είναι πιθανές συνθήκες καταιγίδας. Δώσε προτεραιότητα στα εσωτερικά τραπέζια, προστάτευσε τα εξωτερικά σημεία εξυπηρέτησης και κράτησε αποδοτική την κίνηση του προσωπικού.',
                    'fr' => 'Des conditions orageuses sont possibles. Donnez la priorité aux places intérieures, protégez les points de service extérieurs et gardez les déplacements du personnel efficaces.',
                    'de' => 'Sturmbedingungen sind möglich. Priorisiere Innenplätze, schütze Außen-Servicepunkte und halte die Bewegungen des Personals effizient.',
                    'it' => 'Sono possibili condizioni di tempesta. Dai priorità ai posti interni, proteggi i punti di servizio esterni e mantieni efficiente il movimento del personale.',
                    'es' => 'Son posibles condiciones de tormenta. Prioriza los asientos interiores, protege los puntos de servicio exteriores y mantén eficiente el movimiento del personal.',
                ),
                'rain_readiness' => array(
                    'en' => 'Rain readiness',
                    'el' => 'Ετοιμότητα για βροχή',
                    'fr' => 'Préparation à la pluie',
                    'de' => 'Regenbereitschaft',
                    'it' => 'Prontezza per la pioggia',
                    'es' => 'Preparación para lluvia',
                ),
                'rain_readiness_text' => array(
                    'en' => 'Rain is likely. Prepare indoor capacity, keep entrances clear, and reduce reliance on outdoor service.',
                    'el' => 'Η βροχή είναι πιθανή. Προετοίμασε την εσωτερική χωρητικότητα, κράτησε καθαρές τις εισόδους και μείωσε την εξάρτηση από την εξωτερική εξυπηρέτηση.',
                    'fr' => 'La pluie est probable. Préparez la capacité intérieure, gardez les entrées dégagées et réduisez la dépendance au service extérieur.',
                    'de' => 'Regen ist wahrscheinlich. Bereite Innenkapazität vor, halte Eingänge frei und reduziere die Abhängigkeit vom Außenservice.',
                    'it' => 'È probabile pioggia. Prepara la capacità interna, tieni liberi gli ingressi e riduci la dipendenza dal servizio esterno.',
                    'es' => 'Es probable que llueva. Prepara la capacidad interior, mantén despejadas las entradas y reduce la dependencia del servicio exterior.',
                ),
                'heat_alert' => array(
                    'en' => 'Heat alert',
                    'el' => 'Προειδοποίηση ζέστης',
                    'fr' => 'Alerte chaleur',
                    'de' => 'Hitzewarnung',
                    'it' => 'Allerta caldo',
                    'es' => 'Alerta por calor',
                ),
                'heat_alert_text' => array(
                    'en' => 'High heat is expected. Push cold drinks, check shaded areas, and keep staff hydrated during peak service.',
                    'el' => 'Αναμένεται υψηλή ζέστη. Προώθησε κρύα ποτά, έλεγξε τις σκιερές ζώνες και κράτησε το προσωπικό ενυδατωμένο στις ώρες αιχμής.',
                    'fr' => 'Une forte chaleur est attendue. Poussez les boissons fraîches, vérifiez les zones ombragées et gardez le personnel hydraté pendant le pic de service.',
                    'de' => 'Hohe Hitze wird erwartet. Fördere kalte Getränke, prüfe schattige Bereiche und halte das Personal während Stoßzeiten hydriert.',
                    'it' => 'È previsto molto caldo. Spingi le bevande fredde, controlla le aree ombreggiate e mantieni il personale idratato durante i picchi di servizio.',
                    'es' => 'Se espera mucho calor. Impulsa las bebidas frías, revisa las zonas con sombra y mantén hidratado al personal en horas punta.',
                ),
                'cold_shift' => array(
                    'en' => 'Cold shift',
                    'el' => 'Ψυχρή μεταβολή',
                    'fr' => 'Bascule vers le froid',
                    'de' => 'Kälteeinbruch',
                    'it' => 'Calo termico',
                    'es' => 'Bajada de temperatura',
                ),
                'cold_shift_text' => array(
                    'en' => 'Cool conditions are expected. Favour indoor comfort, warm drinks, and tighter table turnover planning.',
                    'el' => 'Αναμένονται δροσερές συνθήκες. Δώσε προτεραιότητα στην άνεση εσωτερικού χώρου, στα ζεστά ροφήματα και στον πιο σφιχτό προγραμματισμό εναλλαγής τραπεζιών.',
                    'fr' => 'Des conditions fraîches sont attendues. Favorisez le confort intérieur, les boissons chaudes et une rotation des tables plus serrée.',
                    'de' => 'Kühle Bedingungen werden erwartet. Bevorzuge Innenkomfort, warme Getränke und eine engere Planung der Tischrotation.',
                    'it' => 'Sono previste condizioni fresche. Favorisci il comfort interno, le bevande calde e una pianificazione più stretta del turnover tavoli.',
                    'es' => 'Se esperan condiciones frescas. Favorece la comodidad interior, las bebidas calientes y una planificación más ajustada de la rotación de mesas.',
                ),
                'peak_traffic' => array(
                    'en' => 'Peak traffic',
                    'el' => 'Αιχμή κίνησης',
                    'fr' => 'Pic d’activité',
                    'de' => 'Stoßverkehr',
                    'it' => 'Picco di traffico',
                    'es' => 'Pico de actividad',
                ),
                'peak_traffic_text' => array(
                    'en' => 'Live demand is elevated. Keep response times tight, prioritize active tables, and avoid staff bottlenecks.',
                    'el' => 'Η ζωντανή ζήτηση είναι αυξημένη. Κράτησε γρήγορους χρόνους απόκρισης, δώσε προτεραιότητα στα ενεργά τραπέζια και απόφυγε σημεία συμφόρησης του προσωπικού.',
                    'fr' => 'La demande en direct est élevée. Gardez des temps de réponse serrés, priorisez les tables actives et évitez les goulots d’étranglement du personnel.',
                    'de' => 'Die Live-Nachfrage ist erhöht. Halte Reaktionszeiten knapp, priorisiere aktive Tische und vermeide Personalengpässe.',
                    'it' => 'La domanda live è elevata. Mantieni tempi di risposta rapidi, dai priorità ai tavoli attivi ed evita colli di bottiglia del personale.',
                    'es' => 'La demanda en vivo está elevada. Mantén tiempos de respuesta cortos, prioriza las mesas activas y evita cuellos de botella del personal.',
                ),
                'reserved_flow' => array(
                    'en' => 'Reserved flow',
                    'el' => 'Ροή κρατήσεων',
                    'fr' => 'Flux des réservations',
                    'de' => 'Reservierungsfluss',
                    'it' => 'Flusso prenotazioni',
                    'es' => 'Flujo de reservas',
                ),
                'reserved_flow_text' => array(
                    'en' => 'You have multiple bookings today. Keep reserved tables organized and align your service pace with scheduled arrivals.',
                    'el' => 'Έχεις πολλαπλές κρατήσεις σήμερα. Κράτησε οργανωμένα τα κρατημένα τραπέζια και ευθυγράμμισε τον ρυθμό εξυπηρέτησης με τις προγραμματισμένες αφίξεις.',
                    'fr' => 'Vous avez plusieurs réservations aujourd’hui. Gardez les tables réservées organisées et alignez le rythme du service avec les arrivées prévues.',
                    'de' => 'Du hast heute mehrere Buchungen. Halte reservierte Tische organisiert und passe dein Servicetempo an geplante Ankünfte an.',
                    'it' => 'Hai più prenotazioni oggi. Mantieni organizzati i tavoli riservati e allinea il ritmo del servizio con gli arrivi programmati.',
                    'es' => 'Tienes varias reservas hoy. Mantén organizadas las mesas reservadas y alinea el ritmo del servicio con las llegadas programadas.',
                ),
                'online_weather_pending' => array(
                    'en' => '🟡 Online / Weather sync pending',
                    'el' => '🟡 Online / Εκκρεμεί συγχρονισμός καιρού',
                    'fr' => '🟡 En ligne / Synchronisation météo en attente',
                    'de' => '🟡 Online / Wetter-Synchronisierung ausstehend',
                    'it' => '🟡 Online / Sincronizzazione meteo in attesa',
                    'es' => '🟡 En línea / Sincronización del clima pendiente',
                ),
                'online_ready' => array(
                    'en' => '🟢 Online & Ready',
                    'el' => '🟢 Online & Έτοιμο',
                    'fr' => '🟢 En ligne et prêt',
                    'de' => '🟢 Online & bereit',
                    'it' => '🟢 Online e pronto',
                    'es' => '🟢 En línea y listo',
                ),
                'activity_peak_busy' => array(
                    'en' => '🔥 Peak / Busy',
                    'el' => '🔥 Αιχμή / Έντονη κίνηση',
                    'fr' => '🔥 Pic / Fort trafic',
                    'de' => '🔥 Spitze / Hohe Auslastung',
                    'it' => '🔥 Picco / Intenso',
                    'es' => '🔥 Pico / Mucho movimiento',
                ),
                'activity_active_serving' => array(
                    'en' => '⚡ Active / Serving',
                    'el' => '⚡ Ενεργό / Σε εξυπηρέτηση',
                    'fr' => '⚡ Actif / En service',
                    'de' => '⚡ Aktiv / Im Service',
                    'it' => '⚡ Attivo / In servizio',
                    'es' => '⚡ Activo / Atendiendo',
                ),
                'activity_lunch_ready' => array(
                    'en' => '🕒 Lunch Window / Ready',
                    'el' => '🕒 Μεσημεριανό Παράθυρο / Έτοιμο',
                    'fr' => '🕒 Créneau déjeuner / Prêt',
                    'de' => '🕒 Mittagsfenster / Bereit',
                    'it' => '🕒 Fascia pranzo / Pronto',
                    'es' => '🕒 Franja de almuerzo / Listo',
                ),
                'activity_dinner_ready' => array(
                    'en' => '🌙 Dinner Window / Ready',
                    'el' => '🌙 Βραδινό Παράθυρο / Έτοιμο',
                    'fr' => '🌙 Créneau dîner / Prêt',
                    'de' => '🌙 Abendfenster / Bereit',
                    'it' => '🌙 Fascia cena / Pronto',
                    'es' => '🌙 Franja de cena / Listo',
                ),
                'activity_quiet_open' => array(
                    'en' => '☕ Quiet / Open',
                    'el' => '☕ Ήσυχο / Ανοιχτό',
                    'fr' => '☕ Calme / Ouvert',
                    'de' => '☕ Ruhig / Offen',
                    'it' => '☕ Calmo / Aperto',
                    'es' => '☕ Tranquilo / Abierto',
                ),
                'global_pulse_kicker' => array(
                    'en' => 'GLOBAL PULSE',
                    'el' => 'GLOBAL PULSE',
                    'fr' => 'GLOBAL PULSE',
                    'de' => 'GLOBAL PULSE',
                    'it' => 'GLOBAL PULSE',
                    'es' => 'GLOBAL PULSE',
                ),
                'global_pulse_heading' => array(
                    'en' => 'Global Pulse',
                    'el' => 'Παγκόσμιος Παλμός',
                    'fr' => 'Pulse Global',
                    'de' => 'Global Pulse',
                    'it' => 'Pulse Globale',
                    'es' => 'Pulso Global',
                ),
                'global_pulse_next_label' => array(
                    'en' => 'Next notable date:',
                    'el' => 'Επόμενη σημαντική ημερομηνία:',
                    'fr' => 'Prochaine date notable :',
                    'de' => 'Nächstes wichtiges Datum:',
                    'it' => 'Prossima data importante:',
                    'es' => 'Próxima fecha destacada:',
                ),
                'global_pulse_intro' => array(
                    'en' => 'Planet observances.',
                    'el' => 'Παγκόσμιες observances.',
                    'fr' => 'Journées mondiales.',
                    'de' => 'Weltweite Gedenktage.',
                    'it' => 'Ricorrenze mondiali.',
                    'es' => 'Efemérides mundiales.',
                ),
                'no_notable_date_available' => array(
                    'en' => 'No notable date available.',
                    'el' => 'Δεν υπάρχει διαθέσιμη σημαντική ημερομηνία.',
                    'fr' => 'Aucune date notable disponible.',
                    'de' => 'Kein wichtiges Datum verfügbar.',
                    'it' => 'Nessuna data importante disponibile.',
                    'es' => 'No hay fecha destacada disponible.',
                ),
            );

            return isset( $map[ $key ][ $lang ] ) ? $map[ $key ][ $lang ] : ( isset( $map[ $key ]['en'] ) ? $map[ $key ]['en'] : (string) $key );
        }
  
       
        /**
         * Format one forecast range label for the Business Pulse card.
         *
         * @param array<string,mixed> $day         Forecast day row.
         * @param string              $units_label Temperature unit label.
         * @return string
         */
        private function get_business_pulse_range_label( $day, $units_label ) {
            $day         = is_array( $day ) ? $day : array();
            $units_label = is_string( $units_label ) ? $units_label : '';

            $min = isset( $day['min'] ) && null !== $day['min'] ? (int) $day['min'] : null;
            $max = isset( $day['max'] ) && null !== $day['max'] ? (int) $day['max'] : null;

            if ( null !== $min && null !== $max ) {
                return (string) $min . $units_label . '–' . (string) $max . $units_label;
            }

            if ( null !== $max ) {
                return (string) $max . $units_label;
            }

            if ( null !== $min ) {
                return (string) $min . $units_label;
            }

            return '—';
        }

        /**
         * Build a serious operations-focused Business Pulse forecast.
         *
         * Uses real forecast values from the weather bundle:
         * weather code, rain probability, min/max temperature, wind, gusts.
         *
         * @param array<string,mixed> $weather_bundle      Weather bundle.
         * @param string              $lang                Dashboard language.
         * @param int                 $active_requests     Active requests.
         * @param int                 $today_appointments  Today's appointments.
         * @return array<string,string>
         */
        private function build_weather_business_pulse( $weather_bundle, $lang, $active_requests, $today_appointments ) {
            $weather_bundle     = is_array( $weather_bundle ) ? $weather_bundle : array();
            $lang               = in_array( $lang, array( 'en', 'el', 'fr', 'de', 'it', 'es' ), true ) ? $lang : 'en';
            $active_requests    = absint( $active_requests );
            $today_appointments = absint( $today_appointments );

            $fallback = array(
                'en' => array(
                    'title' => 'Weather sync pending',
                    'text'  => 'Forecast data is temporarily unavailable. Keep outdoor operations flexible until the next weather refresh.',
                ),
                'el' => array(
                    'title' => 'Αναμονή συγχρονισμού καιρού',
                    'text'  => 'Τα δεδομένα πρόγνωσης δεν είναι προσωρινά διαθέσιμα. Κράτησε ευέλικτη τη λειτουργία των εξωτερικών χώρων μέχρι την επόμενη ανανέωση.',
                ),
                'fr' => array(
                    'title' => 'Synchronisation météo en attente',
                    'text'  => 'Les données météo sont temporairement indisponibles. Gardez une organisation flexible pour les espaces extérieurs jusqu’au prochain rafraîchissement.',
                ),
                'de' => array(
                    'title' => 'Wettersynchronisierung ausstehend',
                    'text'  => 'Prognosedaten sind vorübergehend nicht verfügbar. Halte den Außenbetrieb bis zur nächsten Aktualisierung flexibel.',
                ),
                'it' => array(
                    'title' => 'Sincronizzazione meteo in attesa',
                    'text'  => 'I dati meteo non sono temporaneamente disponibili. Mantieni flessibili le operazioni esterne fino al prossimo aggiornamento.',
                ),
                'es' => array(
                    'title' => 'Sincronización meteorológica pendiente',
                    'text'  => 'Los datos del pronóstico no están disponibles temporalmente. Mantén flexibles las operaciones exteriores hasta la próxima actualización.',
                ),
            );

            if ( ! empty( $weather_bundle['error'] ) ) {
                return isset( $fallback[ $lang ] ) ? $fallback[ $lang ] : $fallback['en'];
            }

            $days        = isset( $weather_bundle['days'] ) && is_array( $weather_bundle['days'] ) ? $weather_bundle['days'] : array();
            $units_label = isset( $weather_bundle['units_label'] ) ? (string) $weather_bundle['units_label'] : '';

            $today    = ! empty( $days[0] ) && is_array( $days[0] ) ? $days[0] : array();
            $tomorrow = ! empty( $days[1] ) && is_array( $days[1] ) ? $days[1] : $today;

            $today_code = isset( $today['code'] ) ? absint( $today['code'] ) : 0;
            $today_pp   = isset( $today['pp'] ) ? absint( $today['pp'] ) : 0;
            $today_min  = isset( $today['min'] ) && null !== $today['min'] ? (int) $today['min'] : null;
            $today_max  = isset( $today['max'] ) && null !== $today['max'] ? (int) $today['max'] : null;
            $today_wind = isset( $today['wind'] ) && null !== $today['wind'] ? (int) $today['wind'] : null;
            $today_gust = isset( $today['gust'] ) && null !== $today['gust'] ? (int) $today['gust'] : null;

            $tomorrow_code = isset( $tomorrow['code'] ) ? absint( $tomorrow['code'] ) : $today_code;
            $tomorrow_pp   = isset( $tomorrow['pp'] ) ? absint( $tomorrow['pp'] ) : 0;
            $tomorrow_wind = isset( $tomorrow['wind'] ) && null !== $tomorrow['wind'] ? (int) $tomorrow['wind'] : null;
            $tomorrow_gust = isset( $tomorrow['gust'] ) && null !== $tomorrow['gust'] ? (int) $tomorrow['gust'] : null;

            $today_condition    = $this->get_weather_code_short_for_lang( $today_code, $lang );
            $tomorrow_condition = $this->get_weather_code_short_for_lang( $tomorrow_code, $lang );
            $today_range        = $this->get_business_pulse_range_label( $today, $units_label );
            $tomorrow_range     = $this->get_business_pulse_range_label( $tomorrow, $units_label );

            $copy = array(
                'en' => array(
                    'storm_title' => 'Severe weather warning',
                    'storm_text'  => 'Today shows %1$s (%2$s) with rain risk at %3$d%%. Dangerous weather is possible. Suspend or sharply reduce outdoor seating, secure umbrellas, covers, menus, heaters, signage, and any loose terrace equipment, and keep staff ready to move guests indoors immediately. Tomorrow shifts to %4$s (%5$s).',
                    'wind_title'  => 'Strong wind warning',
                    'wind_text'   => 'Today shows %1$s (%2$s) with wind up to %3$dkm/h and gusts up to %4$dkm/h. Collect or secure umbrellas, light furniture, menus, heaters, and anything exposed outside. Limit use of vulnerable outdoor tables and monitor entrances continuously. Tomorrow shifts to %5$s (%6$s).',
                    'snow_title'  => 'Snow / ice risk',
                    'snow_text'   => 'Today shows %1$s (%2$s). Protect entrances and steps, check for slippery surfaces, and do not rely on outdoor seating unless the area is fully protected. Keep walkways clear and prepare for slower arrivals. Tomorrow shifts to %3$s (%4$s).',
                    'fog_title'   => 'Low visibility conditions',
                    'fog_text'    => 'Today shows %1$s (%2$s). Expect reduced visibility and slower arrivals. Keep entrances clearly visible, improve host coordination, and avoid clutter around guest access points. Tomorrow shifts to %3$s (%4$s).',
                    'rain_title'  => 'Rain readiness',
                    'rain_text'   => 'Today shows %1$s (%2$s) with rain risk at %3$d%%. Protect anything exposed outdoors, reduce dependence on terrace seating, and prepare indoor tables earlier than usual. Check drainage, entry mats, and any areas that become slippery when wet. Tomorrow shifts to %4$s (%5$s).',
                    'heat_title'  => 'Heat alert',
                    'heat_text'   => 'Today reaches %1$s with %2$s conditions. Treat heat exposure seriously: keep cold water ready, increase shade coverage, avoid leaving guests or staff in direct sun for long periods, and verify cooling before peak hours. Tomorrow shifts to %3$s (%4$s).',
                    'cold_title'  => 'Cold / frost risk',
                    'cold_text'   => 'Today drops to %1$s with %2$s conditions. Prioritize indoor comfort, inspect exterior surfaces for cold-related slipperiness, and rely on outdoor seating only in protected heated areas. Tomorrow shifts to %3$s (%4$s).',
                    'clear_title' => 'Stable outdoor window',
                    'clear_text'  => 'Today shows %1$s (%2$s) with rain risk at %3$d%%. Outdoor service looks operationally safe. Keep normal checks on terrace setup, shade, and guest comfort. Tomorrow shifts to %4$s (%5$s).',
                    'busy_suffix' => ' Demand is already elevated, so keep indoor tables and runners ready for a fast shift if conditions worsen.',
                    'book_suffix' => ' Reservations are active, so align table readiness early and avoid overcommitting outdoor capacity.',
                ),
                'el' => array(
                    'storm_title' => 'Προειδοποίηση επικίνδυνου καιρού',
                    'storm_text'  => 'Για σήμερα προβλέπεται %1$s (%2$s) με πιθανότητα βροχής %3$d%%. Υπάρχει κίνδυνος επικίνδυνων φαινομένων. Σταμάτησε ή περιόρισε έντονα τα εξωτερικά τραπέζια, ασφάλισε ομπρέλες, καλύμματα, μενού, θερμάστρες, πινακίδες και οτιδήποτε χαλαρό υπάρχει έξω, και κράτησε το προσωπικό έτοιμο για άμεση μεταφορά πελατών σε εσωτερικό χώρο. Αύριο η πρόγνωση δείχνει %4$s (%5$s).',
                    'wind_title'  => 'Προειδοποίηση ισχυρών ανέμων',
                    'wind_text'   => 'Για σήμερα προβλέπεται %1$s (%2$s) με ανέμους έως %3$dkm/h και ριπές έως %4$dkm/h. Μάζεψε ή ασφάλισε ομπρέλες, ελαφριά έπιπλα, μενού, θερμάστρες και οτιδήποτε είναι εκτεθειμένο έξω. Περιόρισε τη χρήση ευάλωτων εξωτερικών τραπεζιών και παρακολούθησε συνεχώς εισόδους και εκτεθειμένα σημεία. Αύριο η πρόγνωση δείχνει %5$s (%6$s).',
                    'snow_title'  => 'Κίνδυνος χιονιού / πάγου',
                    'snow_text'   => 'Για σήμερα προβλέπεται %1$s (%2$s). Προστάτευσε εισόδους και σκαλοπάτια, έλεγξε για ολισθηρές επιφάνειες και μην βασιστείς στη λειτουργία εξωτερικών τραπεζιών εκτός αν ο χώρος είναι πλήρως προστατευμένος. Κράτησε καθαρούς τους διαδρόμους και υπολόγισε πιο αργές αφίξεις. Αύριο η πρόγνωση δείχνει %3$s (%4$s).',
                    'fog_title'   => 'Συνθήκες χαμηλής ορατότητας',
                    'fog_text'    => 'Για σήμερα προβλέπεται %1$s (%2$s). Περίμενε μειωμένη ορατότητα και πιο αργές αφίξεις. Κράτησε καθαρές και εμφανείς τις εισόδους, ενίσχυσε τον συντονισμό υποδοχής και απέφυγε εμπόδια στα σημεία πρόσβασης των πελατών. Αύριο η πρόγνωση δείχνει %3$s (%4$s).',
                    'rain_title'  => 'Ετοιμότητα για βροχή',
                    'rain_text'   => 'Για σήμερα προβλέπεται %1$s (%2$s) με πιθανότητα βροχής %3$d%%. Προστάτευσε ό,τι είναι εκτεθειμένο έξω, μείωσε την εξάρτηση από τα εξωτερικά τραπέζια και ετοίμασε νωρίτερα τους εσωτερικούς χώρους. Έλεγξε απορροές, πατάκια εισόδου και σημεία που γίνονται ολισθηρά όταν βρέχονται. Αύριο η πρόγνωση δείχνει %4$s (%5$s).',
                    'heat_title'  => 'Προειδοποίηση ζέστης',
                    'heat_text'   => 'Για σήμερα η θερμοκρασία φτάνει τους %1$s με συνθήκες %2$s. Αντιμετώπισε σοβαρά τη θερμική καταπόνηση: κράτησε κρύο νερό διαθέσιμο, αύξησε τη σκίαση, μην αφήνεις πελάτες ή προσωπικό για πολλή ώρα σε άμεσο ήλιο και έλεγξε την ψύξη πριν τις ώρες αιχμής. Αύριο η πρόγνωση δείχνει %3$s (%4$s).',
                    'cold_title'  => 'Κίνδυνος κρύου / παγετού',
                    'cold_text'   => 'Για σήμερα η θερμοκρασία πέφτει έως %1$s με συνθήκες %2$s. Δώσε προτεραιότητα στην άνεση εσωτερικού χώρου, έλεγξε εξωτερικές επιφάνειες για ολισθηρότητα λόγω κρύου και κράτησε εξωτερική εξυπηρέτηση μόνο σε προστατευμένα και θερμαινόμενα σημεία. Αύριο η πρόγνωση δείχνει %3$s (%4$s).',
                    'clear_title' => 'Σταθερό παράθυρο για εξωτερική λειτουργία',
                    'clear_text'  => 'Για σήμερα προβλέπεται %1$s (%2$s) με πιθανότητα βροχής %3$d%%. Η εξωτερική λειτουργία φαίνεται επιχειρησιακά ασφαλής. Κράτησε τους συνηθισμένους ελέγχους σε setup, σκίαση και άνεση πελατών. Αύριο η πρόγνωση δείχνει %4$s (%5$s).',
                    'busy_suffix' => ' Η ζήτηση είναι ήδη αυξημένη, οπότε κράτησε έτοιμους εσωτερικούς χώρους και runners για γρήγορη προσαρμογή αν ο καιρός χειροτερέψει.',
                    'book_suffix' => ' Οι κρατήσεις είναι ενεργές, οπότε ετοίμασε έγκαιρα τα τραπέζια και μην υπερδεσμεύσεις την εξωτερική χωρητικότητα.',
                ),
                'fr' => array(
                    'storm_title' => 'Alerte météo dangereuse',
                    'storm_text'  => 'Aujourd’hui : %1$s (%2$s) avec un risque de pluie de %3$d%%. Des phénomènes dangereux sont possibles. Suspendez ou réduisez fortement les places extérieures, sécurisez parasols, protections, menus, chauffages, signalétique et tout équipement mobile, et gardez l’équipe prête à faire entrer les clients immédiatement. Demain : %4$s (%5$s).',
                    'wind_title'  => 'Alerte vents forts',
                    'wind_text'   => 'Aujourd’hui : %1$s (%2$s) avec des vents jusqu’à %3$dkm/h et des rafales jusqu’à %4$dkm/h. Rentrez ou sécurisez parasols, mobilier léger, menus, chauffages et tout élément exposé dehors. Limitez l’usage des tables extérieures vulnérables et surveillez en continu les accès et zones exposées. Demain : %5$s (%6$s).',
                    'snow_title'  => 'Risque de neige / verglas',
                    'snow_text'   => 'Aujourd’hui : %1$s (%2$s). Protégez entrées et marches, surveillez les surfaces glissantes et n’appuyez pas le service sur les places extérieures sauf si la zone est totalement protégée. Gardez les passages dégagés et anticipez des arrivées plus lentes. Demain : %3$s (%4$s).',
                    'fog_title'   => 'Conditions de faible visibilité',
                    'fog_text'    => 'Aujourd’hui : %1$s (%2$s). Attendez-vous à une visibilité réduite et à des arrivées plus lentes. Gardez les entrées bien visibles, renforcez la coordination d’accueil et évitez l’encombrement des accès clients. Demain : %3$s (%4$s).',
                    'rain_title'  => 'Préparation pluie',
                    'rain_text'   => 'Aujourd’hui : %1$s (%2$s) avec un risque de pluie de %3$d%%. Protégez ce qui reste exposé dehors, réduisez la dépendance à la terrasse et préparez les tables intérieures plus tôt que d’habitude. Vérifiez évacuation d’eau, tapis d’entrée et zones glissantes. Demain : %4$s (%5$s).',
                    'heat_title'  => 'Alerte chaleur',
                    'heat_text'   => 'Aujourd’hui la température atteint %1$s avec %2$s. Prenez l’exposition à la chaleur au sérieux : prévoyez eau fraîche, plus d’ombre, évitez de laisser clients ou personnel en plein soleil trop longtemps et vérifiez le refroidissement avant les heures de pointe. Demain : %3$s (%4$s).',
                    'cold_title'  => 'Risque de froid / gel',
                    'cold_text'   => 'Aujourd’hui la température descend à %1$s avec %2$s. Priorisez le confort intérieur, vérifiez les surfaces extérieures pouvant devenir glissantes et limitez le service extérieur aux zones protégées et chauffées. Demain : %3$s (%4$s).',
                    'clear_title' => 'Fenêtre stable pour l’extérieur',
                    'clear_text'  => 'Aujourd’hui : %1$s (%2$s) avec un risque de pluie de %3$d%%. Le service extérieur semble opérationnellement sûr. Conservez les contrôles habituels sur installation, ombrage et confort client. Demain : %4$s (%5$s).',
                    'busy_suffix' => ' La demande est déjà élevée, gardez donc les espaces intérieurs et les runners prêts pour une bascule rapide si la météo se dégrade.',
                    'book_suffix' => ' Les réservations sont actives, alignez donc tôt la préparation des tables et évitez de surengager la capacité extérieure.',
                ),
                'de' => array(
                    'storm_title' => 'Warnung vor gefährlichem Wetter',
                    'storm_text'  => 'Heute: %1$s (%2$s) bei %3$d%% Regenrisiko. Gefährliche Wetterlagen sind möglich. Stoppe oder reduziere Außenplätze stark, sichere Schirme, Abdeckungen, Menüs, Heizgeräte, Beschilderung und lose Terrassenelemente, und halte das Team bereit, Gäste sofort nach innen zu verlagern. Morgen: %4$s (%5$s).',
                    'wind_title'  => 'Warnung vor starkem Wind',
                    'wind_text'   => 'Heute: %1$s (%2$s) mit Wind bis %3$dkm/h und Böen bis %4$dkm/h. Räume Schirme, leichte Möbel, Menüs, Heizgeräte und alles frei Exponierte draußen weg oder sichere es. Begrenze anfällige Außenplätze und überwache Eingänge sowie exponierte Bereiche fortlaufend. Morgen: %5$s (%6$s).',
                    'snow_title'  => 'Schnee- / Glätterisiko',
                    'snow_text'   => 'Heute: %1$s (%2$s). Schütze Eingänge und Stufen, prüfe rutschige Flächen und verlasse dich nicht auf Außenplätze, außer der Bereich ist vollständig geschützt. Halte Wege frei und rechne mit langsameren Ankünften. Morgen: %3$s (%4$s).',
                    'fog_title'   => 'Bedingungen mit geringer Sicht',
                    'fog_text'    => 'Heute: %1$s (%2$s). Rechne mit eingeschränkter Sicht und langsameren Ankünften. Halte Eingänge gut sichtbar, verbessere die Host-Koordination und vermeide Hindernisse an Gästezugängen. Morgen: %3$s (%4$s).',
                    'rain_title'  => 'Regenbereitschaft',
                    'rain_text'   => 'Heute: %1$s (%2$s) bei %3$d%% Regenrisiko. Schütze alles Exponierte draußen, reduziere die Abhängigkeit von Außenplätzen und bereite Innenplätze früher vor. Prüfe Entwässerung, Eingangsmatten und Bereiche, die bei Nässe rutschig werden. Morgen: %4$s (%5$s).',
                    'heat_title'  => 'Hitzealarm',
                    'heat_text'   => 'Heute steigt die Temperatur auf %1$s bei %2$s. Nimm Hitzebelastung ernst: halte kaltes Wasser bereit, erhöhe die Beschattung, lasse Gäste oder Personal nicht lange in direkter Sonne und prüfe die Kühlung vor den Spitzenzeiten. Morgen: %3$s (%4$s).',
                    'cold_title'  => 'Kälte- / Frostrisiko',
                    'cold_text'   => 'Heute fällt die Temperatur auf %1$s bei %2$s. Priorisiere Innenkomfort, prüfe Außenflächen auf Kälte-bedingte Rutschgefahr und nutze Außenservice nur in geschützten, beheizten Bereichen. Morgen: %3$s (%4$s).',
                    'clear_title' => 'Stabiles Außenfenster',
                    'clear_text'  => 'Heute: %1$s (%2$s) bei %3$d%% Regenrisiko. Außenservice wirkt betrieblich sicher. Führe die üblichen Kontrollen für Aufbau, Beschattung und Gästekomfort weiter durch. Morgen: %4$s (%5$s).',
                    'busy_suffix' => ' Die Nachfrage ist bereits erhöht, halte daher Innenflächen und Runner für einen schnellen Wechsel bereit, falls sich das Wetter verschlechtert.',
                    'book_suffix' => ' Reservierungen sind aktiv, richte daher die Tischbereitschaft früh aus und überbuche die Außenkapazität nicht.',
                ),
                'it' => array(
                    'storm_title' => 'Allerta meteo pericolosa',
                    'storm_text'  => 'Oggi: %1$s (%2$s) con rischio pioggia al %3$d%%. Sono possibili fenomeni pericolosi. Sospendi o riduci fortemente i posti esterni, metti in sicurezza ombrelloni, coperture, menu, stufe, segnaletica e tutto ciò che può spostarsi, e tieni il personale pronto a trasferire subito i clienti all’interno. Domani: %4$s (%5$s).',
                    'wind_title'  => 'Allerta vento forte',
                    'wind_text'   => 'Oggi: %1$s (%2$s) con vento fino a %3$dkm/h e raffiche fino a %4$dkm/h. Raccogli o metti in sicurezza ombrelloni, arredi leggeri, menu, stufe e tutto ciò che è esposto all’esterno. Limita i tavoli esterni più vulnerabili e controlla continuamente accessi e aree esposte. Domani: %5$s (%6$s).',
                    'snow_title'  => 'Rischio neve / ghiaccio',
                    'snow_text'   => 'Oggi: %1$s (%2$s). Proteggi ingressi e gradini, controlla superfici scivolose e non basare il servizio sui tavoli esterni salvo che l’area sia completamente protetta. Mantieni liberi i passaggi e prevedi arrivi più lenti. Domani: %3$s (%4$s).',
                    'fog_title'   => 'Condizioni di bassa visibilità',
                    'fog_text'    => 'Oggi: %1$s (%2$s). Aspettati visibilità ridotta e arrivi più lenti. Mantieni ben visibili gli ingressi, rafforza il coordinamento all’accoglienza ed evita ostacoli negli accessi dei clienti. Domani: %3$s (%4$s).',
                    'rain_title'  => 'Preparazione pioggia',
                    'rain_text'   => 'Oggi: %1$s (%2$s) con rischio pioggia al %3$d%%. Proteggi ciò che resta esposto all’esterno, riduci la dipendenza dai tavoli fuori e prepara prima del solito gli spazi interni. Controlla drenaggio, tappeti d’ingresso e punti che diventano scivolosi con il bagnato. Domani: %4$s (%5$s).',
                    'heat_title'  => 'Allerta caldo',
                    'heat_text'   => 'Oggi la temperatura arriva a %1$s con condizioni %2$s. Tratta seriamente lo stress da calore: tieni pronta acqua fresca, aumenta le zone d’ombra, evita che clienti o personale restino troppo a lungo sotto il sole diretto e verifica il raffrescamento prima delle ore di punta. Domani: %3$s (%4$s).',
                    'cold_title'  => 'Rischio freddo / gelo',
                    'cold_text'   => 'Oggi la temperatura scende a %1$s con condizioni %2$s. Dai priorità al comfort interno, controlla le superfici esterne per rischio di scivolamento dovuto al freddo e limita il servizio esterno alle aree protette e riscaldate. Domani: %3$s (%4$s).',
                    'clear_title' => 'Finestra stabile per l’esterno',
                    'clear_text'  => 'Oggi: %1$s (%2$s) con rischio pioggia al %3$d%%. Il servizio esterno sembra operativo in sicurezza. Mantieni i controlli abituali su allestimento, ombra e comfort degli ospiti. Domani: %4$s (%5$s).',
                    'busy_suffix' => ' La domanda è già elevata, quindi tieni pronti spazi interni e runner per una rapida transizione se il meteo peggiora.',
                    'book_suffix' => ' Le prenotazioni sono attive, quindi prepara presto i tavoli ed evita di sovra-impegnare la capacità esterna.',
                ),
                'es' => array(
                    'storm_title' => 'Alerta por tiempo peligroso',
                    'storm_text'  => 'Hoy: %1$s (%2$s) con riesgo de lluvia del %3$d%%. Pueden darse fenómenos peligrosos. Suspende o reduce fuertemente las mesas exteriores, asegura sombrillas, cubiertas, menús, calefactores, señalización y cualquier elemento suelto, y mantén al personal listo para mover a los clientes al interior de inmediato. Mañana: %4$s (%5$s).',
                    'wind_title'  => 'Alerta de viento fuerte',
                    'wind_text'   => 'Hoy: %1$s (%2$s) con viento de hasta %3$dkm/h y ráfagas de hasta %4$dkm/h. Recoge o asegura sombrillas, mobiliario ligero, menús, calefactores y cualquier elemento expuesto fuera. Limita las mesas exteriores más vulnerables y vigila continuamente accesos y zonas expuestas. Mañana: %5$s (%6$s).',
                    'snow_title'  => 'Riesgo de nieve / hielo',
                    'snow_text'   => 'Hoy: %1$s (%2$s). Protege entradas y escalones, revisa superficies resbaladizas y no apoyes el servicio en mesas exteriores salvo que la zona esté totalmente protegida. Mantén despejados los accesos y prevé llegadas más lentas. Mañana: %3$s (%4$s).',
                    'fog_title'   => 'Condiciones de baja visibilidad',
                    'fog_text'    => 'Hoy: %1$s (%2$s). Espera menor visibilidad y llegadas más lentas. Mantén bien visibles las entradas, refuerza la coordinación en recepción y evita obstáculos en los accesos de clientes. Mañana: %3$s (%4$s).',
                    'rain_title'  => 'Preparación para lluvia',
                    'rain_text'   => 'Hoy: %1$s (%2$s) con riesgo de lluvia del %3$d%%. Protege todo lo expuesto fuera, reduce la dependencia de la terraza y prepara antes de lo habitual las mesas interiores. Revisa drenaje, alfombrillas de entrada y zonas que se vuelven resbaladizas con agua. Mañana: %4$s (%5$s).',
                    'heat_title'  => 'Alerta de calor',
                    'heat_text'   => 'Hoy la temperatura alcanza %1$s con condiciones %2$s. Toma en serio la exposición al calor: mantén agua fría lista, aumenta la sombra, evita dejar a clientes o personal demasiado tiempo bajo sol directo y revisa la refrigeración antes de las horas punta. Mañana: %3$s (%4$s).',
                    'cold_title'  => 'Riesgo de frío / helada',
                    'cold_text'   => 'Hoy la temperatura baja hasta %1$s con condiciones %2$s. Prioriza la comodidad interior, revisa superficies exteriores por posible deslizamiento debido al frío y deja el servicio exterior solo para zonas protegidas y calefaccionadas. Mañana: %3$s (%4$s).',
                    'clear_title' => 'Ventana estable para exterior',
                    'clear_text'  => 'Hoy: %1$s (%2$s) con riesgo de lluvia del %3$d%%. El servicio exterior parece seguro a nivel operativo. Mantén los controles habituales sobre montaje, sombra y comodidad del cliente. Mañana: %4$s (%5$s).',
                    'busy_suffix' => ' La demanda ya es elevada, así que mantén listos espacios interiores y runners para un cambio rápido si el tiempo empeora.',
                    'book_suffix' => ' Las reservas están activas, así que prepara pronto las mesas y evita sobrecomprometer la capacidad exterior.',
                ),
            );

            $t = isset( $copy[ $lang ] ) ? $copy[ $lang ] : $copy['en'];

            $suffix = '';
            if ( $active_requests >= 5 ) {
                $suffix = $t['busy_suffix'];
            } elseif ( $today_appointments >= 4 ) {
                $suffix = $t['book_suffix'];
            }

            $has_storm = in_array( $today_code, array( 95, 96, 99 ), true ) || in_array( $tomorrow_code, array( 95, 96, 99 ), true );
            $has_snow  = in_array( $today_code, array( 71, 73, 75, 77, 85, 86 ), true ) || in_array( $tomorrow_code, array( 71, 73, 75, 77, 85, 86 ), true );
            $has_fog   = in_array( $today_code, array( 45, 48 ), true );
            $has_rain  = $today_pp >= 55 || $tomorrow_pp >= 60 || in_array( $today_code, array( 51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82 ), true );
            $has_wind  = ( null !== $today_gust && $today_gust >= 45 ) || ( null !== $today_wind && $today_wind >= 35 ) || ( null !== $tomorrow_gust && $tomorrow_gust >= 50 ) || ( null !== $tomorrow_wind && $tomorrow_wind >= 40 );
            $has_heat  = null !== $today_max && $today_max >= 35;
            $has_cold  = ( null !== $today_min && $today_min <= 2 ) || ( null !== $today_max && $today_max <= 10 );

            if ( $has_storm ) {
                return array(
                    'title' => $t['storm_title'],
                    'text'  => sprintf( $t['storm_text'], $today_condition, $today_range, $today_pp, $tomorrow_condition, $tomorrow_range ) . $suffix,
                );
            }

            if ( $has_wind ) {
                return array(
                    'title' => $t['wind_title'],
                    'text'  => sprintf(
                        $t['wind_text'],
                        $today_condition,
                        $today_range,
                        null !== $today_wind ? $today_wind : 0,
                        null !== $today_gust ? $today_gust : 0,
                        $tomorrow_condition,
                        $tomorrow_range
                    ) . $suffix,
                );
            }

            if ( $has_snow ) {
                return array(
                    'title' => $t['snow_title'],
                    'text'  => sprintf( $t['snow_text'], $today_condition, $today_range, $tomorrow_condition, $tomorrow_range ) . $suffix,
                );
            }

            if ( $has_fog ) {
                return array(
                    'title' => $t['fog_title'],
                    'text'  => sprintf( $t['fog_text'], $today_condition, $today_range, $tomorrow_condition, $tomorrow_range ) . $suffix,
                );
            }

            if ( $has_rain ) {
                return array(
                    'title' => $t['rain_title'],
                    'text'  => sprintf( $t['rain_text'], $today_condition, $today_range, $today_pp, $tomorrow_condition, $tomorrow_range ) . $suffix,
                );
            }

            if ( $has_heat ) {
                return array(
                    'title' => $t['heat_title'],
                    'text'  => sprintf( $t['heat_text'], (string) $today_max . $units_label, $today_condition, $tomorrow_condition, $tomorrow_range ) . $suffix,
                );
            }

            if ( $has_cold ) {
                return array(
                    'title' => $t['cold_title'],
                    'text'  => sprintf(
                        $t['cold_text'],
                        null !== $today_min ? (string) $today_min . $units_label : $today_range,
                        $today_condition,
                        $tomorrow_condition,
                        $tomorrow_range
                    ) . $suffix,
                );
            }

            return array(
                'title' => $t['clear_title'],
                'text'  => sprintf( $t['clear_text'], $today_condition, $today_range, $today_pp, $tomorrow_condition, $tomorrow_range ) . $suffix,
            );
        }

        /**
         * Format a compact "updated at" label for world surfaces.
         *
         * @param int $timestamp Unix timestamp.
         * @return string
         */
        private function get_world_updated_label( $timestamp ) {
            $timestamp = absint( $timestamp );

            if ( $timestamp <= 0 ) {
                return '—';
            }

            $today = (string) current_time( 'Y-m-d' );

            if ( wp_date( 'Y-m-d', $timestamp ) === $today ) {
                return wp_date( get_option( 'time_format' ), $timestamp );
            }

            return wp_date( 'M j, ' . get_option( 'time_format' ), $timestamp );
        }

        /**
         * Fetch and cache one remote JSON payload for the Dashboard module.
         *
         * @param string $cache_key Cache key.
         * @param string $url Remote URL.
         * @param int    $expiration Cache lifetime in seconds.
         * @param bool   $allow_remote Whether remote fetches are allowed.
         * @return array<string,mixed>
         */
        private function get_dashboard_remote_json( $cache_key, $url, $expiration = 600, $allow_remote = true ) {
            $cache_key  = sanitize_key( (string) $cache_key );
            $url        = esc_url_raw( (string) $url );
            $expiration = absint( $expiration );
            $allow_remote = (bool) $allow_remote;

            if ( '' === $cache_key || '' === $url ) {
                return array();
            }

            $cached = get_transient( $cache_key );

            if ( is_array( $cached ) ) {
                return $cached;
            }

            if ( ! $allow_remote ) {
                return array();
            }

            $response = wp_safe_remote_get(
                $url,
                array(
                    'timeout'   => 10,
                    'sslverify' => true,
                    'headers'   => array( 'Accept' => 'application/json' ),
                )
            );

            if ( is_wp_error( $response ) ) {
                return array();
            }

            $body = json_decode( wp_remote_retrieve_body( $response ), true );

            if ( ! is_array( $body ) ) {
                return array();
            }

            set_transient( $cache_key, $body, max( 60, $expiration ) );

            return $body;
        }

        /**
         * Build the local "My World" snapshot from the weather bundle.
         *
         * @param array<string,mixed> $weather_bundle Weather bundle.
         * @param string              $lang Dashboard language.
         * @return array<string,mixed>
         */
        private function build_world_my_world( $weather_bundle, $lang ) {
            $weather_bundle = is_array( $weather_bundle ) ? $weather_bundle : array();
            $lang           = in_array( $lang, array( 'en', 'el', 'fr', 'de', 'it', 'es' ), true ) ? $lang : 'en';

            $alert_title = $this->get_dashboard_phrase( 'steady_service_window', $lang );
            $alert_text  = $this->get_dashboard_phrase( 'steady_service_window_text', $lang );
            $alert_level = 'calm';
            $days        = isset( $weather_bundle['days'] ) && is_array( $weather_bundle['days'] ) ? $weather_bundle['days'] : array();
            $today       = ! empty( $days[0] ) && is_array( $days[0] ) ? $days[0] : array();
            $today_code  = isset( $today['code'] ) ? absint( $today['code'] ) : 0;
            $today_pp    = isset( $today['pp'] ) ? absint( $today['pp'] ) : 0;
            $today_max   = isset( $today['max'] ) && null !== $today['max'] ? (int) $today['max'] : null;
            $today_gust  = isset( $today['gust'] ) && null !== $today['gust'] ? (int) $today['gust'] : null;

            if ( ! empty( $weather_bundle['error'] ) ) {
                $alert_title = $this->get_dashboard_phrase( 'weather_sync_pending', $lang );
                $alert_text  = $this->get_dashboard_phrase( 'weather_sync_pending_text', $lang );
                $alert_level = 'warning';
            } elseif ( in_array( $today_code, array( 95, 96, 99 ), true ) || $today_pp >= 75 ) {
                $alert_title = $this->get_dashboard_phrase( 'storm_watch', $lang );
                $alert_text  = $this->get_dashboard_phrase( 'storm_watch_text', $lang );
                $alert_level = 'danger';
            } elseif ( $today_pp >= 45 || ( null !== $today_gust && $today_gust >= 45 ) ) {
                $alert_title = $this->get_dashboard_phrase( 'rain_readiness', $lang );
                $alert_text  = $this->get_dashboard_phrase( 'rain_readiness_text', $lang );
                $alert_level = 'warning';
            } elseif ( null !== $today_max && $today_max >= 35 ) {
                $alert_title = $this->get_dashboard_phrase( 'heat_alert', $lang );
                $alert_text  = $this->get_dashboard_phrase( 'heat_alert_text', $lang );
                $alert_level = 'warning';
            }

            return array(
                'place'             => isset( $weather_bundle['place'] ) ? (string) $weather_bundle['place'] : '',
                'now_label'         => isset( $weather_bundle['now_label'] ) ? (string) $weather_bundle['now_label'] : '—',
                'condition'         => isset( $weather_bundle['condition'] ) ? (string) $weather_bundle['condition'] : '',
                'tip'               => isset( $weather_bundle['tip'] ) ? (string) $weather_bundle['tip'] : '',
                'min_label'         => isset( $weather_bundle['today_min_label'] ) ? (string) $weather_bundle['today_min_label'] : '—',
                'max_label'         => isset( $weather_bundle['today_max_label'] ) ? (string) $weather_bundle['today_max_label'] : '—',
                'rain_label'        => isset( $weather_bundle['today_rain_label'] ) ? (string) $weather_bundle['today_rain_label'] : '—',
                'sunrise_label'     => isset( $weather_bundle['sunrise_label'] ) ? (string) $weather_bundle['sunrise_label'] : '—',
                'sunset_label'      => isset( $weather_bundle['sunset_label'] ) ? (string) $weather_bundle['sunset_label'] : '—',
                'air_quality_label' => isset( $weather_bundle['air_quality_label'] ) ? (string) $weather_bundle['air_quality_label'] : 'AQI —',
                'alert_title'       => $alert_title,
                'alert_text'        => $alert_text,
                'alert_level'       => $alert_level,
                'source_label'      => 'Open-Meteo',
                'updated_label'     => $this->get_world_updated_label( isset( $weather_bundle['fetched_at'] ) ? (int) $weather_bundle['fetched_at'] : 0 ),
            );
        }

        /**
         * Fetch the latest global earthquake snapshot.
         *
         * @param bool $allow_remote Whether remote fetches are allowed.
         * @return array<string,mixed>
         */
        private function get_world_earthquake_snapshot( $allow_remote = true ) {
            $snapshot = array(
                'source_label'  => 'USGS',
                'updated_label' => '—',
                'count_24h'     => 0,
                'largest_label' => '—',
                'largest_place' => __( 'No earthquake data yet.', 'noesia-smart-waiter' ),
                'items'         => array(),
            );

            $payload = $this->get_dashboard_remote_json(
                'noesia_swa_dashboard_world_quakes',
                'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson',
                600,
                $allow_remote
            );

            if ( empty( $payload['features'] ) || ! is_array( $payload['features'] ) ) {
                return $snapshot;
            }

            $items        = array();
            $largest_mag  = null;
            $largest_place = '';
            $generated    = isset( $payload['metadata']['generated'] ) ? (int) round( ((float) $payload['metadata']['generated']) / 1000 ) : 0;

            foreach ( $payload['features'] as $feature ) {
                if ( ! is_array( $feature ) ) {
                    continue;
                }

                $properties = isset( $feature['properties'] ) && is_array( $feature['properties'] ) ? $feature['properties'] : array();
                $geometry   = isset( $feature['geometry'] ) && is_array( $feature['geometry'] ) ? $feature['geometry'] : array();
                $coords     = isset( $geometry['coordinates'] ) && is_array( $geometry['coordinates'] ) ? $geometry['coordinates'] : array();
                if ( count( $coords ) < 2 ) {
                    continue;
                }

                $mag = isset( $properties['mag'] ) && is_numeric( $properties['mag'] ) ? round( (float) $properties['mag'], 1 ) : null;
                $lat = is_numeric( $coords[1] ) ? (float) $coords[1] : null;
                $lng = is_numeric( $coords[0] ) ? (float) $coords[0] : null;

                if ( null === $lat || null === $lng || null === $mag ) {
                    continue;
                }

                $place     = isset( $properties['place'] ) ? sanitize_text_field( wp_strip_all_tags( (string) $properties['place'] ) ) : __( 'Unknown location', 'noesia-smart-waiter' );
                $timestamp = isset( $properties['time'] ) ? (int) round( ((float) $properties['time']) / 1000 ) : 0;
                $severity  = 'minor';

                if ( $mag >= 6 ) {
                    $severity = 'major';
                } elseif ( $mag >= 4.5 ) {
                    $severity = 'strong';
                } elseif ( $mag >= 3 ) {
                    $severity = 'moderate';
                }

                if ( null === $largest_mag || $mag > $largest_mag ) {
                    $largest_mag   = $mag;
                    $largest_place = $place;
                }

                $items[] = array(
                    'id'              => sanitize_title( $place . '-' . (string) $timestamp ),
                    'place'           => $place,
                    'magnitude'       => $mag,
                    'magnitude_label' => number_format_i18n( $mag, 1 ),
                    'label'           => number_format_i18n( $mag, 1 ) . ' • ' . $place,
                    'lat'             => $lat,
                    'lng'             => $lng,
                    'timestamp'       => $timestamp,
                    'updated_label'   => $this->get_world_updated_label( $timestamp ),
                    'severity'        => $severity,
                    'source_url'      => isset( $properties['url'] ) ? esc_url_raw( (string) $properties['url'] ) : '',
                );
            }

            usort(
                $items,
                static function ( $left, $right ) {
                    $left_ts  = isset( $left['timestamp'] ) ? (int) $left['timestamp'] : 0;
                    $right_ts = isset( $right['timestamp'] ) ? (int) $right['timestamp'] : 0;

                    return $right_ts <=> $left_ts;
                }
            );

            $snapshot['count_24h'] = count( $items );
            $snapshot['items']     = array_slice( $items, 0, 20 );

            if ( null !== $largest_mag ) {
                $snapshot['largest_label'] = number_format_i18n( $largest_mag, 1 );
                $snapshot['largest_place'] = $largest_place;
            }

            if ( $generated > 0 ) {
                $snapshot['updated_label'] = $this->get_world_updated_label( $generated );
            } elseif ( ! empty( $items[0]['timestamp'] ) ) {
                $snapshot['updated_label'] = $this->get_world_updated_label( (int) $items[0]['timestamp'] );
            }

            return $snapshot;
        }

        /**
         * Fetch a compact ISS position snapshot.
         *
         * @param bool $allow_remote Whether remote fetches are allowed.
         * @return array<string,mixed>
         */
        private function get_world_iss_snapshot( $allow_remote = true ) {
            $snapshot = array(
                'source_label'   => 'wheretheiss.at',
                'source_url'     => 'https://www.wheretheiss.at/',
                'updated_label'  => '—',
                'has_position'   => false,
                'lat'            => null,
                'lng'            => null,
                'altitude_label' => '—',
                'velocity_label' => '—',
                'visibility'     => '—',
                'region_label'   => __( 'Live orbit unavailable right now', 'noesia-smart-waiter' ),
            );

            $payload = $this->get_dashboard_remote_json(
                'noesia_swa_dashboard_world_iss',
                'https://api.wheretheiss.at/v1/satellites/25544',
                300,
                $allow_remote
            );

            if ( empty( $payload ) ) {
                return $snapshot;
            }

            $lat = isset( $payload['latitude'] ) && is_numeric( $payload['latitude'] ) ? (float) $payload['latitude'] : null;
            $lng = isset( $payload['longitude'] ) && is_numeric( $payload['longitude'] ) ? (float) $payload['longitude'] : null;

            if ( null === $lat || null === $lng ) {
                return $snapshot;
            }

            $altitude = isset( $payload['altitude'] ) && is_numeric( $payload['altitude'] ) ? round( (float) $payload['altitude'] ) : null;
            $velocity = isset( $payload['velocity'] ) && is_numeric( $payload['velocity'] ) ? round( (float) $payload['velocity'] ) : null;
            $timestamp = isset( $payload['timestamp'] ) ? absint( (int) $payload['timestamp'] ) : 0;

            $snapshot['has_position']   = true;
            $snapshot['lat']            = $lat;
            $snapshot['lng']            = $lng;
            $snapshot['altitude_label'] = null !== $altitude ? (string) $altitude . ' km' : '—';
            $snapshot['velocity_label'] = null !== $velocity ? number_format_i18n( $velocity ) . ' km/h' : '—';
            $snapshot['visibility']     = isset( $payload['visibility'] ) ? sanitize_text_field( (string) $payload['visibility'] ) : '—';
            $snapshot['updated_label']  = $this->get_world_updated_label( $timestamp );
            $snapshot['region_label']   = $this->get_world_orbit_region_label( $lat, $lng );

            return $snapshot;
        }

        /**
         * Resolve an approximate orbital region label for the ISS.
         *
         * This intentionally stays honest and continent/ocean-level because the
         * current snapshot does not ship with a local country polygon dataset.
         *
         * @param float $lat Latitude.
         * @param float $lng Longitude.
         * @return string
         */
        private function get_world_orbit_region_label( $lat, $lng ) {
            $lat = (float) $lat;
            $lng = (float) $lng;

            if ( $lat >= 66 ) {
                return __( 'Over the Arctic region', 'noesia-smart-waiter' );
            }

            if ( $lat <= -66 ) {
                return __( 'Over the Antarctic region', 'noesia-smart-waiter' );
            }

            if ( $lat >= 36 && $lat <= 72 && $lng >= -12 && $lng <= 45 ) {
                return __( 'Over Europe', 'noesia-smart-waiter' );
            }

            if ( $lat >= -35 && $lat <= 37 && $lng >= -20 && $lng <= 52 ) {
                return __( 'Over Africa', 'noesia-smart-waiter' );
            }

            if ( $lat >= 7 && $lat <= 83 && $lng >= 45 && $lng <= 180 ) {
                return __( 'Over Asia', 'noesia-smart-waiter' );
            }

            if ( $lat >= -48 && $lat <= 10 && $lng >= 110 && $lng <= 180 ) {
                return __( 'Over Oceania', 'noesia-smart-waiter' );
            }

            if ( $lat >= 15 && $lat <= 84 && $lng >= -170 && $lng <= -52 ) {
                return __( 'Over North America', 'noesia-smart-waiter' );
            }

            if ( $lat >= -56 && $lat <= 15 && $lng >= -92 && $lng <= -30 ) {
                return __( 'Over South America', 'noesia-smart-waiter' );
            }

            if ( $lng >= -52 && $lng <= 15 ) {
                return __( 'Over the Atlantic Ocean', 'noesia-smart-waiter' );
            }

            if ( $lng >= 20 && $lng <= 146 && $lat >= -55 && $lat <= 30 ) {
                return __( 'Over the Indian Ocean', 'noesia-smart-waiter' );
            }

            return __( 'Over the Pacific Ocean', 'noesia-smart-waiter' );
        }

        /**
         * Resolve a readable label for GDACS disaster event types.
         *
         * @param string $event_type Event type code.
         * @return string
         */
        private function get_world_disaster_type_label( $event_type ) {
            $event_type = strtoupper( sanitize_key( (string) $event_type ) );

            $map = array(
                'TC' => __( 'Tropical Cyclone', 'noesia-smart-waiter' ),
                'FL' => __( 'Flood', 'noesia-smart-waiter' ),
                'WF' => __( 'Wildfire', 'noesia-smart-waiter' ),
                'VO' => __( 'Volcano', 'noesia-smart-waiter' ),
                'DR' => __( 'Drought', 'noesia-smart-waiter' ),
            );

            return isset( $map[ $event_type ] ) ? $map[ $event_type ] : __( 'Major Event', 'noesia-smart-waiter' );
        }

        /**
         * Fetch major global disaster signals from GDACS.
         *
         * @param bool $allow_remote Whether remote fetches are allowed.
         * @return array<string,mixed>
         */
        private function get_world_events_snapshot( $allow_remote = true ) {
            $snapshot = array(
                'source_label'  => 'GDACS',
                'updated_label' => '—',
                'count'         => 0,
                'items'         => array(),
            );

            $payload = $this->get_dashboard_remote_json(
                'noesia_swa_dashboard_world_events',
                'https://www.gdacs.org/contentdata/xml/DR_FL_TC_VO_WF_map.geojson',
                900,
                $allow_remote
            );

            if ( empty( $payload['features'] ) || ! is_array( $payload['features'] ) ) {
                $payload = $this->get_dashboard_remote_json(
                    'noesia_swa_dashboard_world_events_fallback',
                    'https://www.gdacs.org/contentdata/xml/gdacs_app_feed.json',
                    900,
                    $allow_remote
                );
            }

            if ( empty( $payload['features'] ) || ! is_array( $payload['features'] ) ) {
                return $snapshot;
            }

            $items = array();

            foreach ( $payload['features'] as $feature ) {
                $properties = isset( $feature['properties'] ) && is_array( $feature['properties'] ) ? $feature['properties'] : array();
                $geometry   = isset( $feature['geometry'] ) && is_array( $feature['geometry'] ) ? $feature['geometry'] : array();
                $coords     = isset( $geometry['coordinates'] ) && is_array( $geometry['coordinates'] ) ? $geometry['coordinates'] : array();
                $urls       = isset( $properties['urls'] ) && is_array( $properties['urls'] ) ? $properties['urls'] : array();
                $url        = isset( $properties['url'] ) && is_array( $properties['url'] ) ? $properties['url'] : array();

                if ( count( $coords ) < 2 ) {
                    continue;
                }

                $event_type = isset( $properties['eventtype'] ) ? strtoupper( sanitize_key( (string) $properties['eventtype'] ) ) : '';
                $is_current = isset( $properties['iscurrent'] ) ? filter_var( $properties['iscurrent'], FILTER_VALIDATE_BOOLEAN ) : true;
                $lat        = is_numeric( $coords[1] ) ? (float) $coords[1] : null;
                $lng        = is_numeric( $coords[0] ) ? (float) $coords[0] : null;

                if ( ! $is_current || null === $lat || null === $lng || '' === $event_type || 'EQ' === $event_type ) {
                    continue;
                }

                $alert_level = isset( $properties['alertlevel'] ) ? sanitize_text_field( (string) $properties['alertlevel'] ) : '';
                $country     = isset( $properties['country'] ) ? sanitize_text_field( (string) $properties['country'] ) : '';
                $title       = isset( $properties['name'] ) ? sanitize_text_field( (string) $properties['name'] ) : '';
                $timestamp   = 0;

                if ( '' === $title && ! empty( $properties['eventname'] ) ) {
                    $title = sanitize_text_field( (string) $properties['eventname'] );
                }

                if ( '' === $title && ! empty( $properties['description'] ) ) {
                    $title = sanitize_text_field( (string) $properties['description'] );
                }

                if ( '' === $title ) {
                    $title = $this->get_world_disaster_type_label( $event_type );
                }

                if ( '' === $country && ! empty( $properties['affectedcountries'] ) && is_array( $properties['affectedcountries'] ) ) {
                    $affected = reset( $properties['affectedcountries'] );

                    if ( is_array( $affected ) && ! empty( $affected['countryname'] ) ) {
                        $country = sanitize_text_field( (string) $affected['countryname'] );
                    }
                }

                if ( ! empty( $properties['datemodified'] ) ) {
                    $timestamp = strtotime( sanitize_text_field( (string) $properties['datemodified'] ) );
                }

                if ( $timestamp <= 0 && ! empty( $properties['fromdate'] ) ) {
                    $timestamp = strtotime( sanitize_text_field( (string) $properties['fromdate'] ) );
                }

                $severity_label = '';
                if ( ! empty( $properties['severitydata'] ) && is_array( $properties['severitydata'] ) && ! empty( $properties['severitydata']['severitytext'] ) ) {
                    $severity_label = sanitize_text_field( (string) $properties['severitydata']['severitytext'] );
                }

                $items[] = array(
                    'id'              => sanitize_title( $event_type . '-' . $title . '-' . (string) $timestamp ),
                    'type'            => $event_type,
                    'type_label'      => $this->get_world_disaster_type_label( $event_type ),
                    'title'           => $title,
                    'country'         => $country,
                    'alert_level'     => $alert_level,
                    'severity_label'  => $severity_label,
                    'lat'             => $lat,
                    'lng'             => $lng,
                    'timestamp'       => $timestamp > 0 ? (int) $timestamp : 0,
                    'updated_label'   => $timestamp > 0 ? $this->get_world_updated_label( (int) $timestamp ) : '—',
                    'source_url'      => isset( $urls['report'] ) ? esc_url_raw( (string) $urls['report'] ) : ( isset( $url['report'] ) ? esc_url_raw( (string) $url['report'] ) : '' ),
                );
            }

            usort(
                $items,
                static function ( $left, $right ) {
                    $weights = array(
                        'Red'    => 3,
                        'Orange' => 2,
                        'Green'  => 1,
                    );

                    $left_alert  = isset( $weights[ (string) ( $left['alert_level'] ?? '' ) ] ) ? (int) $weights[ (string) $left['alert_level'] ] : 0;
                    $right_alert = isset( $weights[ (string) ( $right['alert_level'] ?? '' ) ] ) ? (int) $weights[ (string) $right['alert_level'] ] : 0;

                    if ( $left_alert !== $right_alert ) {
                        return $right_alert <=> $left_alert;
                    }

                    return (int) ( $right['timestamp'] ?? 0 ) <=> (int) ( $left['timestamp'] ?? 0 );
                }
            );

            $snapshot['count'] = count( $items );
            $snapshot['items'] = array_slice( $items, 0, 16 );

            if ( ! empty( $items[0]['timestamp'] ) ) {
                $snapshot['updated_label'] = $this->get_world_updated_label( (int) $items[0]['timestamp'] );
            }

            return $snapshot;
        }

        /**
         * Return the curated world window definition for Feel the World.
         *
         * @return array<string,string>
         */
        private function get_world_window( $iss ) {
            $iss = is_array( $iss ) ? $iss : array();
            $region_label = isset( $iss['region_label'] ) ? (string) $iss['region_label'] : '';

            return array(
                'title'        => __( 'ISS Live', 'noesia-smart-waiter' ),
                'description'  => '' !== $region_label
                    ? sprintf(
                        /* translators: %s: orbit region */
                        __( 'The ISS is currently %s. Follow the live orbit scene here and open the official NASA Live page in a new tab whenever you want the station feed.', 'noesia-smart-waiter' ),
                        $region_label
                    )
                    : __( 'Live ISS telemetry with a direct path to the official NASA Live page.', 'noesia-smart-waiter' ),
                'source_label' => __( 'Official NASA Live', 'noesia-smart-waiter' ),
                'window_label' => '' !== $region_label ? $region_label : __( 'Live orbit unavailable right now', 'noesia-smart-waiter' ),
                'watch_url'    => 'https://www.nasa.gov/live/',
                'watch_label'  => __( 'Open NASA Live', 'noesia-smart-waiter' ),
            );
        }

        /**
         * Convert one "HH:MM" label into a timestamp for the current day.
         *
         * @param string $label Time label.
         * @param string $timezone_id Timezone identifier.
         * @return int
         */
        private function get_world_time_label_timestamp( $label, $timezone_id ) {
            $label       = sanitize_text_field( (string) $label );
            $timezone_id = is_string( $timezone_id ) ? trim( $timezone_id ) : '';

            if ( ! preg_match( '/^(\d{1,2}):(\d{2})$/', $label, $matches ) ) {
                return 0;
            }

            try {
                $timezone = new DateTimeZone( '' !== $timezone_id ? $timezone_id : wp_timezone_string() );
            } catch ( Exception $exception ) {
                $timezone = wp_timezone();
            }

            $date = new DateTimeImmutable( 'now', $timezone );
            $date = $date->setTime( (int) $matches[1], (int) $matches[2] );

            return (int) $date->getTimestamp();
        }

        /**
         * Format a duration label for world widgets.
         *
         * @param int $seconds Duration in seconds.
         * @return string
         */
        private function get_world_duration_label( $seconds ) {
            $seconds = absint( (int) $seconds );

            if ( $seconds <= 0 ) {
                return '—';
            }

            $hours   = (int) floor( $seconds / HOUR_IN_SECONDS );
            $minutes = (int) floor( ( $seconds % HOUR_IN_SECONDS ) / MINUTE_IN_SECONDS );

            if ( $hours > 0 ) {
                return sprintf(
                    /* translators: 1: hours, 2: minutes */
                    __( '%1$sh %2$sm', 'noesia-smart-waiter' ),
                    number_format_i18n( $hours ),
                    number_format_i18n( $minutes )
                );
            }

            return sprintf(
                /* translators: %s: minutes */
                __( '%sm', 'noesia-smart-waiter' ),
                number_format_i18n( max( 1, $minutes ) )
            );
        }

        /**
         * Resolve an approximate moon phase label.
         *
         * @param int $timestamp Reference timestamp.
         * @return string
         */
        private function get_world_moon_phase_label( $timestamp ) {
            $timestamp = absint( (int) $timestamp );

            if ( $timestamp <= 0 ) {
                $timestamp = current_time( 'timestamp' );
            }

            $days_since_new_moon = ( $timestamp - strtotime( '2001-01-24 13:00:00 UTC' ) ) / DAY_IN_SECONDS;
            $phase               = fmod( 0.20439731 + ( $days_since_new_moon / 29.53058867 ), 1 );

            if ( $phase < 0 ) {
                $phase += 1;
            }

            if ( $phase < 0.03 || $phase >= 0.97 ) {
                return __( 'New Moon', 'noesia-smart-waiter' );
            }
            if ( $phase < 0.22 ) {
                return __( 'Waxing Crescent', 'noesia-smart-waiter' );
            }
            if ( $phase < 0.28 ) {
                return __( 'First Quarter', 'noesia-smart-waiter' );
            }
            if ( $phase < 0.47 ) {
                return __( 'Waxing Gibbous', 'noesia-smart-waiter' );
            }
            if ( $phase < 0.53 ) {
                return __( 'Full Moon', 'noesia-smart-waiter' );
            }
            if ( $phase < 0.72 ) {
                return __( 'Waning Gibbous', 'noesia-smart-waiter' );
            }
            if ( $phase < 0.78 ) {
                return __( 'Last Quarter', 'noesia-smart-waiter' );
            }

            return __( 'Waning Crescent', 'noesia-smart-waiter' );
        }

        /**
         * Build the Sky Watch card context.
         *
         * @param array<string,mixed> $weather_bundle Weather bundle.
         * @return array<string,string>
         */
        private function get_world_sky_watch( $weather_bundle ) {
            $weather_bundle = is_array( $weather_bundle ) ? $weather_bundle : array();
            $timezone_id    = isset( $weather_bundle['timezone'] ) ? sanitize_text_field( (string) $weather_bundle['timezone'] ) : wp_timezone_string();
            $sunrise_label  = isset( $weather_bundle['sunrise_label'] ) ? (string) $weather_bundle['sunrise_label'] : '—';
            $sunset_label   = isset( $weather_bundle['sunset_label'] ) ? (string) $weather_bundle['sunset_label'] : '—';
            $sunrise_ts     = $this->get_world_time_label_timestamp( $sunrise_label, $timezone_id );
            $sunset_ts      = $this->get_world_time_label_timestamp( $sunset_label, $timezone_id );
            $now_ts         = current_time( 'timestamp' );
            $next_cycle     = __( 'Solar cycle ready', 'noesia-smart-waiter' );

            if ( $sunrise_ts > 0 && $sunset_ts > 0 ) {
                if ( $now_ts < $sunrise_ts ) {
                    $next_cycle = sprintf(
                        /* translators: %s: duration */
                        __( 'Sunrise in %s', 'noesia-smart-waiter' ),
                        $this->get_world_duration_label( $sunrise_ts - $now_ts )
                    );
                } elseif ( $now_ts < $sunset_ts ) {
                    $next_cycle = sprintf(
                        /* translators: %s: duration */
                        __( 'Sunset in %s', 'noesia-smart-waiter' ),
                        $this->get_world_duration_label( $sunset_ts - $now_ts )
                    );
                } else {
                    $next_cycle = __( 'Night cycle in progress', 'noesia-smart-waiter' );
                }
            }

            return array(
                'timezone_label'   => '' !== $timezone_id ? str_replace( '_', ' ', $timezone_id ) : __( 'Local time', 'noesia-smart-waiter' ),
                'sunrise_label'    => $sunrise_label,
                'sunset_label'     => $sunset_label,
                'day_length_label' => ( $sunrise_ts > 0 && $sunset_ts > 0 && $sunset_ts > $sunrise_ts ) ? $this->get_world_duration_label( $sunset_ts - $sunrise_ts ) : '—',
                'moon_phase_label' => $this->get_world_moon_phase_label( $now_ts ),
                'next_cycle_label' => $next_cycle,
            );
        }

        /**
         * Build world clock cards.
         *
         * @return array<int,array<string,string>>
         */
        private function get_world_clock_locations( $options ) {
            $options   = is_array( $options ) ? $options : array();
            $raw       = isset( $options['dashboard_world_cities'] ) ? sanitize_textarea_field( (string) $options['dashboard_world_cities'] ) : '';
            $locations = array();

            if ( '' !== trim( $raw ) ) {
                $lines = preg_split( '/\r\n|\r|\n/', $raw );

                if ( is_array( $lines ) ) {
                    foreach ( $lines as $line ) {
                        $line = trim( (string) $line );

                        if ( '' === $line ) {
                            continue;
                        }

                        $city = '';
                        $country = '';
                        $timezone = '';

                        if ( false !== strpos( $line, '|' ) ) {
                            $parts    = array_map( 'trim', explode( '|', $line ) );
                            $city     = isset( $parts[0] ) ? sanitize_text_field( (string) $parts[0] ) : '';
                            $country  = isset( $parts[1] ) ? $this->normalize_country_code( (string) $parts[1] ) : '';
                            $timezone = isset( $parts[2] ) ? sanitize_text_field( (string) $parts[2] ) : '';
                        } else {
                            $parts   = array_map( 'trim', explode( ',', $line ) );
                            $city    = isset( $parts[0] ) ? sanitize_text_field( (string) $parts[0] ) : '';
                            $country = isset( $parts[1] ) ? $this->normalize_country_code( (string) $parts[1] ) : '';
                        }

                        if ( '' === $city ) {
                            continue;
                        }

                        if ( '' !== $timezone ) {
                            try {
                                new DateTimeZone( $timezone );
                            } catch ( Exception $exception ) {
                                $timezone = '';
                            }
                        }

                        $locations[] = array(
                            'city'     => $city,
                            'country'  => $country,
                            'timezone' => $timezone,
                        );

                        if ( count( $locations ) >= 4 ) {
                            break;
                        }
                    }
                }
            }

            if ( empty( $locations ) ) {
                $fallback_city    = isset( $options['weather_city'] ) ? sanitize_text_field( (string) $options['weather_city'] ) : '';
                $fallback_country = isset( $options['weather_country'] ) ? $this->normalize_country_code( (string) $options['weather_country'] ) : '';

                if ( '' !== $fallback_city ) {
                    $locations[] = array(
                        'city'     => $fallback_city,
                        'country'  => $fallback_country,
                        'timezone' => '',
                    );
                }
            }

            return array_slice( $locations, 0, 4 );
        }

        /**
         * Build world clock cards.
         *
         * @param array<string,mixed> $options Plugin options.
         * @return array<int,array<string,string>>
         */
        private function get_world_clocks( $options, $allow_remote = true ) {
            $locations = $this->get_world_clock_locations( $options );
            $service   = $this->get_weather_service();
            $clocks    = array();

            foreach ( $locations as $location ) {
                $city     = isset( $location['city'] ) ? (string) $location['city'] : '';
                $country  = isset( $location['country'] ) ? (string) $location['country'] : '';
                $timezone = isset( $location['timezone'] ) ? (string) $location['timezone'] : '';
                $clock    = array(
                    'city'       => $city,
                    'country'    => $country,
                    'country_name' => '',
                    'timezone'   => $timezone,
                    'time_label' => '—',
                    'day_label'  => '',
                    'temp_label' => '—',
                    'condition_label' => '',
                    'rain_label' => '—',
                    'wind_label' => '—',
                    'daylight_label' => '—',
                    'daylight_state' => '',
                    'lat' => null,
                    'lng' => null,
                );

                if ( $service && method_exists( $service, 'get_location_snapshot' ) ) {
                    $snapshot = $service->get_location_snapshot( $city, $country, $timezone, $options, $allow_remote );

                    if ( is_array( $snapshot ) ) {
                        $clock['city']       = isset( $snapshot['city'] ) ? (string) $snapshot['city'] : $clock['city'];
                        $clock['country']    = isset( $snapshot['country'] ) ? (string) $snapshot['country'] : $clock['country'];
                        $clock['country_name'] = isset( $snapshot['country_name'] ) ? (string) $snapshot['country_name'] : $clock['country_name'];
                        $clock['timezone']   = isset( $snapshot['timezone'] ) ? (string) $snapshot['timezone'] : $clock['timezone'];
                        $clock['time_label'] = isset( $snapshot['time_label'] ) ? (string) $snapshot['time_label'] : $clock['time_label'];
                        $clock['day_label']  = isset( $snapshot['day_label'] ) ? (string) $snapshot['day_label'] : $clock['day_label'];
                        $clock['temp_label'] = isset( $snapshot['temp_label'] ) ? (string) $snapshot['temp_label'] : $clock['temp_label'];
                        $clock['condition_label'] = isset( $snapshot['condition_label'] ) ? (string) $snapshot['condition_label'] : $clock['condition_label'];
                        $clock['rain_label'] = isset( $snapshot['rain_label'] ) ? (string) $snapshot['rain_label'] : $clock['rain_label'];
                        $clock['wind_label'] = isset( $snapshot['wind_label'] ) ? (string) $snapshot['wind_label'] : $clock['wind_label'];
                        $clock['daylight_label'] = isset( $snapshot['daylight_label'] ) ? (string) $snapshot['daylight_label'] : $clock['daylight_label'];
                        $clock['daylight_state'] = isset( $snapshot['daylight_state'] ) ? (string) $snapshot['daylight_state'] : $clock['daylight_state'];
                        $clock['lat'] = isset( $snapshot['lat'] ) && is_numeric( $snapshot['lat'] ) ? (float) $snapshot['lat'] : $clock['lat'];
                        $clock['lng'] = isset( $snapshot['lng'] ) && is_numeric( $snapshot['lng'] ) ? (float) $snapshot['lng'] : $clock['lng'];
                    }
                }

                if ( '—' === $clock['time_label'] && '' !== $clock['timezone'] ) {
                    try {
                        $timezone_object      = new DateTimeZone( $clock['timezone'] );
                        $date                 = new DateTimeImmutable( 'now', $timezone_object );
                        $clock['time_label']  = $date->format( 'H:i' );
                        $clock['day_label']   = wp_date( 'D', $date->getTimestamp(), $timezone_object );
                    } catch ( Exception $exception ) {
                        $clock['time_label'] = '—';
                    }
                }

                $clocks[] = $clock;
            }

            return $clocks;
        }

        /**
         * Determine whether the compare cards still need a live hydration pass.
         *
         * @param array<int,array<string,mixed>> $clocks World compare cards.
         * @return bool
         */
        private function world_clocks_need_live_refresh( $clocks ) {
            $clocks = is_array( $clocks ) ? $clocks : array();

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

            foreach ( $clocks as $clock ) {
                if ( ! is_array( $clock ) ) {
                    continue;
                }

                if ( '—' === (string) ( $clock['temp_label'] ?? '—' ) ) {
                    return true;
                }

                if ( '—' === (string) ( $clock['rain_label'] ?? '—' ) ) {
                    return true;
                }

                if ( '—' === (string) ( $clock['wind_label'] ?? '—' ) ) {
                    return true;
                }

                if ( '—' === (string) ( $clock['daylight_label'] ?? '—' ) ) {
                    return true;
                }
            }

            return false;
        }

        /**
         * Merge earthquakes and major events into one live stream list.
         *
         * @param array<string,mixed> $earthquakes Earthquake snapshot.
         * @param array<string,mixed> $events      Major events snapshot.
         * @return array<int,array<string,string>>
         */
        private function get_world_live_stream( $earthquakes, $events, $iss = array() ) {
            $earthquakes = is_array( $earthquakes ) ? $earthquakes : array();
            $events      = is_array( $events ) ? $events : array();
            $iss         = is_array( $iss ) ? $iss : array();
            $items       = array();

            if ( ! empty( $iss['has_position'] ) ) {
                $items[] = array(
                    'id'         => 'iss-live',
                    'type'       => 'orbit',
                    /* translators: %s: region label for the current ISS position. */
                    'label'      => ! empty( $iss['region_label'] ) ? sprintf( __( 'ISS • %s', 'noesia-smart-waiter' ), (string) $iss['region_label'] ) : __( 'ISS live orbit', 'noesia-smart-waiter' ),
                    'meta'       => trim(
                        implode(
                            ' · ',
                            array_filter(
                                array(
                                    isset( $iss['velocity_label'] ) ? (string) $iss['velocity_label'] : '',
                                    isset( $iss['altitude_label'] ) ? (string) $iss['altitude_label'] : '',
                                    isset( $iss['updated_label'] ) ? (string) $iss['updated_label'] : '',
                                )
                            )
                        )
                    ),
                    'pill_label' => __( 'Orbit', 'noesia-smart-waiter' ),
                    'pill_class' => 'info',
                    'timestamp'  => current_time( 'timestamp' ),
                    'source_url' => 'https://www.nasa.gov/live/',
                    'action_label' => __( 'Open NASA Live', 'noesia-smart-waiter' ),
                );
            }

            if ( ! empty( $earthquakes['items'] ) && is_array( $earthquakes['items'] ) ) {
                foreach ( array_slice( $earthquakes['items'], 0, 12 ) as $item ) {
                    if ( ! is_array( $item ) ) {
                        continue;
                    }

                    $meta_parts = array_filter(
                        array(
                            isset( $item['updated_label'] ) ? (string) $item['updated_label'] : '',
                            isset( $item['place'] ) ? (string) $item['place'] : '',
                        )
                    );

                    $items[] = array(
                        'id'            => isset( $item['id'] ) ? sanitize_key( (string) $item['id'] ) : 'quake-' . count( $items ),
                        'type'          => 'earthquake',
                        'label'         => isset( $item['label'] ) ? (string) $item['label'] : __( 'Earthquake', 'noesia-smart-waiter' ),
                        'meta'          => implode( ' · ', $meta_parts ),
                        'pill_label'    => __( 'Quake', 'noesia-smart-waiter' ),
                        'pill_class'    => isset( $item['severity'] ) ? sanitize_html_class( (string) $item['severity'] ) : 'moderate',
                        'timestamp'     => isset( $item['timestamp'] ) ? (int) $item['timestamp'] : 0,
                        'source_url'    => isset( $item['source_url'] ) ? esc_url_raw( (string) $item['source_url'] ) : '',
                        'action_label'  => __( 'Open report', 'noesia-smart-waiter' ),
                    );
                }
            }

            if ( ! empty( $events['items'] ) && is_array( $events['items'] ) ) {
                foreach ( array_slice( $events['items'], 0, 8 ) as $item ) {
                    if ( ! is_array( $item ) ) {
                        continue;
                    }

                    $meta_parts = array_filter(
                        array(
                            isset( $item['type_label'] ) ? (string) $item['type_label'] : '',
                            isset( $item['country'] ) ? (string) $item['country'] : '',
                            isset( $item['updated_label'] ) ? (string) $item['updated_label'] : '',
                        )
                    );

                    $items[] = array(
                        'id'         => isset( $item['id'] ) ? sanitize_key( (string) $item['id'] ) : 'event-' . count( $items ),
                        'type'       => 'disaster',
                        'label'      => isset( $item['title'] ) ? (string) $item['title'] : __( 'Major Event', 'noesia-smart-waiter' ),
                        'meta'       => implode( ' · ', $meta_parts ),
                        'pill_label' => isset( $item['type_label'] ) ? (string) $item['type_label'] : __( 'Event', 'noesia-smart-waiter' ),
                        'pill_class' => ! empty( $item['alert_level'] ) ? strtolower( sanitize_html_class( (string) $item['alert_level'] ) ) : 'warning',
                        'timestamp'  => isset( $item['timestamp'] ) ? (int) $item['timestamp'] : 0,
                        'source_url' => isset( $item['source_url'] ) ? esc_url_raw( (string) $item['source_url'] ) : '',
                        'action_label' => __( 'Open report', 'noesia-smart-waiter' ),
                    );
                }
            }

            usort(
                $items,
                static function ( $left, $right ) {
                    return (int) ( $right['timestamp'] ?? 0 ) <=> (int) ( $left['timestamp'] ?? 0 );
                }
            );

            return array_slice( $items, 0, 20 );
        }

        /**
         * Build the points consumed by the Feel the World map surface.
         *
         * @param array<string,mixed> $weather_bundle Weather bundle.
         * @param array<string,mixed> $my_world My World snapshot.
         * @param array<string,mixed> $earthquakes Earthquake snapshot.
         * @param array<string,mixed> $iss ISS snapshot.
         * @param array<string,mixed> $options Plugin options.
         * @return array<int,array<string,mixed>>
         */
        private function get_world_map_points( $weather_bundle, $my_world, $earthquakes, $events, $iss, $clocks, $options ) {
            $weather_bundle = is_array( $weather_bundle ) ? $weather_bundle : array();
            $my_world       = is_array( $my_world ) ? $my_world : array();
            $earthquakes    = is_array( $earthquakes ) ? $earthquakes : array();
            $events         = is_array( $events ) ? $events : array();
            $iss            = is_array( $iss ) ? $iss : array();
            $clocks         = is_array( $clocks ) ? $clocks : array();
            $options        = is_array( $options ) ? $options : array();
            $points         = array();

            $local_lat = isset( $weather_bundle['latitude'] ) && is_numeric( $weather_bundle['latitude'] ) ? (float) $weather_bundle['latitude'] : null;
            $local_lng = isset( $weather_bundle['longitude'] ) && is_numeric( $weather_bundle['longitude'] ) ? (float) $weather_bundle['longitude'] : null;
            $local_now = isset( $my_world['now_label'] ) ? sanitize_text_field( (string) $my_world['now_label'] ) : '';
            $local_condition = isset( $my_world['condition'] ) ? sanitize_text_field( (string) $my_world['condition'] ) : '';
            $local_meta = trim( implode( ' · ', array_filter( array( $local_now, $local_condition ) ) ) );

            if ( null !== $local_lat && null !== $local_lng ) {
                $points[] = array(
                    'id'       => 'local-weather',
                    'type'     => 'weather',
                    'title'    => isset( $my_world['place'] ) ? (string) $my_world['place'] : __( 'Local weather', 'noesia-smart-waiter' ),
                    'meta'     => $local_meta,
                    'lat'      => $local_lat,
                    'lng'      => $local_lng,
                    'priority' => 100,
                    'source_url' => '',
                    'action_label' => '',
                );
            }

            if ( ! empty( $clocks ) ) {
                foreach ( array_slice( $clocks, 0, 4 ) as $clock ) {
                    $clock = is_array( $clock ) ? $clock : array();
                    $clock_lat = isset( $clock['lat'] ) && is_numeric( $clock['lat'] ) ? (float) $clock['lat'] : null;
                    $clock_lng = isset( $clock['lng'] ) && is_numeric( $clock['lng'] ) ? (float) $clock['lng'] : null;
                    $clock_city = isset( $clock['city'] ) ? sanitize_text_field( (string) $clock['city'] ) : '';
                    $clock_country_name = isset( $clock['country_name'] ) ? sanitize_text_field( (string) $clock['country_name'] ) : '';

                    if ( null === $clock_lat || null === $clock_lng || '' === $clock_city ) {
                        continue;
                    }

                    if ( null !== $local_lat && null !== $local_lng && abs( $clock_lat - $local_lat ) < 0.15 && abs( $clock_lng - $local_lng ) < 0.15 ) {
                        continue;
                    }

                    $clock_meta_parts = array_filter(
                        array(
                            isset( $clock['temp_label'] ) ? (string) $clock['temp_label'] : '',
                            isset( $clock['condition_label'] ) ? (string) $clock['condition_label'] : '',
                            isset( $clock['daylight_state'] ) ? (string) $clock['daylight_state'] : '',
                        )
                    );

                    $points[] = array(
                        'id'       => sanitize_title( 'city-weather-' . $clock_city . '-' . $clock_country_name ),
                        'type'     => 'weather',
                        'title'    => '' !== $clock_country_name ? $clock_city . ', ' . $clock_country_name : $clock_city,
                        'meta'     => implode( ' · ', $clock_meta_parts ),
                        'country_name' => $clock_country_name,
                        'country_code' => isset( $clock['country'] ) ? sanitize_text_field( (string) $clock['country'] ) : '',
                        'lat'      => $clock_lat,
                        'lng'      => $clock_lng,
                        'priority' => 72,
                        'source_url' => '',
                        'action_label' => '',
                    );
                }
            }

            if ( ! empty( $earthquakes['items'] ) && is_array( $earthquakes['items'] ) ) {
                foreach ( array_slice( $earthquakes['items'], 0, 20 ) as $item ) {
                    if ( ! is_array( $item ) || ! isset( $item['lat'], $item['lng'] ) ) {
                        continue;
                    }

                    $points[] = array(
                        'id'       => isset( $item['id'] ) ? sanitize_key( (string) $item['id'] ) : 'quake-' . count( $points ),
                        'type'     => 'earthquake',
                        'title'    => isset( $item['label'] ) ? (string) $item['label'] : __( 'Earthquake', 'noesia-smart-waiter' ),
                        'meta'     => trim(
                            implode(
                                ' · ',
                                array_filter(
                                    array(
                                        isset( $item['magnitude_label'] ) ? 'M ' . (string) $item['magnitude_label'] : '',
                                        isset( $item['severity'] ) ? ucfirst( (string) $item['severity'] ) : '',
                                        isset( $item['updated_label'] ) ? (string) $item['updated_label'] : '',
                                    )
                                )
                            )
                        ),
                        'lat'      => (float) $item['lat'],
                        'lng'      => (float) $item['lng'],
                        'priority' => 80,
                        'source_url' => isset( $item['source_url'] ) ? esc_url_raw( (string) $item['source_url'] ) : '',
                        'action_label' => __( 'Open report', 'noesia-smart-waiter' ),
                    );
                }
            }

            if ( ! empty( $events['items'] ) && is_array( $events['items'] ) ) {
                foreach ( array_slice( $events['items'], 0, 12 ) as $item ) {
                    if ( ! is_array( $item ) || ! isset( $item['lat'], $item['lng'] ) ) {
                        continue;
                    }

                    $points[] = array(
                        'id'       => isset( $item['id'] ) ? sanitize_key( (string) $item['id'] ) : 'event-' . count( $points ),
                        'type'     => 'disaster',
                        'title'    => isset( $item['title'] ) ? (string) $item['title'] : __( 'Major Event', 'noesia-smart-waiter' ),
                        'meta'     => trim(
                            implode(
                                ' · ',
                                array_filter(
                                    array(
                                        isset( $item['type_label'] ) ? (string) $item['type_label'] : '',
                                        isset( $item['alert_level'] ) ? (string) $item['alert_level'] : '',
                                        isset( $item['country'] ) ? (string) $item['country'] : '',
                                        isset( $item['updated_label'] ) ? (string) $item['updated_label'] : '',
                                    )
                                )
                            )
                        ),
                        'country_name' => isset( $item['country'] ) ? sanitize_text_field( (string) $item['country'] ) : '',
                        'lat'      => (float) $item['lat'],
                        'lng'      => (float) $item['lng'],
                        'priority' => 85,
                        'source_url' => isset( $item['source_url'] ) ? esc_url_raw( (string) $item['source_url'] ) : '',
                        'action_label' => __( 'Open report', 'noesia-smart-waiter' ),
                    );
                }
            }

            if ( ! empty( $iss['has_position'] ) && isset( $iss['lat'], $iss['lng'] ) ) {
                $points[] = array(
                    'id'       => 'iss-live',
                    'type'     => 'orbit',
                    'title'    => __( 'ISS Live', 'noesia-smart-waiter' ),
                    'meta'     => trim(
                        implode(
                            ' · ',
                            array_filter(
                                array(
                                    isset( $iss['region_label'] ) ? (string) $iss['region_label'] : '',
                                    isset( $iss['altitude_label'] ) ? (string) $iss['altitude_label'] : '',
                                    isset( $iss['velocity_label'] ) ? (string) $iss['velocity_label'] : '',
                                )
                            )
                        )
                    ),
                    'lat'      => (float) $iss['lat'],
                    'lng'      => (float) $iss['lng'],
                    'priority' => 90,
                    'source_url' => 'https://www.nasa.gov/live/',
                    'action_label' => __( 'Open NASA Live', 'noesia-smart-waiter' ),
                );
            }

            /**
             * Allow modules to contribute extra map points without coupling the dashboard
             * to specific app internals.
             *
             * @param array<int,array<string,mixed>> $points Current map points.
             * @param array<string,mixed>            $options Plugin options.
             * @param array<string,mixed>            $weather_bundle Weather bundle.
             * @param array<string,mixed>            $earthquakes Earthquake snapshot.
             * @param array<string,mixed>            $events Major events snapshot.
             * @param array<string,mixed>            $iss ISS snapshot.
             * @param Noesia_SWA_Dashboard_Data      $this Dashboard data service.
             */
            $points = apply_filters( 'noesia_swa_dashboard_world_map_points', $points, $options, $weather_bundle, $earthquakes, $events, $iss, $this );

            if ( ! is_array( $points ) ) {
                return array();
            }

            usort(
                $points,
                static function ( $left, $right ) {
                    $left_priority  = isset( $left['priority'] ) ? (int) $left['priority'] : 0;
                    $right_priority = isset( $right['priority'] ) ? (int) $right['priority'] : 0;

                    return $right_priority <=> $left_priority;
                }
            );

            return array_values( $points );
        }

        /**
         * Build a Google News RSS URL for world briefing searches.
         *
         * @param string $lang Language code.
         * @param string $country Country code.
         * @param string $topic Google topic token.
         * @param string $query Search query.
         * @return string
         */
        private function get_world_brief_rss_url( $lang, $country, $topic = '', $query = '' ) {
            $lang    = in_array( $lang, $this->get_supported_languages(), true ) ? $lang : 'en';
            $country = strtoupper( preg_replace( '/[^A-Z]/', '', (string) $country ) );
            $country = substr( (string) $country, 0, 2 );
            $topic   = strtoupper( sanitize_key( (string) $topic ) );
            $query   = trim( preg_replace( '/
|
|
/', ' ', sanitize_text_field( (string) $query ) ) );

            if ( '' === $country ) {
                $country = $this->get_news_country( $lang );
            }

            $lang_for_google = ( 'en' === $lang ) ? 'en-US' : $lang;
            $args            = array(
                'hl'   => $lang_for_google,
                'gl'   => $country,
                'ceid' => $country . ':' . $lang,
            );

            if ( '' !== $topic ) {
                return add_query_arg(
                    $args,
                    'https://news.google.com/rss/headlines/section/topic/' . rawurlencode( $topic )
                );
            }

            if ( '' !== $query ) {
                $args['q'] = $query;
                return add_query_arg( $args, 'https://news.google.com/rss/search' );
            }

            return add_query_arg( $args, 'https://news.google.com/rss' );
        }

        /**
         * Parse a Google News RSS feed into compact world-brief items.
         *
         * @param string $lang Language code.
         * @param string $country Country code.
         * @param string $topic Google topic token.
         * @param string $query Search query.
         * @param int    $limit Maximum number of items.
         * @return array<int,array<string,mixed>>
         */
        private function get_world_brief_feed_items( $lang, $country, $topic = '', $query = '', $limit = 5 ) {
            $limit     = max( 1, min( 10, absint( $limit ) ) );
            $cache_key = 'noesia_swa_world_brief_' . md5( strtolower( $lang . '|' . $country . '|' . $topic . '|' . $query . '|' . $limit ) );
            $cached    = get_transient( $cache_key );

            if ( is_array( $cached ) ) {
                return $cached;
            }

            $response = wp_safe_remote_get(
                $this->get_world_brief_rss_url( $lang, $country, $topic, $query ),
                array(
                    'timeout'    => 15,
                    'sslverify'  => true,
                    'user-agent' => 'Noesia Smart Waiter/' . ( defined( 'NOESIA_SWA_VERSION' ) ? NOESIA_SWA_VERSION : '1.0.0' ) . '; ' . home_url( '/' ),
                    'headers'    => array(
                        'Accept' => 'application/rss+xml, application/xml, text/xml;q=0.9, */*;q=0.8',
                    ),
                )
            );

            if ( is_wp_error( $response ) ) {
                return array();
            }

            $body = trim( (string) wp_remote_retrieve_body( $response ) );

            if ( '' === $body ) {
                return array();
            }

            $previous_state = libxml_use_internal_errors( true );
            $xml            = simplexml_load_string( $body );
            libxml_clear_errors();
            libxml_use_internal_errors( $previous_state );

            if ( ! $xml || empty( $xml->channel->item ) ) {
                return array();
            }

            $items = array();

            foreach ( $xml->channel->item as $item ) {
                $title = isset( $item->title ) ? sanitize_text_field( (string) $item->title ) : '';
                $link  = isset( $item->link ) ? esc_url_raw( (string) $item->link ) : '';
                $source = '';
                $source_url = '';
                $pub_date_ms = 0;

                if ( isset( $item->source ) ) {
                    $source     = sanitize_text_field( (string) $item->source );
                    $attributes = $item->source->attributes();
                    if ( $attributes && isset( $attributes['url'] ) ) {
                        $source_url = esc_url_raw( (string) $attributes['url'] );
                    }
                }

                if ( isset( $item->pubDate ) ) {
                    $pub_ts = strtotime( (string) $item->pubDate );
                    if ( false !== $pub_ts ) {
                        $pub_date_ms = (int) $pub_ts * 1000;
                    }
                }

                if ( '' === $title || '' === $link ) {
                    continue;
                }

                $items[] = array(
                    'title'       => $title,
                    'link'        => $link,
                    'source'      => '' !== $source ? $source : __( 'Verified source', 'noesia-smart-waiter' ),
                    'sourceUrl'   => $source_url,
                    'pubDateMs'   => $pub_date_ms,
                );

                if ( count( $items ) >= $limit ) {
                    break;
                }
            }

            if ( ! empty( $items ) ) {
                set_transient( $cache_key, $items, 10 * MINUTE_IN_SECONDS );
            }

            return $items;
        }

        /**
         * Resolve one country label for the selected dashboard language.
         *
         * @param string $country_code ISO alpha-2 country code.
         * @param string $lang Dashboard language.
         * @return string
         */
        private function get_world_country_label( $country_code, $lang ) {
            $country_code = strtoupper( preg_replace( '/[^A-Z]/', '', (string) $country_code ) );
            $country_code = substr( $country_code, 0, 2 );
            $lang         = in_array( $lang, $this->get_supported_languages(), true ) ? $lang : 'en';

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

            if ( class_exists( 'Locale' ) ) {
                $locale_map = array(
                    'el' => 'el_GR',
                    'fr' => 'fr_FR',
                    'de' => 'de_DE',
                    'it' => 'it_IT',
                    'es' => 'es_ES',
                    'en' => 'en_US',
                );
                $locale = isset( $locale_map[ $lang ] ) ? $locale_map[ $lang ] : 'en_US';
                $name   = \Locale::getDisplayRegion( '-'. $country_code, $locale );

                if ( is_string( $name ) && '' !== trim( $name ) ) {
                    return sanitize_text_field( $name );
                }
            }

            return $country_code;
        }

        /**
         * Build the world-brief feed payload for the current focus and search.
         *
         * @param array<string,mixed> $options Plugin options.
         * @param array<string,mixed> $weather_bundle Weather bundle.
         * @param array<string,mixed> $world_my_world Local weather snapshot.
         * @param string              $focus Requested focus.
         * @param string              $query Requested search query.
         * @param string              $lang Dashboard language.
         * @return array<string,mixed>
         */
        private function get_world_brief_payload( $options, $weather_bundle, $world_my_world, $focus, $query, $lang ) {
            $options         = is_array( $options ) ? $options : array();
            $weather_bundle  = is_array( $weather_bundle ) ? $weather_bundle : array();
            $world_my_world  = is_array( $world_my_world ) ? $world_my_world : array();
            $focus           = in_array( $focus, array( 'local', 'country', 'global' ), true ) ? $focus : 'country';
            $query           = trim( preg_replace( '/\s+/', ' ', sanitize_text_field( (string) $query ) ) );
            $lang            = in_array( $lang, $this->get_supported_languages(), true ) ? $lang : 'en';
            $country         = $this->get_news_country( $lang, $options );
            $local_seed      = isset( $options['weather_city'] ) ? sanitize_text_field( (string) $options['weather_city'] ) : '';
            $local_label     = isset( $world_my_world['place'] ) ? sanitize_text_field( (string) $world_my_world['place'] ) : $local_seed;
            $country_label   = $this->get_world_country_label( $country, $lang );
            $search_query    = '';
            $topic           = '';
            $target_label    = '' !== $query ? $query : $country_label;

            if ( '' !== $query ) {
                $search_query = $query;
                $target_label = $query;
            } elseif ( 'global' === $focus ) {
                $topic        = 'WORLD';
                $target_label = __( 'Whole Planet', 'noesia-smart-waiter' );
            } elseif ( 'local' === $focus ) {
                $search_query = '' !== $local_seed ? $local_seed : $local_label;
                $target_label = '' !== $local_label ? $local_label : __( 'My Place', 'noesia-smart-waiter' );
            } else {
                $target_label = '' !== $country_label ? $country_label : __( 'My Country', 'noesia-smart-waiter' );
            }

            return array(
                'focus'         => $focus,
                'query'         => $query,
                'target_label'  => $target_label,
                'items'         => $this->get_world_brief_feed_items( $lang, $country, $topic, $search_query, 5 ),
            );
        }

        /**
         * Build the live Feel the World payload.
         *
         * @param array<string,mixed> $options Plugin options.
         * @return array<string,mixed>
         */
        public function build_world_live_payload( $options, $request_args = array() ) {
            $options            = is_array( $options ) ? $options : array();
            $request_args       = is_array( $request_args ) ? $request_args : array();
            $weather_bundle     = $this->get_weather_bundle( $options );
            $dashboard_lang     = $this->get_dashboard_language( $options );
            $world_my_world     = $this->build_world_my_world( $weather_bundle, $dashboard_lang );
            $world_live_enabled = $this->is_world_live_data_enabled( $options );
            $brief_focus        = isset( $request_args['focus'] ) ? sanitize_key( (string) $request_args['focus'] ) : 'country';
            $brief_query        = isset( $request_args['query'] ) ? sanitize_text_field( (string) $request_args['query'] ) : '';

            if ( ! $world_live_enabled ) {
                $world_earthquakes = $this->get_empty_world_earthquake_snapshot();
                $world_events      = $this->get_empty_world_events_snapshot();
                $world_iss         = $this->get_empty_world_iss_snapshot();

                return array(
                    'world_live_enabled' => false,
                    'world_earthquakes'  => $world_earthquakes,
                    'world_events'       => $world_events,
                    'world_iss'          => $world_iss,
                    'world_window'       => $this->get_world_window( $world_iss ),
                    'world_clocks'       => array(),
                    'world_map_points'   => array(),
                    'world_live_stream'  => array(),
                    'world_brief'        => array(
                        'focus'        => $brief_focus,
                        'query'        => $brief_query,
                        'target_label' => '',
                        'items'        => array(),
                    ),
                );
            }

            $world_earthquakes = $this->get_world_earthquake_snapshot( true );
            $world_events      = $this->get_world_events_snapshot( true );
            $world_iss         = $this->get_world_iss_snapshot( true );
            $world_window      = $this->get_world_window( $world_iss );
            $world_clocks      = $this->get_world_clocks( $options, true );
            $world_map_points  = $this->get_world_map_points( $weather_bundle, $world_my_world, $world_earthquakes, $world_events, $world_iss, $world_clocks, $options );

            return array(
                'world_live_enabled' => true,
                'world_earthquakes'  => $world_earthquakes,
                'world_events'       => $world_events,
                'world_iss'          => $world_iss,
                'world_window'       => $world_window,
                'world_clocks'       => $world_clocks,
                'world_map_points'   => $world_map_points,
                'world_live_stream'  => $this->get_world_live_stream( $world_earthquakes, $world_events, $world_iss ),
                'world_brief'        => $this->get_world_brief_payload( $options, $weather_bundle, $world_my_world, $brief_focus, $brief_query, $dashboard_lang ),
            );
        }
        /**
         * Build the full dashboard data context.
         *
         * @param array<string,mixed> $options Plugin options.
         * @return array<string,mixed>
         */
        public function build_context( $options ) {
            $options        = is_array( $options ) ? $options : array();
            $news_language  = isset( $options['news_language'] ) ? sanitize_key( (string) $options['news_language'] ) : 'en';
            $dashboard_lang = $this->get_dashboard_language( $options );
            $news_country   = $this->get_news_country( $news_language, $options );

            $active_requests_query = new WP_Query(
                array(
                    'post_type'      => 'noesia_request',
                    'post_status'    => 'publish',
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required to filter module-owned request status stored in post meta.
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required to filter appointment date and status stored in post meta.
                    'meta_query'     => array(
                        array(
                            'key'   => 'noesia_status',
                            'value' => 'active',
                        ),
                    ),
                    'posts_per_page' => 1,
                    'fields'         => 'ids',
                    'orderby'        => 'date',
                    'order'          => 'ASC',
                )
            );

            $dashboard_active_requests = (int) $active_requests_query->found_posts;
            wp_reset_postdata();

            $host_name         = isset( $options['profile_host_name'] ) ? sanitize_text_field( (string) $options['profile_host_name'] ) : '';
            $business_name     = isset( $options['profile_business_name'] ) ? sanitize_text_field( (string) $options['profile_business_name'] ) : '';
            $business_type     = isset( $options['profile_business_type'] ) ? sanitize_text_field( (string) $options['profile_business_type'] ) : '';
            $profile_address   = isset( $options['profile_address'] ) ? sanitize_textarea_field( (string) $options['profile_address'] ) : '';
            $profile_phone     = isset( $options['profile_phone'] ) ? sanitize_text_field( (string) $options['profile_phone'] ) : '';
            $profile_email     = isset( $options['profile_email'] ) ? sanitize_email( (string) $options['profile_email'] ) : '';
            $profile_website   = isset( $options['profile_website'] ) ? esc_url( (string) $options['profile_website'] ) : '';
            $profile_tax_id    = isset( $options['profile_tax_id'] ) ? sanitize_text_field( (string) $options['profile_tax_id'] ) : '';
            $profile_reg_no    = isset( $options['profile_registration_no'] ) ? sanitize_text_field( (string) $options['profile_registration_no'] ) : '';
            $briefing_enabled  = 'yes';
            $briefing_updates  = 'yes';
            $briefing_schedule = 'yes';

            $current_hour = (int) current_time( 'G' );

            if ( $current_hour >= 5 && $current_hour < 12 ) {
                $greeting_base = __( 'Good morning', 'noesia-smart-waiter' );
            } elseif ( $current_hour >= 12 && $current_hour < 18 ) {
                $greeting_base = __( 'Good afternoon', 'noesia-smart-waiter' );
            } else {
                $greeting_base = __( 'Good evening', 'noesia-smart-waiter' );
            }

            $greeting_text = ( '' !== $host_name )
                ? sprintf(
                    /* translators: 1: greeting, 2: host name */
                    __( '%1$s, %2$s', 'noesia-smart-waiter' ),
                    $greeting_base,
                    $host_name
                )
                : $greeting_base;

            $today_requests_query = new WP_Query(
                array(
                    'post_type'      => 'noesia_request',
                    'post_status'    => 'publish',
                    'posts_per_page' => 1,
                    'fields'         => 'ids',
                    'date_query'     => array(
                        array(
                            'year'  => (int) current_time( 'Y' ),
                            'month' => (int) current_time( 'n' ),
                            'day'   => (int) current_time( 'j' ),
                        ),
                    ),
                )
            );

            $today_requests_count = (int) $today_requests_query->found_posts;
            wp_reset_postdata();

            $briefing_updates_text = sprintf(
                /* translators: %d: active requests count */
                _n( '%d Active Request', '%d Active Requests', $dashboard_active_requests, 'noesia-smart-waiter' ),
                $dashboard_active_requests
            );

            if ( '' !== $business_name && '' !== $business_type ) {
                $profile_business_text = $business_name . ' · ' . $business_type;
            } elseif ( '' !== $business_name ) {
                $profile_business_text = $business_name;
            } elseif ( '' !== $business_type ) {
                $profile_business_text = $business_type;
            } else {
                $profile_business_text = __( 'Not set yet', 'noesia-smart-waiter' );
            }

            $profile_contact_parts = array();

            if ( '' !== $profile_phone ) {
                $profile_contact_parts[] = $profile_phone;
            }
            if ( '' !== $profile_email ) {
                $profile_contact_parts[] = $profile_email;
            }
            if ( '' !== $profile_website ) {
                $profile_contact_parts[] = $profile_website;
            }

            $profile_contact_text = ! empty( $profile_contact_parts )
                ? implode( ' · ', $profile_contact_parts )
                : __( 'Not set yet', 'noesia-smart-waiter' );

            $profile_legal_parts = array();

            if ( '' !== $profile_tax_id ) {
                $profile_legal_parts[] = sprintf(
                    /* translators: %s: tax id */
                    __( 'VAT: %s', 'noesia-smart-waiter' ),
                    $profile_tax_id
                );
            }
            if ( '' !== $profile_reg_no ) {
                $profile_legal_parts[] = sprintf(
                    /* translators: %s: registration number */
                    __( 'Reg: %s', 'noesia-smart-waiter' ),
                    $profile_reg_no
                );
            }
            if ( '' !== $profile_address ) {
                $profile_legal_parts[] = $profile_address;
            }

            $profile_legal_text = ! empty( $profile_legal_parts )
                ? implode( ' · ', $profile_legal_parts )
                : __( 'Not set yet', 'noesia-smart-waiter' );

            $weather_bundle     = $this->get_weather_bundle( $options );
            $world_my_world     = $this->build_world_my_world( $weather_bundle, $dashboard_lang );
            $world_live_enabled = $this->is_world_live_data_enabled( $options );
            $world_sky_watch    = $this->get_world_sky_watch( $weather_bundle );

            if ( $world_live_enabled ) {
                $world_earthquakes = $this->get_world_earthquake_snapshot( false );
                $world_events      = $this->get_world_events_snapshot( false );
                $world_iss         = $this->get_world_iss_snapshot( false );
                $world_window      = $this->get_world_window( $world_iss );
                $world_clocks      = $this->get_world_clocks( $options, false );
                $world_map_points  = $this->get_world_map_points( $weather_bundle, $world_my_world, $world_earthquakes, $world_events, $world_iss, $world_clocks, $options );
                $world_live_stream = $this->get_world_live_stream( $world_earthquakes, $world_events, $world_iss );
            } else {
                $world_earthquakes = $this->get_empty_world_earthquake_snapshot();
                $world_events      = $this->get_empty_world_events_snapshot();
                $world_iss         = $this->get_empty_world_iss_snapshot();
                $world_window      = $this->get_world_window( $world_iss );
                $world_clocks      = array();
                $world_map_points  = array();
                $world_live_stream = array();
            }
            $today_ymd      = (string) current_time( 'Y-m-d' );

            $today_appointments_query = new WP_Query(
                array(
                    'post_type'      => 'noesia_appointment',
                    'post_status'    => 'publish',
                    'posts_per_page' => 1,
                    'fields'         => 'ids',
                    'meta_query'     => array(
                        array(
                            'key'   => 'noesia_appt_date',
                            'value' => $today_ymd,
                        ),
                        array(
                            'key'     => 'noesia_appt_status',
                            'value'   => 'cancelled',
                            'compare' => '!=',
                        ),
                    ),
                )
            );

            $dashboard_today_appointments = (int) $today_appointments_query->found_posts;
            wp_reset_postdata();

            $briefing_schedule_count = (string) $dashboard_today_appointments;
            $briefing_schedule_label = ( 1 === $dashboard_today_appointments )
                ? __( 'Appointment today', 'noesia-smart-waiter' )
                : __( 'Appointments today', 'noesia-smart-waiter' );

            $dashboard_active_badge = array(
                'type'    => 'requests',
                'visible' => true,
                'count'   => $dashboard_active_requests,
                'label'   => __( 'Active Requests', 'noesia-smart-waiter' ),
                'text'    => sprintf(
                    /* translators: %d: active requests count */
                    _n( '%d Active Request', '%d Active Requests', $dashboard_active_requests, 'noesia-smart-waiter' ),
                    $dashboard_active_requests
                ),
                'target'  => 'design',
            );

            if ( class_exists( 'Noesia_SWA_Settings_Shell_Data' ) ) {
                $dashboard_shell_data_service = new Noesia_SWA_Settings_Shell_Data();
                $dashboard_active_badge       = $dashboard_shell_data_service->get_active_badge_context( $options, $dashboard_active_requests );
            }

                 $business_pulse_snapshot = $this->build_weather_business_pulse(
    $weather_bundle,
    $dashboard_lang,
    $dashboard_active_requests,
    $dashboard_today_appointments
);

            $business_pulse_title = isset( $business_pulse_snapshot['title'] )
                ? (string) $business_pulse_snapshot['title']
                : $this->get_dashboard_phrase( 'steady_service_window', $dashboard_lang );

            $business_pulse_text = isset( $business_pulse_snapshot['text'] )
                ? (string) $business_pulse_snapshot['text']
                : $this->get_dashboard_phrase( 'steady_service_window_text', $dashboard_lang );

            $dashboard_system_health = ! empty( $weather_bundle['error'] )
                ? $this->get_dashboard_phrase( 'online_weather_pending', $dashboard_lang )
                : $this->get_dashboard_phrase( 'online_ready', $dashboard_lang );

            if ( $dashboard_active_requests >= 5 ) {
                $dashboard_activity_status = $this->get_dashboard_phrase( 'activity_peak_busy', $dashboard_lang );
            } elseif ( $dashboard_active_requests > 0 ) {
                $dashboard_activity_status = $this->get_dashboard_phrase( 'activity_active_serving', $dashboard_lang );
            } elseif ( $current_hour >= 12 && $current_hour <= 15 ) {
                $dashboard_activity_status = $this->get_dashboard_phrase( 'activity_lunch_ready', $dashboard_lang );
            } elseif ( $current_hour >= 18 && $current_hour <= 22 ) {
                $dashboard_activity_status = $this->get_dashboard_phrase( 'activity_dinner_ready', $dashboard_lang );
            } else {
                $dashboard_activity_status = $this->get_dashboard_phrase( 'activity_quiet_open', $dashboard_lang );
            }

            $dashboard_orders_status = (string) (int) $dashboard_active_requests;

            return array(
                'news_language'                        => $news_language,
                'dashboard_language'                   => $dashboard_lang,
                'news_country'                         => $news_country,
                'host_name'                            => $host_name,
                'business_name'                        => $business_name,
                'business_type'                        => $business_type,
                'profile_address'                      => $profile_address,
                'profile_phone'                        => $profile_phone,
                'profile_email'                        => $profile_email,
                'profile_website'                      => $profile_website,
                'profile_tax_id'                       => $profile_tax_id,
                'profile_reg_no'                       => $profile_reg_no,
                'briefing_enabled'                     => $briefing_enabled,
                'briefing_updates'                     => $briefing_updates,
                'briefing_schedule'                    => $briefing_schedule,
                'current_hour'                         => $current_hour,
                'greeting_text'                        => $greeting_text,
                'today_requests_count'                 => $today_requests_count,
                'briefing_updates_text'                => $briefing_updates_text,
                'briefing_schedule_count'              => $briefing_schedule_count,
                'briefing_schedule_label'              => $briefing_schedule_label,
                'profile_business_text'                => $profile_business_text,
                'profile_contact_text'                 => $profile_contact_text,
                'profile_legal_text'                   => $profile_legal_text,
                'weather_bundle'                       => $weather_bundle,
                'forecast_days'                        => $this->get_forecast_days( $weather_bundle ),
                'world_live_enabled'                   => $world_live_enabled,
                'world_my_world'                       => $world_my_world,
                'world_earthquakes'                    => $world_earthquakes,
                'world_events'                         => $world_events,
                'world_iss'                            => $world_iss,
                'world_window'                         => $world_window,
                'world_sky_watch'                      => $world_sky_watch,
                'world_clocks'                         => $world_clocks,
                'world_map_points'                     => $world_map_points,
                'world_live_stream'                    => $world_live_stream,
                'dashboard_active_requests'            => $dashboard_active_requests,
                'dashboard_today_appointments'         => $dashboard_today_appointments,
                'dashboard_active_badge_type'          => isset( $dashboard_active_badge['type'] ) ? (string) $dashboard_active_badge['type'] : 'requests',
                'dashboard_active_badge_visible'       => ! empty( $dashboard_active_badge['visible'] ),
                'dashboard_active_badge_count'         => isset( $dashboard_active_badge['count'] ) ? (int) $dashboard_active_badge['count'] : 0,
                'dashboard_active_badge_label'         => isset( $dashboard_active_badge['label'] ) ? (string) $dashboard_active_badge['label'] : __( 'Active Requests', 'noesia-smart-waiter' ),
                'dashboard_active_badge_text'          => isset( $dashboard_active_badge['text'] ) ? (string) $dashboard_active_badge['text'] : '',
                'dashboard_active_badge_target'        => isset( $dashboard_active_badge['target'] ) ? (string) $dashboard_active_badge['target'] : 'design',
                'business_pulse_title'                 => $business_pulse_title,
                'business_pulse_text'                  => $business_pulse_text,
                'business_pulse_kicker'                => $this->get_dashboard_phrase( 'smart_ai_alert', $dashboard_lang ),
                'business_pulse_heading'               => $this->get_dashboard_phrase( 'business_pulse', $dashboard_lang ),
                'business_pulse_status_label'          => $this->get_dashboard_phrase( 'live_operational_status', $dashboard_lang ),
                'business_pulse_intro'                 => $this->get_dashboard_phrase( 'business_pulse_intro', $dashboard_lang ),
                'business_pulse_local_conditions_label'=> $this->get_dashboard_phrase( 'local_conditions', $dashboard_lang ),
                'business_pulse_system_health_label'   => $this->get_dashboard_phrase( 'system_health', $dashboard_lang ),
                'business_pulse_daily_activity_label'  => $this->get_dashboard_phrase( 'daily_activity', $dashboard_lang ),
                'business_pulse_live_orders_label'     => $this->get_dashboard_phrase( 'live_orders', $dashboard_lang ),
                'dashboard_system_health'              => $dashboard_system_health,
                'dashboard_activity_status'            => $dashboard_activity_status,
                'dashboard_orders_status'              => $dashboard_orders_status,
                'global_pulse_kicker'                  => $this->get_dashboard_phrase( 'global_pulse_kicker', $dashboard_lang ),
                'global_pulse_heading'                 => $this->get_dashboard_phrase( 'global_pulse_heading', $dashboard_lang ),
                'global_pulse_next_label'              => $this->get_dashboard_phrase( 'global_pulse_next_label', $dashboard_lang ),
                'global_pulse_intro'                   => $this->get_dashboard_phrase( 'global_pulse_intro', $dashboard_lang ),
                'dashboard_next_notable'               => $this->get_next_notable_day( $options ),
            );
        }
    }
}
