<?php
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value,WordPress.DB.SlowDBQuery.slow_db_query_meta_query,WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- NexDirectory uses trusted WordPress/custom table names and context-specific custom queries; user-controlled values are sanitized or prepared before use.

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

trait NEXDIRECTORY_Frontend_Trait {
    // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This trait normalizes dynamic submit/update/auth fields through context-specific validators, including file and password fields that cannot use a generic text sanitizer.
    // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only query-string routing, prefill, PWA asset, and token verification flows do not mutate data; mutating handlers use explicit nonces.

    /**
     * Returns a complete, sanitized saved-filter payload.
     *
     * @param int $user_id
     * @return array<string, string>
     */
    private function get_saved_filter_values($user_id) {
        $defaults = [
            's' => '',
            'event_type' => '',
            'event_mode' => '',
            'event_price' => '',
            'event_tag' => '',
            'city' => '',
            'date_from' => '',
            'official' => '',
        ];
        $stored = get_user_meta(absint((int) $user_id), 'nexdirectory_saved_filters', true);
        if (!is_array($stored)) {
            return $defaults;
        }

        $saved = [];
        foreach ($defaults as $key => $fallback) {
            $value = $stored[$key] ?? $fallback;
            $saved[$key] = is_scalar($value) ? sanitize_text_field((string) $value) : '';
        }
        if (!in_array($saved['official'], ['0', '1'], true)) {
            $saved['official'] = '';
        }
        return $saved;
    }

    /**
     * Limits public submission and account-form attempts to five per ten minutes.
     *
     * @return bool
     */
    private function is_submission_rate_limited() {
        if (current_user_can('manage_options') || $this->user_can_moderate_events()) {
            return false;
        }

        $user_id = absint((int) get_current_user_id());
        $remote_addr = sanitize_text_field((string) wp_unslash($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
        $identity = $user_id > 0 ? 'user:' . $user_id : 'ip:' . $remote_addr;
        $key = 'nexdirectory_submission_rate_' . md5($identity);
        $attempts = (int) get_transient($key);
        if ($attempts >= 5) {
            return true;
        }
        set_transient($key, (string) ($attempts + 1), 10 * MINUTE_IN_SECONDS);
        return false;
    }

    /**
     * Records a blocked spam attempt for platform metrics.
     *
     * @return void
     */
    private function increment_spam_counter() {
        $count = max(0, (int) get_option('nexdirectory_spam_blocks_count', 0));
        update_option('nexdirectory_spam_blocks_count', $count + 1, false);
    }

    /**
     * Detects obvious promotional or link-based signup payloads.
     *
     * @param string $value
     * @return bool
     */
    private function suspicious_signup_content($value) {
        $value = strtolower(trim((string) $value));
        if ($value === '') {
            return false;
        }
        foreach (['http://', 'https://', '.com', 'bonus', 'usd', 'casino', 'crypto', 'promo', 'xbet'] as $marker) {
            if (strpos($value, $marker) !== false) {
                return true;
            }
        }
        return false;
    }

    /**
     * Normalizes design colors with a safe fallback.
     *
     * @param string $value
     * @param string $default
     * @return string
     */
    private function sanitize_design_color($value, $default = '#FF5E14') {
        $color = sanitize_hex_color((string) $value);
        if (is_string($color) && $color !== '') {
            return $color;
        }
        $fallback = sanitize_hex_color((string) $default);
        return is_string($fallback) && $fallback !== '' ? $fallback : '#FF5E14';
    }

    /**
     * Adds safe relationship tokens to external HTTP(S) links.
     *
     * @param string $html
     * @return string
     */
    private function add_nofollow_to_external_links($html) {
        $html = (string) $html;
        if ($html === '' || stripos($html, '<a') === false) {
            return $html;
        }

        $home_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        $home_host = (string) preg_replace('/^www\./i', '', $home_host);
        $is_external_http = static function ($href) use ($home_host) {
            $href = trim((string) $href);
            $scheme = strtolower((string) wp_parse_url($href, PHP_URL_SCHEME));
            if ($scheme === '' && strpos($href, '//') === 0) {
                $scheme = 'https';
            }
            if (!in_array($scheme, ['http', 'https'], true)) {
                return false;
            }
            $host = strtolower((string) wp_parse_url($href, PHP_URL_HOST));
            $host = (string) preg_replace('/^www\./i', '', $host);
            return $host !== '' && $host !== $home_host;
        };
        $merge_rel = static function ($value) {
            $tokens = array_values(array_filter(preg_split('/\s+/', trim((string) $value)) ?: []));
            $normalized = array_map('strtolower', $tokens);
            foreach (['nofollow', 'noopener', 'noreferrer', 'ugc'] as $required) {
                if (!in_array($required, $normalized, true)) {
                    $tokens[] = $required;
                    $normalized[] = $required;
                }
            }
            return implode(' ', $tokens);
        };

        if (class_exists('WP_HTML_Tag_Processor')) {
            $processor = new WP_HTML_Tag_Processor($html);
            while ($processor->next_tag('A')) {
                $href = $processor->get_attribute('href');
                if (!is_string($href) || !$is_external_http($href)) {
                    continue;
                }
                $processor->set_attribute('rel', $merge_rel((string) $processor->get_attribute('rel')));
            }
            return $processor->get_updated_html();
        }

        $updated = preg_replace_callback('/<a\b[^>]*>/i', static function ($match) use ($is_external_http, $merge_rel) {
            $tag = (string) ($match[0] ?? '');
            if (!preg_match('/\shref\s*=\s*(["\'])(.*?)\1/i', $tag, $href_match) || !$is_external_http((string) ($href_match[2] ?? ''))) {
                return $tag;
            }
            if (preg_match('/\srel\s*=\s*(["\'])(.*?)\1/i', $tag)) {
                return (string) preg_replace_callback(
                    '/(\srel\s*=\s*)(["\'])(.*?)\2/i',
                    static function ($rel_match) use ($merge_rel) {
                        return (string) $rel_match[1] . (string) $rel_match[2] . esc_attr($merge_rel((string) $rel_match[3])) . (string) $rel_match[2];
                    },
                    $tag,
                    1
                );
            }
            $trimmed = rtrim($tag);
            $closing = substr($trimmed, -2) === '/>' ? '/>' : '>';
            return substr($trimmed, 0, -strlen($closing)) . ' rel="nofollow noopener noreferrer ugc"' . $closing;
        }, $html);
        return is_string($updated) ? $updated : $html;
    }

    public function render_events_archive($atts = []) {
        $atts = shortcode_atts([
            'posts_per_page' => 12,
            'show_sidebar' => 'yes',
        ], (array) $atts, 'nexdirectory_events_archive');
        $posts_per_page = max(1, (int) $atts['posts_per_page']);
        $show_sidebar = $atts['show_sidebar'] !== 'no';
        $ui_labels = $this->get_site_profile_ui_labels();
        $profile_key = sanitize_key((string) ($ui_labels['profile_key'] ?? 'events'));
        $is_events_profile = $profile_key === 'events';
        $is_job_profile = $profile_key === 'job_board';
        $show_price_filter = !$is_job_profile;
        $show_official_filter = $is_events_profile;
        $resolve_archive_visibility = static function ($option_key, $default) {
            return (string) get_option($option_key, $default ? '1' : '0') === '1';
        };
        $show_search_filter = $resolve_archive_visibility('nexdirectory_map_filter_show_search', true);
        $show_type_filter = $resolve_archive_visibility('nexdirectory_map_filter_show_type', true);
        $show_mode_filter = $resolve_archive_visibility('nexdirectory_map_filter_show_mode', false);
        $show_tag_filter = $resolve_archive_visibility('nexdirectory_map_filter_show_tag', false);
        $show_price_filter = $show_price_filter && $resolve_archive_visibility('nexdirectory_map_filter_show_price', true);
        $show_official_filter = $show_official_filter && $resolve_archive_visibility('nexdirectory_map_filter_show_official', false);
        $show_date_filter = $resolve_archive_visibility('nexdirectory_map_filter_show_date', true);

        $saved = is_user_logged_in() ? $this->get_saved_filter_values(get_current_user_id()) : [
            's' => '',
            'event_type' => '',
            'event_mode' => '',
            'event_price' => '',
            'event_tag' => '',
            'city' => '',
            'date_from' => '',
            'official' => '',
        ];
        $use_saved = isset($_GET['nexdirectory_use_saved']) && sanitize_key((string) wp_unslash($_GET['nexdirectory_use_saved'])) === '1';
        $organizer_id = isset($_GET['organizer']) ? absint((int) wp_unslash($_GET['organizer'])) : (isset($_GET['author']) ? absint((int) wp_unslash($_GET['author'])) : 0);
        $type = isset($_GET['event_type']) ? sanitize_text_field((string) wp_unslash($_GET['event_type'])) : ($use_saved ? $saved['event_type'] : '');
        $mode = isset($_GET['event_mode']) ? sanitize_text_field((string) wp_unslash($_GET['event_mode'])) : ($use_saved ? $saved['event_mode'] : '');
        $price_range = isset($_GET['event_price']) ? sanitize_text_field((string) wp_unslash($_GET['event_price'])) : ($use_saved ? $saved['event_price'] : '');
        $tag = isset($_GET['event_tag']) ? sanitize_text_field((string) wp_unslash($_GET['event_tag'])) : ($use_saved ? $saved['event_tag'] : '');
        $city = isset($_GET['city']) ? sanitize_text_field((string) wp_unslash($_GET['city'])) : ($use_saved ? $saved['city'] : '');
        $date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : ($use_saved ? $saved['date_from'] : '');
        $official = isset($_GET['official']) ? sanitize_text_field((string) wp_unslash($_GET['official'])) : ($use_saved ? ($saved['official'] ?? '') : '');
        $archive_search_query = get_query_var('nexdirectory_search', '');
        if (isset($_GET['nexdirectory_search'])) {
            $archive_search_query = wp_unslash($_GET['nexdirectory_search']);
        } elseif ($archive_search_query === '' && isset($_GET['s'])) {
            // Backward compatibility for links produced before the archive used
            // its own query key. The request filter keeps these on the page.
            $archive_search_query = wp_unslash($_GET['s']);
        }
        $search = $archive_search_query !== ''
            ? sanitize_text_field((string) $archive_search_query)
            : ($use_saved ? $saved['s'] : '');
        $archive_orderby = isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : 'meta_value';
        if (!in_array($archive_orderby, ['meta_value', 'date'], true)) {
            $archive_orderby = 'meta_value';
        }
        $paged = max(1, get_query_var('paged'), isset($_GET['paged']) ? absint((int) wp_unslash($_GET['paged'])) : 1);
        $filters = [
            's' => $search,
            'event_type' => $type,
            'event_mode' => $mode,
            'event_price' => $price_range,
            'event_tag' => $tag,
            'city' => $city,
            'date_from' => $date_from,
            'official' => in_array($official, ['0', '1'], true) ? $official : '',
            'orderby' => $archive_orderby === 'date' ? 'date' : '',
            'nex_filter' => method_exists($this, 'parse_builder_checkin_dynamic_filters')
                ? $this->parse_builder_checkin_dynamic_filters($_GET, 'archive') // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only public archive filters.
                : [],
        ];
        $has_active_filters = false;
        foreach ($filters as $filter_value) {
            if (
                (is_array($filter_value) && !empty($filter_value))
                || (!is_array($filter_value) && trim((string) $filter_value) !== '')
            ) {
                $has_active_filters = true;
                break;
            }
        }
        $query = $this->build_archive_query($filters, $posts_per_page, $paged, $organizer_id);

        if ($has_active_filters) {
            $post_ids_for_filter_options = [];
            foreach ((array) $query->posts as $post_item) {
                $candidate_id = is_object($post_item) ? (int) ($post_item->ID ?? 0) : (int) $post_item;
                if ($candidate_id > 0) {
                    $post_ids_for_filter_options[] = $candidate_id;
                }
            }
            $post_ids_for_filter_options = array_values(array_unique($post_ids_for_filter_options));
            $filter_options = $this->build_archive_filter_options($post_ids_for_filter_options, $filters);
        } else {
            $filter_options = $this->build_global_archive_filter_options($filters);
        }

        return $this->render_frontend_template('events-archive.php', [
            'show_sidebar' => $show_sidebar,
            'organizer_id' => $organizer_id,
            'posts_per_page' => $posts_per_page,
            'search' => $search,
            'type' => $type,
            'mode' => $mode,
            'price_range' => $price_range,
            'tag' => $tag,
            'city' => $city,
            'date_from' => $date_from,
            'paged' => $paged,
            'filters' => $filters,
            'has_active_filters' => $has_active_filters,
            'filter_options' => $filter_options,
            'query' => $query,
            'official' => in_array($official, ['0', '1'], true) ? $official : '',
            'show_search_filter' => $show_search_filter,
            'show_type_filter' => $show_type_filter,
            'show_mode_filter' => $show_mode_filter,
            'show_tag_filter' => $show_tag_filter,
            'show_date_filter' => $show_date_filter,
            'show_price_filter' => $show_price_filter,
            'show_official_filter' => $show_official_filter,
            'ui_labels' => $ui_labels,
        ]);
    }


    public function render_events_map($atts = []) {
        static $map_rendered = false;
        if ($map_rendered) {
            return '';
        }
        $map_rendered = true;

        $atts = shortcode_atts([
            'max_events' => 200,
            'show_sidebar' => 'yes',
            'show_search' => '',
            'show_type' => '',
            'show_mode' => '',
            'show_tag' => '',
            'show_price' => '',
            'show_official' => '',
            'show_date' => '',
            'show_distance' => '',
            'show_list' => 'yes',
            'map_height' => 620,
            'view' => '',
            'layout' => '',
            'density' => '',
            'mobile_filters' => '',
        ], (array) $atts, 'nexdirectory_events_map');
        $max_events = max(12, (int) $atts['max_events']);
        $show_sidebar = $atts['show_sidebar'] !== 'no';
        $resolve_filter_visibility = static function ($shortcode_value, $option_key, $default) {
            $global_enabled = (string) get_option($option_key, $default ? '1' : '0') === '1';
            if (!$global_enabled) {
                return false;
            }
            if ($shortcode_value === '' || $shortcode_value === null) {
                return true;
            }
            $value = strtolower(trim((string) $shortcode_value));
            if (in_array($value, ['no', '0', 'false', 'off'], true)) {
                return false;
            }
            if (in_array($value, ['yes', '1', 'true', 'on'], true)) {
                return true;
            }
            return true;
        };
        $show_search = $resolve_filter_visibility($atts['show_search'], 'nexdirectory_map_filter_show_search', true);
        $show_type = $resolve_filter_visibility($atts['show_type'], 'nexdirectory_map_filter_show_type', true);
        $show_mode = $resolve_filter_visibility($atts['show_mode'], 'nexdirectory_map_filter_show_mode', false);
        $show_tag = $resolve_filter_visibility($atts['show_tag'], 'nexdirectory_map_filter_show_tag', false);
        $show_price = $resolve_filter_visibility($atts['show_price'], 'nexdirectory_map_filter_show_price', true);
        $show_official = $resolve_filter_visibility($atts['show_official'], 'nexdirectory_map_filter_show_official', false);
        $show_date = $resolve_filter_visibility($atts['show_date'], 'nexdirectory_map_filter_show_date', true);
        $show_distance = $resolve_filter_visibility($atts['show_distance'], 'nexdirectory_map_filter_show_distance', true);
        $show_list = $atts['show_list'] !== 'no';
        $map_height = max(320, min(1200, (int) $atts['map_height']));
        $default_view = sanitize_key((string) get_option('nexdirectory_map_view_template', 'default'));
        if (!in_array($default_view, ['default', 'triple'], true)) {
            $default_view = 'default';
        }
        $requested_view = sanitize_key((string) $atts['view']);
        $map_view = in_array($requested_view, ['default', 'triple'], true) ? $requested_view : $default_view;
        $allowed_layouts = ['three-column', 'below-list', 'below-grid', 'below-grid-large', 'grid-no-map', 'grid-no-map-large'];
        $default_layout = sanitize_key((string) get_option('nexdirectory_map_layout_default', 'three-column'));
        if (!in_array($default_layout, $allowed_layouts, true)) {
            $default_layout = 'three-column';
        }
        $layout = $atts['layout'] !== '' ? sanitize_key((string) $atts['layout']) : $default_layout;
        if (!in_array($layout, $allowed_layouts, true)) {
            $layout = $default_layout;
        }
        $default_density = sanitize_key((string) get_option('nexdirectory_map_results_density', 'comfortable'));
        if (!in_array($default_density, ['comfortable', 'compact'], true)) {
            $default_density = 'comfortable';
        }
        $density = $atts['density'] !== '' ? sanitize_key((string) $atts['density']) : $default_density;
        if (!in_array($density, ['comfortable', 'compact'], true)) {
            $density = $default_density;
        }
        $default_mobile_filters = sanitize_key((string) get_option('nexdirectory_map_mobile_filter_mode', 'drawer'));
        if (!in_array($default_mobile_filters, ['drawer', 'inline'], true)) {
            $default_mobile_filters = 'drawer';
        }
        $mobile_filters = $atts['mobile_filters'] !== '' ? sanitize_key((string) $atts['mobile_filters']) : $default_mobile_filters;
        if (!in_array($mobile_filters, ['drawer', 'inline'], true)) {
            $mobile_filters = $default_mobile_filters;
        }
        if (!$show_list && in_array($layout, ['grid-no-map', 'grid-no-map-large'], true)) {
            $layout = 'below-list';
        }
        $ui_labels = $this->get_site_profile_ui_labels();
        $profile_key = sanitize_key((string) ($ui_labels['profile_key'] ?? 'events'));
        $is_events_profile = $profile_key === 'events';
        $is_job_profile = $profile_key === 'job_board';
        if ($is_job_profile) {
            $show_price = false;
        }
        if (!$is_events_profile) {
            $show_official = false;
        }

        wp_enqueue_style('nexdirectory-leaflet');
        wp_enqueue_style('nexdirectory-leaflet-markercluster-default');
        wp_enqueue_script('nexdirectory-leaflet');
        wp_enqueue_script('nexdirectory-leaflet-markercluster');

        $template_file = $map_view === 'triple' ? 'MapPage-tripleColumn.php' : 'events-map.php';
        $template_path = plugin_dir_path(NEXDIRECTORY_PLUGIN_FILE) . 'templates/frontend/' . $template_file;
        if (!is_readable($template_path)) {
            $template_file = 'events-map.php';
        }

        return $this->render_frontend_template($template_file, [
            'max_events' => $max_events,
            'show_sidebar' => $show_sidebar,
            'show_search' => $show_search,
            'show_type' => $show_type,
            'show_mode' => $show_mode,
            'show_tag' => $show_tag,
            'show_price' => $show_price,
            'show_official' => $show_official,
            'show_date' => $show_date,
            'show_distance' => $show_distance,
            'show_list' => $show_list,
            'map_height' => $map_height,
            'layout' => $layout,
            'density' => $density,
            'map_view' => $map_view,
            'mobile_filter_mode' => $mobile_filters,
            'ui_labels' => $ui_labels,
        ]);
    }    private function sanitize_widget_template($value, $fallback = '1') {
        $allowed = ['1', '2', '3', '4', '5', '6'];
        $value = sanitize_key((string) $value);
        if (in_array($value, $allowed, true)) {
            return $value;
        }
        return in_array((string) $fallback, $allowed, true) ? (string) $fallback : '1';
    }


    private function sanitize_widget_yes_no($value, $default = 'yes') {
        $value = strtolower(trim((string) $value));
        if (in_array($value, ['yes', '1', 'true', 'on'], true)) {
            return 'yes';
        }
        if (in_array($value, ['no', '0', 'false', 'off'], true)) {
            return 'no';
        }
        return strtolower(trim((string) $default)) === 'no' ? 'no' : 'yes';
    }


    private function parse_widget_slug_terms($raw) {
        $values = $this->parse_delimited_values($raw);
        $slugs = [];
        foreach ((array) $values as $value) {
            $slug = nexdirectory_sanitize_title((string) $value);
            if ($slug !== '') {
                $slugs[] = $slug;
            }
        }
        return array_values(array_unique($slugs));
    }


    private function sanitize_widget_custom_class($raw) {
        $items = preg_split('/\s+/', (string) $raw);
        if (!is_array($items)) {
            return '';
        }
        $classes = [];
        foreach ($items as $item) {
            $class_name = sanitize_html_class((string) $item);
            if ($class_name !== '') {
                $classes[] = $class_name;
            }
        }
        return implode(' ', array_values(array_unique($classes)));
    }


    private function sanitize_widget_custom_css($raw) {
        $css = trim((string) $raw);
        if ($css === '') {
            return '';
        }
        $css = str_ireplace(['<st' . 'yle', '</st' . 'yle'], '', $css);
        $css = str_replace(['<', '>'], '', $css);
        $css = preg_replace('/@import[^;]+;?/i', '', $css);
        return trim((string) $css);
    }


    private function build_widget_style_vars(array $style_config = []) {
        $style_bits = [];

        $accent = sanitize_hex_color((string) ($style_config['accent_color'] ?? ''));
        if ($accent) {
            $style_bits[] = '--nexdirectory-widget-accent:' . $accent;
        }
        $bg_color = sanitize_hex_color((string) ($style_config['bg_color'] ?? ''));
        if ($bg_color) {
            $style_bits[] = '--nexdirectory-widget-card-bg:' . $bg_color;
        }
        $text_color = sanitize_hex_color((string) ($style_config['text_color'] ?? ''));
        if ($text_color) {
            $style_bits[] = '--nexdirectory-widget-card-text:' . $text_color;
        }

        $radius = absint((int) ($style_config['radius'] ?? 0));
        if ($radius > 0) {
            $style_bits[] = '--nexdirectory-widget-card-radius:' . min(48, $radius) . 'px';
        }
        $gap = absint((int) ($style_config['gap'] ?? 0));
        if ($gap > 0) {
            $style_bits[] = '--nexdirectory-widget-gap:' . min(64, $gap) . 'px';
        }
        $image_height = absint((int) ($style_config['image_height'] ?? 0));
        if ($image_height > 0) {
            $style_bits[] = '--nexdirectory-widget-image-height:' . min(520, $image_height) . 'px';
        }
        $columns_desktop = absint((int) ($style_config['columns_desktop'] ?? 0));
        if ($columns_desktop > 0) {
            $style_bits[] = '--nexdirectory-widget-cols-desktop:' . max(1, min(6, $columns_desktop));
        }
        $columns_tablet = absint((int) ($style_config['columns_tablet'] ?? 0));
        if ($columns_tablet > 0) {
            $style_bits[] = '--nexdirectory-widget-cols-tablet:' . max(1, min(4, $columns_tablet));
        }
        $columns_mobile = absint((int) ($style_config['columns_mobile'] ?? 0));
        if ($columns_mobile > 0) {
            $style_bits[] = '--nexdirectory-widget-cols-mobile:' . max(1, min(2, $columns_mobile));
        }

        return empty($style_bits) ? '' : implode(';', $style_bits);
    }


    private function build_widget_query_config(array $atts = []) {
        $limit = max(1, min(60, absint((int) ($atts['limit'] ?? 6))));
        $search = sanitize_text_field((string) ($atts['search'] ?? ''));
        $city = sanitize_text_field((string) ($atts['city'] ?? ''));
        $date_from = sanitize_text_field((string) ($atts['date_from'] ?? ''));
        $official = sanitize_text_field((string) ($atts['official'] ?? ''));
        if (!in_array($official, ['0', '1'], true)) {
            $official = '';
        }

        $include_past = $this->sanitize_widget_yes_no($atts['include_past'] ?? 'no', 'no') === 'yes';
        if (!$include_past && $date_from === '') {
            $date_from = current_time('Y-m-d');
        }

        $orderby = sanitize_key((string) ($atts['orderby'] ?? 'start'));
        if (!in_array($orderby, ['start', 'date', 'title', 'modified', 'rand'], true)) {
            $orderby = 'start';
        }
        $order = strtoupper(sanitize_text_field((string) ($atts['order'] ?? 'ASC')));
        if (!in_array($order, ['ASC', 'DESC'], true)) {
            $order = 'ASC';
        }
        $tax_relation = strtoupper(sanitize_text_field((string) ($atts['tax_relation'] ?? 'AND')));
        if (!in_array($tax_relation, ['AND', 'OR'], true)) {
            $tax_relation = 'AND';
        }

        $custom_taxonomy = sanitize_key((string) ($atts['custom_taxonomy'] ?? ''));
        if ($custom_taxonomy !== '' && !taxonomy_exists($custom_taxonomy)) {
            $custom_taxonomy = '';
        }

        return [
            'limit' => $limit,
            'search' => $search,
            'city' => $city,
            'date_from' => $date_from,
            'official' => $official,
            'include_past' => $include_past,
            'orderby' => $orderby,
            'order' => $order,
            'tax_relation' => $tax_relation,
            'event_type_terms' => $this->parse_widget_slug_terms($atts['event_type'] ?? ''),
            'event_mode_terms' => $this->parse_widget_slug_terms($atts['event_mode'] ?? ''),
            'event_price_terms' => $this->parse_widget_slug_terms($atts['event_price'] ?? ''),
            'event_tag_terms' => $this->parse_widget_slug_terms($atts['event_tag'] ?? ''),
            'custom_taxonomy' => $custom_taxonomy,
            'custom_terms' => $this->parse_widget_slug_terms($atts['custom_terms'] ?? ''),
        ];
    }


    private function build_widget_events_query(array $query_config = []) {
        $tax_query = [];
        $add_tax_terms = static function (&$bucket, $taxonomy, $terms) {
            if (!taxonomy_exists((string) $taxonomy)) {
                return;
            }
            if (!is_array($terms) || empty($terms)) {
                return;
            }
            $bucket[] = [
                'taxonomy' => (string) $taxonomy,
                'field' => 'slug',
                'terms' => array_values(array_unique(array_filter(array_map('nexdirectory_sanitize_title', $terms)))),
            ];
        };

        $add_tax_terms($tax_query, self::TAX_TYPE, $query_config['event_type_terms'] ?? []);
        $add_tax_terms($tax_query, self::TAX_MODE, $query_config['event_mode_terms'] ?? []);
        $add_tax_terms($tax_query, self::TAX_PRICE, $query_config['event_price_terms'] ?? []);
        $add_tax_terms($tax_query, self::TAX_TAG, $query_config['event_tag_terms'] ?? []);

        $custom_taxonomy = sanitize_key((string) ($query_config['custom_taxonomy'] ?? ''));
        if ($custom_taxonomy !== '') {
            $add_tax_terms($tax_query, $custom_taxonomy, $query_config['custom_terms'] ?? []);
        }

        if (count($tax_query) > 1) {
            $tax_query = array_merge([
                'relation' => in_array((string) ($query_config['tax_relation'] ?? 'AND'), ['AND', 'OR'], true)
                    ? (string) $query_config['tax_relation']
                    : 'AND',
            ], $tax_query);
        }

        $meta_query = [];
        $city = sanitize_text_field((string) ($query_config['city'] ?? ''));
        if ($city !== '') {
            $meta_query[] = [
                'key' => 'nexdirectory_city',
                'value' => $city,
                'compare' => 'LIKE',
            ];
        }

        $date_from = sanitize_text_field((string) ($query_config['date_from'] ?? ''));
        if ($date_from !== '') {
            $meta_query[] = [
                'key' => 'nexdirectory_start',
                'value' => $date_from,
                'compare' => '>=',
                'type' => 'CHAR',
            ];
        }

        $official = sanitize_text_field((string) ($query_config['official'] ?? ''));
        if (in_array($official, ['0', '1'], true)) {
            $meta_query[] = [
                'key' => 'nexdirectory_official_event',
                'value' => $official,
                'compare' => '=',
            ];
        }

        $orderby = sanitize_key((string) ($query_config['orderby'] ?? 'start'));
        $order = in_array((string) ($query_config['order'] ?? 'ASC'), ['ASC', 'DESC'], true)
            ? (string) $query_config['order']
            : 'ASC';

        $query_args = [
            'post_type' => self::POST_TYPE,
            'post_status' => 'publish',
            'posts_per_page' => max(1, min(60, absint((int) ($query_config['limit'] ?? 6)))),
            'ignore_sticky_posts' => true,
            'no_found_rows' => false,
            'suppress_filters' => false,
            's' => sanitize_text_field((string) ($query_config['search'] ?? '')),
            'order' => $order,
        ];

        if (!empty($tax_query)) {
            $query_args['tax_query'] = $tax_query;
        }
        if (!empty($meta_query)) {
            $query_args['meta_query'] = $meta_query;
        }

        switch ($orderby) {
            case 'date':
                $query_args['orderby'] = 'date';
                break;
            case 'title':
                $query_args['orderby'] = 'title';
                break;
            case 'modified':
                $query_args['orderby'] = 'modified';
                break;
            case 'rand':
                $query_args['orderby'] = 'rand';
                unset($query_args['order']);
                break;
            case 'start':
            default:
                $query_args['meta_key'] = 'nexdirectory_start';
                $query_args['orderby'] = 'meta_value';
                break;
        }

        return new WP_Query($query_args);
    }


    private function get_widget_event_location_label($post_id) {
        $city = sanitize_text_field((string) get_post_meta($post_id, 'nexdirectory_city', true));
        $country = sanitize_text_field((string) get_post_meta($post_id, 'nexdirectory_country', true));
        $venue = sanitize_text_field((string) get_post_meta($post_id, 'nexdirectory_venue', true));
        $address = sanitize_text_field((string) get_post_meta($post_id, 'nexdirectory_address', true));

        $parts = [];
        if ($city !== '') {
            $parts[] = $city;
        }
        if ($country !== '') {
            $parts[] = $country;
        }
        if (empty($parts) && $venue !== '') {
            $parts[] = $venue;
        }
        if (empty($parts) && $address !== '') {
            $parts[] = $address;
        }
        return implode(', ', $parts);
    }


    private function get_widget_event_badges($post_id) {
        $badges = [];
        $mode_terms = wp_get_post_terms($post_id, self::TAX_MODE, ['fields' => 'names']);
        $price_terms = wp_get_post_terms($post_id, self::TAX_PRICE, ['fields' => 'names']);
        $type_terms = wp_get_post_terms($post_id, self::TAX_TYPE, ['fields' => 'names']);
        $official_value = (string) get_post_meta($post_id, 'nexdirectory_official_event', true);
        if ($official_value === '1') {
            $classification_labels = (array) $this->get_classification_labels();
            $badges[] = sanitize_text_field((string) ($classification_labels['official'] ?? __('Official', 'nexdirectory')));
        }
        if (!empty($mode_terms) && isset($mode_terms[0])) {
            $badges[] = sanitize_text_field((string) $mode_terms[0]);
        }
        if (!empty($price_terms) && isset($price_terms[0])) {
            $badges[] = sanitize_text_field((string) $price_terms[0]);
        }
        if (!empty($type_terms) && isset($type_terms[0])) {
            $badges[] = sanitize_text_field((string) $type_terms[0]);
        }
        return array_slice(array_values(array_unique(array_filter($badges))), 0, 4);
    }


    private function get_widget_bento_size_class($template, $index) {
        $template = $this->sanitize_widget_template($template, '1');
        $patterns = [
            '1' => ['nexdirectory-bento-item--wide', 'nexdirectory-bento-item--tall', '', '', 'nexdirectory-bento-item--wide', ''],
            '2' => ['nexdirectory-bento-item--large', '', 'nexdirectory-bento-item--wide', '', 'nexdirectory-bento-item--tall', ''],
            '3' => ['nexdirectory-bento-item--tall', 'nexdirectory-bento-item--wide', '', 'nexdirectory-bento-item--large', '', ''],
            '4' => ['nexdirectory-bento-item--large', '', '', 'nexdirectory-bento-item--wide', '', 'nexdirectory-bento-item--tall'],
            '5' => ['nexdirectory-bento-item--wide', '', 'nexdirectory-bento-item--tall', '', 'nexdirectory-bento-item--large', ''],
            '6' => ['nexdirectory-bento-item--tall', '', 'nexdirectory-bento-item--wide', '', '', 'nexdirectory-bento-item--large'],
        ];
        $pattern = isset($patterns[$template]) ? (array) $patterns[$template] : (array) $patterns['1'];
        if (empty($pattern)) {
            return '';
        }
        return (string) $pattern[$index % count($pattern)];
    }


    private function render_widget_event_item($post_id, array $options = []) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return '';
        }
        $view = sanitize_key((string) ($options['view'] ?? 'grid'));
        if (!in_array($view, ['grid', 'bento', 'search'], true)) {
            $view = 'grid';
        }

        $title = get_the_title($post_id);
        $url = get_permalink($post_id);
        $cover = $this->get_event_cover_image_url($post_id, 'medium_large', 'thumbnail');
        $date_label = $this->format_event_date($post_id);
        $location_label = $this->get_widget_event_location_label($post_id);
        $badges = $this->get_widget_event_badges($post_id);
        $template = $this->sanitize_widget_template($options['template'] ?? '1', '1');
        $show_meta = $this->sanitize_widget_yes_no($options['show_meta'] ?? 'yes', 'yes') === 'yes';
        $show_excerpt = $this->sanitize_widget_yes_no($options['show_excerpt'] ?? 'yes', 'yes') === 'yes';
        $excerpt_length = max(8, min(70, absint((int) ($options['excerpt_length'] ?? 20))));
        $excerpt = nexdirectory_strip_all_tags((string) get_the_excerpt($post_id));
        if ($excerpt !== '') {
            $excerpt = wp_trim_words($excerpt, $excerpt_length, '…');
        }

        if ($view === 'search') {
            ob_start();
            ?>
            <article class="nexdirectory-widget-search-item nexdirectory-widget-search-item--template-<?php echo esc_attr($template); ?>" data-nexdirectory-event-id="<?php echo esc_attr((string) $post_id); ?>">
                <a class="nexdirectory-widget-search-link" href="<?php echo esc_url((string) $url); ?>">
                    <span class="nexdirectory-widget-search-thumb-wrap">
                        <?php if ($cover !== '') : ?>
                            <img class="nexdirectory-widget-search-thumb" src="<?php echo esc_url((string) $cover); ?>" alt="<?php echo esc_attr((string) $title); ?>" loading="lazy" decoding="async" fetchpriority="low">
                        <?php else : ?>
                            <span class="nexdirectory-widget-search-thumb nexdirectory-widget-search-thumb--fallback" aria-hidden="true"></span>
                        <?php endif; ?>
                    </span>
                    <span class="nexdirectory-widget-search-body">
                        <strong class="nexdirectory-widget-search-title"><?php echo esc_html((string) $title); ?></strong>
                        <?php if ($show_meta && $date_label !== '') : ?>
                            <span class="nexdirectory-widget-search-meta"><?php echo esc_html((string) $date_label); ?></span>
                        <?php endif; ?>
                        <?php if ($show_meta && $location_label !== '') : ?>
                            <span class="nexdirectory-widget-search-meta"><?php echo esc_html((string) $location_label); ?></span>
                        <?php endif; ?>
                        <?php if (!empty($badges)) : ?>
                            <span class="nexdirectory-widget-search-chips">
                                <?php foreach ($badges as $badge) : ?>
                                    <span class="nexdirectory-widget-chip"><?php echo esc_html((string) $badge); ?></span>
                                <?php endforeach; ?>
                            </span>
                        <?php endif; ?>
                    </span>
                </a>
            </article>
            <?php
            return (string) ob_get_clean();
        }

        $card_classes = [
            'nexdirectory-widget-card',
            'nexdirectory-widget-card--' . $view,
            'nexdirectory-widget-card--template-' . $template,
        ];
        if ($view === 'bento') {
            $card_classes[] = $this->get_widget_bento_size_class($template, absint((int) ($options['index'] ?? 0)));
            $hover_effect = sanitize_key((string) ($options['hover_effect'] ?? 'lift'));
            if (!in_array($hover_effect, ['lift', 'zoom', 'glow', 'tilt'], true)) {
                $hover_effect = 'lift';
            }
            $card_classes[] = 'nexdirectory-widget-hover--' . $hover_effect;
        }
        $card_classes = implode(' ', array_filter(array_map('sanitize_html_class', $card_classes)));

        ob_start();
        ?>
        <article class="<?php echo esc_attr($card_classes); ?>" data-nexdirectory-event-id="<?php echo esc_attr((string) $post_id); ?>">
            <a class="nexdirectory-widget-card-link" href="<?php echo esc_url((string) $url); ?>">
                <span class="nexdirectory-widget-card-media">
                    <?php if ($cover !== '') : ?>
                        <img class="nexdirectory-widget-card-image" src="<?php echo esc_url((string) $cover); ?>" alt="<?php echo esc_attr((string) $title); ?>" loading="lazy" decoding="async" fetchpriority="low">
                    <?php else : ?>
                        <span class="nexdirectory-widget-card-image nexdirectory-widget-card-image--fallback" aria-hidden="true"></span>
                    <?php endif; ?>
                </span>
                <span class="nexdirectory-widget-card-body">
                    <strong class="nexdirectory-widget-card-title"><?php echo esc_html((string) $title); ?></strong>
                    <?php if ($show_meta && $date_label !== '') : ?>
                        <span class="nexdirectory-widget-card-meta"><?php echo esc_html((string) $date_label); ?></span>
                    <?php endif; ?>
                    <?php if ($show_meta && $location_label !== '') : ?>
                        <span class="nexdirectory-widget-card-meta"><?php echo esc_html((string) $location_label); ?></span>
                    <?php endif; ?>
                    <?php if (!empty($badges)) : ?>
                        <span class="nexdirectory-widget-card-chips">
                            <?php foreach ($badges as $badge) : ?>
                                <span class="nexdirectory-widget-chip"><?php echo esc_html((string) $badge); ?></span>
                            <?php endforeach; ?>
                        </span>
                    <?php endif; ?>
                    <?php if ($show_excerpt && $excerpt !== '') : ?>
                        <span class="nexdirectory-widget-card-excerpt"><?php echo esc_html((string) $excerpt); ?></span>
                    <?php endif; ?>
                    <span class="nexdirectory-widget-card-cta"><?php esc_html_e('View Details', 'nexdirectory'); ?></span>
                </span>
            </a>
        </article>
        <?php
        return (string) ob_get_clean();
    }


    private function render_widget_events_markup($query, array $options = []) {
        $view = sanitize_key((string) ($options['view'] ?? 'grid'));
        if (!in_array($view, ['grid', 'bento', 'search'], true)) {
            $view = 'grid';
        }
        $template = $this->sanitize_widget_template($options['template'] ?? '1', '1');
        $wrapper_classes = [
            'nexdirectory-widget-results',
            'nexdirectory-widget-results--' . $view,
            'nexdirectory-widget-results--template-' . $template,
        ];
        if ($view === 'bento') {
            $wrapper_classes[] = 'nexdirectory-widget-bento-grid';
        } elseif ($view === 'search') {
            $wrapper_classes[] = 'nexdirectory-widget-search-results';
        } else {
            $wrapper_classes[] = 'nexdirectory-widget-grid-results';
        }

        $empty_text = sanitize_text_field((string) ($options['empty_text'] ?? __('No matching listings found.', 'nexdirectory')));
        ob_start();
        ?>
        <div class="<?php echo esc_attr(implode(' ', array_map('sanitize_html_class', $wrapper_classes))); ?>">
            <?php if ($query instanceof WP_Query && $query->have_posts()) : ?>
                <?php
                $index = 0;
                while ($query->have_posts()) :
                    $query->the_post();
                    echo wp_kses((string) $this->render_widget_event_item(get_the_ID(), array_merge($options, ['index' => $index])), NEXDIRECTORY_Escaping::allowed_html());
                    $index++;
                endwhile;
                wp_reset_postdata();
                ?>
            <?php else : ?>
                <div class="nexdirectory-widget-empty"><?php echo esc_html((string) $empty_text); ?></div>
            <?php endif; ?>
        </div>
        <?php
        return (string) ob_get_clean();
    }


    public function render_events_query_grid($atts = []) {
        $atts = shortcode_atts([
            'title' => '',
            'subtitle' => '',
            'template' => '1',
            'limit' => 9,
            'orderby' => 'start',
            'order' => 'ASC',
            'search' => '',
            'event_type' => '',
            'event_mode' => '',
            'event_tag' => '',
            'event_price' => '',
            'city' => '',
            'date_from' => '',
            'official' => '',
            'custom_taxonomy' => '',
            'custom_terms' => '',
            'tax_relation' => 'AND',
            'include_past' => 'no',
            'show_excerpt' => 'yes',
            'show_meta' => 'yes',
            'excerpt_length' => 20,
            'columns_desktop' => 3,
            'columns_tablet' => 2,
            'columns_mobile' => 1,
            'gap' => 24,
            'radius' => 18,
            'image_height' => 220,
            'accent_color' => '',
            'bg_color' => '',
            'text_color' => '',
            'custom_class' => '',
            'custom_css' => '',
        ], (array) $atts, 'nexdirectory_events_query_grid');

        $query_config = $this->build_widget_query_config($atts);
        $query = $this->build_widget_events_query($query_config);
        $wrapper_id = sanitize_html_class(wp_unique_id('nexdirectory-widget-grid-'));
        $template = $this->sanitize_widget_template($atts['template'], '1');
        $style_vars = $this->build_widget_style_vars([
            'accent_color' => $atts['accent_color'],
            'bg_color' => $atts['bg_color'],
            'text_color' => $atts['text_color'],
            'radius' => $atts['radius'],
            'gap' => $atts['gap'],
            'image_height' => $atts['image_height'],
            'columns_desktop' => $atts['columns_desktop'],
            'columns_tablet' => $atts['columns_tablet'],
            'columns_mobile' => $atts['columns_mobile'],
        ]);
        $custom_class = $this->sanitize_widget_custom_class($atts['custom_class']);
        $custom_css = $this->sanitize_widget_custom_css($atts['custom_css']);
        $results_markup = $this->render_widget_events_markup($query, [
            'view' => 'grid',
            'template' => $template,
            'show_excerpt' => $atts['show_excerpt'],
            'show_meta' => $atts['show_meta'],
            'excerpt_length' => $atts['excerpt_length'],
            'empty_text' => __('No matching listings found for this grid query.', 'nexdirectory'),
        ]);

        return $this->render_frontend_template('widget-events-grid.php', [
            'wrapper_id' => $wrapper_id,
            'template' => $template,
            'title' => sanitize_text_field((string) $atts['title']),
            'subtitle' => sanitize_text_field((string) $atts['subtitle']),
            'results_markup' => $results_markup,
            'style_vars' => $style_vars,
            'custom_class' => $custom_class,
            'custom_css' => $custom_css,
            'query_count' => $query instanceof WP_Query ? (int) $query->post_count : 0,
        ]);
    }


    public function render_events_bento_grid($atts = []) {
        $atts = shortcode_atts([
            'title' => '',
            'subtitle' => '',
            'template' => '1',
            'limit' => 8,
            'orderby' => 'start',
            'order' => 'ASC',
            'search' => '',
            'event_type' => '',
            'event_mode' => '',
            'event_tag' => '',
            'event_price' => '',
            'city' => '',
            'date_from' => '',
            'official' => '',
            'custom_taxonomy' => '',
            'custom_terms' => '',
            'tax_relation' => 'AND',
            'include_past' => 'no',
            'show_excerpt' => 'yes',
            'show_meta' => 'yes',
            'excerpt_length' => 18,
            'hover_effect' => 'lift',
            'columns_desktop' => 4,
            'columns_tablet' => 2,
            'columns_mobile' => 1,
            'gap' => 22,
            'radius' => 18,
            'image_height' => 220,
            'accent_color' => '',
            'bg_color' => '',
            'text_color' => '',
            'custom_class' => '',
            'custom_css' => '',
        ], (array) $atts, 'nexdirectory_events_bento_grid');

        $query_config = $this->build_widget_query_config($atts);
        $query = $this->build_widget_events_query($query_config);
        $wrapper_id = sanitize_html_class(wp_unique_id('nexdirectory-widget-bento-'));
        $template = $this->sanitize_widget_template($atts['template'], '1');
        $style_vars = $this->build_widget_style_vars([
            'accent_color' => $atts['accent_color'],
            'bg_color' => $atts['bg_color'],
            'text_color' => $atts['text_color'],
            'radius' => $atts['radius'],
            'gap' => $atts['gap'],
            'image_height' => $atts['image_height'],
            'columns_desktop' => $atts['columns_desktop'],
            'columns_tablet' => $atts['columns_tablet'],
            'columns_mobile' => $atts['columns_mobile'],
        ]);
        $custom_class = $this->sanitize_widget_custom_class($atts['custom_class']);
        $custom_css = $this->sanitize_widget_custom_css($atts['custom_css']);
        $results_markup = $this->render_widget_events_markup($query, [
            'view' => 'bento',
            'template' => $template,
            'show_excerpt' => $atts['show_excerpt'],
            'show_meta' => $atts['show_meta'],
            'excerpt_length' => $atts['excerpt_length'],
            'hover_effect' => $atts['hover_effect'],
            'empty_text' => __('No matching listings found for this bento query.', 'nexdirectory'),
        ]);

        return $this->render_frontend_template('widget-events-bento.php', [
            'wrapper_id' => $wrapper_id,
            'template' => $template,
            'title' => sanitize_text_field((string) $atts['title']),
            'subtitle' => sanitize_text_field((string) $atts['subtitle']),
            'results_markup' => $results_markup,
            'style_vars' => $style_vars,
            'custom_class' => $custom_class,
            'custom_css' => $custom_css,
            'query_count' => $query instanceof WP_Query ? (int) $query->post_count : 0,
        ]);
    }


    public function render_events_ajax_search($atts = []) {
        $atts = shortcode_atts([
            'title' => '',
            'subtitle' => '',
            'template' => '1',
            'limit' => 8,
            'min_chars' => 2,
            'search_placeholder' => __('Search listings by keyword...', 'nexdirectory'),
            'orderby' => 'start',
            'order' => 'ASC',
            'search' => '',
            'event_type' => '',
            'event_mode' => '',
            'event_tag' => '',
            'event_price' => '',
            'city' => '',
            'date_from' => '',
            'official' => '',
            'custom_taxonomy' => '',
            'custom_terms' => '',
            'tax_relation' => 'AND',
            'include_past' => 'no',
            'show_type' => 'yes',
            'show_mode' => 'yes',
            'show_tag' => 'yes',
            'show_price' => 'yes',
            'show_city' => 'yes',
            'show_date' => 'yes',
            'show_official' => 'no',
            'show_excerpt' => 'no',
            'show_meta' => 'yes',
            'excerpt_length' => 14,
            'gap' => 14,
            'radius' => 14,
            'accent_color' => '',
            'bg_color' => '',
            'text_color' => '',
            'custom_class' => '',
            'custom_css' => '',
        ], (array) $atts, 'nexdirectory_events_ajax_search');

        $query_config = $this->build_widget_query_config($atts);
        $query = $this->build_widget_events_query($query_config);
        $wrapper_id = sanitize_html_class(wp_unique_id('nexdirectory-widget-search-'));
        $template = $this->sanitize_widget_template($atts['template'], '1');
        $style_vars = $this->build_widget_style_vars([
            'accent_color' => $atts['accent_color'],
            'bg_color' => $atts['bg_color'],
            'text_color' => $atts['text_color'],
            'radius' => $atts['radius'],
            'gap' => $atts['gap'],
        ]);
        $custom_class = $this->sanitize_widget_custom_class($atts['custom_class']);
        $custom_css = $this->sanitize_widget_custom_css($atts['custom_css']);

        $show_type = $this->sanitize_widget_yes_no($atts['show_type'], 'yes') === 'yes';
        $show_mode = $this->sanitize_widget_yes_no($atts['show_mode'], 'yes') === 'yes';
        $show_tag = $this->sanitize_widget_yes_no($atts['show_tag'], 'yes') === 'yes';
        $show_price = $this->sanitize_widget_yes_no($atts['show_price'], 'yes') === 'yes';
        $show_city = $this->sanitize_widget_yes_no($atts['show_city'], 'yes') === 'yes';
        $show_date = $this->sanitize_widget_yes_no($atts['show_date'], 'yes') === 'yes';
        $show_official = $this->sanitize_widget_yes_no($atts['show_official'], 'no') === 'yes';
        $results_markup = $this->render_widget_events_markup($query, [
            'view' => 'search',
            'template' => $template,
            'show_excerpt' => $atts['show_excerpt'],
            'show_meta' => $atts['show_meta'],
            'excerpt_length' => $atts['excerpt_length'],
            'empty_text' => __('No listings match your current filters.', 'nexdirectory'),
        ]);

        $client_config = [
            'endpoint' => esc_url_raw(rest_url('nexdirectory/v1/widget-search')),
            'template' => $template,
            'view' => 'search',
            'limit' => max(1, min(60, absint((int) $atts['limit']))),
            'minChars' => max(0, min(8, absint((int) $atts['min_chars']))),
            'showExcerpt' => $this->sanitize_widget_yes_no($atts['show_excerpt'], 'no'),
            'showMeta' => $this->sanitize_widget_yes_no($atts['show_meta'], 'yes'),
            'excerptLength' => max(8, min(70, absint((int) $atts['excerpt_length']))),
            'order' => in_array(strtoupper((string) $atts['order']), ['ASC', 'DESC'], true) ? strtoupper((string) $atts['order']) : 'ASC',
            'orderby' => in_array((string) $atts['orderby'], ['start', 'date', 'title', 'modified', 'rand'], true) ? (string) $atts['orderby'] : 'start',
            'taxRelation' => in_array(strtoupper((string) $atts['tax_relation']), ['AND', 'OR'], true) ? strtoupper((string) $atts['tax_relation']) : 'AND',
            'customTaxonomy' => sanitize_key((string) $atts['custom_taxonomy']),
            'customTerms' => implode(',', $this->parse_widget_slug_terms($atts['custom_terms'])),
            'includePast' => $this->sanitize_widget_yes_no($atts['include_past'], 'no'),
        ];

        return $this->render_frontend_template('widget-events-ajax-search.php', [
            'wrapper_id' => $wrapper_id,
            'template' => $template,
            'title' => sanitize_text_field((string) $atts['title']),
            'subtitle' => sanitize_text_field((string) $atts['subtitle']),
            'search_placeholder' => sanitize_text_field((string) $atts['search_placeholder']),
            'style_vars' => $style_vars,
            'custom_class' => $custom_class,
            'custom_css' => $custom_css,
            'results_markup' => $results_markup,
            'result_count' => $query instanceof WP_Query ? (int) $query->post_count : 0,
            'show_type' => $show_type,
            'show_mode' => $show_mode,
            'show_tag' => $show_tag,
            'show_price' => $show_price,
            'show_city' => $show_city,
            'show_date' => $show_date,
            'show_official' => $show_official,
            'event_type_options' => $show_type ? get_terms(['taxonomy' => self::TAX_TYPE, 'hide_empty' => true]) : [],
            'event_mode_options' => $show_mode ? get_terms(['taxonomy' => self::TAX_MODE, 'hide_empty' => true]) : [],
            'event_tag_options' => $show_tag ? get_terms(['taxonomy' => self::TAX_TAG, 'hide_empty' => true]) : [],
            'event_price_options' => $show_price ? get_terms(['taxonomy' => self::TAX_PRICE, 'hide_empty' => true]) : [],
            'search_config' => $client_config,
            'default_filters' => [
                'search' => sanitize_text_field((string) $atts['search']),
                'event_type' => sanitize_text_field((string) $atts['event_type']),
                'event_mode' => sanitize_text_field((string) $atts['event_mode']),
                'event_tag' => sanitize_text_field((string) $atts['event_tag']),
                'event_price' => sanitize_text_field((string) $atts['event_price']),
                'city' => sanitize_text_field((string) $atts['city']),
                'date_from' => sanitize_text_field((string) $atts['date_from']),
                'official' => sanitize_text_field((string) $atts['official']),
            ],
        ]);
    }


    public function rest_widget_search($request) {
        $params = [
            'template' => sanitize_text_field((string) $request->get_param('template')),
            'limit' => (int) $request->get_param('limit'),
            'orderby' => sanitize_text_field((string) $request->get_param('orderby')),
            'order' => sanitize_text_field((string) $request->get_param('order')),
            'search' => sanitize_text_field((string) $request->get_param('search')),
            'event_type' => sanitize_text_field((string) $request->get_param('event_type')),
            'event_mode' => sanitize_text_field((string) $request->get_param('event_mode')),
            'event_tag' => sanitize_text_field((string) $request->get_param('event_tag')),
            'event_price' => sanitize_text_field((string) $request->get_param('event_price')),
            'city' => sanitize_text_field((string) $request->get_param('city')),
            'date_from' => sanitize_text_field((string) $request->get_param('date_from')),
            'official' => sanitize_text_field((string) $request->get_param('official')),
            'custom_taxonomy' => sanitize_key((string) $request->get_param('custom_taxonomy')),
            'custom_terms' => sanitize_text_field((string) $request->get_param('custom_terms')),
            'tax_relation' => sanitize_text_field((string) $request->get_param('tax_relation')),
            'include_past' => sanitize_text_field((string) $request->get_param('include_past')),
            'show_excerpt' => sanitize_text_field((string) $request->get_param('show_excerpt')),
            'show_meta' => sanitize_text_field((string) $request->get_param('show_meta')),
            'excerpt_length' => (int) $request->get_param('excerpt_length'),
        ];
        $view = sanitize_key((string) $request->get_param('view'));
        if (!in_array($view, ['grid', 'bento', 'search'], true)) {
            $view = 'search';
        }
        $query_config = $this->build_widget_query_config($params);
        $query = $this->build_widget_events_query($query_config);
        $template = $this->sanitize_widget_template($params['template'] ?? '1', '1');

        $html = $this->render_widget_events_markup($query, [
            'view' => $view,
            'template' => $template,
            'show_excerpt' => $params['show_excerpt'] ?? 'no',
            'show_meta' => $params['show_meta'] ?? 'yes',
            'excerpt_length' => $params['excerpt_length'] ?? 14,
            'empty_text' => __('No listings match your current filters.', 'nexdirectory'),
        ]);

        return rest_ensure_response([
            'html' => $html,
            'count' => $query instanceof WP_Query ? (int) $query->post_count : 0,
            'found' => $query instanceof WP_Query ? (int) $query->found_posts : 0,
        ]);
    }


    public function render_whats_new($atts = []) {
        $atts = shortcode_atts([
            'posts_per_page' => 12,
        ], (array) $atts, 'nexdirectory_whats_new');

        $posts_per_page = max(1, (int) $atts['posts_per_page']);
        $paged = max(1, get_query_var('paged'), isset($_GET['paged']) ? absint((int) wp_unslash($_GET['paged'])) : 1);
        $filters = [
            's' => '',
            'event_type' => '',
            'event_mode' => '',
            'event_price' => '',
            'event_tag' => '',
            'city' => '',
            'date_from' => '',
            'official' => '',
        ];

        $query = new WP_Query([
            'post_type' => self::POST_TYPE,
            'post_status' => 'publish',
            'posts_per_page' => $posts_per_page,
            'paged' => $paged,
            'orderby' => 'date',
            'order' => 'DESC',
        ]);
        $ui_labels = $this->get_site_profile_ui_labels();

        return $this->render_frontend_template('whats-new.php', [
            'posts_per_page' => $posts_per_page,
            'paged' => $paged,
            'filters' => $filters,
            'query' => $query,
            'ui_labels' => $ui_labels,
        ]);
    }


    private function resolve_profile_key_for_post($post_id, $fallback = '') {
        $post_id = absint((int) $post_id);
        $fallback = sanitize_key((string) $fallback);
        if ($fallback === '' && method_exists($this, 'get_site_profile_key')) {
            $fallback = sanitize_key((string) $this->get_site_profile_key());
        }

        $catalog = method_exists($this, 'get_site_profile_catalog') ? (array) $this->get_site_profile_catalog() : [];
        if ($fallback === '' || !isset($catalog[$fallback])) {
            $fallback = isset($catalog['events']) ? 'events' : $fallback;
        }
        if ($fallback === '') {
            $fallback = 'events';
        }

        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            return $fallback;
        }

        // Enforce a single active template mode across the site.
        // This prevents cross-template wording/components from leaking via legacy per-post meta signals.
        if ($fallback !== '' && isset($catalog[$fallback])) {
            return $fallback;
        }

        $stored_profile = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_listing_profile', true));
        $stored_profile_valid = $stored_profile !== '' && isset($catalog[$stored_profile]);
        if ($stored_profile_valid && $stored_profile !== 'events') {
            return $stored_profile;
        }

        $job_signal_keys = [
            'nexdirectory_job_type',
            'nexdirectory_job_work_mode',
            'nexdirectory_job_salary',
            'nexdirectory_job_apply_url',
            'nexdirectory_job_application_email',
            'nexdirectory_job_closing_date',
            'nexdirectory_job_department',
        ];
        $has_job_signal = false;
        foreach ($job_signal_keys as $meta_key) {
            if (trim((string) get_post_meta($post_id, $meta_key, true)) !== '') {
                $has_job_signal = true;
                break;
            }
        }
        if ($has_job_signal) {
            return 'job_board';
        }

        $market_signal_keys = [
            'nexdirectory_market_condition',
            'nexdirectory_market_price_type',
            'nexdirectory_market_seller_type',
            'nexdirectory_market_listing_status',
            'nexdirectory_market_brand',
            'nexdirectory_market_model',
            'nexdirectory_market_sku',
            'nexdirectory_market_delivery_methods',
            'nexdirectory_market_shipping_notes',
            'nexdirectory_market_expires_on',
        ];
        $has_marketplace_signal = false;
        foreach ($market_signal_keys as $meta_key) {
            if (trim((string) get_post_meta($post_id, $meta_key, true)) !== '') {
                $has_marketplace_signal = true;
                break;
            }
        }
        if ($has_marketplace_signal) {
            return 'marketplace';
        }

        $mode_terms = wp_get_post_terms($post_id, self::TAX_MODE, ['fields' => 'names']);
        if (is_wp_error($mode_terms)) {
            $mode_terms = [];
        }
        $mode_terms = array_filter(array_map(static function ($name) {
            return strtolower(trim((string) $name));
        }, (array) $mode_terms), static function ($name) {
            return $name !== '';
        });
        $mode_terms_joined = implode(' ', $mode_terms);

        $price_terms = wp_get_post_terms($post_id, self::TAX_PRICE, ['fields' => 'names']);
        if (is_wp_error($price_terms)) {
            $price_terms = [];
        }
        $price_terms = array_filter(array_map(static function ($name) {
            return strtolower(trim((string) $name));
        }, (array) $price_terms), static function ($name) {
            return $name !== '';
        });
        $price_terms_joined = implode(' ', $price_terms);

        $has_marketplace_term_signal = (
            strpos($mode_terms_joined, 'standard listing') !== false
            || strpos($mode_terms_joined, 'online listing') !== false
            || strpos($price_terms_joined, 'fixed price') !== false
            || strpos($price_terms_joined, 'negotiable') !== false
        );
        if ($has_marketplace_term_signal) {
            return 'marketplace';
        }

        $company_name = trim((string) get_post_meta($post_id, 'nexdirectory_company_name', true));
        $market_condition = trim((string) get_post_meta($post_id, 'nexdirectory_market_condition', true));
        $has_directory_term_signal = (
            strpos($mode_terms_joined, 'on-site service') !== false
            || strpos($mode_terms_joined, 'online service') !== false
            || strpos($price_terms_joined, 'paid service') !== false
            || strpos($price_terms_joined, 'free service') !== false
        );
        $has_event_mode_signal = (
            strpos($mode_terms_joined, 'in-person') !== false
            || strpos($mode_terms_joined, 'online stream') !== false
            || strpos($mode_terms_joined, 'video stream') !== false
            || strpos($mode_terms_joined, 'hybrid') !== false
        );
        if ($has_directory_term_signal) {
            return 'directory';
        }
        if ($company_name !== '' && !$has_job_signal && $market_condition === '' && !$has_event_mode_signal) {
            $recurrence_value = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_recurrence', true));
            $has_event_specific_signal = (
                (string) get_post_meta($post_id, 'nexdirectory_official_event', true) === '1'
                || trim((string) get_post_meta($post_id, 'nexdirectory_event_stream_url', true)) !== ''
                || ($recurrence_value !== '' && $recurrence_value !== 'none')
            );
            if (!$has_event_specific_signal) {
                return 'directory';
            }
        }

        $event_stream_url = trim((string) get_post_meta($post_id, 'nexdirectory_event_stream_url', true));
        $recurrence = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_recurrence', true));
        $sessions_json = (string) get_post_meta($post_id, 'nexdirectory_sessions', true);
        $sessions = json_decode($sessions_json, true);
        $has_sessions = is_array($sessions) && !empty($sessions);
        $official_flag = (string) get_post_meta($post_id, 'nexdirectory_official_event', true) === '1';
        $has_event_specific_signal = ($event_stream_url !== '' || ($recurrence !== '' && $recurrence !== 'none') || $has_sessions || $official_flag);

        if ($stored_profile_valid) {
            if ($stored_profile === 'events' && $fallback !== 'events') {
                return $fallback;
            }
            return $stored_profile;
        }

        if ($fallback !== 'events') {
            return $fallback;
        }

        if ($has_event_specific_signal) {
            return 'events';
        }

        return $fallback;
    }


    private function persist_listing_profile_for_post($post_id, $profile_key) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            return;
        }
        $profile_key = sanitize_key((string) $profile_key);
        $catalog = method_exists($this, 'get_site_profile_catalog') ? (array) $this->get_site_profile_catalog() : [];
        if ($profile_key === '' || !isset($catalog[$profile_key])) {
            $profile_key = 'events';
        }
        update_post_meta($post_id, 'nexdirectory_listing_profile', $profile_key);
    }


    private function user_is_account_app_non_owner_in_single_org_mode($user_id = 0) {
        $user_id = absint((int) $user_id);
        if ($user_id <= 0 && is_user_logged_in()) {
            $user_id = (int) get_current_user_id();
        }
        if ($user_id <= 0) {
            return false;
        }

        $platform_mode = sanitize_key((string) get_option('nexdirectory_platform_mode', 'user_generated'));
        if ($platform_mode !== 'single_organisation') {
            return false;
        }
        if (user_can($user_id, 'manage_options')) {
            return false;
        }

        $owned_count = (int) count_user_posts($user_id, self::POST_TYPE, false);
        return $owned_count <= 0;
    }


    private function get_account_app_default_tab($user_id = 0, array $allowed_tabs = []) {
        $allowed_tabs = array_values(array_filter(array_map('sanitize_key', $allowed_tabs)));
        if ($this->user_is_account_app_non_owner_in_single_org_mode($user_id) && in_array('saved', $allowed_tabs, true)) {
            return 'saved';
        }
        if (in_array('account', $allowed_tabs, true)) {
            return 'account';
        }
        return isset($allowed_tabs[0]) ? (string) $allowed_tabs[0] : 'account';
    }


    private function get_account_app_allowed_tabs($user_id = 0) {
        $tabs = ['account', 'analytics', 'listings', 'submit', 'inbox', 'saved'];
        $user_id = absint((int) $user_id);
        if ($user_id <= 0 && is_user_logged_in()) {
            $user_id = (int) get_current_user_id();
        }

        $platform_mode = sanitize_key((string) get_option('nexdirectory_platform_mode', 'user_generated'));
        $can_manage = $user_id > 0 ? user_can($user_id, 'manage_options') : current_user_can('manage_options');
        if ($platform_mode === 'single_organisation' && !$can_manage) {
            $tabs = array_values(array_diff($tabs, ['listings', 'submit']));
            if ($this->user_is_account_app_non_owner_in_single_org_mode($user_id)) {
                $tabs = array_values(array_diff($tabs, ['analytics']));
            }
        }

        return $tabs;
    }


    private function sanitize_account_app_tab($tab, $user_id = 0) {
        $tab = sanitize_key((string) $tab);
        $allowed_tabs = $this->get_account_app_allowed_tabs($user_id);
        $default_tab = $this->get_account_app_default_tab($user_id, $allowed_tabs);
        if (!in_array($tab, $allowed_tabs, true)) {
            $tab = $default_tab;
        }
        return $tab;
    }


    private function sanitize_account_app_listings_subtab($subtab) {
        $subtab = sanitize_key((string) $subtab);
        $allowed_subtabs = ['manage'];
        $boosts_enabled = function_exists('nexdirectory_is_pro_feature_enabled')
            && (
                nexdirectory_is_pro_feature_enabled('boost_packages')
                || nexdirectory_is_pro_feature_enabled('monetization')
            );
        if ($boosts_enabled) {
            $allowed_subtabs[] = 'boosts';
        }
        $allowed_subtabs = array_values(array_unique(array_filter(array_map(
            'sanitize_key',
            (array) apply_filters('nexdirectory_extension_account_listing_subtabs', $allowed_subtabs, $this)
        ))));
        if (!in_array($subtab, $allowed_subtabs, true)) {
            $subtab = 'manage';
        }
        return $subtab;
    }


    private function render_account_app_template(array $options = []) {
        $options = wp_parse_args($options, [
            'initial_tab' => 'account',
            'initial_listings_subtab' => 'manage',
            'show_nav' => '',
            'posts_per_page' => 30,
            'tickets_limit' => 200,
            'notifications_limit' => 80,
            'analytics_days' => 30,
            'singleton' => true,
            'login_message' => __('Please log in to manage your account.', 'nexdirectory'),
        ]);

        static $account_app_rendered_once = false;

        $initial_tab = $this->sanitize_account_app_tab($options['initial_tab']);
        $requested_tab = isset($_GET['nexdirectory_tab'])
            ? sanitize_key((string) wp_unslash($_GET['nexdirectory_tab']))
            : '';
        if ($requested_tab !== '') {
            $initial_tab = $this->sanitize_account_app_tab($requested_tab);
        }
        $initial_listings_subtab = $this->sanitize_account_app_listings_subtab($options['initial_listings_subtab']);
        $requested_listings_subtab = isset($_GET['listings_subtab']) ? sanitize_key((string) wp_unslash($_GET['listings_subtab'])) : '';
        if ($requested_listings_subtab === '' && isset($_GET['nexdirectory_listings_subtab'])) {
            $requested_listings_subtab = sanitize_key((string) wp_unslash($_GET['nexdirectory_listings_subtab']));
        }
        if ($requested_listings_subtab !== '') {
            $initial_listings_subtab = $this->sanitize_account_app_listings_subtab($requested_listings_subtab);
            $initial_tab = 'listings';
        }
        $show_nav_setting = (string) $options['show_nav'];
        $singleton = !empty($options['singleton']);
        $default_show_nav = (string) get_option('nexdirectory_default_submit_show_nav', '1') === '1';
        $show_nav = $show_nav_setting === '' ? $default_show_nav : ($show_nav_setting !== 'no');
        $login_message = sanitize_text_field((string) $options['login_message']);
        if ($login_message === '') {
            $login_message = __('Please log in to manage your account.', 'nexdirectory');
        }

        if (!is_user_logged_in()) {
            return $this->redirect_to_login_screen($login_message);
        }

        if ($singleton && $account_app_rendered_once) {
            return '';
        }
        if ($singleton) {
            $account_app_rendered_once = true;
        }

        $current_user_id = (int) get_current_user_id();
        $allowed_tabs = $this->get_account_app_allowed_tabs($current_user_id);
        $initial_tab = $this->sanitize_account_app_tab($initial_tab, $current_user_id);
        if ($initial_tab === 'listings' && !in_array('listings', $allowed_tabs, true)) {
            $initial_listings_subtab = 'manage';
        }
        $ui_labels = $this->get_site_profile_ui_labels();
        $profile_key = sanitize_key((string) ($ui_labels['profile_key'] ?? 'events'));
        $is_events_profile = (string) ($ui_labels['is_events_profile'] ?? ($profile_key === 'events' ? '1' : '0')) === '1';
        if (!$is_events_profile && $initial_listings_subtab === 'webapp') {
            $initial_listings_subtab = 'manage';
        }
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $singular_lower = sanitize_text_field((string) ($ui_labels['singular_lower'] ?? __('event', 'nexdirectory')));

        $events_url = $this->get_platform_page_url(
            'nexdirectory_page_events_url',
            $this->get_profile_route_slugs('events', $profile_key),
            $this->get_profile_route_default_url('events', $profile_key)
        );
        $submit_event_url = $this->get_platform_page_url(
            'nexdirectory_page_submit_event_url',
            $this->get_profile_route_slugs('submit', $profile_key),
            $this->get_profile_route_default_url('submit', $profile_key)
        );
        $my_events_url = $this->get_platform_page_url(
            'nexdirectory_page_my_events_url',
            $this->get_profile_route_slugs('my_items', $profile_key),
            $this->get_profile_route_default_url('my_items', $profile_key)
        );
        $my_tickets_url = $this->get_platform_page_url(
            'nexdirectory_page_my_tickets_url',
            $this->get_profile_route_slugs('attending', $profile_key),
            $this->get_profile_route_default_url('attending', $profile_key)
        );

        $user = wp_get_current_user();
        $account_description = sanitize_text_field((string) ($ui_labels['account_description'] ?? __('Update your profile and manage your listing activity.', 'nexdirectory')));
        $gdpr_marketing_opt_in = $this->user_allows_organizer_communications((int) $user->ID);
        $event_messages_enabled = $this->user_allows_event_messages((int) $user->ID);
        $gdpr_terms_accepted_at = absint((int) get_user_meta((int) $user->ID, 'nexdirectory_gdpr_terms_accepted_at', true));
        $gdpr_terms_accepted_label = $gdpr_terms_accepted_at > 0
            ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), $gdpr_terms_accepted_at)
            : '';

        $days = isset($_GET['nexdirectory_owner_days']) ? absint((int) wp_unslash($_GET['nexdirectory_owner_days'])) : absint((int) $options['analytics_days']);
        if (!in_array($days, [7, 30, 90, 365], true)) {
            $days = 30;
        }
        $analytics_title = sanitize_text_field((string) ($ui_labels['owner_analytics_title'] ?? __('Analytics', 'nexdirectory')));
        $analytics_description = sanitize_text_field((string) ($ui_labels['owner_analytics_description'] ?? __('Review listing views, clicks, enquiries, and discovery performance.', 'nexdirectory')));

        $posts_per_page = max(1, (int) $options['posts_per_page']);
        $load_listings_data = $initial_tab === 'listings'
            || isset($_GET['attendees_event'])
            || isset($_GET['event_id']);
        $query = new WP_Query();
        $visible_event_ids = [];
        if ($load_listings_data) {
            $owned_event_ids = get_posts([
                'post_type' => self::POST_TYPE,
                'author' => $current_user_id,
                'post_status' => ['pending', 'publish', 'draft'],
                'posts_per_page' => -1,
                'fields' => 'ids',
                'orderby' => 'date',
                'order' => 'DESC',
                'no_found_rows' => true,
                'suppress_filters' => false,
            ]);
            $team_event_ids = $this->get_event_ids_for_team_user($current_user_id);
            $visible_event_ids = array_values(array_unique(array_map('absint', array_merge(
                is_array($owned_event_ids) ? $owned_event_ids : [],
                is_array($team_event_ids) ? $team_event_ids : []
            ))));
            $query_args = [
                'post_type' => self::POST_TYPE,
                'post_status' => ['pending', 'publish', 'draft'],
                'posts_per_page' => $posts_per_page,
                'orderby' => 'date',
                'order' => 'DESC',
                'post__in' => !empty($visible_event_ids) ? $visible_event_ids : [0],
            ];
            $query = new WP_Query($query_args);
        }
        $open_event_id = isset($_GET['event_id']) ? absint((int) wp_unslash($_GET['event_id'])) : 0;
        if ($open_event_id > 0 && !$this->user_can_edit_event($open_event_id, $current_user_id)) {
            $open_event_id = 0;
        }
        $attendees_event_id = isset($_GET['attendees_event']) ? absint((int) wp_unslash($_GET['attendees_event'])) : 0;
        $attendees_panel = null;
        if ($attendees_event_id > 0) {
            $owner_id = (int) get_post_field('post_author', $attendees_event_id);
            $can_view_attendees = get_post_type($attendees_event_id) === self::POST_TYPE
                && (
                    $owner_id === $current_user_id
                    || $this->user_can_moderate_events()
                    || $this->user_has_event_team_role($attendees_event_id, $current_user_id, ['owner', 'editor', 'support'], ['active'], 'website')
                );
            if ($can_view_attendees) {
                $attendee_rows = $this->get_event_attendee_rows($attendees_event_id);
                $communication_allowed_count = count(array_filter($attendee_rows, static function ($row) {
                    return (string) ($row['communication_opt_in'] ?? '0') === '1';
                }));
                $attendees_panel = [
                    'event_id' => $attendees_event_id,
                    'event_title' => (string) get_the_title($attendees_event_id),
                    'rows' => $attendee_rows,
                    'yes_count' => count(array_filter($attendee_rows, static function ($row) {
                        return (string) ($row['status'] ?? '') === 'yes';
                    })),
                    'waitlist_count' => count(array_filter($attendee_rows, static function ($row) {
                        return (string) ($row['status'] ?? '') === 'waitlist';
                    })),
                    'communication_allowed_count' => $communication_allowed_count,
                    'communication_blocked_count' => max(0, count($attendee_rows) - $communication_allowed_count),
                    'export_action' => admin_url('admin-post.php'),
                    'export_nonce' => wp_create_nonce('nexdirectory_export_event_attendees'),
                    'email_action' => admin_url('admin-post.php'),
                    'email_nonce' => wp_create_nonce('nexdirectory_email_event_attendees'),
                ];
            } else {
                $attendees_event_id = 0;
            }
        }

        $force_new_submit = sanitize_key((string) wp_unslash($_GET['nexdirectory_new'] ?? '')) === '1';
        $edit_event_id = $force_new_submit
            ? 0
            : (isset($_GET['edit_event_id']) ? absint((int) wp_unslash($_GET['edit_event_id'])) : 0);
        $resolved_profile_key = $this->resolve_profile_key_for_post($edit_event_id, $profile_key);
        $submission_assets_needed = $initial_tab === 'submit' || $edit_event_id > 0;
        if ($submission_assets_needed) {
            wp_enqueue_style('nexdirectory-leaflet');
            wp_enqueue_script('nexdirectory-leaflet');
        }
        $use_rich_editor = (string) get_option('nexdirectory_use_rich_editor', '1') === '1';
        if ($submission_assets_needed && $use_rich_editor && function_exists('wp_enqueue_editor')) {
            wp_enqueue_editor();
        }
        $prefill = $submission_assets_needed
            ? $this->build_submit_form_prefill($edit_event_id, $resolved_profile_key)
            : [];
        if ($edit_event_id > 0 && empty($prefill['is_edit'])) {
            $locked_demo_edit = is_callable([$this, 'is_demo_event_edit_locked'])
                ? (bool) $this->is_demo_event_edit_locked($edit_event_id, $current_user_id)
                : false;
            $this->redirect_with_message(
                $my_events_url,
                $locked_demo_edit
                    ? __('This demo event is read-only. Duplicate it to create an editable copy.', 'nexdirectory')
                    /* translators: %s: Placeholder value. */
                    : sprintf(__('You can only edit your own %s.', 'nexdirectory'), $singular_lower),
                'error'
            );
        }
        if (!empty($prefill['is_edit']) && in_array('submit', $allowed_tabs, true)) {
            $initial_tab = 'submit';
        }
        $custom_fields_enabled = $this->is_custom_submission_builder_available('account_app', $current_user_id);
        $per_event_email_templates_enabled = function_exists('nexdirectory_is_pro_feature_enabled')
            && nexdirectory_is_pro_feature_enabled('notification_automation')
            && (string) get_option('nexdirectory_enable_per_event_email_templates', '0') === '1';
        $organizer_event_seo_enabled = (string) get_option('nexdirectory_enable_event_seo_fields', '1') === '1';
        $custom_form_fields = $custom_fields_enabled
            ? (is_array($prefill['custom_form_fields'] ?? null) ? (array) $prefill['custom_form_fields'] : [])
            : [];
        if (!$custom_fields_enabled) {
            $prefill['custom_form_fields'] = [];
            $prefill['custom_fields_values'] = [];
        }
        $custom_taxonomy_fields = [];
        $taxonomy_definitions = $submission_assets_needed
            ? $this->get_custom_taxonomy_definitions_for_submit()
            : [];
        foreach ($taxonomy_definitions as $taxonomy_row) {
            $taxonomy_slug = sanitize_key((string) ($taxonomy_row['slug'] ?? ''));
            if ($taxonomy_slug === '' || !taxonomy_exists($taxonomy_slug)) {
                continue;
            }
            $term_rows = get_terms([
                'taxonomy' => $taxonomy_slug,
                'hide_empty' => false,
                'orderby' => 'name',
                'order' => 'ASC',
            ]);
            if (is_wp_error($term_rows)) {
                continue;
            }
            $selected_ids = [];
            if (is_array($prefill['custom_taxonomy_terms'] ?? null)) {
                $selected_ids = array_values(array_filter(array_map('absint', (array) ($prefill['custom_taxonomy_terms'][$taxonomy_slug] ?? []))));
            }
            $custom_taxonomy_fields[] = [
                'slug' => $taxonomy_slug,
                'label' => sanitize_text_field((string) ($taxonomy_row['label'] ?? $taxonomy_slug)),
                'hierarchical' => (string) ($taxonomy_row['hierarchical'] ?? '1') === '1',
                'terms' => is_array($term_rows) ? $term_rows : [],
                'selected_ids' => $selected_ids,
            ];
        }
        $upload_limits = $this->get_upload_limits();
        $excerpt_max_chars = max(80, min(1000, (int) get_option('nexdirectory_excerpt_max_chars', 220)));
        $classification_labels = $this->get_classification_labels();
        $official_pending_note = sprintf(
            /* translators: %s: Placeholder value. */
            __('Official %s classification is reviewed by admins before it appears publicly.', 'nexdirectory'),
            strtolower($singular_label)
        );
        $event_completion_missing = [];
        if (!empty($prefill['is_edit']) && !empty($prefill['edit_event_id'])) {
            $event_completion_missing = $this->get_event_optional_sections_missing((int) $prefill['edit_event_id'], $resolved_profile_key);
        }
        $notifications_limit = max(1, min(300, (int) $options['notifications_limit']));
        $notifications_title = __('Notifications & Inbox', 'nexdirectory');
        $notifications_description = __('Review account activity, team and listing updates, then continue your conversations.', 'nexdirectory');
        $threads = [];
        $active_thread_id = 0;
        $active_thread = [];
        $active_messages = [];
        $active_subject = '';
        $active_event_title = '';
        $active_event_url = '';
        $active_other_name = __('Member', 'nexdirectory');
        $active_other_id = 0;
        $reply_subject = '';
        $unread_count = 0;

        if ($initial_tab === 'inbox' && $this->user_can_access_platform($current_user_id)) {
            $requested_thread_id = isset($_GET['thread']) ? absint((int) wp_unslash($_GET['thread'])) : 0;
            $highlight_message_id = isset($_GET['message']) ? absint((int) wp_unslash($_GET['message'])) : 0;

            $threads = $this->get_inbox_threads_for_user($current_user_id, $notifications_limit);
            if ($requested_thread_id > 0) {
                foreach ($threads as $thread_row) {
                    if ((int) ($thread_row['thread_id'] ?? 0) === $requested_thread_id) {
                        $active_thread_id = $requested_thread_id;
                        break;
                    }
                }
            }
            if ($active_thread_id > 0) {
                $active_thread = (array) $this->get_inbox_thread_for_user($active_thread_id, $current_user_id);
                if (!empty($active_thread)) {
                    $active_post_id = absint((int) ($active_thread['post_id'] ?? 0));
                    if ($active_post_id > 0) {
                        $active_event_title = sanitize_text_field((string) get_the_title($active_post_id));
                        $active_event_url = (string) get_permalink($active_post_id);
                    }
                    $active_subject = sanitize_text_field((string) ($active_thread['subject'] ?? ''));
                    if ($active_subject === '' && $active_event_title !== '') {
                        /* translators: %s: Placeholder value. */
                        $active_subject = sprintf(__('Message about %s', 'nexdirectory'), $active_event_title);
                    }
                    $participant_a = absint((int) ($active_thread['participant_a'] ?? 0));
                    $participant_b = absint((int) ($active_thread['participant_b'] ?? 0));
                    $active_other_id = $participant_a === $current_user_id ? $participant_b : $participant_a;
                    if ($active_other_id > 0) {
                        $other_user = get_user_by('id', $active_other_id);
                        if ($other_user instanceof WP_User) {
                            $active_other_name = sanitize_text_field((string) ($other_user->display_name ?: $other_user->user_login));
                        }
                    }

                    $raw_messages = $this->get_inbox_messages_for_thread($active_thread_id, 500);
                    $sender_ids = [];
                    foreach ($raw_messages as $raw_message) {
                        $sender_id = absint((int) ($raw_message['sender_id'] ?? 0));
                        if ($sender_id > 0) {
                            $sender_ids[] = $sender_id;
                        }
                    }
                    $sender_ids = array_values(array_unique($sender_ids));
                    $sender_map = [];
                    if (!empty($sender_ids)) {
                        $sender_users = get_users([
                            'include' => $sender_ids,
                            'fields' => ['ID', 'display_name', 'user_login'],
                        ]);
                        foreach ((array) $sender_users as $sender_user) {
                            if (!$sender_user instanceof WP_User) {
                                continue;
                            }
                            $sender_map[(int) $sender_user->ID] = sanitize_text_field((string) ($sender_user->display_name ?: $sender_user->user_login));
                        }
                    }
                    foreach ($raw_messages as $raw_message) {
                        $message_id = absint((int) ($raw_message['id'] ?? 0));
                        if ($message_id <= 0) {
                            continue;
                        }
                        $sender_id = absint((int) ($raw_message['sender_id'] ?? 0));
                        $created_ts = strtotime((string) ($raw_message['created_at'] ?? ''));
                        $message_subject = sanitize_text_field((string) ($raw_message['subject'] ?? ''));
                        if ($message_subject === '') {
                            $message_subject = $active_subject;
                        }
                        $active_messages[] = [
                            'message_id' => $message_id,
                            'sender_id' => $sender_id,
                            'sender_name' => (string) ($sender_map[$sender_id] ?? __('Member', 'nexdirectory')),
                            'subject' => $message_subject,
                            'body' => nexdirectory_sanitize_textarea((string) ($raw_message['body'] ?? '')),
                            'attachment_url' => esc_url_raw((string) ($raw_message['attachment_url'] ?? '')),
                            'created_label' => $created_ts ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), $created_ts) : '',
                            'is_mine' => $sender_id === $current_user_id,
                            'is_highlight' => $highlight_message_id > 0 && $highlight_message_id === $message_id,
                        ];
                    }
                    $reply_subject = $active_subject;
                }
            }

            $unread_count = $this->get_inbox_unread_count_for_user($current_user_id);
        }

        $tickets_limit = max(1, (int) $options['tickets_limit']);
        $saved_rows = $initial_tab === 'saved'
            ? $this->get_rsvp_rows_for_user($current_user_id, $tickets_limit)
            : [];

        $account_context = [
            'available_tabs' => $allowed_tabs,
            'initial_tab' => $initial_tab,
            'initial_listings_subtab' => $initial_listings_subtab,
            'show_nav' => $show_nav,
            'ui_labels' => $ui_labels,
            'profile_key' => $profile_key,
            'user' => $user,
            'account_description' => $account_description,
            'gdpr_marketing_opt_in' => $gdpr_marketing_opt_in,
            'event_messages_enabled' => $event_messages_enabled,
            'gdpr_terms_accepted_label' => $gdpr_terms_accepted_label,
            'analytics_title' => $analytics_title,
            'analytics_description' => $analytics_description,
            'days' => $days,
            'events_url' => $events_url,
            'query' => $query,
            'open_event_id' => $open_event_id,
            'submit_event_url' => $submit_event_url,
            'my_events_url' => $my_events_url,
            'my_tickets_url' => $my_tickets_url,
            'attendees_event_id' => $attendees_event_id,
            'attendees_panel' => $attendees_panel,
            'use_rich_editor' => $use_rich_editor,
            'submit_recaptcha_required' => $this->should_enforce_submit_form_recaptcha(),
            'prefill' => $prefill,
            'custom_form_fields' => $custom_form_fields,
            'custom_field_types' => $submission_assets_needed ? $this->get_custom_field_types() : [],
            'custom_fields_enabled' => $custom_fields_enabled,
            'per_event_email_templates_enabled' => $per_event_email_templates_enabled,
            'organizer_event_seo_enabled' => $organizer_event_seo_enabled,
            'upload_limits' => $upload_limits,
            'excerpt_max_chars' => $excerpt_max_chars,
            'custom_taxonomy_fields' => $custom_taxonomy_fields,
            'classification_labels' => $classification_labels,
            'official_pending_note' => $official_pending_note,
            'event_completion_missing' => $event_completion_missing,
            'notifications_title' => $notifications_title,
            'notifications_description' => $notifications_description,
            'threads' => $threads,
            'active_thread_id' => $active_thread_id,
            'active_thread' => $active_thread,
            'active_messages' => $active_messages,
            'active_subject' => $active_subject,
            'active_event_title' => $active_event_title,
            'active_event_url' => $active_event_url,
            'active_other_name' => $active_other_name,
            'active_other_id' => $active_other_id,
            'reply_action' => nexdirectory_add_query_arg('nexdirectory_tab', 'inbox', admin_url('admin-post.php')),
            'reply_nonce' => $active_thread_id > 0 ? wp_create_nonce('nexdirectory_inbox_reply') : '',
            'reply_subject' => $reply_subject,
            'unread_count' => $unread_count,
            'saved_rows' => $saved_rows,
        ];
        $account_context = apply_filters(
            'nexdirectory_account_app_context',
            $account_context,
            $current_user_id,
            $profile_key,
            $this
        );
        $account_context = is_array($account_context) ? $account_context : [];
        $output = $this->render_frontend_template('account-app.php', $account_context);
        wp_reset_postdata();
        return $output;
    }


    public function render_submit_event($atts = []) {
        $atts = shortcode_atts([
            'show_nav' => '',
        ], (array) $atts, 'nexdirectory_submit_event');
        $ui_labels = $this->get_site_profile_ui_labels();
        $singular_lower = sanitize_text_field((string) ($ui_labels['singular_lower'] ?? __('event', 'nexdirectory')));

        return $this->render_account_app_template([
            'initial_tab' => 'submit',
            'show_nav' => (string) $atts['show_nav'],
            /* translators: %s: Placeholder value. */
            'login_message' => sprintf(__('Please log in to submit your %s.', 'nexdirectory'), $singular_lower),
        ]);
    }

    private function get_safe_redirect_url($redirect_to, $fallback = '') {
        $fallback = esc_url_raw((string) $fallback);
        if ($fallback === '') {
            $fallback = home_url('/');
        }
        $redirect_to = esc_url_raw((string) $redirect_to);
        if ($redirect_to === '') {
            return $fallback;
        }
        $home_host = wp_parse_url(home_url('/'), PHP_URL_HOST);
        $redirect_host = wp_parse_url($redirect_to, PHP_URL_HOST);
        if ($home_host && $redirect_host && !hash_equals((string) $home_host, (string) $redirect_host)) {
            return $fallback;
        }
        return $redirect_to;
    }

    private function sanitize_password_value($value) {
        if (!is_scalar($value)) {
            return '';
        }
        $password = (string) $value;
        $password = str_replace("\0", '', $password);
        return wp_check_invalid_utf8($password);
    }

    private function sanitize_custom_builder_json_payload($value) {
        if (!is_scalar($value)) {
            return '[]';
        }
        $payload = (string) $value;
        $payload = str_replace("\0", '', $payload);
        $payload = wp_check_invalid_utf8($payload);
        return $payload === '' ? '[]' : $payload;
    }

    private function get_event_optional_sections_missing($post_id, $profile_key = '') {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            return [];
        }
        $resolved_profile_key = $this->resolve_profile_key_for_post($post_id, $profile_key);
        $ui_labels = $this->get_site_profile_ui_labels($resolved_profile_key);
        $profile_key = sanitize_key((string) ($ui_labels['profile_key'] ?? 'events'));
        $is_events_profile = $profile_key === 'events';
        $is_job_profile = $profile_key === 'job_board';
        $is_marketplace_profile = $profile_key === 'marketplace';

        $missing = [];
        $content_raw = trim((string) nexdirectory_strip_all_tags((string) get_post_field('post_content', $post_id)));
        if ($content_raw === '') {
            $missing[] = __('Overview description', 'nexdirectory');
        }

        $venue = trim((string) get_post_meta($post_id, 'nexdirectory_venue', true));
        $address = trim((string) get_post_meta($post_id, 'nexdirectory_address', true));
        $city = trim((string) get_post_meta($post_id, 'nexdirectory_city', true));
        $country = trim((string) get_post_meta($post_id, 'nexdirectory_country', true));
        $lat = get_post_meta($post_id, 'nexdirectory_lat', true);
        $lng = get_post_meta($post_id, 'nexdirectory_lng', true);
        $has_location = ($venue !== '' || $address !== '' || $city !== '' || $country !== '' || (is_numeric($lat) && is_numeric($lng)));
        $website = trim((string) get_post_meta($post_id, 'nexdirectory_external_url', true));

        $contact_email = sanitize_email((string) get_post_meta($post_id, 'nexdirectory_contact_email', true));
        $contact_phone = trim((string) get_post_meta($post_id, 'nexdirectory_contact_phone', true));
        $has_contact_details = $contact_email !== '' || $contact_phone !== '';

        if ($is_events_profile) {
            if (!$has_location) {
                $missing[] = __('Location details', 'nexdirectory');
            }
            $gallery_urls = $this->get_event_gallery_image_urls($post_id, 'thumbnail');
            $cover_url = esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_cover_image_url', true));
            if ($cover_url !== '') {
                $normalized_cover = untrailingslashit($cover_url);
                $gallery_urls = array_values(array_filter($gallery_urls, static function ($url) use ($normalized_cover) {
                    return untrailingslashit((string) $url) !== $normalized_cover;
                }));
            }
            if (empty($gallery_urls)) {
                $missing[] = __('Gallery images', 'nexdirectory');
            }
            $tag_ids = wp_get_post_terms($post_id, self::TAX_TAG, ['fields' => 'ids']);
            if (is_wp_error($tag_ids) || empty($tag_ids)) {
                $missing[] = __('Tags', 'nexdirectory');
            }
            $sessions = $this->parse_sessions((string) get_post_meta($post_id, 'nexdirectory_sessions', true));
            if (empty($sessions)) {
                $missing[] = __('A Day Of Action itinerary', 'nexdirectory');
            }
            if ($website === '') {
                $missing[] = __('Event website URL', 'nexdirectory');
            }
            $end_value = trim((string) get_post_meta($post_id, 'nexdirectory_end', true));
            if ($end_value === '') {
                $missing[] = __('End date', 'nexdirectory');
            }
            if (!$has_contact_details) {
                $missing[] = __('Contact details', 'nexdirectory');
            }
        } elseif ($is_job_profile) {
            $salary = trim((string) get_post_meta($post_id, 'nexdirectory_job_salary', true));
            if ($salary === '') {
                $missing[] = __('Salary range', 'nexdirectory');
            }
            $work_mode = trim((string) get_post_meta($post_id, 'nexdirectory_job_work_mode', true));
            if ($work_mode === '') {
                $missing[] = __('Work location type', 'nexdirectory');
            }
            $job_type = trim((string) get_post_meta($post_id, 'nexdirectory_job_type', true));
            if ($job_type === '') {
                $missing[] = __('Employment type', 'nexdirectory');
            }
            $apply_url = trim((string) get_post_meta($post_id, 'nexdirectory_job_apply_url', true));
            $apply_email = sanitize_email((string) get_post_meta($post_id, 'nexdirectory_job_application_email', true));
            if ($apply_url === '' && $apply_email === '') {
                $missing[] = __('Application method', 'nexdirectory');
            }
            $closing_date = trim((string) get_post_meta($post_id, 'nexdirectory_job_closing_date', true));
            if ($closing_date === '') {
                $missing[] = __('Closing date', 'nexdirectory');
            }
            $company_name = trim((string) get_post_meta($post_id, 'nexdirectory_company_name', true));
            if ($company_name === '') {
                $missing[] = __('Company name', 'nexdirectory');
            }
            if (!$has_contact_details && $apply_email === '') {
                $missing[] = __('Recruiter contact details', 'nexdirectory');
            }
        } elseif ($is_marketplace_profile) {
            $price_value = trim((string) get_post_meta($post_id, 'nexdirectory_price', true));
            if ($price_value === '') {
                $missing[] = __('Asking price', 'nexdirectory');
            }
            $price_type = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_market_price_type', true));
            if (!in_array($price_type, ['fixed', 'negotiable', 'auction', 'free'], true)) {
                $missing[] = __('Price type', 'nexdirectory');
            }
            $condition = trim((string) get_post_meta($post_id, 'nexdirectory_market_condition', true));
            if ($condition === '') {
                $missing[] = __('Condition', 'nexdirectory');
            }
            $delivery_methods = preg_split('/[\s,]+/', (string) get_post_meta($post_id, 'nexdirectory_market_delivery_methods', true));
            if (!is_array($delivery_methods) || empty(array_filter(array_map('sanitize_key', $delivery_methods)))) {
                $missing[] = __('Delivery methods', 'nexdirectory');
            }
            if (!$has_location) {
                $missing[] = __('Location details', 'nexdirectory');
            }
            if (!$has_contact_details && $website === '') {
                $missing[] = __('Contact method', 'nexdirectory');
            }
        } else {
            if (!$has_location) {
                $missing[] = __('Location details', 'nexdirectory');
            }
            $tag_ids = wp_get_post_terms($post_id, self::TAX_TAG, ['fields' => 'ids']);
            if (is_wp_error($tag_ids) || empty($tag_ids)) {
                $missing[] = __('Tags', 'nexdirectory');
            }
            if ($website === '') {
                $missing[] = __('Website URL', 'nexdirectory');
            }
            if (!$has_contact_details) {
                $missing[] = __('Contact details', 'nexdirectory');
            }
        }

        return array_values(array_unique(array_filter(array_map('nexdirectory_sanitize_text', $missing))));
    }


    private function get_event_optional_sections_hint_text(array $missing, $max_items = 4) {
        $clean = array_values(array_filter(array_map(static function ($item) {
            return sanitize_text_field((string) $item);
        }, $missing), static function ($item) {
            return trim((string) $item) !== '';
        }));
        if (empty($clean)) {
            return '';
        }
        $max_items = max(1, (int) $max_items);
        $visible = array_slice($clean, 0, $max_items);
        $remaining = max(0, count($clean) - count($visible));
        /* translators: %d: Placeholder value. */
        $suffix = $remaining > 0 ? sprintf(__(' (+%d more)', 'nexdirectory'), $remaining) : '';
        return sprintf(
            /* translators: 1: Placeholder value 1, 2: Placeholder value 2. */
            __('Tip: add %1$s to complete this listing%2$s.', 'nexdirectory'),
            implode(', ', $visible),
            $suffix
        );
    }


    private function resolve_submit_prefill_source_post_id($post_id) {
        $post_id = absint((int) $post_id);
        return $post_id > 0 ? $post_id : 0;
    }


    private function recover_existing_event_title_value($post_id, $fallback = '') {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return sanitize_text_field((string) $fallback);
        }

        $title = trim(sanitize_text_field((string) $fallback));
        if ($title === '') {
            $title = trim(sanitize_text_field((string) get_the_title($post_id)));
        }
        if ($title === '') {
            foreach (['nexdirectory_meta_title', 'nexdirectory_import_title', 'nexdirectory_title', 'title'] as $meta_key) {
                $meta_title = trim(sanitize_text_field((string) get_post_meta($post_id, $meta_key, true)));
                if ($meta_title !== '') {
                    $title = $meta_title;
                    break;
                }
            }
        }
        if ($title === '') {
            $post_name = sanitize_title((string) get_post_field('post_name', $post_id));
            if ($post_name !== '') {
                $title = trim(sanitize_text_field(str_replace(['-', '_'], ' ', $post_name)));
            }
        }
        if ($title === '') {
            /* translators: %d: Placeholder value. */
            $title = sprintf(__('Untitled Listing #%d', 'nexdirectory'), $post_id);
        }

        return $title;
    }


    private function recover_existing_event_datetime_value($post_id, $meta_key = 'nexdirectory_start') {
        $post_id = absint((int) $post_id);
        $meta_key = sanitize_key((string) $meta_key);
        if ($post_id <= 0 || !in_array($meta_key, ['nexdirectory_start', 'nexdirectory_end'], true)) {
            return '';
        }
        $candidate_keys = $meta_key === 'nexdirectory_start'
            ? ['nexdirectory_start', 'start', 'event_start', 'nexdirectory_start_raw', 'nexdirectory_start_date']
            : ['nexdirectory_end', 'end', 'event_end', 'nexdirectory_end_raw', 'nexdirectory_end_date'];
        foreach ($candidate_keys as $candidate_key) {
            $normalized = $this->normalize_prefill_datetime_meta_value((string) get_post_meta($post_id, $candidate_key, true));
            if ($normalized !== '') {
                return $normalized;
            }
        }
        return '';
    }


    private function normalize_prefill_datetime_meta_value($value) {
        $raw = trim(nexdirectory_scalar_to_string($value));
        if ($raw === '') {
            return '';
        }

        if (is_numeric($raw)) {
            $timestamp = (int) $raw;
            if ($timestamp > 9999999999) {
                $timestamp = (int) floor($timestamp / 1000);
            }
            if ($timestamp > 0) {
                return gmdate('Y-m-d H:i:s', $timestamp);
            }
        }

        $normalized = trim((string) str_replace('T', ' ', $raw));
        if ($normalized === '') {
            return '';
        }
        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $normalized) === 1) {
            return $normalized . ' 00:00:00';
        }
        if (preg_match('/^\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}(?::\d{2})?$/', $normalized) === 1) {
            $time_part = trim((string) substr($normalized, 10));
            if (preg_match('/^\d{1,2}:\d{2}$/', $time_part) === 1) {
                $time_part .= ':00';
            }
            return trim(substr($normalized, 0, 10) . ' ' . $time_part);
        }

        $timestamp = strtotime($normalized);
        if ($timestamp !== false && $timestamp > 0) {
            $has_time = preg_match('/\b\d{1,2}:\d{2}(:\d{2})?\b/', $normalized) === 1;
            return $has_time ? wp_date('Y-m-d H:i:s', $timestamp) : wp_date('Y-m-d 00:00:00', $timestamp);
        }

        return '';
    }


    private function sync_event_price_with_ticket_request($is_events_profile = false) {
        if (
            !$is_events_profile
            || !function_exists('nexdirectory_is_pro_feature_enabled')
            || !nexdirectory_is_pro_feature_enabled('ticketing')
        ) {
            return;
        }

        do_action('nexdirectory_extension_sync_event_price_request', $this);
    }


    private function get_event_price_from_ticket_meta($post_id, $fallback = '') {
        $post_id = absint((int) $post_id);
        $fallback = sanitize_text_field((string) $fallback);
        if (
            $post_id <= 0
            || !function_exists('nexdirectory_is_pro_feature_enabled')
            || !nexdirectory_is_pro_feature_enabled('ticketing')
        ) {
            return $fallback;
        }

        return sanitize_text_field((string) apply_filters(
            'nexdirectory_extension_event_price_display',
            $fallback,
            $post_id,
            $this
        ));
    }


    private function is_sparse_autosave_value($value) {
        if (is_array($value)) {
            foreach ($value as $item) {
                if (!$this->is_sparse_autosave_value($item)) {
                    return false;
                }
            }
            return true;
        }
        if (is_bool($value) || is_int($value) || is_float($value)) {
            return false;
        }
        return trim(nexdirectory_scalar_to_string($value)) === '';
    }


    private function is_empty_json_collection_value($value) {
        $normalized = strtolower(trim(nexdirectory_scalar_to_string($value)));
        return in_array($normalized, ['', '[]', '{}', 'null'], true);
    }

    private function should_skip_sparse_meta_update($post_id, $meta_key, $value, $is_existing_update = false) {
        if (!$is_existing_update) {
            return false;
        }
        if (!$this->is_sparse_autosave_value($value)) {
            return false;
        }

        $meta_key = sanitize_key((string) $meta_key);
        $protected_sparse_keys = [
            'nexdirectory_start',
            'nexdirectory_end',
            'nexdirectory_cover_image_url',
            'nexdirectory_gallery_urls',
            'nexdirectory_video_urls',
            'nexdirectory_address',
            'nexdirectory_city',
            'nexdirectory_country',
            'nexdirectory_lat',
            'nexdirectory_lng',
        ];
        if (!in_array($meta_key, $protected_sparse_keys, true)) {
            return false;
        }

        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return false;
        }

        $existing_value = trim(nexdirectory_scalar_to_string(get_post_meta($post_id, $meta_key, true)));
        return $existing_value !== '';
    }


    private function sanitize_event_seo_title($value) {
        $title = sanitize_text_field((string) $value);
        if (function_exists('mb_substr')) {
            $title = mb_substr($title, 0, 60);
        } else {
            $title = substr($title, 0, 60);
        }
        return trim((string) $title);
    }


    private function sanitize_event_seo_description($value) {
        $description = nexdirectory_sanitize_textarea((string) $value);
        $description = trim((string) preg_replace('/\s+/u', ' ', (string) $description));
        if (function_exists('mb_substr')) {
            $description = mb_substr((string) $description, 0, 158);
        } else {
            $description = substr((string) $description, 0, 158);
        }
        return trim((string) $description);
    }


    private function build_submit_form_prefill($post_id = 0, $profile_key = '') {
        $post_id = absint($post_id);
        $custom_form_fields = [];
        $resolved_profile_key = $this->resolve_profile_key_for_post($post_id, $profile_key);
        $profile_ui = $this->get_site_profile_ui_labels($resolved_profile_key);
        $is_events_profile = (string) ($profile_ui['is_events_profile'] ?? '1') === '1';
        $email_defaults = $this->get_default_event_email_templates();
        $defaults = [
            'is_edit' => false,
            'edit_event_id' => 0,
            'post_title' => '',
            'post_content' => '',
            'post_excerpt' => '',
            'nexdirectory_cover_image_url' => '',
            'nexdirectory_gallery_urls' => '',
            'nexdirectory_video_urls' => '',
            'nexdirectory_contact_phone' => '',
            'nexdirectory_contact_email' => wp_get_current_user()->user_email ?? '',
            'nexdirectory_city' => '',
            'nexdirectory_address' => '',
            'nexdirectory_venue' => '',
            'nexdirectory_company_name' => '',
            'nexdirectory_country' => '',
            'nexdirectory_lat' => '',
            'nexdirectory_lng' => '',
            'nexdirectory_directory_business_type' => 'independent-business',
            'nexdirectory_directory_listing_status' => 'active',
            'nexdirectory_directory_contact_person' => '',
            'nexdirectory_directory_service_area' => '',
            'nexdirectory_directory_operating_hours' => '',
            'nexdirectory_directory_response_time' => '',
            'nexdirectory_directory_established_year' => '',
            'nexdirectory_directory_pricing_summary' => '',
            'nexdirectory_directory_appointment_url' => '',
            'nexdirectory_directory_whatsapp_url' => '',
            'nexdirectory_directory_services' => '',
            'nexdirectory_directory_highlights' => '',
            'nexdirectory_directory_certifications' => '',
            'nexdirectory_external_url' => '',
            'nexdirectory_event_stream_url' => '',
            'nexdirectory_job_type' => '',
            'nexdirectory_job_work_mode' => '',
            'nexdirectory_job_posted_date' => '',
            'nexdirectory_job_compensation_type' => '',
            'nexdirectory_job_contract_duration' => '',
            'nexdirectory_job_working_hours' => '',
            'nexdirectory_job_salary' => '',
            'nexdirectory_job_apply_url' => '',
            'nexdirectory_job_application_email' => '',
            'nexdirectory_job_closing_date' => '',
            'nexdirectory_job_department' => '',
            'nexdirectory_job_experience_level' => '',
            'nexdirectory_job_reporting_to' => '',
            'nexdirectory_job_remote_policy' => '',
            'nexdirectory_job_visa_sponsorship' => '0',
            'nexdirectory_job_open_positions' => '',
            'nexdirectory_job_interview_process' => '',
            'nexdirectory_job_expected_start_date' => '',
            'nexdirectory_job_skills' => '',
            'nexdirectory_job_responsibilities' => '',
            'nexdirectory_job_requirements' => '',
            'nexdirectory_job_benefits' => '',
            'nexdirectory_job_equity' => '',
            'nexdirectory_job_bonus' => '',
            'nexdirectory_job_recruitment_timeline' => '',
            'nexdirectory_job_company_intro_video' => '',
            'nexdirectory_job_role_overview_video' => '',
            'nexdirectory_job_company_website' => '',
            'nexdirectory_job_company_size' => '',
            'nexdirectory_job_industry' => '',
            'nexdirectory_market_condition' => '',
            'nexdirectory_market_price_type' => 'fixed',
            'nexdirectory_market_quantity' => '',
            'nexdirectory_market_sku' => '',
            'nexdirectory_market_brand' => '',
            'nexdirectory_market_model' => '',
            'nexdirectory_market_year' => '',
            'nexdirectory_market_seller_type' => 'private',
            'nexdirectory_market_listing_status' => 'active',
            'nexdirectory_market_expires_on' => '',
            'nexdirectory_market_delivery_methods' => '',
            'nexdirectory_market_returns_accepted' => '0',
            'nexdirectory_market_accept_offers' => '1',
            'nexdirectory_market_payment_methods' => '',
            'nexdirectory_market_condition_notes' => '',
            'nexdirectory_market_shipping_notes' => '',
            'nexdirectory_event_mode_option' => 'in-person',
            'nexdirectory_price_simple' => 'free',
            'event_type_term_id' => 0,
            'event_tags' => '',
            'nexdirectory_start_local' => '',
            'nexdirectory_end_local' => '',
            'nexdirectory_start_raw' => '',
            'nexdirectory_end_raw' => '',
            'nexdirectory_timezone' => '',
            'nexdirectory_capacity' => '',
            'nexdirectory_attendee_visibility' => 'counts',
            'nexdirectory_price' => '',
            'nexdirectory_ticket_sales_enabled' => '0',
            'nexdirectory_ticket_price' => '',
            'nexdirectory_ticket_button_label' => '',
            'nexdirectory_meta_title' => '',
            'nexdirectory_meta_description' => '',
            'nexdirectory_official_event' => '0',
            'nexdirectory_is_recurring' => '0',
            'nexdirectory_recurrence_until' => '',
            'nexdirectory_email_subject_confirmation' => (string) get_option('nexdirectory_email_subject_confirmation', (string) ($email_defaults['nexdirectory_email_subject_confirmation'] ?? '')),
            'nexdirectory_email_subject_reminder' => (string) get_option('nexdirectory_email_subject_reminder', (string) ($email_defaults['nexdirectory_email_subject_reminder'] ?? '')),
            'nexdirectory_email_subject_update' => (string) get_option('nexdirectory_email_subject_update', (string) ($email_defaults['nexdirectory_email_subject_update'] ?? '')),
            'nexdirectory_email_subject_cancellation' => (string) get_option('nexdirectory_email_subject_cancellation', (string) ($email_defaults['nexdirectory_email_subject_cancellation'] ?? '')),
            'nexdirectory_email_body_confirmation' => (string) get_option('nexdirectory_email_body_confirmation', (string) ($email_defaults['nexdirectory_email_body_confirmation'] ?? '')),
            'nexdirectory_email_body_reminder' => (string) get_option('nexdirectory_email_body_reminder', (string) ($email_defaults['nexdirectory_email_body_reminder'] ?? '')),
            'nexdirectory_email_body_update' => (string) get_option('nexdirectory_email_body_update', (string) ($email_defaults['nexdirectory_email_body_update'] ?? '')),
            'nexdirectory_email_body_cancellation' => (string) get_option('nexdirectory_email_body_cancellation', (string) ($email_defaults['nexdirectory_email_body_cancellation'] ?? '')),
            'sessions' => [],
            'key_speakers' => [],
            'custom_form_fields' => [],
            'custom_fields_values' => [],
            'custom_taxonomy_terms' => [],
        ];
        foreach (['confirmation', 'reminder', 'update', 'cancellation'] as $template_key) {
            $subject_option_key = 'nexdirectory_email_subject_' . $template_key;
            if (trim((string) ($defaults[$subject_option_key] ?? '')) === '') {
                $defaults[$subject_option_key] = (string) ($email_defaults[$subject_option_key] ?? '');
            }
            $body_option_key = 'nexdirectory_email_body_' . $template_key;
            if (trim((string) ($defaults[$body_option_key] ?? '')) === '') {
                $defaults[$body_option_key] = (string) ($email_defaults[$body_option_key] ?? '');
            }
        }
        if ($post_id <= 0) {
            return $defaults;
        }
        if (get_post_type($post_id) !== self::POST_TYPE) {
            return $defaults;
        }
        if (!$this->user_can_edit_event($post_id)) {
            $current_user_id = (int) get_current_user_id();
            if ($current_user_id <= 0 || !current_user_can('edit_post', $post_id)) {
                return $defaults;
            }
        }
        $prefill_target_post_id = $post_id;
        $post_id = $this->resolve_submit_prefill_source_post_id($post_id);

        $prefill = $defaults;
        $prefill['is_edit'] = true;
        $prefill['edit_event_id'] = $prefill_target_post_id;
        $prefill['post_title'] = (string) $this->recover_existing_event_title_value($post_id, (string) get_the_title($post_id));
        $prefill['post_content'] = (string) get_post_field('post_content', $post_id);
        $prefill['post_excerpt'] = (string) get_post_field('post_excerpt', $post_id);
        if (trim((string) $prefill['post_title']) === '' && trim((string) $prefill['post_content']) !== '') {
            $prefill['post_title'] = wp_trim_words(nexdirectory_strip_all_tags((string) $prefill['post_content']), 8, '');
        }
        if (trim((string) $prefill['post_title']) === '') {
            /* translators: %d: Placeholder value. */
            $prefill['post_title'] = sprintf(__('Untitled Listing #%d', 'nexdirectory'), $prefill_target_post_id);
        }
        if (trim($prefill['post_excerpt']) === '') {
            if (trim($prefill['post_content']) !== '') {
                $prefill['post_excerpt'] = wp_trim_words(nexdirectory_strip_all_tags((string) $prefill['post_content']), 26);
            }
        }
        $prefill['nexdirectory_cover_image_url'] = (string) $this->get_event_cover_image_url($post_id, 'full', 'thumbnail');
        if ($prefill['nexdirectory_cover_image_url'] === '' && has_post_thumbnail($post_id)) {
            $prefill['nexdirectory_cover_image_url'] = esc_url_raw((string) get_the_post_thumbnail_url($post_id, 'full'));
        }
        $prefill['nexdirectory_gallery_urls'] = implode("\n", $this->get_event_gallery_image_urls($post_id, 'thumbnail'));
        if ($prefill['nexdirectory_cover_image_url'] === '' && trim((string) $prefill['nexdirectory_gallery_urls']) !== '') {
            $gallery_rows = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', (string) $prefill['nexdirectory_gallery_urls']) ?: [])));
            if (!empty($gallery_rows)) {
                $prefill['nexdirectory_cover_image_url'] = esc_url_raw((string) $gallery_rows[0]);
            }
        }
        $prefill['nexdirectory_video_urls'] = (string) get_post_meta($post_id, 'nexdirectory_video_urls', true);
        $prefill['nexdirectory_contact_phone'] = (string) get_post_meta($post_id, 'nexdirectory_contact_phone', true);
        $prefill['nexdirectory_contact_email'] = (string) get_post_meta($post_id, 'nexdirectory_contact_email', true);
        if ($prefill['nexdirectory_contact_email'] === '') {
            $prefill['nexdirectory_contact_email'] = (string) (wp_get_current_user()->user_email ?? '');
        }
        $prefill['nexdirectory_city'] = (string) get_post_meta($post_id, 'nexdirectory_city', true);
        $prefill['nexdirectory_address'] = (string) get_post_meta($post_id, 'nexdirectory_address', true);
        $prefill['nexdirectory_venue'] = (string) get_post_meta($post_id, 'nexdirectory_venue', true);
        $prefill['nexdirectory_company_name'] = (string) get_post_meta($post_id, 'nexdirectory_company_name', true);
        $prefill['nexdirectory_country'] = (string) get_post_meta($post_id, 'nexdirectory_country', true);
        $prefill['nexdirectory_lat'] = (string) get_post_meta($post_id, 'nexdirectory_lat', true);
        $prefill['nexdirectory_lng'] = (string) get_post_meta($post_id, 'nexdirectory_lng', true);
        $prefill['nexdirectory_directory_business_type'] = (string) get_post_meta($post_id, 'nexdirectory_directory_business_type', true);
        $prefill['nexdirectory_directory_listing_status'] = (string) get_post_meta($post_id, 'nexdirectory_directory_listing_status', true);
        $prefill['nexdirectory_directory_contact_person'] = (string) get_post_meta($post_id, 'nexdirectory_directory_contact_person', true);
        $prefill['nexdirectory_directory_service_area'] = (string) get_post_meta($post_id, 'nexdirectory_directory_service_area', true);
        $prefill['nexdirectory_directory_operating_hours'] = (string) get_post_meta($post_id, 'nexdirectory_directory_operating_hours', true);
        $prefill['nexdirectory_directory_response_time'] = (string) get_post_meta($post_id, 'nexdirectory_directory_response_time', true);
        $prefill['nexdirectory_directory_established_year'] = (string) get_post_meta($post_id, 'nexdirectory_directory_established_year', true);
        $prefill['nexdirectory_directory_pricing_summary'] = (string) get_post_meta($post_id, 'nexdirectory_directory_pricing_summary', true);
        $prefill['nexdirectory_directory_appointment_url'] = (string) get_post_meta($post_id, 'nexdirectory_directory_appointment_url', true);
        $prefill['nexdirectory_directory_whatsapp_url'] = (string) get_post_meta($post_id, 'nexdirectory_directory_whatsapp_url', true);
        $prefill['nexdirectory_directory_services'] = (string) get_post_meta($post_id, 'nexdirectory_directory_services', true);
        $prefill['nexdirectory_directory_highlights'] = (string) get_post_meta($post_id, 'nexdirectory_directory_highlights', true);
        $prefill['nexdirectory_directory_certifications'] = (string) get_post_meta($post_id, 'nexdirectory_directory_certifications', true);
        $prefill['nexdirectory_external_url'] = (string) get_post_meta($post_id, 'nexdirectory_external_url', true);
        $prefill['nexdirectory_event_stream_url'] = (string) get_post_meta($post_id, 'nexdirectory_event_stream_url', true);
        $prefill['nexdirectory_job_type'] = (string) get_post_meta($post_id, 'nexdirectory_job_type', true);
        $prefill['nexdirectory_job_work_mode'] = (string) get_post_meta($post_id, 'nexdirectory_job_work_mode', true);
        $prefill['nexdirectory_job_posted_date'] = (string) get_post_meta($post_id, 'nexdirectory_job_posted_date', true);
        $prefill['nexdirectory_job_compensation_type'] = (string) get_post_meta($post_id, 'nexdirectory_job_compensation_type', true);
        $prefill['nexdirectory_job_contract_duration'] = (string) get_post_meta($post_id, 'nexdirectory_job_contract_duration', true);
        $prefill['nexdirectory_job_working_hours'] = (string) get_post_meta($post_id, 'nexdirectory_job_working_hours', true);
        $prefill['nexdirectory_job_salary'] = (string) get_post_meta($post_id, 'nexdirectory_job_salary', true);
        $prefill['nexdirectory_job_apply_url'] = (string) get_post_meta($post_id, 'nexdirectory_job_apply_url', true);
        $prefill['nexdirectory_job_application_email'] = (string) get_post_meta($post_id, 'nexdirectory_job_application_email', true);
        $prefill['nexdirectory_job_closing_date'] = (string) get_post_meta($post_id, 'nexdirectory_job_closing_date', true);
        $prefill['nexdirectory_job_department'] = (string) get_post_meta($post_id, 'nexdirectory_job_department', true);
        $prefill['nexdirectory_job_experience_level'] = (string) get_post_meta($post_id, 'nexdirectory_job_experience_level', true);
        $prefill['nexdirectory_job_reporting_to'] = (string) get_post_meta($post_id, 'nexdirectory_job_reporting_to', true);
        $prefill['nexdirectory_job_remote_policy'] = (string) get_post_meta($post_id, 'nexdirectory_job_remote_policy', true);
        $prefill['nexdirectory_job_visa_sponsorship'] = (string) get_post_meta($post_id, 'nexdirectory_job_visa_sponsorship', true);
        $prefill['nexdirectory_job_open_positions'] = (string) get_post_meta($post_id, 'nexdirectory_job_open_positions', true);
        $prefill['nexdirectory_job_interview_process'] = (string) get_post_meta($post_id, 'nexdirectory_job_interview_process', true);
        $prefill['nexdirectory_job_expected_start_date'] = (string) get_post_meta($post_id, 'nexdirectory_job_expected_start_date', true);
        $prefill['nexdirectory_job_skills'] = (string) get_post_meta($post_id, 'nexdirectory_job_skills', true);
        $prefill['nexdirectory_job_responsibilities'] = (string) get_post_meta($post_id, 'nexdirectory_job_responsibilities', true);
        $prefill['nexdirectory_job_requirements'] = (string) get_post_meta($post_id, 'nexdirectory_job_requirements', true);
        $prefill['nexdirectory_job_benefits'] = (string) get_post_meta($post_id, 'nexdirectory_job_benefits', true);
        $prefill['nexdirectory_job_equity'] = (string) get_post_meta($post_id, 'nexdirectory_job_equity', true);
        $prefill['nexdirectory_job_bonus'] = (string) get_post_meta($post_id, 'nexdirectory_job_bonus', true);
        $prefill['nexdirectory_job_recruitment_timeline'] = (string) get_post_meta($post_id, 'nexdirectory_job_recruitment_timeline', true);
        $prefill['nexdirectory_job_company_intro_video'] = (string) get_post_meta($post_id, 'nexdirectory_job_company_intro_video', true);
        $prefill['nexdirectory_job_role_overview_video'] = (string) get_post_meta($post_id, 'nexdirectory_job_role_overview_video', true);
        $prefill['nexdirectory_job_company_website'] = (string) get_post_meta($post_id, 'nexdirectory_job_company_website', true);
        $prefill['nexdirectory_job_company_size'] = (string) get_post_meta($post_id, 'nexdirectory_job_company_size', true);
        $prefill['nexdirectory_job_industry'] = (string) get_post_meta($post_id, 'nexdirectory_job_industry', true);
        $prefill['nexdirectory_market_condition'] = (string) get_post_meta($post_id, 'nexdirectory_market_condition', true);
        $prefill['nexdirectory_market_price_type'] = (string) get_post_meta($post_id, 'nexdirectory_market_price_type', true);
        $prefill['nexdirectory_market_quantity'] = (string) get_post_meta($post_id, 'nexdirectory_market_quantity', true);
        $prefill['nexdirectory_market_sku'] = (string) get_post_meta($post_id, 'nexdirectory_market_sku', true);
        $prefill['nexdirectory_market_brand'] = (string) get_post_meta($post_id, 'nexdirectory_market_brand', true);
        $prefill['nexdirectory_market_model'] = (string) get_post_meta($post_id, 'nexdirectory_market_model', true);
        $prefill['nexdirectory_market_year'] = (string) get_post_meta($post_id, 'nexdirectory_market_year', true);
        $prefill['nexdirectory_market_seller_type'] = (string) get_post_meta($post_id, 'nexdirectory_market_seller_type', true);
        $prefill['nexdirectory_market_listing_status'] = (string) get_post_meta($post_id, 'nexdirectory_market_listing_status', true);
        $prefill['nexdirectory_market_expires_on'] = (string) get_post_meta($post_id, 'nexdirectory_market_expires_on', true);
        $prefill['nexdirectory_market_delivery_methods'] = (string) get_post_meta($post_id, 'nexdirectory_market_delivery_methods', true);
        $prefill['nexdirectory_market_returns_accepted'] = (string) get_post_meta($post_id, 'nexdirectory_market_returns_accepted', true);
        $prefill['nexdirectory_market_accept_offers'] = (string) get_post_meta($post_id, 'nexdirectory_market_accept_offers', true);
        $prefill['nexdirectory_market_payment_methods'] = (string) get_post_meta($post_id, 'nexdirectory_market_payment_methods', true);
        $prefill['nexdirectory_market_condition_notes'] = (string) get_post_meta($post_id, 'nexdirectory_market_condition_notes', true);
        $prefill['nexdirectory_market_shipping_notes'] = (string) get_post_meta($post_id, 'nexdirectory_market_shipping_notes', true);
        $prefill['nexdirectory_start_raw'] = $this->normalize_prefill_datetime_meta_value((string) get_post_meta($post_id, 'nexdirectory_start', true));
        $prefill['nexdirectory_end_raw'] = $this->normalize_prefill_datetime_meta_value((string) get_post_meta($post_id, 'nexdirectory_end', true));
        if ($prefill['nexdirectory_market_expires_on'] === '' && preg_match('/^\d{4}-\d{2}-\d{2}/', (string) $prefill['nexdirectory_end_raw'], $market_end_match)) {
            $prefill['nexdirectory_market_expires_on'] = (string) ($market_end_match[0] ?? '');
        }
        $prefill['nexdirectory_start_local'] = $this->format_datetime_local_input((string) $prefill['nexdirectory_start_raw']);
        $prefill['nexdirectory_end_local'] = $this->format_datetime_local_input((string) $prefill['nexdirectory_end_raw']);
        $prefill['nexdirectory_timezone'] = (string) get_post_meta($post_id, 'nexdirectory_timezone', true);
        if ($prefill['nexdirectory_timezone'] === '') {
            $prefill['nexdirectory_timezone'] = $this->sanitize_timezone_value((string) get_option('timezone_string', ''));
        }
        $prefill['nexdirectory_capacity'] = (string) get_post_meta($post_id, 'nexdirectory_capacity', true);
        $prefill['nexdirectory_attendee_visibility'] = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_attendee_visibility', true));
        if (!in_array($prefill['nexdirectory_attendee_visibility'], ['off', 'counts', 'members', 'public'], true)) {
            $prefill['nexdirectory_attendee_visibility'] = 'counts';
        }
        $prefill['nexdirectory_price'] = (string) get_post_meta($post_id, 'nexdirectory_price', true);
        $prefill['nexdirectory_ticket_sales_enabled'] = (string) get_post_meta($post_id, 'nexdirectory_ticket_sales_enabled', true);
        $prefill['nexdirectory_ticket_price'] = (string) get_post_meta($post_id, 'nexdirectory_ticket_price', true);
        $prefill['nexdirectory_ticket_button_label'] = (string) get_post_meta($post_id, 'nexdirectory_ticket_button_label', true);
        $prefill['nexdirectory_meta_title'] = (string) get_post_meta($post_id, 'nexdirectory_meta_title', true);
        $prefill['nexdirectory_meta_description'] = (string) get_post_meta($post_id, 'nexdirectory_meta_description', true);
        $requested_official_pending = (string) get_post_meta($post_id, 'nexdirectory_official_requested', true) === '1';
        $prefill['nexdirectory_official_event'] = ($this->get_official_event_flag($post_id) || $requested_official_pending) ? '1' : '0';
        $prefill['nexdirectory_recurrence_until'] = (string) get_post_meta($post_id, 'nexdirectory_recurrence_until', true);
        $prefill['nexdirectory_email_subject_confirmation'] = (string) get_post_meta($post_id, 'nexdirectory_email_subject_confirmation', true);
        $prefill['nexdirectory_email_subject_reminder'] = (string) get_post_meta($post_id, 'nexdirectory_email_subject_reminder', true);
        $prefill['nexdirectory_email_subject_update'] = (string) get_post_meta($post_id, 'nexdirectory_email_subject_update', true);
        $prefill['nexdirectory_email_subject_cancellation'] = (string) get_post_meta($post_id, 'nexdirectory_email_subject_cancellation', true);
        $prefill['nexdirectory_email_body_confirmation'] = (string) get_post_meta($post_id, 'nexdirectory_email_body_confirmation', true);
        $prefill['nexdirectory_email_body_reminder'] = (string) get_post_meta($post_id, 'nexdirectory_email_body_reminder', true);
        $prefill['nexdirectory_email_body_update'] = (string) get_post_meta($post_id, 'nexdirectory_email_body_update', true);
        $prefill['nexdirectory_email_body_cancellation'] = (string) get_post_meta($post_id, 'nexdirectory_email_body_cancellation', true);
        if ($prefill['nexdirectory_market_price_type'] === '') {
            $prefill['nexdirectory_market_price_type'] = 'fixed';
        }
        if ($prefill['nexdirectory_market_seller_type'] === '') {
            $prefill['nexdirectory_market_seller_type'] = 'private';
        }
        if ($prefill['nexdirectory_market_listing_status'] === '') {
            $prefill['nexdirectory_market_listing_status'] = 'active';
        }
        if ($prefill['nexdirectory_directory_business_type'] === '') {
            $prefill['nexdirectory_directory_business_type'] = 'independent-business';
        }
        if ($prefill['nexdirectory_directory_listing_status'] === '') {
            $prefill['nexdirectory_directory_listing_status'] = 'active';
        }
        if ($prefill['nexdirectory_market_returns_accepted'] !== '1') {
            $prefill['nexdirectory_market_returns_accepted'] = '0';
        }
        if ($prefill['nexdirectory_market_accept_offers'] !== '0') {
            $prefill['nexdirectory_market_accept_offers'] = '1';
        }
        if ($prefill['nexdirectory_ticket_sales_enabled'] !== '1') {
            $prefill['nexdirectory_ticket_sales_enabled'] = '0';
        }
        foreach (['confirmation', 'reminder', 'update', 'cancellation'] as $template_key) {
            $subject_option_key = 'nexdirectory_email_subject_' . $template_key;
            if (trim((string) $prefill[$subject_option_key]) === '') {
                $subject_default = (string) ($email_defaults[$subject_option_key] ?? '');
                $subject_value = trim((string) get_option($subject_option_key, $subject_default));
                $prefill[$subject_option_key] = $subject_value !== '' ? $subject_value : $subject_default;
            }
            $body_option_key = 'nexdirectory_email_body_' . $template_key;
            if (trim((string) $prefill[$body_option_key]) === '') {
                $body_default = (string) ($email_defaults[$body_option_key] ?? '');
                $body_value = trim((string) get_option($body_option_key, $body_default));
                $prefill[$body_option_key] = $body_value !== '' ? $body_value : $body_default;
            }
        }

        $saved_price_simple = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_price_simple', true));
        if (in_array($saved_price_simple, ['free', 'paid'], true)) {
            $prefill['nexdirectory_price_simple'] = $saved_price_simple;
        }

        $mode_terms = wp_get_post_terms($post_id, self::TAX_MODE, ['fields' => 'all']);
        if (!is_wp_error($mode_terms) && !empty($mode_terms)) {
            $mode_names = strtolower(implode(' ', array_map(static function ($term) {
                return (string) ($term->name ?? '');
            }, $mode_terms)));
            if (strpos($mode_names, 'online') !== false || strpos($mode_names, 'stream') !== false || strpos($mode_names, 'video') !== false || strpos($mode_names, 'hybrid') !== false) {
                $prefill['nexdirectory_event_mode_option'] = 'video-stream';
            }
        }

        $price_terms = wp_get_post_terms($post_id, self::TAX_PRICE, ['fields' => 'all']);
        if (!is_wp_error($price_terms) && !empty($price_terms) && $saved_price_simple === '') {
            $price_names = strtolower(implode(' ', array_map(static function ($term) {
                return (string) ($term->name ?? '');
            }, $price_terms)));
            if (strpos($price_names, 'paid') !== false || strpos($price_names, 'premium') !== false || strpos($price_names, 'cost') !== false) {
                $prefill['nexdirectory_price_simple'] = 'paid';
            }
        }

        $type_ids = wp_get_post_terms($post_id, self::TAX_TYPE, ['fields' => 'ids']);
        if (!is_wp_error($type_ids) && !empty($type_ids)) {
            $prefill['event_type_term_id'] = (int) $type_ids[0];
        }
        $tag_names = wp_get_post_terms($post_id, self::TAX_TAG, ['fields' => 'names']);
        if (!is_wp_error($tag_names) && !empty($tag_names)) {
            $prefill['event_tags'] = implode(', ', array_map('nexdirectory_sanitize_text', $tag_names));
        }

        $recurrence = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_recurrence', true));
        $sessions_raw = json_decode((string) get_post_meta($post_id, 'nexdirectory_sessions', true), true);
        $sessions = [];
        if (is_array($sessions_raw)) {
            foreach ($sessions_raw as $session) {
                if (!is_array($session)) {
                    continue;
                }
                $sessions[] = [
                    'title' => sanitize_text_field((string) ($session['title'] ?? '')),
                    'start' => $this->format_datetime_local_input((string) ($session['start'] ?? '')),
                    'end' => $this->format_datetime_local_input((string) ($session['end'] ?? '')),
                    'location' => sanitize_text_field((string) ($session['location'] ?? '')),
                    'speaker' => sanitize_text_field((string) ($session['speaker'] ?? '')),
                    'day' => sanitize_text_field((string) ($session['day'] ?? '')),
                    'notes' => nexdirectory_sanitize_textarea((string) ($session['notes'] ?? '')),
                    'attachments' => implode("\n", array_values(array_filter(array_map('esc_url_raw', (array) ($session['attachments'] ?? []))))),
                    'session_key' => sanitize_key((string) ($session['session_key'] ?? '')),
                ];
            }
        }
        $key_speakers = $this->parse_key_speakers((string) get_post_meta($post_id, 'nexdirectory_key_speakers', true));
        if ($is_events_profile) {
            if (!empty($sessions) || ($recurrence !== '' && $recurrence !== 'none')) {
                $prefill['nexdirectory_is_recurring'] = '1';
                $prefill['sessions'] = $sessions;
            }
            $prefill['key_speakers'] = $key_speakers;
        } else {
            $prefill['nexdirectory_is_recurring'] = '0';
            $prefill['nexdirectory_recurrence_until'] = '';
            $prefill['sessions'] = [];
            $prefill['key_speakers'] = [];
        }
        $prefill['custom_form_fields'] = $this->get_post_custom_field_schema($post_id, true);
        $custom_form_fields = $prefill['custom_form_fields'];
        $prefill['custom_fields_values'] = $this->get_post_custom_field_values($post_id, $custom_form_fields);
        foreach ($this->get_custom_taxonomy_definitions_for_submit() as $taxonomy_row) {
            $taxonomy_slug = sanitize_key((string) ($taxonomy_row['slug'] ?? ''));
            if ($taxonomy_slug === '' || !taxonomy_exists($taxonomy_slug)) {
                continue;
            }
            $term_ids = wp_get_post_terms($post_id, $taxonomy_slug, ['fields' => 'ids']);
            if (is_wp_error($term_ids)) {
                continue;
            }
            $prefill['custom_taxonomy_terms'][$taxonomy_slug] = array_values(array_filter(array_map('absint', (array) $term_ids)));
        }

        return $prefill;
    }


    private function normalize_submitted_youtube_id($value) {
        $normalized = preg_replace('/[^A-Za-z0-9_-]/', '', (string) $value);
        return substr((string) $normalized, 0, 32);
    }


    private function save_custom_taxonomy_terms_from_request($post_id) {
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Called only after submit/update nonce verification.
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return;
        }
        foreach ($this->get_custom_taxonomy_definitions_for_submit() as $taxonomy_row) {
            $taxonomy_slug = sanitize_key((string) ($taxonomy_row['slug'] ?? ''));
            if ($taxonomy_slug === '' || !taxonomy_exists($taxonomy_slug)) {
                continue;
            }
            $field_name = 'nexdirectory_tax_' . $taxonomy_slug;
            $raw_values = isset($_POST[$field_name]) ? wp_unslash($_POST[$field_name]) : [];
            if (!is_array($raw_values)) {
                $raw_values = [$raw_values];
            }
            $raw_values = array_values(array_filter(array_map(static function ($value) {
                return sanitize_text_field((string) $value);
            }, $raw_values), static function ($value) {
                return $value !== '';
            }));
            if (empty($raw_values)) {
                wp_set_post_terms($post_id, [], $taxonomy_slug, false);
                continue;
            }
            $this->set_terms_from_values($post_id, $taxonomy_slug, $raw_values);
        }
        // phpcs:enable WordPress.Security.NonceVerification.Missing
    }


    private function get_or_create_term_id($taxonomy, $name) {
        $name = trim((string) $name);
        if ($name === '') {
            return 0;
        }
        $term = get_term_by('name', $name, $taxonomy);
        if ($term && !is_wp_error($term)) {
            return (int) $term->term_id;
        }
        $created = wp_insert_term($name, $taxonomy);
        if (is_wp_error($created)) {
            return 0;
        }
        return (int) ($created['term_id'] ?? 0);
    }


    private function build_video_urls_from_fields($stream_url, $youtube_id_one, $youtube_id_two, $raw_video_urls = '') {
        $urls = [];
        $stream_url = esc_url_raw((string) $stream_url);
        if ($stream_url !== '') {
            $urls[] = $stream_url;
        }
        if ($youtube_id_one !== '') {
            $urls[] = 'https://www.youtube.com/watch?v=' . $youtube_id_one;
        }
        if ($youtube_id_two !== '') {
            $urls[] = 'https://www.youtube.com/watch?v=' . $youtube_id_two;
        }
        foreach ($this->parse_multiline_urls((string) $raw_video_urls) as $existing) {
            $urls[] = esc_url_raw((string) $existing);
        }
        return implode("\n", array_values(array_unique(array_filter($urls))));
    }


    private function parse_event_tags_input($raw_input, $max_tags = 8, $max_length = 40) {
        $max_tags = max(1, (int) $max_tags);
        $max_length = max(1, (int) $max_length);
        $raw_input = (string) $raw_input;
        if (trim($raw_input) === '') {
            return [];
        }

        $parts = array_map(static function ($item) {
            return sanitize_text_field((string) $item);
        }, explode(',', $raw_input));
        $parts = array_values(array_filter(array_map('trim', $parts), static function ($item) {
            return $item !== '';
        }));

        if (count($parts) > $max_tags) {
            return new WP_Error(
                'nexdirectory_tags_limit',
                /* translators: %d: Placeholder value. */
                sprintf(__('Use up to %d tags (comma separated).', 'nexdirectory'), $max_tags)
            );
        }

        foreach ($parts as $tag) {
            $length = function_exists('mb_strlen') ? mb_strlen($tag) : strlen($tag);
            if ($length > $max_length) {
                return new WP_Error(
                    'nexdirectory_tag_length',
                    /* translators: %d: Placeholder value. */
                    sprintf(__('Each tag must be %d characters or fewer.', 'nexdirectory'), $max_length)
                );
            }
        }

        return array_values(array_unique($parts));
    }


    private function hydrate_event_datetime_fields_from_request() {
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Called only from nonce-verified submit/update handlers.
        $start_date_raw = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_start_date'] ?? ''));
        $start_time_raw = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_start_time'] ?? ''));
        $end_date_raw = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_end_date'] ?? ''));
        $end_time_raw = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_end_time'] ?? ''));

        if ((string) $start_date_raw !== '' || (string) $start_time_raw !== '') {
            $_POST['nexdirectory_start'] = $this->build_event_datetime_from_inputs($start_date_raw, $start_time_raw);
        } else {
            $_POST['nexdirectory_start'] = '';
        }

        $combined_start_raw = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_start'] ?? ''));
        if ((string) $end_date_raw === '' && (string) $end_time_raw !== '' && $combined_start_raw !== '') {
            $start_date_fallback = substr($combined_start_raw, 0, 10);
            if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $start_date_fallback) === 1) {
                $end_date_raw = $start_date_fallback;
            }
        }

        if ((string) $end_date_raw !== '' || (string) $end_time_raw !== '') {
            $_POST['nexdirectory_end'] = $this->build_event_datetime_from_inputs($end_date_raw, $end_time_raw);
        } else {
            $_POST['nexdirectory_end'] = '';
        }

        // phpcs:enable WordPress.Security.NonceVerification.Missing
    }


    private function maybe_fill_request_coordinates_from_location_fields() {
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Called only from nonce-verified submit/update handlers.
        $lat_raw = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_lat'] ?? ''));
        $lng_raw = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_lng'] ?? ''));
        if (is_numeric($lat_raw) && is_numeric($lng_raw)) {
            return;
        }

        $address = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_address'] ?? ''));
        $city = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_city'] ?? ''));
        $country = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_country'] ?? ''));
        $venue = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_venue'] ?? ''));

        $geo = $this->geocode_event_location_from_parts($address, $city, $country, $venue);
        if (!is_array($geo)) {
            return;
        }

        if (!is_numeric($lat_raw) && isset($geo['lat']) && is_numeric($geo['lat'])) {
            $_POST['nexdirectory_lat'] = (string) $geo['lat'];
        }
        if (!is_numeric($lng_raw) && isset($geo['lng']) && is_numeric($geo['lng'])) {
            $_POST['nexdirectory_lng'] = (string) $geo['lng'];
        }
        if ($city === '' && !empty($geo['city'])) {
            $_POST['nexdirectory_city'] = sanitize_text_field((string) $geo['city']);
        }
        if ($country === '' && !empty($geo['country'])) {
            $_POST['nexdirectory_country'] = sanitize_text_field((string) $geo['country']);
        }
        if ($address === '' && !empty($geo['address'])) {
            $_POST['nexdirectory_address'] = sanitize_text_field((string) $geo['address']);
        }
        // phpcs:enable WordPress.Security.NonceVerification.Missing
    }


    public function handle_submit_event() {
        check_admin_referer('nexdirectory_submit_event_nonce', 'nexdirectory_nonce');

        $is_ajax_submit = sanitize_key((string) wp_unslash($_POST['nexdirectory_ajax'] ?? '')) === '1';
        $edit_post_id = isset($_POST['nexdirectory_edit_post_id']) ? absint((int) wp_unslash($_POST['nexdirectory_edit_post_id'])) : 0;
        $autosave_post_id = isset($_POST['nexdirectory_autosave_post_id']) ? absint((int) wp_unslash($_POST['nexdirectory_autosave_post_id'])) : 0;
        $profile_source_post_id = $edit_post_id > 0 ? $edit_post_id : $autosave_post_id;
        $profile_key = $this->resolve_profile_key_for_post($profile_source_post_id);
        $ui_labels = $this->get_site_profile_ui_labels($profile_key);
        $profile_key = sanitize_key((string) ($ui_labels['profile_key'] ?? $profile_key));
        $is_events_profile = $profile_key === 'events';
        $is_job_profile = $profile_key === 'job_board';
        $is_directory_profile = $profile_key === 'directory';
        $is_marketplace_profile = $profile_key === 'marketplace';
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $singular_lower = sanitize_text_field((string) ($ui_labels['singular_lower'] ?? strtolower($singular_label)));

        if (!is_user_logged_in()) {
            wp_die(esc_html__('You must be logged in.', 'nexdirectory'));
        }
        $current_user_id = (int) get_current_user_id();
        $is_existing_edit_request = $edit_post_id > 0 && $this->user_can_edit_event($edit_post_id, $current_user_id);
        if (!$this->user_can_submit_events($current_user_id) && !$is_existing_edit_request) {
            wp_die(
                esc_html(
                    sprintf(
                        /* translators: %s: singular listing label */
                        esc_html__('You do not have permission to submit %s.', 'nexdirectory'),
                        strtolower($singular_label)
                    )
                )
            );
        }
        if (!$is_existing_edit_request && !$this->user_verified_for_first_submission($current_user_id)) {
            $resent = false;
            $last_sent = (int) get_user_meta($current_user_id, 'nexdirectory_email_verify_last_sent', true);
            if ($last_sent <= 0 || (time() - $last_sent) > 300) {
                $resent = (bool) $this->send_verification_email($current_user_id);
                if ($resent) {
                    update_user_meta($current_user_id, 'nexdirectory_email_verify_last_sent', (string) time());
                }
            }

            $this->redirect_with_message(
                wp_get_referer() ?: home_url('/'),
                $resent
                    ? __('Verify your email before first submission. A verification email was sent to your account inbox.', 'nexdirectory')
                    : __('Verify your email before first submission. Check your inbox or resend verification from account settings.', 'nexdirectory'),
                'error'
            );
        }

        if (sanitize_text_field((string) wp_unslash($_POST['nexdirectory_website'] ?? '')) !== '') {
            $this->increment_spam_counter();
            wp_die(esc_html__('Spam check failed.', 'nexdirectory'));
        }

        if ($this->is_submission_rate_limited()) {
            $this->increment_spam_counter();
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Too many attempts. Please wait and try again.', 'nexdirectory'), 'error');
        }
        $recaptcha_token = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_recaptcha_token'] ?? ''));
        if ($this->should_enforce_submit_form_recaptcha() && !$this->verify_recaptcha_token($recaptcha_token)) {
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('reCAPTCHA validation failed. Please try again.', 'nexdirectory'), 'error');
        }

        $this->hydrate_event_datetime_fields_from_request();
        $requested_official_value = $this->normalize_official_event_value(sanitize_key((string) wp_unslash($_POST['nexdirectory_official_event'] ?? '0')));
        $_POST['nexdirectory_official_event'] = $requested_official_value;
        $_POST['nexdirectory_timezone'] = $this->sanitize_timezone_value(wp_unslash($_POST['nexdirectory_timezone'] ?? ''));
        $_POST['nexdirectory_sessions'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_sessions'] ?? '[]'));
        $_POST['nexdirectory_key_speakers'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_key_speakers'] ?? '[]'));
        $_POST['nexdirectory_recurrence'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_recurrence'] ?? 'none'));
        $_POST['nexdirectory_recurrence_until'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_recurrence_until'] ?? ''));
        $_POST['nexdirectory_lock_pin'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_lock_pin'] ?? '0')) === '1' ? '1' : '0';
        $_POST['nexdirectory_price_simple'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_price_simple'] ?? 'free'));
        $_POST['nexdirectory_event_stream_url'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_event_stream_url'] ?? ''));
        $_POST['nexdirectory_meta_title'] = $this->sanitize_event_seo_title(wp_unslash($_POST['nexdirectory_meta_title'] ?? ''));
        $_POST['nexdirectory_meta_description'] = $this->sanitize_event_seo_description(wp_unslash($_POST['nexdirectory_meta_description'] ?? ''));
        if ((string) get_option('nexdirectory_enable_event_seo_fields', '1') !== '1') {
            unset($_POST['nexdirectory_meta_title'], $_POST['nexdirectory_meta_description']);
        }
        $_POST['nexdirectory_youtube_video_id_1'] = $this->normalize_submitted_youtube_id(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_youtube_video_id_1'] ?? '')));
        $_POST['nexdirectory_youtube_video_id_2'] = $this->normalize_submitted_youtube_id(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_youtube_video_id_2'] ?? '')));
        $_POST['nexdirectory_company_name'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_company_name'] ?? ''));
        $_POST['nexdirectory_directory_business_type'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_directory_business_type'] ?? 'independent-business'));
        $_POST['nexdirectory_directory_listing_status'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_directory_listing_status'] ?? 'active'));
        $_POST['nexdirectory_directory_contact_person'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_contact_person'] ?? ''));
        $_POST['nexdirectory_directory_service_area'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_service_area'] ?? ''));
        $_POST['nexdirectory_directory_operating_hours'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_operating_hours'] ?? ''));
        $_POST['nexdirectory_directory_response_time'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_response_time'] ?? ''));
        $_POST['nexdirectory_directory_established_year'] = absint((int) wp_unslash($_POST['nexdirectory_directory_established_year'] ?? 0));
        $_POST['nexdirectory_directory_pricing_summary'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_pricing_summary'] ?? ''));
        $_POST['nexdirectory_directory_appointment_url'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_directory_appointment_url'] ?? ''));
        $_POST['nexdirectory_directory_whatsapp_url'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_directory_whatsapp_url'] ?? ''));
        $_POST['nexdirectory_directory_services'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_directory_services'] ?? ''));
        $_POST['nexdirectory_directory_highlights'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_directory_highlights'] ?? ''));
        $_POST['nexdirectory_directory_certifications'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_directory_certifications'] ?? ''));
        $_POST['nexdirectory_job_type'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_type'] ?? ''));
        $_POST['nexdirectory_job_work_mode'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_job_work_mode'] ?? ''));
        $_POST['nexdirectory_job_posted_date'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_posted_date'] ?? ''));
        $_POST['nexdirectory_job_compensation_type'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_compensation_type'] ?? ''));
        $_POST['nexdirectory_job_contract_duration'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_contract_duration'] ?? ''));
        $_POST['nexdirectory_job_working_hours'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_working_hours'] ?? ''));
        $_POST['nexdirectory_job_salary'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_salary'] ?? ''));
        $_POST['nexdirectory_job_apply_url'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_job_apply_url'] ?? ''));
        $_POST['nexdirectory_job_application_email'] = sanitize_email((string) wp_unslash($_POST['nexdirectory_job_application_email'] ?? ''));
        $_POST['nexdirectory_job_closing_date'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_closing_date'] ?? ''));
        $_POST['nexdirectory_job_department'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_department'] ?? ''));
        $_POST['nexdirectory_job_experience_level'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_experience_level'] ?? ''));
        $_POST['nexdirectory_job_reporting_to'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_reporting_to'] ?? ''));
        $_POST['nexdirectory_job_remote_policy'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_remote_policy'] ?? ''));
        $_POST['nexdirectory_job_visa_sponsorship'] = ((string) wp_unslash($_POST['nexdirectory_job_visa_sponsorship'] ?? '0')) === '1' ? '1' : '0';
        $_POST['nexdirectory_job_open_positions'] = absint((int) wp_unslash($_POST['nexdirectory_job_open_positions'] ?? 0));
        $_POST['nexdirectory_job_interview_process'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_job_interview_process'] ?? ''));
        $_POST['nexdirectory_job_expected_start_date'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_expected_start_date'] ?? ''));
        $_POST['nexdirectory_job_skills'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_skills'] ?? ''));
        $_POST['nexdirectory_job_responsibilities'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_job_responsibilities'] ?? ''));
        $_POST['nexdirectory_job_requirements'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_job_requirements'] ?? ''));
        $_POST['nexdirectory_job_benefits'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_job_benefits'] ?? ''));
        $_POST['nexdirectory_job_equity'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_equity'] ?? ''));
        $_POST['nexdirectory_job_bonus'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_bonus'] ?? ''));
        $_POST['nexdirectory_job_recruitment_timeline'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_job_recruitment_timeline'] ?? ''));
        $_POST['nexdirectory_job_company_intro_video'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_job_company_intro_video'] ?? ''));
        $_POST['nexdirectory_job_role_overview_video'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_job_role_overview_video'] ?? ''));
        $_POST['nexdirectory_job_company_website'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_job_company_website'] ?? ''));
        $_POST['nexdirectory_job_company_size'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_company_size'] ?? ''));
        $_POST['nexdirectory_job_industry'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_industry'] ?? ''));
        $_POST['nexdirectory_market_condition'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_condition'] ?? ''));
        $_POST['nexdirectory_market_price_type'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_price_type'] ?? 'fixed'));
        $_POST['nexdirectory_market_quantity'] = absint((int) wp_unslash($_POST['nexdirectory_market_quantity'] ?? 0));
        $_POST['nexdirectory_market_sku'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_sku'] ?? ''));
        $_POST['nexdirectory_market_brand'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_brand'] ?? ''));
        $_POST['nexdirectory_market_model'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_model'] ?? ''));
        $_POST['nexdirectory_market_year'] = absint((int) wp_unslash($_POST['nexdirectory_market_year'] ?? 0));
        $_POST['nexdirectory_market_seller_type'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_seller_type'] ?? 'private'));
        $_POST['nexdirectory_market_listing_status'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_listing_status'] ?? 'active'));
        $_POST['nexdirectory_market_expires_on'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_expires_on'] ?? ''));
        $_POST['nexdirectory_market_returns_accepted'] = ((string) wp_unslash($_POST['nexdirectory_market_returns_accepted'] ?? '0')) === '1' ? '1' : '0';
        $_POST['nexdirectory_market_accept_offers'] = ((string) wp_unslash($_POST['nexdirectory_market_accept_offers'] ?? '1')) === '0' ? '0' : '1';
        $_POST['nexdirectory_market_payment_methods'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_payment_methods'] ?? ''));
        $_POST['nexdirectory_market_condition_notes'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_market_condition_notes'] ?? ''));
        $_POST['nexdirectory_market_shipping_notes'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_market_shipping_notes'] ?? ''));
        $market_delivery_methods_raw = map_deep(wp_unslash($_POST['nexdirectory_market_delivery_methods'] ?? []), 'sanitize_text_field');
        if (!is_array($market_delivery_methods_raw)) {
            $market_delivery_methods_raw = preg_split('/[\s,]+/', (string) $market_delivery_methods_raw);
            if (!is_array($market_delivery_methods_raw)) {
                $market_delivery_methods_raw = [];
            }
        }
        $allowed_market_delivery_methods = ['pickup', 'shipping', 'local-delivery', 'digital'];
        $market_delivery_methods = array_values(array_unique(array_filter(array_map(static function ($method) use ($allowed_market_delivery_methods) {
            $normalized = sanitize_key((string) $method);
            return in_array($normalized, $allowed_market_delivery_methods, true) ? $normalized : '';
        }, $market_delivery_methods_raw))));
        $_POST['nexdirectory_market_delivery_methods'] = implode(',', $market_delivery_methods);
        $_POST['nexdirectory_listing_profile'] = $profile_key;
        $this->sync_event_price_with_ticket_request($is_events_profile);
        $job_company_website = (string) wp_unslash($_POST['nexdirectory_job_company_website'] ?? '');
        if ($is_job_profile && trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_external_url'] ?? ''))) === '' && $job_company_website !== '') {
            $_POST['nexdirectory_external_url'] = $job_company_website;
        }

        $intent = sanitize_key(wp_unslash($_POST['nexdirectory_intent'] ?? 'pending'));
        $post_status = in_array($intent, ['draft', 'preview'], true) ? 'draft' : 'pending';
        $requires_complete_submission = !in_array($intent, ['draft', 'preview'], true);
        $is_edit_mode = false;
        if ($edit_post_id > 0) {
            if (!$this->user_can_edit_event($edit_post_id)) {
                $this->redirect_with_message(
                    wp_get_referer() ?: home_url('/'),
                    /* translators: %s: Placeholder value. */
                    sprintf(__('You can only edit your own %s.', 'nexdirectory'), $singular_lower),
                    'error'
                );
            }
            $is_edit_mode = true;
        }
        $existing_post_id = 0;
        if ($is_edit_mode) {
            $existing_post_id = $edit_post_id;
        } elseif ($autosave_post_id > 0 && $this->user_can_edit_event($autosave_post_id)) {
            $existing_post_id = $autosave_post_id;
        }
        $title = sanitize_text_field((string) wp_unslash($_POST['post_title'] ?? ''));
        if ($existing_post_id > 0 && $title === '') {
            $title = $this->recover_existing_event_title_value($existing_post_id, (string) get_the_title($existing_post_id));
        }
        if ($existing_post_id > 0 && trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_start'] ?? ''))) === '') {
            $recovered_start = $this->recover_existing_event_datetime_value($existing_post_id, 'nexdirectory_start');
            if ($recovered_start !== '') {
                $_POST['nexdirectory_start'] = $recovered_start;
            }
        }
        if ($existing_post_id > 0 && trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_end'] ?? ''))) === '') {
            $recovered_end = $this->recover_existing_event_datetime_value($existing_post_id, 'nexdirectory_end');
            if ($recovered_end !== '') {
                $_POST['nexdirectory_end'] = $recovered_end;
            }
        }
        if ($requires_complete_submission && strlen($title) < 8) {
            /* translators: %s: Placeholder value. */
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), sprintf(__('%s name should be at least 8 characters.', 'nexdirectory'), $singular_label), 'error');
        }
        $contact_email_raw = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_contact_email'] ?? '')));
        $contact_email = sanitize_email($contact_email_raw);
        if ($contact_email_raw !== '' && !is_email($contact_email)) {
            if ($requires_complete_submission) {
                $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Please enter a valid contact email or leave it blank.', 'nexdirectory'), 'error');
            }
            $contact_email = '';
        }
        $_POST['nexdirectory_contact_email'] = $contact_email;
        $job_application_email_raw = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_application_email'] ?? '')));
        $job_application_email = sanitize_email($job_application_email_raw);
        if ($job_application_email_raw !== '' && !is_email($job_application_email)) {
            if ($requires_complete_submission) {
                $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Please enter a valid application email or leave it blank.', 'nexdirectory'), 'error');
            }
            $job_application_email = '';
        }
        $_POST['nexdirectory_job_application_email'] = $job_application_email;
        $parsed_tags = $this->parse_event_tags_input(sanitize_text_field((string) wp_unslash($_POST['event_tags'] ?? '')), 8, 40);
        if (is_wp_error($parsed_tags)) {
            if ($requires_complete_submission) {
                $this->redirect_with_message(wp_get_referer() ?: home_url('/'), $parsed_tags->get_error_message(), 'error');
            }
            $parsed_tags = [];
        }

        if (!$is_events_profile) {
            $start_value = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_start'] ?? '')));
            $_POST['nexdirectory_recurrence'] = 'none';
            $_POST['nexdirectory_recurrence_until'] = '';
            $_POST['nexdirectory_sessions'] = '[]';
            $_POST['nexdirectory_key_speakers'] = '[]';
            if ($is_job_profile) {
                $job_posted_date = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_posted_date'] ?? '')));
                if ($job_posted_date !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $job_posted_date) && strtotime($job_posted_date . ' 00:00:00')) {
                    $_POST['nexdirectory_start'] = $job_posted_date . ' 09:00:00';
                } elseif ($start_value === '') {
                    $_POST['nexdirectory_start'] = current_time('Y-m-d H:i:s');
                }
                $posted_start_value = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_start'] ?? ''));
                if ($job_posted_date === '' && $posted_start_value !== '' && preg_match('/^\d{4}-\d{2}-\d{2}/', $posted_start_value, $posted_match)) {
                    $_POST['nexdirectory_job_posted_date'] = (string) ($posted_match[0] ?? '');
                }
                $job_closing = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_closing_date'] ?? '')));
                if ($job_closing !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $job_closing)) {
                    $_POST['nexdirectory_end'] = $job_closing . ' 23:59:00';
                } elseif (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_end'] ?? ''))) === '') {
                    $_POST['nexdirectory_end'] = '';
                }
            } elseif ($is_marketplace_profile) {
                if ($start_value === '') {
                    $_POST['nexdirectory_start'] = current_time('Y-m-d H:i:s');
                }
                $market_expires_on = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_expires_on'] ?? '')));
                if ($market_expires_on !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $market_expires_on)) {
                    $_POST['nexdirectory_end'] = $market_expires_on . ' 23:59:00';
                } else {
                    $_POST['nexdirectory_end'] = '';
                }
            } else {
                if ($start_value === '') {
                    $_POST['nexdirectory_start'] = current_time('Y-m-d H:i:s');
                }
                $_POST['nexdirectory_end'] = '';
            }
        }

        if ($requires_complete_submission) {
            $time_check = $this->validate_event_times(
                sanitize_text_field((string) wp_unslash($_POST['nexdirectory_start'] ?? '')),
                sanitize_text_field((string) wp_unslash($_POST['nexdirectory_end'] ?? '')),
                true
            );
            if (is_wp_error($time_check)) {
                $this->redirect_with_message(wp_get_referer() ?: home_url('/'), $time_check->get_error_message(), 'error');
            }
            if ($is_job_profile) {
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_company_name'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Company name is required for job listings.', 'nexdirectory'), 'error');
                }
                $job_posted = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_posted_date'] ?? '')));
                if ($job_posted !== '' && (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $job_posted) || !strtotime($job_posted . ' 00:00:00'))) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Posted date is invalid.', 'nexdirectory'), 'error');
                }
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_type'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Employment type is required for job listings.', 'nexdirectory'), 'error');
                }
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_department'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Department is required for job listings.', 'nexdirectory'), 'error');
                }
                if (!in_array((string) wp_unslash($_POST['nexdirectory_job_work_mode'] ?? ''), ['on-site', 'hybrid', 'remote'], true)) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Work setup is required for job listings.', 'nexdirectory'), 'error');
                }
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_compensation_type'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Compensation type is required for job listings.', 'nexdirectory'), 'error');
                }
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_salary'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Salary range is required for job listings.', 'nexdirectory'), 'error');
                }
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_apply_url'] ?? ''))) === '' && trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_application_email'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Provide at least one application method (URL or email).', 'nexdirectory'), 'error');
                }
                $job_closing = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_closing_date'] ?? '')));
                if ($job_closing === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Closing date is required for job listings.', 'nexdirectory'), 'error');
                }
                if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $job_closing) || !strtotime($job_closing . ' 00:00:00')) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Closing date is invalid.', 'nexdirectory'), 'error');
                }
            } elseif ($is_directory_profile) {
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_company_name'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Business name is required for directory listings.', 'nexdirectory'), 'error');
                }
                if (!in_array((string) wp_unslash($_POST['nexdirectory_directory_business_type'] ?? ''), ['independent-business', 'agency', 'private-owner', 'nonprofit', 'multi-location', 'franchise'], true)) {
                    $_POST['nexdirectory_directory_business_type'] = 'independent-business';
                }
                if (!in_array((string) wp_unslash($_POST['nexdirectory_directory_listing_status'] ?? ''), ['active', 'temporarily-closed', 'permanently-closed'], true)) {
                    $_POST['nexdirectory_directory_listing_status'] = 'active';
                }
                $has_contact_method = (
                    trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_contact_phone'] ?? ''))) !== ''
                    || trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_contact_email'] ?? ''))) !== ''
                    || trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_external_url'] ?? ''))) !== ''
                    || trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_appointment_url'] ?? ''))) !== ''
                    || trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_whatsapp_url'] ?? ''))) !== ''
                );
                if (!$has_contact_method) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Add at least one owner contact method (email, phone, website, booking URL, or WhatsApp).', 'nexdirectory'), 'error');
                }
            } elseif ($is_marketplace_profile) {
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_condition'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Condition is required for marketplace listings.', 'nexdirectory'), 'error');
                }
                $market_price_type = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_price_type'] ?? 'fixed'));
                if (!in_array($market_price_type, ['fixed', 'negotiable', 'auction', 'free'], true)) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Price type is invalid for marketplace listings.', 'nexdirectory'), 'error');
                }
                if ($market_price_type !== 'free' && trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_price'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Asking price is required unless this listing is marked as free.', 'nexdirectory'), 'error');
                }
                if ($market_price_type === 'free' && trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_price'] ?? ''))) === '') {
                    $_POST['nexdirectory_price'] = __('Free', 'nexdirectory');
                }
                $market_listing_status = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_listing_status'] ?? 'active'));
                if (!in_array($market_listing_status, ['active', 'reserved', 'sold', 'expired'], true)) {
                    $_POST['nexdirectory_market_listing_status'] = 'active';
                }
                $market_seller_type = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_seller_type'] ?? 'private'));
                if (!in_array($market_seller_type, ['private', 'business'], true)) {
                    $_POST['nexdirectory_market_seller_type'] = 'private';
                }
                $market_expires_on = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_expires_on'] ?? '')));
                if ($market_expires_on !== '' && (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $market_expires_on) || !strtotime($market_expires_on . ' 00:00:00'))) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Expiry date is invalid for marketplace listings.', 'nexdirectory'), 'error');
                }
                $market_delivery_selected = array_filter(array_map('sanitize_key', preg_split('/[\s,]+/', sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_delivery_methods'] ?? ''))) ?: []));
                if (empty($market_delivery_selected)) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Select at least one delivery method for marketplace listings.', 'nexdirectory'), 'error');
                }
                $has_contact_method = (
                    trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_contact_phone'] ?? ''))) !== ''
                    || trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_contact_email'] ?? ''))) !== ''
                    || trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_external_url'] ?? ''))) !== ''
                );
                if (!$has_contact_method) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Add at least one seller contact method (email, phone, or external listing URL).', 'nexdirectory'), 'error');
                }
            }
        }
        if ((string) wp_unslash($_POST['nexdirectory_timezone'] ?? '') === '') {
            $site_tz = $this->sanitize_timezone_value(wp_timezone_string());
            if ($site_tz !== '') {
                $_POST['nexdirectory_timezone'] = $site_tz;
            }
        }
        $address_value = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_address'] ?? ''));
        $city_value = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_city'] ?? ''));
        $venue_value = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_venue'] ?? ''));
        if ($requires_complete_submission && $address_value === '' && ($is_events_profile || $is_directory_profile)) {
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Address is required.', 'nexdirectory'), 'error');
        }
        if ($city_value === '' && $address_value !== '') {
            $parts = array_filter(array_map('trim', explode(',', $address_value)));
            $city_value = !empty($parts) ? (string) end($parts) : $address_value;
            $_POST['nexdirectory_city'] = $city_value;
        }
        $_POST['nexdirectory_venue'] = $venue_value;
        $this->maybe_fill_request_coordinates_from_location_fields();

        $content = wp_kses_post(wp_unslash($_POST['post_content'] ?? ''));
        if ($existing_post_id > 0 && !array_key_exists('post_content', $_POST)) {
            $content = (string) get_post_field('post_content', $existing_post_id);
        }
        if ($existing_post_id > 0 && trim((string) $content) === '') {
            $existing_content = (string) get_post_field('post_content', $existing_post_id);
            if (trim($existing_content) !== '') {
                $content = $existing_content;
            }
        }
        $excerpt = sanitize_textarea_field((string) wp_unslash($_POST['post_excerpt'] ?? ''));
        if ($existing_post_id > 0 && !array_key_exists('post_excerpt', $_POST)) {
            $excerpt = (string) get_post_field('post_excerpt', $existing_post_id);
        }
        if ($existing_post_id > 0 && trim((string) $excerpt) === '') {
            $existing_excerpt = (string) get_post_field('post_excerpt', $existing_post_id);
            if (trim($existing_excerpt) !== '') {
                $excerpt = $existing_excerpt;
            }
        }
        $excerpt_max_chars = max(80, min(1000, (int) get_option('nexdirectory_excerpt_max_chars', 220)));
        $excerpt_length = function_exists('mb_strlen') ? mb_strlen($excerpt) : strlen($excerpt);
        if ($excerpt_length > $excerpt_max_chars) {
            $this->redirect_with_message(
                wp_get_referer() ?: home_url('/'),
                /* translators: %d: Placeholder value. */
                sprintf(__('Short excerpt must be %d characters or fewer.', 'nexdirectory'), $excerpt_max_chars),
                'error'
            );
        }
        if ($excerpt === '') {
            $excerpt = wp_trim_words(nexdirectory_strip_all_tags($content), 26);
        }
        $custom_fields_enabled = $this->is_custom_submission_builder_available('submit', get_current_user_id());
        $custom_form_fields = [];
        $custom_field_values = [];
        if ($custom_fields_enabled) {
            $custom_payload = $this->sanitize_custom_builder_json_payload(wp_unslash($_POST['nexdirectory_custom_fields_builder_json'] ?? '[]'));
            $custom_parsed = $this->parse_custom_field_builder_payload($custom_payload);
            $custom_form_fields = is_array($custom_parsed['fields'] ?? null) ? (array) $custom_parsed['fields'] : [];
            $custom_field_values = is_array($custom_parsed['values'] ?? null) ? (array) $custom_parsed['values'] : [];
            if ($requires_complete_submission) {
                $custom_required_check = $this->validate_custom_form_fields_required($custom_form_fields, $custom_field_values);
                if (is_wp_error($custom_required_check)) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), $custom_required_check->get_error_message(), 'error');
                }
            }
        }

        $duplicate_candidate_id = 0;
        if ((string) get_option('nexdirectory_enable_duplicate_detection', '1') === '1') {
            $duplicate_candidate_id = $this->find_duplicate_event_candidate(
                $title,
                sanitize_text_field((string) wp_unslash($_POST['nexdirectory_start'] ?? '')),
                $city_value,
                $address_value,
                $venue_value,
                $is_edit_mode ? $edit_post_id : 0
            );
        }

        $post_args = [
            'post_type' => self::POST_TYPE,
            'post_status' => $post_status,
            'post_author' => get_current_user_id(),
            'post_title' => $title,
            'post_content' => $content,
            'post_excerpt' => $excerpt,
        ];
        if ($is_edit_mode) {
            $post_args['ID'] = $edit_post_id;
            unset($post_args['post_author']);
            $post_id = wp_update_post($post_args, true);
        } elseif ($autosave_post_id > 0 && $this->user_can_edit_event($autosave_post_id)) {
            $post_args['ID'] = $autosave_post_id;
            unset($post_args['post_author']);
            $post_id = wp_update_post($post_args, true);
        } else {
            $post_id = wp_insert_post($post_args, true);
        }

        if (is_wp_error($post_id)) {
            wp_die(esc_html($post_id->get_error_message()));
        }

        $upload_limits = $this->get_upload_limits();
        $cover_upload_urls = $this->collect_uploaded_gallery_image_urls_from_request(
            'nexdirectory_cover_file',
            1,
            (int) ($upload_limits['cover_max_bytes'] ?? 512000)
        );
        $submitted_cover_url = esc_url_raw((string) wp_unslash($_POST['nexdirectory_cover_image_url'] ?? ''));
        if ($submitted_cover_url === '' && !empty($cover_upload_urls[0])) {
            $_POST['nexdirectory_cover_image_url'] = (string) $cover_upload_urls[0];
        }
        $submitted_cover_url = esc_url_raw((string) wp_unslash($_POST['nexdirectory_cover_image_url'] ?? ''));
        $_POST['nexdirectory_cover_image_url'] = $this->can_current_user_use_media_url($submitted_cover_url, get_current_user_id())
            ? $submitted_cover_url
            : '';

        $submitted_gallery_urls = $this->parse_multiline_urls(sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_gallery_urls'] ?? '')));
        $uploaded_gallery_urls = $this->collect_uploaded_gallery_image_urls_from_request('nexdirectory_gallery_files');
        if (!empty($uploaded_gallery_urls)) {
            $_POST['nexdirectory_gallery_urls'] = implode("\n", array_values(array_unique(array_merge($submitted_gallery_urls, $uploaded_gallery_urls))));
        }
        $_POST['nexdirectory_gallery_urls'] = $this->filter_gallery_urls_text_for_user(
            sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_gallery_urls'] ?? '')),
            get_current_user_id()
        );
        $normalized_media = $this->normalize_cover_and_gallery_meta_urls(
            esc_url_raw((string) wp_unslash($_POST['nexdirectory_cover_image_url'] ?? '')),
            sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_gallery_urls'] ?? ''))
        );
        $_POST['nexdirectory_cover_image_url'] = (string) ($normalized_media['cover'] ?? '');
        $_POST['nexdirectory_gallery_urls'] = implode("\n", (array) ($normalized_media['gallery'] ?? []));

        $is_existing_post_update = $existing_post_id > 0;
        foreach ($this->get_event_meta_fields() as $key => $type) {
            if (!array_key_exists($key, $_POST)) {
                if ($is_existing_post_update) {
                    continue;
                }
                $value = '';
            } else {
                // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Main submit nonce is verified before this meta loop.
                $value = map_deep(wp_unslash($_POST[$key]), 'sanitize_textarea_field');
            }
            if ($this->should_skip_sparse_meta_update($post_id, $key, $value, $is_existing_post_update)) {
                continue;
            }
            update_post_meta($post_id, $key, $this->sanitize_meta_value($key, $value));
        }
        $this->persist_listing_profile_for_post($post_id, $profile_key);
        $resolved_official_value = $this->apply_official_flag_with_admin_gate($post_id, $requested_official_value, get_current_user_id());
        $_POST['nexdirectory_official_event'] = $resolved_official_value;

        $video_urls = $this->build_video_urls_from_fields(
            esc_url_raw((string) wp_unslash($_POST['nexdirectory_event_stream_url'] ?? '')),
            $this->normalize_submitted_youtube_id(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_youtube_video_id_1'] ?? ''))),
            $this->normalize_submitted_youtube_id(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_youtube_video_id_2'] ?? ''))),
            sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_video_urls'] ?? ''))
        );
        $this->update_media_meta(
            $post_id,
            $video_urls,
            sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_gallery_urls'] ?? ''))
        );
        if ($custom_fields_enabled) {
            do_action(
                'nexdirectory_persist_submission_custom_builder',
                $post_id,
                [
                    'fields' => $custom_form_fields,
                    'values' => $custom_field_values,
                ],
                ['source' => 'submit'],
                $this
            );
        }

        $sessions = $this->parse_sessions(sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_sessions'] ?? '[]')));
        update_post_meta($post_id, 'nexdirectory_sessions', wp_json_encode($sessions));
        $key_speakers = $this->parse_key_speakers(sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_key_speakers'] ?? '[]')));
        update_post_meta($post_id, 'nexdirectory_key_speakers', wp_json_encode($key_speakers));
        if (sanitize_key((string) wp_unslash($_POST['nexdirectory_recurrence'] ?? '')) === 'custom' && !empty($sessions)) {
            $last_date = '';
            foreach ($sessions as $session) {
                $candidate = (string) ($session['end'] ?? $session['start'] ?? '');
                if ($candidate !== '' && ($last_date === '' || $candidate > $last_date)) {
                    $last_date = $candidate;
                }
            }
            if ($last_date !== '') {
                update_post_meta($post_id, 'nexdirectory_recurrence_until', substr($last_date, 0, 10));
            }
        }

        $event_type_id = isset($_POST['event_type']) ? absint((int) wp_unslash($_POST['event_type'])) : 0;
        if ($event_type_id > 0) {
            wp_set_post_terms($post_id, [$event_type_id], self::TAX_TYPE, false);
        }

        if ($is_job_profile) {
            $job_type_terms = [];
            $job_department_label = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_department'] ?? '')));
            $job_type_label = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_type'] ?? '')));
            foreach ([$job_department_label, $job_type_label] as $candidate_term) {
                if ($candidate_term === '') {
                    continue;
                }
                $candidate_term_id = $this->get_or_create_term_id(self::TAX_TYPE, $candidate_term);
                if ($candidate_term_id > 0) {
                    $job_type_terms[] = (int) $candidate_term_id;
                }
            }
            if (!empty($job_type_terms)) {
                wp_set_post_terms($post_id, array_values(array_unique($job_type_terms)), self::TAX_TYPE, false);
            }
            $work_mode = sanitize_key((string) wp_unslash($_POST['nexdirectory_job_work_mode'] ?? ''));
            $work_mode_label = '';
            if ($work_mode === 'remote') {
                $work_mode_label = __('Remote', 'nexdirectory');
            } elseif ($work_mode === 'hybrid') {
                $work_mode_label = __('Hybrid', 'nexdirectory');
            } elseif ($work_mode === 'on-site') {
                $work_mode_label = __('On-site', 'nexdirectory');
            }
            if ($work_mode_label !== '') {
                $work_mode_term_id = $this->get_or_create_term_id(self::TAX_MODE, $work_mode_label);
                if ($work_mode_term_id > 0) {
                    wp_set_post_terms($post_id, [$work_mode_term_id], self::TAX_MODE, false);
                }
            }
            wp_set_post_terms($post_id, [], self::TAX_PRICE, false);
        } elseif ($is_marketplace_profile) {
            $delivery_methods = array_filter(array_map('sanitize_key', preg_split('/[\s,]+/', sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_delivery_methods'] ?? ''))) ?: []));
            $delivery_methods = array_values(array_unique(array_filter($delivery_methods, static function ($method) {
                return in_array($method, ['pickup', 'shipping', 'local-delivery', 'digital'], true);
            })));
            $delivery_labels = [
                'pickup' => __('Pickup', 'nexdirectory'),
                'shipping' => __('Shipping', 'nexdirectory'),
                'local-delivery' => __('Local Delivery', 'nexdirectory'),
                'digital' => __('Digital Delivery', 'nexdirectory'),
            ];
            $mode_term_ids = [];
            foreach ($delivery_methods as $delivery_method) {
                $term_name = (string) ($delivery_labels[$delivery_method] ?? '');
                if ($term_name === '') {
                    continue;
                }
                $term_id = $this->get_or_create_term_id(self::TAX_MODE, $term_name);
                if ($term_id > 0) {
                    $mode_term_ids[] = (int) $term_id;
                }
            }
            if (!empty($mode_term_ids)) {
                wp_set_post_terms($post_id, array_values(array_unique($mode_term_ids)), self::TAX_MODE, false);
            } else {
                wp_set_post_terms($post_id, [], self::TAX_MODE, false);
            }

            $market_price_type = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_price_type'] ?? 'fixed'));
            $price_term_map = [
                'free' => __('Free', 'nexdirectory'),
                'fixed' => __('Fixed Price', 'nexdirectory'),
                'negotiable' => __('Negotiable', 'nexdirectory'),
                'auction' => __('Auction', 'nexdirectory'),
            ];
            $price_term_name = (string) ($price_term_map[$market_price_type] ?? $price_term_map['fixed']);
            $price_term_id = $this->get_or_create_term_id(self::TAX_PRICE, $price_term_name);
            if ($price_term_id > 0) {
                wp_set_post_terms($post_id, [$price_term_id], self::TAX_PRICE, false);
            } else {
                wp_set_post_terms($post_id, [], self::TAX_PRICE, false);
            }
        } elseif ($is_directory_profile) {
            $directory_status = sanitize_key((string) wp_unslash($_POST['nexdirectory_directory_listing_status'] ?? 'active'));
            $status_term_map = [
                'active' => __('Active', 'nexdirectory'),
                'temporarily-closed' => __('Temporarily Closed', 'nexdirectory'),
                'permanently-closed' => __('Permanently Closed', 'nexdirectory'),
            ];
            $status_term_name = (string) ($status_term_map[$directory_status] ?? $status_term_map['active']);
            $status_term_id = $this->get_or_create_term_id(self::TAX_MODE, $status_term_name);
            if ($status_term_id > 0) {
                wp_set_post_terms($post_id, [$status_term_id], self::TAX_MODE, false);
            } else {
                wp_set_post_terms($post_id, [], self::TAX_MODE, false);
            }

            wp_set_post_terms($post_id, [], self::TAX_PRICE, false);

            $directory_type = sanitize_key((string) wp_unslash($_POST['nexdirectory_directory_business_type'] ?? 'independent-business'));
            $directory_type_term_map = [
                'independent-business' => __('Independent Business', 'nexdirectory'),
                'agency' => __('Agency', 'nexdirectory'),
                'private-owner' => __('Private Owner', 'nexdirectory'),
                'nonprofit' => __('Nonprofit / Community', 'nexdirectory'),
                'multi-location' => __('Multi-location Brand', 'nexdirectory'),
                'franchise' => __('Franchise', 'nexdirectory'),
            ];
            $directory_type_term_name = (string) ($directory_type_term_map[$directory_type] ?? '');
            if ($directory_type_term_name !== '') {
                $existing_type_ids = wp_get_post_terms($post_id, self::TAX_TYPE, ['fields' => 'ids']);
                if (is_wp_error($existing_type_ids) || !is_array($existing_type_ids)) {
                    $existing_type_ids = [];
                }
                $directory_type_term_id = $this->get_or_create_term_id(self::TAX_TYPE, $directory_type_term_name);
                if ($directory_type_term_id > 0) {
                    $existing_type_ids[] = (int) $directory_type_term_id;
                    wp_set_post_terms($post_id, array_values(array_unique(array_map('absint', $existing_type_ids))), self::TAX_TYPE, false);
                }
            }
        } else {
            $mode_option = sanitize_key((string) wp_unslash($_POST['nexdirectory_event_mode_option'] ?? 'in-person'));
            $price_option = sanitize_key((string) wp_unslash($_POST['nexdirectory_price_simple'] ?? 'free'));

            if ($is_events_profile) {
                $mode_name = $mode_option === 'video-stream' ? 'Online Stream Event' : 'In-person';
                $price_name = $price_option === 'paid' ? 'Paid' : 'Free';
            } elseif ($is_directory_profile) {
                $mode_name = $mode_option === 'video-stream' ? __('Online Service', 'nexdirectory') : __('On-site Service', 'nexdirectory');
                $price_name = $price_option === 'paid' ? __('Paid Service', 'nexdirectory') : __('Free Service', 'nexdirectory');
            } else {
                $mode_name = $mode_option === 'video-stream' ? __('Online', 'nexdirectory') : __('Standard', 'nexdirectory');
                $price_name = $price_option === 'paid' ? __('Paid', 'nexdirectory') : __('Free', 'nexdirectory');
            }

            $mode_term_id = $this->get_or_create_term_id(self::TAX_MODE, $mode_name);
            if ($mode_term_id > 0) {
                wp_set_post_terms($post_id, [$mode_term_id], self::TAX_MODE, false);
            }

            $price_term_id = $this->get_or_create_term_id(self::TAX_PRICE, $price_name);
            if ($price_term_id > 0) {
                wp_set_post_terms($post_id, [$price_term_id], self::TAX_PRICE, false);
            }
        }

        wp_set_post_terms($post_id, (array) $parsed_tags, self::TAX_TAG, false);
        $this->save_custom_taxonomy_terms_from_request($post_id);
        $missing_optional_sections = $this->get_event_optional_sections_missing($post_id, $profile_key);
        if (!empty($missing_optional_sections)) {
            update_post_meta($post_id, 'nexdirectory_missing_optional_sections', wp_json_encode($missing_optional_sections));
        } else {
            delete_post_meta($post_id, 'nexdirectory_missing_optional_sections');
        }
        $completion_hint_text = $this->get_event_optional_sections_hint_text($missing_optional_sections);

        if ($post_status === 'pending') {
            $this->notify_admin_submission($post_id);
            $this->notify_author_status($post_id, 'pending', '');
            if ($duplicate_candidate_id > 0) {
                update_post_meta($post_id, 'nexdirectory_duplicate_candidate', (string) $duplicate_candidate_id);
            } else {
                delete_post_meta($post_id, 'nexdirectory_duplicate_candidate');
            }
            $message = $is_edit_mode
                /* translators: %s: Placeholder value. */
                ? sprintf(__('%s updated and sent for admin verification.', 'nexdirectory'), $singular_label)
                /* translators: %s: Placeholder value. */
                : sprintf(__('%s submitted and sent for admin verification.', 'nexdirectory'), $singular_label);
            $message .= ' ' . __('Your listing will go live once it has been approved by the site admin.', 'nexdirectory');
            if ($requested_official_value === '1' && $resolved_official_value !== '1') {
                $message .= ' ' . __('Official classification was requested and will appear only after admin confirmation.', 'nexdirectory');
            }
            if ($duplicate_candidate_id > 0) {
                $message .= ' ' . __('Potential duplicate detected (same title/date/location). Admin review recommended.', 'nexdirectory');
            }
            if ($completion_hint_text !== '') {
                $message .= ' ' . $completion_hint_text;
            }
            $my_events_url = $this->get_platform_page_url(
                'nexdirectory_page_my_events_url',
                $this->get_profile_route_slugs('my_items'),
                $this->get_profile_route_default_url('my_items')
            );
            $this->redirect_with_message(
                $my_events_url,
                $message
            );
        }

        if ($intent === 'preview') {
            $preview_url = (string) get_preview_post_link($post_id);
            if ($preview_url === '' && $post_id > 0) {
                $preview_url = nexdirectory_add_query_arg([
                    'p' => (int) $post_id,
                    'post_type' => $this::POST_TYPE,
                    'preview' => 'true',
                ], home_url('/'));
            }
            if ($is_ajax_submit) {
                wp_send_json_success([
                    /* translators: %s: Placeholder value. */
                    'message' => sprintf(__('%s preview is ready.', 'nexdirectory'), $singular_label),
                    'type' => 'success',
                    'post_id' => (int) $post_id,
                    'edit_event_id' => (int) $post_id,
                    'preview_url' => $preview_url,
                ]);
            }
            $this->safe_redirect_or_fallback($preview_url);
        }

        $this->redirect_with_message(
            wp_get_referer() ?: home_url('/'),
            ($is_edit_mode
                /* translators: %s: Placeholder value. */
                ? sprintf(__('%s changes saved as draft.', 'nexdirectory'), $singular_label)
                /* translators: %s: Placeholder value. */
                : sprintf(__('%s saved as draft.', 'nexdirectory'), $singular_label))
            . ($completion_hint_text !== '' ? (' ' . $completion_hint_text) : '')
        );
    }


    public function handle_autosave_event() {
        if (!is_user_logged_in()) {
            wp_send_json_error(['message' => __('Login required.', 'nexdirectory')], 403);
        }
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field((string) wp_unslash($_POST['nonce'])), 'nexdirectory_autosave_event_nonce')) {
            wp_send_json_error(['message' => __('Invalid nonce.', 'nexdirectory')], 403);
        }

        $autosave_post_id = isset($_POST['nexdirectory_autosave_post_id']) ? absint((int) wp_unslash($_POST['nexdirectory_autosave_post_id'])) : 0;
        $edit_post_id = isset($_POST['nexdirectory_edit_post_id']) ? absint((int) wp_unslash($_POST['nexdirectory_edit_post_id'])) : 0;
        $current_user_id = (int) get_current_user_id();
        $can_edit_existing = (
            ($edit_post_id > 0 && $this->user_can_edit_event($edit_post_id, $current_user_id))
            || ($autosave_post_id > 0 && $this->user_can_edit_event($autosave_post_id, $current_user_id))
        );
        if (!$this->user_can_submit_events($current_user_id) && !$can_edit_existing) {
            wp_send_json_error(['message' => __('You do not have permission to submit events.', 'nexdirectory')], 403);
        }
        $profile_source_post_id = $edit_post_id > 0 ? $edit_post_id : $autosave_post_id;
        $profile_key = $this->resolve_profile_key_for_post($profile_source_post_id);
        $is_events_profile = $profile_key === 'events';
        $is_edit_autosave = $edit_post_id > 0 && $this->user_can_edit_event($edit_post_id, $current_user_id);
        $has_existing_autosave_target = $autosave_post_id > 0 && $this->user_can_edit_event($autosave_post_id, $current_user_id);
        $title = sanitize_text_field((string) wp_unslash($_POST['post_title'] ?? ''));
        if ($title === '' && $is_edit_autosave) {
            $title = $this->recover_existing_event_title_value($edit_post_id, (string) get_the_title($edit_post_id));
        }
        if ($title === '' && $autosave_post_id > 0 && $this->user_can_edit_event($autosave_post_id, $current_user_id)) {
            $title = $this->recover_existing_event_title_value($autosave_post_id, (string) get_the_title($autosave_post_id));
        }
        if ($title === '') {
            wp_send_json_error(['message' => __('Missing title.', 'nexdirectory')], 400);
        }
        $content = wp_kses_post(wp_unslash($_POST['post_content'] ?? ''));
        if ($is_edit_autosave && !array_key_exists('post_content', $_POST)) {
            $content = (string) get_post_field('post_content', $edit_post_id);
        }
        if ($is_edit_autosave && trim((string) $content) === '') {
            $existing_content = (string) get_post_field('post_content', $edit_post_id);
            if (trim($existing_content) !== '') {
                $content = $existing_content;
            }
        }
        $excerpt = sanitize_textarea_field((string) wp_unslash($_POST['post_excerpt'] ?? ''));
        if ($is_edit_autosave && !array_key_exists('post_excerpt', $_POST)) {
            $excerpt = (string) get_post_field('post_excerpt', $edit_post_id);
        }
        if ($is_edit_autosave && trim((string) $excerpt) === '') {
            $existing_excerpt = (string) get_post_field('post_excerpt', $edit_post_id);
            if (trim($existing_excerpt) !== '') {
                $excerpt = $existing_excerpt;
            }
        }
        $excerpt_max_chars = max(80, min(1000, (int) get_option('nexdirectory_excerpt_max_chars', 220)));
        $excerpt_length = function_exists('mb_strlen') ? mb_strlen($excerpt) : strlen($excerpt);
        if ($excerpt !== '' && $excerpt_length > $excerpt_max_chars) {
            $excerpt = function_exists('mb_substr') ? mb_substr($excerpt, 0, $excerpt_max_chars) : substr($excerpt, 0, $excerpt_max_chars);
        }
        if ($excerpt === '') {
            $excerpt = wp_trim_words(nexdirectory_strip_all_tags($content), 26);
        }

        $post_data = [
            'post_type' => self::POST_TYPE,
            'post_status' => 'draft',
            'post_author' => get_current_user_id(),
            'post_title' => $title,
            'post_content' => $content,
            'post_excerpt' => $excerpt,
        ];

        if ($is_edit_autosave) {
            $post_data['ID'] = $edit_post_id;
            unset($post_data['post_status'], $post_data['post_author']);
            $post_id = wp_update_post($post_data, true);
        } elseif ($autosave_post_id > 0 && $this->user_can_edit_event($autosave_post_id)) {
            $post_data['ID'] = $autosave_post_id;
            unset($post_data['post_author']);
            $post_id = wp_update_post($post_data, true);
        } else {
            $post_id = wp_insert_post($post_data, true);
        }

        if (is_wp_error($post_id) || !$post_id) {
            wp_send_json_error(['message' => is_wp_error($post_id) ? $post_id->get_error_message() : __('Autosave failed.', 'nexdirectory')], 500);
        }

        $this->hydrate_event_datetime_fields_from_request();
        $requested_official_value = $this->normalize_official_event_value(sanitize_key((string) wp_unslash($_POST['nexdirectory_official_event'] ?? '0')));
        $_POST['nexdirectory_official_event'] = $requested_official_value;
        $_POST['nexdirectory_lock_pin'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_lock_pin'] ?? '0')) === '1' ? '1' : '0';
        $_POST['nexdirectory_price_simple'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_price_simple'] ?? 'free'));
        $_POST['nexdirectory_event_stream_url'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_event_stream_url'] ?? ''));
        $_POST['nexdirectory_meta_title'] = $this->sanitize_event_seo_title(wp_unslash($_POST['nexdirectory_meta_title'] ?? ''));
        $_POST['nexdirectory_meta_description'] = $this->sanitize_event_seo_description(wp_unslash($_POST['nexdirectory_meta_description'] ?? ''));
        if ((string) get_option('nexdirectory_enable_event_seo_fields', '1') !== '1') {
            unset($_POST['nexdirectory_meta_title'], $_POST['nexdirectory_meta_description']);
        }
        $_POST['nexdirectory_youtube_video_id_1'] = $this->normalize_submitted_youtube_id(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_youtube_video_id_1'] ?? '')));
        $_POST['nexdirectory_youtube_video_id_2'] = $this->normalize_submitted_youtube_id(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_youtube_video_id_2'] ?? '')));
        $_POST['nexdirectory_gallery_urls'] = $this->filter_gallery_urls_text_for_user(
            sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_gallery_urls'] ?? '')),
            $current_user_id
        );
        $_POST['nexdirectory_listing_profile'] = $profile_key;
        $this->sync_event_price_with_ticket_request($is_events_profile);

        $is_existing_autosave_update = $is_edit_autosave || ($autosave_post_id > 0 && $this->user_can_edit_event($autosave_post_id));
        foreach ($this->get_event_meta_fields() as $key => $type) {
            if (!array_key_exists($key, $_POST)) {
                if ($is_existing_autosave_update) {
                    continue;
                }
                $value = '';
            } else {
                // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Autosave nonce is verified before this meta loop.
                $value = map_deep(wp_unslash($_POST[$key]), 'sanitize_textarea_field');
            }
            if ($this->should_skip_sparse_meta_update($post_id, $key, $value, $is_existing_autosave_update)) {
                continue;
            }
            update_post_meta($post_id, $key, $this->sanitize_meta_value($key, $value));
        }
        $this->persist_listing_profile_for_post($post_id, $profile_key);
        $resolved_official_value = $this->apply_official_flag_with_admin_gate($post_id, $requested_official_value, get_current_user_id());
        $_POST['nexdirectory_official_event'] = $resolved_official_value;

        $video_urls = $this->build_video_urls_from_fields(
            esc_url_raw((string) wp_unslash($_POST['nexdirectory_event_stream_url'] ?? '')),
            $this->normalize_submitted_youtube_id(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_youtube_video_id_1'] ?? ''))),
            $this->normalize_submitted_youtube_id(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_youtube_video_id_2'] ?? ''))),
            sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_video_urls'] ?? ''))
        );
        $this->update_media_meta(
            $post_id,
            $video_urls,
            sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_gallery_urls'] ?? ''))
        );
        if ($this->is_custom_submission_builder_available('autosave', get_current_user_id()) && isset($_POST['nexdirectory_custom_fields_builder_json'])) {
            $custom_payload = $this->sanitize_custom_builder_json_payload(wp_unslash($_POST['nexdirectory_custom_fields_builder_json'] ?? '[]'));
            $custom_parsed = $this->parse_custom_field_builder_payload($custom_payload);
            $custom_form_fields = is_array($custom_parsed['fields'] ?? null) ? (array) $custom_parsed['fields'] : [];
            $custom_field_values = is_array($custom_parsed['values'] ?? null) ? (array) $custom_parsed['values'] : [];
            do_action(
                'nexdirectory_persist_submission_custom_builder',
                $post_id,
                [
                    'fields' => $custom_form_fields,
                    'values' => $custom_field_values,
                ],
                ['source' => 'autosave'],
                $this
            );
        }
        $this->save_custom_taxonomy_terms_from_request($post_id);
        $raw_sessions_input = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_sessions'] ?? '[]'));
        $raw_key_speakers_input = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_key_speakers'] ?? '[]'));
        $has_existing_sessions = trim((string) get_post_meta($post_id, 'nexdirectory_sessions', true)) !== '';
        $has_existing_key_speakers = trim((string) get_post_meta($post_id, 'nexdirectory_key_speakers', true)) !== '';
        if (!$is_existing_autosave_update || !$this->is_empty_json_collection_value($raw_sessions_input) || !$has_existing_sessions) {
            update_post_meta($post_id, 'nexdirectory_sessions', wp_json_encode($this->parse_sessions($raw_sessions_input)));
        }
        if (!$is_existing_autosave_update || !$this->is_empty_json_collection_value($raw_key_speakers_input) || !$has_existing_key_speakers) {
            update_post_meta($post_id, 'nexdirectory_key_speakers', wp_json_encode($this->parse_key_speakers($raw_key_speakers_input)));
        }

        wp_send_json_success([
            'message' => __('Draft autosaved.', 'nexdirectory'),
            'post_id' => $post_id,
        ]);
    }


    public function render_my_events($atts = []) {
        $atts = shortcode_atts([
            'posts_per_page' => 30,
        ], (array) $atts, 'nexdirectory_my_events');
        $ui_labels = $this->get_site_profile_ui_labels();
        $my_items_title = sanitize_text_field((string) ($ui_labels['my_items_title'] ?? __('My Listings', 'nexdirectory')));

        return $this->render_account_app_template([
            'initial_tab' => 'listings',
            'initial_listings_subtab' => 'manage',
            'posts_per_page' => max(1, (int) $atts['posts_per_page']),
            /* translators: %s: Placeholder value. */
            'login_message' => sprintf(__('Please log in to access %s.', 'nexdirectory'), $my_items_title),
        ]);
    }


    public function render_boost_posts($atts = []) {
        if (
            !function_exists('nexdirectory_is_pro_feature_enabled')
            || (
                !nexdirectory_is_pro_feature_enabled('boost_packages')
                && !nexdirectory_is_pro_feature_enabled('monetization')
            )
        ) {
            return '';
        }

        return (string) apply_filters('nexdirectory_render_extension_boost_posts', '', (array) $atts, $this);
    }


    private function get_rsvp_rows_for_user($user_id, $limit = 200) {
        $user_id = absint((int) $user_id);
        $limit = max(1, min(1000, (int) $limit));
        if ($user_id <= 0) {
            return [];
        }

        global $wpdb;
        $table = $this->get_rsvp_table_name();
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT post_id, status, created_at, updated_at FROM {$table} WHERE user_id = %d ORDER BY updated_at DESC LIMIT %d",
            $user_id,
            $limit
        ), ARRAY_A);
        if (!is_array($rows) || empty($rows)) {
            return [];
        }

        $raw_post_ids = array_values(array_unique(array_filter(array_map(static function ($row) {
            return absint((int) ($row['post_id'] ?? 0));
        }, $rows))));
        if (!empty($raw_post_ids) && function_exists('_prime_post_caches')) {
            _prime_post_caches($raw_post_ids, true, true);
        }

        $prepared_rows = [];
        foreach ($rows as $row) {
            $post_id = absint((int) ($row['post_id'] ?? 0));
            if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
                continue;
            }

            $status = sanitize_key((string) ($row['status'] ?? ''));
            if (!in_array($status, ['yes', 'waitlist'], true)) {
                continue;
            }

            $post_status = (string) get_post_status($post_id);
            if (!in_array($post_status, ['publish', 'pending', 'draft'], true)) {
                continue;
            }

            $prepared_rows[] = [
                'post_id' => $post_id,
                'status' => $status,
                'post_status' => $post_status,
                'updated_at' => (string) ($row['updated_at'] ?? ''),
            ];
        }
        if (empty($prepared_rows)) {
            return [];
        }

        $out = [];
        foreach ($prepared_rows as $row) {
            $post_id = absint((int) ($row['post_id'] ?? 0));
            $status = sanitize_key((string) ($row['status'] ?? ''));
            $post_status = (string) ($row['post_status'] ?? 'publish');
            $start_raw = (string) get_post_meta($post_id, 'nexdirectory_start', true);
            $end_raw = (string) get_post_meta($post_id, 'nexdirectory_end', true);
            $start_ts = strtotime($start_raw);
            $end_ts = $end_raw !== '' ? strtotime($end_raw) : false;
            $updated_ts = strtotime((string) ($row['updated_at'] ?? ''));
            $address = sanitize_text_field((string) get_post_meta($post_id, 'nexdirectory_address', true));
            $city = sanitize_text_field((string) get_post_meta($post_id, 'nexdirectory_city', true));
            $country = sanitize_text_field((string) get_post_meta($post_id, 'nexdirectory_country', true));
            $location = trim((string) preg_replace('/\s+/u', ' ', implode(', ', array_filter([$address, $city, $country], static function ($v) {
                return trim((string) $v) !== '';
            }))));
            $map_link = $location !== '' ? esc_url_raw('https://maps.google.com/?q=' . rawurlencode($location)) : '';

            $session_rows = [];
            $session_day_labels = [];
            $sessions_raw = $this->parse_sessions((string) get_post_meta($post_id, 'nexdirectory_sessions', true));
            if (is_array($sessions_raw)) {
                foreach (array_values(array_slice($sessions_raw, 0, 20)) as $session_index => $session_row) {
                    if (!is_array($session_row)) {
                        continue;
                    }
                    $session_title = sanitize_text_field((string) ($session_row['title'] ?? ''));
                    $session_start_raw = (string) ($session_row['start'] ?? '');
                    $session_end_raw = (string) ($session_row['end'] ?? '');
                    $session_start_ts = $session_start_raw !== '' ? strtotime($session_start_raw) : false;
                    $session_end_ts = $session_end_raw !== '' ? strtotime($session_end_raw) : false;
                    $session_day_label = sanitize_text_field((string) ($session_row['day'] ?? ''));
                    if ($session_day_label === '' && $session_start_ts) {
                        $session_day_label = wp_date(get_option('date_format'), (int) $session_start_ts);
                    }
                    if ($session_day_label !== '') {
                        $session_day_labels[] = $session_day_label;
                    }
                    $time_bits = [];
                    if ($session_start_ts) {
                        $time_bits[] = $this->event_datetime_has_time($session_start_raw)
                            ? wp_date(get_option('time_format'), (int) $session_start_ts)
                            : wp_date(get_option('date_format'), (int) $session_start_ts);
                    }
                    if ($session_end_ts) {
                        $time_bits[] = $this->event_datetime_has_time($session_end_raw)
                            ? wp_date(get_option('time_format'), (int) $session_end_ts)
                            : wp_date(get_option('date_format'), (int) $session_end_ts);
                    }
                    if ($session_start_ts && $session_end_ts && wp_date('Y-m-d', (int) $session_start_ts) !== wp_date('Y-m-d', (int) $session_end_ts)) {
                        $time_bits = [
                            $this->format_event_datetime_for_display($session_start_raw),
                            $this->format_event_datetime_for_display($session_end_raw),
                        ];
                    }
                    if ($session_title === '' && empty($time_bits) && trim((string) ($session_row['speaker'] ?? '')) === '' && trim((string) ($session_row['location'] ?? '')) === '') {
                        continue;
                    }
                    $session_key = sanitize_key((string) ($session_row['session_key'] ?? ''));
                    if ($session_key === '') {
                        $session_key = sanitize_key('s' . substr(md5((string) $post_id . '|' . (string) $session_index . '|' . (string) $session_title . '|' . (string) $session_start_raw . '|' . (string) $session_end_raw), 0, 24));
                    }
                    $session_rows[] = [
                        'session_uid' => sanitize_key('e' . (string) $post_id . '-' . $session_key),
                        /* translators: %d: Placeholder value. */
                        'title' => $session_title !== '' ? $session_title : sprintf(__('Session %d', 'nexdirectory'), $session_index + 1),
                        'time' => implode(' - ', $time_bits),
                        'speaker' => sanitize_text_field((string) ($session_row['speaker'] ?? '')),
                        'location' => sanitize_text_field((string) ($session_row['location'] ?? '')),
                        'day_label' => $session_day_label,
                        'notes' => nexdirectory_sanitize_textarea((string) ($session_row['notes'] ?? '')),
                    ];
                }
            }
            $session_day_labels = array_values(array_unique(array_filter(array_map('sanitize_text_field', $session_day_labels))));

            $prepared_row = [
                'post_id' => $post_id,
                'title' => (string) get_the_title($post_id),
                'url' => get_permalink($post_id),
                'status' => $status,
                'status_label' => $status === 'waitlist' ? __('Waitlist', 'nexdirectory') : __('Confirmed', 'nexdirectory'),
                'event_status' => $post_status,
                'event_status_label' => $this->event_status_badge($post_status),
                'start_label' => $start_ts ? $this->format_event_datetime_for_display($start_raw) : __('Date TBD', 'nexdirectory'),
                'event_date_label' => $this->format_event_date($post_id),
                'end_label' => $end_ts ? $this->format_event_datetime_for_display($end_raw) : '',
                'updated_label' => $updated_ts ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), $updated_ts) : '',
                'location' => $location,
                'map_link' => $map_link,
                'sessions' => $session_rows,
                'session_day_labels' => $session_day_labels,
                'can_cancel' => is_user_logged_in() && (int) get_current_user_id() === $user_id,
                'rsvp_nonce' => wp_create_nonce('nexdirectory_rsvp_event'),
            ];
            $out[] = (array) apply_filters(
                'nexdirectory_extension_saved_rsvp_row',
                $prepared_row,
                $post_id,
                $user_id,
                $this
            );
        }

        return $out;
    }


    private function get_event_attendee_rows($post_id) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            return [];
        }

        global $wpdb;
        $table = $this->get_rsvp_table_name();
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT user_id, status, created_at, updated_at FROM {$table} WHERE post_id = %d ORDER BY status ASC, updated_at DESC",
            $post_id
        ), ARRAY_A);
        if (!is_array($rows)) {
            $rows = [];
        }

        $user_ids = [];
        foreach ($rows as $row) {
            $user_id = absint((int) ($row['user_id'] ?? 0));
            if ($user_id > 0) {
                $user_ids[] = $user_id;
            }
        }
        $user_ids = array_values(array_unique($user_ids));
        if (!empty($user_ids)) {
            cache_users($user_ids);
            update_meta_cache('user', $user_ids);
        }
        $out = [];
        foreach ($rows as $row) {
            $user_id = absint((int) ($row['user_id'] ?? 0));
            if ($user_id <= 0) {
                continue;
            }
            $user = get_user_by('id', $user_id);
            if (!$user instanceof WP_User) {
                continue;
            }
            $status = sanitize_key((string) ($row['status'] ?? 'yes'));
            if (!in_array($status, ['yes', 'waitlist'], true)) {
                $status = 'yes';
            }
            $updated_ts = strtotime((string) ($row['updated_at'] ?? ''));
            $communication_opt_in = $this->user_allows_organizer_communications($user_id);

            $prepared_row = [
                'user_id' => $user_id,
                'name' => (string) ($user->display_name ?: $user->user_login),
                'email' => sanitize_email((string) $user->user_email),
                'status' => $status,
                'status_label' => $status === 'waitlist' ? __('Waitlist', 'nexdirectory') : __('Confirmed', 'nexdirectory'),
                'updated_label' => $updated_ts ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), $updated_ts) : '',
                'communication_opt_in' => $communication_opt_in ? '1' : '0',
                'communication_label' => $communication_opt_in
                    ? __('Email Updates: On', 'nexdirectory')
                    : __('Email Updates: Off', 'nexdirectory'),
            ];
            $out[] = (array) apply_filters(
                'nexdirectory_extension_attendee_row',
                $prepared_row,
                $post_id,
                $user_id,
                $this
            );
        }

        return $out;
    }


    public function render_whos_attending($atts = []) {
        $status_filter = 'yes';
        $atts = shortcode_atts([
            'event_id' => 0,
            'status' => 'yes',
        ], (array) $atts, 'nexdirectory_whos_attending');
        $ui_labels = $this->get_site_profile_ui_labels();
        $profile_key = sanitize_key((string) ($ui_labels['profile_key'] ?? ''));
        $is_events_profile = (string) ($ui_labels['is_events_profile'] ?? '1') === '1';
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Listing', 'nexdirectory')));
        $plural_label = sanitize_text_field((string) ($ui_labels['plural'] ?? __('Listings', 'nexdirectory')));

        $event_id = absint((int) ($atts['event_id'] ?? 0));
        if ($event_id <= 0 && isset($_GET['event_id'])) {
            $event_id = absint((int) wp_unslash($_GET['event_id']));
        }
        if ($event_id <= 0 && isset($_GET['attendees_event'])) {
            $event_id = absint((int) wp_unslash($_GET['attendees_event']));
        }

        $status_filter = sanitize_key((string) ($atts['status'] ?? 'yes'));
        if (isset($_GET['status'])) {
            $status_filter = sanitize_key((string) wp_unslash($_GET['status']));
        }
        if (!in_array($status_filter, ['yes', 'waitlist', 'all'], true)) {
            $status_filter = 'yes';
        }

        $events_url = $this->get_platform_page_url(
            'nexdirectory_page_events_url',
            $this->get_profile_route_slugs('events', $profile_key),
            $this->get_profile_route_default_url('events', $profile_key)
        );

        $page_title = $is_events_profile
            ? __('Who\'s Attending', 'nexdirectory')
            /* translators: %s: Placeholder value. */
            : sprintf(__('Saved %s', 'nexdirectory'), $plural_label);
        $page_description = $is_events_profile
            ? __('See who has RSVP\'d for this event.', 'nexdirectory')
            /* translators: %s: Placeholder value. */
            : sprintf(__('See who has saved this %s.', 'nexdirectory'), strtolower((string) $singular_label));
        $state_message = '';
        $rows = [];
        $event_title = '';
        $event_url = '';
        $confirmed_count = 0;
        $waitlist_count = 0;
        $privacy_notice = '';
        $can_view_private = false;

        if ($event_id <= 0) {
            $state_message = $is_events_profile
                ? __('Select an event to view attendees.', 'nexdirectory')
                /* translators: %s: Placeholder value. */
                : sprintf(__('Select one %s to view followers.', 'nexdirectory'), strtolower((string) $singular_label));
        } elseif (get_post_type($event_id) !== self::POST_TYPE) {
            $state_message = $is_events_profile
                ? __('Invalid event selected.', 'nexdirectory')
                /* translators: %s: Placeholder value. */
                : sprintf(__('Invalid %s selected.', 'nexdirectory'), strtolower((string) $singular_label));
            $event_id = 0;
        } else {
            $event_title = (string) get_the_title($event_id);
            $event_url = (string) get_permalink($event_id);
            $owner_id = (int) get_post_field('post_author', $event_id);
            $can_view_private = is_user_logged_in() && ((int) get_current_user_id() === $owner_id || $this->user_can_moderate_events());
            $attendee_policy = sanitize_key((string) get_post_meta($event_id, 'nexdirectory_attendee_visibility', true));
            if (!in_array($attendee_policy, ['off', 'counts', 'members', 'public'], true)) {
                $attendee_policy = 'counts';
            }
            $post_status = (string) get_post_status($event_id);
            if ($post_status !== 'publish' && !$can_view_private) {
                $state_message = __('This attendee list is not public yet.', 'nexdirectory');
                $event_id = 0;
            } else {
                $raw_rows = $this->get_event_attendee_rows($event_id);
                $confirmed_count = count(array_filter($raw_rows, static function ($row) {
                    return (string) ($row['status'] ?? '') === 'yes';
                }));
                $waitlist_count = count(array_filter($raw_rows, static function ($row) {
                    return (string) ($row['status'] ?? '') === 'waitlist';
                }));

                $can_view_names = $can_view_private
                    || $attendee_policy === 'public'
                    || ($attendee_policy === 'members' && is_user_logged_in());
                if (!$can_view_private) {
                    $waitlist_count = 0;
                    $status_filter = 'yes';
                }
                if ($attendee_policy === 'off' && !$can_view_private) {
                    $confirmed_count = 0;
                    $privacy_notice = __('The organizer has hidden attendee information for this event.', 'nexdirectory');
                } elseif (!$can_view_names) {
                    $privacy_notice = __('Attendee names are private. Only the confirmed total is shown.', 'nexdirectory');
                } else {
                    foreach ($raw_rows as $row) {
                        $row_status = sanitize_key((string) ($row['status'] ?? ''));
                        if (!$can_view_private && $row_status === 'waitlist') {
                            continue;
                        }
                        if ($status_filter !== 'all' && $row_status !== $status_filter) {
                            continue;
                        }
                        $attendee_user_id = absint((int) ($row['user_id'] ?? 0));
                        $attendee_visibility = sanitize_key((string) get_user_meta($attendee_user_id, 'nexdirectory_attendee_visibility', true));
                        if (!in_array($attendee_visibility, ['public', 'members', 'private'], true)) {
                            $attendee_visibility = 'members';
                        }
                        $viewer_is_attendee = is_user_logged_in() && (int) get_current_user_id() === $attendee_user_id;
                        $attendee_allows_view = $can_view_private
                            || $viewer_is_attendee
                            || $attendee_visibility === 'public'
                            || ($attendee_visibility === 'members' && is_user_logged_in());
                        if (!$attendee_allows_view) {
                            continue;
                        }
                        $attendee_user = $attendee_user_id > 0 ? get_user_by('id', $attendee_user_id) : false;
                        $safe_attendee_name = sanitize_text_field((string) get_user_meta($attendee_user_id, 'nexdirectory_profile_public_name', true));
                        if ($safe_attendee_name === '' && $attendee_user instanceof WP_User) {
                            $candidate_name = sanitize_text_field((string) $attendee_user->display_name);
                            $login_name = sanitize_text_field((string) $attendee_user->user_login);
                            if ($candidate_name !== '' && !hash_equals($candidate_name, $login_name)) {
                                $safe_attendee_name = $candidate_name;
                            }
                        }
                        if ($safe_attendee_name === '') {
                            $safe_attendee_name = __('Attendee', 'nexdirectory');
                        }
                        $rows[] = [
                            'name' => $safe_attendee_name,
                            'status' => $row_status,
                            'status_label' => sanitize_text_field((string) ($row['status_label'] ?? '')),
                            'updated_label' => sanitize_text_field((string) ($row['updated_label'] ?? '')),
                        ];
                    }
                }

                if (empty($rows)) {
                    if ($privacy_notice !== '') {
                        $state_message = $privacy_notice;
                    } else {
                        $state_message = $is_events_profile
                            ? __('No visible RSVPs yet for this event.', 'nexdirectory')
                            /* translators: %s: Placeholder value. */
                            : sprintf(__('No visible followers yet for this %s.', 'nexdirectory'), strtolower((string) $singular_label));
                    }
                }
            }
        }

        return $this->render_frontend_template('whos-attending.php', [
            'ui_labels' => $ui_labels,
            'profile_key' => $profile_key,
            'page_title' => $page_title,
            'page_description' => $page_description,
            'events_url' => $events_url,
            'event_id' => $event_id,
            'event_title' => $event_title,
            'event_url' => $event_url,
            'rows' => $rows,
            'status_filter' => $status_filter,
            'confirmed_count' => $confirmed_count,
            'waitlist_count' => $waitlist_count,
            'state_message' => $state_message,
            'privacy_notice' => $privacy_notice,
            'can_view_private' => $can_view_private,
            'is_events_profile' => $is_events_profile,
        ]);
    }


    public function render_my_tickets($atts = []) {
        $atts = shortcode_atts([
            'limit' => 200,
        ], (array) $atts, 'nexdirectory_my_tickets');
        $ui_labels = $this->get_site_profile_ui_labels();
        $attending_title = sanitize_text_field((string) ($ui_labels['attending_title'] ?? __('Saved Listings', 'nexdirectory')));
        return $this->render_account_app_template([
            'initial_tab' => 'saved',
            'tickets_limit' => max(1, (int) $atts['limit']),
            /* translators: %s: Placeholder value. */
            'login_message' => sprintf(__('Please log in to access %s.', 'nexdirectory'), $attending_title),
        ]);
    }    public function render_notifications_center($atts = []) {
        $atts = shortcode_atts([
            'limit' => 80,
        ], (array) $atts, 'nexdirectory_notifications');
        return $this->render_account_app_template([
            'initial_tab' => 'inbox',
            'notifications_limit' => max(1, min(300, (int) $atts['limit'])),
            'login_message' => __('Please log in to access your inbox.', 'nexdirectory'),
        ]);
    }

    /**
     * Normalizes one CSV row and neutralizes spreadsheet formulas.
     *
     * CSV is an interchange format rather than an HTML context, so values are
     * preserved as text. Cells whose first meaningful character can start a
     * spreadsheet formula are prefixed with an apostrophe before fputcsv()
     * performs the actual CSV quoting.
     *
     * @param array<int, mixed> $row Raw CSV cells.
     * @return array<int, string>
     */
    private function sanitize_csv_row($row) {
        $safe_row = [];

        foreach ((array) $row as $cell) {
            if (is_scalar($cell) || $cell === null) {
                $value = wp_check_invalid_utf8((string) $cell);
            } else {
                $encoded = wp_json_encode($cell);
                $value = is_string($encoded) ? $encoded : '';
            }

            $value = str_replace("\0", '', $value);
            $is_plain_number = preg_match('/^[+\-]?(?:\d+(?:\.\d+)?|\.\d+)$/D', $value) === 1;
            if (!$is_plain_number && preg_match('/^(?:[\t\r\n]|[\p{Z}\s\x{FEFF}]*[=+\-@])/u', $value) === 1) {
                $value = "'" . $value;
            }

            $safe_row[] = $value;
        }

        return $safe_row;
    }


    public function handle_export_event_attendees() {
        if (!is_user_logged_in()) {
            wp_die(esc_html__('Login required.', 'nexdirectory'));
        }
        if (!isset($_POST['nexdirectory_export_attendees_nonce']) || !wp_verify_nonce(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_export_attendees_nonce'])), 'nexdirectory_export_event_attendees')) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'));
        }
        $post_id = isset($_POST['event_id']) ? absint((int) wp_unslash($_POST['event_id'])) : 0;
        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            wp_die(esc_html__('Invalid event.', 'nexdirectory'));
        }

        $owner_id = (int) get_post_field('post_author', $post_id);
        if ($owner_id !== get_current_user_id() && !$this->user_can_moderate_events()) {
            wp_die(esc_html__('You cannot export attendees for this event.', 'nexdirectory'));
        }

        $rows = $this->get_event_attendee_rows($post_id);
        nocache_headers();
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . sanitize_file_name('event-attendees-' . $post_id . '-' . gmdate('Ymd-His') . '.csv') . '"');

        try {
            $output = new SplFileObject('php://output', 'w');
        } catch (RuntimeException $e) {
            exit;
        }
        $output->fputcsv($this->sanitize_csv_row(['Name', 'Email', 'Status', 'Communication', 'Updated']));
        foreach ($rows as $row) {
            $output->fputcsv($this->sanitize_csv_row([
                (string) ($row['name'] ?? ''),
                (string) ($row['email'] ?? ''),
                (string) ($row['status_label'] ?? ''),
                (string) ($row['communication_label'] ?? ''),
                (string) ($row['updated_label'] ?? ''),
            ]));
        }
        exit;
    }


    public function handle_email_event_attendees() {
        if (!is_user_logged_in()) {
            wp_die(esc_html__('Login required.', 'nexdirectory'));
        }
        if (!$this->user_can_access_platform(get_current_user_id())) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }

        $email_nonce = isset($_POST['nexdirectory_email_attendees_nonce'])
            ? sanitize_text_field((string) wp_unslash($_POST['nexdirectory_email_attendees_nonce']))
            : '';
        if ($email_nonce === '' || !wp_verify_nonce($email_nonce, 'nexdirectory_email_event_attendees')) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'));
        }
        $post_id = isset($_POST['event_id']) ? absint((int) wp_unslash($_POST['event_id'])) : 0;
        if ($post_id <= 0) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'));
        }

        $profile_key = $this->resolve_profile_key_for_post($post_id);
        if ($profile_key === '') {
            $profile_key = $this->get_site_profile_key();
        }
        $my_events_url = $this->get_platform_page_url(
            'nexdirectory_page_my_events_url',
            $this->get_profile_route_slugs('my_items', $profile_key),
            $this->get_profile_route_default_url('my_items', $profile_key)
        );
        $fallback_redirect = $post_id > 0 ? nexdirectory_add_query_arg('attendees_event', (string) $post_id, $my_events_url) : $my_events_url;
        $redirect = wp_get_referer() ?: $fallback_redirect;

        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            $this->redirect_with_message($redirect, __('Invalid event.', 'nexdirectory'), 'error');
        }

        $owner_id = (int) get_post_field('post_author', $post_id);
        if ($owner_id !== get_current_user_id() && !$this->user_can_moderate_events()) {
            wp_die(esc_html__('You cannot message attendees for this event.', 'nexdirectory'));
        }

        $audience = sanitize_key((string) wp_unslash($_POST['audience'] ?? 'confirmed'));
        if (!in_array($audience, ['confirmed', 'waitlist', 'all'], true)) {
            $audience = 'confirmed';
        }
        $subject_template = sanitize_text_field((string) wp_unslash($_POST['subject'] ?? ''));
        $message_template = trim(sanitize_text_field((string) wp_unslash($_POST['message'] ?? '')));
        $send_copy = (string) wp_unslash($_POST['send_copy'] ?? '0') === '1';

        if ($subject_template === '' || $message_template === '') {
            $this->redirect_with_message($redirect, __('Subject and message are required.', 'nexdirectory'), 'error');
        }
        if (function_exists('mb_strlen') ? mb_strlen($subject_template) > 160 : strlen($subject_template) > 160) {
            $this->redirect_with_message($redirect, __('Subject must be 160 characters or fewer.', 'nexdirectory'), 'error');
        }
        if (function_exists('mb_strlen') ? mb_strlen($message_template) > 6000 : strlen($message_template) > 6000) {
            $this->redirect_with_message($redirect, __('Message must be 6000 characters or fewer.', 'nexdirectory'), 'error');
        }

        $rows = $this->get_event_attendee_rows($post_id);
        if (empty($rows)) {
            $this->redirect_with_message($redirect, __('No attendees are available for messaging yet.', 'nexdirectory'), 'error');
        }

        $status_filter = '';
        if ($audience === 'confirmed') {
            $status_filter = 'yes';
        } elseif ($audience === 'waitlist') {
            $status_filter = 'waitlist';
        }

        $sender = wp_get_current_user();
        $sender_name = sanitize_text_field((string) ($sender->display_name ?: $sender->user_login));
        $sender_email = sanitize_email((string) ($sender->user_email ?? ''));
        $event_title = (string) get_the_title($post_id);
        $event_url = (string) get_permalink($post_id);
        $site_name = sanitize_text_field((string) $this->get_white_label_brand_name());

        $sent_count = 0;
        $failed_count = 0;
        $skipped_no_consent = 0;
        $skipped_invalid = 0;
        $notification_failed_count = 0;
        $failed_reasons = [];
        $processed = [];

        foreach ($rows as $row) {
            $row_status = sanitize_key((string) ($row['status'] ?? ''));
            if ($status_filter !== '' && $row_status !== $status_filter) {
                continue;
            }
            $recipient_id = absint((int) ($row['user_id'] ?? 0));
            if ($recipient_id <= 0 || isset($processed[$recipient_id])) {
                continue;
            }
            $processed[$recipient_id] = true;

            if (!$this->user_allows_organizer_communications($recipient_id)) {
                $skipped_no_consent++;
                continue;
            }

            $recipient = get_user_by('id', $recipient_id);
            if (!$recipient instanceof WP_User) {
                $skipped_invalid++;
                continue;
            }

            $tokens = [
                '{event_title}' => $event_title,
                '{event_url}' => $event_url,
                '{recipient_name}' => (string) ($recipient->display_name ?: $recipient->user_login),
                '{attendee_name}' => (string) ($recipient->display_name ?: $recipient->user_login),
                '{organizer_name}' => $sender_name,
                '{site_name}' => $site_name,
            ];
            $subject = nexdirectory_sanitize_text(strtr($subject_template, $tokens));
            if ($subject === '') {
                /* translators: %s: Placeholder value. */
                $subject = sprintf(__('Update about %s', 'nexdirectory'), $event_title);
            }

            $rendered_message = nexdirectory_sanitize_textarea((string) strtr($message_template, $tokens));
            if ($rendered_message === '') {
                $rendered_message = sprintf(
                    /* translators: 1: Placeholder value 1, 2: Placeholder value 2. */
                    __('Update about %1$s: %2$s', 'nexdirectory'),
                    $event_title,
                    $event_url
                );
            }

            $message_result = $this->inbox_send_message($post_id, get_current_user_id(), $recipient_id, $subject, $rendered_message);
            if (is_wp_error($message_result)) {
                $failed_count++;
                $error_code = sanitize_key((string) $message_result->get_error_code());
                if ($error_code === '') {
                    $error_code = 'unknown_error';
                }
                $failed_reasons[$error_code] = (int) ($failed_reasons[$error_code] ?? 0) + 1;
                $this->append_runtime_log('warning', 'Attendee inbox delivery failed', [
                    'post_id' => (string) $post_id,
                    'recipient_id' => (string) $recipient_id,
                    'error_code' => (string) $error_code,
                    'error_message' => (string) $message_result->get_error_message(),
                ]);
                continue;
            }
            $sent_count++;
            $thread_id = absint((int) ($message_result['thread_id'] ?? 0));
            $message_id = absint((int) ($message_result['message_id'] ?? 0));
            $notification_sent = $this->send_inbox_message_notification_email(
                $recipient_id,
                get_current_user_id(),
                $post_id,
                $thread_id,
                $message_id,
                $subject
            );
            if (!$notification_sent) {
                $notification_failed_count++;
            }
        }

        $copy_inbox_sent = false;
        $copy_email_sent = false;
        if ($send_copy) {
            /* translators: %s: Placeholder value. */
            $copy_subject = sprintf(__('Copy: attendee message for %s', 'nexdirectory'), $event_title);
            $copy_body = sprintf(
                "%s\n\n%s\n\n%s",
                __('This is a copy of the message sent to consented attendees.', 'nexdirectory'),
                $subject_template,
                $message_template
            );

            $copy_inbox_result = $this->inbox_send_message(
                $post_id,
                get_current_user_id(),
                get_current_user_id(),
                $copy_subject,
                nexdirectory_sanitize_textarea($copy_body)
            );
            $copy_inbox_sent = !is_wp_error($copy_inbox_result);
            if (!$copy_inbox_sent) {
                $this->append_runtime_log('warning', 'Attendee copy inbox delivery failed', [
                    'post_id' => (string) $post_id,
                    'sender_id' => (string) get_current_user_id(),
                    'error_code' => is_wp_error($copy_inbox_result) ? (string) $copy_inbox_result->get_error_code() : 'unknown_error',
                    'error_message' => is_wp_error($copy_inbox_result) ? (string) $copy_inbox_result->get_error_message() : 'unknown',
                ]);
            }

            if (is_email($sender_email)) {
                $copy_email_sent = (bool) $this->send_platform_email($sender_email, $copy_subject, '', [
                    'post_id' => $post_id,
                    'event_title' => $event_title,
                    'event_url' => $event_url,
                    'custom_body_html' => wpautop(wp_kses_post($copy_body)),
                    'fallback_text' => nexdirectory_sanitize_textarea($copy_body),
                ]);
            }
        }

        $this->append_runtime_log('info', 'Attendee communication campaign processed', [
            'post_id' => (string) $post_id,
            'audience' => $audience,
            'sent' => (string) $sent_count,
            'failed' => (string) $failed_count,
            'skipped_no_consent' => (string) $skipped_no_consent,
            'skipped_invalid' => (string) $skipped_invalid,
            'notification_failed' => (string) $notification_failed_count,
            'failed_reasons' => wp_json_encode($failed_reasons),
        ]);

        $message = sprintf(
            /* translators: 1: Placeholder value 1, 2: Placeholder value 2, 3: Placeholder value 3, 4: Placeholder value 4. */
            __('Campaign processed. Inbox messages sent: %1$d. Failed: %2$d. Skipped (GDPR opt-out): %3$d. Skipped (invalid): %4$d.', 'nexdirectory'),
            $sent_count,
            $failed_count,
            $skipped_no_consent,
            $skipped_invalid
        );
        if ($notification_failed_count > 0) {
            $message .= ' ' . sprintf(
                /* translators: %d: Placeholder value. */
                __('Notification emails not delivered: %d.', 'nexdirectory'),
                $notification_failed_count
            );
        }
        if ($failed_count > 0 && !empty($failed_reasons)) {
            arsort($failed_reasons);
            $summary_bits = [];
            foreach ($failed_reasons as $reason => $reason_count) {
                $summary_bits[] = $reason . ': ' . (int) $reason_count;
                if (count($summary_bits) >= 3) {
                    break;
                }
            }
            if (!empty($summary_bits)) {
                $message .= ' ' . sprintf(
                    /* translators: %s: Placeholder value. */
                    __('Failure reasons: %s.', 'nexdirectory'),
                    implode(', ', $summary_bits)
                );
            }
        }
        if ($send_copy && $copy_inbox_sent && $copy_email_sent) {
            $message .= ' ' . __('A copy was added to your inbox and sent to your account email.', 'nexdirectory');
        } elseif ($send_copy && $copy_inbox_sent && !$copy_email_sent) {
            $message .= ' ' . __('A copy was added to your inbox. Email copy could not be delivered.', 'nexdirectory');
        } elseif ($send_copy && !$copy_inbox_sent && $copy_email_sent) {
            $message .= ' ' . __('A copy was sent to your account email.', 'nexdirectory');
        } elseif ($send_copy) {
            $message .= ' ' . __('Could not send a copy to your inbox or account email.', 'nexdirectory');
        }

        $notice_type = $sent_count > 0 ? 'success' : 'error';
        $this->redirect_with_message($redirect, $message, $notice_type);
    }


    private function get_inbox_threads_table_name() {
        global $wpdb;
        return $wpdb->prefix . self::INBOX_THREADS_TABLE;
    }


    private function get_inbox_messages_table_name() {
        global $wpdb;
        return $wpdb->prefix . self::INBOX_MESSAGES_TABLE;
    }


    private function normalize_inbox_participants($user_a, $user_b) {
        $user_a = absint((int) $user_a);
        $user_b = absint((int) $user_b);
        if ($user_a <= 0 || $user_b <= 0) {
            return [0, 0];
        }
        if ($user_a <= $user_b) {
            return [$user_a, $user_b];
        }
        return [$user_b, $user_a];
    }


    private function get_inbox_thread_id_for_participants($post_id, $participant_one, $participant_two) {
        $post_id = absint((int) $post_id);
        [$participant_a, $participant_b] = $this->normalize_inbox_participants($participant_one, $participant_two);
        if ($post_id <= 0 || $participant_a <= 0 || $participant_b <= 0) {
            return 0;
        }

        global $wpdb;
        $threads_table = $this->get_inbox_threads_table_name();
        return absint((int) $wpdb->get_var($wpdb->prepare(
            "SELECT id FROM {$threads_table} WHERE post_id = %d AND participant_a = %d AND participant_b = %d LIMIT 1",
            $post_id,
            $participant_a,
            $participant_b
        )));
    }


    private function get_or_create_inbox_thread($post_id, $participant_one, $participant_two, $subject = '') {
        $post_id = absint((int) $post_id);
        [$participant_a, $participant_b] = $this->normalize_inbox_participants($participant_one, $participant_two);
        if ($post_id <= 0 || $participant_a <= 0 || $participant_b <= 0) {
            return new WP_Error('nexdirectory_inbox_invalid_thread', __('Invalid inbox thread request.', 'nexdirectory'));
        }

        global $wpdb;
        $threads_table = $this->get_inbox_threads_table_name();
        $existing_thread_id = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT id FROM {$threads_table} WHERE post_id = %d AND participant_a = %d AND participant_b = %d LIMIT 1",
            $post_id,
            $participant_a,
            $participant_b
        ));
        if ($existing_thread_id > 0) {
            if ($subject !== '') {
                $wpdb->update(
                    $threads_table,
                    ['subject' => sanitize_text_field((string) $subject)],
                    ['id' => $existing_thread_id],
                    ['%s'],
                    ['%d']
                );
            }
            return $existing_thread_id;
        }

        $now = current_time('mysql', 1);
        $inserted = $wpdb->insert($threads_table, [
            'post_id' => $post_id,
            'participant_a' => $participant_a,
            'participant_b' => $participant_b,
            'subject' => sanitize_text_field((string) $subject),
            'created_at' => $now,
            'updated_at' => $now,
            'last_message_id' => 0,
            'last_sender_id' => 0,
        ], ['%d', '%d', '%d', '%s', '%s', '%s', '%d', '%d']);
        if ($inserted === false) {
            $existing_thread_id = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT id FROM {$threads_table} WHERE post_id = %d AND participant_a = %d AND participant_b = %d LIMIT 1",
                $post_id,
                $participant_a,
                $participant_b
            ));
            if ($existing_thread_id > 0) {
                return $existing_thread_id;
            }
            return new WP_Error('nexdirectory_inbox_thread_create_failed', __('Could not start inbox thread.', 'nexdirectory'));
        }

        return (int) $wpdb->insert_id;
    }


    /**
     * Resolves the notification store without exposing it to templates.
     *
     * @return NEXDIRECTORY_Notification_Service|null
     */
    private function get_account_notification_service() {
        if (!function_exists('nexdirectory_get_bootstrap')) {
            return null;
        }
        $bootstrap = nexdirectory_get_bootstrap();
        if (!$bootstrap instanceof NEXDIRECTORY_Plugin_Bootstrap || !method_exists($bootstrap, 'get_service')) {
            return null;
        }
        $service = $bootstrap->get_service('notifier');
        return $service instanceof NEXDIRECTORY_Notification_Service ? $service : null;
    }


    /**
     * Stores a notification when the notification service is available.
     *
     * @param array<string, mixed> $args Notification fields.
     * @return int|false
     */
    private function create_account_notification(array $args) {
        $service = $this->get_account_notification_service();
        if (!$service) {
            return false;
        }
        return $service->create_notification($args);
    }


    private function inbox_send_message($post_id, $sender_id, $recipient_id, $subject, $body, $attachment_url = '') {
        $post_id = absint((int) $post_id);
        $sender_id = absint((int) $sender_id);
        $recipient_id = absint((int) $recipient_id);
        $subject = sanitize_text_field((string) $subject);
        $body = trim((string) nexdirectory_sanitize_textarea((string) $body));
        $attachment_url = esc_url_raw((string) $attachment_url);
        if ($post_id <= 0 || $sender_id <= 0 || $recipient_id <= 0 || $body === '') {
            return new WP_Error('nexdirectory_inbox_invalid_message', __('Invalid inbox message request.', 'nexdirectory'));
        }

        $thread_id = $this->get_or_create_inbox_thread($post_id, $sender_id, $recipient_id, $subject);
        if (is_wp_error($thread_id)) {
            return $thread_id;
        }

        global $wpdb;
        $messages_table = $this->get_inbox_messages_table_name();
        $threads_table = $this->get_inbox_threads_table_name();
        $now = current_time('mysql', 1);
        $read_at = $sender_id === $recipient_id ? $now : null;
        $inserted = $wpdb->insert($messages_table, [
            'thread_id' => (int) $thread_id,
            'post_id' => $post_id,
            'sender_id' => $sender_id,
            'recipient_id' => $recipient_id,
            'subject' => $subject,
            'body' => $body,
            'attachment_url' => $attachment_url,
            'created_at' => $now,
            'read_at' => $read_at,
        ], ['%d', '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%s']);
        if ($inserted === false) {
            return new WP_Error('nexdirectory_inbox_message_create_failed', __('Could not send inbox message.', 'nexdirectory'));
        }
        $message_id = (int) $wpdb->insert_id;

        $wpdb->update($threads_table, [
            'updated_at' => $now,
            'last_message_id' => $message_id,
            'last_sender_id' => $sender_id,
            'subject' => $subject,
        ], ['id' => (int) $thread_id], ['%s', '%d', '%d', '%s'], ['%d']);

        if ($recipient_id !== $sender_id && (string) get_post_meta($post_id, 'nexdirectory_demo_imported', true) !== '1') {
            $sender = get_user_by('id', $sender_id);
            $sender_name = $sender instanceof WP_User
                ? sanitize_text_field((string) ($sender->display_name ?: $sender->user_login))
                : __('Member', 'nexdirectory');
            /* translators: %s: Message sender display name. */
            $notification_title = sprintf(__('New message from %s', 'nexdirectory'), $sender_name);
            $this->create_account_notification([
                'recipient_id' => $recipient_id,
                'actor_id' => $sender_id,
                'type' => 'message',
                'object_id' => $post_id,
                'secondary_id' => $message_id,
                'title' => $notification_title,
                'body' => wp_trim_words(wp_strip_all_tags($body), 24, '…'),
                'url' => $this->build_inbox_thread_url((int) $thread_id, $message_id),
                'dedupe_key' => 'message:' . $message_id,
            ]);
        }

        return [
            'thread_id' => (int) $thread_id,
            'message_id' => $message_id,
        ];
    }


    private function get_notifications_page_url() {
        $base_url = (string) $this->get_platform_page_url(
            'nexdirectory_page_account_url',
            $this->get_profile_route_slugs('account'),
            $this->get_profile_route_default_url('account')
        );
        if ($base_url === '') {
            $base_url = (string) $this->get_platform_page_url(
                'nexdirectory_page_submit_event_url',
                $this->get_profile_route_slugs('submit'),
                $this->get_profile_route_default_url('submit')
            );
        }
        if ($base_url === '') {
            $base_url = (string) home_url('/submit/');
        }
        return esc_url_raw($base_url);
    }


    private function build_inbox_thread_url($thread_id = 0, $message_id = 0) {
        $thread_id = absint((int) $thread_id);
        $message_id = absint((int) $message_id);
        $base_url = $this->get_notifications_page_url();
        $query_args = [
            'nexdirectory_tab' => 'inbox',
        ];
        if ($thread_id > 0) {
            $query_args['thread'] = $thread_id;
        }
        if ($message_id > 0) {
            $query_args['message'] = $message_id;
        }
        $url = !empty($query_args) ? nexdirectory_add_query_arg($query_args, $base_url) : $base_url;
        if (strpos($url, '#') === false) {
            $url .= '#inbox';
        }
        return $url;
    }


    private function send_inbox_message_notification_email($recipient_id, $sender_id, $post_id, $thread_id, $message_id, $subject_line = '') {
        $recipient_id = absint((int) $recipient_id);
        $sender_id = absint((int) $sender_id);
        $post_id = absint((int) $post_id);
        $thread_id = absint((int) $thread_id);
        $message_id = absint((int) $message_id);
        if ($recipient_id <= 0 || $sender_id <= 0 || $post_id <= 0 || $thread_id <= 0 || $message_id <= 0) {
            return false;
        }

        $recipient = get_user_by('id', $recipient_id);
        if (!$recipient instanceof WP_User || !is_email((string) $recipient->user_email)) {
            return false;
        }
        $sender = get_user_by('id', $sender_id);
        $sender_name = $sender instanceof WP_User ? sanitize_text_field((string) ($sender->display_name ?: $sender->user_login)) : __('A member', 'nexdirectory');
        $event_title = sanitize_text_field(
            wp_specialchars_decode((string) get_the_title($post_id), ENT_QUOTES)
        );
        $thread_url = $this->build_inbox_thread_url($thread_id, $message_id);
        $subject = $subject_line !== ''
            ? sanitize_text_field((string) $subject_line)
            /* translators: 1: Placeholder value 1, 2: Placeholder value 2. */
            : sprintf(__('New message from %1$s about %2$s', 'nexdirectory'), $sender_name, $event_title);
        $body = sprintf(
            __("You received a new inbox message from %1\$s about \"%2\$s\".\n\nOpen your inbox:\n%3\$s", 'nexdirectory'),
            $sender_name,
            $event_title,
            $thread_url
        );

        return (bool) $this->send_platform_email((string) $recipient->user_email, $subject, '', [
            'post_id' => $post_id,
            'event_title' => $event_title,
            'event_url' => $thread_url,
            'custom_body_html' => wpautop(wp_kses_post($body)),
            'fallback_text' => nexdirectory_sanitize_textarea((string) $body),
        ]);
    }


    private function get_inbox_unread_count_for_user($user_id = 0) {
        $user_id = absint((int) $user_id);
        if ($user_id <= 0) {
            return 0;
        }
        global $wpdb;
        $messages_table = $this->get_inbox_messages_table_name();
        return max(0, (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$messages_table} WHERE recipient_id = %d AND read_at IS NULL",
            $user_id
        )));
    }


    private function get_inbox_threads_for_user($user_id, $limit = 60) {
        $user_id = absint((int) $user_id);
        $limit = max(1, min(300, (int) $limit));
        if ($user_id <= 0) {
            return [];
        }

        global $wpdb;
        $threads_table = $this->get_inbox_threads_table_name();
        $messages_table = $this->get_inbox_messages_table_name();
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT t.id, t.post_id, t.participant_a, t.participant_b, t.subject, t.updated_at, t.last_message_id, t.last_sender_id, m.body AS last_body, m.created_at AS last_created_at, m.subject AS last_subject
             FROM {$threads_table} t
             LEFT JOIN {$messages_table} m ON m.id = t.last_message_id
             WHERE t.participant_a = %d OR t.participant_b = %d
             ORDER BY t.updated_at DESC
             LIMIT %d",
            $user_id,
            $user_id,
            $limit
        ), ARRAY_A);
        if (!is_array($rows) || empty($rows)) {
            return [];
        }

        $thread_ids = [];
        $other_user_ids = [];
        $thread_post_ids = [];
        foreach ($rows as $row) {
            $thread_id = absint((int) ($row['id'] ?? 0));
            if ($thread_id > 0) {
                $thread_ids[] = $thread_id;
            }
            $participant_a = absint((int) ($row['participant_a'] ?? 0));
            $participant_b = absint((int) ($row['participant_b'] ?? 0));
            $other_id = $participant_a === $user_id ? $participant_b : $participant_a;
            if ($other_id > 0) {
                $other_user_ids[] = $other_id;
            }
            $row_post_id = absint((int) ($row['post_id'] ?? 0));
            if ($row_post_id > 0) {
                $thread_post_ids[] = $row_post_id;
            }
        }
        $thread_ids = array_values(array_unique($thread_ids));
        $other_user_ids = array_values(array_unique($other_user_ids));
        $thread_post_ids = array_values(array_unique($thread_post_ids));
        if (!empty($thread_post_ids) && function_exists('_prime_post_caches')) {
            _prime_post_caches($thread_post_ids, false, false);
        }

        $unread_map = [];
        if (!empty($thread_ids)) {
            $id_list_sql = implode(',', array_map('absint', $thread_ids));
            $unread_rows = $wpdb->get_results($wpdb->prepare(
                "SELECT thread_id, COUNT(*) AS unread_count FROM {$messages_table} WHERE recipient_id = %d AND read_at IS NULL AND thread_id IN ({$id_list_sql}) GROUP BY thread_id",
                $user_id
            ), ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            if (is_array($unread_rows)) {
                foreach ($unread_rows as $unread_row) {
                    $unread_map[absint((int) ($unread_row['thread_id'] ?? 0))] = max(0, (int) ($unread_row['unread_count'] ?? 0));
                }
            }
        }

        $users_map = [];
        if (!empty($other_user_ids)) {
            $users = get_users([
                'include' => $other_user_ids,
                'fields' => ['ID', 'display_name', 'user_login'],
            ]);
            foreach ((array) $users as $user) {
                if (!$user instanceof WP_User) {
                    continue;
                }
                $users_map[(int) $user->ID] = sanitize_text_field((string) ($user->display_name ?: $user->user_login));
            }
        }

        $out = [];
        foreach ($rows as $row) {
            $thread_id = absint((int) ($row['id'] ?? 0));
            if ($thread_id <= 0) {
                continue;
            }
            $participant_a = absint((int) ($row['participant_a'] ?? 0));
            $participant_b = absint((int) ($row['participant_b'] ?? 0));
            $other_id = $participant_a === $user_id ? $participant_b : $participant_a;
            $last_created_ts = strtotime((string) ($row['last_created_at'] ?? ''));
            $post_id = absint((int) ($row['post_id'] ?? 0));
            $event_title = $post_id > 0 ? (string) get_the_title($post_id) : '';
            $out[] = [
                'thread_id' => $thread_id,
                'post_id' => $post_id,
                'event_title' => $event_title,
                'subject' => sanitize_text_field((string) (($row['last_subject'] ?? '') !== '' ? $row['last_subject'] : ($row['subject'] ?? ''))),
                'last_body' => sanitize_text_field((string) wp_trim_words((string) ($row['last_body'] ?? ''), 18)),
                'last_time' => $last_created_ts ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), $last_created_ts) : '',
                'last_sender_id' => absint((int) ($row['last_sender_id'] ?? 0)),
                'other_user_id' => $other_id,
                'other_name' => (string) ($users_map[$other_id] ?? __('Member', 'nexdirectory')),
                'unread_count' => (int) ($unread_map[$thread_id] ?? 0),
                'thread_url' => $this->build_inbox_thread_url($thread_id, 0),
            ];
        }

        return $out;
    }


    private function get_inbox_thread_for_user($thread_id, $user_id) {
        $thread_id = absint((int) $thread_id);
        $user_id = absint((int) $user_id);
        if ($thread_id <= 0 || $user_id <= 0) {
            return null;
        }

        global $wpdb;
        $threads_table = $this->get_inbox_threads_table_name();
        $row = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$threads_table} WHERE id = %d LIMIT 1",
            $thread_id
        ), ARRAY_A);
        if (!is_array($row)) {
            return null;
        }
        $participant_a = absint((int) ($row['participant_a'] ?? 0));
        $participant_b = absint((int) ($row['participant_b'] ?? 0));
        if ($participant_a !== $user_id && $participant_b !== $user_id) {
            return null;
        }
        return $row;
    }


    private function mark_inbox_thread_read_for_user($thread_id, $user_id) {
        $thread_id = absint((int) $thread_id);
        $user_id = absint((int) $user_id);
        if ($thread_id <= 0 || $user_id <= 0) {
            return;
        }
        global $wpdb;
        $messages_table = $this->get_inbox_messages_table_name();
        $now = current_time('mysql', 1);
        $wpdb->query($wpdb->prepare(
            "UPDATE {$messages_table} SET read_at = %s WHERE thread_id = %d AND recipient_id = %d AND read_at IS NULL",
            $now,
            $thread_id,
            $user_id
        )); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    }


    private function get_inbox_messages_for_thread($thread_id, $limit = 400) {
        $thread_id = absint((int) $thread_id);
        $limit = max(1, min(2000, (int) $limit));
        if ($thread_id <= 0) {
            return [];
        }
        global $wpdb;
        $messages_table = $this->get_inbox_messages_table_name();
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, thread_id, post_id, sender_id, recipient_id, subject, body, attachment_url, created_at, read_at
             FROM {$messages_table}
             WHERE thread_id = %d
             ORDER BY created_at ASC, id ASC
             LIMIT %d",
            $thread_id,
            $limit
        ), ARRAY_A);
        return is_array($rows) ? $rows : [];
    }


    private function get_inbox_latest_unread_for_user($user_id) {
        $user_id = absint((int) $user_id);
        if ($user_id <= 0) {
            return [];
        }
        global $wpdb;
        $messages_table = $this->get_inbox_messages_table_name();
        $row = $wpdb->get_row($wpdb->prepare(
            "SELECT id, thread_id, post_id, sender_id, subject, created_at
             FROM {$messages_table}
             WHERE recipient_id = %d AND read_at IS NULL
             ORDER BY id DESC
             LIMIT 1",
            $user_id
        ), ARRAY_A);
        return is_array($row) ? $row : [];
    }


    public function handle_inbox_reply() {
        $redirect = isset($_POST['redirect_to']) ? esc_url_raw((string) wp_unslash($_POST['redirect_to'])) : '';
        if ($redirect === '') {
            $redirect = wp_get_referer() ?: home_url('/');
        }

        if (!is_user_logged_in()) {
            wp_safe_redirect(wp_login_url($redirect));
            exit;
        }
        if (!$this->user_can_access_platform(get_current_user_id())) {
            $this->redirect_with_message($redirect, __('Not allowed.', 'nexdirectory'), 'error');
        }

        if (!isset($_POST['nexdirectory_inbox_reply_nonce']) || !wp_verify_nonce(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_inbox_reply_nonce'])), 'nexdirectory_inbox_reply')) {
            $this->redirect_with_message($redirect, __('Invalid request.', 'nexdirectory'), 'error');
        }
        $thread_id = isset($_POST['thread_id']) ? absint((int) wp_unslash($_POST['thread_id'])) : 0;
        if ($thread_id <= 0) {
            $this->redirect_with_message($redirect, __('Invalid inbox thread.', 'nexdirectory'), 'error');
        }

        $current_user_id = (int) get_current_user_id();
        $thread = $this->get_inbox_thread_for_user($thread_id, $current_user_id);
        if (!is_array($thread) || empty($thread)) {
            $this->redirect_with_message($redirect, __('You cannot reply to this inbox thread.', 'nexdirectory'), 'error');
        }
        $message = sanitize_textarea_field((string) wp_unslash($_POST['message'] ?? ''));
        if (trim($message) === '') {
            $this->redirect_with_message($redirect, __('Message content is required.', 'nexdirectory'), 'error');
        }

        $participant_a = absint((int) ($thread['participant_a'] ?? 0));
        $participant_b = absint((int) ($thread['participant_b'] ?? 0));
        $recipient_id = $participant_a === $current_user_id ? $participant_b : $participant_a;
        if ($recipient_id <= 0 || $recipient_id === $current_user_id) {
            $this->redirect_with_message($redirect, __('Invalid inbox recipient.', 'nexdirectory'), 'error');
        }

        $post_id = absint((int) ($thread['post_id'] ?? 0));
        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            $this->redirect_with_message($redirect, __('Inbox thread is no longer available.', 'nexdirectory'), 'error');
        }

        $subject = sanitize_text_field((string) wp_unslash($_POST['subject'] ?? ''));
        if ($subject === '') {
            $subject = sanitize_text_field((string) ($thread['subject'] ?? ''));
        }
        if ($subject === '') {
            /* translators: %s: Placeholder value. */
            $subject = sprintf(__('Message about %s', 'nexdirectory'), get_the_title($post_id));
        }

        $send_result = $this->inbox_send_message($post_id, $current_user_id, $recipient_id, $subject, $message);
        if (is_wp_error($send_result)) {
            $this->redirect_with_message($redirect, $send_result->get_error_message(), 'error');
        }

        $message_id = absint((int) ($send_result['message_id'] ?? 0));
        $this->send_inbox_message_notification_email(
            $recipient_id,
            $current_user_id,
            $post_id,
            $thread_id,
            $message_id,
            $subject
        );

        $this->redirect_with_message(
            $this->build_inbox_thread_url($thread_id, $message_id),
            __('Reply sent.', 'nexdirectory')
        );
    }


    public function render_events_calendar($atts = []) {
        $atts = shortcode_atts([
            'max_events' => 300,
        ], (array) $atts, 'nexdirectory_events_calendar');
        $max_events = max(10, min(1000, (int) $atts['max_events']));
        $ui_labels = $this->get_site_profile_ui_labels();
        $profile_key = sanitize_key((string) ($ui_labels['profile_key'] ?? ''));

        $requested_month = isset($_GET['nexdirectory_month']) ? sanitize_text_field((string) wp_unslash($_GET['nexdirectory_month'])) : '';
        if (!preg_match('/^\d{4}\-\d{2}$/', $requested_month)) {
            $requested_month = wp_date('Y-m');
        }

        $tz = wp_timezone();
        try {
            $month_cursor = new DateTimeImmutable($requested_month . '-01 00:00:00', $tz);
        } catch (Exception $e) {
            $month_cursor = new DateTimeImmutable('first day of this month 00:00:00', $tz);
        }
        $month_start = $month_cursor->setTime(0, 0, 0);
        $next_month = $month_start->modify('first day of next month');
        $prev_month = $month_start->modify('first day of previous month');
        $month_end_day = $next_month->modify('-1 day')->setTime(0, 0, 0);
        $month_start_sql = $month_start->format('Y-m-d H:i:s');
        $next_month_sql = $next_month->format('Y-m-d H:i:s');

        $query = new WP_Query([
            'post_type' => self::POST_TYPE,
            'post_status' => 'publish',
            'posts_per_page' => $max_events,
            'meta_key' => 'nexdirectory_start',
            'orderby' => 'meta_value',
            'order' => 'ASC',
            'meta_query' => [
                'relation' => 'AND',
                [
                    'key' => 'nexdirectory_start',
                    'value' => $next_month_sql,
                    'compare' => '<',
                    'type' => 'DATETIME',
                ],
                [
                    'relation' => 'OR',
                    [
                        'key' => 'nexdirectory_end',
                        'value' => $month_start_sql,
                        'compare' => '>=',
                        'type' => 'DATETIME',
                    ],
                    [
                        'key' => 'nexdirectory_start',
                        'value' => $month_start_sql,
                        'compare' => '>=',
                        'type' => 'DATETIME',
                    ],
                ],
            ],
        ]);

        $events_by_day = [];
        $decode_calendar_text = static function ($raw) {
            $value = nexdirectory_strip_all_tags((string) $raw, true);
            // Decode twice so strings like "&amp;#8211;" resolve to the final glyph.
            $value = html_entity_decode($value, ENT_QUOTES, 'UTF-8');
            $value = html_entity_decode($value, ENT_QUOTES, 'UTF-8');
            return nexdirectory_sanitize_text($value);
        };
        if ($query->have_posts()) {
            while ($query->have_posts()) {
                $query->the_post();
                $event_id = get_the_ID();
                $start_raw = (string) get_post_meta($event_id, 'nexdirectory_start', true);
                $end_raw = (string) get_post_meta($event_id, 'nexdirectory_end', true);
                $start_ts = strtotime($start_raw);
                if (!$start_ts) {
                    continue;
                }
                $end_ts = $end_raw !== '' ? strtotime($end_raw) : false;
                if (!$end_ts || $end_ts < $start_ts) {
                    $end_ts = $start_ts;
                }

                $event_start = (new DateTimeImmutable('@' . $start_ts))->setTimezone($tz);
                $event_end = (new DateTimeImmutable('@' . $end_ts))->setTimezone($tz);
                $event_start_day = $event_start->setTime(0, 0, 0);
                $event_end_day = $event_end->setTime(0, 0, 0);
                $is_multi_day = $event_end_day > $event_start_day;

                $range_start = $event_start_day < $month_start ? $month_start : $event_start_day;
                $range_end = $event_end_day > $month_end_day ? $month_end_day : $event_end_day;
                if ($range_end < $range_start) {
                    continue;
                }

                $city = $decode_calendar_text((string) get_post_meta($event_id, 'nexdirectory_city', true));
                $title = $decode_calendar_text((string) get_the_title());
                $event_title = $title !== '' ? $title : sanitize_text_field((string) __('Untitled Event', 'nexdirectory'));
                $event_url = get_permalink();
                $start_day_key = $event_start_day->format('Y-m-d');
                $end_day_key = $event_end_day->format('Y-m-d');

                for ($cursor = $range_start; $cursor <= $range_end; $cursor = $cursor->modify('+1 day')) {
                    $day_key = $cursor->format('Y-m-d');
                    if (!isset($events_by_day[$day_key])) {
                        $events_by_day[$day_key] = [];
                    }

                    $is_start_day = $day_key === $start_day_key;
                    $is_end_day = $day_key === $end_day_key;
                    $time_label = '';
                    if ($is_start_day && $this->event_datetime_has_time($start_raw)) {
                        $time_label = wp_date(get_option('time_format'), $start_ts);
                    } elseif ($is_end_day && $is_multi_day && $this->event_datetime_has_time($end_raw)) {
                        $time_label = sprintf(
                            /* translators: %s: time */
                            __('Ends %s', 'nexdirectory'),
                            wp_date(get_option('time_format'), $end_ts)
                        );
                    } elseif ($is_multi_day) {
                        $time_label = __('Multi-day', 'nexdirectory');
                    }

                    $events_by_day[$day_key][] = [
                        'id' => $event_id,
                        'title' => $event_title,
                        'url' => $event_url,
                        'time' => $time_label,
                        'city' => $city,
                        'type' => 'event',
                        'is_multi_day' => $is_multi_day,
                    ];
                }
            }
        }
        wp_reset_postdata();

        $start_of_week = max(0, min(6, (int) get_option('start_of_week', 1)));
        $first_weekday = (int) $month_start->format('w');
        $leading_empty = ($first_weekday - $start_of_week + 7) % 7;
        $days_in_month = (int) $month_start->format('t');
        $today_key = wp_date('Y-m-d');

        $cells = [];
        for ($i = 0; $i < $leading_empty; $i++) {
            $cells[] = null;
        }
        for ($day = 1; $day <= $days_in_month; $day++) {
            $day_key = $month_start->setDate(
                (int) $month_start->format('Y'),
                (int) $month_start->format('m'),
                $day
            )->format('Y-m-d');
            $cells[] = [
                'day' => $day,
                'day_key' => $day_key,
                'is_today' => $day_key === $today_key,
                'events' => isset($events_by_day[$day_key]) ? $events_by_day[$day_key] : [],
            ];
        }
        while (count($cells) % 7 !== 0) {
            $cells[] = null;
        }

        $weekday_labels = [];
        for ($i = 0; $i < 7; $i++) {
            $weekday = ($start_of_week + $i) % 7;
            $weekday_labels[] = wp_date('D', strtotime('Sunday +' . $weekday . ' days'));
        }

        $base_url = nexdirectory_remove_query_arg('nexdirectory_month', (string) $this->current_url());
        $prev_url = nexdirectory_add_query_arg('nexdirectory_month', $prev_month->format('Y-m'), $base_url);
        $next_url = nexdirectory_add_query_arg('nexdirectory_month', $next_month->format('Y-m'), $base_url);

        return $this->render_frontend_template('events-calendar.php', [
            'month_label' => wp_date('F Y', $month_start->getTimestamp()),
            'month_key' => $month_start->format('Y-m'),
            'weekday_labels' => $weekday_labels,
            'cells' => $cells,
            'prev_url' => $prev_url,
            'next_url' => $next_url,
            'events_url' => $this->get_platform_page_url(
                'nexdirectory_page_events_url',
                $this->get_profile_route_slugs('events', $profile_key),
                $this->get_profile_route_default_url('events', $profile_key)
            ),
            'max_inline_per_day' => 3,
            'ui_labels' => $ui_labels,
        ]);
    }


    public function handle_upload_image() {
        if (!is_user_logged_in()) {
            wp_send_json_error(['message' => __('You are not allowed to upload files.', 'nexdirectory')], 403);
        }
        $current_user_id = absint((int) get_current_user_id());
        if (!$this->user_can_submit_events($current_user_id)) {
            wp_send_json_error(['message' => __('You do not have permission to upload files.', 'nexdirectory')], 403);
        }
        if (!$this->user_verified_for_first_submission($current_user_id)) {
            wp_send_json_error(['message' => __('Verify your email before uploading files.', 'nexdirectory')], 403);
        }
        if (!isset($_POST['nexdirectory_upload_nonce']) || !wp_verify_nonce(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_upload_nonce'])), 'nexdirectory_upload_image_nonce')) {
            wp_send_json_error(['message' => __('Invalid upload request.', 'nexdirectory')], 400);
        }
        if (empty($_FILES['file']) || !is_array($_FILES['file'])) {
            wp_send_json_error(['message' => __('No image file received.', 'nexdirectory')], 400);
        }

        $file = $this->sanitize_uploaded_file_array($_FILES['file']);
        $upload_error = absint((int) ($file['error'] ?? UPLOAD_ERR_NO_FILE));
        if ($upload_error !== UPLOAD_ERR_OK) {
            wp_send_json_error(['message' => __('The upload did not complete. Please choose the file again.', 'nexdirectory')], 400);
        }
        $size = isset($file['size']) ? absint((int) $file['size']) : 0;
        if ($size <= 0) {
            wp_send_json_error(['message' => __('Uploaded file is empty.', 'nexdirectory')], 400);
        }
        $rate_window = 10 * MINUTE_IN_SECONDS;
        $rate_key = 'nexdirectory_upload_rate_' . $current_user_id . '_' . (int) floor(time() / $rate_window);
        $upload_count = absint((int) get_transient($rate_key));
        $upload_limit = max(3, min(60, (int) apply_filters('nexdirectory_frontend_upload_limit_per_window', 20, $current_user_id, $rate_window)));
        if ($upload_count >= $upload_limit) {
            wp_send_json_error(['message' => __('Upload limit reached. Please wait a few minutes and try again.', 'nexdirectory')], 429);
        }
        set_transient($rate_key, $upload_count + 1, $rate_window + MINUTE_IN_SECONDS);
        $upload_kind = sanitize_key((string) wp_unslash($_POST['nexdirectory_upload_kind'] ?? 'image'));
        if (!in_array($upload_kind, ['image', 'file'], true)) {
            $upload_kind = 'image';
        }
        $upload_target = sanitize_key((string) wp_unslash($_POST['nexdirectory_upload_target'] ?? 'gallery'));
        $limits = $this->get_upload_limits();
        $max_bytes = (int) ($limits['gallery_max_bytes'] ?? 2048000);
        if (in_array($upload_target, ['cover', 'custom_image'], true)) {
            $max_bytes = (int) ($limits['cover_max_bytes'] ?? 512000);
        } elseif (in_array($upload_target, ['custom_file', 'file'], true) || $upload_kind === 'file') {
            $max_bytes = (int) ($limits['file_max_bytes'] ?? 2097152);
        }
        if ($max_bytes > 0 && $size > $max_bytes) {
            $max_kb = max(1, (int) floor($max_bytes / 1024));
            wp_send_json_error([
                /* translators: %d: Placeholder value. */
                'message' => sprintf(__('File is too large. Max allowed size is %d KB.', 'nexdirectory'), $max_kb),
            ], 400);
        }

        $tmp_name = isset($file['tmp_name']) ? (string) $file['tmp_name'] : '';
        $name = isset($file['name']) ? (string) $file['name'] : '';
        $allowed_mimes = get_allowed_mime_types();
        $check = wp_check_filetype_and_ext($tmp_name, $name, $allowed_mimes);
        $mime = (string) ($check['type'] ?? '');
        if ($mime === '') {
            $basic = wp_check_filetype($name, $allowed_mimes);
            $mime = (string) ($basic['type'] ?? '');
        }
        if ($mime === '' && $upload_kind === 'image' && function_exists('getimagesize')) {
            $image_info = @getimagesize($tmp_name); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
            if (is_array($image_info) && !empty($image_info['mime']) && strpos((string) $image_info['mime'], 'image/') === 0) {
                $mime = (string) $image_info['mime'];
            }
        }
        if ($upload_kind === 'image') {
            if ($mime === '' || strpos($mime, 'image/') !== 0) {
                wp_send_json_error(['message' => __('Only image uploads are allowed for this field.', 'nexdirectory')], 400);
            }
        } elseif ($mime === '') {
            wp_send_json_error(['message' => __('This file type is not allowed.', 'nexdirectory')], 400);
        }

        $uploaded = $this->move_frontend_upload_to_plugin_uploads($file, $mime, $upload_kind === 'image' ? 'frontend-media' : 'frontend-files');
        if (is_wp_error($uploaded)) {
            $message = (string) $uploaded->get_error_message();
            $this->append_runtime_log('error', 'Frontend upload failed', [
                'target' => $upload_target,
                'kind' => $upload_kind,
                'filename' => sanitize_file_name((string) $name),
                'reason' => $message,
            ]);
            wp_send_json_error(['message' => $message !== '' ? $message : __('Image upload failed.', 'nexdirectory')], 500);
        }

        $attachment = [
            'post_mime_type' => (string) ($uploaded['type'] ?? $mime),
            'post_title' => sanitize_text_field(pathinfo((string) $name, PATHINFO_FILENAME)),
            'post_content' => '',
            'post_status' => 'inherit',
            'post_author' => get_current_user_id(),
        ];
        $attachment_id = wp_insert_attachment($attachment, (string) $uploaded['file']);
        if (!$attachment_id || is_wp_error($attachment_id)) {
            $this->append_runtime_log('error', 'Frontend upload attachment insert failed', [
                'target' => $upload_target,
                'kind' => $upload_kind,
                'filename' => sanitize_file_name((string) $name),
                'error' => is_wp_error($attachment_id) ? $attachment_id->get_error_message() : '',
            ]);
            wp_send_json_error(['message' => __('Could not store uploaded image.', 'nexdirectory')], 500);
        }

        // Skip frontend image sub-size generation to avoid server memory/time failures.

        $url = wp_get_attachment_url((int) $attachment_id);
        if (!is_string($url) || $url === '') {
            $this->append_runtime_log('error', 'Frontend upload missing attachment URL', [
                'attachment_id' => (string) (int) $attachment_id,
                'filename' => sanitize_file_name((string) $name),
            ]);
            wp_send_json_error(['message' => __('Image uploaded but URL was unavailable.', 'nexdirectory')], 500);
        }

        wp_send_json_success([
            'id' => (int) $attachment_id,
            'url' => esc_url_raw($url),
            'kind' => $upload_kind,
            'name' => sanitize_file_name((string) $name),
        ]);
    }

    /**
     * Keeps low-trust submitters out of WordPress' unrestricted media endpoints.
     *
     * Nex Directory uploads remain available through the scoped handler above,
     * where verification, MIME, size, nonce, and rate checks are enforced.
     *
     * @param array<string, bool> $allcaps
     * @param array<int, string> $caps
     * @param array<int, mixed> $args
     * @param WP_User $user
     * @return array<string, bool>
     */
    public function filter_submitter_core_upload_capability($allcaps, $caps, $args, $user) {
        if (!$user instanceof WP_User) {
            return is_array($allcaps) ? $allcaps : [];
        }
        $roles = array_map('sanitize_key', (array) $user->roles);
        if (in_array('event_submitter', $roles, true) && empty($allcaps['manage_options'])) {
            unset($allcaps['upload_files']);
        }
        return is_array($allcaps) ? $allcaps : [];
    }

    /**
     * Sanitizes a single or multi-file upload array before validation.
     *
     * @param array<string, mixed> $file
     * @return array<string, mixed>
     */
    private function sanitize_uploaded_file_array(array $file) {
        $sanitize_list = static function ($value, $callback) {
            if (is_array($value)) {
                return array_map(
                    static function ($item) use ($callback) {
                        return $callback($item);
                    },
                    $value
                );
            }
            return $callback($value);
        };

        $sanitized = $file;
        if (array_key_exists('name', $sanitized)) {
            $sanitized['name'] = $sanitize_list($sanitized['name'], static function ($value) {
                return sanitize_file_name((string) $value);
            });
        }
        if (array_key_exists('tmp_name', $sanitized)) {
            $sanitized['tmp_name'] = $sanitize_list($sanitized['tmp_name'], static function ($value) {
                return sanitize_text_field((string) $value);
            });
        }
        if (array_key_exists('type', $sanitized)) {
            $sanitized['type'] = $sanitize_list($sanitized['type'], static function ($value) {
                return sanitize_mime_type((string) $value);
            });
        }
        if (array_key_exists('error', $sanitized)) {
            $sanitized['error'] = $sanitize_list($sanitized['error'], static function ($value) {
                return absint((int) $value);
            });
        }
        if (array_key_exists('size', $sanitized)) {
            $sanitized['size'] = $sanitize_list($sanitized['size'], static function ($value) {
                return absint((int) $value);
            });
        }

        return $sanitized;
    }

    /**
     * Checks that a submit/update request nonce has already authorized file processing.
     *
     * @return bool
     */
    private function current_request_has_listing_write_nonce() {
        $submit_nonce = isset($_POST['nexdirectory_nonce'])
            ? sanitize_text_field((string) wp_unslash($_POST['nexdirectory_nonce']))
            : '';
        if ($submit_nonce !== '' && wp_verify_nonce($submit_nonce, 'nexdirectory_submit_event_nonce')) {
            return true;
        }

        $post_id = isset($_POST['post_id']) ? absint((int) wp_unslash($_POST['post_id'])) : 0;
        $update_nonce = isset($_POST['nexdirectory_update_nonce'])
            ? sanitize_text_field((string) wp_unslash($_POST['nexdirectory_update_nonce']))
            : '';

        return $post_id > 0
            && $update_nonce !== ''
            && wp_verify_nonce($update_nonce, 'nexdirectory_update_event');
    }

    /**
     * Moves a verified PHP upload into WordPress uploads.
     *
     * @param array<string, mixed> $file
     * @param string               $mime
     * @param string               $subdirectory
     * @return array{file:string,url:string,type:string}|WP_Error
     */
    private function move_frontend_upload_to_plugin_uploads(array $file, $mime, $subdirectory = 'frontend-media') {
        $tmp_name = isset($file['tmp_name']) ? (string) $file['tmp_name'] : '';
        if ($tmp_name === '' || !is_uploaded_file($tmp_name)) {
            return new WP_Error('nexdirectory_upload_tmp_invalid', __('The uploaded file could not be processed.', 'nexdirectory'));
        }

        $original_name = sanitize_file_name((string) ($file['name'] ?? 'upload'));
        if ($original_name === '') {
            $original_name = 'upload';
        }

        if (!function_exists('wp_handle_upload')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }

        $file['name'] = $original_name;
        $file['type'] = sanitize_mime_type((string) $mime);
        $uploaded = wp_handle_upload($file, [
            'test_form' => false,
            'mimes' => get_allowed_mime_types(),
        ]);

        if (!is_array($uploaded) || !empty($uploaded['error'])) {
            $message = is_array($uploaded) ? sanitize_text_field((string) ($uploaded['error'] ?? '')) : '';
            return new WP_Error('nexdirectory_upload_move_failed', $message !== '' ? $message : __('The uploaded file could not be stored.', 'nexdirectory'));
        }
        if (empty($uploaded['file']) || empty($uploaded['url'])) {
            return new WP_Error('nexdirectory_upload_move_failed', __('The uploaded file could not be stored.', 'nexdirectory'));
        }

        return [
            'file' => wp_normalize_path((string) $uploaded['file']),
            'url' => esc_url_raw((string) $uploaded['url']),
            'type' => sanitize_mime_type((string) ($uploaded['type'] ?? $mime)),
        ];
    }

    /**
     * Sideloads a plugin-owned local/remote source file into the media library.
     *
     * @param string $source_path
     * @param string $filename
     * @param int    $post_id
     * @param string $subdirectory
     * @return array{url:string,attachment_id:int}|WP_Error
     */
    private function store_file_source_as_attachment($source_path, $filename, $post_id = 0, $subdirectory = 'demo-media') {
        $source_path = (string) $source_path;
        if ($source_path === '' || !is_readable($source_path) || !is_file($source_path)) {
            return new WP_Error('nexdirectory_source_file_missing', __('The source file could not be imported.', 'nexdirectory'));
        }

        $filename = sanitize_file_name((string) $filename);
        if ($filename === '') {
            $filename = 'nexdirectory-file-' . substr(md5($source_path), 0, 12);
        }

        $filetype = wp_check_filetype($filename, get_allowed_mime_types());
        $mime = sanitize_mime_type((string) ($filetype['type'] ?? ''));
        if ($mime === '') {
            return new WP_Error('nexdirectory_source_file_type_invalid', __('The source file type is not allowed.', 'nexdirectory'));
        }

        if (!function_exists('media_handle_sideload')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
            require_once ABSPATH . 'wp-admin/includes/media.php';
            require_once ABSPATH . 'wp-admin/includes/image.php';
        }

        $temporary_file = wp_tempnam($filename);
        if (!$temporary_file) {
            return new WP_Error('nexdirectory_source_temp_failed', __('The source file could not be prepared for import.', 'nexdirectory'));
        }

        $filesystem = function_exists('nexdirectory_get_wp_filesystem') ? nexdirectory_get_wp_filesystem() : null;
        if (!is_object($filesystem) || !method_exists($filesystem, 'copy') || !$filesystem->copy($source_path, $temporary_file, true, defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : 0644)) {
            wp_delete_file($temporary_file);
            return new WP_Error('nexdirectory_source_file_copy_failed', __('The source file could not be copied into uploads.', 'nexdirectory'));
        }

        $file_array = [
            'name' => $filename,
            'type' => $mime,
            'tmp_name' => $temporary_file,
            'error' => 0,
            'size' => filesize($temporary_file),
        ];

        $attachment_id = media_handle_sideload($file_array, absint((int) $post_id));
        if (!$attachment_id || is_wp_error($attachment_id)) {
            wp_delete_file($temporary_file);
            return is_wp_error($attachment_id)
                ? $attachment_id
                : new WP_Error('nexdirectory_source_attachment_failed', __('The source file could not be registered.', 'nexdirectory'));
        }

        $attachment_url = esc_url_raw((string) wp_get_attachment_url(absint((int) $attachment_id)));
        if ($attachment_url === '') {
            return new WP_Error('nexdirectory_source_attachment_url_failed', __('The imported media URL could not be resolved.', 'nexdirectory'));
        }

        return [
            'url' => $attachment_url,
            'attachment_id' => absint((int) $attachment_id),
        ];
    }


    private function normalize_video_urls_textarea($raw) {
        $rows = preg_split('/\r\n|\r|\n/', (string) $raw);
        if (!is_array($rows)) {
            return '';
        }

        $normalized = [];
        foreach ($rows as $row) {
            $value = trim((string) $row);
            if ($value === '') {
                continue;
            }
            if (preg_match('/^[A-Za-z0-9_-]{11}$/', $value)) {
                $normalized[] = 'https://www.youtube.com/watch?v=' . $value;
                continue;
            }
            $url = esc_url_raw($value);
            if ($url !== '') {
                $normalized[] = $url;
            }
        }

        return implode("\n", array_values(array_unique($normalized)));
    }


    private function collect_uploaded_gallery_image_urls_from_request($field_name, $max_files_override = 0, $max_bytes_override = 0) {
        $submit_nonce = isset($_POST['nexdirectory_nonce'])
            ? sanitize_text_field((string) wp_unslash($_POST['nexdirectory_nonce']))
            : '';
        $update_nonce = isset($_POST['nexdirectory_update_nonce'])
            ? sanitize_text_field((string) wp_unslash($_POST['nexdirectory_update_nonce']))
            : '';
        $post_id_for_nonce = isset($_POST['post_id']) ? absint((int) wp_unslash($_POST['post_id'])) : 0;
        $has_submit_nonce = $submit_nonce !== '' && wp_verify_nonce($submit_nonce, 'nexdirectory_submit_event_nonce');
        $has_update_nonce = $post_id_for_nonce > 0 && $update_nonce !== '' && wp_verify_nonce($update_nonce, 'nexdirectory_update_event');
        if (!$has_submit_nonce && !$has_update_nonce) {
            return [];
        }
        $field_name = sanitize_key((string) $field_name);
        if ($field_name === '' || empty($_FILES[$field_name]) || !is_array($_FILES[$field_name])) {
            return [];
        }

        $files = $this->sanitize_uploaded_file_array($_FILES[$field_name]);
        if (isset($files['name']) && !is_array($files['name'])) {
            $files = [
                'name' => [$files['name']],
                'tmp_name' => [isset($files['tmp_name']) ? $files['tmp_name'] : ''],
                'error' => [isset($files['error']) ? $files['error'] : UPLOAD_ERR_NO_FILE],
                'size' => [isset($files['size']) ? $files['size'] : 0],
                'type' => [isset($files['type']) ? $files['type'] : ''],
            ];
        }
        $names = isset($files['name']) && is_array($files['name']) ? $files['name'] : [];
        $tmp_names = isset($files['tmp_name']) && is_array($files['tmp_name']) ? $files['tmp_name'] : [];
        $errors = isset($files['error']) && is_array($files['error']) ? $files['error'] : [];
        $sizes = isset($files['size']) && is_array($files['size']) ? $files['size'] : [];
        $types = isset($files['type']) && is_array($files['type']) ? $files['type'] : [];
        if (empty($names)) {
            return [];
        }
        $limits = $this->get_upload_limits();
        $max_bytes = (int) ($limits['gallery_max_bytes'] ?? 2048000);
        $max_files = (int) ($limits['gallery_max_files'] ?? 10);
        if ((int) $max_bytes_override > 0) {
            $max_bytes = (int) $max_bytes_override;
        }
        if ((int) $max_files_override > 0) {
            $max_files = (int) $max_files_override;
        }
        if ($max_files < 1) {
            $max_files = 10;
        }

        $urls = [];
        foreach ($names as $index => $name) {
            if (count($urls) >= $max_files) {
                break;
            }
            $error = isset($errors[$index]) ? (int) $errors[$index] : UPLOAD_ERR_NO_FILE;
            if ($error === UPLOAD_ERR_NO_FILE) {
                continue;
            }
            if ($error !== UPLOAD_ERR_OK) {
                continue;
            }

            $tmp_name = isset($tmp_names[$index]) ? (string) $tmp_names[$index] : '';
            $file_name = isset($names[$index]) ? (string) $names[$index] : '';
            $size = isset($sizes[$index]) ? (int) $sizes[$index] : 0;
            if ($tmp_name === '' || $file_name === '' || $size <= 0 || $size > $max_bytes) {
                continue;
            }

            $check = wp_check_filetype_and_ext($tmp_name, $file_name);
            $mime = (string) ($check['type'] ?? '');
            if ($mime === '' || strpos($mime, 'image/') !== 0) {
                continue;
            }

            $single_file = [
                'name' => $file_name,
                'type' => isset($types[$index]) ? (string) $types[$index] : $mime,
                'tmp_name' => $tmp_name,
                'error' => $error,
                'size' => $size,
            ];
            $uploaded = $this->move_frontend_upload_to_plugin_uploads($single_file, $mime, 'frontend-media');
            if (is_wp_error($uploaded) || !is_array($uploaded) || empty($uploaded['file'])) {
                continue;
            }

            $attachment = [
                'post_mime_type' => (string) ($uploaded['type'] ?? $mime),
                'post_title' => nexdirectory_sanitize_text(pathinfo($file_name, PATHINFO_FILENAME)),
                'post_content' => '',
                'post_status' => 'inherit',
                'post_author' => get_current_user_id(),
            ];
            $attachment_id = wp_insert_attachment($attachment, (string) $uploaded['file']);
            if (!$attachment_id || is_wp_error($attachment_id)) {
                continue;
            }

            // Skip frontend image sub-size generation to avoid server memory/time failures.

            $url = wp_get_attachment_url((int) $attachment_id);
            if (is_string($url) && $url !== '') {
                $urls[] = esc_url_raw($url);
            }
        }

        return array_values(array_unique($urls));
    }


    private function collect_uploaded_asset_urls_from_request($field_name, $max_files_override = 0, $max_bytes_override = 0) {
        $submit_nonce = isset($_POST['nexdirectory_nonce'])
            ? sanitize_text_field((string) wp_unslash($_POST['nexdirectory_nonce']))
            : '';
        $update_nonce = isset($_POST['nexdirectory_update_nonce'])
            ? sanitize_text_field((string) wp_unslash($_POST['nexdirectory_update_nonce']))
            : '';
        $post_id_for_nonce = isset($_POST['post_id']) ? absint((int) wp_unslash($_POST['post_id'])) : 0;
        $has_submit_nonce = $submit_nonce !== '' && wp_verify_nonce($submit_nonce, 'nexdirectory_submit_event_nonce');
        $has_update_nonce = $post_id_for_nonce > 0 && $update_nonce !== '' && wp_verify_nonce($update_nonce, 'nexdirectory_update_event');
        if (!$has_submit_nonce && !$has_update_nonce) {
            return [];
        }
        $field_name = sanitize_key((string) $field_name);
        if ($field_name === '' || empty($_FILES[$field_name]) || !is_array($_FILES[$field_name])) {
            return [];
        }

        $files = $this->sanitize_uploaded_file_array($_FILES[$field_name]);
        if (isset($files['name']) && !is_array($files['name'])) {
            $files = [
                'name' => [$files['name']],
                'tmp_name' => [isset($files['tmp_name']) ? $files['tmp_name'] : ''],
                'error' => [isset($files['error']) ? $files['error'] : UPLOAD_ERR_NO_FILE],
                'size' => [isset($files['size']) ? $files['size'] : 0],
                'type' => [isset($files['type']) ? $files['type'] : ''],
            ];
        }

        $names = isset($files['name']) && is_array($files['name']) ? $files['name'] : [];
        $tmp_names = isset($files['tmp_name']) && is_array($files['tmp_name']) ? $files['tmp_name'] : [];
        $errors = isset($files['error']) && is_array($files['error']) ? $files['error'] : [];
        $sizes = isset($files['size']) && is_array($files['size']) ? $files['size'] : [];
        $types = isset($files['type']) && is_array($files['type']) ? $files['type'] : [];
        if (empty($names)) {
            return [];
        }

        $limits = $this->get_upload_limits();
        $max_files = (int) ($max_files_override > 0 ? $max_files_override : 10);
        if ($max_files < 1) {
            $max_files = 10;
        }
        $max_bytes = (int) ($max_bytes_override > 0 ? $max_bytes_override : ($limits['file_max_bytes'] ?? 2097152));

        $allowed_mimes = get_allowed_mime_types();
        $urls = [];
        foreach ($names as $index => $name) {
            if (count($urls) >= $max_files) {
                break;
            }
            $error = isset($errors[$index]) ? (int) $errors[$index] : UPLOAD_ERR_NO_FILE;
            if ($error === UPLOAD_ERR_NO_FILE || $error !== UPLOAD_ERR_OK) {
                continue;
            }

            $tmp_name = isset($tmp_names[$index]) ? (string) $tmp_names[$index] : '';
            $file_name = isset($names[$index]) ? (string) $names[$index] : '';
            $size = isset($sizes[$index]) ? (int) $sizes[$index] : 0;
            if ($tmp_name === '' || $file_name === '' || $size <= 0 || ($max_bytes > 0 && $size > $max_bytes)) {
                continue;
            }

            $check = wp_check_filetype_and_ext($tmp_name, $file_name, $allowed_mimes);
            $mime = (string) ($check['type'] ?? '');
            if ($mime === '') {
                $fallback = wp_check_filetype($file_name, $allowed_mimes);
                $mime = (string) ($fallback['type'] ?? '');
            }
            if ($mime === '') {
                continue;
            }

            $single_file = [
                'name' => $file_name,
                'type' => isset($types[$index]) ? (string) $types[$index] : $mime,
                'tmp_name' => $tmp_name,
                'error' => $error,
                'size' => $size,
            ];
            $uploaded = $this->move_frontend_upload_to_plugin_uploads($single_file, $mime, 'frontend-files');
            if (is_wp_error($uploaded) || !is_array($uploaded) || empty($uploaded['file'])) {
                continue;
            }

            $attachment = [
                'post_mime_type' => (string) ($uploaded['type'] ?? $mime),
                'post_title' => nexdirectory_sanitize_text(pathinfo($file_name, PATHINFO_FILENAME)),
                'post_content' => '',
                'post_status' => 'inherit',
                'post_author' => get_current_user_id(),
            ];
            $attachment_id = wp_insert_attachment($attachment, (string) $uploaded['file']);
            if (!$attachment_id || is_wp_error($attachment_id)) {
                continue;
            }

            $url = wp_get_attachment_url((int) $attachment_id);
            if (is_string($url) && $url !== '') {
                $urls[] = esc_url_raw($url);
            }
        }

        return array_values(array_unique($urls));
    }


    public function handle_update_event() {
        $post_id = isset($_POST['post_id']) ? absint((int) wp_unslash($_POST['post_id'])) : 0;
        $profile_key = $this->resolve_profile_key_for_post($post_id);
        $ui_labels = $this->get_site_profile_ui_labels($profile_key);
        $profile_key = sanitize_key((string) ($ui_labels['profile_key'] ?? $profile_key));
        $is_events_profile = $profile_key === 'events';
        $is_job_profile = $profile_key === 'job_board';
        $is_directory_profile = $profile_key === 'directory';
        $is_marketplace_profile = $profile_key === 'marketplace';
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));

        if (!is_user_logged_in()) {
            wp_die(esc_html__('You must be logged in.', 'nexdirectory'));
        }
        $current_user_id = (int) get_current_user_id();
        if (!$this->user_can_submit_events($current_user_id) && !$this->user_can_edit_event($post_id, $current_user_id)) {
            /* translators: %s: Placeholder value. */
            wp_die(
                esc_html(
                    sprintf(
                        /* translators: %s: Listing singular label (lowercase). */
                        esc_html__('You do not have permission to edit %s.', 'nexdirectory'),
                        strtolower($singular_label)
                    )
                )
            );
        }

        if (!$this->user_can_edit_event($post_id, $current_user_id)) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }

        if (!isset($_POST['nexdirectory_update_nonce']) || !wp_verify_nonce(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_update_nonce'])), 'nexdirectory_update_event')) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'));
        }

        $this->hydrate_event_datetime_fields_from_request();
        $_POST['nexdirectory_timezone'] = $this->sanitize_timezone_value(wp_unslash($_POST['nexdirectory_timezone'] ?? ''));
        $_POST['nexdirectory_sessions'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_sessions'] ?? '[]'));
        $_POST['nexdirectory_key_speakers'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_key_speakers'] ?? '[]'));
        $requested_official_value = $this->normalize_official_event_value(sanitize_key((string) wp_unslash($_POST['nexdirectory_official_event'] ?? '0')));
        $_POST['nexdirectory_official_event'] = $requested_official_value;
        $_POST['nexdirectory_lock_pin'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_lock_pin'] ?? '0')) === '1' ? '1' : '0';
        $_POST['nexdirectory_price_simple'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_price_simple'] ?? 'free'));
        $_POST['nexdirectory_event_stream_url'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_event_stream_url'] ?? ''));
        $_POST['nexdirectory_meta_title'] = $this->sanitize_event_seo_title(wp_unslash($_POST['nexdirectory_meta_title'] ?? ''));
        $_POST['nexdirectory_meta_description'] = $this->sanitize_event_seo_description(wp_unslash($_POST['nexdirectory_meta_description'] ?? ''));
        if ((string) get_option('nexdirectory_enable_event_seo_fields', '1') !== '1') {
            unset($_POST['nexdirectory_meta_title'], $_POST['nexdirectory_meta_description']);
        }
        $_POST['nexdirectory_youtube_video_id_1'] = $this->normalize_submitted_youtube_id(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_youtube_video_id_1'] ?? '')));
        $_POST['nexdirectory_youtube_video_id_2'] = $this->normalize_submitted_youtube_id(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_youtube_video_id_2'] ?? '')));
        $_POST['nexdirectory_company_name'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_company_name'] ?? ''));
        $_POST['nexdirectory_directory_business_type'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_directory_business_type'] ?? 'independent-business'));
        $_POST['nexdirectory_directory_listing_status'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_directory_listing_status'] ?? 'active'));
        $_POST['nexdirectory_directory_contact_person'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_contact_person'] ?? ''));
        $_POST['nexdirectory_directory_service_area'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_service_area'] ?? ''));
        $_POST['nexdirectory_directory_operating_hours'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_operating_hours'] ?? ''));
        $_POST['nexdirectory_directory_response_time'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_response_time'] ?? ''));
        $_POST['nexdirectory_directory_established_year'] = absint((int) wp_unslash($_POST['nexdirectory_directory_established_year'] ?? 0));
        $_POST['nexdirectory_directory_pricing_summary'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_pricing_summary'] ?? ''));
        $_POST['nexdirectory_directory_appointment_url'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_directory_appointment_url'] ?? ''));
        $_POST['nexdirectory_directory_whatsapp_url'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_directory_whatsapp_url'] ?? ''));
        $_POST['nexdirectory_directory_services'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_directory_services'] ?? ''));
        $_POST['nexdirectory_directory_highlights'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_directory_highlights'] ?? ''));
        $_POST['nexdirectory_directory_certifications'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_directory_certifications'] ?? ''));
        $_POST['nexdirectory_job_type'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_type'] ?? ''));
        $_POST['nexdirectory_job_work_mode'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_job_work_mode'] ?? ''));
        $_POST['nexdirectory_job_posted_date'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_posted_date'] ?? ''));
        $_POST['nexdirectory_job_compensation_type'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_compensation_type'] ?? ''));
        $_POST['nexdirectory_job_contract_duration'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_contract_duration'] ?? ''));
        $_POST['nexdirectory_job_working_hours'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_working_hours'] ?? ''));
        $_POST['nexdirectory_job_salary'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_salary'] ?? ''));
        $_POST['nexdirectory_job_apply_url'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_job_apply_url'] ?? ''));
        $_POST['nexdirectory_job_application_email'] = sanitize_email((string) wp_unslash($_POST['nexdirectory_job_application_email'] ?? ''));
        $_POST['nexdirectory_job_closing_date'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_closing_date'] ?? ''));
        $_POST['nexdirectory_job_department'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_department'] ?? ''));
        $_POST['nexdirectory_job_experience_level'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_experience_level'] ?? ''));
        $_POST['nexdirectory_job_reporting_to'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_reporting_to'] ?? ''));
        $_POST['nexdirectory_job_remote_policy'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_remote_policy'] ?? ''));
        $_POST['nexdirectory_job_visa_sponsorship'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_job_visa_sponsorship'] ?? '0')) === '1' ? '1' : '0';
        $_POST['nexdirectory_job_open_positions'] = absint((int) wp_unslash($_POST['nexdirectory_job_open_positions'] ?? 0));
        $_POST['nexdirectory_job_interview_process'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_job_interview_process'] ?? ''));
        $_POST['nexdirectory_job_expected_start_date'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_expected_start_date'] ?? ''));
        $_POST['nexdirectory_job_skills'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_skills'] ?? ''));
        $_POST['nexdirectory_job_responsibilities'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_job_responsibilities'] ?? ''));
        $_POST['nexdirectory_job_requirements'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_job_requirements'] ?? ''));
        $_POST['nexdirectory_job_benefits'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_job_benefits'] ?? ''));
        $_POST['nexdirectory_job_equity'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_equity'] ?? ''));
        $_POST['nexdirectory_job_bonus'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_bonus'] ?? ''));
        $_POST['nexdirectory_job_recruitment_timeline'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_job_recruitment_timeline'] ?? ''));
        $_POST['nexdirectory_job_company_intro_video'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_job_company_intro_video'] ?? ''));
        $_POST['nexdirectory_job_role_overview_video'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_job_role_overview_video'] ?? ''));
        $_POST['nexdirectory_job_company_website'] = esc_url_raw((string) wp_unslash($_POST['nexdirectory_job_company_website'] ?? ''));
        $_POST['nexdirectory_job_company_size'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_company_size'] ?? ''));
        $_POST['nexdirectory_job_industry'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_industry'] ?? ''));
        $_POST['nexdirectory_market_condition'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_condition'] ?? ''));
        $_POST['nexdirectory_market_price_type'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_price_type'] ?? 'fixed'));
        $_POST['nexdirectory_market_quantity'] = absint((int) wp_unslash($_POST['nexdirectory_market_quantity'] ?? 0));
        $_POST['nexdirectory_market_sku'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_sku'] ?? ''));
        $_POST['nexdirectory_market_brand'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_brand'] ?? ''));
        $_POST['nexdirectory_market_model'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_model'] ?? ''));
        $_POST['nexdirectory_market_year'] = absint((int) wp_unslash($_POST['nexdirectory_market_year'] ?? 0));
        $_POST['nexdirectory_market_seller_type'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_seller_type'] ?? 'private'));
        $_POST['nexdirectory_market_listing_status'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_listing_status'] ?? 'active'));
        $_POST['nexdirectory_market_expires_on'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_expires_on'] ?? ''));
        $_POST['nexdirectory_market_returns_accepted'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_returns_accepted'] ?? '0')) === '1' ? '1' : '0';
        $_POST['nexdirectory_market_accept_offers'] = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_accept_offers'] ?? '1')) === '0' ? '0' : '1';
        $_POST['nexdirectory_market_payment_methods'] = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_payment_methods'] ?? ''));
        $_POST['nexdirectory_market_condition_notes'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_market_condition_notes'] ?? ''));
        $_POST['nexdirectory_market_shipping_notes'] = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_market_shipping_notes'] ?? ''));
        $update_market_delivery_raw = map_deep(wp_unslash($_POST['nexdirectory_market_delivery_methods'] ?? []), 'sanitize_text_field');
        if (!is_array($update_market_delivery_raw)) {
            $update_market_delivery_raw = preg_split('/[\s,]+/', (string) $update_market_delivery_raw);
            if (!is_array($update_market_delivery_raw)) {
                $update_market_delivery_raw = [];
            }
        }
        $allowed_market_delivery_methods = ['pickup', 'shipping', 'local-delivery', 'digital'];
        $update_market_delivery = array_values(array_unique(array_filter(array_map(static function ($method) use ($allowed_market_delivery_methods) {
            $normalized = sanitize_key((string) $method);
            return in_array($normalized, $allowed_market_delivery_methods, true) ? $normalized : '';
        }, $update_market_delivery_raw))));
        $_POST['nexdirectory_market_delivery_methods'] = implode(',', $update_market_delivery);
        $_POST['nexdirectory_listing_profile'] = $profile_key;
        $this->sync_event_price_with_ticket_request($is_events_profile);
        $job_company_website = (string) wp_unslash($_POST['nexdirectory_job_company_website'] ?? '');
        if ($is_job_profile && trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_external_url'] ?? ''))) === '' && $job_company_website !== '') {
            $_POST['nexdirectory_external_url'] = $job_company_website;
        }
        if ((string) wp_unslash($_POST['nexdirectory_timezone'] ?? '') === '') {
            $_POST['nexdirectory_timezone'] = $this->sanitize_timezone_value((string) get_post_meta($post_id, 'nexdirectory_timezone', true));
        }
        $intent = sanitize_key(wp_unslash($_POST['nexdirectory_intent'] ?? 'pending'));
        $is_draft_intent = in_array($intent, ['draft', 'preview'], true);
        $requires_complete_submission = !$is_draft_intent;

        $title = sanitize_text_field((string) wp_unslash($_POST['post_title'] ?? ''));
        if ($title === '') {
            $title = $this->recover_existing_event_title_value($post_id, (string) get_the_title($post_id));
        }
        if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_start'] ?? ''))) === '') {
            $recovered_start = $this->recover_existing_event_datetime_value($post_id, 'nexdirectory_start');
            if ($recovered_start !== '') {
                $_POST['nexdirectory_start'] = $recovered_start;
            }
        }
        if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_end'] ?? ''))) === '') {
            $recovered_end = $this->recover_existing_event_datetime_value($post_id, 'nexdirectory_end');
            if ($recovered_end !== '') {
                $_POST['nexdirectory_end'] = $recovered_end;
            }
        }
        if ($requires_complete_submission && strlen($title) < 8) {
            /* translators: %s: Placeholder value. */
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), sprintf(__('%s name should be at least 8 characters.', 'nexdirectory'), $singular_label), 'error');
        }
        $job_application_email_raw = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_application_email'] ?? '')));
        $job_application_email = sanitize_email($job_application_email_raw);
        if ($job_application_email_raw !== '' && !is_email($job_application_email)) {
            if ($requires_complete_submission) {
                $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Please enter a valid application email or leave it blank.', 'nexdirectory'), 'error');
            }
            $job_application_email = '';
        }
        $_POST['nexdirectory_job_application_email'] = $job_application_email;
        $contact_email_raw = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_contact_email'] ?? '')));
        $contact_email = sanitize_email($contact_email_raw);
        if ($contact_email_raw !== '' && !is_email($contact_email)) {
            if ($requires_complete_submission) {
                $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Please enter a valid contact email or leave it blank.', 'nexdirectory'), 'error');
            }
            $contact_email = '';
        }
        $_POST['nexdirectory_contact_email'] = $contact_email;
        if (!$is_events_profile) {
            $start_value = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_start'] ?? '')));
            $_POST['nexdirectory_recurrence'] = 'none';
            $_POST['nexdirectory_recurrence_until'] = '';
            $_POST['nexdirectory_sessions'] = '[]';
            $_POST['nexdirectory_key_speakers'] = '[]';
            if ($is_job_profile) {
                $job_posted_date = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_posted_date'] ?? '')));
                if ($job_posted_date !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $job_posted_date) && strtotime($job_posted_date . ' 00:00:00')) {
                    $_POST['nexdirectory_start'] = $job_posted_date . ' 09:00:00';
                } elseif ($start_value === '') {
                    $_POST['nexdirectory_start'] = current_time('Y-m-d H:i:s');
                }
                $posted_start_value = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_start'] ?? ''));
                if ($job_posted_date === '' && $posted_start_value !== '' && preg_match('/^\d{4}-\d{2}-\d{2}/', $posted_start_value, $posted_match)) {
                    $_POST['nexdirectory_job_posted_date'] = (string) ($posted_match[0] ?? '');
                }
                $job_closing = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_closing_date'] ?? '')));
                if ($job_closing !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $job_closing)) {
                    $_POST['nexdirectory_end'] = $job_closing . ' 23:59:00';
                } elseif (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_end'] ?? ''))) === '') {
                    $_POST['nexdirectory_end'] = '';
                }
            } elseif ($is_marketplace_profile) {
                if ($start_value === '') {
                    $_POST['nexdirectory_start'] = current_time('Y-m-d H:i:s');
                }
                $market_expires_on = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_expires_on'] ?? '')));
                if ($market_expires_on !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $market_expires_on)) {
                    $_POST['nexdirectory_end'] = $market_expires_on . ' 23:59:00';
                } else {
                    $_POST['nexdirectory_end'] = '';
                }
            } else {
                if ($start_value === '') {
                    $_POST['nexdirectory_start'] = current_time('Y-m-d H:i:s');
                }
                $_POST['nexdirectory_end'] = '';
            }
        }
        if ($requires_complete_submission) {
            $time_check = $this->validate_event_times(
                sanitize_text_field((string) wp_unslash($_POST['nexdirectory_start'] ?? '')),
                sanitize_text_field((string) wp_unslash($_POST['nexdirectory_end'] ?? '')),
                true
            );
            if (is_wp_error($time_check)) {
                $this->redirect_with_message(wp_get_referer() ?: home_url('/'), $time_check->get_error_message(), 'error');
            }
            if ($is_job_profile) {
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_company_name'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Company name is required for job listings.', 'nexdirectory'), 'error');
                }
                $job_posted = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_posted_date'] ?? '')));
                if ($job_posted !== '' && (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $job_posted) || !strtotime($job_posted . ' 00:00:00'))) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Posted date is invalid.', 'nexdirectory'), 'error');
                }
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_type'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Employment type is required for job listings.', 'nexdirectory'), 'error');
                }
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_department'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Department is required for job listings.', 'nexdirectory'), 'error');
                }
                if (!in_array((string) wp_unslash($_POST['nexdirectory_job_work_mode'] ?? ''), ['on-site', 'hybrid', 'remote'], true)) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Work setup is required for job listings.', 'nexdirectory'), 'error');
                }
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_compensation_type'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Compensation type is required for job listings.', 'nexdirectory'), 'error');
                }
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_salary'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Salary range is required for job listings.', 'nexdirectory'), 'error');
                }
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_apply_url'] ?? ''))) === '' && trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_application_email'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Provide at least one application method (URL or email).', 'nexdirectory'), 'error');
                }
                $job_closing = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_closing_date'] ?? '')));
                if ($job_closing === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Closing date is required for job listings.', 'nexdirectory'), 'error');
                }
                if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $job_closing) || !strtotime($job_closing . ' 00:00:00')) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Closing date is invalid.', 'nexdirectory'), 'error');
                }
            } elseif ($is_directory_profile) {
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_company_name'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Business name is required for directory listings.', 'nexdirectory'), 'error');
                }
                if (!in_array((string) wp_unslash($_POST['nexdirectory_directory_business_type'] ?? ''), ['independent-business', 'agency', 'private-owner', 'nonprofit', 'multi-location', 'franchise'], true)) {
                    $_POST['nexdirectory_directory_business_type'] = 'independent-business';
                }
                if (!in_array((string) wp_unslash($_POST['nexdirectory_directory_listing_status'] ?? ''), ['active', 'temporarily-closed', 'permanently-closed'], true)) {
                    $_POST['nexdirectory_directory_listing_status'] = 'active';
                }
                $has_contact_method = (
                    trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_contact_phone'] ?? ''))) !== ''
                    || trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_contact_email'] ?? ''))) !== ''
                    || trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_external_url'] ?? ''))) !== ''
                    || trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_appointment_url'] ?? ''))) !== ''
                    || trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_directory_whatsapp_url'] ?? ''))) !== ''
                );
                if (!$has_contact_method) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Add at least one owner contact method (email, phone, website, booking URL, or WhatsApp).', 'nexdirectory'), 'error');
                }
            } elseif ($is_marketplace_profile) {
                if (trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_condition'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Condition is required for marketplace listings.', 'nexdirectory'), 'error');
                }
                $market_price_type = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_price_type'] ?? 'fixed'));
                if (!in_array($market_price_type, ['fixed', 'negotiable', 'auction', 'free'], true)) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Price type is invalid for marketplace listings.', 'nexdirectory'), 'error');
                }
                if ($market_price_type !== 'free' && trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_price'] ?? ''))) === '') {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Asking price is required unless this listing is marked as free.', 'nexdirectory'), 'error');
                }
                if ($market_price_type === 'free' && trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_price'] ?? ''))) === '') {
                    $_POST['nexdirectory_price'] = __('Free', 'nexdirectory');
                }
                $market_listing_status = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_listing_status'] ?? 'active'));
                if (!in_array($market_listing_status, ['active', 'reserved', 'sold', 'expired'], true)) {
                    $_POST['nexdirectory_market_listing_status'] = 'active';
                }
                $market_seller_type = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_seller_type'] ?? 'private'));
                if (!in_array($market_seller_type, ['private', 'business'], true)) {
                    $_POST['nexdirectory_market_seller_type'] = 'private';
                }
                $market_expires_on = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_expires_on'] ?? '')));
                if ($market_expires_on !== '' && (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $market_expires_on) || !strtotime($market_expires_on . ' 00:00:00'))) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Expiry date is invalid for marketplace listings.', 'nexdirectory'), 'error');
                }
                $market_delivery_selected = array_filter(array_map('sanitize_key', preg_split('/[\s,]+/', sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_delivery_methods'] ?? ''))) ?: []));
                if (empty($market_delivery_selected)) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Select at least one delivery method for marketplace listings.', 'nexdirectory'), 'error');
                }
                $has_contact_method = (
                    trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_contact_phone'] ?? ''))) !== ''
                    || trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_contact_email'] ?? ''))) !== ''
                    || trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_external_url'] ?? ''))) !== ''
                );
                if (!$has_contact_method) {
                    $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Add at least one seller contact method (email, phone, or external listing URL).', 'nexdirectory'), 'error');
                }
            }
        }
        $this->maybe_fill_request_coordinates_from_location_fields();
        $parsed_tags = $this->parse_event_tags_input(sanitize_text_field((string) wp_unslash($_POST['event_tags'] ?? '')), 8, 40);
        if (is_wp_error($parsed_tags)) {
            if ($requires_complete_submission) {
                $this->redirect_with_message(wp_get_referer() ?: home_url('/'), $parsed_tags->get_error_message(), 'error');
            }
            $parsed_tags = [];
        }
        $excerpt = sanitize_textarea_field((string) wp_unslash($_POST['post_excerpt'] ?? ''));
        if (!array_key_exists('post_excerpt', $_POST)) {
            $excerpt = (string) get_post_field('post_excerpt', $post_id);
        }
        $post_content = wp_kses_post(wp_unslash($_POST['post_content'] ?? ''));
        if (!array_key_exists('post_content', $_POST)) {
            $post_content = (string) get_post_field('post_content', $post_id);
        }
        $excerpt_max_chars = max(80, min(1000, (int) get_option('nexdirectory_excerpt_max_chars', 220)));
        $excerpt_length = function_exists('mb_strlen') ? mb_strlen($excerpt) : strlen($excerpt);
        if ($excerpt_length > $excerpt_max_chars) {
            $this->redirect_with_message(
                wp_get_referer() ?: home_url('/'),
                /* translators: %d: Placeholder value. */
                sprintf(__('Short excerpt must be %d characters or fewer.', 'nexdirectory'), $excerpt_max_chars),
                'error'
            );
        }
        $existing_status = (string) get_post_status($post_id);
        $edit_policy = sanitize_key((string) get_option('nexdirectory_user_edit_policy', 'require_approval'));
        if (!in_array($edit_policy, ['allow', 'require_approval', 'disallow'], true)) {
            $edit_policy = 'require_approval';
        }
        $auto_approve_after_first = (string) get_option('nexdirectory_auto_approve_edits_after_first_approval', '0') === '1';
        if (
            $edit_policy === 'require_approval'
            && $auto_approve_after_first
            && $existing_status === 'publish'
            && $this->user_has_published_listings(get_current_user_id())
        ) {
            $edit_policy = 'allow';
        }
        if ($this->user_can_bypass_edit_approval(get_current_user_id())) {
            $edit_policy = 'allow';
        }
        if ($edit_policy === 'disallow' && $existing_status === 'publish') {
            $this->redirect_with_message(
                wp_get_referer() ?: home_url('/'),
                /* translators: %s: Placeholder value. */
                sprintf(__('You cannot edit published %s. Contact an admin for updates.', 'nexdirectory'), strtolower($singular_label)),
                'error'
            );
        }
        $target_status = 'pending';
        if ($is_draft_intent) {
            $target_status = 'draft';
        } elseif ($edit_policy === 'allow') {
            $target_status = in_array($existing_status, ['publish', 'pending', 'draft'], true) ? $existing_status : 'pending';
        } elseif ($edit_policy === 'disallow') {
            $target_status = in_array($existing_status, ['pending', 'draft'], true) ? $existing_status : 'pending';
        }

        wp_update_post([
            'ID' => $post_id,
            'post_title' => $title,
            'post_content' => $post_content,
            'post_excerpt' => $excerpt,
            'post_status' => $target_status,
        ]);

        $upload_limits = $this->get_upload_limits();
        $cover_uploaded_urls = $this->collect_uploaded_gallery_image_urls_from_request(
            'nexdirectory_cover_file',
            1,
            (int) ($upload_limits['cover_max_bytes'] ?? 512000)
        );
        $cover_input_url = esc_url_raw((string) wp_unslash($_POST['nexdirectory_cover_image_url'] ?? ''));
        if ($cover_input_url === '' && !empty($cover_uploaded_urls[0])) {
            $cover_input_url = esc_url_raw((string) $cover_uploaded_urls[0]);
        }
        if ($cover_input_url !== '' && !$this->can_current_user_use_media_url($cover_input_url, get_current_user_id())) {
            $cover_input_url = '';
        }

        $gallery_urls_input = $this->parse_multiline_urls(sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_gallery_urls'] ?? '')));
        $gallery_urls_uploaded = $this->collect_uploaded_gallery_image_urls_from_request('nexdirectory_gallery_files');
        $gallery_urls_merged = array_values(array_unique(array_merge($gallery_urls_input, $gallery_urls_uploaded)));
        $gallery_urls_merged = $this->filter_media_urls_for_user($gallery_urls_merged, get_current_user_id());
        $normalized_media = $this->normalize_cover_and_gallery_meta_urls($cover_input_url, implode("\n", $gallery_urls_merged));
        $_POST['nexdirectory_cover_image_url'] = (string) ($normalized_media['cover'] ?? '');
        $_POST['nexdirectory_gallery_urls'] = implode("\n", (array) ($normalized_media['gallery'] ?? []));

        foreach ($this->get_event_meta_fields() as $meta_key => $meta_type) {
            if (!array_key_exists($meta_key, $_POST)) {
                continue;
            }
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Update nonce is verified before this meta loop.
            $value = map_deep(wp_unslash($_POST[$meta_key]), 'sanitize_textarea_field');
            if ($this->should_skip_sparse_meta_update($post_id, $meta_key, $value, true)) {
                continue;
            }
            update_post_meta($post_id, $meta_key, $this->sanitize_meta_value($meta_key, $value));
        }
        $this->persist_listing_profile_for_post($post_id, $profile_key);
        $resolved_official_value = $this->apply_official_flag_with_admin_gate($post_id, $requested_official_value, get_current_user_id());
        $_POST['nexdirectory_official_event'] = $resolved_official_value;

        $video_urls_raw = $this->normalize_video_urls_textarea(sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_video_urls'] ?? '')));
        $gallery_urls_raw = sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_gallery_urls'] ?? ''));

        $duplicate_candidate_id = 0;
        if ((string) get_option('nexdirectory_enable_duplicate_detection', '1') === '1') {
            $duplicate_candidate_id = $this->find_duplicate_event_candidate(
                $title,
                sanitize_text_field((string) wp_unslash($_POST['nexdirectory_start'] ?? '')),
                sanitize_text_field((string) wp_unslash($_POST['nexdirectory_city'] ?? '')),
                sanitize_text_field((string) wp_unslash($_POST['nexdirectory_address'] ?? '')),
                sanitize_text_field((string) wp_unslash($_POST['nexdirectory_venue'] ?? '')),
                $post_id
            );
        }
        if ($duplicate_candidate_id > 0) {
            update_post_meta($post_id, 'nexdirectory_duplicate_candidate', (string) $duplicate_candidate_id);
        } else {
            delete_post_meta($post_id, 'nexdirectory_duplicate_candidate');
        }

        $this->update_media_meta(
            $post_id,
            $video_urls_raw,
            $gallery_urls_raw
        );
        if ($this->is_custom_submission_builder_available('update', get_current_user_id()) && isset($_POST['nexdirectory_custom_fields_builder_json'])) {
            $custom_payload = $this->sanitize_custom_builder_json_payload(wp_unslash($_POST['nexdirectory_custom_fields_builder_json'] ?? '[]'));
            $custom_parsed = $this->parse_custom_field_builder_payload($custom_payload);
            $custom_form_fields = is_array($custom_parsed['fields'] ?? null) ? (array) $custom_parsed['fields'] : [];
            $custom_field_values = is_array($custom_parsed['values'] ?? null) ? (array) $custom_parsed['values'] : [];
            do_action(
                'nexdirectory_persist_submission_custom_builder',
                $post_id,
                [
                    'fields' => $custom_form_fields,
                    'values' => $custom_field_values,
                ],
                ['source' => 'update'],
                $this
            );
        }
        update_post_meta($post_id, 'nexdirectory_sessions', wp_json_encode($this->parse_sessions(sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_sessions'] ?? '[]')))));
        update_post_meta($post_id, 'nexdirectory_key_speakers', wp_json_encode($this->parse_key_speakers(sanitize_textarea_field((string) wp_unslash($_POST['nexdirectory_key_speakers'] ?? '[]')))));
        $event_type_id = isset($_POST['event_type']) ? absint((int) wp_unslash($_POST['event_type'])) : 0;
        if ($event_type_id > 0) {
            wp_set_post_terms($post_id, [$event_type_id], self::TAX_TYPE, false);
        }
        if ($is_job_profile) {
            $job_type_terms = [];
            $job_department_label = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_department'] ?? '')));
            $job_type_label = trim(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_job_type'] ?? '')));
            foreach ([$job_department_label, $job_type_label] as $candidate_term) {
                if ($candidate_term === '') {
                    continue;
                }
                $candidate_term_id = $this->get_or_create_term_id(self::TAX_TYPE, $candidate_term);
                if ($candidate_term_id > 0) {
                    $job_type_terms[] = (int) $candidate_term_id;
                }
            }
            if (!empty($job_type_terms)) {
                wp_set_post_terms($post_id, array_values(array_unique($job_type_terms)), self::TAX_TYPE, false);
            }
            $work_mode = sanitize_key((string) wp_unslash($_POST['nexdirectory_job_work_mode'] ?? ''));
            $work_mode_label = '';
            if ($work_mode === 'remote') {
                $work_mode_label = __('Remote', 'nexdirectory');
            } elseif ($work_mode === 'hybrid') {
                $work_mode_label = __('Hybrid', 'nexdirectory');
            } elseif ($work_mode === 'on-site') {
                $work_mode_label = __('On-site', 'nexdirectory');
            }
            if ($work_mode_label !== '') {
                $work_mode_term_id = $this->get_or_create_term_id(self::TAX_MODE, $work_mode_label);
                if ($work_mode_term_id > 0) {
                    wp_set_post_terms($post_id, [$work_mode_term_id], self::TAX_MODE, false);
                }
            }
            wp_set_post_terms($post_id, [], self::TAX_PRICE, false);
        } elseif ($is_marketplace_profile) {
            $delivery_methods = array_filter(array_map('sanitize_key', preg_split('/[\s,]+/', sanitize_text_field((string) wp_unslash($_POST['nexdirectory_market_delivery_methods'] ?? ''))) ?: []));
            $delivery_methods = array_values(array_unique(array_filter($delivery_methods, static function ($method) {
                return in_array($method, ['pickup', 'shipping', 'local-delivery', 'digital'], true);
            })));
            $delivery_labels = [
                'pickup' => __('Pickup', 'nexdirectory'),
                'shipping' => __('Shipping', 'nexdirectory'),
                'local-delivery' => __('Local Delivery', 'nexdirectory'),
                'digital' => __('Digital Delivery', 'nexdirectory'),
            ];
            $mode_term_ids = [];
            foreach ($delivery_methods as $delivery_method) {
                $term_name = (string) ($delivery_labels[$delivery_method] ?? '');
                if ($term_name === '') {
                    continue;
                }
                $term_id = $this->get_or_create_term_id(self::TAX_MODE, $term_name);
                if ($term_id > 0) {
                    $mode_term_ids[] = (int) $term_id;
                }
            }
            if (!empty($mode_term_ids)) {
                wp_set_post_terms($post_id, array_values(array_unique($mode_term_ids)), self::TAX_MODE, false);
            } else {
                wp_set_post_terms($post_id, [], self::TAX_MODE, false);
            }

            $market_price_type = sanitize_key((string) wp_unslash($_POST['nexdirectory_market_price_type'] ?? 'fixed'));
            $price_term_map = [
                'free' => __('Free', 'nexdirectory'),
                'fixed' => __('Fixed Price', 'nexdirectory'),
                'negotiable' => __('Negotiable', 'nexdirectory'),
                'auction' => __('Auction', 'nexdirectory'),
            ];
            $price_term_name = (string) ($price_term_map[$market_price_type] ?? $price_term_map['fixed']);
            $price_term_id = $this->get_or_create_term_id(self::TAX_PRICE, $price_term_name);
            if ($price_term_id > 0) {
                wp_set_post_terms($post_id, [$price_term_id], self::TAX_PRICE, false);
            } else {
                wp_set_post_terms($post_id, [], self::TAX_PRICE, false);
            }
        } elseif ($is_directory_profile) {
            $directory_status = sanitize_key((string) wp_unslash($_POST['nexdirectory_directory_listing_status'] ?? 'active'));
            $status_term_map = [
                'active' => __('Active', 'nexdirectory'),
                'temporarily-closed' => __('Temporarily Closed', 'nexdirectory'),
                'permanently-closed' => __('Permanently Closed', 'nexdirectory'),
            ];
            $status_term_name = (string) ($status_term_map[$directory_status] ?? $status_term_map['active']);
            $status_term_id = $this->get_or_create_term_id(self::TAX_MODE, $status_term_name);
            if ($status_term_id > 0) {
                wp_set_post_terms($post_id, [$status_term_id], self::TAX_MODE, false);
            } else {
                wp_set_post_terms($post_id, [], self::TAX_MODE, false);
            }

            wp_set_post_terms($post_id, [], self::TAX_PRICE, false);

            $directory_type = sanitize_key((string) wp_unslash($_POST['nexdirectory_directory_business_type'] ?? 'independent-business'));
            $directory_type_term_map = [
                'independent-business' => __('Independent Business', 'nexdirectory'),
                'agency' => __('Agency', 'nexdirectory'),
                'private-owner' => __('Private Owner', 'nexdirectory'),
                'nonprofit' => __('Nonprofit / Community', 'nexdirectory'),
                'multi-location' => __('Multi-location Brand', 'nexdirectory'),
                'franchise' => __('Franchise', 'nexdirectory'),
            ];
            $directory_type_term_name = (string) ($directory_type_term_map[$directory_type] ?? '');
            if ($directory_type_term_name !== '') {
                $existing_type_ids = wp_get_post_terms($post_id, self::TAX_TYPE, ['fields' => 'ids']);
                if (is_wp_error($existing_type_ids) || !is_array($existing_type_ids)) {
                    $existing_type_ids = [];
                }
                $directory_type_term_id = $this->get_or_create_term_id(self::TAX_TYPE, $directory_type_term_name);
                if ($directory_type_term_id > 0) {
                    $existing_type_ids[] = (int) $directory_type_term_id;
                    wp_set_post_terms($post_id, array_values(array_unique(array_map('absint', $existing_type_ids))), self::TAX_TYPE, false);
                }
            }
        } else {
            $mode_option = sanitize_key((string) wp_unslash($_POST['nexdirectory_event_mode_option'] ?? 'in-person'));
            $price_option = sanitize_key((string) wp_unslash($_POST['nexdirectory_price_simple'] ?? 'free'));

            if ($is_events_profile) {
                $mode_name = $mode_option === 'video-stream' ? 'Online Stream Event' : 'In-person';
                $price_name = $price_option === 'paid' ? 'Paid' : 'Free';
            } elseif ($is_directory_profile) {
                $mode_name = $mode_option === 'video-stream' ? __('Online Service', 'nexdirectory') : __('On-site Service', 'nexdirectory');
                $price_name = $price_option === 'paid' ? __('Paid Service', 'nexdirectory') : __('Free Service', 'nexdirectory');
            } else {
                $mode_name = $mode_option === 'video-stream' ? __('Online', 'nexdirectory') : __('Standard', 'nexdirectory');
                $price_name = $price_option === 'paid' ? __('Paid', 'nexdirectory') : __('Free', 'nexdirectory');
            }

            $mode_term_id = $this->get_or_create_term_id(self::TAX_MODE, $mode_name);
            if ($mode_term_id > 0) {
                wp_set_post_terms($post_id, [$mode_term_id], self::TAX_MODE, false);
            }

            $price_term_id = $this->get_or_create_term_id(self::TAX_PRICE, $price_name);
            if ($price_term_id > 0) {
                wp_set_post_terms($post_id, [$price_term_id], self::TAX_PRICE, false);
            }
        }
        wp_set_post_terms($post_id, (array) $parsed_tags, self::TAX_TAG, false);
        $this->save_custom_taxonomy_terms_from_request($post_id);
        $missing_optional_sections = $this->get_event_optional_sections_missing($post_id, $profile_key);
        if (!empty($missing_optional_sections)) {
            update_post_meta($post_id, 'nexdirectory_missing_optional_sections', wp_json_encode($missing_optional_sections));
        } else {
            delete_post_meta($post_id, 'nexdirectory_missing_optional_sections');
        }
        $completion_hint_text = $this->get_event_optional_sections_hint_text($missing_optional_sections);

        if ($target_status === 'pending') {
            $this->notify_author_status($post_id, 'pending', '');
        } elseif ($target_status === 'publish') {
            $this->notify_author_status($post_id, 'publish', '');
        }
        if ($target_status === 'pending') {
            /* translators: %s: Placeholder value. */
            $message = sprintf(__('%s updated and sent for re-verification.', 'nexdirectory'), $singular_label);
        } elseif ($target_status === 'draft') {
            /* translators: %s: Placeholder value. */
            $message = sprintf(__('%s changes saved as draft.', 'nexdirectory'), $singular_label);
        } else {
            /* translators: %s: Placeholder value. */
            $message = sprintf(__('%s updated successfully.', 'nexdirectory'), $singular_label);
        }
        if ($duplicate_candidate_id > 0) {
            $message .= ' ' . __('Potential duplicate detected (same title/date/location).', 'nexdirectory');
        }
        if ($completion_hint_text !== '') {
            $message .= ' ' . $completion_hint_text;
        }

        $this->redirect_with_message(
            wp_get_referer() ?: home_url('/'),
            $message
        );
    }


    public function handle_delete_event() {
        $request_nonce = isset($_POST['nexdirectory_delete_request_nonce'])
            ? sanitize_text_field((string) wp_unslash($_POST['nexdirectory_delete_request_nonce']))
            : '';
        if ($request_nonce === '' || !wp_verify_nonce($request_nonce, 'nexdirectory_delete_event_request')) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'), '', ['response' => 400]);
        }

        $is_ajax_request = sanitize_key((string) wp_unslash($_POST['nexdirectory_ajax'] ?? '')) === '1';
        $fallback_redirect = wp_get_referer() ?: home_url('/');
        $redirect_to = $this->get_safe_redirect_url(esc_url_raw((string) wp_unslash($_POST['redirect_to'] ?? '')), $fallback_redirect);
        $respond_error = function ($message, $status_code = 400) use ($is_ajax_request, $redirect_to) {
            if ($is_ajax_request) {
                wp_send_json_error(['message' => (string) $message], (int) $status_code);
            }
            $this->redirect_with_message($redirect_to, (string) $message, 'error');
        };
        $respond_success = function ($message, array $payload = []) use ($is_ajax_request, $redirect_to) {
            if ($is_ajax_request) {
                wp_send_json_success(array_merge(['message' => (string) $message], $payload));
            }
            $this->redirect_with_message($redirect_to, (string) $message);
        };

        if (!is_user_logged_in()) {
            $respond_error(__('You must be logged in.', 'nexdirectory'), 403);
        }
        if (!$this->user_can_submit_events(get_current_user_id())) {
            $respond_error(__('You do not have permission to delete events.', 'nexdirectory'), 403);
        }

        $post_id = isset($_POST['post_id']) ? absint((int) wp_unslash($_POST['post_id'])) : 0;
        $is_owner = $post_id > 0 && (int) get_post_field('post_author', $post_id) === (int) get_current_user_id();
        if (!$post_id || get_post_type($post_id) !== self::POST_TYPE || (!$is_owner && !$this->user_can_moderate_events())) {
            $respond_error(__('Not allowed.', 'nexdirectory'), 403);
        }

        wp_trash_post($post_id);

        $respond_success(
            __('Event moved to trash.', 'nexdirectory'),
            [
                'post_id' => (int) $post_id,
            ]
        );
    }


    private function resolve_live_toggle_target_status($user_id = 0, $existing_status = 'draft') {
        $user_id = absint((int) $user_id);
        $existing_status = sanitize_key((string) $existing_status);
        if ($this->user_can_moderate_events($user_id)) {
            return 'publish';
        }

        $edit_policy = sanitize_key((string) get_option('nexdirectory_user_edit_policy', 'require_approval'));
        if (!in_array($edit_policy, ['allow', 'require_approval', 'disallow'], true)) {
            $edit_policy = 'require_approval';
        }

        $auto_approve_after_first = (string) get_option('nexdirectory_auto_approve_edits_after_first_approval', '0') === '1';
        if (
            $edit_policy === 'require_approval'
            && $auto_approve_after_first
            && $existing_status === 'publish'
            && $this->user_has_published_listings($user_id)
        ) {
            $edit_policy = 'allow';
        }
        if ($this->user_can_bypass_edit_approval($user_id)) {
            $edit_policy = 'allow';
        }

        return $edit_policy === 'allow' ? 'publish' : 'pending';
    }


    public function handle_toggle_event_live_draft() {
        check_admin_referer('nexdirectory_toggle_event_live_draft', 'nexdirectory_toggle_event_live_draft_nonce');
        $post_id = isset($_POST['post_id']) ? absint((int) wp_unslash($_POST['post_id'])) : 0;
        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'), '', ['response' => 400]);
        }

        $fallback_redirect = wp_get_referer() ?: home_url('/');
        $redirect_to = $this->get_safe_redirect_url(esc_url_raw((string) wp_unslash($_POST['redirect_to'] ?? '')), $fallback_redirect);
        $is_ajax_request = sanitize_key((string) wp_unslash($_POST['nexdirectory_ajax'] ?? '')) === '1';
        $respond_error = function ($message, $status_code = 400) use ($is_ajax_request, $redirect_to) {
            if ($is_ajax_request) {
                wp_send_json_error(['message' => (string) $message], (int) $status_code);
            }
            $this->redirect_with_message($redirect_to, (string) $message, 'error');
        };
        $respond_success = function ($message, array $payload = []) use ($is_ajax_request, $redirect_to) {
            if ($is_ajax_request) {
                wp_send_json_success(array_merge(['message' => (string) $message], $payload));
            }
            $this->redirect_with_message($redirect_to, (string) $message);
        };

        if (!is_user_logged_in()) {
            $respond_error(__('You must be logged in.', 'nexdirectory'), 403);
        }

        $profile_key = $this->resolve_profile_key_for_post($post_id);
        $ui_labels = $this->get_site_profile_ui_labels($profile_key);
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));

        $current_user_id = (int) get_current_user_id();
        $is_owner_or_team_owner = $this->can_manage_event_team($post_id, $current_user_id);
        if (!$is_owner_or_team_owner && !$this->user_can_moderate_events($current_user_id)) {
            $respond_error(__('You do not have permission to change listing status.', 'nexdirectory'), 403);
        }

        $desired_state_raw = wp_unslash($_POST['desired_state'] ?? '');
        if (is_array($desired_state_raw)) {
            $desired_state_raw = end($desired_state_raw);
        }
        $desired_state = sanitize_key((string) $desired_state_raw);
        if (!in_array($desired_state, ['live', 'draft'], true)) {
            $respond_error(__('Invalid status toggle request.', 'nexdirectory'), 400);
        }

        $existing_status = sanitize_key((string) get_post_status($post_id));
        if (!in_array($existing_status, ['publish', 'pending', 'draft'], true)) {
            $existing_status = 'draft';
        }

        if ($desired_state === 'draft') {
            if ($existing_status !== 'draft') {
                wp_update_post([
                    'ID' => $post_id,
                    'post_status' => 'draft',
                ]);
                update_post_meta($post_id, 'nexdirectory_moderation_action', 'owner_unpublished');
                $this->notify_author_status($post_id, 'draft', '');
            }
            $respond_success(
                /* translators: %s: Placeholder value. */
                sprintf(__('%s moved to draft.', 'nexdirectory'), $singular_label),
                [
                    'post_id' => (int) $post_id,
                    'status' => 'draft',
                    'status_label' => $this->event_status_badge('draft'),
                    'state_live' => false,
                    'pending_moderation' => false,
                ]
            );
        }

        $target_status = $this->resolve_live_toggle_target_status($current_user_id, $existing_status);
        if ($target_status === 'publish') {
            if ($existing_status !== 'publish') {
                wp_update_post([
                    'ID' => $post_id,
                    'post_status' => 'publish',
                ]);
                update_post_meta($post_id, 'nexdirectory_moderation_action', 'approve');
                $this->notify_author_status($post_id, 'publish', '');
            }
            $respond_success(
                /* translators: %s: Placeholder value. */
                sprintf(__('%s is now live.', 'nexdirectory'), $singular_label),
                [
                    'post_id' => (int) $post_id,
                    'status' => 'publish',
                    'status_label' => $this->event_status_badge('publish'),
                    'state_live' => true,
                    'pending_moderation' => false,
                ]
            );
        }

        wp_update_post([
            'ID' => $post_id,
            'post_status' => 'pending',
        ]);
        update_post_meta($post_id, 'nexdirectory_moderation_action', 'owner_live_request');
        update_post_meta($post_id, 'nexdirectory_moderation_note', '');
        $this->notify_admin_submission($post_id);
        $this->notify_author_status($post_id, 'pending', '');
        $respond_success(
            sprintf(
                /* translators: %1$s: Placeholder value. */
                __('%1$s submitted for moderation. It will go live once approved.', 'nexdirectory'),
                $singular_label
            ),
            [
                'post_id' => (int) $post_id,
                'status' => 'pending',
                'status_label' => $this->event_status_badge('pending'),
                'state_live' => false,
                'pending_moderation' => true,
            ]
        );
    }


    public function handle_save_filters() {
        if (!is_user_logged_in()) {
            wp_die(esc_html__('You must be logged in.', 'nexdirectory'));
        }
        if (!$this->user_can_access_platform(get_current_user_id())) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        if (!isset($_POST['nexdirectory_save_filters_nonce']) || !wp_verify_nonce(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_save_filters_nonce'])), 'nexdirectory_save_filters_nonce')) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'));
        }
        $filters = [
            's' => sanitize_text_field((string) wp_unslash($_POST['s'] ?? '')),
            'event_type' => sanitize_text_field((string) wp_unslash($_POST['event_type'] ?? '')),
            'event_mode' => sanitize_text_field((string) wp_unslash($_POST['event_mode'] ?? '')),
            'event_price' => sanitize_text_field((string) wp_unslash($_POST['event_price'] ?? '')),
            'event_tag' => sanitize_text_field((string) wp_unslash($_POST['event_tag'] ?? '')),
            'city' => sanitize_text_field((string) wp_unslash($_POST['city'] ?? '')),
            'date_from' => sanitize_text_field((string) wp_unslash($_POST['date_from'] ?? '')),
            'official' => sanitize_text_field((string) wp_unslash($_POST['official'] ?? '')),
        ];
        if (!in_array((string) ($filters['official'] ?? ''), ['0', '1'], true)) {
            $filters['official'] = '';
        }
        update_user_meta(get_current_user_id(), 'nexdirectory_saved_filters', $filters);
        $redirect = !empty($_POST['redirect_to']) ? esc_url_raw(wp_unslash($_POST['redirect_to'])) : home_url('/');
        $home_host = wp_parse_url(home_url('/'), PHP_URL_HOST);
        $redirect_host = wp_parse_url($redirect, PHP_URL_HOST);
        if (!$redirect || ($redirect_host && $home_host && !hash_equals((string) $home_host, (string) $redirect_host))) {
            $redirect = home_url('/');
        }
        $this->redirect_with_message($redirect, __('Filters saved to your account.', 'nexdirectory'));
    }

    public function render_account($atts = []) {
        $current_user_id = is_user_logged_in() ? (int) get_current_user_id() : 0;
        $initial_tab = $this->user_is_account_app_non_owner_in_single_org_mode($current_user_id) ? 'saved' : 'account';
        return $this->render_account_app_template([
            'initial_tab' => $initial_tab,
            'login_message' => __('Please log in to manage your account.', 'nexdirectory'),
        ]);
    }


    public function handle_account_update() {
        if (!is_user_logged_in()) {
            wp_die(esc_html__('You must be logged in.', 'nexdirectory'));
        }
        if (!$this->user_can_access_platform(get_current_user_id())) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }

        check_admin_referer('nexdirectory_account_update_nonce', 'nexdirectory_account_nonce');

        $userdata = [
            'ID' => get_current_user_id(),
            'display_name' => sanitize_text_field((string) wp_unslash($_POST['display_name'] ?? '')),
            'user_email' => sanitize_email(wp_unslash($_POST['user_email'] ?? '')),
        ];
        $gdpr_marketing_opt_in = (string) wp_unslash($_POST['nexdirectory_gdpr_marketing_opt_in'] ?? '0') === '1';
        $event_messages_enabled = (string) wp_unslash($_POST['nexdirectory_event_messages_enabled'] ?? '1') === '1';

        $new_pass = $this->sanitize_password_value(wp_unslash($_POST['new_pass'] ?? ''));
        if (!empty($new_pass)) {
            $userdata['user_pass'] = $new_pass;
        }

        $result = wp_update_user($userdata);

        if (is_wp_error($result)) {
            $this->redirect_with_message(
                wp_get_referer() ?: home_url('/'),
                $result->get_error_message(),
                'error'
            );
        }
        $this->update_user_organizer_communication_consent(get_current_user_id(), $gdpr_marketing_opt_in);
        $this->update_user_event_message_preference(get_current_user_id(), $event_messages_enabled);
        $this->save_profile_privacy_settings(get_current_user_id(), $_POST);

        $this->redirect_with_message(
            wp_get_referer() ?: home_url('/'),
            __('Account updated successfully.', 'nexdirectory')
        );
    }


    public function render_user_management($atts = []) {
        $atts = shortcode_atts([
            'limit' => 100,
            'admin_context' => 0,
        ], (array) $atts, 'nexdirectory_user_management');
        $limit = max(1, (int) $atts['limit']);
        $admin_context = (string) $atts['admin_context'] === '1';

        if (!$this->user_can_manage_organizers()) {
            return $this->redirect_to_login_screen(__('Unauthorized access. Admin login required.', 'nexdirectory'));
        }

        $search = isset($_GET['user_search']) ? sanitize_text_field((string) wp_unslash($_GET['user_search'])) : '';
        $users = get_users([
            'search' => $search ? '*' . $search . '*' : '',
            'search_columns' => ['user_login', 'user_email', 'display_name'],
            'number' => $limit,
            'orderby' => 'registered',
            'order' => 'DESC',
        ]);
        $ui_labels = $this->get_site_profile_ui_labels();

        return $this->render_frontend_template('user-management.php', [
            'search' => $search,
            'users' => $users,
            'limit' => $limit,
            'ui_labels' => $ui_labels,
            'admin_context' => $admin_context,
        ]);
    }


    private function get_user_management_events($user_id, $limit = 12) {
        $user_id = absint($user_id);
        $limit = max(1, min(50, (int) $limit));
        if ($user_id <= 0) {
            return [];
        }
        return get_posts([
            'post_type' => self::POST_TYPE,
            'post_status' => ['publish', 'pending', 'draft'],
            'author' => $user_id,
            'posts_per_page' => $limit,
            'orderby' => 'date',
            'order' => 'DESC',
        ]);
    }


    private function render_user_management_rows($users, $ui_labels = []) {
        $users = is_array($users) ? $users : [];
        $ui_labels = is_array($ui_labels) ? $ui_labels : [];
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $plural_label = sanitize_text_field((string) ($ui_labels['plural'] ?? __('Events', 'nexdirectory')));
        $owner_singular = sanitize_text_field((string) ($ui_labels['owner_singular'] ?? __('Organizer', 'nexdirectory')));
        $singular_lower = sanitize_text_field((string) ($ui_labels['singular_lower'] ?? strtolower((string) $singular_label)));
        ob_start();
        if (empty($users)) {
            ?>
            <tr>
                <td colspan="5" class="nexdirectory-admin-ops-empty-state" data-label="<?php esc_attr_e('Users', 'nexdirectory'); ?>">
                    <?php esc_html_e('No users found for this query.', 'nexdirectory'); ?>
                </td>
            </tr>
            <?php
            return (string) ob_get_clean();
        }

        foreach ($users as $user) :
            if (!$user instanceof WP_User) {
                continue;
            }
            $events = $this->get_user_management_events($user->ID, 14);
            $role = !empty($user->roles) ? (string) $user->roles[0] : 'subscriber';
            $verified = get_user_meta($user->ID, 'nexdirectory_verified_organizer', true) === '1';
            $trusted_editor = $this->user_is_trusted_editor($user->ID);
            $is_admin = in_array('administrator', (array) $user->roles, true);
            $events_page_url = $this->get_platform_page_url(
                'nexdirectory_page_events_url',
                $this->get_profile_route_slugs('events'),
                $this->get_profile_route_default_url('events')
            );
            if ($events_page_url === '') {
                $events_page_url = home_url('/');
            }
            $user_events_url = nexdirectory_add_query_arg('organizer', $user->ID, $events_page_url);
            $status_counts = ['publish' => 0, 'pending' => 0, 'draft' => 0];
            foreach ($events as $event_post) {
                $status = (string) get_post_status($event_post);
                if (isset($status_counts[$status])) {
                    $status_counts[$status]++;
                }
            }
            $display_name = (string) ($user->display_name ?: $user->user_login);
            $joined_label = wp_date(get_option('date_format'), strtotime((string) $user->user_registered));
            $role_label = ucwords(str_replace('_', ' ', $role));
            ?>
            <tr>
                <td class="nexdirectory-user-cell nexdirectory-admin-ops-user" data-label="<?php esc_attr_e('User', 'nexdirectory'); ?>">
                    <strong><?php echo esc_html($display_name); ?></strong><br>
                    <a href="mailto:<?php echo esc_attr(sanitize_email($user->user_email)); ?>"><?php echo esc_html($user->user_email); ?></a><br>
                    <?php /* translators: %s: Placeholder value. */ ?>
                    <small class="nexdirectory-admin-ops-meta"><?php echo esc_html(sprintf(__('Joined %s', 'nexdirectory'), $joined_label)); ?></small>
                </td>
                <td data-label="<?php esc_attr_e('Role', 'nexdirectory'); ?>">
                    <div class="nexdirectory-admin-ops-stack">
                        <span class="nexdirectory-admin-ops-pill"><?php echo esc_html($role_label); ?></span>
                        <?php if (!$is_admin) : ?>
                            <small class="nexdirectory-admin-ops-meta"><?php echo esc_html($trusted_editor ? __('Trusted edits enabled', 'nexdirectory') : __('Trusted edits require approval', 'nexdirectory')); ?></small>
                        <?php endif; ?>
                    </div>
                </td>
                <td data-label="<?php esc_attr_e('Verified', 'nexdirectory'); ?>">
                    <span class="nexdirectory-admin-ops-pill <?php echo $verified ? 'nexdirectory-admin-ops-pill-success' : 'nexdirectory-admin-ops-pill-muted'; ?>">
                        <?php echo esc_html($verified ? __('Yes', 'nexdirectory') : __('No', 'nexdirectory')); ?>
                    </span>
                </td>
                <td data-label="<?php echo esc_attr($plural_label); ?>">
                    <div class="nexdirectory-admin-ops-stack">
                        <strong><?php echo esc_html((string) count($events)); ?></strong>
                        <?php /* translators: 1: Placeholder value 1, 2: Placeholder value 2, 3: Placeholder value 3. */ ?>
                        <small class="nexdirectory-admin-ops-meta"><?php echo esc_html(sprintf(__('Published %1$d · Pending %2$d · Draft %3$d', 'nexdirectory'), (int) $status_counts['publish'], (int) $status_counts['pending'], (int) $status_counts['draft'])); ?></small>
                    </div>
                </td>
                <td class="nexdirectory-actions nexdirectory-admin-actions nexdirectory-admin-ops-actions" data-label="<?php esc_attr_e('Actions', 'nexdirectory'); ?>">
                    <div class="nexdirectory-admin-ops-action-grid">
                        <a class="nexdirectory-admin-ops-button nexdirectory-admin-ops-button-secondary" href="<?php echo esc_url(admin_url('user-edit.php?user_id=' . $user->ID)); ?>">
                            <?php esc_html_e('Open Profile', 'nexdirectory'); ?>
                        </a>
                        <a class="nexdirectory-admin-ops-button nexdirectory-admin-ops-button-secondary" href="<?php echo esc_url($user_events_url); ?>">
                            <?php /* translators: %s: Placeholder value. */ ?>
                            <?php echo esc_html(sprintf(__('View %s', 'nexdirectory'), $plural_label)); ?>
                        </a>
                        <?php if (!$is_admin) : ?>
                            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="nexdirectory-inline-form">
                                <input type="hidden" name="action" value="nexdirectory_manage_user_role">
                                <input type="hidden" name="user_id" value="<?php echo esc_attr((string) $user->ID); ?>">
                                <input type="hidden" name="role" value="<?php echo esc_attr($role === 'event_submitter' ? 'subscriber' : 'event_submitter'); ?>">
                                <?php wp_nonce_field('nexdirectory_manage_user_role', 'nexdirectory_manage_user_nonce'); ?>
                                <button class="nexdirectory-admin-ops-button" type="submit">
                                    <?php /* translators: %s: Placeholder value. */ ?>
                                    <?php echo esc_html($role === 'event_submitter' ? __('Set Subscriber', 'nexdirectory') : sprintf(__('Set %s Submitter', 'nexdirectory'), $singular_label)); ?>
                                </button>
                            </form>
                            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="nexdirectory-inline-form">
                                <input type="hidden" name="action" value="nexdirectory_toggle_verified">
                                <input type="hidden" name="user_id" value="<?php echo esc_attr((string) $user->ID); ?>">
                                <?php wp_nonce_field('nexdirectory_toggle_verified', 'nexdirectory_toggle_verified_nonce'); ?>
                                <button class="nexdirectory-admin-ops-button nexdirectory-admin-ops-button-secondary" type="submit">
                                    <?php /* translators: %s: Placeholder value. */ ?>
                                    <?php echo esc_html($verified ? __('Remove Badge', 'nexdirectory') : sprintf(__('Verify %s', 'nexdirectory'), $owner_singular)); ?>
                                </button>
                            </form>
                            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="nexdirectory-inline-form">
                                <input type="hidden" name="action" value="nexdirectory_toggle_user_edit_trust">
                                <input type="hidden" name="user_id" value="<?php echo esc_attr((string) $user->ID); ?>">
                                <?php wp_nonce_field('nexdirectory_toggle_user_edit_trust', 'nexdirectory_toggle_user_edit_trust_nonce'); ?>
                                <button class="nexdirectory-admin-ops-button nexdirectory-admin-ops-button-secondary" type="submit">
                                    <?php echo esc_html($trusted_editor ? __('Require Approval', 'nexdirectory') : __('Trust Edits', 'nexdirectory')); ?>
                                </button>
                            </form>
                        <?php endif; ?>
                    </div>

                    <?php if ($is_admin) : ?>
                        <p class="nexdirectory-admin-ops-meta"><?php esc_html_e('Role and trust controls are unavailable for administrator accounts.', 'nexdirectory'); ?></p>
                    <?php endif; ?>

                    <?php if (!empty($events)) : ?>
                        <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="nexdirectory-user-event-tools nexdirectory-admin-ops-user-tools">
                            <input type="hidden" name="action" value="nexdirectory_manage_user_event">
                            <input type="hidden" name="user_id" value="<?php echo esc_attr((string) $user->ID); ?>">
                            <?php wp_nonce_field('nexdirectory_manage_user_event', 'nexdirectory_manage_user_event_nonce'); ?>
                            <label><?php echo esc_html($singular_label); ?></label>
                            <select name="event_id">
                                <?php foreach ($events as $event_post) : ?>
                                    <option value="<?php echo esc_attr((string) $event_post->ID); ?>">
                                        <?php echo esc_html(get_the_title($event_post->ID) . ' (' . ucfirst((string) get_post_status($event_post->ID)) . ')'); ?>
                                    </option>
                                <?php endforeach; ?>
                            </select>
                            <input type="text" name="admin_subject" placeholder="<?php esc_attr_e('Email subject (optional)', 'nexdirectory'); ?>">
                            <?php /* translators: %1$s: Placeholder value. */ ?>
                            <textarea name="admin_message" rows="2" placeholder="<?php echo esc_attr(sprintf(__('Email note to %1$s owner (optional)', 'nexdirectory'), $singular_lower)); ?>"></textarea>
                            <div class="nexdirectory-actions-inline">
                                <?php /* translators: %s: Placeholder value. */ ?>
                                <button class="nexdirectory-admin-ops-button nexdirectory-admin-ops-button-secondary" type="submit" name="intent" value="open_event"><?php echo esc_html(sprintf(__('Open %s', 'nexdirectory'), $singular_label)); ?></button>
                                <?php /* translators: %s: Placeholder value. */ ?>
                                <button class="nexdirectory-admin-ops-button" type="submit" name="intent" value="publish_event"><?php echo esc_html(sprintf(__('Publish %s', 'nexdirectory'), $singular_label)); ?></button>
                                <?php /* translators: %s: Placeholder value. */ ?>
                                <button class="nexdirectory-admin-ops-button nexdirectory-admin-ops-button-secondary" type="submit" name="intent" value="unpublish_event"><?php echo esc_html(sprintf(__('Unpublish %s', 'nexdirectory'), $singular_label)); ?></button>
                                <?php /* translators: %s: Placeholder value. */ ?>
                                <button class="nexdirectory-admin-ops-button nexdirectory-admin-ops-button-secondary" type="submit" name="intent" value="email_owner"><?php echo esc_html(sprintf(__('Email %s', 'nexdirectory'), $owner_singular)); ?></button>
                            </div>
                        </form>
                    <?php endif; ?>
                </td>
            </tr>
            <?php
        endforeach;

        return (string) ob_get_clean();
    }    public function rest_user_search_permission() {
        return $this->user_can_manage_organizers();
    }


    public function rest_authenticated_permission() {
        return is_user_logged_in() && $this->user_can_access_platform();
    }


    public function rest_moderation_permission() {
        return $this->user_can_moderate_events();
    }


    /**
     * Returns the bounded ledger option used for one-use extension import IDs.
     *
     * @return string
     */
    private function get_extension_replay_ledger_option_name() {
        return 'nexdirectory_extension_replay_ledger_v1';
    }


    /**
     * Returns a site-specific MySQL advisory lock name.
     *
     * @return string
     */
    private function get_extension_replay_lock_name() {
        global $wpdb;
        $site_key = defined('DB_NAME') ? (string) DB_NAME : '';
        $site_key .= '|' . (string) $wpdb->prefix;
        return substr('nexdirectory_ext_replay_' . hash('sha256', $site_key), 0, 64);
    }


    /**
     * Serializes replay-ledger reads and writes across concurrent REST workers.
     *
     * @return string Acquired lock name, or an empty string on failure.
     */
    private function acquire_extension_replay_lock() {
        global $wpdb;
        $lock_name = $this->get_extension_replay_lock_name();
        $acquired = (int) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, 3)', $lock_name));
        return $acquired === 1 ? $lock_name : '';
    }


    /**
     * Releases the replay-ledger advisory lock.
     *
     * @param string $lock_name Acquired lock name.
     * @return void
     */
    private function release_extension_replay_lock($lock_name) {
        global $wpdb;
        $lock_name = (string) $lock_name;
        if ($lock_name !== '') {
            $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock_name));
        }
    }


    /**
     * Returns valid, unexpired request hashes from a raw ledger value.
     *
     * @param mixed $raw_ledger Stored option value.
     * @param int   $now Current Unix timestamp.
     * @return array<string, int>
     */
    private function normalize_extension_replay_ledger($raw_ledger, $now = 0) {
        $now = $now > 0 ? (int) $now : time();
        $ledger = [];
        if (!is_array($raw_ledger)) {
            return $ledger;
        }
        foreach ($raw_ledger as $request_hash => $expires_at) {
            $request_hash = strtolower((string) $request_hash);
            $expires_at = (int) $expires_at;
            if (!preg_match('/^[a-f0-9]{64}$/', $request_hash) || $expires_at <= $now) {
                continue;
            }
            $ledger[$request_hash] = $expires_at;
        }
        return $ledger;
    }


    /**
     * Schedules periodic pruning for the replay ledger.
     *
     * @return void
     */
    public function schedule_extension_replay_cleanup() {
        if (!wp_next_scheduled('nexdirectory_extension_replay_cleanup')) {
            wp_schedule_event(time() + HOUR_IN_SECONDS, 'hourly', 'nexdirectory_extension_replay_cleanup');
        }
    }


    /**
     * Removes expired extension request IDs from the bounded ledger.
     *
     * @return void
     */
    public function cleanup_extension_replay_ledger() {
        $lock_name = $this->acquire_extension_replay_lock();
        if ($lock_name === '') {
            return;
        }
        try {
            $option_name = $this->get_extension_replay_ledger_option_name();
            $raw_ledger = get_option($option_name, false);
            $ledger = $this->normalize_extension_replay_ledger($raw_ledger, time());
            if (empty($ledger)) {
                delete_option($option_name);
            } elseif ($ledger !== $raw_ledger) {
                update_option($option_name, $ledger, false);
            }
        } finally {
            $this->release_extension_replay_lock($lock_name);
        }
    }


    /**
     * Atomically consumes an authenticated extension import request ID.
     *
     * @param string $request_fingerprint Signed request identifier or legacy signature fingerprint.
     * @param int    $timestamp Signed Unix timestamp.
     * @return true|WP_Error
     */
    private function claim_extension_import_request_id($request_fingerprint, $timestamp) {
        $lock_name = $this->acquire_extension_replay_lock();
        if ($lock_name === '') {
            return new WP_Error(
                'nexdirectory_extension_replay_store_busy',
                __('Extension request verification is temporarily busy. Retry with a new signed request.', 'nexdirectory'),
                ['status' => 503]
            );
        }

        try {
            $option_name = $this->get_extension_replay_ledger_option_name();
            $raw_ledger = get_option($option_name, false);
            $ledger = $this->normalize_extension_replay_ledger($raw_ledger, time());
            $request_hash = hash('sha256', (string) $request_fingerprint);
            if (isset($ledger[$request_hash])) {
                return new WP_Error(
                    'nexdirectory_extension_request_replayed',
                    __('This extension import request was already processed. Retry with a new signed request.', 'nexdirectory'),
                    ['status' => 409]
                );
            }

            $maximum_entries = 1024;
            if (count($ledger) >= $maximum_entries) {
                return new WP_Error(
                    'nexdirectory_extension_replay_store_full',
                    __('Too many extension imports are awaiting replay expiry. Wait briefly and retry.', 'nexdirectory'),
                    ['status' => 429]
                );
            }

            // A future timestamp at the edge of the accepted skew remains valid
            // for almost ten minutes, so retain its marker through that window.
            $ledger[$request_hash] = max(time() + 300, (int) $timestamp + 301);
            $saved = $raw_ledger === false
                ? add_option($option_name, $ledger, '', false)
                : update_option($option_name, $ledger, false);
            if (!$saved) {
                return new WP_Error(
                    'nexdirectory_extension_replay_store_failed',
                    __('Extension request verification could not be saved. Retry with a new signed request.', 'nexdirectory'),
                    ['status' => 503]
                );
            }
        } finally {
            $this->release_extension_replay_lock($lock_name);
        }

        return true;
    }


    private function validate_extension_request_auth($request, $require_signature = false) {
        if ((string) get_option('nexdirectory_extension_enabled', '0') !== '1') {
            return new WP_Error('nexdirectory_extension_disabled', __('Extension import API is disabled in settings.', 'nexdirectory'), ['status' => 403]);
        }

        $has_hash = trim((string) get_option('nexdirectory_extension_app_pass_hash', '')) !== '';
        $legacy_pass = trim((string) get_option('nexdirectory_extension_app_pass', ''));
        if (!$has_hash && $legacy_pass === '') {
            return new WP_Error('nexdirectory_extension_not_configured', __('Extension connection key is not configured.', 'nexdirectory'), ['status' => 500]);
        }

        $provided_pass = trim((string) $request->get_header('x-nexdirectory-app-pass'));
        if ($provided_pass === '' || !$this->extension_app_pass_matches($provided_pass)) {
            $this->append_runtime_log('warning', 'Extension auth failed: invalid app pass', [
                'route' => (string) $request->get_route(),
                'ip' => sanitize_text_field((string) wp_unslash($_SERVER['REMOTE_ADDR'] ?? '')),
            ]);
            return new WP_Error('nexdirectory_extension_unauthorized', __('Invalid extension credentials.', 'nexdirectory'), ['status' => 401]);
        }

        $timestamp_raw = trim((string) $request->get_header('x-nexdirectory-timestamp'));
        $signature = trim((string) $request->get_header('x-nexdirectory-signature'));
        $route = '/' . ltrim((string) $request->get_route(), '/');
        $is_import_request = rtrim($route, '/') === '/nexdirectory/v1/extension/import';
        $request_id = '';
        if ($require_signature || $timestamp_raw !== '' || $signature !== '') {
            if ($timestamp_raw === '' || $signature === '') {
                return new WP_Error('nexdirectory_extension_signature_missing', __('Signed timestamp headers are required for this request.', 'nexdirectory'), ['status' => 401]);
            }
            if ($is_import_request) {
                $request_id = trim((string) $request->get_header('x-nexdirectory-request-id'));
                if (
                    $request_id !== ''
                    && (
                        strlen($request_id) < 16
                        || strlen($request_id) > 128
                        || !preg_match('/^[A-Za-z0-9][A-Za-z0-9._:-]+$/', $request_id)
                    )
                ) {
                    return new WP_Error(
                        'nexdirectory_extension_request_id_invalid',
                        __('X-NexDirectory-Request-ID must be a unique 16 to 128 character identifier.', 'nexdirectory'),
                        ['status' => 401]
                    );
                }
            }
            if (!preg_match('/^-?\d+$/', $timestamp_raw)) {
                return new WP_Error('nexdirectory_extension_signature_invalid', __('Invalid extension signature timestamp.', 'nexdirectory'), ['status' => 401]);
            }
            $timestamp = (int) $timestamp_raw;
            if (abs(time() - $timestamp) > 300) {
                return new WP_Error('nexdirectory_extension_signature_expired', __('Extension signature expired. Retry the request.', 'nexdirectory'), ['status' => 401]);
            }
            $body = (string) $request->get_body();
            $signed_payload = $timestamp_raw . '.';
            if ($is_import_request && $request_id !== '') {
                $signed_payload .= $request_id . '.';
            }
            $signed_payload .= $body;
            $expected_signature = hash_hmac('sha256', $signed_payload, $provided_pass);
            if (!hash_equals($expected_signature, $signature)) {
                return new WP_Error('nexdirectory_extension_signature_invalid', __('Extension request signature check failed.', 'nexdirectory'), ['status' => 401]);
            }
            if ($is_import_request) {
                // New clients sign a one-use request ID. Existing published
                // clients remain protected by consuming the exact v1 signed
                // request fingerprint, without breaking their connection.
                $request_fingerprint = $request_id !== ''
                    ? 'v2|' . $request_id
                    : 'v1|' . $timestamp_raw . '|' . $signature;
                $claimed = $this->claim_extension_import_request_id($request_fingerprint, $timestamp);
                if (is_wp_error($claimed)) {
                    return $claimed;
                }
            }
        }

        return true;
    }


    public function rest_extension_permission($request) {
        return $this->validate_extension_request_auth($request, true);
    }


    private function normalize_extension_terms_value($value) {
        if (is_array($value)) {
            $parts = array_map('nexdirectory_sanitize_text', array_map('strval', $value));
            $parts = array_values(array_filter(array_map('trim', $parts), static function ($item) {
                return $item !== '';
            }));
            return implode(',', $parts);
        }
        return sanitize_text_field((string) $value);
    }


    private function normalize_extension_url_lines($value) {
        if (is_array($value)) {
            $value = implode("\n", array_map('strval', $value));
        }
        $urls = $this->parse_multiline_urls((string) $value);
        return implode("\n", array_values(array_unique(array_filter($urls))));
    }


    private function normalize_extension_sessions_json($value) {
        if (is_string($value)) {
            $decoded = json_decode($value, true);
            if (is_array($decoded)) {
                return wp_json_encode($decoded);
            }
            return wp_json_encode($this->parse_sessions($value));
        }
        if (is_array($value)) {
            return wp_json_encode($value);
        }
        return wp_json_encode([]);
    }

    private function normalize_extension_key_speakers_json($value) {
        if (is_string($value)) {
            $decoded = json_decode($value, true);
            if (is_array($decoded)) {
                return wp_json_encode($decoded);
            }
            return wp_json_encode($this->parse_key_speakers($value));
        }
        if (is_array($value)) {
            return wp_json_encode($value);
        }
        return wp_json_encode([]);
    }

    /**
     * Checks whether a licensed extension has enabled per-listing builder
     * mutations for the current request.
     *
     * The default is deliberately false: the saved Free option alone cannot
     * authorize premium payload parsing or writes.
     *
     * @param string $context Surface or handler identifier.
     * @param int    $user_id User ID.
     * @return bool
     */
    private function is_custom_submission_builder_available($context = '', $user_id = 0) {
        return (bool) apply_filters(
            'nexdirectory_custom_submission_builder_enabled',
            false,
            sanitize_key((string) $context),
            absint((int) $user_id),
            $this
        );
    }

    private function get_extension_custom_field_schema() {
        $schema = apply_filters(
            'nexdirectory_extension_custom_field_schema',
            [],
            $this,
            get_current_user_id()
        );
        return is_array($schema) ? array_values($schema) : [];
    }

    private function normalize_extension_form_field_type($type) {
        $type = sanitize_key((string) $type);
        if (in_array($type, ['text', 'textarea', 'url', 'email', 'number', 'date', 'datetime-local', 'checkbox', 'select'], true)) {
            return $type;
        }
        if ($type === 'datetime') {
            return 'datetime-local';
        }
        return 'text';
    }

    private function get_extension_core_meta_keys() {
        return [
            'nexdirectory_start',
            'nexdirectory_end',
            'nexdirectory_timezone',
            'nexdirectory_venue',
            'nexdirectory_address',
            'nexdirectory_city',
            'nexdirectory_country',
            'nexdirectory_capacity',
            'nexdirectory_price',
            'nexdirectory_ticket_sales_enabled',
            'nexdirectory_ticket_price',
            'nexdirectory_ticket_button_label',
            'nexdirectory_meta_title',
            'nexdirectory_meta_description',
            'nexdirectory_external_url',
            'nexdirectory_event_stream_url',
            'nexdirectory_contact_name',
            'nexdirectory_contact_email',
            'nexdirectory_contact_phone',
            'nexdirectory_cover_image_url',
            'nexdirectory_video_urls',
            'nexdirectory_gallery_urls',
            'nexdirectory_supporting_docs',
            'nexdirectory_official_event',
            'nexdirectory_recurrence',
            'nexdirectory_recurrence_until',
        ];
    }

    private function get_extension_submit_template_field_names() {
        static $cached = null;
        if (is_array($cached)) {
            return $cached;
        }

        $template_path = plugin_dir_path(NEXDIRECTORY_PLUGIN_FILE) . 'templates/frontend/submit-event.php';
        if (!is_readable($template_path)) {
            $cached = [];
            return $cached;
        }

        $contents = file_get_contents($template_path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reads a bundled template to expose supported extension fields.
        if (!is_string($contents) || $contents === '') {
            $cached = [];
            return $cached;
        }

        if (!preg_match_all('/name=(["\'])([^"\']+)\\1/', $contents, $matches)) {
            $cached = [];
            return $cached;
        }

        $names = [];
        foreach ((array) ($matches[2] ?? []) as $raw_name) {
            $raw_name = trim((string) $raw_name);
            if ($raw_name === '') {
                continue;
            }
            if (strpos($raw_name, '<?php') !== false) {
                if (strpos($raw_name, 'nexdirectory_tax_') === 0) {
                    $names[] = 'nexdirectory_tax_*';
                }
                continue;
            }
            $normalized = preg_replace('/\[\]$/', '', $raw_name);
            $key = sanitize_key((string) $normalized);
            if ($key === '') {
                continue;
            }
            if (strpos($key, 'nexdirectory_') !== 0 && !in_array($key, ['post_title', 'post_excerpt', 'post_content', 'event_tags'], true)) {
                continue;
            }
            $names[] = $key;
        }

        $names = array_values(array_unique($names));
        sort($names, SORT_STRING);
        $cached = $names;
        return $cached;
    }

    private function is_extension_system_submit_field($key) {
        $key = sanitize_key((string) $key);
        if ($key === '') {
            return true;
        }
        $ignored = [
            'action',
            'nexdirectory_nonce',
            'nexdirectory_website',
            'nexdirectory_recaptcha_token',
            'nexdirectory_autosave_post_id',
            'nexdirectory_edit_post_id',
            'nexdirectory_custom_fields_builder_json',
            'nexdirectory_cover_file',
            'nexdirectory_gallery_files',
            'nexdirectory_intent',
            'nexdirectory_start_date',
            'nexdirectory_start_time',
            'nexdirectory_end_date',
            'nexdirectory_end_time',
            'nexdirectory_is_recurring',
            'nexdirectory_sessions',
            'nexdirectory_youtube_video_id_1',
            'nexdirectory_youtube_video_id_2',
        ];
        return in_array($key, $ignored, true);
    }

    private function guess_extension_form_field_type_from_key($key) {
        $key = sanitize_key((string) $key);
        if ($key === '') {
            return 'text';
        }
        if (substr($key, -5) === '_date') {
            return 'date';
        }
        if (substr($key, -5) === '_time') {
            return 'text';
        }
        if (strpos($key, '_email') !== false) {
            return 'email';
        }
        if (strpos($key, '_url') !== false || strpos($key, '_website') !== false) {
            return 'url';
        }
        if (strpos($key, '_description') !== false || strpos($key, '_details') !== false || strpos($key, '_notes') !== false) {
            return 'textarea';
        }
        if (strpos($key, '_count') !== false || strpos($key, '_quantity') !== false || strpos($key, '_capacity') !== false || strpos($key, '_year') !== false) {
            return 'number';
        }
        return 'text';
    }

    private function get_extension_form_field_label($key) {
        $key = sanitize_key((string) $key);
        $label = str_replace('nexdirectory_', '', $key);
        $label = str_replace('_', ' ', (string) $label);
        $label = ucwords(trim((string) $label));
        return $label !== '' ? $label : $key;
    }

    private function get_extension_form_field_options($key) {
        $key = sanitize_key((string) $key);
        $maps = [
            'nexdirectory_job_work_mode' => ['on-site', 'hybrid', 'remote'],
            'nexdirectory_job_compensation_type' => ['Salary', 'Hourly', 'Contract / Day Rate', 'Commission', 'Volunteer'],
            'nexdirectory_job_remote_policy' => ['On-site', 'Remote', 'Hybrid'],
            'nexdirectory_job_visa_sponsorship' => ['0', '1'],
            'nexdirectory_market_price_type' => ['fixed', 'negotiable', 'auction', 'free'],
            'nexdirectory_market_seller_type' => ['private', 'business'],
            'nexdirectory_market_listing_status' => ['active', 'reserved', 'sold', 'expired'],
            'nexdirectory_market_delivery_methods' => ['pickup', 'shipping', 'local-delivery', 'digital'],
            'nexdirectory_market_returns_accepted' => ['0', '1'],
            'nexdirectory_market_accept_offers' => ['0', '1'],
            'nexdirectory_directory_listing_status' => ['active', 'temporarily-closed', 'permanently-closed'],
            'nexdirectory_directory_business_type' => ['independent-business', 'agency', 'private-owner', 'nonprofit', 'multi-location', 'franchise'],
            'nexdirectory_event_mode_option' => ['in-person', 'video-stream'],
            'nexdirectory_price_simple' => ['free', 'paid'],
            'nexdirectory_ticket_sales_enabled' => ['0', '1'],
            'nexdirectory_official_event' => ['0', '1'],
            'nexdirectory_lock_pin' => ['0', '1'],
        ];
        return isset($maps[$key]) ? implode("\n", (array) $maps[$key]) : '';
    }

    private function get_extension_form_field_schema() {
        $meta_fields = is_callable([$this, 'get_event_meta_fields']) ? (array) $this->get_event_meta_fields() : [];
        $exclude_core = $this->get_extension_core_meta_keys();
        $exclude_map = array_fill_keys($exclude_core, true);
        $schema = [];
        $seen = [];

        foreach ($meta_fields as $meta_key => $meta_type) {
            $meta_key = sanitize_key((string) $meta_key);
            if ($meta_key === '' || isset($exclude_map[$meta_key]) || isset($seen[$meta_key])) {
                continue;
            }
            $seen[$meta_key] = true;
            $options = $this->get_extension_form_field_options($meta_key);
            $normalized_type = $this->normalize_extension_form_field_type((string) $meta_type);
            if ($options !== '' && !in_array($normalized_type, ['textarea', 'checkbox'], true)) {
                $normalized_type = 'select';
            }
            if (in_array($meta_key, ['nexdirectory_market_returns_accepted', 'nexdirectory_market_accept_offers', 'nexdirectory_lock_pin', 'nexdirectory_job_visa_sponsorship'], true)) {
                $normalized_type = 'checkbox';
            }
            $schema[] = [
                'key' => $meta_key,
                'label' => $this->get_extension_form_field_label($meta_key),
                'type' => $normalized_type,
                'required' => '0',
                'placeholder' => '',
                'options' => $options,
                'source' => 'meta',
            ];
        }

        $custom_taxonomies = is_callable([$this, 'get_custom_taxonomy_definitions_for_submit'])
            ? (array) $this->get_custom_taxonomy_definitions_for_submit()
            : [];
        foreach ($custom_taxonomies as $taxonomy_row) {
            $taxonomy_row = is_array($taxonomy_row) ? $taxonomy_row : [];
            $taxonomy_slug = sanitize_key((string) ($taxonomy_row['slug'] ?? ''));
            if ($taxonomy_slug === '' || !taxonomy_exists($taxonomy_slug)) {
                continue;
            }
            $field_key = 'nexdirectory_tax_' . $taxonomy_slug;
            if (isset($seen[$field_key])) {
                continue;
            }
            $seen[$field_key] = true;
            $term_rows = get_terms([
                'taxonomy' => $taxonomy_slug,
                'hide_empty' => false,
                'orderby' => 'name',
                'order' => 'ASC',
                'fields' => 'names',
            ]);
            $option_rows = [];
            if (is_array($term_rows)) {
                foreach ($term_rows as $term_name) {
                    $clean = sanitize_text_field((string) $term_name);
                    if ($clean !== '') {
                        $option_rows[] = $clean;
                    }
                }
            }
            $schema[] = [
                'key' => $field_key,
                'label' => sanitize_text_field((string) ($taxonomy_row['label'] ?? $taxonomy_slug)),
                'type' => 'textarea',
                'required' => '0',
                'placeholder' => __('Comma separated values', 'nexdirectory'),
                'options' => implode("\n", $option_rows),
                'source' => 'taxonomy',
            ];
        }

        $submit_fields = $this->get_extension_submit_template_field_names();
        foreach ($submit_fields as $submit_key) {
            $submit_key = sanitize_key((string) $submit_key);
            if ($submit_key === '' || $submit_key === 'nexdirectory_tax_*') {
                continue;
            }
            if (isset($seen[$submit_key]) || isset($exclude_map[$submit_key]) || $this->is_extension_system_submit_field($submit_key)) {
                continue;
            }
            if (strpos($submit_key, 'nexdirectory_') !== 0) {
                continue;
            }
            $seen[$submit_key] = true;
            $options = $this->get_extension_form_field_options($submit_key);
            $normalized_type = $this->guess_extension_form_field_type_from_key($submit_key);
            if ($options !== '' && !in_array($normalized_type, ['textarea', 'checkbox'], true)) {
                $normalized_type = 'select';
            }
            if (in_array($submit_key, ['nexdirectory_market_returns_accepted', 'nexdirectory_market_accept_offers', 'nexdirectory_lock_pin', 'nexdirectory_job_visa_sponsorship'], true)) {
                $normalized_type = 'checkbox';
            }
            $schema[] = [
                'key' => $submit_key,
                'label' => $this->get_extension_form_field_label($submit_key),
                'type' => $normalized_type,
                'required' => '0',
                'placeholder' => '',
                'options' => $options,
                'source' => 'submit-template',
            ];
        }

        usort($schema, static function ($a, $b) {
            $label_a = sanitize_text_field((string) ($a['label'] ?? ''));
            $label_b = sanitize_text_field((string) ($b['label'] ?? ''));
            return strcasecmp($label_a, $label_b);
        });

        return $schema;
    }

    private function normalize_extension_custom_fields_payload(array $payload) {
        $raw = $payload['custom_fields'] ?? ($payload['custom_field_values'] ?? ($payload['nexdirectory_custom_fields_data'] ?? []));
        if (is_string($raw)) {
            $decoded = json_decode($raw, true);
            $raw = is_array($decoded) ? $decoded : [];
        }
        if (!is_array($raw)) {
            return [];
        }

        $normalized = [];
        foreach ($raw as $key => $value) {
            $id = sanitize_key((string) $key);
            if ($id === '') {
                continue;
            }
            if (is_array($value)) {
                $normalized[$id] = wp_json_encode($value);
            } elseif (is_bool($value)) {
                $normalized[$id] = $value ? '1' : '0';
            } else {
                $normalized[$id] = (string) $value;
            }
        }
        return $normalized;
    }

    private function normalize_extension_extra_fields_payload(array $payload) {
        $meta_fields = is_callable([$this, 'get_event_meta_fields']) ? (array) $this->get_event_meta_fields() : [];
        $allowed_meta_keys = array_fill_keys(array_map('sanitize_key', array_keys($meta_fields)), true);
        foreach ($this->get_extension_submit_template_field_names() as $submit_field_key) {
            $submit_field_key = sanitize_key((string) $submit_field_key);
            if ($submit_field_key === '' || strpos($submit_field_key, 'nexdirectory_') !== 0 || $this->is_extension_system_submit_field($submit_field_key)) {
                continue;
            }
            $allowed_meta_keys[$submit_field_key] = true;
        }
        $normalized = [];
        $sources = [];

        $raw_extra = $payload['extra_fields'] ?? [];
        if (is_string($raw_extra)) {
            $decoded_extra = json_decode($raw_extra, true);
            $raw_extra = is_array($decoded_extra) ? $decoded_extra : [];
        }
        if (is_array($raw_extra)) {
            $sources[] = $raw_extra;
        }
        $sources[] = $payload;

        foreach ($sources as $source) {
            if (!is_array($source)) {
                continue;
            }
            foreach ($source as $raw_key => $raw_value) {
                $key = sanitize_key((string) $raw_key);
                if ($key === '') {
                    continue;
                }
                $is_meta_key = strpos($key, 'nexdirectory_') === 0 && isset($allowed_meta_keys[$key]);
                $is_custom_tax_key = strpos($key, 'nexdirectory_tax_') === 0;
                if (!$is_meta_key && !$is_custom_tax_key) {
                    continue;
                }

                if (is_array($raw_value)) {
                    if ($is_custom_tax_key || $key === 'nexdirectory_market_delivery_methods') {
                        $parts = array_values(array_filter(array_map('sanitize_text_field', $raw_value), static function ($part) {
                            return trim((string) $part) !== '';
                        }));
                        $normalized[$key] = implode(',', $parts);
                    } else {
                        $normalized[$key] = wp_json_encode($raw_value);
                    }
                } elseif (is_bool($raw_value)) {
                    $normalized[$key] = $raw_value ? '1' : '0';
                } else {
                    $normalized[$key] = (string) $raw_value;
                }
            }
        }

        return $normalized;
    }

    private function apply_extension_import_enhanced_fields($post_id, array $row, array $payload) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return;
        }

        $seo_enabled = (string) get_option('nexdirectory_enable_organizer_event_seo', '1') === '1';
        if ($seo_enabled) {
            $meta_title = $this->sanitize_event_seo_title((string) ($row['meta_title'] ?? ($payload['meta_title'] ?? '')));
            $meta_description = $this->sanitize_event_seo_description((string) ($row['meta_description'] ?? ($payload['meta_description'] ?? '')));
            if ($meta_title !== '') {
                update_post_meta($post_id, 'nexdirectory_meta_title', $meta_title);
            }
            if ($meta_description !== '') {
                update_post_meta($post_id, 'nexdirectory_meta_description', $meta_description);
            }
        }

        $schema = $this->get_extension_custom_field_schema();
        if (empty($schema)) {
            return;
        }
        $raw_values = $this->normalize_extension_custom_fields_payload($payload);
        if (empty($raw_values)) {
            return;
        }

        do_action(
            'nexdirectory_persist_extension_custom_fields',
            $post_id,
            $raw_values,
            $schema,
            $this
        );
    }

    private function normalize_extension_import_content_mode($value = '') {
        $ai_importer_enabled = function_exists('nexdirectory_is_pro_feature_enabled')
            && nexdirectory_is_pro_feature_enabled('ai_importer');
        if (!$ai_importer_enabled) {
            return 'verbatim';
        }

        $mode = sanitize_key((string) $value);
        if (in_array($mode, ['rewrite', 'verbatim'], true)) {
            return $mode;
        }
        $mode = sanitize_key((string) get_option('nexdirectory_extension_import_content_mode', 'rewrite'));
        return in_array($mode, ['rewrite', 'verbatim'], true) ? $mode : 'rewrite';
    }

    /**
     * Lets a licensed extension enhance normalized import copy.
     *
     * Free keeps manual and verbatim imports functional. Optional content
     * transformations are supplied entirely by an active extension.
     *
     * @param array<string, mixed> $row Normalized import row.
     * @param array<string, mixed> $source_payload Original source payload.
     * @return array<string, mixed>
     */
    private function maybe_rewrite_extension_import_copy(array $row, array $source_payload = []) {
        if (!function_exists('nexdirectory_call_extension_method')) {
            return $row;
        }

        $rewritten = nexdirectory_call_extension_method(
            __FUNCTION__,
            [$row, $source_payload],
            $row
        );

        return is_array($rewritten) ? $rewritten : $row;
    }


    private function build_extension_import_row(array $payload, array $extension_preferences = []) {
        $default_status = sanitize_key((string) get_option('nexdirectory_extension_import_status', 'draft'));
        if (!in_array($default_status, ['draft', 'publish'], true)) {
            $default_status = 'draft';
        }
        $media_policy = sanitize_key((string) get_option('nexdirectory_extension_import_media_policy', ''));
        if (!in_array($media_policy, ['none', 'images', 'images_video', 'video'], true)) {
            $media_policy = 'none';
        }
        $pref_status = sanitize_key((string) ($extension_preferences['import_status'] ?? ''));
        // A connected client may make an import safer (publish -> draft), but it
        // must never elevate a server draft policy to publish.
        if ($default_status === 'publish' && in_array($pref_status, ['draft', 'publish'], true)) {
            $default_status = $pref_status;
        }
        $pref_media_policy = sanitize_key((string) ($extension_preferences['media_upload_policy'] ?? ''));
        $allowed_client_media_policies = [
            'none' => ['none'],
            'images' => ['none', 'images'],
            'video' => ['none', 'video'],
            'images_video' => ['none', 'images', 'video', 'images_video'],
        ];
        if (in_array($pref_media_policy, (array) ($allowed_client_media_policies[$media_policy] ?? ['none']), true)) {
            $media_policy = $pref_media_policy;
        }
        // Rewriting can invoke a paid remote service and materially change copy,
        // so only the server-side administrator policy controls it.
        $content_mode = $this->normalize_extension_import_content_mode();

        $start = (string) ($payload['start'] ?? $payload['start_date'] ?? ($payload['nexdirectory_start'] ?? ''));
        $end = (string) ($payload['end'] ?? $payload['end_date'] ?? ($payload['nexdirectory_end'] ?? ''));
        $event_url = esc_url_raw((string) ($payload['external_url'] ?? $payload['event_url'] ?? $payload['url'] ?? ($payload['nexdirectory_external_url'] ?? '')));
        $stream_url = esc_url_raw((string) ($payload['stream_url'] ?? $payload['video_stream_url'] ?? ($payload['nexdirectory_event_stream_url'] ?? '')));
        $video_urls = $this->normalize_extension_url_lines($payload['video_urls'] ?? ($payload['videos'] ?? ($payload['nexdirectory_video_urls'] ?? '')));
        if ($stream_url !== '') {
            $video_urls = $this->normalize_extension_url_lines($video_urls . "\n" . $stream_url);
        }
        $recurrence = sanitize_key((string) ($payload['recurrence'] ?? ($payload['nexdirectory_recurrence'] ?? 'none')));
        if (!in_array($recurrence, ['none', 'daily', 'weekly', 'monthly'], true)) {
            $recurrence = 'none';
        }
        $custom_fields_payload = $this->normalize_extension_custom_fields_payload($payload);
        $extra_fields_payload = $this->normalize_extension_extra_fields_payload($payload);

        $row = [
            'title' => sanitize_text_field((string) ($payload['title'] ?? '')),
            'description' => wp_kses_post((string) ($payload['description'] ?? $payload['content'] ?? '')),
            'excerpt' => nexdirectory_sanitize_textarea((string) ($payload['excerpt'] ?? '')),
            'meta_title' => sanitize_text_field((string) ($payload['meta_title'] ?? ($payload['nexdirectory_meta_title'] ?? ''))),
            'meta_description' => nexdirectory_sanitize_textarea((string) ($payload['meta_description'] ?? ($payload['nexdirectory_meta_description'] ?? ''))),
            'start' => $this->normalize_datetime_local($start),
            'end' => $this->normalize_datetime_local($end),
            'timezone' => sanitize_text_field((string) ($payload['timezone'] ?? ($payload['nexdirectory_timezone'] ?? ''))),
            'venue' => sanitize_text_field((string) ($payload['venue'] ?? $payload['location_name'] ?? ($payload['nexdirectory_venue'] ?? ''))),
            'address' => sanitize_text_field((string) ($payload['address'] ?? $payload['location'] ?? ($payload['nexdirectory_address'] ?? ''))),
            'city' => sanitize_text_field((string) ($payload['city'] ?? ($payload['nexdirectory_city'] ?? ''))),
            'country' => sanitize_text_field((string) ($payload['country'] ?? ($payload['nexdirectory_country'] ?? ''))),
            'lat' => is_numeric($payload['lat'] ?? ($payload['nexdirectory_lat'] ?? null)) ? (string) ($payload['lat'] ?? ($payload['nexdirectory_lat'] ?? '')) : '',
            'lng' => is_numeric($payload['lng'] ?? ($payload['nexdirectory_lng'] ?? null)) ? (string) ($payload['lng'] ?? ($payload['nexdirectory_lng'] ?? '')) : '',
            'capacity' => absint((int) ($payload['capacity'] ?? ($payload['nexdirectory_capacity'] ?? 0))),
            'price' => sanitize_text_field((string) ($payload['price'] ?? ($payload['nexdirectory_price'] ?? ''))),
            'ticket_sales_enabled' => in_array((string) ($payload['ticket_sales_enabled'] ?? ($payload['nexdirectory_ticket_sales_enabled'] ?? '0')), ['1', 'true', 'yes'], true) ? '1' : '0',
            'ticket_price' => sanitize_text_field((string) ($payload['ticket_price'] ?? ($payload['nexdirectory_ticket_price'] ?? ''))),
            'ticket_button_label' => sanitize_text_field((string) ($payload['ticket_button_label'] ?? ($payload['nexdirectory_ticket_button_label'] ?? ''))),
            'external_url' => $event_url,
            'stream_url' => $stream_url,
            'contact_email' => sanitize_email((string) ($payload['contact_email'] ?? ($payload['nexdirectory_contact_email'] ?? ''))),
            'contact_phone' => sanitize_text_field((string) ($payload['contact_phone'] ?? ($payload['nexdirectory_contact_phone'] ?? ''))),
            'contact_name' => sanitize_text_field((string) ($payload['contact_name'] ?? ($payload['nexdirectory_contact_name'] ?? ''))),
            'cover_image_url' => esc_url_raw((string) ($payload['cover_image_url'] ?? $payload['image_url'] ?? $payload['thumbnail_url'] ?? ($payload['nexdirectory_cover_image_url'] ?? ''))),
            'official_event' => $this->normalize_official_event_value($payload['official_event'] ?? ($payload['official'] ?? ($payload['nexdirectory_official_event'] ?? '0'))),
            'video_urls' => $video_urls,
            'gallery_urls' => $this->normalize_extension_url_lines($payload['gallery_urls'] ?? ($payload['gallery'] ?? $payload['images'] ?? ($payload['nexdirectory_gallery_urls'] ?? ''))),
            'supporting_docs_urls' => $this->normalize_extension_url_lines($payload['supporting_docs_urls'] ?? ($payload['supporting_docs'] ?? ($payload['documents'] ?? ($payload['nexdirectory_supporting_docs'] ?? '')))),
            'sessions' => $this->normalize_extension_sessions_json($payload['sessions'] ?? ($payload['itinerary'] ?? ($payload['agenda'] ?? []))),
            'key_speakers' => $this->normalize_extension_key_speakers_json($payload['key_speakers'] ?? ($payload['speakers'] ?? ($payload['speaker_profiles'] ?? []))),
            'event_type' => $this->normalize_extension_terms_value($payload['event_type'] ?? ($payload['type'] ?? '')),
            'event_mode' => $this->normalize_extension_terms_value($payload['event_mode'] ?? ($payload['mode'] ?? '')),
            'event_price_range' => $this->normalize_extension_terms_value($payload['event_price_range'] ?? ($payload['price_range'] ?? '')),
            'event_tags' => $this->normalize_extension_terms_value($payload['event_tags'] ?? ($payload['tags'] ?? '')),
            'recurrence' => $recurrence,
            'recurrence_until' => $this->normalize_datetime_local((string) ($payload['recurrence_until'] ?? ($payload['repeat_until'] ?? ($payload['nexdirectory_recurrence_until'] ?? '')))),
            'custom_fields' => $custom_fields_payload,
            'status' => $default_status,
        ];

        foreach ($extra_fields_payload as $extra_key => $extra_value) {
            if (!array_key_exists($extra_key, $row) || trim((string) $row[$extra_key]) === '') {
                $row[$extra_key] = $extra_value;
            }
        }
        if (!empty($extra_fields_payload)) {
            $row['extra_fields'] = $extra_fields_payload;
        }

        if ($row['excerpt'] === '' && $row['description'] !== '') {
            $row['excerpt'] = wp_trim_words(nexdirectory_strip_all_tags($row['description']), 26);
        }

        if ($content_mode === 'rewrite') {
            $row = $this->maybe_rewrite_extension_import_copy($row, $payload);
        }

        if ($row['city'] === '' && $row['address'] !== '') {
            $parts = array_filter(array_map('trim', explode(',', $row['address'])));
            if (!empty($parts)) {
                $row['city'] = (string) end($parts);
            }
        }
        $parsed_extension_tags = $this->parse_event_tags_input((string) ($row['event_tags'] ?? ''), 8, 40);
        if (is_wp_error($parsed_extension_tags)) {
            $row['event_tags'] = '';
        } else {
            $row['event_tags'] = implode(',', (array) $parsed_extension_tags);
        }

        if ($media_policy === 'none') {
            $row['cover_image_url'] = '';
            $row['gallery_urls'] = '';
            $row['video_urls'] = '';
        } elseif ($media_policy === 'images') {
            $row['video_urls'] = '';
        } elseif ($media_policy === 'video') {
            $row['cover_image_url'] = '';
            $row['gallery_urls'] = '';
        }

        return $row;
    }


    public function rest_extension_ping($request) {
        $import_status = sanitize_key((string) get_option('nexdirectory_extension_import_status', 'draft'));
        if (!in_array($import_status, ['draft', 'publish'], true)) {
            $import_status = 'draft';
        }
        $media_policy = sanitize_key((string) get_option('nexdirectory_extension_import_media_policy', ''));
        if (!in_array($media_policy, ['none', 'images', 'images_video', 'video'], true)) {
            $media_policy = 'none';
        }
        $custom_fields = $this->get_extension_custom_field_schema();
        $form_fields = $this->get_extension_form_field_schema();
        $submit_fields = $this->get_extension_submit_template_field_names();
        $core_meta_keys = $this->get_extension_core_meta_keys();
        return rest_ensure_response([
            'ok' => true,
            'plugin' => $this->get_white_label_brand_name(),
            'version' => self::VERSION,
            'site_url' => home_url('/'),
            'rest_base' => esc_url_raw(rest_url('nexdirectory/v1/')),
            'authentication' => [
                'import_signature_version' => 2,
                'request_id_header' => 'X-NexDirectory-Request-ID',
                'import_signature_format' => 'timestamp.request_id.body',
                'maximum_clock_skew_seconds' => 300,
                'legacy_signature_v1_supported' => true,
            ],
            'defaults' => [
                'import_status' => $import_status,
                'mark_claimable' => (string) get_option('nexdirectory_extension_import_mark_claimable', '1') === '1',
                'media_upload_policy' => $media_policy,
                'content_mode' => $this->normalize_extension_import_content_mode(),
                'notify_potential_owner_default' => (string) get_option('nexdirectory_extension_notify_potential_owner_default', '0') === '1',
                'allow_images' => in_array($media_policy, ['images', 'images_video'], true),
                'show_autogenerated_disclaimer' => (string) get_option('nexdirectory_enable_autogenerated_disclaimer', '1') === '1',
            ],
            'features' => [
                'claim_listings' => (string) get_option('nexdirectory_enable_claim_listings', '0') === '1',
                'duplicate_detection' => (string) get_option('nexdirectory_enable_duplicate_detection', '1') === '1',
                'custom_fields_enabled' => !empty($custom_fields),
                'form_fields_enabled' => !empty($form_fields),
            ],
            'required_fields' => ['title', 'start'],
            'supports' => ['gallery_urls', 'video_urls', 'supporting_docs_urls', 'sessions', 'key_speakers', 'event_tags', 'event_type', 'event_mode', 'event_price_range', 'recurrence', 'recurrence_until', 'meta_title', 'meta_description', 'ticket_sales_enabled', 'ticket_price', 'ticket_button_label', 'custom_fields', 'form_fields', 'extra_fields', 'custom_taxonomy_terms'],
            'custom_fields' => $custom_fields,
            'form_fields' => $form_fields,
            'submit_fields' => $submit_fields,
            'extension_core_meta_keys' => $core_meta_keys,
            'timestamp' => time(),
        ]);
    }


    public function rest_extension_update_preferences($request) {
        $import_status = sanitize_key((string) get_option('nexdirectory_extension_import_status', 'draft'));
        if (!in_array($import_status, ['draft', 'publish'], true)) {
            $import_status = 'draft';
        }

        $media_policy = sanitize_key((string) get_option('nexdirectory_extension_import_media_policy', 'none'));
        if (!in_array($media_policy, ['none', 'images', 'video', 'images_video'], true)) {
            $media_policy = 'none';
        }

        $notify_default = (string) get_option('nexdirectory_extension_notify_potential_owner_default', '0') === '1' ? '1' : '0';
        $content_mode = $this->normalize_extension_import_content_mode();

        return rest_ensure_response([
            'ok' => true,
            'read_only' => true,
            'message' => __('Server import policy is managed by a WordPress administrator.', 'nexdirectory'),
            'defaults' => [
                'import_status' => $import_status,
                'media_upload_policy' => $media_policy,
                'content_mode' => $content_mode,
                'notify_potential_owner_default' => $notify_default === '1',
            ],
        ]);
    }


    public function rest_extension_import_event($request) {
        $raw_payload = $request->get_json_params();
        if (!is_array($raw_payload)) {
            $decoded = json_decode((string) $request->get_body(), true);
            $raw_payload = is_array($decoded) ? $decoded : [];
        }
        $payload = $raw_payload;
        if (isset($raw_payload['event']) && is_array($raw_payload['event'])) {
            $payload = (array) $raw_payload['event'];
        }
        if (empty($payload)) {
            return new WP_Error('nexdirectory_extension_empty_payload', __('Extension payload is empty.', 'nexdirectory'), ['status' => 400]);
        }

        $extension_preferences = [];
        if (isset($raw_payload['extension_preferences']) && is_array($raw_payload['extension_preferences'])) {
            $extension_preferences = (array) $raw_payload['extension_preferences'];
        }

        $row = $this->build_extension_import_row($payload, $extension_preferences);
        if (trim((string) ($row['title'] ?? '')) === '') {
            return new WP_Error('nexdirectory_extension_missing_title', __('Missing required event title.', 'nexdirectory'), ['status' => 400]);
        }
        if (trim((string) ($row['start'] ?? '')) === '') {
            return new WP_Error('nexdirectory_extension_missing_start', __('Missing required start date/time.', 'nexdirectory'), ['status' => 400]);
        }

        $duplicate_detection_enabled = (string) get_option('nexdirectory_enable_duplicate_detection', '1') === '1';
        $duplicate_event_id = 0;
        if ($duplicate_detection_enabled) {
            $duplicate_event_id = $this->find_duplicate_event_candidate(
                (string) ($row['title'] ?? ''),
                (string) ($row['start'] ?? ''),
                (string) ($row['city'] ?? ''),
                (string) ($row['address'] ?? ''),
                (string) ($row['venue'] ?? ''),
                0
            );
        }

        $author_id = $this->get_default_import_author_id(absint((int) ($payload['author_id'] ?? 0)));
        $created = $this->import_event_from_row(
            $row,
            (string) ($row['status'] ?? 'draft'),
            $duplicate_detection_enabled,
            $author_id
        );
        if (is_wp_error($created)) {
            if ($created->get_error_code() === 'duplicate_event') {
                $duplicate_data = $created->get_error_data('duplicate_event');
                $duplicate_post_id = 0;
                if (is_array($duplicate_data)) {
                    $duplicate_post_id = absint((int) ($duplicate_data['duplicate_post_id'] ?? 0));
                }
                if ($duplicate_post_id <= 0) {
                    $duplicate_post_id = $duplicate_event_id;
                }
                $this->append_runtime_log('info', 'Extension event skipped as duplicate', [
                    'duplicate_post_id' => (string) $duplicate_post_id,
                    'source_url' => esc_url_raw((string) ($raw_payload['source_url'] ?? ($payload['source_url'] ?? ''))),
                ]);

                return rest_ensure_response([
                    'ok' => true,
                    'skipped_duplicate' => true,
                    'post_id' => $duplicate_post_id,
                    'status' => $duplicate_post_id > 0 ? (string) get_post_status($duplicate_post_id) : '',
                    'permalink' => $duplicate_post_id > 0 ? (string) get_permalink($duplicate_post_id) : '',
                    'edit_url' => $duplicate_post_id > 0 ? (string) $this->get_frontend_event_edit_url($duplicate_post_id) : '',
                    'duplicate_candidate' => $duplicate_post_id,
                    'message' => __('Duplicate detected. Existing listing was kept and import was skipped.', 'nexdirectory'),
                ]);
            }
            return new WP_Error(
                (string) $created->get_error_code(),
                (string) $created->get_error_message(),
                ['status' => 400]
            );
        }

        $post_id = absint((int) $created);
        if ($post_id <= 0) {
            return new WP_Error('nexdirectory_extension_import_failed', __('Event import failed unexpectedly.', 'nexdirectory'), ['status' => 500]);
        }

        update_post_meta($post_id, 'nexdirectory_import_source', 'chrome_extension');
        update_post_meta($post_id, 'nexdirectory_generated_by_plugin', '1');
        update_post_meta($post_id, 'nexdirectory_autogenerated_event', '1');
        $source_url = esc_url_raw((string) ($raw_payload['source_url'] ?? ($payload['source_url'] ?? '')));
        update_post_meta($post_id, 'nexdirectory_import_source_url', $source_url);
        update_post_meta($post_id, 'nexdirectory_import_payload_hash', hash('sha256', wp_json_encode($raw_payload)));
        $mark_claimable = (string) get_option('nexdirectory_extension_import_mark_claimable', '1') === '1';
        update_post_meta($post_id, 'nexdirectory_claimable', $mark_claimable ? '1' : '0');
        $this->apply_extension_import_enhanced_fields($post_id, $row, $payload);

        $post_status = (string) get_post_status($post_id);
        if ($post_status === 'pending') {
            $this->notify_admin_submission($post_id);
            $this->notify_author_status(
                $post_id,
                'pending',
                sprintf(
                    /* translators: %s: Placeholder value. */
                    __('Submitted via %s Chrome extension.', 'nexdirectory'),
                    $this->get_white_label_brand_name()
                )
            );
        }

        $claim_page_url = $this->get_platform_page_url('nexdirectory_claim_listing_page_url', 'claim-listing', home_url('/claim-listing/'));
        $claim_url = $mark_claimable ? nexdirectory_add_query_arg('event_id', $post_id, $claim_page_url) : '';
        $owner_notification_sent = false;
        $owner_notification_email = '';
        if ($this->should_send_extension_owner_notification($extension_preferences)) {
            $owner_notification_email = sanitize_email((string) ($extension_preferences['notify_potential_owner_email'] ?? ''));
            if (!is_email($owner_notification_email)) {
                $owner_notification_email = sanitize_email((string) ($row['contact_email'] ?? ''));
            }
            $owner_notification_sent = $this->send_extension_owner_notification(
                $post_id,
                $owner_notification_email,
                $claim_url,
                $source_url
            );
        }
        $this->append_runtime_log('info', 'Extension event imported', [
            'post_id' => (string) $post_id,
            'status' => $post_status,
            'author_id' => (string) $author_id,
            'duplicate_candidate' => (string) $duplicate_event_id,
            'owner_notification_sent' => $owner_notification_sent ? '1' : '0',
            'owner_notification_email' => (string) $owner_notification_email,
        ]);

        return rest_ensure_response([
            'ok' => true,
            'post_id' => $post_id,
            'status' => $post_status,
            'permalink' => get_permalink($post_id),
            'edit_url' => $this->get_frontend_event_edit_url($post_id),
            'claimable' => $mark_claimable,
            'claim_url' => $claim_url,
            'owner_notification_sent' => $owner_notification_sent,
            'owner_notification_email' => $owner_notification_email,
            'duplicate_candidate' => $duplicate_event_id,
            'message' => __('Event imported successfully.', 'nexdirectory'),
        ]);
    }


    private function should_send_extension_owner_notification(array $extension_preferences = []) {
        if ((string) get_option('nexdirectory_notify_extension_potential_owner', '0') !== '1') {
            return false;
        }
        if ((string) get_option('nexdirectory_extension_notify_potential_owner_default', '0') !== '1') {
            return false;
        }
        if (array_key_exists('notify_potential_owner_default', $extension_preferences)) {
            $value = $extension_preferences['notify_potential_owner_default'];
            if (is_bool($value)) {
                return $value;
            }
            $normalized = strtolower(trim((string) $value));
            return in_array($normalized, ['1', 'true', 'yes', 'on'], true);
        }

        // Fallback behavior when extension did not explicitly provide a per-import preference:
        // respect admin-level toggle + default preference.
        return true;
    }


    private function send_extension_owner_notification($post_id, $recipient_email, $claim_url = '', $source_url = '') {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return false;
        }

        $recipient_email = sanitize_email((string) $recipient_email);
        if (!is_email($recipient_email)) {
            return false;
        }

        $source_url = esc_url_raw((string) $source_url);
        $claim_url = esc_url_raw((string) $claim_url);
        $listing_url = (string) get_permalink($post_id);
        $site_name = sanitize_text_field((string) $this->get_white_label_brand_name());

        $subject_template = (string) get_option(
            'nexdirectory_email_subject_extension_potential_owner',
            $this->get_default_event_email_template_value('nexdirectory_email_subject_extension_potential_owner')
        );
        $body_template = (string) get_option(
            'nexdirectory_email_body_extension_potential_owner',
            $this->get_default_event_email_template_value('nexdirectory_email_body_extension_potential_owner')
        );

        $extra_tokens = [
            '{claim_url}' => $claim_url,
            '{source_url}' => $source_url,
            '{site_name}' => $site_name,
            '{listing_url}' => $listing_url,
        ];

        $subject = trim((string) $this->render_event_email_subject($subject_template, $post_id, '', $extra_tokens));
        if ($subject === '') {
            $subject = sprintf(
                /* translators: %s: Placeholder value. */
                __('Your listing is live on %s (claim available)', 'nexdirectory'),
                $site_name
            );
        }
        $custom_body_html = $this->render_event_email_body($body_template, $post_id, '', '', $extra_tokens);
        $fallback_text = sprintf(
            "A listing matching your event appears on %s.\n\nTitle: %s\nView listing: %s\nClaim listing: %s\nSource page: %s",
            $site_name,
            get_the_title($post_id),
            $listing_url,
            $claim_url,
            $source_url
        );

        $sent = (bool) $this->send_platform_email($recipient_email, $subject, '', [
            'post_id' => $post_id,
            'event_title' => get_the_title($post_id),
            'event_url' => $listing_url,
            'custom_body_html' => $custom_body_html,
            'fallback_text' => $fallback_text,
        ]);
        $this->append_runtime_log($sent ? 'info' : 'warning', 'Extension owner notification email ' . ($sent ? 'sent' : 'failed'), [
            'post_id' => (string) $post_id,
            'recipient' => (string) $recipient_email,
            'transport' => (string) get_option('nexdirectory_smtp_enabled', '0') === '1' ? 'smtp' : 'wp_mail',
        ]);
        return $sent;
    }


    public function rest_search_users($request) {
        if (!$this->user_can_manage_organizers()) {
            return new WP_Error('nexdirectory_forbidden', __('Not allowed.', 'nexdirectory'), ['status' => 403]);
        }

        $search = sanitize_text_field((string) $request->get_param('q'));
        $limit = absint((int) $request->get_param('limit'));
        if ($limit < 1) {
            $limit = 100;
        }
        if ($limit > 200) {
            $limit = 200;
        }

        $users = get_users([
            'search' => $search !== '' ? '*' . $search . '*' : '',
            'search_columns' => ['user_login', 'user_email', 'display_name'],
            'number' => $limit,
            'orderby' => 'registered',
            'order' => 'DESC',
        ]);
        $ui_labels = $this->get_site_profile_ui_labels();

        return rest_ensure_response([
            'count' => is_array($users) ? count($users) : 0,
            'html' => $this->render_user_management_rows($users, $ui_labels),
        ]);
    }


    public function rest_save_filters($request) {
        if (!is_user_logged_in()) {
            return new WP_Error('nexdirectory_forbidden', __('You must be logged in.', 'nexdirectory'), ['status' => 403]);
        }

        $filters = [
            's' => sanitize_text_field((string) $request->get_param('s')),
            'event_type' => sanitize_text_field((string) $request->get_param('event_type')),
            'event_mode' => sanitize_text_field((string) $request->get_param('event_mode')),
            'event_price' => sanitize_text_field((string) $request->get_param('event_price')),
            'event_tag' => sanitize_text_field((string) $request->get_param('event_tag')),
            'city' => sanitize_text_field((string) $request->get_param('city')),
            'date_from' => sanitize_text_field((string) $request->get_param('date_from')),
            'official' => sanitize_text_field((string) $request->get_param('official')),
        ];
        if (!in_array((string) ($filters['official'] ?? ''), ['0', '1'], true)) {
            $filters['official'] = '';
        }
        update_user_meta(get_current_user_id(), 'nexdirectory_saved_filters', $filters);

        return rest_ensure_response([
            'saved' => $filters,
            'message' => __('Filters saved to your account.', 'nexdirectory'),
        ]);
    }


    public function rest_inbox_poll($request) {
        if (!is_user_logged_in()) {
            return new WP_Error('nexdirectory_forbidden', __('You must be logged in.', 'nexdirectory'), ['status' => 403]);
        }

        $user_id = (int) get_current_user_id();
        $last_seen_message_id = absint((int) $request->get_param('last_seen_message_id'));
        $latest = $this->get_inbox_latest_unread_for_user($user_id);
        $unread_count = $this->get_inbox_unread_count_for_user($user_id);

        $latest_message_id = absint((int) ($latest['id'] ?? 0));
        $thread_id = absint((int) ($latest['thread_id'] ?? 0));
        $post_id = absint((int) ($latest['post_id'] ?? 0));
        $sender_id = absint((int) ($latest['sender_id'] ?? 0));
        $sender_name = __('Member', 'nexdirectory');
        if ($sender_id > 0) {
            $sender = get_user_by('id', $sender_id);
            if ($sender instanceof WP_User) {
                $sender_name = sanitize_text_field((string) ($sender->display_name ?: $sender->user_login));
            }
        }
        $event_title = $post_id > 0 ? sanitize_text_field((string) get_the_title($post_id)) : '';
        $subject = sanitize_text_field((string) ($latest['subject'] ?? ''));
        if ($subject === '' && $event_title !== '') {
            /* translators: %s: Placeholder value. */
            $subject = sprintf(__('Message about %s', 'nexdirectory'), $event_title);
        }

        $has_new = $latest_message_id > 0 && $latest_message_id > $last_seen_message_id;
        $toast_message = $event_title !== ''
            /* translators: 1: Placeholder value 1, 2: Placeholder value 2. */
            ? sprintf(__('New message about %1$s from %2$s.', 'nexdirectory'), $event_title, $sender_name)
            /* translators: %s: Placeholder value. */
            : sprintf(__('New message from %s.', 'nexdirectory'), $sender_name);

        return rest_ensure_response([
            'ok' => true,
            'unread_count' => (int) $unread_count,
            'latest_message_id' => $latest_message_id,
            'thread_id' => $thread_id,
            'post_id' => $post_id,
            'subject' => $subject,
            'sender_name' => $sender_name,
            'event_title' => $event_title,
            'thread_url' => $thread_id > 0 ? $this->build_inbox_thread_url($thread_id, $latest_message_id) : $this->get_notifications_page_url(),
            'has_new' => $has_new,
            'toast_message' => $toast_message,
        ]);
    }

    /**
     * Reads or updates the current user's recipient-scoped notification feed.
     *
     * @param WP_REST_Request $request REST request.
     * @return WP_REST_Response|WP_Error
     */
    public function rest_notifications_feed($request) {
        if (!is_user_logged_in()) {
            return new WP_Error('nexdirectory_forbidden', __('You must be logged in.', 'nexdirectory'), ['status' => 403]);
        }

        $service = $this->get_account_notification_service();
        if (!$service) {
            return new WP_Error(
                'nexdirectory_notifications_unavailable',
                __('Notifications are temporarily unavailable.', 'nexdirectory'),
                ['status' => 503]
            );
        }

        $user_id = absint((int) get_current_user_id());
        if ($request->get_method() === 'POST') {
            $action = sanitize_key((string) $request->get_param('action'));
            if (!in_array($action, ['mark_read', 'mark_all_read'], true)) {
                return new WP_Error(
                    'nexdirectory_notifications_invalid_action',
                    __('Invalid notification action.', 'nexdirectory'),
                    ['status' => 400]
                );
            }

            $notification_ids = [];
            if ($action === 'mark_read') {
                $raw_ids = $request->get_param('ids');
                if (!is_array($raw_ids)) {
                    $raw_ids = [$request->get_param('id')];
                }
                foreach ((array) $raw_ids as $raw_id) {
                    if (!is_scalar($raw_id)) {
                        continue;
                    }
                    $clean_id = absint((int) $raw_id);
                    if ($clean_id > 0) {
                        $notification_ids[] = $clean_id;
                        if (count($notification_ids) >= 100) {
                            break;
                        }
                    }
                }
                $notification_ids = array_values(array_unique($notification_ids));
                if (empty($notification_ids)) {
                    return new WP_Error(
                        'nexdirectory_notifications_missing_ids',
                        __('Select at least one notification.', 'nexdirectory'),
                        ['status' => 400]
                    );
                }
            }

            $changed = $service->mark_notifications_read($user_id, $notification_ids);
            return rest_ensure_response([
                'ok' => true,
                'changed' => absint((int) $changed),
                'unread_count' => $service->get_notification_unread_count($user_id),
            ]);
        }

        $per_page = max(1, min(50, absint((int) $request->get_param('per_page'))));
        $before_id = absint((int) $request->get_param('before_id'));
        $items = $service->get_notifications_for_user($user_id, $per_page, $before_id);

        return rest_ensure_response([
            'ok' => true,
            'items' => $items,
            'unread_count' => $service->get_notification_unread_count($user_id),
            'next_before_id' => !empty($items) ? absint((int) ($items[count($items) - 1]['id'] ?? 0)) : 0,
        ]);
    }


    public function rest_rsvp_event($request) {
        if (!is_user_logged_in()) {
            return new WP_Error('nexdirectory_forbidden', __('Please log in to RSVP.', 'nexdirectory'), ['status' => 403]);
        }

        $post_id = absint((int) $request->get_param('post_id'));
        $intent = sanitize_key((string) $request->get_param('intent'));
        if ($intent === '') {
            $intent = 'join';
        }

        $result = $this->process_rsvp_action($post_id, $intent, get_current_user_id());
        if (is_wp_error($result)) {
            return new WP_Error(
                (string) $result->get_error_code(),
                (string) $result->get_error_message(),
                ['status' => 400]
            );
        }

        return rest_ensure_response($result);
    }


    public function rest_moderate_event($request) {
        if (!$this->user_can_moderate_events()) {
            return new WP_Error('nexdirectory_forbidden', __('Not allowed.', 'nexdirectory'), ['status' => 403]);
        }

        $post_id = absint((int) ($request->get_param('post_id') ?: $request->get_param('event_id')));
        $decision_raw = $request->get_param('decision');
        if ($decision_raw === null || $decision_raw === '') {
            $decision_raw = $request->get_param('moderation_action');
        }
        if (is_array($decision_raw)) {
            $decision_raw = end($decision_raw);
        }
        $decision = sanitize_key((string) $decision_raw);
        $note = nexdirectory_sanitize_textarea((string) $request->get_param('moderator_note'));
        if ($post_id <= 0 || !in_array($decision, ['approve', 'reject', 'request_changes'], true)) {
            $this->append_runtime_log('warning', 'Invalid moderation request via REST', [
                'post_id' => (string) $post_id,
                'decision' => (string) $decision,
                'user_id' => (string) get_current_user_id(),
            ]);
            return new WP_Error('nexdirectory_bad_request', __('Invalid moderation request.', 'nexdirectory'), ['status' => 400]);
        }

        $result = $this->apply_moderation_decision($post_id, $decision, $note);
        if (is_wp_error($result)) {
            return new WP_Error(
                (string) $result->get_error_code(),
                (string) $result->get_error_message(),
                ['status' => 400]
            );
        }

        return rest_ensure_response([
            'post_id' => $post_id,
            'decision' => $decision,
            'message' => (string) $result,
            'new_status' => (string) get_post_status($post_id),
            'pending_count' => (int) $this->get_pending_events_count(),
        ]);
    }


    private function parse_rest_post_ids($value) {
        if (is_string($value)) {
            $chunks = array_filter(array_map('trim', explode(',', $value)));
            $value = $chunks;
        }
        if (!is_array($value)) {
            return [];
        }
        $ids = array_map('absint', $value);
        $ids = array_values(array_unique(array_filter($ids)));
        return $ids;
    }


    public function rest_moderate_events_bulk($request) {
        if (!$this->user_can_moderate_events()) {
            return new WP_Error('nexdirectory_forbidden', __('Not allowed.', 'nexdirectory'), ['status' => 403]);
        }

        $decision_raw = $request->get_param('decision');
        if ($decision_raw === null || $decision_raw === '') {
            $decision_raw = $request->get_param('bulk_decision');
        }
        if (is_array($decision_raw)) {
            $decision_raw = end($decision_raw);
        }
        $decision = sanitize_key((string) $decision_raw);
        $note = nexdirectory_sanitize_textarea((string) $request->get_param('moderator_note'));
        $post_ids = $this->parse_rest_post_ids($request->get_param('post_ids'));
        if (empty($post_ids) || !in_array($decision, ['approve', 'reject', 'request_changes'], true)) {
            $this->append_runtime_log('warning', 'Invalid bulk moderation request via REST', [
                'decision' => (string) $decision,
                'post_ids' => (string) wp_json_encode($post_ids),
                'user_id' => (string) get_current_user_id(),
            ]);
            return new WP_Error('nexdirectory_bad_request', __('Select events and a valid action for bulk moderation.', 'nexdirectory'), ['status' => 400]);
        }

        $success = 0;
        $failed = 0;
        $errors = [];
        foreach ($post_ids as $post_id) {
            $result = $this->apply_moderation_decision((int) $post_id, $decision, $note);
            if (is_wp_error($result)) {
                $failed++;
                if (count($errors) < 10) {
                    $errors[] = [
                        'post_id' => (int) $post_id,
                        'message' => (string) $result->get_error_message(),
                    ];
                }
            } else {
                $success++;
            }
        }

        return rest_ensure_response([
            'decision' => $decision,
            'success' => $success,
            'failed' => $failed,
            'errors' => $errors,
            'message' => sprintf(
                /* translators: 1: Placeholder value 1, 2: Placeholder value 2. */
                __('Bulk moderation complete. Success: %1$d · Failed: %2$d.', 'nexdirectory'),
                (int) $success,
                (int) $failed
            ),
            'pending_count' => (int) $this->get_pending_events_count(),
        ]);
    }


    public function handle_manage_user_role() {
        $is_ajax_request = (string) wp_unslash($_POST['nexdirectory_ajax'] ?? '') === '1';
        $redirect_to_raw = isset($_POST['redirect_to']) ? esc_url_raw((string) wp_unslash($_POST['redirect_to'])) : '';
        $redirect_to = $this->get_safe_redirect_url($redirect_to_raw, wp_get_referer() ?: home_url('/'));
        $respond_error = function ($message, $status_code = 400) use ($is_ajax_request, $redirect_to) {
            if ($is_ajax_request) {
                wp_send_json_error(['message' => (string) $message], (int) $status_code);
            }
            $this->redirect_with_message($redirect_to, (string) $message, 'error');
        };
        $respond_success = function ($message, array $payload = []) use ($is_ajax_request, $redirect_to) {
            if ($is_ajax_request) {
                wp_send_json_success(array_merge(['message' => (string) $message], $payload));
            }
            $this->redirect_with_message($redirect_to, (string) $message);
        };

        if (!$this->user_can_manage_organizers()) {
            $respond_error(__('Not allowed.', 'nexdirectory'), 403);
        }

        if (!isset($_POST['nexdirectory_manage_user_nonce']) || !wp_verify_nonce(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_manage_user_nonce'])), 'nexdirectory_manage_user_role')) {
            $respond_error(__('Invalid request.', 'nexdirectory'), 400);
        }
        $user_id = isset($_POST['user_id']) ? absint((int) wp_unslash($_POST['user_id'])) : 0;
        $new_role = isset($_POST['role']) ? sanitize_key(wp_unslash($_POST['role'])) : '';
        if (!$user_id || !in_array($new_role, ['subscriber', 'event_submitter'], true)) {
            $respond_error(__('Invalid request.', 'nexdirectory'), 400);
        }

        $user = get_user_by('id', $user_id);
        if (!$user instanceof WP_User || in_array('administrator', (array) $user->roles, true)) {
            $respond_error(__('Invalid target user.', 'nexdirectory'), 400);
        }
        $current_roles = array_values(array_filter(array_map('sanitize_key', (array) $user->roles)));
        if (array_diff($current_roles, ['subscriber', 'event_submitter'])) {
            $respond_error(
                __('This WordPress role is managed outside the directory workspace and was not changed.', 'nexdirectory'),
                403
            );
        }

        $user->set_role($new_role);
        $respond_success(
            __('User role updated.', 'nexdirectory'),
            [
                'user_id' => (int) $user_id,
                'role' => (string) $new_role,
                'role_label' => $new_role === 'event_submitter'
                    ? __('Event Submitter', 'nexdirectory')
                    : __('Subscriber', 'nexdirectory'),
            ]
        );
    }


    public function handle_toggle_verified() {
        check_admin_referer('nexdirectory_toggle_verified', 'nexdirectory_toggle_verified_nonce');

        $is_ajax_request = (string) wp_unslash($_POST['nexdirectory_ajax'] ?? '') === '1';
        $redirect_to = $this->get_safe_redirect_url(esc_url_raw((string) wp_unslash($_POST['redirect_to'] ?? '')), wp_get_referer() ?: home_url('/'));
        $respond_error = function ($message, $status_code = 400) use ($is_ajax_request, $redirect_to) {
            if ($is_ajax_request) {
                wp_send_json_error(['message' => (string) $message], (int) $status_code);
            }
            $this->redirect_with_message($redirect_to, (string) $message, 'error');
        };
        $respond_success = function ($message, array $payload = []) use ($is_ajax_request, $redirect_to) {
            if ($is_ajax_request) {
                wp_send_json_success(array_merge(['message' => (string) $message], $payload));
            }
            $this->redirect_with_message($redirect_to, (string) $message);
        };

        if (!$this->user_can_manage_organizers()) {
            $respond_error(__('Not allowed.', 'nexdirectory'), 403);
        }
        $user_id = isset($_POST['user_id']) ? absint((int) wp_unslash($_POST['user_id'])) : 0;
        if ($user_id <= 0) {
            $respond_error(__('Invalid user.', 'nexdirectory'), 400);
        }
        $requested_verified = sanitize_key((string) wp_unslash($_POST['verified'] ?? ''));
        $current = get_user_meta($user_id, 'nexdirectory_verified_organizer', true) === '1';
        $target_verified = $current;
        if ($requested_verified !== '') {
            $target_verified = in_array($requested_verified, ['1', 'true', 'yes'], true);
        } else {
            $target_verified = !$current;
        }
        update_user_meta($user_id, 'nexdirectory_verified_organizer', $target_verified ? '1' : '0');

        $respond_success(
            $target_verified ? __('Organizer verified.', 'nexdirectory') : __('Organizer badge removed.', 'nexdirectory'),
            [
                'user_id' => (int) $user_id,
                'verified' => (bool) $target_verified,
            ]
        );
    }


    public function handle_toggle_user_edit_trust() {
        check_admin_referer('nexdirectory_toggle_user_edit_trust', 'nexdirectory_toggle_user_edit_trust_nonce');
        $is_ajax_request = (string) wp_unslash($_POST['nexdirectory_ajax'] ?? '') === '1';
        $redirect_to = $this->get_safe_redirect_url(esc_url_raw((string) wp_unslash($_POST['redirect_to'] ?? '')), wp_get_referer() ?: home_url('/'));
        $respond_error = function ($message, $status_code = 400) use ($is_ajax_request, $redirect_to) {
            if ($is_ajax_request) {
                wp_send_json_error(['message' => (string) $message], (int) $status_code);
            }
            $this->redirect_with_message($redirect_to, (string) $message, 'error');
        };
        $respond_success = function ($message, array $payload = []) use ($is_ajax_request, $redirect_to) {
            if ($is_ajax_request) {
                wp_send_json_success(array_merge(['message' => (string) $message], $payload));
            }
            $this->redirect_with_message($redirect_to, (string) $message);
        };

        if (!$this->user_can_manage_organizers()) {
            $respond_error(__('Not allowed.', 'nexdirectory'), 403);
        }
        $user_id = isset($_POST['user_id']) ? absint((int) wp_unslash($_POST['user_id'])) : 0;
        if (!$user_id) {
            $respond_error(__('Invalid user.', 'nexdirectory'), 400);
        }
        $user = get_user_by('id', $user_id);
        if (!$user instanceof WP_User || in_array('administrator', (array) $user->roles, true)) {
            $respond_error(__('Invalid target user.', 'nexdirectory'), 400);
        }

        $is_trusted = (string) get_user_meta($user_id, 'nexdirectory_trusted_editor', true) === '1';
        $requested_state = sanitize_key((string) wp_unslash($_POST['trusted'] ?? ''));
        if (in_array($requested_state, ['trusted', 'require_approval'], true)) {
            $target_trusted = $requested_state === 'trusted';
        } else {
            $target_trusted = !$is_trusted;
        }

        update_user_meta($user_id, 'nexdirectory_trusted_editor', $target_trusted ? '1' : '0');

        $respond_success(
            $target_trusted
                ? __('Trusted edit bypass enabled. This user can update published listings without admin re-approval.', 'nexdirectory')
                : __('Trusted edit bypass removed. This user now follows the global edit approval policy.', 'nexdirectory'),
            [
                'user_id' => (int) $user_id,
                'trusted' => $target_trusted ? 'trusted' : 'require_approval',
            ]
        );
    }


    public function handle_manage_user_note() {
        check_admin_referer('nexdirectory_manage_user_note', 'nexdirectory_manage_user_note_nonce');
        $is_ajax_request = (string) wp_unslash($_POST['nexdirectory_ajax'] ?? '') === '1';
        $redirect_to = $this->get_safe_redirect_url(esc_url_raw((string) wp_unslash($_POST['redirect_to'] ?? '')), wp_get_referer() ?: home_url('/'));
        $respond_error = function ($message, $status_code = 400) use ($is_ajax_request, $redirect_to) {
            if ($is_ajax_request) {
                wp_send_json_error(['message' => (string) $message], (int) $status_code);
            }
            $this->redirect_with_message($redirect_to, (string) $message, 'error');
        };
        $respond_success = function ($message, array $payload = []) use ($is_ajax_request, $redirect_to) {
            if ($is_ajax_request) {
                wp_send_json_success(array_merge(['message' => (string) $message], $payload));
            }
            $this->redirect_with_message($redirect_to, (string) $message);
        };

        if (!$this->user_can_manage_organizers()) {
            $respond_error(__('Not allowed.', 'nexdirectory'), 403);
        }

        $user_id = isset($_POST['user_id']) ? absint((int) wp_unslash($_POST['user_id'])) : 0;
        if ($user_id <= 0) {
            $respond_error(__('Invalid user.', 'nexdirectory'), 400);
        }
        $user = get_user_by('id', $user_id);
        if (!$user instanceof WP_User) {
            $respond_error(__('Invalid target user.', 'nexdirectory'), 400);
        }

        $admin_note = sanitize_textarea_field((string) wp_unslash($_POST['admin_note'] ?? ''));
        if ($admin_note === '') {
            delete_user_meta($user_id, 'nexdirectory_admin_user_note');
            $message = __('Admin note removed.', 'nexdirectory');
        } else {
            update_user_meta($user_id, 'nexdirectory_admin_user_note', $admin_note);
            $message = __('Admin note saved.', 'nexdirectory');
        }

        $respond_success(
            $message,
            [
                'user_id' => (int) $user_id,
                'admin_note' => (string) $admin_note,
            ]
        );
    }


    public function handle_manage_user_event() {
        check_admin_referer('nexdirectory_manage_user_event', 'nexdirectory_manage_user_event_nonce');
        $is_ajax_request = (string) wp_unslash($_POST['nexdirectory_ajax'] ?? '') === '1';
        $redirect = $this->get_safe_redirect_url(esc_url_raw((string) wp_unslash($_POST['redirect_to'] ?? '')), wp_get_referer() ?: home_url('/'));
        $respond_error = function ($message, $status_code = 400) use ($is_ajax_request, $redirect) {
            if ($is_ajax_request) {
                wp_send_json_error(['message' => (string) $message], (int) $status_code);
            }
            $this->redirect_with_message($redirect, (string) $message, 'error');
        };
        $respond_success = function ($message, array $payload = []) use ($is_ajax_request, $redirect) {
            if ($is_ajax_request) {
                wp_send_json_success(array_merge(['message' => (string) $message], $payload));
            }
            $this->redirect_with_message($redirect, (string) $message);
        };

        if (!$this->user_can_moderate_events()) {
            $respond_error(__('Not allowed.', 'nexdirectory'), 403);
        }
        $ui_labels = $this->get_site_profile_ui_labels();
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $singular_lower = sanitize_text_field((string) ($ui_labels['singular_lower'] ?? strtolower((string) $singular_label)));

        $user_id = isset($_POST['user_id']) ? absint((int) wp_unslash($_POST['user_id'])) : 0;
        $post_id = isset($_POST['event_id']) ? absint((int) wp_unslash($_POST['event_id'])) : 0;
        $intent = sanitize_key((string) wp_unslash($_POST['intent'] ?? ''));
        if (!$user_id || !$post_id || !in_array($intent, ['open_event', 'publish_event', 'unpublish_event', 'email_owner'], true)) {
            /* translators: %s: Placeholder value. */
            $respond_error(sprintf(__('Invalid %s management request.', 'nexdirectory'), $singular_lower), 400);
        }
        if (get_post_type($post_id) !== self::POST_TYPE) {
            /* translators: %s: Placeholder value. */
            $respond_error(sprintf(__('Selected post is not a valid %s.', 'nexdirectory'), $singular_lower), 400);
        }
        $author_id = (int) get_post_field('post_author', $post_id);
        if ($author_id !== $user_id) {
            /* translators: %s: Placeholder value. */
            $respond_error(sprintf(__('Selected %s does not belong to the selected user.', 'nexdirectory'), $singular_lower), 400);
        }

        if ($intent === 'open_event') {
            $open_url = (string) get_permalink($post_id);
            if ((string) get_post_status($post_id) !== 'publish') {
                $preview_url = get_preview_post_link($post_id);
                if (is_string($preview_url) && $preview_url !== '') {
                    $open_url = $preview_url;
                }
            }
            if ($is_ajax_request) {
                $respond_success(
                    /* translators: %s: Placeholder value. */
                    sprintf(__('%s link ready.', 'nexdirectory'), $singular_label),
                    [
                        'post_id' => (int) $post_id,
                        'intent' => 'open_event',
                        'open_url' => (string) $open_url,
                    ]
                );
            }
            $this->safe_redirect_or_fallback($open_url);
        }

        if ($intent === 'publish_event') {
            $result = wp_update_post([
                'ID' => $post_id,
                'post_status' => 'publish',
            ]);
            if (is_wp_error($result)) {
                $this->append_runtime_log('error', 'User management publish failed', [
                    'post_id' => (string) $post_id,
                    'user_id' => (string) $user_id,
                    'error' => (string) $result->get_error_message(),
                ]);
                $respond_error(__('Could not publish the selected listing. Please try again.', 'nexdirectory'), 500);
            }
            $respond_success(
                /* translators: %s: Placeholder value. */
                sprintf(__('%s published.', 'nexdirectory'), $singular_label),
                [
                    'post_id' => (int) $post_id,
                    'intent' => 'publish_event',
                    'new_status' => 'publish',
                    'status_label' => ucfirst('publish'),
                ]
            );
        }

        if ($intent === 'unpublish_event') {
            $result = wp_update_post([
                'ID' => $post_id,
                'post_status' => 'draft',
            ]);
            if (is_wp_error($result)) {
                $this->append_runtime_log('error', 'User management unpublish failed', [
                    'post_id' => (string) $post_id,
                    'user_id' => (string) $user_id,
                    'error' => (string) $result->get_error_message(),
                ]);
                $respond_error(__('Could not update the selected listing. Please try again.', 'nexdirectory'), 500);
            }
            /* translators: %s: Placeholder value. */
            $this->notify_author_status($post_id, 'draft', sprintf(__('%s unpublished by administrator.', 'nexdirectory'), $singular_label));
            $respond_success(
                /* translators: %s: Placeholder value. */
                sprintf(__('%s moved to draft.', 'nexdirectory'), $singular_label),
                [
                    'post_id' => (int) $post_id,
                    'intent' => 'unpublish_event',
                    'new_status' => 'draft',
                    'status_label' => ucfirst('draft'),
                ]
            );
        }

        $owner = get_user_by('id', $user_id);
        if (!$owner || !is_email($owner->user_email)) {
            /* translators: %s: Placeholder value. */
            $respond_error(sprintf(__('%s owner email is invalid.', 'nexdirectory'), $singular_label), 400);
        }
        $subject = sanitize_text_field((string) wp_unslash($_POST['admin_subject'] ?? ''));
        if ($subject === '') {
            /* translators: 1: Placeholder value 1, 2: Placeholder value 2. */
            $subject = sprintf(__('Admin message about your %1$s: %2$s', 'nexdirectory'), $singular_lower, get_the_title($post_id));
        }
        $admin_note = sanitize_textarea_field((string) wp_unslash($_POST['admin_message'] ?? ''));
        if ($admin_note === '') {
            /* translators: %s: Placeholder value. */
            $admin_note = sprintf(__('Your %s has an admin update. Please review your listing details.', 'nexdirectory'), $singular_lower);
        }
        $admin_user = wp_get_current_user();
        $headers = [];
        if ($admin_user instanceof WP_User && is_email($admin_user->user_email)) {
            $headers[] = 'Reply-To: ' . $admin_user->display_name . ' <' . $admin_user->user_email . '>';
        }
        $fallback_text = sprintf(
            "Hello %s,\n\n%s\n\n%s: %s\nLink: %s",
            $owner->display_name ?: $owner->user_login,
            $admin_note,
            $singular_label,
            get_the_title($post_id),
            get_permalink($post_id)
        );
        $sent = $this->send_platform_email($owner->user_email, $subject, '', [
            'fallback_text' => $fallback_text,
        ], $headers);
        if ($sent) {
            $respond_success(
                /* translators: %s: Placeholder value. */
                sprintf(__('Message emailed to %s owner.', 'nexdirectory'), $singular_lower),
                [
                    'post_id' => (int) $post_id,
                    'intent' => 'email_owner',
                    'sent' => true,
                ]
            );
        }
        /* translators: %s: Placeholder value. */
        $respond_error(sprintf(__('Could not send email to %s owner.', 'nexdirectory'), $singular_lower), 500);
    }


    public function render_moderation_queue($atts = []) {
        $atts = shortcode_atts([
            'pending_limit' => 100,
            'reports_limit' => 30,
            'admin_context' => 0,
        ], (array) $atts, 'nexdirectory_moderation_queue');
        $pending_limit = max(1, (int) $atts['pending_limit']);
        $reports_limit = max(1, (int) $atts['reports_limit']);
        $admin_context = (string) $atts['admin_context'] === '1';

        if (!$this->user_can_moderate_events()) {
            return '<p>' . esc_html__('Admin/editor access required.', 'nexdirectory') . '</p>';
        }

        $pending = new WP_Query([
            'post_type' => self::POST_TYPE,
            'post_status' => 'pending',
            'posts_per_page' => $pending_limit,
            'orderby' => 'date',
            'order' => 'ASC',
        ]);

        $reports = get_comments([
            'type' => 'nexdirectory_report',
            'status' => 'approve',
            'number' => $reports_limit,
            'orderby' => 'comment_date_gmt',
            'order' => 'DESC',
        ]);
        $claims = get_comments([
            'type' => 'nexdirectory_claim',
            'status' => 'hold',
            'number' => $reports_limit,
            'orderby' => 'comment_date_gmt',
            'order' => 'ASC',
            'meta_query' => [
                'relation' => 'OR',
                [
                    'key' => 'nexdirectory_claim_status',
                    'value' => 'pending',
                ],
                [
                    'key' => 'nexdirectory_claim_status',
                    'compare' => 'NOT EXISTS',
                ],
            ],
        ]);
        $ui_labels = $this->get_site_profile_ui_labels();

        $output = $this->render_frontend_template('moderation-queue.php', [
            'pending' => $pending,
            'reports' => $reports,
            'claims' => $claims,
            'ui_labels' => $ui_labels,
            'admin_context' => $admin_context,
        ]);
        wp_reset_postdata();
        return $output;
    }


    public function handle_moderate_event() {
        if (!$this->user_can_moderate_events()) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        check_admin_referer('nexdirectory_moderate_event', 'nexdirectory_moderate_nonce');
        $post_id = isset($_POST['post_id'])
            ? absint((int) wp_unslash($_POST['post_id']))
            : (isset($_POST['event_id']) ? absint((int) wp_unslash($_POST['event_id'])) : 0);
        if ($post_id <= 0 && isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
            $post_ids = array_values(array_filter(array_map('absint', (array) wp_unslash($_POST['post_ids']))));
            if (!empty($post_ids[0])) {
                $post_id = (int) $post_ids[0];
            }
        }
        $decision = sanitize_key((string) wp_unslash($_POST['moderation_action'] ?? ''));
        if ($decision === '') {
            $decision = sanitize_key((string) wp_unslash($_POST['decision'] ?? ''));
        }
        if ($decision === '') {
            $decision = sanitize_key((string) wp_unslash($_POST['bulk_decision'] ?? ''));
        }
        if ($decision === '') {
            foreach (['approve', 'reject', 'request_changes'] as $decision_key) {
                if (!empty($_POST[$decision_key])) {
                    $decision = $decision_key;
                    break;
                }
            }
        }
        $note = sanitize_textarea_field((string) wp_unslash($_POST['moderator_note'] ?? ''));
        if (!$post_id || !in_array($decision, ['approve', 'reject', 'request_changes'], true)) {
            $this->append_runtime_log('warning', 'Invalid moderation request via form', [
                'post_id' => (string) $post_id,
                'decision' => (string) $decision,
                'user_id' => (string) get_current_user_id(),
            ]);
            $this->redirect_with_message(
                wp_get_referer() ?: home_url('/'),
                __('Invalid moderation request. Please select a valid action and event.', 'nexdirectory'),
                'error'
            );
        }
        $result = $this->apply_moderation_decision($post_id, $decision, $note);
        if (is_wp_error($result)) {
            $this->append_runtime_log('error', 'Moderation decision failed', [
                'post_id' => (string) $post_id,
                'decision' => (string) $decision,
                'error' => (string) $result->get_error_message(),
            ]);
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), $result->get_error_message(), 'error');
        }
        $this->redirect_with_message(wp_get_referer() ?: home_url('/'), (string) $result);
    }


    public function handle_moderate_events_bulk() {
        if (!$this->user_can_moderate_events()) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        if (!isset($_POST['nexdirectory_moderate_bulk_nonce']) || !wp_verify_nonce(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_moderate_bulk_nonce'])), 'nexdirectory_moderate_events_bulk')) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'));
        }

        $decision = sanitize_key((string) wp_unslash($_POST['bulk_decision'] ?? ''));
        if ($decision === '') {
            $decision = sanitize_key((string) wp_unslash($_POST['decision'] ?? ''));
        }
        if ($decision === '') {
            $decision = sanitize_key((string) wp_unslash($_POST['moderation_action'] ?? ''));
        }
        $note = sanitize_textarea_field((string) wp_unslash($_POST['moderator_note'] ?? ''));
        $post_ids = isset($_POST['post_ids']) && is_array($_POST['post_ids']) ? array_map('absint', (array) wp_unslash($_POST['post_ids'])) : [];
        $post_ids = array_values(array_unique(array_filter($post_ids)));

        if (empty($post_ids) || !in_array($decision, ['approve', 'reject', 'request_changes'], true)) {
            $this->append_runtime_log('warning', 'Invalid bulk moderation request via form', [
                'decision' => (string) $decision,
                'post_ids' => (string) wp_json_encode($post_ids),
                'user_id' => (string) get_current_user_id(),
            ]);
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Select events and a valid action for bulk moderation.', 'nexdirectory'), 'error');
        }

        $success = 0;
        $failed = 0;
        foreach ($post_ids as $post_id) {
            $result = $this->apply_moderation_decision((int) $post_id, $decision, $note);
            if (is_wp_error($result)) {
                $failed++;
            } else {
                $success++;
            }
        }

        $message = sprintf(
            /* translators: 1: Placeholder value 1, 2: Placeholder value 2. */
            __('Bulk moderation complete. Success: %1$d · Failed: %2$d.', 'nexdirectory'),
            (int) $success,
            (int) $failed
        );
        $this->redirect_with_message(
            wp_get_referer() ?: home_url('/'),
            $message,
            $failed > 0 ? 'error' : 'success'
        );
    }


    private function is_event_claimable($post_id) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            return false;
        }
        if ((string) get_option('nexdirectory_enable_claim_listings', '0') !== '1') {
            return false;
        }
        $flag = (string) get_post_meta($post_id, 'nexdirectory_claimable', true);
        if ($flag === '1') {
            return true;
        }
        if ($flag === '0') {
            return false;
        }

        $owner_id = (int) get_post_field('post_author', $post_id);
        return $owner_id > 0 && user_can($owner_id, 'manage_options');
    }


    private function get_user_claim_status_for_event($post_id, $user_id) {
        $post_id = absint((int) $post_id);
        $user_id = absint((int) $user_id);
        if ($post_id <= 0 || $user_id <= 0) {
            return '';
        }
        $claims = get_comments([
            'post_id' => $post_id,
            'user_id' => $user_id,
            'type' => 'nexdirectory_claim',
            'orderby' => 'comment_date_gmt',
            'order' => 'DESC',
            'number' => 1,
            'status' => 'all',
        ]);
        if (empty($claims) || !isset($claims[0]) || !$claims[0] instanceof WP_Comment) {
            return '';
        }
        $status = (string) get_comment_meta((int) $claims[0]->comment_ID, 'nexdirectory_claim_status', true);
        if ($status === '') {
            $status = (string) $claims[0]->comment_approved === '1' ? 'approved' : 'pending';
        }
        return sanitize_key($status);
    }

    private function get_listing_claim_lock_name($post_id) {
        global $wpdb;

        $scope = (string) ($wpdb->dbname ?? '') . '|' . (string) $wpdb->prefix . '|' . home_url('/');
        $scope_hash = substr(hash('sha256', $scope), 0, 12);
        $blog_id = substr((string) absint((int) get_current_blog_id()), 0, 10);
        $post_id = substr((string) absint((int) $post_id), 0, 20);

        return 'nexdir_claim_' . $scope_hash . '_' . $blog_id . '_' . $post_id;
    }


    private function acquire_listing_claim_lock($post_id, $timeout = 3) {
        global $wpdb;

        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return false;
        }

        $lock_name = $this->get_listing_claim_lock_name($post_id);
        $timeout = max(0, min(10, (int) $timeout));
        $result = $wpdb->get_var(
            $wpdb->prepare(
                'SELECT GET_LOCK(%s, %d)',
                $lock_name,
                $timeout
            )
        );

        return (string) $result === '1';
    }


    private function release_listing_claim_lock($post_id) {
        global $wpdb;

        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return;
        }

        $wpdb->get_var(
            $wpdb->prepare(
                'SELECT RELEASE_LOCK(%s)',
                $this->get_listing_claim_lock_name($post_id)
            )
        );
    }

    private function refresh_listing_claim_caches($post_id, array $comment_ids = []) {
        $post_id = absint((int) $post_id);
        if ($post_id > 0) {
            clean_post_cache($post_id);
            wp_cache_delete($post_id, 'post_meta');
        }

        foreach (array_values(array_unique(array_filter(array_map('absint', $comment_ids)))) as $comment_id) {
            clean_comment_cache($comment_id);
            wp_cache_delete($comment_id, 'comment_meta');
        }
    }


    private function begin_listing_claim_transaction() {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- A short transaction makes the multi-table ownership transfer atomic.
        return $wpdb->query('START TRANSACTION') !== false;
    }


    private function commit_listing_claim_transaction() {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Completes the atomic claim state transition.
        return $wpdb->query('COMMIT') !== false;
    }


    private function rollback_listing_claim_transaction() {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reverts a partial claim state transition.
        $wpdb->query('ROLLBACK');
    }


    private function set_listing_claim_terminal_state($comment_id, $status, $reviewer_id = 0, $review_note = null) {
        $comment_id = absint((int) $comment_id);
        $status = sanitize_key((string) $status);
        $reviewer_id = absint((int) $reviewer_id);
        if ($comment_id <= 0 || !in_array($status, ['approved', 'rejected', 'superseded'], true)) {
            return false;
        }

        $comment = get_comment($comment_id);
        if (!$comment instanceof WP_Comment || (string) $comment->comment_type !== 'nexdirectory_claim') {
            return false;
        }
        if ((string) $comment->comment_approved !== '0') {
            $privacy_result = wp_set_comment_status($comment_id, 'hold', true);
            if (is_wp_error($privacy_result) || $privacy_result === false) {
                return false;
            }
        }

        $reviewed_at = current_time('mysql');
        $review_note = $review_note === null ? '' : sanitize_textarea_field((string) $review_note);
        update_comment_meta($comment_id, 'nexdirectory_claim_status', $status);
        update_comment_meta($comment_id, 'nexdirectory_claim_reviewed_at', $reviewed_at);
        if ($reviewer_id > 0) {
            update_comment_meta($comment_id, 'nexdirectory_claim_reviewed_by', (string) $reviewer_id);
        } else {
            delete_comment_meta($comment_id, 'nexdirectory_claim_reviewed_by');
        }
        if (trim($review_note) === '') {
            delete_comment_meta($comment_id, 'nexdirectory_claim_review_note');
        } else {
            update_comment_meta($comment_id, 'nexdirectory_claim_review_note', $review_note);
        }

        $this->refresh_listing_claim_caches(0, [$comment_id]);
        $comment = get_comment($comment_id);
        $stored_reviewer_id = absint((int) get_comment_meta($comment_id, 'nexdirectory_claim_reviewed_by', true));
        $stored_review_note = (string) get_comment_meta($comment_id, 'nexdirectory_claim_review_note', true);
        return $comment instanceof WP_Comment
            && (string) $comment->comment_type === 'nexdirectory_claim'
            && (string) $comment->comment_approved === '0'
            && sanitize_key((string) get_comment_meta($comment_id, 'nexdirectory_claim_status', true)) === $status
            && (string) get_comment_meta($comment_id, 'nexdirectory_claim_reviewed_at', true) === $reviewed_at
            && $stored_reviewer_id === $reviewer_id
            && $stored_review_note === $review_note;
    }


    private function supersede_other_pending_listing_claims($event_id, $winner_comment_id, $reviewer_id = 0) {
        $event_id = absint((int) $event_id);
        $winner_comment_id = absint((int) $winner_comment_id);
        $reviewer_id = absint((int) $reviewer_id);
        $result = [
            'comment_ids' => [],
            'user_ids' => [],
        ];

        $other_claims = get_comments([
            'post_id' => $event_id,
            'type' => 'nexdirectory_claim',
            'status' => 'all',
            'number' => 0,
            'orderby' => 'comment_date_gmt',
            'order' => 'ASC',
        ]);
        foreach ($other_claims as $other_claim) {
            if (!$other_claim instanceof WP_Comment || (int) $other_claim->comment_ID === $winner_comment_id) {
                continue;
            }
            $other_comment_id = (int) $other_claim->comment_ID;
            $other_status = sanitize_key((string) get_comment_meta($other_comment_id, 'nexdirectory_claim_status', true));
            $other_is_pending = $other_status === 'pending'
                || ($other_status === '' && (string) $other_claim->comment_approved === '0');
            if (!$other_is_pending) {
                continue;
            }
            if (!$this->set_listing_claim_terminal_state(
                $other_comment_id,
                'superseded',
                $reviewer_id,
                __('Another verified claim was approved first.', 'nexdirectory')
            )) {
                $result['comment_ids'][] = $other_comment_id;
                $result['comment_ids'] = array_values(array_unique($result['comment_ids']));
                $result['user_ids'] = array_values(array_unique($result['user_ids']));
                $this->refresh_listing_claim_caches($event_id, $result['comment_ids']);
                return new WP_Error(
                    'nexdirectory_claim_supersede_failed',
                    __('Could not close a competing pending claim.', 'nexdirectory'),
                    $result
                );
            }
            $result['comment_ids'][] = $other_comment_id;
            if ((int) $other_claim->user_id > 0) {
                $result['user_ids'][] = (int) $other_claim->user_id;
            }
        }

        $result['comment_ids'] = array_values(array_unique($result['comment_ids']));
        $result['user_ids'] = array_values(array_unique($result['user_ids']));
        return $result;
    }


    private function notify_superseded_listing_claim_users(array $user_ids, $event_id) {
        $event_id = absint((int) $event_id);
        foreach (array_values(array_unique(array_filter(array_map('absint', $user_ids)))) as $user_id) {
            $user = get_user_by('id', $user_id);
            if (!$user instanceof WP_User || !is_email((string) $user->user_email)) {
                continue;
            }
            /* translators: %s: Listing title. */
            $subject = sprintf(__('Claim update for "%s"', 'nexdirectory'), get_the_title($event_id));
            $body = sprintf(
                /* translators: 1: Listing title, 2: Listing URL. */
                __("Another verified claim for \"%1\$s\" was approved first, so your pending request has been closed.\n\nListing URL: %2\$s", 'nexdirectory'),
                get_the_title($event_id),
                get_permalink($event_id)
            );
            $this->send_platform_email((string) $user->user_email, $subject, '', [
                'fallback_text' => $body,
                'email_heading' => __('Claim Status Update', 'nexdirectory'),
            ]);
        }
    }


    public function render_claim_listing($atts = []) {
        $atts = shortcode_atts([
            'event_id' => 0,
        ], (array) $atts, 'nexdirectory_claim_listing');
        $ui_labels = $this->get_site_profile_ui_labels();
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $singular_lower = sanitize_text_field((string) ($ui_labels['singular_lower'] ?? strtolower((string) $singular_label)));
        $plural_label = sanitize_text_field((string) ($ui_labels['plural'] ?? __('Events', 'nexdirectory')));

        if ((string) get_option('nexdirectory_enable_claim_listings', '0') !== '1') {
            return '<p>' . esc_html__('Listing claims are currently disabled.', 'nexdirectory') . '</p>';
        }
        if (!is_user_logged_in()) {
            return $this->redirect_to_login_screen(__('Please log in to claim a listing.', 'nexdirectory'));
        }

        $event_id = absint((int) $atts['event_id']);
        if ($event_id <= 0) {
            $event_id = absint((int) ($_GET['event_id'] ?? 0));
        }
        if ($event_id <= 0 || get_post_type($event_id) !== self::POST_TYPE) {
            /* translators: %s: Placeholder value. */
            return '<p>' . esc_html(sprintf(__('Select a valid %s listing to claim.', 'nexdirectory'), $singular_lower)) . '</p>';
        }

        $current_user_id = get_current_user_id();
        $event_owner_id = (int) get_post_field('post_author', $event_id);
        if ($event_owner_id === $current_user_id) {
            return '<p>' . esc_html__('You already own this listing.', 'nexdirectory') . '</p>';
        }
        if (!$this->is_event_claimable($event_id)) {
            return '<p>' . esc_html__('This listing is not currently available for claim.', 'nexdirectory') . '</p>';
        }

        $existing_claim_status = $this->get_user_claim_status_for_event($event_id, $current_user_id);
        $events_url = $this->get_platform_page_url(
            'nexdirectory_page_events_url',
            $this->get_profile_route_slugs('events'),
            $this->get_profile_route_default_url('events')
        );
        $start_raw = (string) get_post_meta($event_id, 'nexdirectory_start', true);
        $start_ts = $start_raw !== '' ? strtotime($start_raw) : false;
        $event_date = $start_ts ? $this->format_event_datetime_for_display($start_raw) : '';
        $location_parts = array_filter([
            (string) get_post_meta($event_id, 'nexdirectory_venue', true),
            (string) get_post_meta($event_id, 'nexdirectory_city', true),
            (string) get_post_meta($event_id, 'nexdirectory_country', true),
        ], static function ($value) {
            return trim((string) $value) !== '';
        });

        return $this->render_frontend_template('claim-listing.php', [
            'event_id' => $event_id,
            'event_title' => get_the_title($event_id),
            'event_url' => get_permalink($event_id),
            'events_url' => $events_url,
            'event_cover' => $this->get_event_cover_image_url($event_id, 'large', 'thumb'),
            'event_date' => $event_date,
            'event_location' => implode(', ', array_map('nexdirectory_sanitize_text', $location_parts)),
            'claim_status' => $existing_claim_status,
            'claim_nonce_html' => wp_nonce_field('nexdirectory_claim_listing', 'nexdirectory_claim_listing_nonce', true, false),
            'ui_labels' => $ui_labels,
            'singular_label' => $singular_label,
            'singular_lower' => $singular_lower,
            'plural_label' => $plural_label,
        ]);
    }


    public function handle_claim_listing() {
        check_admin_referer('nexdirectory_claim_listing', 'nexdirectory_claim_listing_nonce');

        if (!is_user_logged_in()) {
            $redirect_back = wp_get_referer();
            if (!is_string($redirect_back) || trim($redirect_back) === '') {
                $redirect_back = home_url('/');
            }
            $login_url = $this->get_login_signup_url($redirect_back);
            $this->redirect_with_message($login_url, __('Please log in to claim a listing.', 'nexdirectory'), 'error');
        }
        $ui_labels = $this->get_site_profile_ui_labels();
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $singular_lower = sanitize_text_field((string) ($ui_labels['singular_lower'] ?? strtolower((string) $singular_label)));
        if ((string) get_option('nexdirectory_enable_claim_listings', '0') !== '1') {
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Listing claims are disabled.', 'nexdirectory'), 'error');
        }

        $event_id = absint((int) wp_unslash($_POST['event_id'] ?? 0));
        if ($event_id <= 0 || get_post_type($event_id) !== self::POST_TYPE) {
            /* translators: %s: Placeholder value. */
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), sprintf(__('Invalid %s selected.', 'nexdirectory'), $singular_lower), 'error');
        }
        if (!$this->is_event_claimable($event_id)) {
            /* translators: %s: Placeholder value. */
            $this->redirect_with_message(get_permalink($event_id), sprintf(__('This %s is not available for claim.', 'nexdirectory'), $singular_lower), 'error');
        }

        $user_id = get_current_user_id();
        $owner_id = (int) get_post_field('post_author', $event_id);
        if ($owner_id === $user_id) {
            $this->redirect_with_message(get_permalink($event_id), __('You already own this listing.', 'nexdirectory'));
        }

        $event_url = get_permalink($event_id) ?: (wp_get_referer() ?: home_url('/'));
        if (!$this->acquire_listing_claim_lock($event_id)) {
            $this->redirect_with_message(
                $event_url,
                __('Another claim is being processed for this listing. Please try again in a moment.', 'nexdirectory'),
                'error'
            );
        }

        $this->refresh_listing_claim_caches($event_id);
        if (!$this->is_event_claimable($event_id)) {
            $this->release_listing_claim_lock($event_id);
            /* translators: %s: Placeholder value. */
            $this->redirect_with_message($event_url, sprintf(__('This %s is no longer available for claim.', 'nexdirectory'), $singular_lower), 'error');
        }

        $owner_id = (int) get_post_field('post_author', $event_id);
        if ($owner_id === $user_id) {
            $this->release_listing_claim_lock($event_id);
            $this->redirect_with_message($event_url, __('You already own this listing.', 'nexdirectory'));
        }

        if (!$this->begin_listing_claim_transaction()) {
            $this->release_listing_claim_lock($event_id);
            $this->redirect_with_message($event_url, __('Could not start a secure claim transaction. Please try again.', 'nexdirectory'), 'error');
        }

        $existing_claims = get_comments([
            'post_id' => $event_id,
            'user_id' => $user_id,
            'type' => 'nexdirectory_claim',
            'status' => 'all',
            'number' => 20,
            'orderby' => 'comment_date_gmt',
            'order' => 'DESC',
        ]);
        $has_pending_claim = false;
        foreach ($existing_claims as $existing_claim) {
            if (!$existing_claim instanceof WP_Comment) {
                continue;
            }
            $existing_status = sanitize_key((string) get_comment_meta((int) $existing_claim->comment_ID, 'nexdirectory_claim_status', true));
            if ($existing_status === 'pending' || ($existing_status === '' && (string) $existing_claim->comment_approved === '0')) {
                $has_pending_claim = true;
                break;
            }
        }
        if ($has_pending_claim) {
            $this->rollback_listing_claim_transaction();
            $this->release_listing_claim_lock($event_id);
            /* translators: %s: Placeholder value. */
            $this->redirect_with_message($event_url, sprintf(__('You already have a pending claim for this %s.', 'nexdirectory'), $singular_lower), 'error');
        }

        $claim_reason = sanitize_textarea_field((string) wp_unslash($_POST['claim_reason'] ?? ''));
        $require_approval = (string) get_option('nexdirectory_claim_require_admin_approval', '1') === '1';
        $comment_id = wp_insert_comment([
            'comment_post_ID' => $event_id,
            'comment_type' => 'nexdirectory_claim',
            'comment_content' => $claim_reason,
            'user_id' => $user_id,
            'comment_author' => wp_get_current_user()->display_name,
            'comment_author_email' => wp_get_current_user()->user_email,
            'comment_approved' => '0',
        ]);
        if (!$comment_id) {
            $this->rollback_listing_claim_transaction();
            $this->release_listing_claim_lock($event_id);
            $this->redirect_with_message($event_url, __('Could not submit your claim request. Please try again.', 'nexdirectory'), 'error');
        }

        update_comment_meta((int) $comment_id, 'nexdirectory_claim_status', 'pending');
        update_comment_meta((int) $comment_id, 'nexdirectory_claim_requested_at', current_time('mysql'));
        $this->refresh_listing_claim_caches(0, [(int) $comment_id]);
        $pending_comment = get_comment((int) $comment_id);
        $pending_state_valid = $pending_comment instanceof WP_Comment
            && (string) $pending_comment->comment_type === 'nexdirectory_claim'
            && (string) $pending_comment->comment_approved === '0'
            && sanitize_key((string) get_comment_meta((int) $comment_id, 'nexdirectory_claim_status', true)) === 'pending';
        if (!$pending_state_valid) {
            $this->rollback_listing_claim_transaction();
            $this->refresh_listing_claim_caches($event_id, [(int) $comment_id]);
            $this->release_listing_claim_lock($event_id);
            $this->append_runtime_log('error', 'Listing claim pending state could not be persisted', [
                'event_id' => (string) $event_id,
                'comment_id' => (string) $comment_id,
                'user_id' => (string) $user_id,
            ]);
            $this->redirect_with_message($event_url, __('Could not save your claim request. Please try again.', 'nexdirectory'), 'error');
        }

        if (!$require_approval) {
            $ownership_result = wp_update_post([
                'ID' => $event_id,
                'post_author' => $user_id,
            ], true);
            if (is_wp_error($ownership_result)) {
                $this->rollback_listing_claim_transaction();
                $this->refresh_listing_claim_caches($event_id, [(int) $comment_id]);
                $this->release_listing_claim_lock($event_id);
                $this->append_runtime_log('error', 'Listing claim ownership transfer failed', [
                    'event_id' => (string) $event_id,
                    'comment_id' => (string) $comment_id,
                    'user_id' => (string) $user_id,
                    'error' => (string) $ownership_result->get_error_message(),
                ]);
                $this->redirect_with_message($event_url, __('Ownership could not be transferred. No claim changes were saved; please try again.', 'nexdirectory'), 'error');
            }

            $winner_state_saved = $this->set_listing_claim_terminal_state((int) $comment_id, 'approved');
            update_post_meta($event_id, 'nexdirectory_claimable', '0');
            update_post_meta($event_id, 'nexdirectory_claimed_by', (string) $user_id);

            $superseded = $this->supersede_other_pending_listing_claims($event_id, (int) $comment_id);
            $superseded_data = is_wp_error($superseded)
                ? (array) $superseded->get_error_data()
                : (array) $superseded;
            $superseded_comment_ids = (array) ($superseded_data['comment_ids'] ?? []);
            $superseded_user_ids = is_wp_error($superseded) ? [] : (array) ($superseded_data['user_ids'] ?? []);
            $this->refresh_listing_claim_caches($event_id, array_merge([(int) $comment_id], $superseded_comment_ids));
            $auto_approval_valid = $winner_state_saved
                && !is_wp_error($superseded)
                && (int) get_post_field('post_author', $event_id) === $user_id
                && (string) get_post_meta($event_id, 'nexdirectory_claimable', true) === '0'
                && absint((int) get_post_meta($event_id, 'nexdirectory_claimed_by', true)) === $user_id
                && sanitize_key((string) get_comment_meta((int) $comment_id, 'nexdirectory_claim_status', true)) === 'approved';
            if (!$auto_approval_valid || !$this->commit_listing_claim_transaction()) {
                $this->rollback_listing_claim_transaction();
                $this->refresh_listing_claim_caches($event_id, array_merge([(int) $comment_id], $superseded_comment_ids));
                $this->release_listing_claim_lock($event_id);
                $this->append_runtime_log('error', 'Listing claim auto-approval transaction rolled back', [
                    'event_id' => (string) $event_id,
                    'comment_id' => (string) $comment_id,
                    'user_id' => (string) $user_id,
                    'supersede_error' => is_wp_error($superseded) ? (string) $superseded->get_error_message() : '',
                ]);
                $this->redirect_with_message($event_url, __('The claim could not be completed safely. No ownership changes were saved; please try again.', 'nexdirectory'), 'error');
            }

            $this->refresh_listing_claim_caches($event_id, array_merge([(int) $comment_id], $superseded_comment_ids));
            $this->release_listing_claim_lock($event_id);
            $this->notify_superseded_listing_claim_users($superseded_user_ids, $event_id);
            $this->create_account_notification([
                'recipient_id' => $user_id,
                'actor_id' => 0,
                'type' => 'claim_status',
                'object_id' => $event_id,
                'secondary_id' => (int) $comment_id,
                'title' => __('Your listing claim was approved', 'nexdirectory'),
                /* translators: %s: Listing title. */
                'body' => sprintf(__('You now own “%s”.', 'nexdirectory'), get_the_title($event_id)),
                'url' => $event_url,
                'dedupe_key' => 'claim-status:' . (int) $comment_id . ':approved',
            ]);
            $this->append_runtime_log('info', 'Listing claim auto-approved', [
                'event_id' => (string) $event_id,
                'comment_id' => (string) $comment_id,
                'user_id' => (string) $user_id,
                'superseded_claims' => (string) count($superseded_comment_ids),
            ]);
            $this->redirect_with_message($event_url, __('Listing claimed successfully.', 'nexdirectory'), 'success');
        }

        if (!$this->commit_listing_claim_transaction()) {
            $this->rollback_listing_claim_transaction();
            $this->refresh_listing_claim_caches($event_id, [(int) $comment_id]);
            $this->release_listing_claim_lock($event_id);
            $this->append_runtime_log('error', 'Listing claim submission transaction rolled back', [
                'event_id' => (string) $event_id,
                'comment_id' => (string) $comment_id,
                'user_id' => (string) $user_id,
            ]);
            $this->redirect_with_message($event_url, __('Could not save your claim request. Please try again.', 'nexdirectory'), 'error');
        }
        $this->refresh_listing_claim_caches($event_id, [(int) $comment_id]);
        $this->release_listing_claim_lock($event_id);

        $claimant = wp_get_current_user();
        $claimant_name = sanitize_text_field((string) ($claimant->display_name ?: $claimant->user_login));
        $reviewer_ids = array_merge(
            [$owner_id],
            (array) get_users([
                'role' => 'administrator',
                'fields' => 'ids',
                'number' => 50,
            ])
        );
        $reviewer_ids = array_values(array_unique(array_filter(array_map('absint', $reviewer_ids))));
        foreach ($reviewer_ids as $reviewer_id) {
            if ($reviewer_id <= 0 || $reviewer_id === $user_id) {
                continue;
            }
            /* translators: %s: Claimant display name. */
            $claim_title = sprintf(__('New listing claim from %s', 'nexdirectory'), $claimant_name);
            $claim_body = $claim_reason !== ''
                ? $claim_reason
                /* translators: %s: Listing title. */
                : sprintf(__('Review the claim for “%s”.', 'nexdirectory'), get_the_title($event_id));
            $this->create_account_notification([
                'recipient_id' => $reviewer_id,
                'actor_id' => $user_id,
                'type' => 'claim_submitted',
                'object_id' => $event_id,
                'secondary_id' => (int) $comment_id,
                'title' => $claim_title,
                'body' => $claim_body,
                'url' => $this->user_can_moderate_events($reviewer_id)
                    ? admin_url('admin.php?page=nexdirectory-moderation')
                    : $event_url,
                'dedupe_key' => 'claim-submitted:' . (int) $comment_id,
            ]);
        }

        $admin_email = sanitize_email((string) get_option('admin_email'));
        $notify_admin_claim = (string) get_option('nexdirectory_notify_admin_claim_email', '1') === '1';
        if ($notify_admin_claim && is_email($admin_email)) {
            /* translators: %s: Placeholder value. */
            $subject = sprintf(__('New listing claim request: %s', 'nexdirectory'), get_the_title($event_id));
            $moderation_url = admin_url('admin.php?page=nexdirectory-moderation');
            $body = sprintf(
                "A new claim request was submitted.\n\n%s: %s\nListing URL: %s\nClaimant: %s (%s)\nReason: %s\n\nModerate claim: %s",
                $singular_label,
                get_the_title($event_id),
                get_permalink($event_id),
                wp_get_current_user()->display_name,
                wp_get_current_user()->user_email,
                $claim_reason !== '' ? $claim_reason : __('No reason provided.', 'nexdirectory'),
                $moderation_url
            );
            $sent = (bool) $this->send_platform_email($admin_email, $subject, '', [
                'fallback_text' => $body,
            ]);
            if (!$sent) {
                $sent = (bool) wp_mail($admin_email, $subject, $body);
            }
            $this->append_runtime_log($sent ? 'info' : 'warning', 'Admin claim notification email ' . ($sent ? 'sent' : 'failed'), [
                'event_id' => (string) $event_id,
                'comment_id' => (string) $comment_id,
                'admin_email' => (string) $admin_email,
            ]);
        }

        $this->append_runtime_log('info', 'Listing claim submitted', [
            'event_id' => (string) $event_id,
            'comment_id' => (string) $comment_id,
            'user_id' => (string) $user_id,
        ]);
        $this->redirect_with_message($event_url, __('Claim submitted. An admin will review your request.', 'nexdirectory'));
    }


    public function handle_moderate_claim() {
        if (!$this->user_can_moderate_events()) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        check_admin_referer('nexdirectory_moderate_claim', 'nexdirectory_moderate_claim_nonce');
        $redirect_url = wp_get_referer() ?: home_url('/');
        $comment_id = absint((int) wp_unslash($_POST['comment_id'] ?? 0));
        if ($comment_id <= 0) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'), '', ['response' => 400]);
        }
        $ui_labels = $this->get_site_profile_ui_labels();
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $singular_lower = sanitize_text_field((string) ($ui_labels['singular_lower'] ?? strtolower((string) $singular_label)));

        $decision = sanitize_key((string) wp_unslash($_POST['decision'] ?? ''));
        $reject_reason = sanitize_textarea_field((string) wp_unslash($_POST['claim_reject_reason'] ?? ''));
        if ($comment_id <= 0 || !in_array($decision, ['approve', 'reject'], true)) {
            $this->redirect_with_message($redirect_url, __('Invalid claim moderation request.', 'nexdirectory'), 'error');
        }

        $comment = get_comment($comment_id);
        if (!$comment instanceof WP_Comment || (string) $comment->comment_type !== 'nexdirectory_claim') {
            $this->redirect_with_message($redirect_url, __('Claim request not found.', 'nexdirectory'), 'error');
        }

        $event_id = (int) $comment->comment_post_ID;
        $claim_user_id = (int) $comment->user_id;
        if ($event_id <= 0 || $claim_user_id <= 0 || get_post_type($event_id) !== self::POST_TYPE) {
            $this->redirect_with_message($redirect_url, __('Claim request data is invalid.', 'nexdirectory'), 'error');
        }
        if ($decision === 'reject' && trim($reject_reason) === '') {
            $this->redirect_with_message($redirect_url, __('Please provide a rejection reason so it can be emailed to the claimant.', 'nexdirectory'), 'error');
        }
        if (!$this->acquire_listing_claim_lock($event_id)) {
            $this->redirect_with_message(
                $redirect_url,
                __('Another claim decision is being processed for this listing. Please try again in a moment.', 'nexdirectory'),
                'error'
            );
        }

        $this->refresh_listing_claim_caches($event_id, [$comment_id]);
        $comment = get_comment($comment_id);
        if (
            !$comment instanceof WP_Comment
            || (string) $comment->comment_type !== 'nexdirectory_claim'
            || (int) $comment->comment_post_ID !== $event_id
            || (int) $comment->user_id !== $claim_user_id
        ) {
            $this->release_listing_claim_lock($event_id);
            $this->redirect_with_message($redirect_url, __('Claim request not found.', 'nexdirectory'), 'error');
        }
        $current_claim_status = sanitize_key((string) get_comment_meta($comment_id, 'nexdirectory_claim_status', true));
        $claim_is_pending = $current_claim_status === 'pending'
            || ($current_claim_status === '' && (string) $comment->comment_approved === '0');
        if (!$claim_is_pending) {
            $this->release_listing_claim_lock($event_id);
            $this->redirect_with_message($redirect_url, __('This claim has already been reviewed. The page has been refreshed with its current status.', 'nexdirectory'), 'error');
        }

        if ($decision === 'approve') {
            $current_owner_id = (int) get_post_field('post_author', $event_id);
            $claimable_flag = (string) get_post_meta($event_id, 'nexdirectory_claimable', true);
            $claimed_by = absint((int) get_post_meta($event_id, 'nexdirectory_claimed_by', true));
            $owner_is_default_admin = $current_owner_id > 0 && user_can($current_owner_id, 'manage_options');
            $listing_remains_claimable = $claimable_flag === '1'
                || ($claimable_flag === '' && $owner_is_default_admin && $claimed_by <= 0);
            if ($current_owner_id !== $claim_user_id && !$listing_remains_claimable) {
                $this->release_listing_claim_lock($event_id);
                $this->redirect_with_message(
                    $redirect_url,
                    __('This listing is no longer claimable, so the stale approval was not applied.', 'nexdirectory'),
                    'error'
                );
            }
        }

        if (!$this->begin_listing_claim_transaction()) {
            $this->release_listing_claim_lock($event_id);
            $this->redirect_with_message($redirect_url, __('Could not start a secure claim transaction. Please try again.', 'nexdirectory'), 'error');
        }

        $moderator_id = get_current_user_id();
        $superseded_claim_user_ids = [];
        $superseded_comment_ids = [];
        $claim_state_valid = false;
        $state_error = '';
        if ($decision === 'approve') {
            $ownership_result = wp_update_post([
                'ID' => $event_id,
                'post_author' => $claim_user_id,
            ], true);
            if (is_wp_error($ownership_result)) {
                $this->rollback_listing_claim_transaction();
                $this->refresh_listing_claim_caches($event_id, [$comment_id]);
                $this->release_listing_claim_lock($event_id);
                $this->append_runtime_log('error', 'Claim moderation ownership transfer failed', [
                    'event_id' => (string) $event_id,
                    'comment_id' => (string) $comment_id,
                    'claim_user_id' => (string) $claim_user_id,
                    'error' => (string) $ownership_result->get_error_message(),
                ]);
                $this->redirect_with_message($redirect_url, $ownership_result->get_error_message(), 'error');
            }

            $winner_state_saved = $this->set_listing_claim_terminal_state($comment_id, 'approved', $moderator_id);
            update_post_meta($event_id, 'nexdirectory_claimable', '0');
            update_post_meta($event_id, 'nexdirectory_claimed_by', (string) $claim_user_id);

            $superseded = $this->supersede_other_pending_listing_claims($event_id, $comment_id, $moderator_id);
            if (is_wp_error($superseded)) {
                $state_error = (string) $superseded->get_error_message();
                $superseded_data = (array) $superseded->get_error_data();
                $superseded_comment_ids = (array) ($superseded_data['comment_ids'] ?? []);
            } else {
                $superseded_comment_ids = (array) ($superseded['comment_ids'] ?? []);
                $superseded_claim_user_ids = (array) ($superseded['user_ids'] ?? []);
            }
            $this->refresh_listing_claim_caches($event_id, array_merge([$comment_id], $superseded_comment_ids));
            $claim_state_valid = $winner_state_saved
                && !is_wp_error($superseded)
                && (int) get_post_field('post_author', $event_id) === $claim_user_id
                && (string) get_post_meta($event_id, 'nexdirectory_claimable', true) === '0'
                && absint((int) get_post_meta($event_id, 'nexdirectory_claimed_by', true)) === $claim_user_id
                && sanitize_key((string) get_comment_meta($comment_id, 'nexdirectory_claim_status', true)) === 'approved';

            /* translators: %s: Placeholder value. */
            $message = sprintf(__('Claim approved and %s ownership transferred.', 'nexdirectory'), $singular_lower);
        } else {
            $claim_state_valid = $this->set_listing_claim_terminal_state(
                $comment_id,
                'rejected',
                $moderator_id,
                $reject_reason
            );
            $message = __('Claim rejected.', 'nexdirectory');
        }

        if (!$claim_state_valid || !$this->commit_listing_claim_transaction()) {
            $this->rollback_listing_claim_transaction();
            $this->refresh_listing_claim_caches($event_id, array_merge([$comment_id], $superseded_comment_ids));
            $this->release_listing_claim_lock($event_id);
            $this->append_runtime_log('error', 'Claim moderation transaction rolled back', [
                'event_id' => (string) $event_id,
                'comment_id' => (string) $comment_id,
                'decision' => (string) $decision,
                'moderator' => (string) $moderator_id,
                'error' => $state_error,
            ]);
            $this->redirect_with_message(
                $redirect_url,
                __('The claim decision could not be saved safely. No ownership changes were applied; please try again.', 'nexdirectory'),
                'error'
            );
        }

        $this->refresh_listing_claim_caches($event_id, array_merge([$comment_id], $superseded_comment_ids));
        $this->release_listing_claim_lock($event_id);
        $claim_notification_title = $decision === 'approve'
            ? __('Your listing claim was approved', 'nexdirectory')
            : __('Your listing claim was not approved', 'nexdirectory');
        $claim_notification_body = $decision === 'approve'
            /* translators: %s: Listing title. */
            ? sprintf(__('You now own “%s”.', 'nexdirectory'), get_the_title($event_id))
            : (trim($reject_reason) !== '' ? $reject_reason : __('Review the listing and contact the site team if you need help.', 'nexdirectory'));
        $this->create_account_notification([
            'recipient_id' => $claim_user_id,
            'actor_id' => $moderator_id,
            'type' => 'claim_status',
            'object_id' => $event_id,
            'secondary_id' => $comment_id,
            'title' => $claim_notification_title,
            'body' => $claim_notification_body,
            'url' => get_permalink($event_id),
            'dedupe_key' => 'claim-status:' . $comment_id . ':' . $decision,
        ]);
        $claim_user = get_user_by('id', $claim_user_id);
        if ($claim_user instanceof WP_User && is_email((string) $claim_user->user_email)) {
            /* translators: %s: Placeholder value. */
            $subject = sprintf(__('Claim update for "%s"', 'nexdirectory'), get_the_title($event_id));
            $status_text = $decision === 'approve' ? __('approved', 'nexdirectory') : __('rejected', 'nexdirectory');
            $body = sprintf(
                __("Your claim for \"%1\$s\" was %2\$s.\n\nListing URL: %3\$s", 'nexdirectory'),
                get_the_title($event_id),
                $status_text,
                get_permalink($event_id)
            );
            if ($decision === 'reject' && trim($reject_reason) !== '') {
                $body .= "\n\n" . sprintf(
                    /* translators: %s: Placeholder value. */
                    __('Reason from moderator: %s', 'nexdirectory'),
                    $reject_reason
                );
            }
            $this->send_platform_email((string) $claim_user->user_email, $subject, '', [
                'fallback_text' => $body,
                'email_heading' => __('Claim Status Update', 'nexdirectory'),
            ]);
        }

        $this->notify_superseded_listing_claim_users($superseded_claim_user_ids, $event_id);

        $this->append_runtime_log('info', 'Claim moderation completed', [
            'event_id' => (string) $event_id,
            'comment_id' => (string) $comment_id,
            'decision' => (string) $decision,
            'reject_reason' => (string) $reject_reason,
            'moderator' => (string) $moderator_id,
            'superseded_claims' => (string) count($superseded_comment_ids),
        ]);
        $this->redirect_with_message($redirect_url, $message);
    }


    public function handle_moderate_report() {
        if (!$this->user_can_moderate_events()) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        check_admin_referer('nexdirectory_moderate_report', 'nexdirectory_moderate_report_nonce');

        $ui_labels = $this->get_site_profile_ui_labels();
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $singular_lower = sanitize_text_field((string) ($ui_labels['singular_lower'] ?? strtolower((string) $singular_label)));

        $comment_id = isset($_POST['comment_id'])
            ? absint((int) wp_unslash($_POST['comment_id']))
            : (isset($_POST['report_id']) ? absint((int) wp_unslash($_POST['report_id'])) : 0);
        $intent = sanitize_key((string) wp_unslash($_POST['intent'] ?? ''));
        if ($comment_id <= 0 || !in_array($intent, ['toggle_event_status', 'resolve_report', 'email_reporter', 'email_owner'], true)) {
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Invalid report moderation request.', 'nexdirectory'), 'error');
        }
        $comment = get_comment($comment_id);
        if (!$comment instanceof WP_Comment || (string) $comment->comment_type !== 'nexdirectory_report') {
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Report not found.', 'nexdirectory'), 'error');
        }

        $event_id = (int) $comment->comment_post_ID;
        if ($event_id <= 0 || get_post_type($event_id) !== self::POST_TYPE) {
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Report is linked to an invalid listing.', 'nexdirectory'), 'error');
        }

        if ($intent === 'resolve_report') {
            wp_set_comment_status($comment_id, 'hold');
            update_comment_meta($comment_id, 'nexdirectory_report_resolved_by', (string) get_current_user_id());
            update_comment_meta($comment_id, 'nexdirectory_report_resolved_at', current_time('mysql'));
            $this->append_runtime_log('info', 'Report resolved', [
                'comment_id' => (string) $comment_id,
                'event_id' => (string) $event_id,
                'moderator_id' => (string) get_current_user_id(),
            ]);
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Report marked as resolved.', 'nexdirectory'));
        }

        if ($intent === 'toggle_event_status') {
            $current_status = sanitize_key((string) get_post_status($event_id));
            $next_status = $current_status === 'publish' ? 'draft' : 'publish';
            $result = wp_update_post([
                'ID' => $event_id,
                'post_status' => $next_status,
            ], true);
            if (is_wp_error($result)) {
                $this->redirect_with_message(wp_get_referer() ?: home_url('/'), $result->get_error_message(), 'error');
            }
            $note = $next_status === 'publish'
                /* translators: %s: Placeholder value. */
                ? sprintf(__('%s reactivated after moderation review.', 'nexdirectory'), $singular_label)
                /* translators: %s: Placeholder value. */
                : sprintf(__('%s temporarily deactivated pending moderation review.', 'nexdirectory'), $singular_label);
            update_post_meta($event_id, 'nexdirectory_moderation_note', $note);
            update_post_meta($event_id, 'nexdirectory_moderation_action', $next_status === 'publish' ? 'approve' : 'reject');
            $this->notify_author_status($event_id, $next_status, $note);
            $this->append_runtime_log('info', 'Report toggle event status', [
                'comment_id' => (string) $comment_id,
                'event_id' => (string) $event_id,
                'old_status' => (string) $current_status,
                'new_status' => (string) $next_status,
                'moderator_id' => (string) get_current_user_id(),
            ]);
            $this->redirect_with_message(
                wp_get_referer() ?: home_url('/'),
                $next_status === 'publish'
                    /* translators: %s: Placeholder value. */
                    ? sprintf(__('%s activated.', 'nexdirectory'), $singular_label)
                    /* translators: %s: Placeholder value. */
                    : sprintf(__('%s deactivated and moved to draft.', 'nexdirectory'), $singular_label)
            );
        }

        $admin_subject = sanitize_text_field((string) wp_unslash($_POST['admin_subject'] ?? ''));
        $admin_message = sanitize_textarea_field((string) wp_unslash($_POST['admin_message'] ?? ''));
        if (trim($admin_message) === '') {
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Please provide a message before sending.', 'nexdirectory'), 'error');
        }

        $target_email = '';
        $target_label = '';
        $target_name = '';
        if ($intent === 'email_reporter') {
            if ((int) $comment->user_id > 0) {
                $report_user = get_user_by('id', (int) $comment->user_id);
                if ($report_user instanceof WP_User && is_email((string) $report_user->user_email)) {
                    $target_email = (string) $report_user->user_email;
                    $target_name = (string) ($report_user->display_name ?: $report_user->user_login);
                }
            }
            if ($target_email === '' && is_email((string) $comment->comment_author_email)) {
                $target_email = (string) $comment->comment_author_email;
                $target_name = sanitize_text_field((string) $comment->comment_author);
            }
            $target_label = __('reporter', 'nexdirectory');
            if ($admin_subject === '') {
                /* translators: %s: Placeholder value. */
                $admin_subject = sprintf(__('Update on your report for "%s"', 'nexdirectory'), get_the_title($event_id));
            }
        } else {
            $owner_id = (int) get_post_field('post_author', $event_id);
            $owner = $owner_id > 0 ? get_user_by('id', $owner_id) : null;
            if ($owner instanceof WP_User && is_email((string) $owner->user_email)) {
                $target_email = (string) $owner->user_email;
                $target_name = (string) ($owner->display_name ?: $owner->user_login);
            }
            /* translators: %s: Placeholder value. */
            $target_label = sprintf(__('%s owner', 'nexdirectory'), $singular_lower);
            if ($admin_subject === '') {
                /* translators: %s: Placeholder value. */
                $admin_subject = sprintf(__('Moderation note for "%s"', 'nexdirectory'), get_the_title($event_id));
            }
        }

        if ($target_email === '' || !is_email($target_email)) {
            /* translators: %s: Placeholder value. */
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), sprintf(__('Could not determine a valid email for this %s.', 'nexdirectory'), $target_label), 'error');
        }

        $admin_user = wp_get_current_user();
        $headers = [];
        if ($admin_user instanceof WP_User && is_email((string) $admin_user->user_email)) {
            $headers[] = 'Reply-To: ' . $admin_user->display_name . ' <' . $admin_user->user_email . '>';
        }
        $fallback_text = sprintf(
            "Hello %s,\n\n%s\n\n%s: %s\nLink: %s",
            $target_name !== '' ? $target_name : __('there', 'nexdirectory'),
            $admin_message,
            $singular_label,
            get_the_title($event_id),
            get_permalink($event_id)
        );

        $sent = (bool) $this->send_platform_email($target_email, $admin_subject, '', [
            'fallback_text' => $fallback_text,
        ], $headers);
        if (!$sent) {
            $sent = (bool) wp_mail($target_email, $admin_subject, $fallback_text, $headers);
        }
        if (!$sent) {
            /* translators: %s: Placeholder value. */
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), sprintf(__('Could not send message to the %s.', 'nexdirectory'), $target_label), 'error');
        }

        update_comment_meta($comment_id, 'nexdirectory_report_last_contacted_by', (string) get_current_user_id());
        update_comment_meta($comment_id, 'nexdirectory_report_last_contacted_at', current_time('mysql'));
        update_comment_meta($comment_id, 'nexdirectory_report_last_contact_target', $intent === 'email_reporter' ? 'reporter' : 'owner');
        $this->append_runtime_log('info', 'Report contact email sent', [
            'comment_id' => (string) $comment_id,
            'event_id' => (string) $event_id,
            'intent' => (string) $intent,
            'target_email' => (string) $target_email,
            'moderator_id' => (string) get_current_user_id(),
        ]);

        $this->redirect_with_message(
            wp_get_referer() ?: home_url('/'),
            $intent === 'email_reporter'
                ? __('Reply sent to reporter.', 'nexdirectory')
                /* translators: %s: Placeholder value. */
                : sprintf(__('Message sent to %s owner.', 'nexdirectory'), $singular_lower)
        );
    }


    /**
     * Adds an in-app moderation result for the listing author.
     *
     * @param int    $post_id Listing ID.
     * @param string $decision Moderation action.
     * @param string $note Moderator note.
     * @return void
     */
    private function notify_account_moderation_decision($post_id, $decision, $note = '') {
        $post_id = absint((int) $post_id);
        $author_id = absint((int) get_post_field('post_author', $post_id));
        if ($post_id <= 0 || $author_id <= 0) {
            return;
        }

        $decision = sanitize_key((string) $decision);
        if ($decision === 'approve') {
            $title = __('Your listing was approved', 'nexdirectory');
            $body = __('It is now published and visible to visitors.', 'nexdirectory');
            $url = get_permalink($post_id);
        } elseif ($decision === 'request_changes') {
            $title = __('Changes were requested for your listing', 'nexdirectory');
            $body = trim((string) $note) !== '' ? nexdirectory_sanitize_textarea((string) $note) : __('Review the requested changes before resubmitting.', 'nexdirectory');
            $url = $this->get_frontend_event_edit_url($post_id);
        } else {
            $title = __('Your listing needs attention', 'nexdirectory');
            $body = trim((string) $note) !== '' ? nexdirectory_sanitize_textarea((string) $note) : __('The listing was not approved. Review it before trying again.', 'nexdirectory');
            $url = $this->get_frontend_event_edit_url($post_id);
        }

        $this->create_account_notification([
            'recipient_id' => $author_id,
            'actor_id' => get_current_user_id(),
            'type' => 'moderation',
            'object_id' => $post_id,
            'title' => $title,
            'body' => $body,
            'url' => $url,
            'dedupe_key' => 'moderation:' . $post_id . ':' . $decision . ':' . (string) get_post_modified_time('U', true, $post_id),
        ]);
    }


    private function apply_moderation_decision($post_id, $decision, $note = '') {
        $post_id = absint((int) $post_id);
        $decision = sanitize_key((string) $decision);
        $note = nexdirectory_sanitize_textarea((string) $note);
        $ui_labels = $this->get_site_profile_ui_labels();
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $singular_lower = sanitize_text_field((string) ($ui_labels['singular_lower'] ?? strtolower((string) $singular_label)));
        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            /* translators: %s: Placeholder value. */
            return new WP_Error('nexdirectory_moderation_invalid_post', sprintf(__('Invalid %s selected for moderation.', 'nexdirectory'), $singular_lower));
        }

        if ($decision === 'approve') {
            wp_update_post([
                'ID' => $post_id,
                'post_status' => 'publish',
            ]);
            update_post_meta($post_id, 'nexdirectory_moderation_note', $note);
            update_post_meta($post_id, 'nexdirectory_moderation_action', 'approve');
            $this->notify_author_status($post_id, 'publish', $note);
            $this->notify_account_moderation_decision($post_id, 'approve', $note);
            /* translators: %s: Placeholder value. */
            return sprintf(__('%s approved.', 'nexdirectory'), $singular_label);
        }

        if ($decision === 'request_changes') {
            if ($note === '') {
                $note = __('Please update required details and resubmit for review.', 'nexdirectory');
            }
            wp_update_post([
                'ID' => $post_id,
                'post_status' => 'draft',
            ]);
            update_post_meta($post_id, 'nexdirectory_moderation_note', $note);
            update_post_meta($post_id, 'nexdirectory_moderation_action', 'request_changes');
            $this->notify_author_status($post_id, 'draft', $note);
            $this->notify_account_moderation_decision($post_id, 'request_changes', $note);
            /* translators: %s: Placeholder value. */
            return sprintf(__('Changes requested. %s moved to draft.', 'nexdirectory'), $singular_label);
        }

        wp_update_post([
            'ID' => $post_id,
            'post_status' => 'draft',
        ]);
        update_post_meta($post_id, 'nexdirectory_moderation_note', $note);
        update_post_meta($post_id, 'nexdirectory_moderation_action', 'reject');
        $this->notify_author_status($post_id, 'draft', $note);
        $this->notify_account_moderation_decision($post_id, 'reject', $note);
        /* translators: %s: Placeholder value. */
        return sprintf(__('%s rejected and moved to draft.', 'nexdirectory'), $singular_label);
    }


    private function resolve_moderation_decision_from_request($source, $keys = []) {
        if (!is_array($source) || empty($keys)) {
            return '';
        }
        foreach ($keys as $key) {
            $key = sanitize_key((string) $key);
            if ($key === '' || !array_key_exists($key, $source)) {
                continue;
            }
            $raw = wp_unslash($source[$key]);
            if (is_array($raw)) {
                foreach (array_reverse($raw) as $candidate) {
                    $normalized = sanitize_key((string) $candidate);
                    if ($normalized !== '') {
                        return $normalized;
                    }
                }
                continue;
            }
            $normalized = sanitize_key((string) $raw);
            if ($normalized !== '') {
                return $normalized;
            }
        }
        return '';
    }


    private function resolve_request_int_value($source, $keys = []) {
        if (!is_array($source) || empty($keys)) {
            return 0;
        }
        foreach ($keys as $key) {
            $key = sanitize_key((string) $key);
            if ($key === '' || !array_key_exists($key, $source)) {
                continue;
            }
            $raw = wp_unslash($source[$key]);
            if (is_array($raw)) {
                foreach ($raw as $candidate) {
                    $candidate_id = absint((int) $candidate);
                    if ($candidate_id > 0) {
                        return $candidate_id;
                    }
                }
                continue;
            }
            $candidate_id = absint((int) $raw);
            if ($candidate_id > 0) {
                return $candidate_id;
            }
        }
        return 0;
    }


    public function render_organizers($atts = []) {
        $atts = shortcode_atts([
            'limit' => 200,
            'verified_only' => 'no',
        ], (array) $atts, 'nexdirectory_organizers');
        $limit = max(1, (int) $atts['limit']);
        $verified_only = $atts['verified_only'] === 'yes';
        $selected_profile_id = isset($_GET['organizer_profile'])
            ? absint((int) wp_unslash($_GET['organizer_profile']))
            : 0;
        $selected_profile = $selected_profile_id > 0
            ? $this->get_public_organizer_profile($selected_profile_id, true)
            : [];
        $organizer_profiles = $selected_profile_id > 0
            ? []
            : $this->get_public_organizer_profiles($limit, $verified_only);
        $ui_labels = $this->get_site_profile_ui_labels();

        return $this->render_frontend_template('organizers.php', [
            'organizer_profiles' => $organizer_profiles,
            'selected_profile' => $selected_profile,
            'selected_profile_id' => $selected_profile_id,
            'verified_only' => $verified_only,
            'ui_labels' => $ui_labels,
        ]);
    }


    public function render_auth_links($atts = []) {
        $atts = shortcode_atts([
            'redirect_to' => '',
            'show_logout' => 'yes',
            'class' => '',
        ], (array) $atts, 'nexdirectory_auth_links');

        $redirect_to = wp_validate_redirect(
            esc_url_raw((string) $atts['redirect_to']),
            $this->current_url()
        );
        $login_url = $this->get_auth_screen_url('login', $redirect_to);
        $signup_url = $this->get_auth_screen_url('signup', $redirect_to);

        $extra_classes_raw = preg_split('/\s+/', sanitize_text_field((string) $atts['class']));
        if (!is_array($extra_classes_raw)) {
            $extra_classes_raw = [];
        }
        $extra_classes = array_values(array_filter(array_map('sanitize_html_class', $extra_classes_raw)));
        $wrapper_classes = array_values(array_unique(array_filter(array_merge([
            'nexdirectory-page-auth-links',
            'nexdirectory-page-auth-links-shortcode',
        ], $extra_classes))));
        $wrapper_class = implode(' ', $wrapper_classes);

        if (!is_user_logged_in()) {
            return '<div class="' . esc_attr($wrapper_class) . '" role="navigation" aria-label="' . esc_attr__('Authentication', 'nexdirectory') . '">'
                . '<a class="nexdirectory-page-auth-link" href="' . esc_url($login_url) . '">' . esc_html__('Login', 'nexdirectory') . '</a>'
                . '<a class="nexdirectory-page-auth-link nexdirectory-page-auth-link-primary" href="' . esc_url($signup_url) . '">' . esc_html__('Sign Up', 'nexdirectory') . '</a>'
                . '</div>';
        }

        $profile_key = $this->get_site_profile_key();
        $account_url = $this->get_platform_page_url(
            'nexdirectory_page_account_url',
            $this->get_profile_route_slugs('account', $profile_key),
            $this->get_profile_route_default_url('account', $profile_key)
        );
        $my_events_url = $this->get_platform_page_url(
            'nexdirectory_page_my_events_url',
            $this->get_profile_route_slugs('my_items', $profile_key),
            $this->get_profile_route_default_url('my_items', $profile_key)
        );
        $notifications_url = $this->get_notifications_page_url();
        $create_event_url = $this->get_platform_page_url(
            'nexdirectory_page_submit_event_url',
            $this->get_profile_route_slugs('submit', $profile_key),
            $this->get_profile_route_default_url('submit', $profile_key)
        );
        $discover_events_url = $this->get_platform_page_url(
            'nexdirectory_page_events_url',
            $this->get_profile_route_slugs('events', $profile_key),
            $this->get_profile_route_default_url('events', $profile_key)
        );
        $whats_new_url = $this->get_platform_page_url(
            'nexdirectory_page_whats_new_url',
            ['whats-new'],
            home_url('/whats-new/')
        );
        $logout_url = wp_logout_url($login_url);
        $show_logout = strtolower((string) $atts['show_logout']) !== 'no';

        $menu_links = [
            [
                'label' => __('My Account', 'nexdirectory'),
                'url' => $account_url,
            ],
            [
                'label' => __('My Events', 'nexdirectory'),
                'url' => $my_events_url,
            ],
            [
                'label' => __('Notifications', 'nexdirectory'),
                'url' => $notifications_url,
            ],
            [
                'label' => __('Create Event', 'nexdirectory'),
                'url' => $create_event_url,
            ],
            [
                'label' => __('Discover Events', 'nexdirectory'),
                'url' => $discover_events_url,
            ],
            [
                'label' => __('What\'s New', 'nexdirectory'),
                'url' => $whats_new_url,
            ],
        ];
        if ($show_logout) {
            $menu_links[] = [
                'label' => __('Logout', 'nexdirectory'),
                'url' => $logout_url,
            ];
        }

        $output = '<div class="' . esc_attr($wrapper_class) . '" role="navigation" aria-label="' . esc_attr__('Account actions', 'nexdirectory') . '">';
        $output .= '<details class="nexdirectory-page-auth-menu">';
        $output .= '<summary class="nexdirectory-page-auth-link nexdirectory-page-auth-link-primary nexdirectory-page-auth-menu-toggle">' . esc_html__('My Account', 'nexdirectory') . '</summary>';
        $output .= '<div class="nexdirectory-page-auth-menu-panel">';
        foreach ($menu_links as $menu_link) {
            $url = esc_url((string) ($menu_link['url'] ?? ''));
            if ($url === '') {
                continue;
            }
            $output .= '<a class="nexdirectory-page-auth-menu-item" href="' . $url . '">' . esc_html((string) ($menu_link['label'] ?? '')) . '</a>';
        }
        $output .= '</div>';
        $output .= '</details>';
        $output .= '</div>';

        return $output;
    }


    public function render_auth($atts = []) {
        $atts = shortcode_atts([
            'redirect_to' => '',
        ], (array) $atts, 'nexdirectory_auth');

        $redirect_from_query = esc_url_raw((string) wp_unslash($_GET['redirect_to'] ?? ''));
        $global_redirect = esc_url_raw((string) get_option('nexdirectory_login_redirect_url', ''));
        if (!empty($atts['redirect_to'])) {
            $redirect = esc_url_raw((string) $atts['redirect_to']);
        } elseif ($redirect_from_query !== '') {
            $redirect = $redirect_from_query;
        } elseif (!empty($global_redirect)) {
            $redirect = $global_redirect;
        } else {
            $redirect = $this->current_url();
        }
        $redirect = wp_validate_redirect($redirect, home_url('/'));

        if (is_user_logged_in()) {
            $account_url = $this->get_platform_page_url('nexdirectory_page_account_url', 'account', home_url('/'));
            $auth_page_url = $this->get_platform_page_url('nexdirectory_page_login_signup_url', 'login-signup', home_url('/login-signup/'));
            $is_auth_target = false;
            if ($auth_page_url !== '') {
                $target_path = (string) wp_parse_url($redirect, PHP_URL_PATH);
                $auth_path = (string) wp_parse_url($auth_page_url, PHP_URL_PATH);
                $is_auth_target = $target_path !== '' && $auth_path !== '' && untrailingslashit($target_path) === untrailingslashit($auth_path);
            }
            if ($redirect === '' || $is_auth_target) {
                $redirect = $account_url;
            }
            $this->safe_redirect_or_fallback($redirect);
        }

        $default_tab = isset($_GET['nexdirectory_auth']) ? sanitize_key((string) wp_unslash($_GET['nexdirectory_auth'])) : 'login';
        if (!in_array($default_tab, ['login', 'signup', 'forgot', 'reset'], true)) {
            $default_tab = 'login';
        }
        $reset_login = sanitize_user((string) wp_unslash($_GET['login'] ?? ''), true);
        $reset_key = sanitize_text_field((string) wp_unslash($_GET['key'] ?? ''));
        $can_reset = false;
        if ($default_tab === 'reset') {
            if ($reset_login === '' || $reset_key === '') {
                $default_tab = 'forgot';
            } else {
                $can_reset = true;
            }
        }
        $ui_labels = $this->get_site_profile_ui_labels();
        $forgot_url = $this->get_auth_screen_url('forgot', $redirect);
        $login_url = $this->get_auth_screen_url('login', $redirect);
        $signup_url = $this->get_auth_screen_url('signup', $redirect);
        $reset_url = $can_reset ? $this->get_auth_reset_url($reset_login, $reset_key, $redirect) : '';

        return $this->render_frontend_template('auth.php', [
            'redirect' => $redirect,
            'default_tab' => $default_tab,
            'ui_labels' => $ui_labels,
            'forgot_url' => $forgot_url,
            'login_url' => $login_url,
            'signup_url' => $signup_url,
            'reset_url' => $reset_url,
            'can_reset' => $can_reset,
            'reset_login' => $reset_login,
            'reset_key' => $reset_key,
        ]);
    }


    private function get_auth_screen_url($tab = 'login', $redirect_to = '') {
        $tab = sanitize_key((string) $tab);
        if (!in_array($tab, ['login', 'signup', 'forgot', 'reset'], true)) {
            $tab = 'login';
        }
        $redirect_to = wp_validate_redirect(esc_url_raw((string) $redirect_to), home_url('/'));
        $custom_login_url = $this->get_custom_login_page_url();
        if ($custom_login_url === '') {
            if ($tab === 'signup') {
                return nexdirectory_add_query_arg('redirect_to', $redirect_to, wp_registration_url());
            }
            if ($tab === 'forgot') {
                return wp_lostpassword_url($redirect_to);
            }
            return wp_login_url($redirect_to);
        }

        $args = ['nexdirectory_auth' => $tab];
        if ($redirect_to !== '') {
            $args['redirect_to'] = $redirect_to;
        }
        return nexdirectory_add_query_arg($args, $custom_login_url);
    }


    private function get_auth_reset_url($login, $key, $redirect_to = '') {
        $login = sanitize_user((string) $login, true);
        $key = sanitize_text_field((string) $key);
        $redirect_to = wp_validate_redirect(esc_url_raw((string) $redirect_to), home_url('/'));
        $core_reset_url = network_site_url('wp-login.php?action=rp', 'login');
        $args = [
            'login' => $login,
            'key' => $key,
        ];
        if ($redirect_to !== '') {
            $args['redirect_to'] = $redirect_to;
        }
        return nexdirectory_add_query_arg($args, $core_reset_url);
    }


    private function recaptcha_enabled() {
        return (string) get_option('nexdirectory_recaptcha_site_key', '') !== ''
            && (string) get_option('nexdirectory_recaptcha_secret_key', '') !== '';
    }


    private function should_enforce_submit_form_recaptcha() {
        if (!$this->recaptcha_enabled()) {
            return false;
        }
        if (is_user_logged_in() && current_user_can('manage_options')) {
            return false;
        }

        return (bool) $this->is_option_enabled('nexdirectory_recaptcha_show_submit', '0');
    }


    private function verify_recaptcha_token($token) {
        if (!$this->recaptcha_enabled()) {
            return true;
        }

        $token = sanitize_text_field((string) $token);
        if ($token === '') {
            return false;
        }

        $response = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', [
            'timeout' => 12,
            'body' => [
                'secret' => (string) get_option('nexdirectory_recaptcha_secret_key', ''),
                'response' => $token,
                'remoteip' => sanitize_text_field((string) wp_unslash($_SERVER['REMOTE_ADDR'] ?? '')),
            ],
        ]);

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

        $code = (int) wp_remote_retrieve_response_code($response);
        if ($code < 200 || $code >= 300) {
            return false;
        }

        $json = json_decode((string) wp_remote_retrieve_body($response), true);
        if (!is_array($json) || empty($json['success'])) {
            return false;
        }

        $expected_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        $response_host = strtolower(trim((string) ($json['hostname'] ?? '')));
        $allowed_hosts = array_values(array_unique(array_filter(array_map(
            static function ($host) {
                return strtolower(trim((string) $host));
            },
            (array) apply_filters(
                'nexdirectory_recaptcha_allowed_hostnames',
                $expected_host !== '' ? [$expected_host] : [],
                $json
            )
        ))));
        if ($expected_host !== '' && ($response_host === '' || !in_array($response_host, $allowed_hosts, true))) {
            return false;
        }

        $version = sanitize_key((string) get_option('nexdirectory_recaptcha_version', 'v3'));
        if ($version === 'v3') {
            $action = sanitize_key((string) ($json['action'] ?? ''));
            if ($action !== 'submit') {
                return false;
            }
            $minimum_score = (float) apply_filters('nexdirectory_recaptcha_minimum_score', 0.5, $json);
            $minimum_score = max(0.0, min(1.0, $minimum_score));
            $score = isset($json['score']) && is_numeric($json['score']) ? (float) $json['score'] : -1.0;
            if ($score < $minimum_score) {
                return false;
            }
        }

        return true;
    }

    public function handle_resend_verification() {
        if (!is_user_logged_in()) {
            wp_die(esc_html__('Login required.', 'nexdirectory'));
        }
        if (!$this->user_can_access_platform(get_current_user_id())) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        if ((string) get_option('nexdirectory_require_email_verification', '1') !== '1') {
            $this->redirect_with_message(wp_get_referer() ?: home_url('/'), __('Email verification is disabled.', 'nexdirectory'));
        }
        if (!isset($_POST['nexdirectory_resend_nonce']) || !wp_verify_nonce(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_resend_nonce'])), 'nexdirectory_resend_verification_nonce')) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'));
        }
        $ok = $this->send_verification_email(get_current_user_id());
        if ($ok) {
            update_user_meta(get_current_user_id(), 'nexdirectory_email_verify_last_sent', (string) time());
        }
        $this->redirect_with_message(
            wp_get_referer() ?: home_url('/'),
            $ok ? __('Verification email sent.', 'nexdirectory') : __('Could not send verification email.', 'nexdirectory'),
            $ok ? 'success' : 'error'
        );
    }


    public function handle_import_events() {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        if (!isset($_POST['nexdirectory_import_nonce']) || !wp_verify_nonce(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_import_nonce'])), 'nexdirectory_import_events_nonce')) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'));
        }
        $step = sanitize_key(wp_unslash($_POST['nexdirectory_import_step'] ?? 'preview'));
        $default_status = sanitize_key(wp_unslash($_POST['nexdirectory_import_default_status'] ?? 'pending'));
        if (!in_array($default_status, ['pending', 'draft', 'publish'], true)) {
            $default_status = 'pending';
        }
        $skip_duplicates = (string) (wp_unslash($_POST['nexdirectory_import_skip_duplicates'] ?? '0')) === '1';
        $import_page = admin_url('admin.php?page=nexdirectory-import');

        if ($step === 'apply') {
            $token = sanitize_key(wp_unslash($_POST['nexdirectory_import_token'] ?? ''));
            if ($token === '') {
                $this->redirect_with_message($import_page, __('Import preview token is missing. Please restart import.', 'nexdirectory'), 'error');
            }
            $payload = get_transient($this->get_import_preview_transient_key($token));
            if (!is_array($payload) || (int) ($payload['user_id'] ?? 0) !== get_current_user_id()) {
                $this->redirect_with_message($import_page, __('Import preview expired. Please upload the file again.', 'nexdirectory'), 'error');
            }

            $rows = (array) ($payload['rows'] ?? []);
            if (empty($rows)) {
                delete_transient($this->get_import_preview_transient_key($token));
                $this->redirect_with_message($import_page, __('No rows available for import. Please upload again.', 'nexdirectory'), 'error');
            }

            $allowed_fields = array_keys($this->get_import_supported_fields());
            $mapping = is_array($payload['mapping'] ?? null) ? (array) $payload['mapping'] : [];
            $posted_mapping = isset($_POST['nexdirectory_import_map']) && is_array($_POST['nexdirectory_import_map']) ? (array) wp_unslash($_POST['nexdirectory_import_map']) : [];
            foreach ($allowed_fields as $field_key) {
                if (array_key_exists($field_key, $posted_mapping)) {
                    $mapping[$field_key] = sanitize_key((string) $posted_mapping[$field_key]);
                }
            }
            if (empty($mapping['title'])) {
                $this->redirect_with_message($import_page . '&nexdirectory_import_preview=' . rawurlencode($token), __('Map at least one source column to "title".', 'nexdirectory'), 'error');
            }

            $success = 0;
            $duplicates = 0;
            $failed = 0;
            $errors = [];
            foreach ($rows as $idx => $row) {
                $mapped_row = $this->map_import_row_with_mapping($row, $mapping);
                $result = $this->import_event_from_row($mapped_row, $default_status, $skip_duplicates);
                if (is_wp_error($result)) {
                    if ($result->get_error_code() === 'duplicate_event') {
                        $duplicates++;
                        continue;
                    }
                    $failed++;
                    if (count($errors) < 5) {
                        $errors[] = sprintf('#%d: %s', (int) $idx + 1, $result->get_error_message());
                    }
                } else {
                    $success++;
                }
            }

            delete_transient($this->get_import_preview_transient_key($token));
            if ($success > 0) {
                $total_imports = (int) get_option('nexdirectory_import_events_count', 0);
                update_option('nexdirectory_import_events_count', $total_imports + $success, false);
            }

            $msg = sprintf(
                /* translators: 1: Placeholder value 1, 2: Placeholder value 2, 3: Placeholder value 3. */
                __('Import complete. Success: %1$d, Skipped duplicates: %2$d, Failed: %3$d', 'nexdirectory'),
                $success,
                $duplicates,
                $failed
            );
            if (!empty($errors)) {
                $msg .= ' | ' . implode(' ; ', $errors);
            }
            $this->redirect_with_message($import_page, $msg, $failed > 0 ? 'error' : 'success');
        }

        if (empty($_FILES['nexdirectory_import_file']) || !is_array($_FILES['nexdirectory_import_file'])) {
            $this->redirect_with_message($import_page, __('Please upload a file.', 'nexdirectory'), 'error');
        }
        $file = $this->sanitize_uploaded_file_array($_FILES['nexdirectory_import_file']);
        if (!empty($file['error']) || empty($file['tmp_name'])) {
            $this->redirect_with_message($import_page, __('Upload failed. Please try again.', 'nexdirectory'), 'error');
        }

        $ext = strtolower(pathinfo((string) ($file['name'] ?? ''), PATHINFO_EXTENSION));
        $rows = $this->parse_import_payload($ext, (string) $file['tmp_name']);
        if (is_wp_error($rows)) {
            $this->redirect_with_message($import_page, $rows->get_error_message(), 'error');
        }
        if (!is_array($rows) || empty($rows)) {
            $this->redirect_with_message($import_page, __('No rows found in import file.', 'nexdirectory'), 'error');
        }

        $normalized_rows = array_map([$this, 'normalize_import_row'], $rows);
        $max_rows = 5000;
        if (count($normalized_rows) > $max_rows) {
            $this->redirect_with_message(
                $import_page,
                /* translators: %d: Placeholder value. */
                sprintf(__('Import exceeds the limit of %d rows per file.', 'nexdirectory'), $max_rows),
                'error'
            );
        }
        $headers = $this->extract_import_headers($normalized_rows);
        if (empty($headers)) {
            $this->redirect_with_message($import_page, __('No valid headers detected in import file.', 'nexdirectory'), 'error');
        }

        $token = sanitize_key(wp_generate_password(20, false, false));
        $preview_payload = [
            'user_id' => get_current_user_id(),
            'rows' => $normalized_rows,
            'row_count' => count($normalized_rows),
            'headers' => $headers,
            'sample_rows' => array_slice($normalized_rows, 0, 5),
            'mapping' => $this->guess_import_mapping($headers),
            'default_status' => $default_status,
            'skip_duplicates' => $skip_duplicates ? '1' : '0',
            'created_at' => time(),
        ];
        set_transient($this->get_import_preview_transient_key($token), $preview_payload, 30 * MINUTE_IN_SECONDS);

        $this->safe_redirect_or_fallback($import_page . '&nexdirectory_import_preview=' . rawurlencode($token));
    }


    private function run_demo_profile_import($demo_profile, $requested_count, $localize_media = false, $time_budget_seconds = 20, $use_demo_owners = true, $starter_blueprint_key = '') {
        $demo_profile = sanitize_key((string) $demo_profile);
        $starter_blueprint_key = sanitize_key((string) $starter_blueprint_key);
        $requested_count = max(1, min(30, absint((int) $requested_count)));
        $localize_media = (bool) $localize_media;
        $time_budget_seconds = max(5, (int) $time_budget_seconds);
        $profile_config = $this->get_demo_profile_config($demo_profile);
        $demo_profile = sanitize_key((string) ($profile_config['key'] ?? $demo_profile));
        $rows = $this->build_demo_import_rows($requested_count, $demo_profile);
        $platform_mode = sanitize_key((string) get_option('nexdirectory_platform_mode', 'user_generated'));
        $started_at = microtime(true);
        $created = 0;
        $duplicates = 0;
        $failed = 0;
        $errors = [];
        $created_post_ids = [];
        $stopped_by_time_budget = false;
        $existing_blueprint_slots = [];
        if ($starter_blueprint_key !== '') {
            $existing_blueprint_post_ids = get_posts([
                'post_type' => NEXDIRECTORY_Plugin::POST_TYPE,
                'post_status' => ['publish', 'pending', 'draft', 'private', 'future'],
                'posts_per_page' => -1,
                'fields' => 'ids',
                'no_found_rows' => true,
                'meta_key' => 'nexdirectory_starter_blueprint',
                'meta_value' => $starter_blueprint_key,
            ]);
            foreach ((array) $existing_blueprint_post_ids as $existing_blueprint_post_id) {
                $existing_slot = absint((int) get_post_meta((int) $existing_blueprint_post_id, 'nexdirectory_starter_blueprint_slot', true));
                if ($existing_slot > 0) {
                    $existing_blueprint_slots[$existing_slot] = true;
                }
            }
        }

        $single_org_demo = $platform_mode === 'single_organisation' || $demo_profile === 'events_single_organisation';
        if ($single_org_demo || !$use_demo_owners) {
            $demo_owner_cycle = [max(1, (int) get_current_user_id())];
        } else {
            $demo_user_pool = $this->get_demo_seed_user_pool(get_current_user_id());
            $demo_owner_cycle = array_values(array_filter(array_unique(array_map('absint', (array) ($demo_user_pool['organizers'] ?? [])))));
            if (empty($demo_owner_cycle)) {
                $demo_owner_cycle = [max(1, (int) get_current_user_id())];
            }
        }
        $demo_owner_index = 0;
        $allow_demo_import_mutation = static function ($allow, $post_id = 0, $user_id = 0, $context = '') {
            $context = sanitize_key((string) $context);
            if (!in_array($context, ['pre_post_update', 'pre_trash_post', 'pre_delete_post', 'map_meta_cap_edit_post', 'map_meta_cap_delete_post'], true)) {
                return $allow;
            }
            $post_id = absint((int) $post_id);
            if ($post_id <= 0 || get_post_type($post_id) !== NEXDIRECTORY_Plugin::POST_TYPE) {
                return $allow;
            }
            if ((string) get_post_meta($post_id, 'nexdirectory_demo_locked', true) !== '1') {
                return $allow;
            }
            return true;
        };

        add_filter('nexdirectory_allow_locked_demo_event_mutation', $allow_demo_import_mutation, 10, 4);
        add_filter('nexdirectory_bypass_demo_event_create_guard', '__return_true', 10, 2);
        try {
            foreach ($rows as $index => $row) {
                $blueprint_slot = (int) $index + 1;
                if ($starter_blueprint_key !== '' && !empty($existing_blueprint_slots[$blueprint_slot])) {
                    $duplicates++;
                    continue;
                }
                if ((microtime(true) - $started_at) >= $time_budget_seconds) {
                    $stopped_by_time_budget = true;
                    break;
                }
                $row = is_array($row) ? $this->normalize_demo_row_remote_media_urls($row) : [];
                $target_owner_id = (int) ($demo_owner_cycle[$demo_owner_index % count($demo_owner_cycle)] ?? 0);
                if ($target_owner_id <= 0) {
                    $target_owner_id = (int) get_current_user_id();
                }
                $result = $this->import_event_from_row($row, 'publish', true, $target_owner_id, false);
                $demo_owner_index++;
                if (is_wp_error($result)) {
                    if ($result->get_error_code() === 'duplicate_event') {
                        $duplicates++;
                        continue;
                    }
                    $failed++;
                    if (count($errors) < 5) {
                        $errors[] = sprintf('#%d: %s', (int) $index + 1, $result->get_error_message());
                    }
                    continue;
                }

                $post_id = absint((int) $result);
                if ($post_id <= 0) {
                    $failed++;
                    continue;
                }

                $effective_row = $row;
                if ($localize_media) {
                    $effective_row = $this->localize_demo_import_row_media_assets($post_id, $row);
                }
                $this->apply_demo_import_post_enrichment($post_id, $effective_row, (int) $index);
                if ($starter_blueprint_key !== '') {
                    update_post_meta($post_id, 'nexdirectory_starter_blueprint', $starter_blueprint_key);
                    update_post_meta($post_id, 'nexdirectory_starter_blueprint_slot', (string) $blueprint_slot);
                    $existing_blueprint_slots[$blueprint_slot] = true;
                }
                $created++;
                $created_post_ids[] = $post_id;

                if (function_exists('gc_collect_cycles')) {
                    gc_collect_cycles();
                }
            }
        } finally {
            remove_filter('nexdirectory_allow_locked_demo_event_mutation', $allow_demo_import_mutation, 10);
            remove_filter('nexdirectory_bypass_demo_event_create_guard', '__return_true', 10);
        }

        if ($created > 0) {
            $total_imports = (int) get_option('nexdirectory_import_events_count', 0);
            update_option('nexdirectory_import_events_count', $total_imports + $created, false);
        }

        return [
            'profile_config' => $profile_config,
            'created' => $created,
            'duplicates' => $duplicates,
            'failed' => $failed,
            'errors' => $errors,
            'created_post_ids' => $created_post_ids,
            'stopped_by_time_budget' => $stopped_by_time_budget,
        ];
    }


    public function handle_apply_starter_blueprint() {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        if (
            !isset($_POST['nexdirectory_starter_blueprint_nonce'])
            || !wp_verify_nonce(
                sanitize_text_field((string) wp_unslash($_POST['nexdirectory_starter_blueprint_nonce'])),
                'nexdirectory_apply_starter_blueprint'
            )
        ) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'));
        }
        $confirmed = sanitize_key((string) wp_unslash($_POST['nexdirectory_blueprint_confirmed'] ?? '0'));
        if ($confirmed !== '1') {
            wp_die(esc_html__('Starter blueprint changes were not confirmed.', 'nexdirectory'));
        }

        $fallback_redirect = admin_url('admin.php?page=nexdirectory-settings&tab=general&subtab=setup');
        $requested_redirect = isset($_POST['nexdirectory_redirect_to'])
            ? esc_url_raw((string) wp_unslash($_POST['nexdirectory_redirect_to']))
            : '';
        $redirect_url = $requested_redirect !== ''
            ? wp_validate_redirect($requested_redirect, $fallback_redirect)
            : $fallback_redirect;

        $blueprint_key = sanitize_key((string) wp_unslash($_POST['nexdirectory_starter_blueprint'] ?? ''));
        $blueprints = method_exists($this, 'get_starter_blueprint_catalog')
            ? (array) $this->get_starter_blueprint_catalog()
            : [];
        if ($blueprint_key === '' || !isset($blueprints[$blueprint_key]) || !is_array($blueprints[$blueprint_key])) {
            $this->redirect_with_message($redirect_url, __('Choose a valid setup template.', 'nexdirectory'), 'error');
        }

        if (function_exists('ignore_user_abort')) {
            @ignore_user_abort(true);
        }

        $blueprint = (array) $blueprints[$blueprint_key];
        $site_profile = sanitize_key((string) ($blueprint['site_profile'] ?? 'events'));
        $demo_profile = sanitize_key((string) ($blueprint['demo_profile'] ?? $site_profile));
        $site_profiles = method_exists($this, 'get_site_profile_catalog') ? (array) $this->get_site_profile_catalog() : [];
        if ($site_profile === '' || !isset($site_profiles[$site_profile])) {
            $this->redirect_with_message($redirect_url, __('This setup template is not available.', 'nexdirectory'), 'error');
        }

        $blueprint_lock_option = 'nexdirectory_starter_blueprint_lock_' . $blueprint_key;
        $blueprint_lock_time = time();
        $blueprint_lock_acquired = add_option($blueprint_lock_option, $blueprint_lock_time, '', false);
        if (!$blueprint_lock_acquired) {
            $existing_lock_time = absint((int) get_option($blueprint_lock_option, 0));
            if ($existing_lock_time > 0 && ($blueprint_lock_time - $existing_lock_time) > (5 * MINUTE_IN_SECONDS)) {
                delete_option($blueprint_lock_option);
                $blueprint_lock_acquired = add_option($blueprint_lock_option, $blueprint_lock_time, '', false);
            }
        }
        if (!$blueprint_lock_acquired) {
            $this->redirect_with_message(
                $redirect_url,
                __('This setup template is already being applied. Please wait for the current setup to finish before trying again.', 'nexdirectory'),
                'error'
            );
        }

        try {
            $previous_profile = method_exists($this, 'get_site_profile_key') ? (string) $this->get_site_profile_key() : 'events';
            $profile_config = $this->get_demo_profile_config($demo_profile);
            $profile_config['site_profile'] = $site_profile;
            $this->apply_demo_profile_settings($profile_config);

            $allowed_blueprint_options = [
                'nexdirectory_platform_mode',
                'nexdirectory_enable_event_reporting',
                'nexdirectory_enable_map_auto_search',
                'nexdirectory_enable_duplicate_detection',
                'nexdirectory_enable_claim_listings',
            ];
            foreach ((array) ($blueprint['options'] ?? []) as $option_name => $option_value) {
                $option_name = sanitize_key((string) $option_name);
                if (!in_array($option_name, $allowed_blueprint_options, true)) {
                    continue;
                }
                if ($option_name === 'nexdirectory_platform_mode') {
                    $option_value = in_array((string) $option_value, ['user_generated', 'single_organisation'], true)
                        ? (string) $option_value
                        : 'user_generated';
                } else {
                    $option_value = (string) $option_value === '1' ? '1' : '0';
                }
                update_option($option_name, $option_value, false);
            }
            update_option(
                'nexdirectory_onboarding_operating_mode',
                (string) get_option('nexdirectory_platform_mode', 'user_generated') === 'single_organisation' ? 'single' : 'user',
                false
            );

            $this->ensure_default_pages();
            if (method_exists($this, 'maybe_sync_profile_page_urls')) {
                $this->maybe_sync_profile_page_urls($previous_profile, $site_profile);
            }
            if (method_exists($this, 'sync_route_options_to_existing_pages')) {
                $this->sync_route_options_to_existing_pages($site_profile);
            }

            $include_content = (string) wp_unslash($_POST['nexdirectory_blueprint_include_content'] ?? '1') === '1';
            $import_summary = [
                'created' => 0,
                'duplicates' => 0,
                'failed' => 0,
                'errors' => [],
            ];
            $sample_count = max(0, min(12, absint((int) ($blueprint['sample_count'] ?? 6))));
            if ($include_content && $sample_count > 0) {
                $import_summary = $this->run_demo_profile_import($demo_profile, $sample_count, false, 20, false, $blueprint_key);
            }

            update_option('nexdirectory_active_starter_blueprint', $blueprint_key, false);
            update_option('nexdirectory_starter_blueprint_applied_at', current_time('mysql'), false);
            $created = absint((int) ($import_summary['created'] ?? 0));
            $duplicates = absint((int) ($import_summary['duplicates'] ?? 0));
            $failed = absint((int) ($import_summary['failed'] ?? 0));
            $label = sanitize_text_field((string) ($blueprint['label'] ?? __('Starter', 'nexdirectory')));
            $message = sprintf(
                /* translators: 1: Blueprint label, 2: Created sample count, 3: Skipped duplicate count. */
                __('%1$s blueprint applied. Core pages and routes are ready. Sample listings created: %2$d; duplicates skipped: %3$d.', 'nexdirectory'),
                $label,
                $created,
                $duplicates
            );
            if (!$include_content || $sample_count === 0) {
                $message = sprintf(
                    /* translators: %s: Blueprint label. */
                    __('%s blueprint applied. Core pages and routes are ready; sample content was skipped.', 'nexdirectory'),
                    $label
                );
            } elseif ($failed > 0) {
                $message .= ' ' . sprintf(
                    /* translators: %d: Failed sample count. */
                    __('Samples that could not be created: %d.', 'nexdirectory'),
                    $failed
                );
            }
        } finally {
            delete_option($blueprint_lock_option);
        }

        $this->redirect_with_message(
            $redirect_url,
            $message,
            $failed > 0 && $created === 0 ? 'error' : 'success'
        );
    }


    public function handle_import_demo_events() {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        if (!isset($_POST['nexdirectory_import_demo_nonce']) || !wp_verify_nonce(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_import_demo_nonce'])), 'nexdirectory_import_demo_events_nonce')) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'));
        }
        $demo_import_confirmed = sanitize_key((string) wp_unslash($_POST['nexdirectory_demo_import_confirmed'] ?? '0'));
        if ($demo_import_confirmed !== '1') {
            wp_die(esc_html__('Demo import was not confirmed.', 'nexdirectory'));
        }
        if (function_exists('ignore_user_abort')) {
            @ignore_user_abort(true);
        }
        $redirect_url = home_url('/');
        $requested_redirect = isset($_POST['nexdirectory_redirect_to']) ? esc_url_raw((string) wp_unslash($_POST['nexdirectory_redirect_to'])) : '';
        if ($requested_redirect !== '') {
            $redirect_url = wp_validate_redirect($requested_redirect, $redirect_url);
        }
        $requested_count = isset($_POST['nexdirectory_demo_count'])
            ? absint((int) wp_unslash($_POST['nexdirectory_demo_count']))
            : 12;
        $requested_count = max(1, min(30, $requested_count));
        $requested_profile = sanitize_key((string) wp_unslash($_POST['nexdirectory_demo_profile'] ?? ''));
        $demo_profile = $this->resolve_demo_import_profile_key($requested_profile);
        $localize_media = (string) wp_unslash($_POST['nexdirectory_demo_localize_media'] ?? '0') === '1';
        $seed_runtime_data = (string) wp_unslash($_POST['nexdirectory_demo_seed_runtime'] ?? '0') === '1';
        $time_budget_seconds = (int) apply_filters(
            'nexdirectory_demo_import_time_budget_seconds',
            $localize_media ? 45 : 20,
            $requested_count,
            $demo_profile
        );
        if ($time_budget_seconds < 5) {
            $time_budget_seconds = 5;
        }
        $import_summary = $this->run_demo_profile_import(
            $demo_profile,
            $requested_count,
            $localize_media,
            $time_budget_seconds,
            true
        );
        $profile_config = (array) ($import_summary['profile_config'] ?? []);
        $created = absint((int) ($import_summary['created'] ?? 0));
        $duplicates = absint((int) ($import_summary['duplicates'] ?? 0));
        $failed = absint((int) ($import_summary['failed'] ?? 0));
        $errors = (array) ($import_summary['errors'] ?? []);
        $created_post_ids = (array) ($import_summary['created_post_ids'] ?? []);
        $stopped_by_time_budget = !empty($import_summary['stopped_by_time_budget']);
        $apply_presets = (string) wp_unslash($_POST['nexdirectory_demo_apply_presets'] ?? '1') === '1';
        if ($apply_presets) {
            $this->apply_demo_profile_settings($profile_config);
        }
        $current_homepage_id = absint((int) get_option('page_on_front', 0));
        $has_static_homepage = (string) get_option('show_on_front', 'posts') === 'page'
            && $current_homepage_id > 0
            && (string) get_post_type($current_homepage_id) === 'page'
            && (string) get_post_status($current_homepage_id) !== 'trash';
        $demo_homepage_id = $created > 0 && !$has_static_homepage
            ? $this->set_whats_new_as_site_homepage()
            : 0;

        $seeded_summary = [];
        if ($seed_runtime_data && !empty($created_post_ids)) {
            $seeded_summary = $this->seed_demo_import_runtime_data($created_post_ids, get_current_user_id());
        }

        $message = sprintf(
            /* translators: 1: Placeholder value 1, 2: Placeholder value 2, 3: Placeholder value 3, 4: Placeholder value 4. */
            __('Demo import complete (%4$s). Created: %1$d, Skipped duplicates: %2$d, Failed: %3$d.', 'nexdirectory'),
            $created,
            $duplicates,
            $failed,
            (string) ($profile_config['label'] ?? __('Default', 'nexdirectory'))
        );
        if (!empty($seeded_summary) && is_array($seeded_summary)) {
            $seed_bits = [];
            $bit_map = [
                'analytics_rows' => __('analytics rows', 'nexdirectory'),
                'messages' => __('messages', 'nexdirectory'),
                'threads' => __('conversations', 'nexdirectory'),
                'rsvps' => __('saved/attending rows', 'nexdirectory'),
                'pending_posts' => __('pending submissions', 'nexdirectory'),
                'draft_posts' => __('draft submissions', 'nexdirectory'),
                'claim_requests' => __('claim requests', 'nexdirectory'),
                'reports' => __('user reports', 'nexdirectory'),
            ];
            foreach ($bit_map as $bit_key => $bit_label) {
                $bit_value = isset($seeded_summary[$bit_key]) ? absint((int) $seeded_summary[$bit_key]) : 0;
                if ($bit_value <= 0) {
                    continue;
                }
                $seed_bits[] = $bit_value . ' ' . $bit_label;
            }
            if (!empty($seed_bits)) {
                $message .= ' ' . sprintf(
                    /* translators: %s: Placeholder value. */
                    __('Runtime demo data seeded: %s.', 'nexdirectory'),
                    implode(', ', $seed_bits)
                );
            }
        }
        if (!empty($errors)) {
            $message .= ' | ' . implode(' ; ', $errors);
        }
        if (!$localize_media) {
            $message .= ' ' . __('Media localization was skipped for faster import.', 'nexdirectory');
        }
        if (!$seed_runtime_data) {
            $message .= ' ' . __('Runtime demo activity seeding was skipped for faster import.', 'nexdirectory');
        }
        if ($demo_homepage_id > 0) {
            $message .= ' ' . __('Homepage set to What\'s New.', 'nexdirectory');
        }
        if ($stopped_by_time_budget) {
            $processed_rows = $created + $duplicates + $failed;
            if ($processed_rows > 0 && $processed_rows < $requested_count) {
                $message .= ' ' . sprintf(
                    /* translators: 1: Placeholder value 1, 2: Placeholder value 2. */
                    __('Time-safe mode processed %1$d of %2$d rows to avoid server timeout. Run import again to continue.', 'nexdirectory'),
                    $processed_rows,
                    $requested_count
                );
            }
        }

        $this->redirect_with_message(
            $redirect_url,
            $message,
            $failed > 0 ? 'error' : 'success'
        );
    }


    private function set_whats_new_as_site_homepage() {
        $whats_new_url = (string) $this->get_platform_page_url(
            'nexdirectory_page_whats_new_url',
            ['whats-new'],
            home_url('/whats-new/')
        );
        $whats_new_page_id = absint((int) url_to_postid($whats_new_url));
        if ($whats_new_page_id <= 0) {
            $whats_new_page = get_page_by_path('whats-new', OBJECT, 'page');
            if ($whats_new_page instanceof WP_Post) {
                $whats_new_page_id = absint((int) $whats_new_page->ID);
            }
        }
        if ($whats_new_page_id <= 0 || (string) get_post_type($whats_new_page_id) !== 'page') {
            return 0;
        }

        $whats_new_permalink = get_permalink($whats_new_page_id);
        if (is_string($whats_new_permalink) && $whats_new_permalink !== '') {
            update_option('nexdirectory_page_whats_new_url', esc_url_raw($whats_new_permalink), false);
        }
        update_option('show_on_front', 'page');
        update_option('page_on_front', $whats_new_page_id);
        if ((int) get_option('page_for_posts', 0) === $whats_new_page_id) {
            update_option('page_for_posts', 0);
        }

        return $whats_new_page_id;
    }


    private function get_demo_profile_key_for_platform_mode($platform_mode = '') {
        $platform_mode = sanitize_key((string) $platform_mode);
        if ($platform_mode === '') {
            $platform_mode = sanitize_key((string) get_option('nexdirectory_platform_mode', 'user_generated'));
        }
        if ($platform_mode === 'single_organisation') {
            return 'events_single_organisation';
        }

        $site_profile_key = method_exists($this, 'get_site_profile_key') ? (string) $this->get_site_profile_key() : 'events';
        if ($site_profile_key === '') {
            $site_profile_key = 'events';
        }
        $site_profile = method_exists($this, 'get_site_profile_config') ? (array) $this->get_site_profile_config($site_profile_key) : [];
        $demo_profile = sanitize_key((string) ($site_profile['demo_profile'] ?? $site_profile_key));
        if ($demo_profile === '') {
            $demo_profile = 'events';
        }

        return $demo_profile;
    }


    private function get_demo_profile_catalog() {
        $site_profiles = method_exists($this, 'get_site_profile_catalog') ? (array) $this->get_site_profile_catalog() : [];
        if (empty($site_profiles)) {
            $site_profiles = [
                'events' => [
                    'label' => __('Events (default)', 'nexdirectory'),
                    'description' => __('Classic event discovery, map, RSVP, and calendar flows.', 'nexdirectory'),
                    'hero_kicker' => __('Discover Events', 'nexdirectory'),
                ],
            ];
        }

        $presets = $this->get_demo_profile_presets();
        $default_preset = $presets['events'] ?? [
            'accent' => '#FF5E14',
            'accent_hover' => '#E04F0D',
            'hero_layout' => 'cinematic',
            'page_layout' => 'modern',
            'map_layout' => 'three-column',
            'map_density' => 'comfortable',
            'map_provider' => 'openstreetmap',
            'map_style' => 'osm-standard',
        ];

        $catalog = [];
        foreach ($site_profiles as $profile_key => $profile_config) {
            $profile_key = sanitize_key((string) $profile_key);
            if ($profile_key === '') {
                continue;
            }

            $profile_config = is_array($profile_config) ? $profile_config : [];
            $preset = isset($presets[$profile_key]) && is_array($presets[$profile_key]) ? $presets[$profile_key] : $default_preset;
            $site_profile = method_exists($this, 'get_site_profile_config') ? (array) $this->get_site_profile_config($profile_key) : [];

            $hero_kicker = sanitize_text_field((string) ($site_profile['hero_kicker'] ?? ($profile_config['hero_kicker'] ?? '')));
            if ($hero_kicker === '') {
                $hero_kicker = sanitize_text_field((string) ($profile_config['hero_kicker'] ?? __('Directory Listing', 'nexdirectory')));
            }

            $catalog[$profile_key] = [
                'label' => sanitize_text_field((string) ($profile_config['label'] ?? $profile_key)),
                'description' => sanitize_text_field((string) ($profile_config['description'] ?? '')),
                'site_profile' => $profile_key,
                'accent' => sanitize_hex_color((string) ($preset['accent'] ?? '#FF5E14')) ?: '#FF5E14',
                'accent_hover' => sanitize_hex_color((string) ($preset['accent_hover'] ?? '#E04F0D')) ?: '#E04F0D',
                'hero_layout' => sanitize_key((string) ($preset['hero_layout'] ?? 'cinematic')),
                'page_layout' => sanitize_key((string) ($preset['page_layout'] ?? 'modern')),
                'map_layout' => sanitize_key((string) ($preset['map_layout'] ?? 'three-column')),
                'map_density' => sanitize_key((string) ($preset['map_density'] ?? 'comfortable')),
                'map_provider' => sanitize_key((string) ($preset['map_provider'] ?? 'openstreetmap')),
                'map_style' => sanitize_key((string) ($preset['map_style'] ?? 'osm-standard')),
                'hero_kicker' => $hero_kicker,
                'topics' => $this->get_demo_profile_topics($profile_key),
                'media_pack' => $this->get_demo_profile_media_pack($profile_key),
            ];
        }
        $single_org_preset = isset($presets['events_single_organisation']) && is_array($presets['events_single_organisation'])
            ? (array) $presets['events_single_organisation']
            : (isset($presets['events']) && is_array($presets['events']) ? (array) $presets['events'] : $default_preset);
        $single_org_hero_kicker = __('Official Event Programme', 'nexdirectory');
        $catalog['events_single_organisation'] = [
            'label' => sanitize_text_field(__('Single-Organisation Events', 'nexdirectory')),
            'description' => sanitize_text_field(__('Admin-operated event programme with official sessions, ticket-ready defaults, and single-owner workflows.', 'nexdirectory')),
            'site_profile' => 'events',
            'accent' => sanitize_hex_color((string) ($single_org_preset['accent'] ?? '#1D4ED8')) ?: '#1D4ED8',
            'accent_hover' => sanitize_hex_color((string) ($single_org_preset['accent_hover'] ?? '#1E40AF')) ?: '#1E40AF',
            'hero_layout' => sanitize_key((string) ($single_org_preset['hero_layout'] ?? 'cinematic')),
            'page_layout' => sanitize_key((string) ($single_org_preset['page_layout'] ?? 'modern')),
            'map_layout' => sanitize_key((string) ($single_org_preset['map_layout'] ?? 'three-column')),
            'map_density' => sanitize_key((string) ($single_org_preset['map_density'] ?? 'comfortable')),
            'map_provider' => sanitize_key((string) ($single_org_preset['map_provider'] ?? 'openstreetmap')),
            'map_style' => sanitize_key((string) ($single_org_preset['map_style'] ?? 'osm-standard')),
            'hero_kicker' => sanitize_text_field($single_org_hero_kicker),
            'topics' => $this->get_demo_profile_topics('events_single_organisation'),
            'media_pack' => $this->get_demo_profile_media_pack('events_single_organisation'),
        ];

        return $catalog;
    }


    private function get_demo_profile_presets() {
        return [
            'events' => ['accent' => '#FF5E14', 'accent_hover' => '#E04F0D', 'hero_layout' => 'cinematic', 'page_layout' => 'modern', 'map_layout' => 'three-column', 'map_density' => 'comfortable', 'map_provider' => 'openstreetmap', 'map_style' => 'osm-standard'],
            'events_single_organisation' => ['accent' => '#1D4ED8', 'accent_hover' => '#1E40AF', 'hero_layout' => 'cinematic', 'page_layout' => 'modern', 'map_layout' => 'three-column', 'map_density' => 'comfortable', 'map_provider' => 'openstreetmap', 'map_style' => 'carto-light'],
            'directory' => ['accent' => '#F97316', 'accent_hover' => '#EA580C', 'hero_layout' => 'split', 'page_layout' => 'fifty-fifty', 'map_layout' => 'below-grid-large', 'map_density' => 'compact', 'map_provider' => 'openstreetmap', 'map_style' => 'carto-light'],
            'local_businesses' => ['accent' => '#F97316', 'accent_hover' => '#EA580C', 'hero_layout' => 'split', 'page_layout' => 'fifty-fifty', 'map_layout' => 'below-grid-large', 'map_density' => 'compact', 'map_provider' => 'openstreetmap', 'map_style' => 'carto-light'],
            'trades_contractors' => ['accent' => '#F59E0B', 'accent_hover' => '#D97706', 'hero_layout' => 'split', 'page_layout' => 'fifty-fifty', 'map_layout' => 'below-grid-large', 'map_density' => 'comfortable', 'map_provider' => 'openstreetmap', 'map_style' => 'osm-standard'],
            'professional_services' => ['accent' => '#0EA5E9', 'accent_hover' => '#0284C7', 'hero_layout' => 'minimal', 'page_layout' => 'fifty-fifty', 'map_layout' => 'three-column', 'map_density' => 'comfortable', 'map_provider' => 'openstreetmap', 'map_style' => 'carto-light'],
            'clubs_community' => ['accent' => '#16A34A', 'accent_hover' => '#15803D', 'hero_layout' => 'compact', 'page_layout' => 'single-column', 'map_layout' => 'below-list', 'map_density' => 'comfortable', 'map_provider' => 'openstreetmap', 'map_style' => 'osm-hot'],
            'venues_directory' => ['accent' => '#A855F7', 'accent_hover' => '#9333EA', 'hero_layout' => 'immersive', 'page_layout' => 'editorial', 'map_layout' => 'three-column', 'map_density' => 'comfortable', 'map_provider' => 'openstreetmap', 'map_style' => 'carto-dark'],
            'property_agents' => ['accent' => '#2563EB', 'accent_hover' => '#1D4ED8', 'hero_layout' => 'cinematic', 'page_layout' => 'modern', 'map_layout' => 'below-grid', 'map_density' => 'comfortable', 'map_provider' => 'openstreetmap', 'map_style' => 'osm-standard'],
            'health_wellness' => ['accent' => '#0F766E', 'accent_hover' => '#0D9488', 'hero_layout' => 'minimal', 'page_layout' => 'fifty-fifty', 'map_layout' => 'below-list', 'map_density' => 'comfortable', 'map_provider' => 'openstreetmap', 'map_style' => 'carto-light'],
            'restaurants_hospitality' => ['accent' => '#DC2626', 'accent_hover' => '#B91C1C', 'hero_layout' => 'spotlight', 'page_layout' => 'fifty-fifty', 'map_layout' => 'below-grid-large', 'map_density' => 'compact', 'map_provider' => 'openstreetmap', 'map_style' => 'carto-dark'],
            'education_training' => ['accent' => '#1D4ED8', 'accent_hover' => '#1E40AF', 'hero_layout' => 'minimal', 'page_layout' => 'fifty-fifty', 'map_layout' => 'below-grid', 'map_density' => 'comfortable', 'map_provider' => 'openstreetmap', 'map_style' => 'osm-standard'],
            'job_board' => ['accent' => '#0F172A', 'accent_hover' => '#1E293B', 'hero_layout' => 'compact', 'page_layout' => 'single-column', 'map_layout' => 'grid-no-map-large', 'map_density' => 'compact', 'map_provider' => 'openstreetmap', 'map_style' => 'carto-light'],
            'marketplace' => ['accent' => '#DB2777', 'accent_hover' => '#BE185D', 'hero_layout' => 'spotlight', 'page_layout' => 'fifty-fifty', 'map_layout' => 'grid-no-map-large', 'map_density' => 'compact', 'map_provider' => 'openstreetmap', 'map_style' => 'osm-standard'],
            'classifieds_marketplace' => ['accent' => '#DB2777', 'accent_hover' => '#BE185D', 'hero_layout' => 'spotlight', 'page_layout' => 'fifty-fifty', 'map_layout' => 'grid-no-map-large', 'map_density' => 'compact', 'map_provider' => 'openstreetmap', 'map_style' => 'osm-standard'],
            'franchises_multilocation' => ['accent' => '#7C3AED', 'accent_hover' => '#6D28D9', 'hero_layout' => 'cinematic', 'page_layout' => 'editorial', 'map_layout' => 'three-column', 'map_density' => 'comfortable', 'map_provider' => 'openstreetmap', 'map_style' => 'carto-light'],
            'nonprofits_charities' => ['accent' => '#E11D48', 'accent_hover' => '#BE123C', 'hero_layout' => 'minimal', 'page_layout' => 'fifty-fifty', 'map_layout' => 'below-list', 'map_density' => 'comfortable', 'map_provider' => 'openstreetmap', 'map_style' => 'osm-hot'],
            'startup_tech' => ['accent' => '#9333EA', 'accent_hover' => '#7E22CE', 'hero_layout' => 'immersive', 'page_layout' => 'editorial', 'map_layout' => 'below-grid-large', 'map_density' => 'comfortable', 'map_provider' => 'openstreetmap', 'map_style' => 'carto-dark'],
            'tourism_attractions' => ['accent' => '#EA580C', 'accent_hover' => '#C2410C', 'hero_layout' => 'cinematic', 'page_layout' => 'fifty-fifty', 'map_layout' => 'below-grid-large', 'map_density' => 'comfortable', 'map_provider' => 'openstreetmap', 'map_style' => 'topo'],
        ];
    }


    private function get_demo_profile_topics($profile_key) {
        $topics = [
            'events' => [
                ['title' => 'Global Robotics Summit', 'type' => 'Conference', 'mode' => 'In-person', 'price_range' => 'Premium', 'price_label' => '$299', 'tags' => 'Robotics|AI|Automation', 'official' => 'yes'],
                ['title' => 'Future of Aerospace', 'type' => 'Seminar', 'mode' => 'Hybrid', 'price_range' => 'Paid', 'price_label' => '$120', 'tags' => 'Aerospace|SpaceTech|Innovation', 'official' => 'yes'],
                ['title' => 'Green Energy Event Forum', 'type' => 'Forum', 'mode' => 'Online Stream Event', 'price_range' => 'Free', 'price_label' => 'Free', 'tags' => 'Energy|Sustainability|Climate', 'official' => 'no'],
                ['title' => 'Infrastructure Resilience Lab', 'type' => 'Workshop', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$85', 'tags' => 'Infrastructure|Civil|Risk', 'official' => 'no'],
            ],
            'events_single_organisation' => [
                ['title' => 'Flagship Annual Engineering Conference', 'type' => 'Conference', 'mode' => 'In-person', 'price_range' => 'Premium', 'price_label' => '$320', 'tags' => 'Flagship|Annual|Conference', 'official' => 'yes'],
                ['title' => 'Future Systems Leadership Summit', 'type' => 'Summit', 'mode' => 'Hybrid', 'price_range' => 'Paid', 'price_label' => '$180', 'tags' => 'Summit|Leadership|Innovation', 'official' => 'yes'],
                ['title' => 'Global Standards Congress', 'type' => 'Congress', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$210', 'tags' => 'Congress|Standards|Policy', 'official' => 'yes'],
                ['title' => 'Technology Expo & Trade Show', 'type' => 'Expo', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$95', 'tags' => 'Expo|Trade Show|Industry', 'official' => 'yes'],
                ['title' => 'Industry Awards Ceremony', 'type' => 'Award Ceremony', 'mode' => 'In-person', 'price_range' => 'Premium', 'price_label' => '$260', 'tags' => 'Awards|Recognition|Gala', 'official' => 'yes'],
                ['title' => 'Executive Networking Evening', 'type' => 'Networking Evening', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$60', 'tags' => 'Networking|Executive|Partners', 'official' => 'yes'],
                ['title' => 'Member Meetup Series', 'type' => 'Meetup', 'mode' => 'In-person', 'price_range' => 'Free', 'price_label' => 'Free', 'tags' => 'Members|Meetup|Recurring', 'official' => 'yes'],
                ['title' => 'Industry Mixer Social', 'type' => 'Mixer', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$45', 'tags' => 'Mixer|Industry|Community', 'official' => 'yes'],
                ['title' => 'Strategy Roundtable Forum', 'type' => 'Roundtable', 'mode' => 'Hybrid', 'price_range' => 'Paid', 'price_label' => '$80', 'tags' => 'Roundtable|Strategy|Leadership', 'official' => 'yes'],
                ['title' => 'Product Launch Showcase', 'type' => 'Product Launch', 'mode' => 'Hybrid', 'price_range' => 'Free', 'price_label' => 'Free', 'tags' => 'Product Launch|Showcase|Brand', 'official' => 'yes'],
                ['title' => 'Innovation Campus Open Day', 'type' => 'Open Day', 'mode' => 'In-person', 'price_range' => 'Free', 'price_label' => 'Free', 'tags' => 'Open Day|Campus|Public', 'official' => 'yes'],
                ['title' => 'Public Engineering Community Event', 'type' => 'Public Event', 'mode' => 'In-person', 'price_range' => 'Free', 'price_label' => 'Free', 'tags' => 'Public|Community|Outreach', 'official' => 'yes'],
                ['title' => 'Members-Only Strategy Session', 'type' => 'Members-Only Event', 'mode' => 'Hybrid', 'price_range' => 'Paid', 'price_label' => '$120', 'tags' => 'Members-Only|Strategy|Private', 'official' => 'yes'],
                ['title' => 'Quarterly Community Forum', 'type' => 'Forum', 'mode' => 'Online Stream Event', 'price_range' => 'Free', 'price_label' => 'Free', 'tags' => 'Forum|Community|Recurring', 'official' => 'yes'],
                ['title' => 'Regional Roadshow Series', 'type' => 'Roadshow', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$70', 'tags' => 'Roadshow|Regional|Tour', 'official' => 'yes'],
                ['title' => 'Brand Activation Live Experience', 'type' => 'Brand Activation', 'mode' => 'In-person', 'price_range' => 'Free', 'price_label' => 'Free', 'tags' => 'Brand|Activation|Experience', 'official' => 'yes'],
                ['title' => 'Engineering Scholarship Fundraiser', 'type' => 'Fundraiser', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$140', 'tags' => 'Fundraiser|Scholarship|Charity', 'official' => 'yes'],
                ['title' => 'VIP Partner Reception', 'type' => 'VIP Event', 'mode' => 'In-person', 'price_range' => 'Premium', 'price_label' => '$240', 'tags' => 'VIP|Partners|Reception', 'official' => 'yes'],
                ['title' => 'Startup Pitch Night', 'type' => 'Pitch Night', 'mode' => 'Hybrid', 'price_range' => 'Free', 'price_label' => 'Free', 'tags' => 'Pitch|Startup|Innovation', 'official' => 'yes'],
                ['title' => 'Hybrid Global Broadcast', 'type' => 'Global Broadcast', 'mode' => 'Hybrid', 'price_range' => 'Free', 'price_label' => 'Free', 'tags' => 'Hybrid|Broadcast|Global', 'official' => 'yes'],
                ['title' => 'Archive Spotlight: Annual Congress 2024', 'type' => 'Archive', 'mode' => 'Online Stream Event', 'price_range' => 'Free', 'price_label' => 'Free Replay', 'tags' => 'Archive|Past Event|On-demand', 'official' => 'yes'],
            ],
            'directory' => [
                ['title' => 'Downtown Artisan Bakery Listing', 'type' => 'Bakery', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'From $8', 'tags' => 'Bakery|Local|Family', 'official' => 'no'],
                ['title' => 'Neighborhood Bike Repair Shop', 'type' => 'Repair', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'Quote Required', 'tags' => 'Bikes|Repair|Community', 'official' => 'no'],
                ['title' => 'Business Legal Advisory Firm', 'type' => 'Legal', 'mode' => 'Hybrid', 'price_range' => 'Paid', 'price_label' => 'Consultation Fee', 'tags' => 'Legal|Advisory|Corporate', 'official' => 'no'],
                ['title' => 'Independent Bookstore Spotlight', 'type' => 'Retail', 'mode' => 'In-person', 'price_range' => 'Free', 'price_label' => 'Free Entry', 'tags' => 'Books|Retail|Culture', 'official' => 'no'],
            ],
            'local_businesses' => [
                ['title' => 'Downtown Artisan Bakery Listing', 'type' => 'Bakery', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'From $8', 'tags' => 'Bakery|Local|Family', 'official' => 'no'],
                ['title' => 'Neighborhood Bike Repair Shop', 'type' => 'Repair', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'Quote Required', 'tags' => 'Bikes|Repair|Community', 'official' => 'no'],
                ['title' => 'Independent Bookstore Spotlight', 'type' => 'Retail', 'mode' => 'In-person', 'price_range' => 'Free', 'price_label' => 'Free Entry', 'tags' => 'Books|Retail|Culture', 'official' => 'no'],
                ['title' => 'Local Florist & Gifts Directory', 'type' => 'Florist', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'From $25', 'tags' => 'Flowers|Gifts|Local', 'official' => 'no'],
            ],
            'trades_contractors' => [
                ['title' => 'Emergency Plumbing Contractor', 'type' => 'Plumbing', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'Quote Required', 'tags' => 'Plumbing|Emergency|Residential', 'official' => 'no'],
                ['title' => 'Electrical Safety Inspection Team', 'type' => 'Electrical', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'From $89', 'tags' => 'Electrical|Safety|Compliance', 'official' => 'no'],
                ['title' => 'Heating & Boiler Installers', 'type' => 'Heating', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'From $120', 'tags' => 'Heating|Boilers|Installers', 'official' => 'no'],
                ['title' => 'Trusted Builders & Renovation Crew', 'type' => 'Builders', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'Estimate Required', 'tags' => 'Builders|Renovation|Homes', 'official' => 'no'],
            ],
            'professional_services' => [
                ['title' => 'Business Legal Advisory Firm', 'type' => 'Legal', 'mode' => 'Hybrid', 'price_range' => 'Paid', 'price_label' => 'Consultation Fee', 'tags' => 'Legal|Advisory|Corporate', 'official' => 'no'],
                ['title' => 'Tax & Accounting Specialists', 'type' => 'Accounting', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'From $150', 'tags' => 'Tax|Accounting|SME', 'official' => 'no'],
                ['title' => 'Brand & Design Consultancy', 'type' => 'Design', 'mode' => 'Online Stream Event', 'price_range' => 'Paid', 'price_label' => 'Retainer', 'tags' => 'Branding|Design|Marketing', 'official' => 'no'],
                ['title' => 'IT Managed Service Provider', 'type' => 'IT Services', 'mode' => 'Hybrid', 'price_range' => 'Paid', 'price_label' => 'Monthly Plan', 'tags' => 'IT|Support|Managed Services', 'official' => 'no'],
            ],
            'clubs_community' => [
                ['title' => 'City Chess Club', 'type' => 'Club', 'mode' => 'In-person', 'price_range' => 'Free', 'price_label' => 'Membership', 'tags' => 'Chess|Community|Club', 'official' => 'no'],
                ['title' => 'Local Running Group', 'type' => 'Sports Club', 'mode' => 'In-person', 'price_range' => 'Free', 'price_label' => 'Free', 'tags' => 'Running|Fitness|Community', 'official' => 'no'],
                ['title' => 'Photography Meetup Collective', 'type' => 'Meetup', 'mode' => 'Hybrid', 'price_range' => 'Low Cost', 'price_label' => '$10', 'tags' => 'Photography|Meetup|Creative', 'official' => 'no'],
                ['title' => 'Neighborhood Parent Network', 'type' => 'Support Group', 'mode' => 'In-person', 'price_range' => 'Free', 'price_label' => 'Free', 'tags' => 'Parents|Support|Local', 'official' => 'no'],
            ],
            'venues_directory' => [
                ['title' => 'Riverside Wedding Venue', 'type' => 'Wedding Venue', 'mode' => 'In-person', 'price_range' => 'Premium', 'price_label' => 'From $2,000', 'tags' => 'Wedding|Venue|Riverside', 'official' => 'no'],
                ['title' => 'Executive Conference Hall', 'type' => 'Conference Venue', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'From $950/day', 'tags' => 'Conference|Corporate|Venue', 'official' => 'no'],
                ['title' => 'Urban Events Warehouse', 'type' => 'Event Space', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'From $650/day', 'tags' => 'Events|Warehouse|City', 'official' => 'no'],
                ['title' => 'Boutique Rooftop Venue', 'type' => 'Private Venue', 'mode' => 'In-person', 'price_range' => 'Premium', 'price_label' => 'From $1,250/day', 'tags' => 'Rooftop|Private|Hospitality', 'official' => 'no'],
            ],
            'property_agents' => [
                ['title' => 'Downtown Apartment Portfolio', 'type' => 'Residential', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'From $420k', 'tags' => 'Apartments|Residential|City', 'official' => 'no'],
                ['title' => 'Commercial Office Brokerage', 'type' => 'Commercial', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'Market Rate', 'tags' => 'Commercial|Offices|Brokerage', 'official' => 'no'],
                ['title' => 'Suburban Family Homes', 'type' => 'Estate Agent', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'From $560k', 'tags' => 'Homes|Family|Suburbs', 'official' => 'no'],
                ['title' => 'Luxury Penthouse Specialists', 'type' => 'Luxury', 'mode' => 'In-person', 'price_range' => 'Premium', 'price_label' => 'From $1.8m', 'tags' => 'Luxury|Penthouse|Prime', 'official' => 'no'],
            ],
            'health_wellness' => [
                ['title' => 'Holistic Wellness Studio', 'type' => 'Wellness', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'From $40', 'tags' => 'Wellness|Mindfulness|Local', 'official' => 'no'],
                ['title' => 'Sports Physiotherapy Clinic', 'type' => 'Physiotherapy', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'From $70', 'tags' => 'Physio|Rehab|Sports', 'official' => 'no'],
                ['title' => 'Nutrition Coaching Service', 'type' => 'Nutrition', 'mode' => 'Online Stream Event', 'price_range' => 'Paid', 'price_label' => 'From $55', 'tags' => 'Nutrition|Coaching|Health', 'official' => 'no'],
                ['title' => 'Community Mental Health Support', 'type' => 'Counselling', 'mode' => 'Hybrid', 'price_range' => 'Low Cost', 'price_label' => '$25', 'tags' => 'Mental Health|Support|Community', 'official' => 'no'],
            ],
            'restaurants_hospitality' => [
                ['title' => 'Rooftop Bistro & Cocktails', 'type' => 'Restaurant', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$$', 'tags' => 'Bistro|Rooftop|Dining', 'official' => 'no'],
                ['title' => 'Family Italian Kitchen', 'type' => 'Restaurant', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$$', 'tags' => 'Italian|Family|Local', 'official' => 'no'],
                ['title' => 'Boutique Hotel & Spa', 'type' => 'Hospitality', 'mode' => 'In-person', 'price_range' => 'Premium', 'price_label' => '$$$', 'tags' => 'Hotel|Spa|Luxury', 'official' => 'no'],
                ['title' => 'Specialty Coffee Roastery', 'type' => 'Cafe', 'mode' => 'In-person', 'price_range' => 'Low Cost', 'price_label' => '$', 'tags' => 'Coffee|Cafe|Roastery', 'official' => 'no'],
            ],
            'education_training' => [
                ['title' => 'Data Analytics Bootcamp Provider', 'type' => 'Bootcamp', 'mode' => 'Hybrid', 'price_range' => 'Paid', 'price_label' => 'From $399', 'tags' => 'Data|Bootcamp|Skills', 'official' => 'no'],
                ['title' => 'Language Learning Academy', 'type' => 'Academy', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'From $120', 'tags' => 'Language|Academy|Training', 'official' => 'no'],
                ['title' => 'Professional Certification Center', 'type' => 'Certification', 'mode' => 'Online Stream Event', 'price_range' => 'Paid', 'price_label' => 'From $250', 'tags' => 'Certification|Professional|Career', 'official' => 'no'],
                ['title' => 'Community Coding School', 'type' => 'Coding School', 'mode' => 'Hybrid', 'price_range' => 'Low Cost', 'price_label' => '$49', 'tags' => 'Coding|Community|Education', 'official' => 'yes'],
            ],
            'job_board' => [
                ['title' => 'Senior DevOps Engineer Opening', 'type' => 'Technology', 'mode' => 'Hybrid', 'price_range' => 'Paid', 'price_label' => '£85k', 'tags' => 'DevOps|Cloud|Hiring', 'official' => 'no'],
                ['title' => 'Marketing Manager Position', 'type' => 'Marketing', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '£58k', 'tags' => 'Marketing|Manager|Full-time', 'official' => 'no'],
                ['title' => 'Customer Success Specialist', 'type' => 'Customer Success', 'mode' => 'Online Stream Event', 'price_range' => 'Paid', 'price_label' => '£42k', 'tags' => 'Customer Success|SaaS|Remote', 'official' => 'no'],
                ['title' => 'Graduate Data Analyst Role', 'type' => 'Analytics', 'mode' => 'Hybrid', 'price_range' => 'Paid', 'price_label' => '£35k', 'tags' => 'Graduate|Data|Analytics', 'official' => 'yes'],
            ],
            'marketplace' => [
                ['title' => 'Vintage Camera Marketplace Listing', 'type' => 'Electronics', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$240', 'tags' => 'Camera|Vintage|Marketplace', 'official' => 'no'],
                ['title' => 'Home Office Furniture Bundle', 'type' => 'Furniture', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$320', 'tags' => 'Furniture|Office|Home', 'official' => 'no'],
                ['title' => 'Mountain Bike Classified Listing', 'type' => 'Sports', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$480', 'tags' => 'Bike|Sports|Preowned', 'official' => 'no'],
                ['title' => 'Local Handmade Crafts Seller', 'type' => 'Crafts', 'mode' => 'In-person', 'price_range' => 'Low Cost', 'price_label' => '$35', 'tags' => 'Crafts|Handmade|Seller', 'official' => 'no'],
            ],
            'classifieds_marketplace' => [
                ['title' => 'Vintage Camera Marketplace Listing', 'type' => 'Electronics', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$240', 'tags' => 'Camera|Vintage|Marketplace', 'official' => 'no'],
                ['title' => 'Home Office Furniture Bundle', 'type' => 'Furniture', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$320', 'tags' => 'Furniture|Office|Home', 'official' => 'no'],
                ['title' => 'Mountain Bike Classified Listing', 'type' => 'Sports', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$480', 'tags' => 'Bike|Sports|Preowned', 'official' => 'no'],
                ['title' => 'Local Handmade Crafts Seller', 'type' => 'Crafts', 'mode' => 'In-person', 'price_range' => 'Low Cost', 'price_label' => '$35', 'tags' => 'Crafts|Handmade|Seller', 'official' => 'no'],
            ],
            'franchises_multilocation' => [
                ['title' => 'BrewBox Downtown Franchise', 'type' => 'Food Franchise', 'mode' => 'In-person', 'price_range' => 'Premium', 'price_label' => '$500k+', 'tags' => 'Franchise|Food|Retail', 'official' => 'no'],
                ['title' => 'FitNation Multi-Site Gym Brand', 'type' => 'Fitness Franchise', 'mode' => 'In-person', 'price_range' => 'Premium', 'price_label' => '$380k+', 'tags' => 'Fitness|Franchise|Brand', 'official' => 'no'],
                ['title' => 'RapidFix Service Franchise', 'type' => 'Service Franchise', 'mode' => 'Hybrid', 'price_range' => 'Paid', 'price_label' => '$220k+', 'tags' => 'Service|Franchise|Growth', 'official' => 'no'],
                ['title' => 'EduLab Tutoring Branch Network', 'type' => 'Education Franchise', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$180k+', 'tags' => 'Education|Franchise|Tutoring', 'official' => 'no'],
            ],
            'nonprofits_charities' => [
                ['title' => 'Community Food Bank Organization', 'type' => 'Charity', 'mode' => 'In-person', 'price_range' => 'Free', 'price_label' => 'Donations', 'tags' => 'Charity|Food Bank|Community', 'official' => 'yes'],
                ['title' => 'Youth Mentorship Nonprofit', 'type' => 'Nonprofit', 'mode' => 'Hybrid', 'price_range' => 'Free', 'price_label' => 'Volunteer', 'tags' => 'Youth|Mentorship|Education', 'official' => 'yes'],
                ['title' => 'Environmental Action Group', 'type' => 'NGO', 'mode' => 'In-person', 'price_range' => 'Free', 'price_label' => 'Donations', 'tags' => 'Environment|NGO|Local', 'official' => 'no'],
                ['title' => 'Community Health Outreach Charity', 'type' => 'Charity', 'mode' => 'In-person', 'price_range' => 'Free', 'price_label' => 'Donations', 'tags' => 'Health|Charity|Outreach', 'official' => 'no'],
            ],
            'startup_tech' => [
                ['title' => 'AI Workflow Startup Profile', 'type' => 'SaaS', 'mode' => 'Hybrid', 'price_range' => 'Premium', 'price_label' => 'Series A', 'tags' => 'AI|Startup|SaaS', 'official' => 'no'],
                ['title' => 'Climate Tech Venture Studio', 'type' => 'Climate Tech', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => 'Seed Stage', 'tags' => 'Climate|Startup|Venture', 'official' => 'no'],
                ['title' => 'Developer Tooling Company', 'type' => 'Developer Tools', 'mode' => 'Online Stream Event', 'price_range' => 'Paid', 'price_label' => 'Growth Stage', 'tags' => 'Developers|APIs|Tooling', 'official' => 'no'],
                ['title' => 'Fintech Platform Builder', 'type' => 'Fintech', 'mode' => 'Hybrid', 'price_range' => 'Premium', 'price_label' => 'Series B', 'tags' => 'Fintech|Payments|Scale-up', 'official' => 'yes'],
            ],
            'tourism_attractions' => [
                ['title' => 'Historic Castle Guided Tour', 'type' => 'Attraction', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$25', 'tags' => 'Tourism|Castle|Guided', 'official' => 'no'],
                ['title' => 'City Museum Experience', 'type' => 'Museum', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$18', 'tags' => 'Museum|Culture|City', 'official' => 'yes'],
                ['title' => 'Adventure Park Outdoor Pass', 'type' => 'Adventure', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$60', 'tags' => 'Adventure|Outdoor|Family', 'official' => 'no'],
                ['title' => 'Scenic Boat Cruise Attraction', 'type' => 'Tour', 'mode' => 'In-person', 'price_range' => 'Paid', 'price_label' => '$35', 'tags' => 'Cruise|Scenic|Tour', 'official' => 'no'],
            ],
        ];

        $profile_key = sanitize_key((string) $profile_key);
        if (!isset($topics[$profile_key]) || !is_array($topics[$profile_key])) {
            return (array) ($topics['events'] ?? []);
        }
        return (array) $topics[$profile_key];
    }

    private function get_demo_photo_urls_from_folder($relative_folder) {
        $relative_folder = trim((string) $relative_folder, "/\\");
        if ($relative_folder === '') {
            return [];
        }

        $source_mode = $this->get_demo_media_source_mode();
        if ($source_mode === 'remote') {
            $remote_urls = $this->get_demo_photo_urls_from_remote_folder($relative_folder);
            if (!empty($remote_urls)) {
                return $remote_urls;
            }
            // Graceful fallback if remote source is unavailable.
            $source_mode = 'auto';
        }

        $local_urls = [];
        if ($source_mode === 'local' || $source_mode === 'auto') {
            $local_relative_folders = array_values(array_unique(array_filter([
                'assets/demo-media/' . $relative_folder,
                $relative_folder,
            ])));
            foreach ($local_relative_folders as $local_relative_folder) {
                $base_dir = trailingslashit((string) plugin_dir_path(NEXDIRECTORY_PLUGIN_FILE)) . $local_relative_folder;
                if (!is_dir($base_dir) || !is_readable($base_dir)) {
                    continue;
                }

                $patterns = [
                    $base_dir . '/*.jpg',
                    $base_dir . '/*.jpeg',
                    $base_dir . '/*.png',
                    $base_dir . '/*.webp',
                    $base_dir . '/*.gif',
                    $base_dir . '/*.avif',
                    $base_dir . '/*.JPG',
                    $base_dir . '/*.JPEG',
                    $base_dir . '/*.PNG',
                    $base_dir . '/*.WEBP',
                    $base_dir . '/*.GIF',
                    $base_dir . '/*.AVIF',
                ];

                $files = [];
                foreach ($patterns as $pattern) {
                    $matched = glob($pattern);
                    if (!is_array($matched) || empty($matched)) {
                        continue;
                    }
                    $files = array_merge($files, $matched);
                }

                if (!empty($files)) {
                    $files = array_values(array_unique(array_filter($files, 'is_file')));
                    natcasesort($files);
                    $files = array_values($files);

                    $base_url = trailingslashit((string) plugins_url($local_relative_folder, NEXDIRECTORY_PLUGIN_FILE));
                    foreach ($files as $file) {
                        $filename = basename((string) $file);
                        if ($filename === '' || $filename[0] === '.') {
                            continue;
                        }
                        $local_urls[] = esc_url_raw($base_url . rawurlencode($filename));
                    }
                }
            }
        }

        if (!empty($local_urls)) {
            return array_values(array_unique(array_filter($local_urls)));
        }

        return $this->get_demo_photo_urls_from_remote_folder($relative_folder);
    }

    private function get_demo_media_source_mode() {
        $mode = (string) get_option('nexdirectory_demo_media_source_mode', 'local');
        $mode = sanitize_key((string) apply_filters('nexdirectory_demo_media_source_mode', $mode));
        if (!in_array($mode, ['remote', 'local', 'auto'], true)) {
            $mode = 'local';
        }
        return $mode;
    }

    private function get_demo_media_remote_base_url() {
        $url = (string) get_option('nexdirectory_demo_media_base_url', '');
        $url = esc_url_raw((string) apply_filters('nexdirectory_demo_media_base_url', $url));
        if ($url === '') {
            return '';
        }
        return trailingslashit($url);
    }

    private function get_demo_remote_photo_file_map() {
        return [
            'Photos/Corporate' => [
                'Event_1_0000_event24.jpg',
                'Event_1_0001_event23.jpg',
                'Event_1_0002_event22.jpg',
                'Event_1_0003_event21.jpg',
                'Event_1_0004_event20.jpg',
                'Event_1_0005_event19.jpg',
                'Event_1_0006_event18.jpg',
                'Event_1_0007_event17.jpg',
                'Event_1_0008_event16.jpg',
                'Event_1_0009_event15.jpg',
                'Event_1_0010_event14.jpg',
                'Event_1_0011_event13.jpg',
                'Event_1_0012_event12.jpg',
                'Event_1_0013_event11.jpg',
                'Event_1_0014_event10_1.jpg',
                'Event_1_0015_event9_1.jpg',
                'Event_1_0016_event8_1.jpg',
                'Event_1_0017_event7_1.jpg',
                'Event_1_0018_event6_1.jpg',
                'Event_1_0019_event4_1.jpg',
                'Event_1_0020_event3_1.jpg',
                'Event_1_0021_event2_1.jpg',
                'Event_1_0022_event1_1.jpg',
            ],
            'Photos/headshots/man' => [
                'man1.jpg',
                'man2.jpg',
                'man3.jpg',
                'man4.jpg',
                'man5.jpg',
                'man6.jpg',
                'man7.jpg',
                'man8.jpg',
                'man9.jpg',
            ],
            'Photos/headshots/woman' => [
                'woman1.jpg',
                'woman3.jpg',
                'woman4.jpg',
                'woman5.jpg',
                'woman6.jpg',
                'woman7.jpg',
            ],
        ];
    }

    private function get_demo_photo_urls_from_remote_folder($relative_folder) {
        $relative_folder = trim((string) $relative_folder, "/\\");
        if ($relative_folder === '') {
            return [];
        }

        $file_map = $this->get_demo_remote_photo_file_map();
        $files = (array) ($file_map[$relative_folder] ?? []);
        if (empty($files)) {
            return [];
        }

        $base_url = $this->get_demo_media_remote_base_url();
        if ($base_url === '') {
            return [];
        }
        $folder_path = str_replace('\\', '/', $relative_folder);
        $urls = [];
        foreach ($files as $file_ref) {
            $file_ref = trim((string) $file_ref);
            if ($file_ref === '') {
                continue;
            }

            if (preg_match('#^https?://#i', $file_ref) === 1) {
                $urls[] = esc_url_raw($file_ref);
                continue;
            }

            // Keep exact filename case for case-sensitive remote paths.
            $filename = trim((string) wp_basename(str_replace('\\', '/', $file_ref)));
            if ($filename === '' || $filename[0] === '.') {
                continue;
            }
            $urls[] = esc_url_raw($base_url . trim($folder_path, '/') . '/' . rawurlencode($filename));
        }

        return array_values(array_unique(array_filter($urls)));
    }

    private function get_demo_corporate_photo_urls() {
        $photos = $this->get_demo_photo_urls_from_folder('Photos/Corporate');
        if (!empty($photos)) {
            return $photos;
        }
        // Backward compatibility for older demo media folder structures.
        return $this->get_demo_photo_urls_from_folder('Photos/DemoContent');
    }

    private function get_demo_headshot_photo_urls($gender = 'man') {
        $gender = sanitize_key((string) $gender);
        if (!in_array($gender, ['man', 'woman'], true)) {
            $gender = 'man';
        }
        return $this->get_demo_photo_urls_from_folder('Photos/headshots/' . $gender);
    }

    private function pick_demo_headshot_url($gender = 'man', array $fallback_urls = []) {
        $headshots = $this->get_demo_headshot_photo_urls($gender);
        if (!empty($headshots)) {
            return esc_url_raw((string) $headshots[wp_rand(0, count($headshots) - 1)]);
        }

        $fallback_urls = array_values(array_filter(array_map('esc_url_raw', $fallback_urls)));
        if (!empty($fallback_urls)) {
            return esc_url_raw((string) $fallback_urls[wp_rand(0, count($fallback_urls) - 1)]);
        }

        return '';
    }

    private function normalize_demo_row_remote_media_urls(array $row) {
        $normalized = [];
        foreach ($row as $row_key => $row_value) {
            $row_key = sanitize_key((string) $row_key);
            if ($row_key === '') {
                continue;
            }
            $normalized[$row_key] = is_string($row_value) ? trim($row_value) : $row_value;
        }
        $row = $normalized;
        if (empty($row)) {
            return [];
        }

        $url_keys = [
            'cover_image_url',
        ];
        foreach ($url_keys as $url_key) {
            if (!array_key_exists($url_key, $row)) {
                continue;
            }
            $row[$url_key] = $this->normalize_demo_media_url_to_remote((string) $row[$url_key]);
        }

        $gallery_urls = $this->parse_multiline_urls((string) ($row['gallery_urls'] ?? ''));
        if (!empty($gallery_urls)) {
            $gallery_urls = array_values(array_filter(array_map(function ($url) {
                return $this->normalize_demo_media_url_to_remote((string) $url);
            }, $gallery_urls)));
            $row['gallery_urls'] = implode("\n", $gallery_urls);
        }

        $key_speakers = $this->parse_key_speakers((string) ($row['key_speakers'] ?? '[]'));
        if (!empty($key_speakers)) {
            foreach ($key_speakers as $speaker_index => $speaker_row) {
                if (!is_array($speaker_row)) {
                    continue;
                }
                if (!empty($speaker_row['image'])) {
                    $key_speakers[$speaker_index]['image'] = $this->normalize_demo_media_url_to_remote((string) $speaker_row['image']);
                }
            }
            $row['key_speakers'] = wp_json_encode($key_speakers);
        }

        $sessions = $this->parse_sessions((string) ($row['sessions'] ?? '[]'));
        if (!empty($sessions)) {
            foreach ($sessions as $session_index => $session_row) {
                if (!is_array($session_row)) {
                    continue;
                }
                $attachments = [];
                foreach ((array) ($session_row['attachments'] ?? []) as $attachment_url) {
                    $attachments[] = $this->normalize_demo_media_url_to_remote((string) $attachment_url);
                }
                $sessions[$session_index]['attachments'] = array_values(array_unique(array_filter($attachments)));
            }
            $row['sessions'] = wp_json_encode($sessions);
        }

        return $row;
    }

    private function normalize_demo_media_url_to_remote($url) {
        $url = trim((string) $url);
        if ($url === '') {
            return '';
        }
        if (preg_match('#^(https?:)?//#i', $url) !== 1) {
            $local_path = $this->resolve_bundled_demo_media_url_to_path($url);
            if ($local_path !== '') {
                $local_url = $this->get_bundled_demo_media_url_from_path($local_path);
                if ($local_url !== '') {
                    return $local_url;
                }
            }

            $relative = ltrim(str_replace('\\', '/', $url), '/');
            $photos_pos = stripos($relative, 'Photos/');
            if ($photos_pos === false) {
                return sanitize_text_field($url);
            }
            $relative = substr($relative, $photos_pos);
            $relative = ltrim((string) $relative, '/');
            if ($relative === '') {
                return sanitize_text_field($url);
            }
            $remote_base_url = $this->get_demo_media_remote_base_url();
            if ($remote_base_url === '') {
                return sanitize_text_field($url);
            }
            return esc_url_raw($remote_base_url . $relative);
        }

        $normalized_url = esc_url_raw($url);
        if ($normalized_url === '') {
            return '';
        }
        if ($this->resolve_bundled_demo_media_url_to_path($normalized_url) !== '') {
            return $normalized_url;
        }

        $path = trim((string) wp_parse_url($normalized_url, PHP_URL_PATH), '/');
        if ($path === '') {
            return $normalized_url;
        }

        $photos_pos = stripos($path, 'Photos/');
        if ($photos_pos === false) {
            return $normalized_url;
        }

        $relative_path = substr($path, $photos_pos);
        $relative_path = ltrim((string) $relative_path, '/');
        if ($relative_path === '') {
            return $normalized_url;
        }

        $remote_base_url = $this->get_demo_media_remote_base_url();
        if ($remote_base_url === '') {
            return $normalized_url;
        }

        return esc_url_raw($remote_base_url . $relative_path);
    }

    private function localize_demo_import_row_media_assets($post_id, array $row) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0 || empty($row)) {
            return $row;
        }

        $row = $this->normalize_demo_row_remote_media_urls($row);

        $cover = esc_url_raw((string) ($row['cover_image_url'] ?? ''));
        $cover_asset = $this->sideload_demo_image_for_post($cover, $post_id);
        if (!empty($cover_asset['url'])) {
            $row['cover_image_url'] = (string) $cover_asset['url'];
            update_post_meta($post_id, 'nexdirectory_cover_image_url', esc_url_raw((string) $cover_asset['url']));
        }
        $cover_attachment_id = absint((int) ($cover_asset['attachment_id'] ?? 0));
        if ($cover_attachment_id > 0) {
            set_post_thumbnail($post_id, $cover_attachment_id);
        }

        $gallery_urls = $this->parse_multiline_urls((string) ($row['gallery_urls'] ?? ''));
        if (!empty($gallery_urls)) {
            $localized_gallery = [];
            foreach ($gallery_urls as $gallery_url) {
                $asset = $this->sideload_demo_image_for_post((string) $gallery_url, $post_id);
                $localized_gallery[] = (string) ($asset['url'] ?? $gallery_url);
            }
            $localized_gallery = array_values(array_unique(array_filter(array_map('esc_url_raw', $localized_gallery))));
            $row['gallery_urls'] = implode("\n", $localized_gallery);
            update_post_meta($post_id, 'nexdirectory_gallery_urls', nexdirectory_sanitize_textarea((string) $row['gallery_urls']));
        }

        $key_speakers = $this->parse_key_speakers((string) ($row['key_speakers'] ?? '[]'));
        if (!empty($key_speakers)) {
            foreach ($key_speakers as $speaker_index => $speaker_row) {
                if (!is_array($speaker_row)) {
                    continue;
                }
                $speaker_image = esc_url_raw((string) ($speaker_row['image'] ?? ''));
                if ($speaker_image === '') {
                    continue;
                }
                $asset = $this->sideload_demo_image_for_post($speaker_image, $post_id);
                if (!empty($asset['url'])) {
                    $key_speakers[$speaker_index]['image'] = esc_url_raw((string) $asset['url']);
                }
            }
            $row['key_speakers'] = wp_json_encode($key_speakers);
            update_post_meta($post_id, 'nexdirectory_key_speakers', wp_json_encode($key_speakers));
        }

        $sessions = $this->parse_sessions((string) ($row['sessions'] ?? '[]'));
        if (!empty($sessions)) {
            foreach ($sessions as $session_index => $session_row) {
                if (!is_array($session_row)) {
                    continue;
                }
                $attachments = [];
                foreach ((array) ($session_row['attachments'] ?? []) as $attachment_url) {
                    $attachment_url = (string) $attachment_url;
                    if ($this->is_demo_image_url_candidate($attachment_url)) {
                        $asset = $this->sideload_demo_image_for_post($attachment_url, $post_id);
                        $attachments[] = (string) ($asset['url'] ?? $attachment_url);
                        continue;
                    }
                    $attachments[] = $attachment_url;
                }
                $sessions[$session_index]['attachments'] = array_values(array_unique(array_filter(array_map('sanitize_text_field', $attachments))));
            }
            $row['sessions'] = wp_json_encode($sessions);
            update_post_meta($post_id, 'nexdirectory_sessions', wp_json_encode($sessions));
        }

        return $row;
    }

    private function is_demo_image_url_candidate($url) {
        $url = esc_url_raw((string) $url);
        if ($url === '' || !wp_http_validate_url($url)) {
            return false;
        }
        $path = (string) wp_parse_url($url, PHP_URL_PATH);
        if ($path === '') {
            return false;
        }
        $ext = strtolower((string) pathinfo($path, PATHINFO_EXTENSION));
        return in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif', 'avif'], true);
    }

    private function resolve_bundled_demo_media_url_to_path($url) {
        $url = trim((string) $url);
        if ($url === '') {
            return '';
        }

        $plugin_dir = trailingslashit((string) plugin_dir_path(NEXDIRECTORY_PLUGIN_FILE));
        $base_candidates = [
            [
                'relative_base' => 'assets/demo-media/',
                'base_dir' => $plugin_dir . 'assets/demo-media/',
            ],
            [
                'relative_base' => 'Photos/',
                'base_dir' => $plugin_dir . 'assets/demo-media/Photos/',
            ],
            [
                'relative_base' => 'Photos/',
                'base_dir' => $plugin_dir . 'Photos/',
            ],
        ];

        if (preg_match('#^(https?:)?//#i', $url) !== 1) {
            $relative_url = ltrim(str_replace('\\', '/', $url), '/');
            foreach ($base_candidates as $base_candidate) {
                $relative_base = (string) ($base_candidate['relative_base'] ?? '');
                $base_dir = (string) ($base_candidate['base_dir'] ?? '');
                if (strncmp($relative_url, $relative_base, strlen($relative_base)) !== 0) {
                    continue;
                }
                return $this->resolve_demo_media_local_path($base_dir, substr($relative_url, strlen($relative_base)));
            }
            return '';
        }

        $url_path = (string) wp_parse_url(esc_url_raw($url), PHP_URL_PATH);
        if ($url_path === '') {
            return '';
        }
        $url_path = '/' . ltrim(rawurldecode(str_replace('\\', '/', $url_path)), '/');

        foreach ($base_candidates as $base_candidate) {
            $relative_base = (string) ($base_candidate['relative_base'] ?? '');
            $base_dir = (string) ($base_candidate['base_dir'] ?? '');
            $base_url_path = (string) wp_parse_url(plugins_url($relative_base, NEXDIRECTORY_PLUGIN_FILE), PHP_URL_PATH);
            if ($base_url_path === '') {
                continue;
            }
            $base_url_path = '/' . trim(rawurldecode(str_replace('\\', '/', $base_url_path)), '/') . '/';
            if (strncmp($url_path, $base_url_path, strlen($base_url_path)) !== 0) {
                continue;
            }
            return $this->resolve_demo_media_local_path($base_dir, substr($url_path, strlen($base_url_path)));
        }

        return '';
    }

    private function get_bundled_demo_media_url_from_path($local_path) {
        $local_path = (string) $local_path;
        if ($local_path === '') {
            return '';
        }

        $plugin_dir = trailingslashit((string) plugin_dir_path(NEXDIRECTORY_PLUGIN_FILE));
        $base_candidates = [
            [
                'base_dir' => $plugin_dir . 'assets/demo-media/',
                'base_url' => 'assets/demo-media/',
            ],
            [
                'base_dir' => $plugin_dir . 'Photos/',
                'base_url' => 'Photos/',
            ],
        ];

        $local_real = realpath($local_path);
        if (!is_string($local_real) || $local_real === '') {
            return '';
        }
        $local_real = str_replace('\\', '/', $local_real);

        foreach ($base_candidates as $base_candidate) {
            $base_dir = (string) ($base_candidate['base_dir'] ?? '');
            $base_url = (string) ($base_candidate['base_url'] ?? '');
            $base_real = realpath($base_dir);
            if (!is_string($base_real) || $base_real === '' || $base_url === '') {
                continue;
            }
            $base_real = rtrim(str_replace('\\', '/', $base_real), '/') . '/';
            if (strncmp($local_real, $base_real, strlen($base_real)) !== 0) {
                continue;
            }
            $relative_path = ltrim(substr($local_real, strlen($base_real)), '/');
            if ($relative_path === '') {
                continue;
            }
            $relative_parts = array_map('rawurlencode', explode('/', $relative_path));
            return esc_url_raw((string) plugins_url($base_url . implode('/', $relative_parts), NEXDIRECTORY_PLUGIN_FILE));
        }

        return '';
    }

    private function resolve_demo_media_local_path($base_dir, $relative_path) {
        $base_dir = (string) $base_dir;
        $relative_path = ltrim(str_replace('\\', '/', rawurldecode((string) $relative_path)), '/');
        if ($base_dir === '' || $relative_path === '' || strpos($relative_path, '..') !== false) {
            return '';
        }

        $base_real = realpath($base_dir);
        $candidate_real = realpath(trailingslashit($base_dir) . $relative_path);
        if (!is_string($base_real) || !is_string($candidate_real)) {
            return '';
        }

        $base_real = rtrim(str_replace('\\', '/', $base_real), '/') . '/';
        $candidate_real_normalized = str_replace('\\', '/', $candidate_real);
        if (strncmp($candidate_real_normalized, $base_real, strlen($base_real)) !== 0) {
            return '';
        }
        if (!is_file($candidate_real) || !is_readable($candidate_real)) {
            return '';
        }

        return $candidate_real;
    }

    private function sideload_demo_image_for_post($url, $post_id = 0) {
        $result = [
            'url' => esc_url_raw((string) $url),
            'attachment_id' => 0,
        ];
        $url = (string) ($result['url'] ?? '');
        $post_id = absint((int) $post_id);
        if (!$this->is_demo_image_url_candidate($url)) {
            return $result;
        }

        static $cache = [];
        $cache_key = md5($url);
        if (isset($cache[$cache_key]) && is_array($cache[$cache_key])) {
            $cached = (array) $cache[$cache_key];
            $cached_id = absint((int) ($cached['attachment_id'] ?? 0));
            if ($cached_id > 0 && $post_id > 0) {
                wp_update_post([
                    'ID' => $cached_id,
                    'post_parent' => $post_id,
                ]);
            }
            return [
                'url' => esc_url_raw((string) ($cached['url'] ?? $url)),
                'attachment_id' => $cached_id,
            ];
        }

        $existing = get_posts([
            'post_type' => 'attachment',
            'post_status' => 'inherit',
            'posts_per_page' => 1,
            'fields' => 'ids',
            'meta_key' => '_nexdirectory_demo_remote_source_url',
            'meta_value' => $url,
            'orderby' => 'ID',
            'order' => 'DESC',
        ]);
        $existing_id = !empty($existing) ? absint((int) $existing[0]) : 0;
        if ($existing_id > 0) {
            $existing_url = esc_url_raw((string) wp_get_attachment_url($existing_id));
            if ($existing_url !== '') {
                if ($post_id > 0) {
                    wp_update_post([
                        'ID' => $existing_id,
                        'post_parent' => $post_id,
                    ]);
                }
                $cache[$cache_key] = [
                    'url' => $existing_url,
                    'attachment_id' => $existing_id,
                ];
                return [
                    'url' => $existing_url,
                    'attachment_id' => $existing_id,
                ];
            }
        }

        $path = (string) wp_parse_url($url, PHP_URL_PATH);
        $filename = sanitize_file_name((string) wp_basename($path));
        if ($filename === '') {
            $filename = 'demo-image-' . substr($cache_key, 0, 12) . '.jpg';
        }

        $bundled_source_file = $this->resolve_bundled_demo_media_url_to_path($url);
        if ($bundled_source_file !== '') {
            $stored = $this->store_file_source_as_attachment($bundled_source_file, $filename, $post_id, 'demo-media');
        } else {
            if (!function_exists('download_url')) {
                require_once ABSPATH . 'wp-admin/includes/file.php';
            }
            $tmp_file = download_url($url, 60);
            if (is_wp_error($tmp_file)) {
                return $result;
            }
            if (!is_file($tmp_file) || filesize($tmp_file) > 8 * MB_IN_BYTES) {
                wp_delete_file($tmp_file);
                return $result;
            }
            $stored = $this->store_file_source_as_attachment($tmp_file, $filename, $post_id, 'demo-media');
            wp_delete_file($tmp_file);
        }

        if (is_wp_error($stored)) {
            return $result;
        }

        $attachment_id = absint((int) ($stored['attachment_id'] ?? 0));
        $local_url = esc_url_raw((string) ($stored['url'] ?? ''));
        if ($local_url === '') {
            return $result;
        }

        update_post_meta($attachment_id, '_nexdirectory_demo_remote_source_url', $url);
        update_post_meta($attachment_id, '_nexdirectory_demo_media_localized', '1');

        $cache[$cache_key] = [
            'url' => $local_url,
            'attachment_id' => $attachment_id,
        ];

        return [
            'url' => $local_url,
            'attachment_id' => $attachment_id,
        ];
    }


    private function get_demo_profile_media_pack($profile_key) {
        $keyword_map = [
            'events' => ['event conference', 'robotics lab', 'aerospace launch', 'technology seminar', 'innovation summit', 'professional meetup', 'auditorium keynote', 'event team'],
            'directory' => ['local shop storefront', 'small business owner', 'agency workspace', 'service provider office', 'community listing', 'bookstore aisle', 'coffee counter', 'independent retailer'],
            'local_businesses' => ['local shop storefront', 'small business owner', 'bakery display', 'bookstore aisle', 'coffee counter', 'neighborhood store', 'florist shop', 'independent retailer'],
            'trades_contractors' => ['construction worker', 'electrician tools', 'plumber service', 'home renovation', 'contractor team', 'boiler install', 'building repair', 'trade specialist'],
            'professional_services' => ['consulting office', 'legal advisor desk', 'accounting team', 'agency workspace', 'business meeting', 'service professionals', 'corporate discussion', 'strategy session'],
            'clubs_community' => ['community club', 'local meetup', 'group discussion', 'sports club', 'chess match', 'community volunteers', 'neighborhood gathering', 'creative workshop'],
            'venues_directory' => ['wedding venue hall', 'conference venue', 'event space interior', 'banquet hall', 'rooftop venue', 'venue lights', 'ballroom setup', 'conference stage'],
            'property_agents' => ['real estate office', 'modern apartment', 'property exterior', 'home interior', 'estate agent', 'for sale house', 'city apartment', 'residential property'],
            'health_wellness' => ['wellness clinic', 'physiotherapy session', 'nutrition coach', 'mental health support', 'yoga studio', 'health consultation', 'wellness center', 'therapy room'],
            'restaurants_hospitality' => ['restaurant interior', 'chef kitchen', 'hotel lobby', 'cafe counter', 'fine dining', 'hospitality service', 'rooftop dining', 'coffee shop'],
            'education_training' => ['classroom training', 'students learning', 'coding bootcamp', 'lecture hall', 'workshop class', 'education campus', 'teacher presentation', 'skills training'],
            'job_board' => ['job interview', 'recruitment fair', 'office team', 'career hiring', 'professional resume', 'workplace meeting', 'job seeker', 'business hiring'],
            'marketplace' => ['marketplace products', 'second hand goods', 'classified listing', 'local market stall', 'vintage camera', 'furniture sale', 'bike resale', 'online seller'],
            'classifieds_marketplace' => ['marketplace products', 'second hand goods', 'classified listing', 'local market stall', 'vintage camera', 'furniture sale', 'bike resale', 'online seller'],
            'franchises_multilocation' => ['franchise storefront', 'brand locations', 'retail chain', 'business expansion', 'multi location business', 'franchise owner', 'chain cafe', 'brand headquarters'],
            'nonprofits_charities' => ['charity volunteers', 'nonprofit team', 'community donation', 'food bank support', 'outreach program', 'charity event', 'volunteer group', 'social impact'],
            'startup_tech' => ['startup office', 'tech founders', 'developer team', 'product launch', 'ai startup', 'venture capital pitch', 'innovation workspace', 'software company'],
            'tourism_attractions' => ['tourist attraction', 'historic castle', 'museum gallery', 'city landmark', 'scenic view', 'boat cruise', 'travel destination', 'adventure park'],
        ];

        $profile_key = sanitize_key((string) $profile_key);
        $keywords = isset($keyword_map[$profile_key]) && is_array($keyword_map[$profile_key]) ? $keyword_map[$profile_key] : (array) ($keyword_map['events'] ?? []);
        if (empty($keywords)) {
            $keywords = ['professional listing'];
        }

        $cover_images = $this->get_demo_corporate_photo_urls();
        $gallery_images = $cover_images;

        if (empty($cover_images) || empty($gallery_images)) {
            $cover_images = [];
            $gallery_images = [];
            $placeholder_hero_url = esc_url_raw(plugins_url('assets/images/placeholder-hero.svg', NEXDIRECTORY_PLUGIN_FILE));
            foreach ($keywords as $index => $keyword) {
                $keyword = trim((string) $keyword);
                if ($keyword === '') {
                    continue;
                }
                $cover_images[] = $placeholder_hero_url;
                $gallery_images[] = $placeholder_hero_url;
            }
        }

        $video_urls = [];
        $supporting_docs = [];

        return [
            'cover_images' => array_values(array_unique(array_filter($cover_images))),
            'gallery_images' => array_values(array_unique(array_filter($gallery_images))),
            'video_urls' => array_values(array_unique(array_filter($video_urls))),
            'supporting_docs' => $supporting_docs,
        ];
    }

    private function get_demo_profile_config($profile_key) {
        $catalog = $this->get_demo_profile_catalog();
        $profile_key = sanitize_key((string) $profile_key);
        if ($profile_key === '' || !isset($catalog[$profile_key])) {
            $profile_key = $this->get_demo_profile_key_for_platform_mode();
        }
        if ($profile_key === '' || !isset($catalog[$profile_key])) {
            $profile_key = 'events';
        }
        if (!isset($catalog[$profile_key])) {
            $first_key = array_key_first($catalog);
            $profile_key = is_string($first_key) ? $first_key : 'events';
        }
        $config = (array) ($catalog[$profile_key] ?? []);
        $config['key'] = $profile_key;
        $config['label'] = sanitize_text_field((string) ($config['label'] ?? 'Demo Profile'));
        $config['description'] = sanitize_text_field((string) ($config['description'] ?? ''));
        return $config;
    }


    private function get_public_demo_import_profile_keys() {
        return ['events', 'directory', 'job_board', 'marketplace'];
    }


    private function get_public_demo_profile_catalog() {
        $catalog = (array) $this->get_demo_profile_catalog();
        $allowed_keys = $this->get_public_demo_import_profile_keys();
        $filtered = [];
        foreach ($allowed_keys as $allowed_key) {
            if (isset($catalog[$allowed_key]) && is_array($catalog[$allowed_key])) {
                $filtered[$allowed_key] = $catalog[$allowed_key];
            }
        }

        if (empty($filtered)) {
            if (isset($catalog['events']) && is_array($catalog['events'])) {
                $filtered = ['events' => $catalog['events']];
            } else {
                $filtered = [
                    'events' => ['label' => __('Events (default)', 'nexdirectory')],
                ];
            }
        }

        return $filtered;
    }


    private function resolve_demo_import_profile_key($requested_profile = '') {
        $requested_profile = sanitize_key((string) $requested_profile);
        $catalog = $this->get_public_demo_profile_catalog();
        $available_keys = array_values(array_filter(array_map('sanitize_key', array_keys($catalog))));
        if ($requested_profile !== '' && in_array($requested_profile, $available_keys, true)) {
            return $requested_profile;
        }
        if (in_array('events', $available_keys, true)) {
            return 'events';
        }
        return (string) ($available_keys[0] ?? 'events');
    }


    private function apply_demo_profile_settings(array $profile_config) {
        $accent = $this->sanitize_design_color((string) ($profile_config['accent'] ?? '#FF5E14'), '#FF5E14');
        $accent_hover = $this->sanitize_design_color((string) ($profile_config['accent_hover'] ?? '#E04F0D'), '#E04F0D');
        $hero_layout = sanitize_key((string) ($profile_config['hero_layout'] ?? 'cinematic'));
        if (!in_array($hero_layout, ['cinematic', 'compact', 'split', 'minimal', 'immersive', 'spotlight'], true)) {
            $hero_layout = 'cinematic';
        }
        $page_layout = sanitize_key((string) ($profile_config['page_layout'] ?? 'modern'));
        if (in_array($page_layout, ['balanced', 'compact-aside', 'cards-heavy'], true)) {
            $page_layout = 'fifty-fifty';
        }
        if (!in_array($page_layout, ['modern', 'single-column', 'editorial', 'fifty-fifty', 'left-sidebar', 'zig-zag', 'full3', 'zen'], true)) {
            $page_layout = 'modern';
        }
        $map_layout = sanitize_key((string) ($profile_config['map_layout'] ?? 'three-column'));
        if (!in_array($map_layout, ['three-column', 'below-list', 'below-grid', 'below-grid-large', 'grid-no-map', 'grid-no-map-large'], true)) {
            $map_layout = 'three-column';
        }
        $map_density = sanitize_key((string) ($profile_config['map_density'] ?? 'comfortable'));
        if (!in_array($map_density, ['compact', 'comfortable'], true)) {
            $map_density = 'comfortable';
        }
        $map_provider = sanitize_key((string) ($profile_config['map_provider'] ?? 'openstreetmap'));
        if (!in_array($map_provider, ['openstreetmap', 'mapbox', 'google'], true)) {
            $map_provider = 'openstreetmap';
        }
        $map_style = sanitize_key((string) ($profile_config['map_style'] ?? 'osm-standard'));
        if (!in_array($map_style, ['osm-standard', 'osm-hot', 'carto-light', 'carto-dark', 'topo', 'mapbox-streets', 'mapbox-outdoors', 'mapbox-satellite', 'google-roadmap', 'google-satellite', 'custom'], true)) {
            $map_style = 'osm-standard';
        }
        $hero_kicker = sanitize_text_field((string) ($profile_config['hero_kicker'] ?? ''));
        if ($hero_kicker === '') {
            $hero_kicker = 'Nex Directory';
        }

        update_option('nexdirectory_style_accent_color', $accent, false);
        update_option('nexdirectory_style_accent_hover_color', $accent_hover, false);
        update_option('nexdirectory_event_hero_layout', $hero_layout, false);
        update_option('nexdirectory_event_page_layout', $page_layout, false);
        update_option('nexdirectory_map_layout_default', $map_layout, false);
        update_option('nexdirectory_map_view_template', 'default', false);
        update_option('nexdirectory_map_results_density', $map_density, false);
        update_option('nexdirectory_map_provider', $map_provider, false);
        update_option('nexdirectory_map_style', $map_style, false);
        update_option('nexdirectory_event_hero_kicker_text', $hero_kicker, false);
        $site_profile_key = sanitize_key((string) ($profile_config['site_profile'] ?? $profile_config['key'] ?? 'events'));
        if ($site_profile_key !== '') {
            update_option('nexdirectory_site_profile', $site_profile_key, false);
        }

        $topics = isset($profile_config['topics']) && is_array($profile_config['topics']) ? $profile_config['topics'] : [];
        foreach ($topics as $topic) {
            if (!is_array($topic)) {
                continue;
            }
            $type = sanitize_text_field((string) ($topic['type'] ?? ''));
            $mode = sanitize_text_field((string) ($topic['mode'] ?? ''));
            $price = sanitize_text_field((string) ($topic['price_range'] ?? ''));
            if ($type !== '' && !term_exists($type, self::TAX_TYPE)) {
                wp_insert_term($type, self::TAX_TYPE);
            }
            if ($mode !== '' && !term_exists($mode, self::TAX_MODE)) {
                wp_insert_term($mode, self::TAX_MODE);
            }
            if ($price !== '' && !term_exists($price, self::TAX_PRICE)) {
                wp_insert_term($price, self::TAX_PRICE);
            }
            $tags = $this->parse_delimited_values((string) ($topic['tags'] ?? ''));
            foreach ($tags as $tag) {
                $tag = sanitize_text_field((string) $tag);
                if ($tag === '') {
                    continue;
                }
                if (!term_exists($tag, self::TAX_TAG)) {
                    wp_insert_term($tag, self::TAX_TAG);
                }
            }
        }
    }


    private function build_demo_gallery_urls_for_row(array $gallery_images, $cover_image, $profile_key, $row_index) {
        $gallery_images = array_values(array_unique(array_filter(array_map('esc_url_raw', $gallery_images), static function ($url) {
            return trim((string) $url) !== '';
        })));
        $cover_image = esc_url_raw((string) $cover_image);

        if ($cover_image !== '') {
            $gallery_images = array_values(array_filter($gallery_images, static function ($url) use ($cover_image) {
                return (string) $url !== $cover_image;
            }));
        }

        $target_count = wp_rand(3, 6);
        $selected = [];

        if (!empty($gallery_images)) {
            $pool_size = count($gallery_images);
            $start_offset = $pool_size > 0 ? (((int) $row_index) % $pool_size) : 0;
            for ($offset = 0; $offset < $pool_size && count($selected) < $target_count; $offset++) {
                $candidate = (string) ($gallery_images[($start_offset + $offset) % $pool_size] ?? '');
                if ($candidate !== '' && !in_array($candidate, $selected, true)) {
                    $selected[] = $candidate;
                }
            }
        }

        $slot = 0;
        while (count($selected) < $target_count && $slot < 24) {
            $fallback_url = esc_url_raw(plugins_url('assets/images/placeholder-hero.svg', NEXDIRECTORY_PLUGIN_FILE));
            if ($fallback_url !== '' && $fallback_url !== $cover_image && !in_array($fallback_url, $selected, true)) {
                $selected[] = $fallback_url;
            }
            $slot++;
        }

        return array_values(array_slice($selected, 0, 6));
    }


    private function build_demo_import_rows($count = 50, $profile_key = 'events') {
        $count = max(1, min(200, absint((int) $count)));
        $profile_key = sanitize_key((string) $profile_key);
        $profile_config = $this->get_demo_profile_config($profile_key);
        $topic_seeds = !empty($profile_config['topics']) && is_array($profile_config['topics'])
            ? array_values((array) $profile_config['topics'])
            : $this->get_demo_profile_topics($profile_key);
        if (empty($topic_seeds)) {
            $topic_seeds = $this->get_demo_profile_topics('events');
        }
        if (empty($topic_seeds)) {
            $topic_seeds = [
                [
                    'title' => __('Community Event', 'nexdirectory'),
                    'type' => __('Conference', 'nexdirectory'),
                    'mode' => __('In-person', 'nexdirectory'),
                    'price_range' => __('Free', 'nexdirectory'),
                    'price_label' => __('Free', 'nexdirectory'),
                    'tags' => 'Community|Networking',
                    'official' => 'yes',
                ],
            ];
        }

        $ui_labels = method_exists($this, 'get_site_profile_ui_labels')
            ? (array) $this->get_site_profile_ui_labels((string) ($profile_config['site_profile'] ?? $profile_key))
            : [];
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $singular_lower = sanitize_text_field((string) ($ui_labels['singular_lower'] ?? strtolower($singular_label)));
        $plural_label = sanitize_text_field((string) ($ui_labels['plural'] ?? __('Events', 'nexdirectory')));
        $is_events_profile = (string) ($ui_labels['is_events_profile'] ?? '1') === '1';
        $is_job_profile = (string) ($ui_labels['is_job_profile'] ?? '0') === '1';
        $is_single_org_demo = $profile_key === 'events_single_organisation';

        $brand_name = sanitize_text_field((string) $this->get_white_label_brand_name());
        if ($brand_name === '') {
            $brand_name = sanitize_text_field((string) get_bloginfo('name'));
        }
        if ($brand_name === '') {
            $brand_name = 'Nex Directory';
        }
        $brand_email_local = nexdirectory_sanitize_title($brand_name);
        if ($brand_email_local === '') {
            $brand_email_local = 'company';
        }
        $single_org_contact_email = $brand_email_local . '.events@example.com';
        $profile_label = sanitize_text_field((string) ($profile_config['label'] ?? 'Demo Profile'));
        $media_pack = isset($profile_config['media_pack']) && is_array($profile_config['media_pack'])
            ? (array) $profile_config['media_pack']
            : $this->get_demo_profile_media_pack($profile_key);
        $location_seeds = [
            ['city' => 'London', 'country' => 'United Kingdom', 'timezone' => 'Europe/London', 'lat' => '51.5074', 'lng' => '-0.1278', 'venue' => 'ExCeL London', 'address' => 'Royal Victoria Dock, 1 Western Gateway'],
            ['city' => 'New York', 'country' => 'United States', 'timezone' => 'America/New_York', 'lat' => '40.7128', 'lng' => '-74.0060', 'venue' => 'Javits Center', 'address' => '429 11th Ave'],
            ['city' => 'Toronto', 'country' => 'Canada', 'timezone' => 'America/Toronto', 'lat' => '43.6532', 'lng' => '-79.3832', 'venue' => 'Metro Toronto Convention Centre', 'address' => '255 Front St W'],
            ['city' => 'Berlin', 'country' => 'Germany', 'timezone' => 'Europe/Berlin', 'lat' => '52.5200', 'lng' => '13.4050', 'venue' => 'CityCube Berlin', 'address' => 'Messedamm 26'],
            ['city' => 'Paris', 'country' => 'France', 'timezone' => 'Europe/Paris', 'lat' => '48.8566', 'lng' => '2.3522', 'venue' => 'Palais des Congres', 'address' => '2 Place de la Porte Maillot'],
            ['city' => 'Singapore', 'country' => 'Singapore', 'timezone' => 'Asia/Singapore', 'lat' => '1.3521', 'lng' => '103.8198', 'venue' => 'Suntec Convention Centre', 'address' => '1 Raffles Blvd'],
            ['city' => 'Sydney', 'country' => 'Australia', 'timezone' => 'Australia/Sydney', 'lat' => '-33.8688', 'lng' => '151.2093', 'venue' => 'ICC Sydney', 'address' => '14 Darling Dr'],
            ['city' => 'Dubai', 'country' => 'United Arab Emirates', 'timezone' => 'Asia/Dubai', 'lat' => '25.2048', 'lng' => '55.2708', 'venue' => 'Dubai World Trade Centre', 'address' => 'Sheikh Zayed Rd'],
            ['city' => 'Tokyo', 'country' => 'Japan', 'timezone' => 'Asia/Tokyo', 'lat' => '35.6762', 'lng' => '139.6503', 'venue' => 'Tokyo Big Sight', 'address' => '3-11-1 Ariake'],
            ['city' => 'Sao Paulo', 'country' => 'Brazil', 'timezone' => 'America/Sao_Paulo', 'lat' => '-23.5505', 'lng' => '-46.6333', 'venue' => 'Expo Center Norte', 'address' => 'Rua Jose Bernardo Pinto, 333'],
        ];
        $cover_images = array_values(array_filter(array_map('esc_url_raw', (array) ($media_pack['cover_images'] ?? []))));
        $gallery_images = array_values(array_filter(array_map('esc_url_raw', (array) ($media_pack['gallery_images'] ?? []))));
        $video_urls = array_values(array_filter(array_map('esc_url_raw', (array) ($media_pack['video_urls'] ?? []))));
        $supporting_docs = array_values(array_filter(array_map('esc_url_raw', (array) ($media_pack['supporting_docs'] ?? []))));

        $corporate_images = $this->get_demo_corporate_photo_urls();
        if (!empty($corporate_images)) {
            $cover_images = $corporate_images;
            $gallery_images = $corporate_images;
        }

        if (empty($cover_images)) {
            $cover_images = [esc_url_raw(plugins_url('assets/images/placeholder-hero.svg', NEXDIRECTORY_PLUGIN_FILE))];
        }
        if (empty($gallery_images)) {
            $gallery_images = [esc_url_raw(plugins_url('assets/images/placeholder-hero.svg', NEXDIRECTORY_PLUGIN_FILE))];
        }
        if (empty($video_urls)) {
            $video_urls = [''];
        }
        if (empty($supporting_docs)) {
            $supporting_docs = [''];
        }
        $speaker_professions = ['Chief Innovation Officer', 'Research Director', 'Principal Engineer', 'Product Lead', 'Solutions Architect'];
        $session_notes_pool = [
            'Arrive 15 minutes early for badge scanning and seating.',
            'Interactive segment included. Bring your questions.',
            'Reference docs and slides are available in the attachment list.',
            'Networking opportunity follows this session.',
            'Save this item to your custom itinerary if attending.',
        ];

        $rows = [];
        $base_start = strtotime('+7 days 09:00');
        if (!$base_start) {
            $base_start = time() + (7 * DAY_IN_SECONDS);
        }

        $male_first_names = ['Liam', 'Noah', 'Ethan', 'Mason', 'James', 'Daniel', 'Aiden', 'Oliver', 'Henry', 'Lucas'];
        $female_first_names = ['Emma', 'Olivia', 'Ava', 'Sophia', 'Mia', 'Isabella', 'Amelia', 'Charlotte', 'Harper', 'Evelyn'];
        $last_names = ['Walker', 'Carter', 'Mitchell', 'Bennett', 'Reed', 'Coleman', 'Murphy', 'Morgan', 'Hayes', 'Foster'];

        for ($i = 0; $i < $count; $i++) {
            $topic = $topic_seeds[$i % count($topic_seeds)];
            $location = $location_seeds[$i % count($location_seeds)];
            $topic_title = sanitize_text_field((string) ($topic['title'] ?? 'Community Event'));
            $topic_type = sanitize_text_field((string) ($topic['type'] ?? 'Conference'));
            $topic_mode = sanitize_text_field((string) ($topic['mode'] ?? 'In-person'));
            $topic_price_range = sanitize_text_field((string) ($topic['price_range'] ?? 'Free'));
            $topic_price_label = sanitize_text_field((string) ($topic['price_label'] ?? 'Free'));
            $topic_tags = sanitize_text_field((string) ($topic['tags'] ?? 'Community'));
            $topic_official = $this->normalize_official_event_value($topic['official'] ?? '0');
            $cover_image = $cover_images[$i % count($cover_images)];
            $gallery_urls_for_row = $this->build_demo_gallery_urls_for_row($gallery_images, $cover_image, $profile_key, $i);
            $video_1 = $video_urls[$i % count($video_urls)];
            $video_2 = $video_urls[($i + 2) % count($video_urls)];
            $doc_1 = $supporting_docs[$i % count($supporting_docs)];
            $doc_2 = $supporting_docs[($i + 1) % count($supporting_docs)];

            $start_ts = (int) strtotime('+' . (string) ($i * 2) . ' days', $base_start);
            if ($is_events_profile && $is_single_org_demo) {
                $is_archived_row = stripos($topic_title, 'archive') !== false || ($i % 6 === 0);
                if ($is_archived_row) {
                    $past_offset_days = 30 + (($i * 11) % 330);
                    $start_ts = (int) strtotime('-' . (string) $past_offset_days . ' days 09:00');
                }
            }
            $duration_hours = 4 + (($i % 4) * 2);
            $end_ts = $start_ts + ($duration_hours * HOUR_IN_SECONDS);
            if ($i % 9 === 0) {
                $end_ts = $start_ts + (2 * DAY_IN_SECONDS);
            }

            $start = wp_date('Y-m-d H:i:s', $start_ts);
            $end = wp_date('Y-m-d H:i:s', $end_ts);
            $event_slug = nexdirectory_sanitize_title($topic_title . '-' . $location['city'] . '-' . ($i + 1));
            $item_path = nexdirectory_sanitize_title($plural_label);
            if ($item_path === '') {
                $item_path = 'events';
            }
            $event_url = 'https://example.com/' . $item_path . '/' . $event_slug;
            $contact_email = $is_single_org_demo
                ? $single_org_contact_email
                : ('listing+' . (string) ($i + 1) . '@nexdirectory.example');
            $speaker_profiles = [];
            if ($is_events_profile) {
                $speaker_count = 3 + ($i % 3);
                for ($speaker_index = 0; $speaker_index < $speaker_count; $speaker_index++) {
                    $speaker_gender = wp_rand(0, 1) === 0 ? 'woman' : 'man';
                    $speaker_first_name_pool = $speaker_gender === 'woman' ? $female_first_names : $male_first_names;
                    $speaker_first_name = $speaker_first_name_pool[wp_rand(0, count($speaker_first_name_pool) - 1)];
                    $speaker_last_name = $last_names[wp_rand(0, count($last_names) - 1)];
                    $speaker_prefix = $speaker_index % 2 === 0 ? 'Dr. ' : '';
                    $speaker_name = $speaker_prefix . $speaker_first_name . ' ' . $speaker_last_name;
                    $speaker_profiles[] = [
                        'name' => sanitize_text_field((string) $speaker_name),
                        'profession' => sanitize_text_field((string) ($speaker_professions[$speaker_index % count($speaker_professions)] ?? 'Speaker')),
                        'subject' => sanitize_text_field((string) $topic_type),
                        'image' => $this->pick_demo_headshot_url($speaker_gender, [$cover_image]),
                        'details' => sprintf(
                            'Leads practical sessions and live discussion around %s for this event.',
                            sanitize_text_field((string) strtolower($topic_type))
                        ),
                    ];
                }
            }

            $sessions = [];
            if ($is_events_profile) {
                $event_span_days = max(1, (int) floor(max(0, $end_ts - $start_ts) / DAY_IN_SECONDS) + 1);
                $day_count = min(3, $event_span_days);
                if ($day_count < 2 && $i % 5 === 0) {
                    $day_count = 2;
                }

                $session_templates = [
                    ['title' => 'Opening Session', 'offset_minutes' => 0, 'duration_minutes' => 90, 'location_suffix' => '', 'attachment_slot' => 0],
                    ['title' => 'Technical Deep Dive', 'offset_minutes' => 120, 'duration_minutes' => 110, 'location_suffix' => ' - Hall B', 'attachment_slot' => 1],
                    ['title' => 'Workshop Sprint', 'offset_minutes' => 270, 'duration_minutes' => 100, 'location_suffix' => ' - Studio', 'attachment_slot' => 2],
                    ['title' => 'Q&A and Networking', 'offset_minutes' => 410, 'duration_minutes' => 75, 'location_suffix' => ' - Lounge', 'attachment_slot' => 3],
                ];

                for ($day_offset = 0; $day_offset < $day_count; $day_offset++) {
                    $day_start_ts = (int) strtotime('+' . (string) $day_offset . ' days', $start_ts);
                    $day_label = wp_date('F j, Y', $day_start_ts);
                    foreach ($session_templates as $template_index => $template) {
                        $template = is_array($template) ? $template : [];
                        if ($day_offset === ($day_count - 1) && $template_index === 2 && $day_count > 1) {
                            continue;
                        }

                        $session_start_ts = $day_start_ts + ((int) ($template['offset_minutes'] ?? 0) * MINUTE_IN_SECONDS);
                        $session_end_ts = $session_start_ts + ((int) ($template['duration_minutes'] ?? 60) * MINUTE_IN_SECONDS);
                        $speaker_profile = !empty($speaker_profiles)
                            ? (array) ($speaker_profiles[($template_index + $day_offset) % count($speaker_profiles)] ?? [])
                            : [];
                        $session_attachments = [];
                        $session_attachments[] = $template_index % 2 === 0 ? $doc_1 : $doc_2;
                        if (!empty($gallery_urls_for_row)) {
                            $session_attachments[] = (string) ($gallery_urls_for_row[($template_index + $day_offset) % count($gallery_urls_for_row)] ?? '');
                        }
                        if ($template_index === 0 && $day_offset === 0) {
                            $session_attachments[] = 'dQw4w9WgXcQ';
                        }
                        $session_attachments = array_values(array_unique(array_filter(array_map('sanitize_text_field', $session_attachments))));
                        $session_title = sanitize_text_field((string) ($template['title'] ?? __('Session', 'nexdirectory')));
                        if ($day_count > 1) {
                            $session_title .= ' (Day ' . (string) ($day_offset + 1) . ')';
                        }
                        $session_notes = sprintf(
                            '%1$s %2$s',
                            (string) ($session_notes_pool[($i + $template_index) % count($session_notes_pool)] ?? ''),
                            sprintf('Track focus: %s.', sanitize_text_field((string) $topic_type))
                        );
                        $sessions[] = [
                            'title' => $session_title,
                            'start' => wp_date('Y-m-d H:i:s', $session_start_ts),
                            'end' => wp_date('Y-m-d H:i:s', $session_end_ts),
                            'location' => sanitize_text_field((string) $location['venue'] . (string) ($template['location_suffix'] ?? '')),
                            'speaker' => sanitize_text_field((string) ($speaker_profile['name'] ?? '')),
                            'day' => $day_label,
                            'notes' => $session_notes,
                            'attachments' => $session_attachments,
                            'session_key' => sanitize_key('demo-' . (string) ($i + 1) . '-d' . (string) ($day_offset + 1) . '-s' . (string) ($template_index + 1)),
                        ];
                    }
                }
            } elseif ($is_job_profile) {
                $sessions = [];
            } else {
                $sessions = [
                    [
                        'title' => 'Open For Enquiries',
                        'start' => $start,
                        'end' => wp_date('Y-m-d H:i:s', $start_ts + (8 * HOUR_IN_SECONDS)),
                        'location' => $location['city'],
                        'day' => wp_date('F j, Y', $start_ts),
                        'notes' => 'Team members are available throughout this window.',
                        'attachments' => [$doc_1],
                        'session_key' => sanitize_key('demo-' . (string) ($i + 1) . '-window-1'),
                    ],
                    [
                        'title' => 'Priority Booking Window',
                        'start' => wp_date('Y-m-d H:i:s', $start_ts + (9 * HOUR_IN_SECONDS)),
                        'end' => wp_date('Y-m-d H:i:s', $start_ts + (11 * HOUR_IN_SECONDS)),
                        'location' => $location['venue'],
                        'day' => wp_date('F j, Y', $start_ts),
                        'notes' => 'Limited slots are prioritised during this period.',
                        'attachments' => [$doc_2],
                        'session_key' => sanitize_key('demo-' . (string) ($i + 1) . '-window-2'),
                    ],
                ];
            }

            $key_speakers = $is_events_profile ? $speaker_profiles : [];
            $topic_tag_options = array_values(array_filter(array_map('nexdirectory_sanitize_text', array_map('trim', explode('|', (string) $topic_tags))), static function ($value) {
                return $value !== '';
            }));
            $topic_tag_options[] = sanitize_text_field((string) $topic_type);
            $topic_tag_options[] = sanitize_text_field((string) $location['city']);
            $topic_tag_options = array_values(array_unique(array_filter($topic_tag_options, static function ($value) {
                return trim((string) $value) !== '';
            })));
            if (empty($topic_tag_options)) {
                $topic_tag_options = ['General'];
            }
            $track_options = implode('|', array_slice($topic_tag_options, 0, 6));
            if ($track_options === '') {
                $track_options = 'General';
            }

            $custom_schema = $is_job_profile ? [] : [
                [
                    'id' => 'lead_sponsor',
                    'label' => $is_events_profile ? 'Lead Sponsor' : 'Primary Partner',
                    'type' => 'text',
                    'placeholder' => 'Sponsor company',
                    'help' => $is_events_profile ? 'Primary sponsor supporting this event.' : 'Primary partner supporting this listing.',
                    'required' => '0',
                    'show_in_event' => '1',
                    'options' => '',
                ],
                [
                    'id' => 'resource_pack',
                    'label' => 'Resource Pack URL',
                    'type' => 'url',
                    'placeholder' => 'https://',
                    'help' => $is_events_profile ? 'Public resource package for attendees.' : 'Public brochure, menu, or brochure URL.',
                    'required' => '0',
                    'show_in_event' => '1',
                    'options' => '',
                ],
                [
                    'id' => 'event_track',
                    'label' => $is_events_profile ? 'Event Track' : ($singular_label . ' Category'),
                    'type' => 'select',
                    'placeholder' => '',
                    'help' => $is_events_profile ? 'Main topic track for this edition.' : 'Main category for this listing.',
                    'required' => '0',
                    'show_in_event' => '1',
                    'options' => $track_options,
                ],
                [
                    'id' => 'certificates_available',
                    'label' => $is_events_profile ? 'Certificates Available' : 'Featured Listing',
                    'type' => 'checkbox',
                    'placeholder' => '',
                    'help' => $is_events_profile ? 'Whether attendees can request participation certificates.' : 'Highlight this listing as featured.',
                    'required' => '0',
                    'show_in_event' => '1',
                    'options' => '',
                ],
                [
                    'id' => 'speaker_notes',
                    'label' => $is_events_profile ? 'Speaker Notes' : 'Owner Notes',
                    'type' => 'wysiwyg',
                    'placeholder' => '',
                    'help' => $is_events_profile ? 'Short organizer note shown on event page.' : 'Short owner note shown on the listing page.',
                    'required' => '0',
                    'show_in_event' => '1',
                    'options' => '',
                ],
            ];

            $mode = $topic_mode;
            $stream_url = ($is_events_profile && ($mode === 'Online Stream Event' || $mode === 'Hybrid')) ? $video_1 : '';
            $title_value = $is_events_profile
                ? ($is_single_org_demo
                    ? $brand_name . ': ' . $topic_title . ' - ' . $location['city']
                    : $topic_title . ' ' . (string) (2026 + (int) floor($i / 25)) . ' - ' . $location['city'])
                : $topic_title . ' - ' . $location['city'];
            $description_value = $is_events_profile
                ? ($is_single_org_demo
                    ? sprintf(
                        'Official %1$s programme item for %2$s in %3$s. This demo reflects a single-company event calendar with sessions, media, map pinning, and moderation-ready metadata.',
                        $topic_title,
                        $brand_name,
                        $location['city']
                    )
                    : sprintf(
                        '%5$s demo %6$s %1$d showcasing submissions, map pins, media galleries, and moderation workflow for %2$s in %3$s. This sample includes full metadata, sessions, taxonomy terms, links, and downloadable documents for %4$s.',
                        (int) ($i + 1),
                        $topic_title,
                        $location['city'],
                        $brand_name,
                        $profile_label,
                        $singular_lower
                    ))
                : ($is_job_profile ? sprintf(
                    '%5$s demo job listing %1$d for %2$s in %3$s. This sample includes salary, role requirements, interview process, location setup, and full apply methods for %4$s.',
                    (int) ($i + 1),
                    $topic_title,
                    $location['city'],
                    $brand_name,
                    $profile_label
                ) : sprintf(
                    '%5$s demo %6$s %1$d showcasing directory workflows for %2$s in %3$s. This sample includes listing metadata, media assets, documents, taxonomy terms, and moderation/claiming flow for %4$s.',
                    (int) ($i + 1),
                    $topic_title,
                    $location['city'],
                    $brand_name,
                    $profile_label,
                    $singular_lower
                ));
            $excerpt_value = $is_events_profile
                ? ($is_single_org_demo
                    ? sprintf(
                        '%1$s official programme entry in %2$s with sessions, media, and attendee-ready details.',
                        $topic_title,
                        $location['city']
                    )
                    : sprintf(
                        'Demo %3$s for %1$s in %2$s. Includes images, videos, documents, sessions, and custom fields.',
                        $topic_title,
                        $location['city'],
                        $singular_lower
                    ))
                : ($is_job_profile ? sprintf(
                    'Demo job listing for %1$s in %2$s. Includes salary, deadline, and application details.',
                    $topic_title,
                    $location['city']
                ) : sprintf(
                    'Demo %3$s for %1$s in %2$s. Includes listing media, docs, business details, and custom fields.',
                    $topic_title,
                    $location['city'],
                    $singular_lower
                ));
            $is_recurring_topic = false;
            if ($is_events_profile) {
                if ($is_single_org_demo) {
                    $recurring_markers = ['meetup', 'mixer', 'forum', 'roadshow', 'open day'];
                    $title_lc = strtolower((string) $topic_title);
                    foreach ($recurring_markers as $marker) {
                        if (strpos($title_lc, (string) $marker) !== false) {
                            $is_recurring_topic = true;
                            break;
                        }
                    }
                } else {
                    $is_recurring_topic = $i % 7 === 0;
                }
            }
            $recurrence_value = $is_recurring_topic ? ($is_single_org_demo ? 'monthly' : 'weekly') : 'none';
            $recurrence_until_value = $is_recurring_topic ? wp_date('Y-m-d', strtotime('+8 weeks', max($start_ts, time()))) : '';
            $job_work_mode = 'on-site';
            if (stripos($topic_mode, 'hybrid') !== false) {
                $job_work_mode = 'hybrid';
            } elseif (stripos($topic_mode, 'online') !== false || stripos($topic_mode, 'remote') !== false || stripos($topic_mode, 'video') !== false) {
                $job_work_mode = 'remote';
            }
            $job_type = (string) ($topic['job_type'] ?? ($i % 6 === 0 ? 'Contract' : ($i % 3 === 0 ? 'Part-time' : 'Full-time')));
            $job_salary = $topic_price_label !== '' ? $topic_price_label : '$55,000 - $75,000';
            $job_department = (string) ($topic['department'] ?? ($topic_type !== '' ? $topic_type : 'Operations'));
            $job_apply_url = 'https://example.com/careers/' . $event_slug;
            $job_application_email = 'jobs+' . (string) ($i + 1) . '@nexdirectory.example';
            $job_closing_date = wp_date('Y-m-d', strtotime('+21 days', $start_ts));
            $job_expected_start_date = wp_date('Y-m-d', strtotime('+45 days', $start_ts));
            $job_remote_policy = $job_work_mode === 'hybrid' ? 'Hybrid (2-3 days on-site)' : ($job_work_mode === 'remote' ? 'Remote-first' : 'On-site');
            $job_skills = implode(', ', array_slice($topic_tag_options, 0, 5));
            $job_company_website = 'https://example.com/companies/' . nexdirectory_sanitize_title($brand_name . '-' . $location['city']);
            $job_company_size = ($i % 4 === 0) ? '201-500 employees' : (($i % 3 === 0) ? '51-200 employees' : '11-50 employees');
            $job_industry = $topic_type !== '' ? $topic_type : 'Technology';
            $email_templates = $is_events_profile
                ? [
                    'nexdirectory_email_subject_confirmation' => 'You are confirmed for {event_title}',
                    'nexdirectory_email_subject_reminder' => 'Reminder: {event_title} starts soon',
                    'nexdirectory_email_subject_update' => 'Update for {event_title}',
                    'nexdirectory_email_subject_cancellation' => '{event_title} has been cancelled',
                    'nexdirectory_email_body_confirmation' => "Hi {organizer_name},\n\nThanks for creating {event_title}. It is now listed for attendees.\n\nDate: {event_date}\nView: {event_url}",
                    'nexdirectory_email_body_reminder' => "Hi {organizer_name},\n\nFriendly reminder for {event_title}.\nDate: {event_date}\nURL: {event_url}",
                    'nexdirectory_email_body_update' => "Hi {organizer_name},\n\nYour event {event_title} was updated.\nDate: {event_date}\nURL: {event_url}",
                    'nexdirectory_email_body_cancellation' => "Hi {organizer_name},\n\n{event_title} has been cancelled.\nIf needed, create a new listing: {event_url}",
                ]
                : [
                    'nexdirectory_email_subject_confirmation' => 'Your listing is in review: {event_title}',
                    'nexdirectory_email_subject_reminder' => 'Reminder: review your listing {event_title}',
                    'nexdirectory_email_subject_update' => 'Listing update: {event_title}',
                    'nexdirectory_email_subject_cancellation' => 'Listing status changed: {event_title}',
                    'nexdirectory_email_body_confirmation' => "Hi {organizer_name},\n\nThanks for creating {event_title}. Your listing is now in review.\n\nDate: {event_date}\nView: {event_url}",
                    'nexdirectory_email_body_reminder' => "Hi {organizer_name},\n\nQuick reminder to review your listing details for {event_title}.\nDate: {event_date}\nURL: {event_url}",
                    'nexdirectory_email_body_update' => "Hi {organizer_name},\n\nYour listing {event_title} was updated.\nDate: {event_date}\nURL: {event_url}",
                    'nexdirectory_email_body_cancellation' => "Hi {organizer_name},\n\n{event_title} changed status. Please review and submit updates if needed.\nURL: {event_url}",
                ];

            $demo_custom_values = $is_job_profile ? [] : [
                'lead_sponsor' => $brand_name . ($is_events_profile ? ' Labs' : ' Network'),
                'resource_pack' => $event_url . '/resources',
                'event_track' => $topic_tag_options[$i % count($topic_tag_options)],
                'certificates_available' => $i % 2 === 0 ? '1' : '0',
                'speaker_notes' => '<p>Doors open 30 minutes early. Bring ID for fast check-in and network with peers from around the world.</p>',
            ];
            $rows[] = [
                'title' => $title_value,
                'description' => $description_value,
                'excerpt' => $excerpt_value,
                'start' => $start,
                'end' => $end,
                'timezone' => $location['timezone'],
                'venue' => $location['venue'],
                'address' => $location['address'],
                'city' => $location['city'],
                'country' => $location['country'],
                'lat' => $location['lat'],
                'lng' => $location['lng'],
                'capacity' => (string) (120 + (($i % 8) * 35)),
                'price' => $topic_price_label,
                'external_url' => $event_url,
                'stream_url' => $stream_url,
                'contact_email' => $contact_email,
                'contact_phone' => '+1-202-555-' . str_pad((string) (1000 + $i), 4, '0', STR_PAD_LEFT),
                'contact_name' => $brand_name . ($is_events_profile ? ' Organizer Team' : ' Listing Team'),
                'cover_image_url' => $cover_image,
                'supporting_docs_urls' => $doc_1 . "\n" . $doc_2,
                'official_event' => $topic_official,
                'recurrence' => $recurrence_value,
                'recurrence_until' => $recurrence_until_value,
                'video_urls' => $video_1 . "\n" . $video_2,
                'gallery_urls' => implode("\n", $gallery_urls_for_row),
                'sessions' => wp_json_encode($sessions),
                'key_speakers' => wp_json_encode($key_speakers),
                'event_type' => $topic_type,
                'event_mode' => $topic_mode,
                'event_price_range' => $topic_price_range,
                'event_tags' => $topic_tags . '|' . $location['city'] . ($is_single_org_demo ? ('|' . $brand_name) : ''),
                'company_name' => $is_job_profile ? ($brand_name . ' ' . $location['city']) : '',
                'job_type' => $is_job_profile ? $job_type : '',
                'job_work_mode' => $is_job_profile ? $job_work_mode : '',
                'job_salary' => $is_job_profile ? $job_salary : '',
                'job_apply_url' => $is_job_profile ? $job_apply_url : '',
                'job_application_email' => $is_job_profile ? $job_application_email : '',
                'job_closing_date' => $is_job_profile ? $job_closing_date : '',
                'job_department' => $is_job_profile ? $job_department : '',
                'job_experience_level' => $is_job_profile ? (($i % 4 === 0) ? 'Senior' : (($i % 3 === 0) ? 'Mid-level' : 'Entry-level')) : '',
                'job_reporting_to' => $is_job_profile ? (($i % 2 === 0) ? 'Head of Department' : 'Hiring Manager') : '',
                'job_remote_policy' => $is_job_profile ? $job_remote_policy : '',
                'job_visa_sponsorship' => $is_job_profile ? (($i % 5 === 0) ? 'yes' : 'no') : '',
                'job_open_positions' => $is_job_profile ? (string) (1 + ($i % 4)) : '',
                'job_interview_process' => $is_job_profile ? 'Screening call, role-specific interview, final interview.' : '',
                'job_expected_start_date' => $is_job_profile ? $job_expected_start_date : '',
                'job_skills' => $is_job_profile ? $job_skills : '',
                'job_responsibilities' => $is_job_profile ? 'Lead key projects, collaborate with cross-functional teams, and deliver measurable outcomes.' : '',
                'job_requirements' => $is_job_profile ? 'Relevant professional experience, clear communication skills, and strong problem-solving ability.' : '',
                'job_benefits' => $is_job_profile ? 'Competitive salary, pension contribution, paid leave, and development budget.' : '',
                'job_company_website' => $is_job_profile ? $job_company_website : '',
                'job_company_size' => $is_job_profile ? $job_company_size : '',
                'job_industry' => $is_job_profile ? $job_industry : '',
                'status' => 'publish',
                'demo_event_stream_url' => $stream_url,
                'demo_custom_schema' => $custom_schema,
                'demo_custom_values' => $demo_custom_values,
                'demo_email_templates' => $email_templates,
            ];
        }

        return $rows;
    }


    private function apply_demo_import_post_enrichment($post_id, array $row, $index = 0) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return;
        }

        $stream_url = esc_url_raw((string) ($row['demo_event_stream_url'] ?? ''));
        if ($stream_url !== '') {
            update_post_meta($post_id, 'nexdirectory_event_stream_url', $stream_url);
        }

        $custom_schema = isset($row['demo_custom_schema']) && is_array($row['demo_custom_schema']) ? $row['demo_custom_schema'] : [];
        $custom_values = isset($row['demo_custom_values']) && is_array($row['demo_custom_values']) ? $row['demo_custom_values'] : [];
        if (!empty($custom_schema)) {
            update_post_meta($post_id, 'nexdirectory_custom_fields_schema', wp_json_encode($custom_schema));
        }
        if (!empty($custom_values)) {
            update_post_meta($post_id, 'nexdirectory_custom_fields_data', wp_json_encode($custom_values));
        }

        $email_templates = isset($row['demo_email_templates']) && is_array($row['demo_email_templates']) ? $row['demo_email_templates'] : [];
        foreach ($email_templates as $meta_key => $meta_value) {
            $meta_key = sanitize_key((string) $meta_key);
            if ($meta_key === '' || strpos($meta_key, 'nexdirectory_email_') !== 0) {
                continue;
            }
            $is_body = strpos($meta_key, 'body') !== false;
            $sanitized_value = $is_body ? nexdirectory_sanitize_textarea((string) $meta_value) : sanitize_text_field((string) $meta_value);
            update_post_meta($post_id, $meta_key, $sanitized_value);
        }

        update_post_meta($post_id, 'nexdirectory_demo_imported', '1');
        update_post_meta($post_id, 'nexdirectory_demo_locked', '1');
        update_post_meta($post_id, 'nexdirectory_demo_imported_at', current_time('mysql'));
        update_post_meta($post_id, 'nexdirectory_demo_import_batch', gmdate('Ymd-His'));
        update_post_meta($post_id, 'nexdirectory_demo_item_index', (string) ((int) $index + 1));
    }


    private function seed_demo_import_runtime_data(array $event_ids, $import_user_id = 0) {
        $summary = [
            'analytics_rows' => 0,
            'messages' => 0,
            'threads' => 0,
            'rsvps' => 0,
            'pending_posts' => 0,
            'draft_posts' => 0,
            'claim_requests' => 0,
            'reports' => 0,
        ];

        $event_ids = array_values(array_unique(array_filter(array_map('absint', $event_ids))));
        if (empty($event_ids)) {
            return $summary;
        }

        $owner_map = [];
        foreach ($event_ids as $event_id) {
            $owner_map[$event_id] = absint((int) get_post_field('post_author', $event_id));
        }

        $user_pool = $this->get_demo_seed_user_pool($import_user_id);
        $attendee_ids = array_values(array_filter(array_unique(array_map('absint', (array) ($user_pool['attendees'] ?? [])))));
        if (empty($attendee_ids)) {
            $attendee_ids = array_values(array_filter(array_map('absint', (array) get_users([
                'number' => 30,
                'orderby' => 'ID',
                'order' => 'ASC',
                'fields' => 'ID',
            ]))));
        }

        $summary['analytics_rows'] = $this->seed_demo_import_analytics_data($event_ids, $owner_map, $attendee_ids);

        $inbox_seed = $this->seed_demo_import_inbox_data($event_ids, $owner_map, $attendee_ids);
        if (is_array($inbox_seed)) {
            $summary['threads'] = absint((int) ($inbox_seed['threads'] ?? 0));
            $summary['messages'] = absint((int) ($inbox_seed['messages'] ?? 0));
        }

        $rsvp_seed = $this->seed_demo_import_attendance_data($event_ids, $owner_map, $attendee_ids);
        if (is_array($rsvp_seed)) {
            $summary['rsvps'] = absint((int) ($rsvp_seed['rsvps'] ?? 0));
        }

        $moderation_seed = $this->seed_demo_import_moderation_data($event_ids, $attendee_ids);
        if (is_array($moderation_seed)) {
            $summary['pending_posts'] = absint((int) ($moderation_seed['pending_posts'] ?? 0));
            $summary['draft_posts'] = absint((int) ($moderation_seed['draft_posts'] ?? 0));
            $summary['claim_requests'] = absint((int) ($moderation_seed['claim_requests'] ?? 0));
            $summary['reports'] = absint((int) ($moderation_seed['reports'] ?? 0));
        }

        return $summary;
    }


    private function get_demo_seed_user_pool($import_user_id = 0) {
        $import_user_id = absint((int) $import_user_id);
        $organizers = [];
        $attendees = [];

        if ($import_user_id > 0) {
            $organizers[] = $import_user_id;
        }

        $existing_organizers = get_users([
            'number' => 10,
            'orderby' => 'ID',
            'order' => 'ASC',
            'fields' => 'ID',
            'role__in' => ['administrator', 'editor', 'author'],
        ]);
        foreach ((array) $existing_organizers as $user_id) {
            $user_id = absint((int) $user_id);
            if ($user_id > 0) {
                $organizers[] = $user_id;
            }
        }
        $organizers = array_values(array_unique(array_filter(array_map('absint', $organizers))));

        $existing_attendees = get_users([
            'number' => 40,
            'orderby' => 'ID',
            'order' => 'ASC',
            'fields' => 'ID',
            'role__in' => ['subscriber', 'contributor', 'author'],
        ]);
        foreach ((array) $existing_attendees as $user_id) {
            $user_id = absint((int) $user_id);
            if ($user_id > 0) {
                $attendees[] = $user_id;
            }
        }

        $demo_organizers = [
            ['login' => 'nexdirectory_demo_organizer_1', 'email' => 'demo.organizer.1@example.com', 'name' => 'Demo Organizer One', 'role' => 'author'],
            ['login' => 'nexdirectory_demo_organizer_2', 'email' => 'demo.organizer.2@example.com', 'name' => 'Demo Organizer Two', 'role' => 'author'],
            ['login' => 'nexdirectory_demo_organizer_3', 'email' => 'demo.organizer.3@example.com', 'name' => 'Demo Organizer Three', 'role' => 'author'],
        ];
        foreach ($demo_organizers as $seed_user) {
            $user_id = $this->upsert_demo_seed_user($seed_user);
            if ($user_id > 0) {
                $organizers[] = $user_id;
                $attendees[] = $user_id;
            }
        }

        $demo_attendees = [
            ['login' => 'nexdirectory_demo_member_1', 'email' => 'demo.member.1@example.com', 'name' => 'Demo Member One', 'role' => 'subscriber'],
            ['login' => 'nexdirectory_demo_member_2', 'email' => 'demo.member.2@example.com', 'name' => 'Demo Member Two', 'role' => 'subscriber'],
            ['login' => 'nexdirectory_demo_member_3', 'email' => 'demo.member.3@example.com', 'name' => 'Demo Member Three', 'role' => 'subscriber'],
            ['login' => 'nexdirectory_demo_member_4', 'email' => 'demo.member.4@example.com', 'name' => 'Demo Member Four', 'role' => 'subscriber'],
            ['login' => 'nexdirectory_demo_member_5', 'email' => 'demo.member.5@example.com', 'name' => 'Demo Member Five', 'role' => 'subscriber'],
            ['login' => 'nexdirectory_demo_member_6', 'email' => 'demo.member.6@example.com', 'name' => 'Demo Member Six', 'role' => 'subscriber'],
            ['login' => 'nexdirectory_demo_member_7', 'email' => 'demo.member.7@example.com', 'name' => 'Demo Member Seven', 'role' => 'subscriber'],
            ['login' => 'nexdirectory_demo_member_8', 'email' => 'demo.member.8@example.com', 'name' => 'Demo Member Eight', 'role' => 'subscriber'],
            ['login' => 'nexdirectory_demo_member_9', 'email' => 'demo.member.9@example.com', 'name' => 'Demo Member Nine', 'role' => 'subscriber'],
            ['login' => 'nexdirectory_demo_member_10', 'email' => 'demo.member.10@example.com', 'name' => 'Demo Member Ten', 'role' => 'subscriber'],
            ['login' => 'nexdirectory_demo_member_11', 'email' => 'demo.member.11@example.com', 'name' => 'Demo Member Eleven', 'role' => 'subscriber'],
            ['login' => 'nexdirectory_demo_member_12', 'email' => 'demo.member.12@example.com', 'name' => 'Demo Member Twelve', 'role' => 'subscriber'],
        ];
        foreach ($demo_attendees as $seed_user) {
            $user_id = $this->upsert_demo_seed_user($seed_user);
            if ($user_id > 0) {
                $attendees[] = $user_id;
            }
        }

        $organizers = array_values(array_unique(array_filter(array_map('absint', $organizers))));
        $attendees = array_values(array_unique(array_filter(array_map('absint', $attendees))));
        if (!empty($organizers)) {
            $attendees = array_values(array_unique(array_merge($attendees, $organizers)));
        }

        return [
            'organizers' => $organizers,
            'attendees' => $attendees,
        ];
    }


    private function upsert_demo_seed_user(array $seed_user = []) {
        $login = sanitize_user((string) ($seed_user['login'] ?? ''), true);
        $email = sanitize_email((string) ($seed_user['email'] ?? ''));
        if ($login === '' || !is_email($email)) {
            return 0;
        }

        $user = get_user_by('email', $email);
        if (!$user instanceof WP_User) {
            $user = get_user_by('login', $login);
        }
        if (!$user instanceof WP_User) {
            return 0;
        }
        if (!$user instanceof WP_User) {
            return 0;
        }

        $user_id = (int) $user->ID;
        if ($user_id <= 0) {
            return 0;
        }
        update_user_meta($user_id, 'nexdirectory_demo_seed_user', '1');
        update_user_meta($user_id, 'nexdirectory_gdpr_marketing_opt_in', '1');
        update_user_meta($user_id, 'nexdirectory_event_messages_enabled', '1');
        update_user_meta($user_id, 'nexdirectory_gdpr_terms_accepted_at', (string) time());

        return $user_id;
    }


    private function seed_demo_import_analytics_data(array $event_ids, array $owner_map = [], array $attendee_ids = []) {
        global $wpdb;
        $table = (string) $wpdb->prefix . self::ANALYTICS_TABLE;
        $table_exists = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
        if ($table_exists !== $table) {
            return 0;
        }

        $row_count = 0;
        $events_count = max(1, count($event_ids));
        $target_rows = 12000;
        $metrics_per_day = 7;
        $days_window = max(7, min(30, (int) floor($target_rows / ($events_count * $metrics_per_day))));
        foreach ($event_ids as $event_id) {
            $event_id = absint((int) $event_id);
            if ($event_id <= 0) {
                continue;
            }
            $owner_id = absint((int) ($owner_map[$event_id] ?? get_post_field('post_author', $event_id)));
            if ($owner_id <= 0) {
                $owner_id = absint((int) get_current_user_id());
            }

            for ($day = 0; $day < $days_window; $day++) {
                $base_ts = strtotime('-' . $day . ' days');
                if (!$base_ts) {
                    continue;
                }
                $created_at = gmdate('Y-m-d H:i:s', $base_ts + wp_rand(0, 86000));
                $viewer_id = !empty($attendee_ids) ? absint((int) $attendee_ids[array_rand($attendee_ids)]) : 0;

                $listing_views = wp_rand(16, 140);
                $map_impressions = $listing_views + wp_rand(8, 120);
                $search_appearances = $listing_views + wp_rand(5, 90);
                $click_phone = wp_rand(0, max(2, (int) floor($listing_views / 7)));
                $click_website = wp_rand(1, max(3, (int) floor($listing_views / 5)));
                $click_whatsapp = wp_rand(0, max(1, (int) floor($listing_views / 9)));
                $enquiries = wp_rand(0, max(2, (int) floor($listing_views / 12)));

                $rows = [
                    ['metric' => 'listing_view', 'value' => $listing_views, 'context' => 'single'],
                    ['metric' => 'map_impression', 'value' => $map_impressions, 'context' => 'map'],
                    ['metric' => 'search_appearance', 'value' => $search_appearances, 'context' => 'search'],
                    ['metric' => 'click_phone', 'value' => $click_phone, 'context' => 'single'],
                    ['metric' => 'click_website', 'value' => $click_website, 'context' => 'single'],
                    ['metric' => 'click_whatsapp', 'value' => $click_whatsapp, 'context' => 'single'],
                    ['metric' => 'enquiry_received', 'value' => $enquiries, 'context' => 'inbox'],
                ];

                foreach ($rows as $row) {
                    if ((float) ($row['value'] ?? 0) <= 0) {
                        continue;
                    }
                    $inserted = $wpdb->insert(
                        $table,
                        [
                            'event_id' => $event_id,
                            'owner_id' => $owner_id,
                            'user_id' => $viewer_id,
                            'metric' => sanitize_key((string) ($row['metric'] ?? '')),
                            'context' => sanitize_key((string) ($row['context'] ?? '')),
                            'meta_key' => '',
                            'meta_value' => '',
                            'value' => (float) ($row['value'] ?? 1),
                            'created_at' => $created_at,
                        ],
                        ['%d', '%d', '%d', '%s', '%s', '%s', '%s', '%f', '%s']
                    ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
                    if ($inserted !== false) {
                        $row_count++;
                    }
                }
            }
        }

        $global_metrics = ['search_filter', 'search_query', 'map_area_search'];
        $meta_keys = ['event_type', 'event_mode', 'event_tag', 'city'];
        $meta_values = ['conference', 'hybrid', 'featured', 'london', 'new-york', 'sao-paulo', 'singapore'];
        $search_queries = ['innovation summit', 'networking', 'green energy', 'future tech', 'aerospace'];
        $global_rows = max(40, min(220, count($event_ids) * 5));
        for ($index = 0; $index < $global_rows; $index++) {
            $metric = (string) $global_metrics[array_rand($global_metrics)];
            $meta_key = $metric === 'search_query' ? 'query' : (string) $meta_keys[array_rand($meta_keys)];
            $meta_value = $metric === 'search_query'
                ? (string) $search_queries[array_rand($search_queries)]
                : (string) $meta_values[array_rand($meta_values)];
            $created_at = gmdate('Y-m-d H:i:s', time() - wp_rand(0, 40 * DAY_IN_SECONDS));
            $viewer_id = !empty($attendee_ids) ? absint((int) $attendee_ids[array_rand($attendee_ids)]) : 0;
            $inserted = $wpdb->insert(
                $table,
                [
                    'event_id' => 0,
                    'owner_id' => 0,
                    'user_id' => $viewer_id,
                    'metric' => sanitize_key($metric),
                    'context' => $metric === 'map_area_search' ? 'map' : 'search',
                    'meta_key' => sanitize_key($meta_key),
                    'meta_value' => sanitize_text_field($meta_value),
                    'value' => (float) wp_rand(1, 6),
                    'created_at' => $created_at,
                ],
                ['%d', '%d', '%d', '%s', '%s', '%s', '%s', '%f', '%s']
            ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            if ($inserted !== false) {
                $row_count++;
            }
        }

        return $row_count;
    }


    private function seed_demo_import_inbox_data(array $event_ids, array $owner_map = [], array $attendee_ids = []) {
        $out = [
            'threads' => 0,
            'messages' => 0,
        ];
        if (!method_exists($this, 'inbox_send_message')) {
            return $out;
        }

        $event_ids = array_values(array_unique(array_filter(array_map('absint', $event_ids))));
        $attendee_ids = array_values(array_unique(array_filter(array_map('absint', $attendee_ids))));
        if (empty($event_ids) || empty($attendee_ids)) {
            return $out;
        }

        global $wpdb;
        $threads_table = $this->get_inbox_threads_table_name();
        $messages_table = $this->get_inbox_messages_table_name();
        $thread_ids = [];

        foreach ($event_ids as $event_id) {
            $owner_id = absint((int) ($owner_map[$event_id] ?? get_post_field('post_author', $event_id)));
            if ($owner_id <= 0) {
                continue;
            }

            $participants = array_values(array_diff($attendee_ids, [$owner_id]));
            if (empty($participants)) {
                continue;
            }
            shuffle($participants);
            $conversation_count = min(count($participants), wp_rand(1, 3));
            $event_title = sanitize_text_field((string) get_the_title($event_id));

            for ($i = 0; $i < $conversation_count; $i++) {
                $sender_id = absint((int) ($participants[$i] ?? 0));
                if ($sender_id <= 0) {
                    continue;
                }

                /* translators: %s: Placeholder value. */
                $subject = sprintf(__('Question about %s', 'nexdirectory'), $event_title);
                $first_message = $this->inbox_send_message(
                    $event_id,
                    $sender_id,
                    $owner_id,
                    $subject,
                    __('Hi, I am interested in this listing. Can you confirm start times and what is included for attendees?', 'nexdirectory')
                );
                if (!is_array($first_message)) {
                    continue;
                }

                $thread_id = absint((int) ($first_message['thread_id'] ?? 0));
                $message_id = absint((int) ($first_message['message_id'] ?? 0));
                if ($thread_id > 0) {
                    $thread_ids[$thread_id] = $thread_id;
                }
                if ($message_id > 0) {
                    $message_time = gmdate('Y-m-d H:i:s', time() - wp_rand(1 * DAY_IN_SECONDS, 28 * DAY_IN_SECONDS));
                    $wpdb->update(
                        $messages_table,
                        ['created_at' => $message_time],
                        ['id' => $message_id],
                        ['%s'],
                        ['%d']
                    ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
                    if ($thread_id > 0) {
                        $wpdb->update(
                            $threads_table,
                            ['updated_at' => $message_time, 'last_message_id' => $message_id, 'last_sender_id' => $sender_id],
                            ['id' => $thread_id],
                            ['%s', '%d', '%d'],
                            ['%d']
                        ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
                    }
                }
                $out['messages']++;

                if (wp_rand(1, 100) <= 85) {
                    $reply = $this->inbox_send_message(
                        $event_id,
                        $owner_id,
                        $sender_id,
                        $subject,
                        __('Thanks for reaching out. Yes, everything is confirmed and you can RSVP directly from the event page.', 'nexdirectory')
                    );
                    if (is_array($reply)) {
                        $reply_message_id = absint((int) ($reply['message_id'] ?? 0));
                        $reply_thread_id = absint((int) ($reply['thread_id'] ?? $thread_id));
                        $reply_time = gmdate('Y-m-d H:i:s', time() - wp_rand(1 * DAY_IN_SECONDS, 20 * DAY_IN_SECONDS));
                        if ($reply_message_id > 0) {
                            $wpdb->update(
                                $messages_table,
                                ['created_at' => $reply_time],
                                ['id' => $reply_message_id],
                                ['%s'],
                                ['%d']
                            ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
                        }
                        if ($reply_thread_id > 0) {
                            $wpdb->update(
                                $threads_table,
                                ['updated_at' => $reply_time, 'last_message_id' => $reply_message_id, 'last_sender_id' => $owner_id],
                                ['id' => $reply_thread_id],
                                ['%s', '%d', '%d'],
                                ['%d']
                            ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
                        }
                        $out['messages']++;
                    }
                }
            }
        }

        $out['threads'] = count($thread_ids);
        return $out;
    }


    private function seed_demo_import_attendance_data(array $event_ids, array $owner_map = [], array $attendee_ids = []) {
        $out = [
            'rsvps' => 0,
        ];
        $event_ids = array_values(array_unique(array_filter(array_map('absint', $event_ids))));
        $attendee_ids = array_values(array_unique(array_filter(array_map('absint', $attendee_ids))));
        if (empty($event_ids) || empty($attendee_ids)) {
            return $out;
        }

        global $wpdb;
        $rsvp_table = $this->get_rsvp_table_name();
        $table_exists = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $rsvp_table));
        if ($table_exists !== $rsvp_table) {
            return $out;
        }

        foreach ($event_ids as $event_id) {
            $owner_id = absint((int) ($owner_map[$event_id] ?? get_post_field('post_author', $event_id)));
            $eligible_attendees = array_values(array_diff($attendee_ids, [$owner_id]));
            if (empty($eligible_attendees)) {
                continue;
            }
            shuffle($eligible_attendees);
            $max_take = min(count($eligible_attendees), wp_rand(3, 10));
            if ($max_take <= 0) {
                continue;
            }

            for ($i = 0; $i < $max_take; $i++) {
                $user_id = absint((int) ($eligible_attendees[$i] ?? 0));
                if ($user_id <= 0) {
                    continue;
                }
                $status = $i === 0 || wp_rand(1, 100) > 18 ? 'yes' : 'waitlist';
                $created_at = gmdate('Y-m-d H:i:s', time() - wp_rand(1 * DAY_IN_SECONDS, 35 * DAY_IN_SECONDS));
                $saved = $wpdb->replace(
                    $rsvp_table,
                    [
                        'post_id' => $event_id,
                        'user_id' => $user_id,
                        'status' => $status,
                        'created_at' => $created_at,
                        'updated_at' => $created_at,
                    ],
                    ['%d', '%d', '%s', '%s', '%s']
                ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
                if ($saved !== false) {
                    $out['rsvps']++;
                }
            }
            $this->sync_rsvp_meta_from_table($event_id);
        }

        return $out;
    }


    private function seed_demo_import_moderation_data(array $event_ids, array $attendee_ids = []) {
        $out = [
            'pending_posts' => 0,
            'draft_posts' => 0,
            'claim_requests' => 0,
            'reports' => 0,
        ];
        $event_ids = array_values(array_unique(array_filter(array_map('absint', $event_ids))));
        if (empty($event_ids)) {
            return $out;
        }

        $mutable_ids = $event_ids;
        shuffle($mutable_ids);
        $pending_target = max(1, min((int) floor(count($mutable_ids) * 0.12), 10));
        $draft_target = max(1, min((int) floor(count($mutable_ids) * 0.08), 8));

        for ($i = 0; $i < $pending_target; $i++) {
            $post_id = absint((int) ($mutable_ids[$i] ?? 0));
            if ($post_id <= 0) {
                continue;
            }
            $updated = wp_update_post([
                'ID' => $post_id,
                'post_status' => 'pending',
            ], true);
            if (!is_wp_error($updated)) {
                $out['pending_posts']++;
            }
        }

        for ($i = $pending_target; $i < ($pending_target + $draft_target); $i++) {
            $post_id = absint((int) ($mutable_ids[$i] ?? 0));
            if ($post_id <= 0) {
                continue;
            }
            $updated = wp_update_post([
                'ID' => $post_id,
                'post_status' => 'draft',
            ], true);
            if (!is_wp_error($updated)) {
                $out['draft_posts']++;
            }
        }

        $report_ids = $event_ids;
        shuffle($report_ids);
        $comment_target = max(2, min((int) floor(count($report_ids) * 0.4), 20));
        $attendee_ids = array_values(array_unique(array_filter(array_map('absint', $attendee_ids))));

        for ($i = 0; $i < $comment_target; $i++) {
            $post_id = absint((int) ($report_ids[$i] ?? 0));
            if ($post_id <= 0) {
                continue;
            }
            $user_id = !empty($attendee_ids) ? absint((int) $attendee_ids[array_rand($attendee_ids)]) : 0;
            $user = $user_id > 0 ? get_user_by('id', $user_id) : null;
            $author_name = $user instanceof WP_User ? (string) ($user->display_name ?: $user->user_login) : __('Demo User', 'nexdirectory');
            $author_email = $user instanceof WP_User ? sanitize_email((string) $user->user_email) : 'demo.user@example.com';
            $comment_time = gmdate('Y-m-d H:i:s', time() - wp_rand(1 * DAY_IN_SECONDS, 20 * DAY_IN_SECONDS));

            if ($i % 2 === 0) {
                $comment_id = wp_insert_comment([
                    'comment_post_ID' => $post_id,
                    'comment_author' => $author_name,
                    'comment_author_email' => $author_email,
                    'comment_author_url' => '',
                    'comment_content' => __('Demo report: details need review for this listing.', 'nexdirectory'),
                    'comment_type' => 'nexdirectory_report',
                    'user_id' => $user_id,
                    'comment_approved' => 1,
                    'comment_date' => get_date_from_gmt($comment_time),
                    'comment_date_gmt' => $comment_time,
                ]);
                if ($comment_id > 0) {
                    $out['reports']++;
                }
            } else {
                $comment_id = wp_insert_comment([
                    'comment_post_ID' => $post_id,
                    'comment_author' => $author_name,
                    'comment_author_email' => $author_email,
                    'comment_author_url' => '',
                    'comment_content' => __('Demo claim request: requesting ownership transfer for this listing.', 'nexdirectory'),
                    'comment_type' => 'nexdirectory_claim',
                    'user_id' => $user_id,
                    'comment_approved' => 0,
                    'comment_date' => get_date_from_gmt($comment_time),
                    'comment_date_gmt' => $comment_time,
                ]);
                if ($comment_id > 0) {
                    update_comment_meta((int) $comment_id, 'nexdirectory_claim_status', 'pending');
                    update_comment_meta((int) $comment_id, 'nexdirectory_claim_requested_at', get_date_from_gmt($comment_time));
                    $out['claim_requests']++;
                }
            }
        }

        return $out;
    }    private function build_event_export_row($post_id) {
        $post_id = absint($post_id);
        $post = get_post($post_id);
        if (!$post || $post->post_type !== self::POST_TYPE) {
            return [];
        }

        $cover_image_url = (string) get_post_meta($post_id, 'nexdirectory_cover_image_url', true);
        if ($cover_image_url === '' && has_post_thumbnail($post_id)) {
            $thumb = get_the_post_thumbnail_url($post_id, 'full');
            if (is_string($thumb) && $thumb !== '') {
                $cover_image_url = $thumb;
            }
        }

        $row = [
            'title' => (string) $post->post_title,
            'description' => (string) $post->post_content,
            'excerpt' => (string) $post->post_excerpt,
            'start' => (string) get_post_meta($post_id, 'nexdirectory_start', true),
            'end' => (string) get_post_meta($post_id, 'nexdirectory_end', true),
            'timezone' => (string) get_post_meta($post_id, 'nexdirectory_timezone', true),
            'venue' => (string) get_post_meta($post_id, 'nexdirectory_venue', true),
            'address' => (string) get_post_meta($post_id, 'nexdirectory_address', true),
            'city' => (string) get_post_meta($post_id, 'nexdirectory_city', true),
            'country' => (string) get_post_meta($post_id, 'nexdirectory_country', true),
            'lat' => (string) get_post_meta($post_id, 'nexdirectory_lat', true),
            'lng' => (string) get_post_meta($post_id, 'nexdirectory_lng', true),
            'capacity' => (string) get_post_meta($post_id, 'nexdirectory_capacity', true),
            'price' => (string) get_post_meta($post_id, 'nexdirectory_price', true),
            'external_url' => (string) get_post_meta($post_id, 'nexdirectory_external_url', true),
            'contact_email' => (string) get_post_meta($post_id, 'nexdirectory_contact_email', true),
            'contact_phone' => (string) get_post_meta($post_id, 'nexdirectory_contact_phone', true),
            'contact_name' => (string) get_post_meta($post_id, 'nexdirectory_contact_name', true),
            'cover_image_url' => $cover_image_url,
            'supporting_docs_urls' => implode('|', $this->parse_multiline_urls((string) get_post_meta($post_id, 'nexdirectory_supporting_docs', true))),
            'official_event' => $this->get_official_event_flag($post_id) ? 'yes' : 'no',
            'recurrence' => (string) get_post_meta($post_id, 'nexdirectory_recurrence', true),
            'recurrence_until' => (string) get_post_meta($post_id, 'nexdirectory_recurrence_until', true),
            'video_urls' => implode('|', $this->parse_multiline_urls((string) get_post_meta($post_id, 'nexdirectory_video_urls', true))),
            'gallery_urls' => implode('|', $this->parse_multiline_urls((string) get_post_meta($post_id, 'nexdirectory_gallery_urls', true))),
            'sessions' => (string) get_post_meta($post_id, 'nexdirectory_sessions', true),
            'key_speakers' => (string) get_post_meta($post_id, 'nexdirectory_key_speakers', true),
            'event_type' => implode('|', wp_get_post_terms($post_id, self::TAX_TYPE, ['fields' => 'names'])),
            'event_mode' => implode('|', wp_get_post_terms($post_id, self::TAX_MODE, ['fields' => 'names'])),
            'event_price_range' => implode('|', wp_get_post_terms($post_id, self::TAX_PRICE, ['fields' => 'names'])),
            'event_tags' => implode('|', wp_get_post_terms($post_id, self::TAX_TAG, ['fields' => 'names'])),
            'status' => (string) get_post_status($post_id),
        ];

        $headers = array_keys($this->get_import_supported_fields());
        $normalized = [];
        foreach ($headers as $header) {
            $normalized[$header] = isset($row[$header]) ? (string) $row[$header] : '';
        }
        return $normalized;
    }


    private function get_demo_export_rows() {
        return [[
            'title' => 'Global Robotics Summit 2026',
            'description' => 'A premier gathering of robotics engineers and researchers from around the world.',
            'excerpt' => 'Robotics conference with itinerary and live event details.',
            'start' => '2026-05-15 09:00:00',
            'end' => '2026-05-17 17:00:00',
            'timezone' => 'Europe/London',
            'venue' => 'ExCeL London',
            'address' => 'Royal Victoria Dock, 1 Western Gateway',
            'city' => 'London',
            'country' => 'United Kingdom',
            'lat' => '51.5085',
            'lng' => '0.0298',
            'capacity' => '500',
            'price' => '£299',
            'external_url' => '',
            'contact_email' => 'info@robotics-summit.com',
            'contact_phone' => '+44 20 0000 0000',
            'contact_name' => 'Robotics Team',
            'cover_image_url' => '',
            'supporting_docs_urls' => '',
            'official_event' => 'no',
            'recurrence' => 'none',
            'recurrence_until' => '',
            'stream_url' => '',
            'video_urls' => '',
            'gallery_urls' => '',
            'sessions' => '[{"title":"Opening Session (Day 1)","start":"2026-05-15 09:00:00","end":"2026-05-15 10:30:00","location":"ExCeL London","speaker":"Dr Jane Smith","day":"May 15, 2026","notes":"Arrive early for badge check-in and seating.","attachments":[],"session_key":"demo-day1-opening"},{"title":"Technical Deep Dive (Day 1)","start":"2026-05-15 11:00:00","end":"2026-05-15 13:00:00","location":"ExCeL London - Hall B","speaker":"Marcus Webb","day":"May 15, 2026","notes":"Interactive workshop with practical examples.","attachments":[],"session_key":"demo-day1-deep-dive"},{"title":"Q&A and Networking (Day 2)","start":"2026-05-16 14:00:00","end":"2026-05-16 15:15:00","location":"ExCeL London - Lounge","speaker":"Elena Rodriguez","day":"May 16, 2026","notes":"Submit questions before this session.","attachments":[],"session_key":"demo-day2-qa"},{"title":"Closing Session (Day 3)","start":"2026-05-17 15:30:00","end":"2026-05-17 17:00:00","location":"ExCeL London","speaker":"Priya Patel","day":"May 17, 2026","notes":"Summary, next steps, and downloadable resource links.","attachments":[],"session_key":"demo-day3-closing"}]',
            'key_speakers' => '[{"name":"Dr Jane Smith","profession":"Robotics Engineer","subject":"Humanoid Systems","image":"","details":"Lead researcher with 15+ years in autonomous robotics."},{"name":"Marcus Webb","profession":"Cloud Architect","subject":"Scalable Systems","image":"","details":"Focuses on high-scale event infrastructure."},{"name":"Elena Rodriguez","profession":"Design Director","subject":"Applied AI UX","image":"","details":"Leads design systems and attendee journey strategy."}]',
            'event_type' => 'Conference',
            'event_mode' => 'In-person',
            'event_price_range' => 'Premium',
            'event_tags' => 'AI|Manufacturing|Robotics',
            'status' => 'pending',
        ]];
    }    private function stream_export_payload($format, $rows, $filename_prefix = 'events-export') {
        $format = sanitize_key((string) $format);
        if (!in_array($format, ['csv', 'json'], true)) {
            $format = 'csv';
        }
        $rows = is_array($rows) ? $rows : [];
        $headers = array_keys($this->get_import_supported_fields());

        nocache_headers();
        if ($format === 'json') {
            header('Content-Type: application/json; charset=utf-8');
            header('Content-Disposition: attachment; filename="' . sanitize_file_name($filename_prefix . '.json') . '"');
            echo wp_json_encode($rows, JSON_PRETTY_PRINT);
            exit;
        }

        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . sanitize_file_name($filename_prefix . '.csv') . '"');
        try {
            $output = new SplFileObject('php://output', 'w');
        } catch (RuntimeException $e) {
            exit;
        }
        $output->fputcsv($this->sanitize_csv_row($headers));
        foreach ($rows as $row) {
            $line = [];
            foreach ($headers as $header) {
                $line[] = isset($row[$header]) ? (string) $row[$header] : '';
            }
            $output->fputcsv($this->sanitize_csv_row($line));
        }
        exit;
    }    public function handle_export_events() {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        if (!isset($_POST['nexdirectory_export_nonce']) || !wp_verify_nonce(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_export_nonce'])), 'nexdirectory_export_events_nonce')) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'));
        }

        $format = sanitize_key((string) wp_unslash($_POST['nexdirectory_export_format'] ?? 'csv'));
        $post_statuses = ['publish', 'pending', 'draft'];
        $ids = get_posts([
            'post_type' => self::POST_TYPE,
            'post_status' => $post_statuses,
            'posts_per_page' => -1,
            'fields' => 'ids',
            'orderby' => 'date',
            'order' => 'DESC',
        ]);

        $rows = [];
        foreach ((array) $ids as $post_id) {
            $row = $this->build_event_export_row((int) $post_id);
            if (!empty($row)) {
                $rows[] = $row;
            }
        }

        $this->stream_export_payload($format, $rows, 'event-events-export-' . gmdate('Ymd-His'));
    }


    public function handle_export_demo_events() {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        if (!isset($_POST['nexdirectory_export_nonce']) || !wp_verify_nonce(sanitize_text_field((string) wp_unslash($_POST['nexdirectory_export_nonce'])), 'nexdirectory_export_events_nonce')) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'));
        }
        $format = sanitize_key((string) wp_unslash($_POST['nexdirectory_export_format'] ?? 'csv'));
        $this->stream_export_payload($format, $this->get_demo_export_rows(), 'event-events-demo-template');
    }


    public function handle_email_verification() {
        if (empty($_GET['nexdirectory_verify_email']) || empty($_GET['token'])) {
            return;
        }
        if ((string) get_option('nexdirectory_require_email_verification', '1') !== '1') {
            $this->redirect_with_message(home_url('/'), __('Email verification is not required.', 'nexdirectory'));
        }
        $user_id = absint((int) wp_unslash($_GET['nexdirectory_verify_email']));
        $token = sanitize_text_field((string) wp_unslash($_GET['token']));
        if (!$user_id || !$token) {
            return;
        }
        $stored = (string) get_user_meta($user_id, 'nexdirectory_email_verify_token', true);
        $expires = (int) get_user_meta($user_id, 'nexdirectory_email_verify_expires', true);
        if ($stored === '' || !hash_equals($stored, $token) || ($expires > 0 && time() > $expires)) {
            $this->redirect_with_message(home_url('/'), __('Verification link invalid or expired.', 'nexdirectory'), 'error');
        }
        update_user_meta($user_id, 'nexdirectory_email_verified', '1');
        delete_user_meta($user_id, 'nexdirectory_email_verify_token');
        delete_user_meta($user_id, 'nexdirectory_email_verify_expires');
        delete_user_meta($user_id, 'nexdirectory_email_verify_last_sent');
        $platform_mode = sanitize_key((string) get_option('nexdirectory_platform_mode', 'user_generated'));
        $this->redirect_with_message(
            home_url('/'),
            $platform_mode === 'single_organisation'
                ? __('Email verified successfully. Your account is now active.', 'nexdirectory')
                : __('Email verified. You can now submit your first event.', 'nexdirectory')
        );
    }


    /**
     * Adds attendee and organizer activity for an RSVP transition.
     *
     * @param int    $post_id Listing ID.
     * @param int    $user_id Attendee ID.
     * @param string $status RSVP status.
     * @param string $action joined, removed, or promoted.
     * @return void
     */
    private function notify_account_rsvp_change($post_id, $user_id, $status, $action = 'joined') {
        $post_id = absint((int) $post_id);
        $user_id = absint((int) $user_id);
        $status = sanitize_key((string) $status);
        $action = sanitize_key((string) $action);
        if ($post_id <= 0 || $user_id <= 0) {
            return;
        }

        $event_title = sanitize_text_field((string) get_the_title($post_id));
        $event_url = get_permalink($post_id);
        $owner_id = absint((int) get_post_field('post_author', $post_id));
        $attendee = get_user_by('id', $user_id);
        $attendee_name = $attendee instanceof WP_User
            ? sanitize_text_field((string) ($attendee->display_name ?: $attendee->user_login))
            : __('A member', 'nexdirectory');
        $dedupe_suffix = (string) current_time('timestamp', true);

        if ($action === 'promoted') {
            $this->create_account_notification([
                'recipient_id' => $user_id,
                'actor_id' => $owner_id,
                'type' => 'waitlist_promoted',
                'object_id' => $post_id,
                'title' => __('You are off the waitlist', 'nexdirectory'),
                /* translators: %s: Event title. */
                'body' => sprintf(__('Your place at “%s” is now confirmed.', 'nexdirectory'), $event_title),
                'url' => $event_url,
                'dedupe_key' => 'waitlist-promoted:' . $post_id . ':' . $user_id,
            ]);
            return;
        }

        if ($action === 'joined') {
            $is_waitlist = $status === 'waitlist';
            if ($is_waitlist) {
                $attendee_notification_body = sprintf(
                    /* translators: %s: Event title. */
                    __('We will update you when a place opens for “%s”.', 'nexdirectory'),
                    $event_title
                );
            } else {
                $attendee_notification_body = sprintf(
                    /* translators: %s: Event title. */
                    __('You are attending “%s”.', 'nexdirectory'),
                    $event_title
                );
            }
            $this->create_account_notification([
                'recipient_id' => $user_id,
                'actor_id' => $owner_id,
                'type' => $is_waitlist ? 'waitlist' : 'rsvp',
                'object_id' => $post_id,
                'title' => $is_waitlist ? __('You joined the waitlist', 'nexdirectory') : __('Your RSVP is confirmed', 'nexdirectory'),
                'body' => $attendee_notification_body,
                'url' => $event_url,
                'dedupe_key' => 'rsvp:' . $post_id . ':' . $user_id . ':' . $status . ':' . $dedupe_suffix,
            ]);
        }

        if ($owner_id > 0 && $owner_id !== $user_id) {
            if ($action === 'removed') {
                /* translators: %s: Attendee display name. */
                $owner_title = sprintf(__('%s cancelled their RSVP', 'nexdirectory'), $attendee_name);
            } elseif ($status === 'waitlist') {
                /* translators: %s: Attendee display name. */
                $owner_title = sprintf(__('%s joined the waitlist', 'nexdirectory'), $attendee_name);
            } else {
                $owner_title = sprintf(
                    /* translators: %s: Attendee display name. */
                    __('%s confirmed attendance', 'nexdirectory'),
                    $attendee_name
                );
            }
            /* translators: %s: Event title. */
            $owner_body = sprintf(__('Attendance changed for “%s”.', 'nexdirectory'), $event_title);
            $this->create_account_notification([
                'recipient_id' => $owner_id,
                'actor_id' => $user_id,
                'type' => $status === 'waitlist' ? 'waitlist' : 'rsvp',
                'object_id' => $post_id,
                'title' => $owner_title,
                'body' => $owner_body,
                'url' => $event_url,
                'dedupe_key' => 'rsvp-owner:' . $post_id . ':' . $user_id . ':' . $action . ':' . $dedupe_suffix,
            ]);
        }
    }


    private function process_rsvp_action($post_id, $intent = 'join', $user_id = 0) {
        $post_id = absint((int) $post_id);
        $intent = sanitize_key((string) $intent);
        if ($intent === '') {
            $intent = 'join';
        }
        $user_id = absint((int) $user_id);
        if ($user_id <= 0) {
            $user_id = get_current_user_id();
        }
        $profile_key = $this->resolve_profile_key_for_post($post_id);
        $ui_labels = $this->get_site_profile_ui_labels($profile_key);
        $is_events_profile = (string) ($ui_labels['is_events_profile'] ?? '1') === '1';
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $singular_lower = function_exists('mb_strtolower') ? mb_strtolower($singular_label, 'UTF-8') : strtolower($singular_label);
        $join_button_label = sanitize_text_field((string) ($ui_labels['single_rsvp_join'] ?? ($is_events_profile ? __('RSVP / Join Waitlist', 'nexdirectory') : __('Save Listing', 'nexdirectory'))));
        $leave_button_label = sanitize_text_field((string) ($ui_labels['single_rsvp_cancel'] ?? ($is_events_profile ? __('Cancel RSVP', 'nexdirectory') : __('Remove Saved Listing', 'nexdirectory'))));

        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            /* translators: %s: Placeholder value. */
            return new WP_Error('nexdirectory_rsvp_invalid_event', $is_events_profile ? __('Invalid event.', 'nexdirectory') : sprintf(__('Invalid %s.', 'nexdirectory'), $singular_lower));
        }
        if ($user_id <= 0) {
            /* translators: %s: Placeholder value. */
            return new WP_Error('nexdirectory_rsvp_login_required', $is_events_profile ? __('Please log in to RSVP.', 'nexdirectory') : sprintf(__('Please log in to save this %s.', 'nexdirectory'), $singular_lower));
        }
        $post_status = (string) get_post_status($post_id);
        $owner_id = (int) get_post_field('post_author', $post_id);
        if ($post_status !== 'publish' && $user_id !== $owner_id && !user_can($user_id, 'edit_post', $post_id)) {
            /* translators: %s: Placeholder value. */
            return new WP_Error('nexdirectory_rsvp_private_event', $is_events_profile ? __('This event is not available for RSVP.', 'nexdirectory') : sprintf(__('This %s is not available right now.', 'nexdirectory'), $singular_lower));
        }

        global $wpdb;
        $table = $this->get_rsvp_table_name();
        $capacity = (int) get_post_meta($post_id, 'nexdirectory_capacity', true);
        if (!$is_events_profile) {
            $capacity = 0;
        }
        $now = current_time('mysql', 1);
        $existing_status = $wpdb->get_var($wpdb->prepare(
            "SELECT status FROM {$table} WHERE post_id = %d AND user_id = %d LIMIT 1",
            $post_id,
            $user_id
        ));
        if (!empty($wpdb->last_error)) {
            $this->append_runtime_log('error', 'RSVP status query failed', [
                'post_id' => (string) $post_id,
                'user_id' => (string) $user_id,
                'error' => (string) $wpdb->last_error,
            ]);
            return new WP_Error('nexdirectory_rsvp_query_failed', $is_events_profile ? __('RSVP request failed. Please retry.', 'nexdirectory') : __('Save request failed. Please retry.', 'nexdirectory'));
        }
        $has_existing_row = $existing_status !== null && $existing_status !== '';

        if ($intent === 'join' && $has_existing_row) {
            $existing_status = in_array((string) $existing_status, ['yes', 'waitlist'], true) ? (string) $existing_status : 'yes';
            return [
                'result' => 'joined',
                'status' => $existing_status,
                'message' => $existing_status === 'waitlist'
                    ? __('You are already on the waitlist.', 'nexdirectory')
                    : ($is_events_profile ? __('Your RSVP is already confirmed.', 'nexdirectory') : __('This listing is already saved.', 'nexdirectory')),
                'post_id' => $post_id,
                'rsvp_count' => count($this->get_event_attendees_by_status($post_id, 'yes')),
                'waitlist_count' => count($this->get_event_attendees_by_status($post_id, 'waitlist')),
                'next_intent' => 'leave',
                'button_label' => $leave_button_label,
            ];
        }

        if ($intent !== 'join') {
            $lock_name = 'nexdirectory_rsvp_' . $post_id;
            $locked = 0;
            $promoted_user_id = 0;
            if ($has_existing_row) {
                $locked = (int) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, 5)', $lock_name));
                if ($locked !== 1) {
                    return new WP_Error('nexdirectory_rsvp_busy', $is_events_profile ? __('RSVP is busy. Please retry.', 'nexdirectory') : __('Save action is busy. Please retry.', 'nexdirectory'));
                }
            }
            $deleted = $wpdb->delete($table, [
                'post_id' => $post_id,
                'user_id' => $user_id,
            ], ['%d', '%d']);
            if ($deleted === false) {
                $this->append_runtime_log('error', 'RSVP removal failed', [
                    'post_id' => (string) $post_id,
                    'user_id' => (string) $user_id,
                    'error' => (string) $wpdb->last_error,
                ]);
                if ($locked === 1) {
                    $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock_name));
                }
                return new WP_Error('nexdirectory_rsvp_remove_failed', $is_events_profile ? __('RSVP removal failed. Please retry.', 'nexdirectory') : __('Saved-listing removal failed. Please retry.', 'nexdirectory'));
            }
            if ($locked === 1 && $is_events_profile && (string) $existing_status === 'yes' && $capacity > 0) {
                $promote_user_id = absint((int) $wpdb->get_var($wpdb->prepare(
                    "SELECT user_id FROM {$table} WHERE post_id = %d AND status = 'waitlist' ORDER BY created_at ASC, id ASC LIMIT 1",
                    $post_id
                )));
                if ($promote_user_id > 0) {
                    $promoted = $wpdb->update(
                        $table,
                        [
                            'status' => 'yes',
                            'updated_at' => $now,
                        ],
                        [
                            'post_id' => $post_id,
                            'user_id' => $promote_user_id,
                            'status' => 'waitlist',
                        ],
                        ['%s', '%s'],
                        ['%d', '%d', '%s']
                    );
                    if (is_int($promoted) && $promoted > 0) {
                        $promoted_user_id = $promote_user_id;
                    }
                }
            }
            if ($locked === 1) {
                $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock_name));
            }
            $this->sync_rsvp_meta_from_table($post_id);
            clean_post_cache($post_id);
            if ($is_events_profile) {
                $this->notify_account_rsvp_change($post_id, $user_id, (string) $existing_status, 'removed');
                if ($promoted_user_id > 0) {
                    $this->notify_account_rsvp_change($post_id, $promoted_user_id, 'yes', 'promoted');
                }
            }

            return [
                'result' => 'removed',
                'status' => 'none',
                'message' => $is_events_profile ? __('RSVP removed.', 'nexdirectory') : __('Listing removed from saved items.', 'nexdirectory'),
                'post_id' => $post_id,
                'rsvp_count' => count($this->get_event_attendees_by_status($post_id, 'yes')),
                'waitlist_count' => count($this->get_event_attendees_by_status($post_id, 'waitlist')),
                'next_intent' => 'join',
                'button_label' => $join_button_label,
            ];
        }

        $lock_name = 'nexdirectory_rsvp_' . $post_id;
        $locked = (int) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, 5)', $lock_name));
        if ($locked !== 1) {
            $this->append_runtime_log('warning', 'RSVP lock acquisition failed', [
                'post_id' => (string) $post_id,
                'user_id' => (string) $user_id,
            ]);
            return new WP_Error('nexdirectory_rsvp_busy', $is_events_profile ? __('RSVP is busy. Please retry.', 'nexdirectory') : __('Save action is busy. Please retry.', 'nexdirectory'));
        }
        $locked_existing_status = sanitize_key((string) $wpdb->get_var($wpdb->prepare(
            "SELECT status FROM {$table} WHERE post_id = %d AND user_id = %d LIMIT 1",
            $post_id,
            $user_id
        )));
        if (in_array($locked_existing_status, ['yes', 'waitlist'], true)) {
            $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock_name));
            return [
                'result' => 'joined',
                'status' => $locked_existing_status,
                'message' => $locked_existing_status === 'waitlist'
                    ? __('You are already on the waitlist.', 'nexdirectory')
                    : ($is_events_profile ? __('Your RSVP is already confirmed.', 'nexdirectory') : __('This listing is already saved.', 'nexdirectory')),
                'post_id' => $post_id,
                'rsvp_count' => count($this->get_event_attendees_by_status($post_id, 'yes')),
                'waitlist_count' => count($this->get_event_attendees_by_status($post_id, 'waitlist')),
                'next_intent' => 'leave',
                'button_label' => $leave_button_label,
            ];
        }
        $current_yes = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$table} WHERE post_id = %d AND status = 'yes'",
            $post_id
        ));
        $new_status = ($capacity > 0 && $current_yes >= $capacity) ? 'waitlist' : 'yes';
        $result = $wpdb->replace($table, [
            'post_id' => $post_id,
            'user_id' => $user_id,
            'status' => $new_status,
            'created_at' => $now,
            'updated_at' => $now,
        ], ['%d', '%d', '%s', '%s', '%s']);
        if ($result === false) {
            $this->append_runtime_log('error', 'RSVP save failed', [
                'post_id' => (string) $post_id,
                'user_id' => (string) $user_id,
                'status' => $new_status,
                'error' => (string) $wpdb->last_error,
            ]);
            $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock_name));
            return new WP_Error('nexdirectory_rsvp_save_failed', $is_events_profile ? __('RSVP could not be saved. Please retry.', 'nexdirectory') : __('Listing could not be saved. Please retry.', 'nexdirectory'));
        }
        $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock_name));
        $this->sync_rsvp_meta_from_table($post_id);
        clean_post_cache($post_id);
        if ($is_events_profile) {
            $this->notify_account_rsvp_change($post_id, $user_id, $new_status, 'joined');
        }

        $message = $new_status === 'waitlist'
            ? __('Capacity reached. You were added to waitlist.', 'nexdirectory')
            : ($is_events_profile ? __('RSVP confirmed.', 'nexdirectory') : __('Listing saved.', 'nexdirectory'));

        return [
            'result' => 'joined',
            'status' => $new_status,
            'message' => $message,
            'post_id' => $post_id,
            'rsvp_count' => count($this->get_event_attendees_by_status($post_id, 'yes')),
            'waitlist_count' => count($this->get_event_attendees_by_status($post_id, 'waitlist')),
            'next_intent' => 'leave',
            'button_label' => $leave_button_label,
        ];
    }


    public function handle_rsvp_event() {
        check_admin_referer('nexdirectory_rsvp_event', 'nexdirectory_rsvp_nonce');
        $post_id = isset($_POST['post_id']) ? absint((int) wp_unslash($_POST['post_id'])) : 0;
        $intent = sanitize_key((string) wp_unslash($_POST['intent'] ?? 'join'));
        $profile_key = $this->resolve_profile_key_for_post($post_id);
        $ui_labels = $this->get_site_profile_ui_labels($profile_key);
        $is_events_profile = (string) ($ui_labels['is_events_profile'] ?? '1') === '1';
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $singular_lower = function_exists('mb_strtolower') ? mb_strtolower($singular_label, 'UTF-8') : strtolower($singular_label);
        if (!is_user_logged_in()) {
            /* translators: %s: Placeholder value. */
            wp_die(
                $is_events_profile
                    ? esc_html__('Please log in to RSVP.', 'nexdirectory')
                    : esc_html(
                        sprintf(
                            /* translators: %s: Listing singular label (lowercase). */
                            esc_html__('Please log in to save this %s.', 'nexdirectory'),
                            $singular_lower
                        )
                    )
            );
        }
        if (!$this->user_can_access_platform(get_current_user_id())) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        if (!$post_id || get_post_type($post_id) !== self::POST_TYPE) {
            /* translators: %s: Placeholder value. */
            wp_die(
                $is_events_profile
                    ? esc_html__('Invalid event.', 'nexdirectory')
                    : esc_html(
                        sprintf(
                            /* translators: %s: Listing singular label (lowercase). */
                            esc_html__('Invalid %s.', 'nexdirectory'),
                            $singular_lower
                        )
                    )
            );
        }
        $redirect = wp_get_referer() ?: get_permalink($post_id);
        $result = $this->process_rsvp_action($post_id, $intent, get_current_user_id());
        if (is_wp_error($result)) {
            $this->redirect_with_message($redirect, $result->get_error_message(), 'error');
        }
        $this->redirect_with_message(
            $redirect,
            /* translators: %s: Placeholder value. */
            (string) ($result['message'] ?? ($is_events_profile ? __('RSVP updated.', 'nexdirectory') : sprintf(__('%s updated.', 'nexdirectory'), $singular_label)))
        );
    }


    public function handle_report_event() {
        check_admin_referer('nexdirectory_report_event', 'nexdirectory_report_nonce');

        $ui_labels = method_exists($this, 'get_site_profile_ui_labels') ? (array) $this->get_site_profile_ui_labels() : [];
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $singular_lower = function_exists('mb_strtolower') ? mb_strtolower($singular_label, 'UTF-8') : strtolower($singular_label);
        if ((string) get_option('nexdirectory_enable_event_reporting', '1') !== '1') {
            /* translators: %s: Placeholder value. */
            wp_die(esc_html(sprintf(esc_html__('%s reporting is disabled.', 'nexdirectory'), $singular_label)));
        }
        if (!is_user_logged_in()) {
            /* translators: %s: Placeholder value. */
            wp_die(esc_html(sprintf(esc_html__('Please log in to report %s.', 'nexdirectory'), $singular_lower)));
        }
        if (!$this->user_can_access_platform(get_current_user_id())) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        $post_id = isset($_POST['post_id']) ? absint((int) wp_unslash($_POST['post_id'])) : 0;
        $reason = sanitize_textarea_field((string) wp_unslash($_POST['reason'] ?? ''));
        if (!$post_id || !$reason || get_post_type($post_id) !== self::POST_TYPE) {
            wp_die(esc_html__('Invalid report.', 'nexdirectory'));
        }
        $post_status = (string) get_post_status($post_id);
        $owner_id = (int) get_post_field('post_author', $post_id);
        if ($post_status !== 'publish' && get_current_user_id() !== $owner_id && !current_user_can('edit_post', $post_id)) {
            wp_die(esc_html__('Invalid report.', 'nexdirectory'));
        }
        wp_insert_comment([
            'comment_post_ID' => $post_id,
            'comment_content' => $reason,
            'comment_type' => 'nexdirectory_report',
            'user_id' => get_current_user_id(),
            'comment_author' => wp_get_current_user()->display_name,
            'comment_author_email' => wp_get_current_user()->user_email,
            'comment_approved' => 1,
        ]);

        $admin_email = sanitize_email((string) get_option('admin_email'));
        if (is_email($admin_email)) {
            /* translators: %s: Placeholder value. */
            $subject = sprintf(__('%s report submitted', 'nexdirectory'), $singular_label);
            $moderation_url = admin_url('admin.php?page=nexdirectory-moderation');
            $body = sprintf(
                "%s: %s\nReason: %s\nLink: %s\nModeration Queue: %s",
                $singular_label,
                get_the_title($post_id),
                $reason,
                get_permalink($post_id),
                $moderation_url
            );

            $sent = (bool) $this->send_platform_email($admin_email, $subject, '', [
                'fallback_text' => $body,
            ]);
            if (!$sent) {
                $sent = (bool) wp_mail($admin_email, $subject, $body);
            }
            $this->append_runtime_log($sent ? 'info' : 'warning', 'Admin report notification email ' . ($sent ? 'sent' : 'failed'), [
                'post_id' => (string) $post_id,
                'admin_email' => (string) $admin_email,
            ]);
        }

        $this->redirect_with_message(
            wp_get_referer() ?: get_permalink($post_id),
            sprintf(
                /* translators: %s: Placeholder value. */
                __('Thanks for flagging this %s. Our moderation team will review it shortly.', 'nexdirectory'),
                $singular_lower
            )
        );
    }


    public function handle_contact_holder() {
        $nonce = isset($_POST['nexdirectory_contact_nonce'])
            ? sanitize_text_field((string) wp_unslash($_POST['nexdirectory_contact_nonce']))
            : '';
        if ($nonce === '' || !wp_verify_nonce($nonce, 'nexdirectory_contact_holder')) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'), '', ['response' => 400]);
        }
        $post_id = isset($_POST['post_id']) ? absint((int) wp_unslash($_POST['post_id'])) : 0;
        if ($post_id <= 0) {
            wp_die(esc_html__('Invalid request.', 'nexdirectory'), '', ['response' => 400]);
        }

        $ui_labels = method_exists($this, 'get_site_profile_ui_labels') ? (array) $this->get_site_profile_ui_labels() : [];
        $is_job_profile = (string) ($ui_labels['is_job_profile'] ?? '0') === '1';
        $is_events_profile = (string) ($ui_labels['is_events_profile'] ?? '1') === '1';
        $singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Event', 'nexdirectory')));
        $singular_lower = function_exists('mb_strtolower') ? mb_strtolower($singular_label, 'UTF-8') : strtolower($singular_label);
        if (!is_user_logged_in()) {
            wp_die(esc_html__('Please log in to contact organizers.', 'nexdirectory'));
        }
        if (!$this->user_can_access_platform(get_current_user_id())) {
            wp_die(esc_html__('Not allowed.', 'nexdirectory'));
        }
        if (!$post_id || get_post_type($post_id) !== self::POST_TYPE) {
            /* translators: %s: Placeholder value. */
            wp_die(esc_html(sprintf(esc_html__('Invalid %s.', 'nexdirectory'), $singular_lower)));
        }
        $post_status = (string) get_post_status($post_id);
        $owner_id = (int) get_post_field('post_author', $post_id);
        if ($post_status !== 'publish' && get_current_user_id() !== $owner_id && !current_user_can('edit_post', $post_id)) {
            /* translators: %s: Placeholder value. */
            wp_die(esc_html(sprintf(esc_html__('Invalid %s.', 'nexdirectory'), $singular_lower)));
        }
        if ($is_events_profile && $owner_id > 0 && !$this->user_allows_event_messages($owner_id)) {
            $this->redirect_with_message(
                wp_get_referer() ?: get_permalink($post_id),
                __('Messaging is disabled for this event.', 'nexdirectory'),
                'error'
            );
        }
        $recaptcha_token = sanitize_text_field((string) wp_unslash($_POST['nexdirectory_recaptcha_token'] ?? ''));
        if ($this->recaptcha_enabled() && !$this->verify_recaptcha_token($recaptcha_token)) {
            $this->increment_spam_counter();
            $this->redirect_with_message(wp_get_referer() ?: get_permalink($post_id), __('reCAPTCHA validation failed.', 'nexdirectory'), 'error');
        }

        $subject = sanitize_text_field((string) wp_unslash($_POST['subject'] ?? ''));
        $message = sanitize_textarea_field((string) wp_unslash($_POST['message'] ?? ''));

        $uploaded_attachment_path = '';
        $uploaded_attachment_url = '';
        $uploaded_attachment_file_id = 0;
        $application_cv_file = isset($_FILES['application_cv']) && is_array($_FILES['application_cv'])
            ? $this->sanitize_uploaded_file_array($_FILES['application_cv'])
            : [];
        $has_cv_upload = !empty($application_cv_file) && ((int) ($application_cv_file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE);
        if ($is_job_profile && $has_cv_upload) {
            $upload_error = (int) ($application_cv_file['error'] ?? UPLOAD_ERR_NO_FILE);
            if ($upload_error !== UPLOAD_ERR_OK) {
                $this->redirect_with_message(wp_get_referer() ?: get_permalink($post_id), __('CV upload failed. Please try again.', 'nexdirectory'), 'error');
            }
            $max_kb = max(64, min(20480, absint((int) get_option('nexdirectory_upload_file_max_kb', 2048))));
            $max_bytes = $max_kb * 1024;
            $file_size = absint((int) ($application_cv_file['size'] ?? 0));
            if ($file_size <= 0 || $file_size > $max_bytes) {
                $this->redirect_with_message(
                    wp_get_referer() ?: get_permalink($post_id),
                    /* translators: %d: Placeholder value. */
                    sprintf(__('CV must be a PDF up to %d KB.', 'nexdirectory'), $max_kb),
                    'error'
                );
            }
            $raw_file_name = sanitize_file_name((string) ($application_cv_file['name'] ?? 'cv.pdf'));
            $check = wp_check_filetype_and_ext((string) ($application_cv_file['tmp_name'] ?? ''), $raw_file_name, ['pdf' => 'application/pdf']);
            $ext = strtolower((string) ($check['ext'] ?? ''));
            if ($ext !== 'pdf') {
                $this->redirect_with_message(wp_get_referer() ?: get_permalink($post_id), __('Only PDF CV uploads are allowed.', 'nexdirectory'), 'error');
            }
            $temporary_path = (string) ($application_cv_file['tmp_name'] ?? '');
            if ($temporary_path === '' || !is_uploaded_file($temporary_path)) {
                $this->redirect_with_message(wp_get_referer() ?: get_permalink($post_id), __('CV upload could not be processed.', 'nexdirectory'), 'error');
            }
            // Move directly from PHP's temporary upload area into protected
            // storage. The CV never transits the public media/uploads directory.
            $uploaded_attachment_path = $temporary_path;
            $private_attachment = $this->protect_uploaded_attachment(
                $uploaded_attachment_path,
                $raw_file_name,
                $post_id,
                get_current_user_id(),
                $owner_id
            );
            if (is_wp_error($private_attachment)) {
                if ($uploaded_attachment_path !== '' && file_exists($uploaded_attachment_path) && is_file($uploaded_attachment_path)) {
                    wp_delete_file($uploaded_attachment_path);
                }
                $this->redirect_with_message(
                    wp_get_referer() ?: get_permalink($post_id),
                    $private_attachment->get_error_message(),
                    'error'
                );
            }
            $uploaded_attachment_file_id = absint((int) ($private_attachment['file_id'] ?? 0));
            $uploaded_attachment_path = (string) ($private_attachment['path'] ?? '');
            $uploaded_attachment_url = esc_url_raw((string) ($private_attachment['url'] ?? ''));
        }

        if ($is_job_profile) {
            if ($subject === '') {
                /* translators: %s: Placeholder value. */
                $subject = sprintf(__('Job application: %s', 'nexdirectory'), get_the_title($post_id));
            }
            if ($message === '' && $uploaded_attachment_url === '') {
                if ($uploaded_attachment_file_id > 0) {
                    $this->delete_private_attachment($uploaded_attachment_file_id);
                }
                $this->redirect_with_message(wp_get_referer() ?: get_permalink($post_id), __('Please add a short message or upload your CV.', 'nexdirectory'), 'error');
            }
        } elseif ($subject === '' || $message === '') {
            if ($uploaded_attachment_file_id > 0) {
                $this->delete_private_attachment($uploaded_attachment_file_id);
            }
            $this->redirect_with_message(wp_get_referer() ?: get_permalink($post_id), __('Subject and message are required.', 'nexdirectory'), 'error');
        }

        $sender_id = (int) get_current_user_id();
        $author_id = (int) get_post_field('post_author', $post_id);
        $recipient_id = $author_id;
        $contact_target = sanitize_key((string) wp_unslash($_POST['nexdirectory_contact_target'] ?? 'owner'));
        $send_to_admin_inbox = $is_events_profile && $contact_target === 'admin';

        if ($send_to_admin_inbox) {
            $admin_recipient_id = 0;
            $admin_email = sanitize_email((string) get_option('admin_email'));
            if (is_email((string) $admin_email)) {
                $admin_user = get_user_by('email', (string) $admin_email);
                if ($admin_user instanceof WP_User) {
                    $admin_recipient_id = (int) $admin_user->ID;
                }
            }
            if ($admin_recipient_id <= 0 || $admin_recipient_id === $sender_id) {
                $admin_ids = get_users([
                    'role' => 'administrator',
                    'fields' => 'ID',
                    'orderby' => 'ID',
                    'order' => 'ASC',
                    'number' => 20,
                ]);
                if (is_array($admin_ids)) {
                    foreach ($admin_ids as $admin_id_candidate) {
                        $admin_id_candidate = absint((int) $admin_id_candidate);
                        if ($admin_id_candidate > 0 && $admin_id_candidate !== $sender_id) {
                            $admin_recipient_id = $admin_id_candidate;
                            break;
                        }
                    }
                }
            }
            if ($admin_recipient_id > 0) {
                $recipient_id = $admin_recipient_id;
            }
        }

        if ($recipient_id <= 0) {
            if ($uploaded_attachment_file_id > 0) {
                $this->delete_private_attachment($uploaded_attachment_file_id);
            }
            /* translators: %s: Placeholder value. */
            $this->redirect_with_message(wp_get_referer() ?: get_permalink($post_id), sprintf(__('No inbox recipient is configured for this %s.', 'nexdirectory'), $singular_lower), 'error');
        }
        if ($recipient_id === $sender_id && !$send_to_admin_inbox) {
            if ($uploaded_attachment_file_id > 0) {
                $this->delete_private_attachment($uploaded_attachment_file_id);
            }
            $this->redirect_with_message(wp_get_referer() ?: get_permalink($post_id), __('You cannot message your own listing.', 'nexdirectory'), 'error');
        }

        $message_body = $message;
        if ($is_job_profile) {
            $message_body = trim((string) $message_body) !== '' ? $message_body : __('No message provided. CV attached.', 'nexdirectory');
        }
        if ($uploaded_attachment_url !== '') {
            $message_body .= "\n\n" . sprintf(
                /* translators: %s: Placeholder value. */
                __('Attachment: %s', 'nexdirectory'),
                $uploaded_attachment_url
            );
        }

        $send_result = $this->inbox_send_message(
            $post_id,
            $sender_id,
            $recipient_id,
            $subject,
            $message_body,
            $uploaded_attachment_url
        );
        if (is_wp_error($send_result)) {
            if ($uploaded_attachment_file_id > 0) {
                $this->delete_private_attachment($uploaded_attachment_file_id);
            }
            $this->redirect_with_message(wp_get_referer() ?: get_permalink($post_id), __('Message could not be sent to inbox. Please try again.', 'nexdirectory'), 'error');
        }

        $thread_id = absint((int) ($send_result['thread_id'] ?? 0));
        $message_id = absint((int) ($send_result['message_id'] ?? 0));
        if ($uploaded_attachment_file_id > 0 && $message_id > 0) {
            $this->bind_private_attachment_to_message($uploaded_attachment_file_id, $message_id, $recipient_id);
        }
        $inbox_only_delivery = (string) wp_unslash($_POST['nexdirectory_inbox_only'] ?? '0') === '1';
        if (!$inbox_only_delivery) {
            $this->send_inbox_message_notification_email(
                $recipient_id,
                $sender_id,
                $post_id,
                $thread_id,
                $message_id,
                $subject
            );
        }
        $this->record_analytics_event($post_id, 'enquiry_received', [
            'context' => $is_job_profile ? 'job_apply_form' : 'contact_form',
            'user_id' => get_current_user_id(),
        ]);

        $this->redirect_with_message(
            wp_get_referer() ?: get_permalink($post_id),
            $send_to_admin_inbox
                ? __('Message sent to admin inbox.', 'nexdirectory')
                : ($is_job_profile
                ? __('Application sent to the listing owner inbox.', 'nexdirectory')
                /* translators: %s: Placeholder value. */
                : sprintf(__('Message sent to this %s inbox.', 'nexdirectory'), $singular_lower))
        );
    }


    public function on_event_status_transition($new_status, $old_status, $post) {
        if (!$post) {
            return;
        }
        $post_type = sanitize_key((string) ($post->post_type ?? ''));
        if ($post_type === '') {
            return;
        }
        if ($post_type !== self::POST_TYPE) {
            return;
        }
        if ($this->is_generating_recurrences) {
            return;
        }

        $post_id = (int) $post->ID;

        if ($old_status === 'pending' && $new_status !== 'pending') {
            delete_post_meta($post_id, 'nexdirectory_admin_pending_notified');
        }
        if ($new_status === 'pending' && $old_status !== 'pending') {
            $this->notify_admin_submission($post_id);
        }

        if ($new_status !== 'publish') {
            return;
        }
        if ($old_status !== 'publish') {
            $this->maybe_confirm_requested_official_event($post_id);
            if (get_post_meta($post_id, 'nexdirectory_generated_from_parent', true)) {
                return;
            }
            $this->generate_recurrences($post_id);
        }
    }


    private function generate_recurrences($post_id) {
        $recurrence = get_post_meta($post_id, 'nexdirectory_recurrence', true);
        $until = get_post_meta($post_id, 'nexdirectory_recurrence_until', true);
        $start = get_post_meta($post_id, 'nexdirectory_start', true);
        $end = get_post_meta($post_id, 'nexdirectory_end', true);
        if (!in_array($recurrence, ['daily', 'weekly', 'monthly'], true) || !$until || !$start) {
            return;
        }
        if (get_post_meta($post_id, 'nexdirectory_recurrence_generated', true) === '1') {
            return;
        }

        $start_ts = strtotime($start);
        $end_ts = $end ? strtotime($end) : false;
        $until_ts = strtotime($until . ' 23:59:59');
        if (!$start_ts || !$until_ts || $until_ts <= $start_ts) {
            return;
        }

        $step = '+1 day';
        if ($recurrence === 'weekly') {
            $step = '+1 week';
        } elseif ($recurrence === 'monthly') {
            $step = '+1 month';
        }

        $limit = 52;
        $count = 0;
        $current_start = $start_ts;
        $this->is_generating_recurrences = true;
        while ($count < $limit) {
            $current_start = strtotime($step, $current_start);
            if (!$current_start || $current_start > $until_ts) {
                break;
            }
            $author_id = (int) get_post_field('post_author', $post_id);
            if (!$this->can_create_more_events($author_id, 1)) {
                break;
            }
            $new_id = wp_insert_post([
                'post_type' => self::POST_TYPE,
                'post_status' => 'publish',
                'post_author' => $author_id,
                'post_title' => get_the_title($post_id),
                'post_content' => get_post_field('post_content', $post_id),
                'post_excerpt' => get_post_field('post_excerpt', $post_id),
            ]);
            if (!$new_id || is_wp_error($new_id)) {
                break;
            }

            foreach ($this->get_event_meta_fields() as $key => $type) {
                update_post_meta($new_id, $key, get_post_meta($post_id, $key, true));
            }
            update_post_meta($new_id, 'nexdirectory_generated_from_parent', (string) $post_id);
            update_post_meta($new_id, 'nexdirectory_recurrence', 'none');
            update_post_meta($new_id, 'nexdirectory_recurrence_until', '');
            update_post_meta($new_id, 'nexdirectory_start', gmdate('Y-m-d H:i:s', $current_start));
            if ($end_ts) {
                $duration = max(0, $end_ts - $start_ts);
                update_post_meta($new_id, 'nexdirectory_end', gmdate('Y-m-d H:i:s', $current_start + $duration));
            }
            wp_set_post_terms($new_id, wp_get_post_terms($post_id, self::TAX_TYPE, ['fields' => 'ids']), self::TAX_TYPE, false);
            wp_set_post_terms($new_id, wp_get_post_terms($post_id, self::TAX_MODE, ['fields' => 'ids']), self::TAX_MODE, false);

            $count++;
        }
        $this->is_generating_recurrences = false;

        update_post_meta($post_id, 'nexdirectory_recurrence_generated', '1');
    }    public function handle_calendar_export() {
        $post_id = absint(get_query_var('nexdirectory_ics'));
        if (!$post_id || get_post_type($post_id) !== self::POST_TYPE) {
            return;
        }
        $post = get_post($post_id);
        if (!$post || $post->post_status !== 'publish') {
            status_header(404);
            exit;
        }

        $start_raw = trim((string) get_post_meta($post_id, 'nexdirectory_start', true));
        $end_raw = trim((string) get_post_meta($post_id, 'nexdirectory_end', true));
        $timezone_name = $this->sanitize_timezone_value((string) get_post_meta($post_id, 'nexdirectory_timezone', true));
        try {
            $event_timezone = $timezone_name !== '' ? new DateTimeZone($timezone_name) : wp_timezone();
        } catch (Exception $e) {
            $event_timezone = wp_timezone();
        }

        $is_all_day = (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $start_raw);
        $start_datetime = $this->parse_ics_event_datetime($start_raw, $event_timezone, $is_all_day);
        if (!$start_datetime instanceof DateTimeImmutable) {
            status_header(400);
            exit;
        }

        $end_datetime = $this->parse_ics_event_datetime($end_raw, $event_timezone, $is_all_day);
        if (!$end_datetime instanceof DateTimeImmutable) {
            $end_datetime = $is_all_day ? $start_datetime->modify('+1 day') : $start_datetime->modify('+1 hour');
        } elseif ($is_all_day) {
            $end_datetime = $end_datetime->modify('+1 day');
        }
        if ($end_datetime <= $start_datetime) {
            $end_datetime = $is_all_day ? $start_datetime->modify('+1 day') : $start_datetime->modify('+1 hour');
        }

        $site_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        $site_host = trim((string) preg_replace('/[^a-z0-9.-]/', '', $site_host), '.-');
        if ($site_host === '') {
            $site_host = 'local';
        }
        $summary = html_entity_decode(nexdirectory_strip_all_tags((string) $post->post_title), ENT_QUOTES, get_bloginfo('charset') ?: 'UTF-8');
        $description = html_entity_decode(
            nexdirectory_strip_all_tags((string) ($post->post_excerpt ?: $post->post_content)),
            ENT_QUOTES,
            get_bloginfo('charset') ?: 'UTF-8'
        );
        $location = html_entity_decode(
            nexdirectory_strip_all_tags((string) get_post_meta($post_id, 'nexdirectory_address', true)),
            ENT_QUOTES,
            get_bloginfo('charset') ?: 'UTF-8'
        );
        $modified_timestamp = strtotime((string) $post->post_modified_gmt . ' UTC');
        $published_timestamp = strtotime((string) $post->post_date_gmt . ' UTC');
        $modified_timestamp = $modified_timestamp ? (int) $modified_timestamp : time();
        $published_timestamp = $published_timestamp ? (int) $published_timestamp : $modified_timestamp;
        $sequence = max(0, (int) floor(($modified_timestamp - $published_timestamp) / MINUTE_IN_SECONDS));

        $lines = [
            'BEGIN:VCALENDAR',
            'VERSION:2.0',
            'PRODID:-//Nex Directory//Calendar//EN',
            'CALSCALE:GREGORIAN',
            'METHOD:PUBLISH',
            'BEGIN:VEVENT',
            'UID:nexdirectory-' . get_current_blog_id() . '-' . $post_id . '@' . $site_host,
            'DTSTAMP:' . gmdate('Ymd\THis\Z', $modified_timestamp),
            'CREATED:' . gmdate('Ymd\THis\Z', $published_timestamp),
            'LAST-MODIFIED:' . gmdate('Ymd\THis\Z', $modified_timestamp),
            'SEQUENCE:' . $sequence,
            'STATUS:CONFIRMED',
        ];
        if ($is_all_day) {
            $lines[] = 'DTSTART;VALUE=DATE:' . $start_datetime->format('Ymd');
            $lines[] = 'DTEND;VALUE=DATE:' . $end_datetime->format('Ymd');
        } else {
            $utc_timezone = new DateTimeZone('UTC');
            $lines[] = 'DTSTART:' . $start_datetime->setTimezone($utc_timezone)->format('Ymd\THis\Z');
            $lines[] = 'DTEND:' . $end_datetime->setTimezone($utc_timezone)->format('Ymd\THis\Z');
        }
        $lines[] = 'SUMMARY:' . $this->escape_ics_text($summary);
        $lines[] = 'DESCRIPTION:' . $this->escape_ics_text($description);
        $lines[] = 'LOCATION:' . $this->escape_ics_text($location);
        $lines[] = 'URL:' . esc_url((string) get_permalink($post_id));
        $lines[] = 'END:VEVENT';
        $lines[] = 'END:VCALENDAR';

        $folded_lines = array_map([$this, 'fold_ics_line'], $lines);
        $ics = implode("\r\n", $folded_lines) . "\r\n";

        nocache_headers();
        header('Content-Type: text/calendar; charset=utf-8');
        header('Content-Disposition: attachment; filename="nex-directory-event-' . $post_id . '.ics"');
        echo $ics; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- RFC 5545 bytes are escaped and must not be HTML-encoded.
        exit;
    }

    /**
     * Parses a stored event value in its declared event timezone.
     *
     * @param string $value
     * @param DateTimeZone $timezone
     * @param bool $date_only
     * @return DateTimeImmutable|null
     */
    private function parse_ics_event_datetime($value, DateTimeZone $timezone, $date_only = false) {
        $value = trim((string) $value);
        if ($value === '') {
            return null;
        }
        $formats = $date_only
            ? ['!Y-m-d']
            : ['!Y-m-d H:i:s', '!Y-m-d H:i', '!Y-m-d\TH:i:s', '!Y-m-d\TH:i'];
        foreach ($formats as $format) {
            $datetime = DateTimeImmutable::createFromFormat($format, $value, $timezone);
            $errors = DateTimeImmutable::getLastErrors();
            if (
                $datetime instanceof DateTimeImmutable
                && ($errors === false || ((int) ($errors['warning_count'] ?? 0) === 0 && (int) ($errors['error_count'] ?? 0) === 0))
            ) {
                return $datetime;
            }
        }
        try {
            return new DateTimeImmutable($value, $timezone);
        } catch (Exception $e) {
            return null;
        }
    }

    /**
     * Escapes one RFC 5545 text value.
     *
     * @param string $value
     * @return string
     */
    private function escape_ics_text($value) {
        $value = str_replace(["\r\n", "\r"], "\n", (string) $value);
        return str_replace(
            ['\\', ';', ',', "\n"],
            ['\\\\', '\\;', '\\,', '\\n'],
            $value
        );
    }

    /**
     * Folds a content line to RFC 5545's 75-octet limit.
     *
     * @param string $line
     * @return string
     */
    private function fold_ics_line($line) {
        $remaining = (string) $line;
        $chunks = [];
        $limit = 73;
        while (strlen($remaining) > $limit) {
            $chunk = function_exists('mb_strcut')
                ? mb_strcut($remaining, 0, $limit, 'UTF-8')
                : substr($remaining, 0, $limit);
            if ($chunk === '') {
                break;
            }
            $chunks[] = $chunk;
            $remaining = substr($remaining, strlen($chunk));
            $limit = 72;
        }
        $chunks[] = $remaining;
        return implode("\r\n ", $chunks);
    }


    private function emit_json_ld_schema(array $schema) {
        if (empty($schema)) {
            return;
        }
        $json = wp_json_encode($schema);
        if (!is_string($json) || $json === '') {
            return;
        }
        wp_print_inline_script_tag($json, ['type' => 'application/ld+json']);
    }


    private function normalize_schema_text($value, $max_length = 500) {
        $text = trim((string) nexdirectory_strip_all_tags((string) $value));
        if ($text !== '') {
            $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        }
        if ($text === '') {
            return '';
        }
        $text = (string) preg_replace('/\s+/u', ' ', $text);
        $max_length = max(40, min(5000, (int) $max_length));
        if (function_exists('mb_strlen') && function_exists('mb_substr')) {
            if (mb_strlen($text, 'UTF-8') > $max_length) {
                $text = rtrim((string) mb_substr($text, 0, $max_length, 'UTF-8')) . '…';
            }
            return $text;
        }
        if (strlen($text) > $max_length) {
            $text = rtrim(substr($text, 0, $max_length)) . '...';
        }
        return $text;
    }


    private function get_schema_home_url() {
        return esc_url_raw((string) home_url('/'));
    }


    private function get_schema_site_logo_url() {
        $logo_url = esc_url_raw((string) get_option('nexdirectory_frontend_sticky_nav_logo_url', ''));
        if ($logo_url !== '') {
            return $logo_url;
        }

        $custom_logo_id = absint((int) get_theme_mod('custom_logo'));
        if ($custom_logo_id > 0) {
            $custom_logo_url = esc_url_raw((string) wp_get_attachment_image_url($custom_logo_id, 'full'));
            if ($custom_logo_url !== '') {
                return $custom_logo_url;
            }
        }

        $site_icon_id = absint((int) get_option('site_icon', 0));
        if ($site_icon_id > 0) {
            $site_icon_url = esc_url_raw((string) wp_get_attachment_image_url($site_icon_id, 'full'));
            if ($site_icon_url !== '') {
                return $site_icon_url;
            }
        }

        return '';
    }


    private function build_global_organization_schema() {
        $home_url = $this->get_schema_home_url();
        if ($home_url === '') {
            return [];
        }

        $brand_name = method_exists($this, 'get_white_label_brand_name')
            ? sanitize_text_field((string) $this->get_white_label_brand_name())
            : '';
        if ($brand_name === '') {
            $brand_name = $this->normalize_schema_text((string) get_option('blogname', get_bloginfo('name')), 140);
        }
        if ($brand_name === '') {
            return [];
        }

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'Organization',
            '@id' => $home_url . '#organization',
            'name' => $brand_name,
            'url' => $home_url,
        ];

        $description = $this->normalize_schema_text((string) get_bloginfo('description'), 260);
        if ($description !== '') {
            $schema['description'] = $description;
        }

        $logo_url = $this->get_schema_site_logo_url();
        if ($logo_url !== '') {
            $schema['logo'] = [
                '@type' => 'ImageObject',
                'url' => $logo_url,
            ];
        }

        return $schema;
    }


    private function build_global_website_schema() {
        $home_url = $this->get_schema_home_url();
        if ($home_url === '') {
            return [];
        }

        $site_name = method_exists($this, 'get_white_label_brand_name')
            ? sanitize_text_field((string) $this->get_white_label_brand_name())
            : '';
        if ($site_name === '') {
            $site_name = $this->normalize_schema_text((string) get_option('blogname', get_bloginfo('name')), 140);
        }
        if ($site_name === '') {
            return [];
        }

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'WebSite',
            '@id' => $home_url . '#website',
            'name' => $site_name,
            'url' => $home_url,
            'inLanguage' => str_replace('_', '-', get_locale()),
            'publisher' => [
                '@id' => $home_url . '#organization',
            ],
        ];

        $description = $this->normalize_schema_text((string) get_bloginfo('description'), 260);
        if ($description !== '') {
            $schema['description'] = $description;
        }

        $search_target = esc_url_raw((string) nexdirectory_add_query_arg('s', '{search_term_string}', $home_url));
        if ($search_target !== '') {
            $schema['potentialAction'] = [
                '@type' => 'SearchAction',
                'target' => $search_target,
                'query-input' => 'required name=search_term_string',
            ];
        }

        return $schema;
    }


    private function build_generic_page_schema() {
        $url = '';
        $page_id = absint((int) get_queried_object_id());
        if (is_singular() && $page_id > 0) {
            $permalink = get_permalink($page_id);
            if (is_string($permalink) && $permalink !== '') {
                $url = $permalink;
            }
        }
        if ($url === '') {
            $url = (string) $this->current_url();
        }
        $url = esc_url_raw((string) $url);
        if ($url === '') {
            return [];
        }

        $title = $this->normalize_schema_text((string) wp_get_document_title(), 140);
        if ($title === '' && $page_id > 0) {
            $title = $this->normalize_schema_text(get_the_title($page_id), 140);
        }
        if ($title === '') {
            return [];
        }

        $schema_type = 'WebPage';
        if (is_search()) {
            $schema_type = 'SearchResultsPage';
        } elseif (is_author()) {
            $schema_type = 'ProfilePage';
        } elseif (is_home() || is_front_page() || is_archive()) {
            $schema_type = 'CollectionPage';
        }

        $home_url = $this->get_schema_home_url();
        $schema = [
            '@context' => 'https://schema.org',
            '@type' => $schema_type,
            '@id' => $url . '#webpage',
            'name' => $title,
            'url' => $url,
            'inLanguage' => str_replace('_', '-', get_locale()),
        ];
        if ($home_url !== '') {
            $schema['isPartOf'] = [
                '@id' => $home_url . '#website',
            ];
        }

        $description = '';
        if ($page_id > 0) {
            $excerpt = (string) get_post_field('post_excerpt', $page_id);
            $content = (string) get_post_field('post_content', $page_id);
            if ($excerpt !== '') {
                $description = $this->normalize_schema_text($excerpt, 360);
            } elseif ($content !== '') {
                $description = $this->normalize_schema_text($content, 360);
            }
        } elseif (is_tax() || is_category() || is_tag()) {
            $term = get_queried_object();
            if ($term instanceof WP_Term) {
                $description = $this->normalize_schema_text((string) term_description((int) $term->term_id), 360);
            }
        } elseif (is_author()) {
            $author_object = get_queried_object();
            if ($author_object instanceof WP_User) {
                $description = $this->normalize_schema_text((string) $author_object->description, 360);
            } elseif (is_object($author_object) && isset($author_object->description)) {
                $description = $this->normalize_schema_text((string) $author_object->description, 360);
            }
        } elseif (is_search()) {
            $search_query = $this->normalize_schema_text(get_search_query(false), 100);
            if ($search_query !== '') {
                $description = $this->normalize_schema_text(
                    sprintf(
                        /* translators: %s: search query */
                        __('Search results for "%s".', 'nexdirectory'),
                        $search_query
                    ),
                    360
                );
            }
        }

        if ($description === '') {
            $description = $this->normalize_schema_text((string) get_bloginfo('description'), 360);
        }
        if ($description !== '') {
            $schema['description'] = $description;
        }

        if ($page_id > 0) {
            $featured_image_url = esc_url_raw((string) get_the_post_thumbnail_url($page_id, 'full'));
            if ($featured_image_url !== '') {
                $schema['primaryImageOfPage'] = [
                    '@type' => 'ImageObject',
                    'url' => $featured_image_url,
                ];
            }
        }

        if (is_search()) {
            $search_target = esc_url_raw((string) nexdirectory_add_query_arg('s', '{search_term_string}', $home_url !== '' ? $home_url : $url));
            if ($search_target !== '') {
                $schema['potentialAction'] = [
                    '@type' => 'SearchAction',
                    'target' => $search_target,
                    'query-input' => 'required name=search_term_string',
                ];
            }
        }

        if (is_author()) {
            $author_object = get_queried_object();
            if ($author_object instanceof WP_User) {
                $person_schema = $this->build_person_schema_from_user($author_object, $url);
            } elseif (is_object($author_object) && isset($author_object->ID)) {
                $person_schema = $this->build_person_schema_from_user((int) $author_object->ID, $url);
            } else {
                $person_schema = [];
            }
            if (!empty($person_schema['@id'])) {
                $schema['mainEntity'] = [
                    '@id' => (string) $person_schema['@id'],
                ];
            }
        }

        return $schema;
    }


    private function build_generic_breadcrumb_schema($current_label = '') {
        if (is_front_page()) {
            return [];
        }

        $home_url = $this->get_schema_home_url();
        if ($home_url === '') {
            $home_url = esc_url_raw((string) $this->current_url());
        }

        $items = [];
        $items[] = [
            'label' => __('Home', 'nexdirectory'),
            'url' => $home_url,
        ];

        $current_label = $this->normalize_schema_text($current_label, 140);
        if (is_singular()) {
            $page_id = absint((int) get_queried_object_id());
            if ($page_id > 0) {
                $ancestors = array_reverse((array) get_post_ancestors($page_id));
                foreach ($ancestors as $ancestor_id) {
                    $ancestor_id = absint((int) $ancestor_id);
                    if ($ancestor_id <= 0) {
                        continue;
                    }
                    $ancestor_label = $this->normalize_schema_text(get_the_title($ancestor_id), 120);
                    $ancestor_url = esc_url_raw((string) get_permalink($ancestor_id));
                    if ($ancestor_label === '' || $ancestor_url === '') {
                        continue;
                    }
                    $items[] = [
                        'label' => $ancestor_label,
                        'url' => $ancestor_url,
                    ];
                }

                $page_label = $current_label !== '' ? $current_label : $this->normalize_schema_text(get_the_title($page_id), 140);
                $page_url = esc_url_raw((string) get_permalink($page_id));
                if ($page_label !== '' && $page_url !== '') {
                    $items[] = [
                        'label' => $page_label,
                        'url' => $page_url,
                    ];
                }
            }
        } elseif (is_tax() || is_category() || is_tag()) {
            $term = get_queried_object();
            if ($term instanceof WP_Term) {
                $parent_ids = array_reverse((array) get_ancestors((int) $term->term_id, (string) $term->taxonomy, 'taxonomy'));
                foreach ($parent_ids as $parent_id) {
                    $parent_id = absint((int) $parent_id);
                    if ($parent_id <= 0) {
                        continue;
                    }
                    $parent_term = get_term($parent_id, (string) $term->taxonomy);
                    if (!$parent_term instanceof WP_Term) {
                        continue;
                    }
                    $parent_label = $this->normalize_schema_text((string) $parent_term->name, 120);
                    $parent_link = get_term_link($parent_term);
                    $parent_url = is_wp_error($parent_link) ? '' : esc_url_raw((string) $parent_link);
                    if ($parent_label === '' || $parent_url === '') {
                        continue;
                    }
                    $items[] = [
                        'label' => $parent_label,
                        'url' => $parent_url,
                    ];
                }

                $term_label = $current_label !== '' ? $current_label : $this->normalize_schema_text((string) $term->name, 140);
                $term_link = get_term_link($term);
                $term_url = is_wp_error($term_link) ? '' : esc_url_raw((string) $term_link);
                if ($term_label !== '' && $term_url !== '') {
                    $items[] = [
                        'label' => $term_label,
                        'url' => $term_url,
                    ];
                }
            }
        } elseif (is_author()) {
            $author_object = get_queried_object();
            if ($author_object instanceof WP_User) {
                $author_label = $current_label !== '' ? $current_label : $this->normalize_schema_text((string) $author_object->display_name, 140);
                $author_url = esc_url_raw((string) get_author_posts_url((int) $author_object->ID));
                if ($author_label !== '' && $author_url !== '') {
                    $items[] = [
                        'label' => $author_label,
                        'url' => $author_url,
                    ];
                }
            } elseif (is_object($author_object) && isset($author_object->ID)) {
                $author_id = absint((int) $author_object->ID);
                $author_label = $current_label !== '' ? $current_label : $this->normalize_schema_text((string) ($author_object->display_name ?? ''), 140);
                $author_url = esc_url_raw((string) get_author_posts_url($author_id));
                if ($author_label !== '' && $author_url !== '') {
                    $items[] = [
                        'label' => $author_label,
                        'url' => $author_url,
                    ];
                }
            }
        } else {
            $archive_title = $current_label !== '' ? $current_label : $this->normalize_schema_text((string) wp_get_document_title(), 140);
            $archive_url = esc_url_raw((string) $this->current_url());
            if ($archive_title !== '' && $archive_url !== '') {
                $items[] = [
                    'label' => $archive_title,
                    'url' => $archive_url,
                ];
            }
        }

        if (count($items) < 2) {
            return [];
        }

        $crumbs = [];
        $position = 1;
        foreach ($items as $item) {
            $label = $this->normalize_schema_text((string) ($item['label'] ?? ''), 140);
            $item_url = esc_url_raw((string) ($item['url'] ?? ''));
            if ($label === '' || $item_url === '') {
                continue;
            }
            $crumbs[] = [
                '@type' => 'ListItem',
                'position' => $position,
                'item' => [
                    '@id' => $item_url,
                    'name' => $label,
                ],
            ];
            $position++;
        }

        if (count($crumbs) < 2) {
            return [];
        }

        return [
            '@context' => 'https://schema.org',
            '@type' => 'BreadcrumbList',
            'itemListElement' => $crumbs,
        ];
    }


    private function build_person_schema_from_user($user, $primary_url = '') {
        $user_id = $user instanceof WP_User ? (int) $user->ID : absint((int) $user);
        $profile = $this->get_public_organizer_profile($user_id, true);
        if (empty($profile)) {
            return [];
        }
        $name = $this->normalize_schema_text((string) ($profile['name'] ?? ''), 140);
        if ($name === '') {
            return [];
        }

        $profile_url = esc_url_raw((string) ($profile['profile_url'] ?? ''));
        if ($profile_url === '') {
            $profile_url = esc_url_raw((string) $primary_url);
        }

        $person_id = $profile_url !== ''
            ? $profile_url . '#person'
            : $this->get_schema_home_url() . '#person-' . $user_id;

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'Person',
            '@id' => $person_id,
            'name' => $name,
        ];
        if ($profile_url !== '') {
            $schema['url'] = $profile_url;
        }

        $bio = $this->normalize_schema_text((string) ($profile['bio'] ?? ''), 600);
        if ($bio !== '') {
            $schema['description'] = $bio;
        }

        $avatar_url = esc_url_raw((string) ($profile['avatar_url'] ?? ''));
        if ($avatar_url !== '') {
            $schema['image'] = $avatar_url;
        }
        $public_email = sanitize_email((string) ($profile['email'] ?? ''));
        if (is_email($public_email)) {
            $schema['email'] = $public_email;
        }
        $public_phone = sanitize_text_field((string) ($profile['phone'] ?? ''));
        if ($public_phone !== '') {
            $schema['telephone'] = $public_phone;
        }
        $same_as = array_values(array_filter(array_map('esc_url_raw', [
            (string) ($profile['website_url'] ?? ''),
            (string) ($profile['linkedin_url'] ?? ''),
            (string) ($profile['instagram_url'] ?? ''),
        ])));
        if (!empty($same_as)) {
            $schema['sameAs'] = $same_as;
        }
        $location = $this->normalize_schema_text((string) ($profile['location'] ?? ''), 160);
        if ($location !== '') {
            $schema['homeLocation'] = [
                '@type' => 'Place',
                'name' => $location,
            ];
        }

        return $schema;
    }


    private function build_event_speaker_schema_payloads($post_id, $event_url = '') {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return [
                'schemas' => [],
                'references' => [],
            ];
        }

        $speakers = $this->parse_key_speakers((string) get_post_meta($post_id, 'nexdirectory_key_speakers', true));
        if (!is_array($speakers) || empty($speakers)) {
            return [
                'schemas' => [],
                'references' => [],
            ];
        }

        $event_url = esc_url_raw((string) $event_url);
        $speaker_counts = [];
        $speaker_schemas = [];
        $speaker_refs = [];
        foreach (array_values(array_slice((array) $speakers, 0, 24)) as $index => $speaker_row) {
            if (!is_array($speaker_row)) {
                continue;
            }
            $name = $this->normalize_schema_text((string) ($speaker_row['name'] ?? ''), 140);
            if ($name === '') {
                continue;
            }
            $slug_base = sanitize_title($name);
            if ($slug_base === '') {
                $slug_base = 'speaker-' . ((int) $index + 1);
            }
            $slug_count = isset($speaker_counts[$slug_base]) ? ((int) $speaker_counts[$slug_base] + 1) : 1;
            $speaker_counts[$slug_base] = $slug_count;
            $slug = $slug_base . ($slug_count > 1 ? '-' . $slug_count : '');
            $speaker_id = $event_url !== ''
                ? $event_url . '#speaker-' . $slug
                : $this->get_schema_home_url() . '#speaker-' . (int) $post_id . '-' . ((int) $index + 1);

            $speaker_schema = [
                '@context' => 'https://schema.org',
                '@type' => 'Person',
                '@id' => $speaker_id,
                'name' => $name,
            ];

            $profession = $this->normalize_schema_text((string) ($speaker_row['profession'] ?? ''), 140);
            if ($profession !== '') {
                $speaker_schema['jobTitle'] = $profession;
            }
            $subject = $this->normalize_schema_text((string) ($speaker_row['subject'] ?? ''), 180);
            if ($subject !== '') {
                $speaker_schema['knowsAbout'] = $subject;
            }
            $details = $this->normalize_schema_text((string) ($speaker_row['details'] ?? ''), 500);
            if ($details !== '') {
                $speaker_schema['description'] = $details;
            }
            $image_url = esc_url_raw((string) ($speaker_row['image'] ?? ''));
            if ($image_url !== '') {
                $speaker_schema['image'] = $image_url;
            }

            $speaker_schemas[] = $speaker_schema;
            $speaker_refs[] = [
                '@id' => $speaker_id,
            ];
        }

        return [
            'schemas' => $speaker_schemas,
            'references' => $speaker_refs,
        ];
    }


    private function build_event_sub_event_schema_items($post_id, $event_tz, $fallback_start_dt, $fallback_end_dt) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return [];
        }

        $sessions = $this->parse_sessions((string) get_post_meta($post_id, 'nexdirectory_sessions', true));
        if (!is_array($sessions) || empty($sessions)) {
            return [];
        }

        if (!$event_tz instanceof DateTimeZone) {
            try {
                $event_tz = new DateTimeZone('UTC');
            } catch (Exception $e) {
                return [];
            }
        }

        $fallback_start_ts = $fallback_start_dt instanceof DateTime ? (int) $fallback_start_dt->getTimestamp() : 0;
        $fallback_end_ts = $fallback_end_dt instanceof DateTime ? (int) $fallback_end_dt->getTimestamp() : 0;
        if ($fallback_end_ts > 0 && $fallback_start_ts > 0 && $fallback_end_ts <= $fallback_start_ts) {
            $fallback_end_ts = $fallback_start_ts + HOUR_IN_SECONDS;
        }

        $sub_events = [];
        foreach (array_values(array_slice((array) $sessions, 0, 30)) as $index => $session_row) {
            if (!is_array($session_row)) {
                continue;
            }

            $session_title = $this->normalize_schema_text((string) ($session_row['title'] ?? ''), 180);
            if ($session_title === '') {
                /* translators: %d: Placeholder value. */
                $session_title = sprintf(__('Session %d', 'nexdirectory'), (int) $index + 1);
            }

            $session_start_raw = trim((string) ($session_row['start'] ?? ''));
            $session_end_raw = trim((string) ($session_row['end'] ?? ''));
            $session_start_dt = null;
            $session_end_dt = null;

            try {
                if ($session_start_raw !== '') {
                    $session_start_dt = new DateTime($session_start_raw, $event_tz);
                }
            } catch (Exception $e) {
                $session_start_dt = null;
            }
            if (!$session_start_dt instanceof DateTime && $fallback_start_ts > 0) {
                try {
                    $session_start_dt = new DateTime('@' . $fallback_start_ts);
                    $session_start_dt->setTimezone($event_tz);
                } catch (Exception $e) {
                    $session_start_dt = null;
                }
            }

            try {
                if ($session_end_raw !== '') {
                    $session_end_dt = new DateTime($session_end_raw, $event_tz);
                }
            } catch (Exception $e) {
                $session_end_dt = null;
            }
            if (!$session_end_dt instanceof DateTime && $session_start_dt instanceof DateTime) {
                $session_end_dt = clone $session_start_dt;
                $session_end_dt->modify('+1 hour');
            }
            if (!$session_end_dt instanceof DateTime && $fallback_end_ts > 0) {
                try {
                    $session_end_dt = new DateTime('@' . $fallback_end_ts);
                    $session_end_dt->setTimezone($event_tz);
                } catch (Exception $e) {
                    $session_end_dt = null;
                }
            }
            if ($session_start_dt instanceof DateTime && $session_end_dt instanceof DateTime && $session_end_dt <= $session_start_dt) {
                $session_end_dt = clone $session_start_dt;
                $session_end_dt->modify('+1 hour');
            }

            $session_location = $this->normalize_schema_text((string) ($session_row['location'] ?? ''), 180);
            $session_speaker = $this->normalize_schema_text((string) ($session_row['speaker'] ?? ''), 140);
            $sub_event = [
                '@type' => 'Event',
                'name' => $session_title,
            ];
            if ($session_start_dt instanceof DateTime) {
                $sub_event['startDate'] = $session_start_dt->format('c');
            }
            if ($session_end_dt instanceof DateTime) {
                $sub_event['endDate'] = $session_end_dt->format('c');
            }
            if ($session_location !== '') {
                $sub_event['location'] = [
                    '@type' => 'Place',
                    'name' => $session_location,
                ];
            }
            if ($session_speaker !== '') {
                $sub_event['performer'] = [
                    '@type' => 'Person',
                    'name' => $session_speaker,
                ];
            }

            $sub_events[] = $sub_event;
        }

        return $sub_events;
    }


    private function build_supporting_document_schema_items($post_id, $limit = 6) {
        $post_id = absint((int) $post_id);
        $limit = max(1, min(20, (int) $limit));
        if ($post_id <= 0) {
            return [];
        }
        $urls = $this->parse_multiline_urls((string) get_post_meta($post_id, 'nexdirectory_supporting_docs', true));
        if (empty($urls)) {
            return [];
        }
        $items = [];
        foreach (array_slice((array) $urls, 0, $limit) as $index => $url) {
            $url = esc_url_raw((string) $url);
            if ($url === '') {
                continue;
            }
            $path = (string) wp_parse_url($url, PHP_URL_PATH);
            $extension = strtolower((string) pathinfo($path, PATHINFO_EXTENSION));
            $mime = wp_check_filetype($url);
            $media_item = [
                '@type' => 'MediaObject',
                'contentUrl' => $url,
                'url' => $url,
                /* translators: %d: Placeholder value. */
                'name' => sprintf(__('Document %d', 'nexdirectory'), (int) $index + 1),
            ];
            if (!empty($mime['type'])) {
                $media_item['encodingFormat'] = sanitize_text_field((string) $mime['type']);
            } elseif ($extension !== '') {
                $media_item['encodingFormat'] = 'application/' . sanitize_key($extension);
            }
            $items[] = $media_item;
        }
        return $items;
    }


    private function get_platform_schema_shortcode_map() {
        $prefixes = ['nexdirectory', 'nexdirectory'];
        $suffixes_by_context = [
            'archive' => ['events_archive', 'listings_archive', 'events_query_grid', 'events_bento_grid', 'events_ajax_search'],
            'map' => ['events_map', 'listings_map'],
            'calendar' => ['events_calendar', 'listings_calendar'],
            'submit' => ['submit_event', 'submit'],
            'my_items' => ['my_events', 'my_listings'],
            'attending' => ['my_tickets', 'saved_listings'],
            'attendees' => ['whos_attending', 'attendees'],
            'notifications' => ['notifications'],
            'whats_new' => ['whats_new'],
            'account' => ['account'],
            'auth' => ['auth'],
            'user_management' => ['user_management'],
            'moderation' => ['moderation_queue'],
            'organizers' => ['organizers'],
            'claim' => ['claim_listing'],
        ];

        $map = [];
        foreach ($suffixes_by_context as $context => $suffixes) {
            $tags = [];
            foreach ($prefixes as $prefix) {
                foreach ((array) $suffixes as $suffix) {
                    $tag = sanitize_key((string) $prefix . '_' . $suffix);
                    if ($tag !== '') {
                        $tags[] = $tag;
                    }
                }
            }
            $map[$context] = array_values(array_unique($tags));
        }
        return (array) apply_filters('nexdirectory_extension_schema_shortcode_map', $map, $this);
    }


    private function detect_platform_schema_page_context() {
        if (is_post_type_archive(self::POST_TYPE)) {
            return 'archive';
        }

        if (is_tax()) {
            $queried_tax = sanitize_key((string) get_query_var('taxonomy'));
            $supported_taxonomies = [
                self::TAX_TYPE,
                self::TAX_MODE,
                self::TAX_TAG,
                self::TAX_PRICE,
            ];
            $supported_taxonomies = array_merge($supported_taxonomies, array_keys((array) get_option('nexdirectory_custom_taxonomies', [])));
            if (in_array($queried_tax, array_map('sanitize_key', $supported_taxonomies), true)) {
                return 'archive';
            }
        }

        if (is_search()) {
            $post_type = get_query_var('post_type');
            if ($post_type === self::POST_TYPE || (is_array($post_type) && in_array(self::POST_TYPE, $post_type, true))) {
                return 'archive';
            }
        }

        if (!is_singular() || is_singular(self::POST_TYPE)) {
            return '';
        }

        $page = get_queried_object();
        if (!$page instanceof WP_Post) {
            return '';
        }

        $content = $this->normalize_legacy_shortcode_content((string) $page->post_content);
        if ($content !== '') {
            $shortcode_map = $this->get_platform_schema_shortcode_map();
            foreach ($shortcode_map as $context => $tags) {
                foreach ((array) $tags as $tag) {
                    if (has_shortcode($content, (string) $tag)) {
                        return (string) $context;
                    }
                }
            }
        }

        $page_id = absint((int) $page->ID);
        if ($page_id > 0) {
            $profile_key = $this->get_site_profile_key();
            $route_context_map = [
                'archive' => ['option' => 'nexdirectory_page_events_url', 'route' => 'events'],
                'map' => ['option' => 'nexdirectory_page_events_map_url', 'route' => 'map'],
                'calendar' => ['option' => 'nexdirectory_page_events_calendar_url', 'route' => 'calendar'],
                'submit' => ['option' => 'nexdirectory_page_submit_event_url', 'route' => 'submit'],
                'my_items' => ['option' => 'nexdirectory_page_my_events_url', 'route' => 'my_items'],
                'attending' => ['option' => 'nexdirectory_page_my_tickets_url', 'route' => 'attending'],
                'attendees' => ['option' => 'nexdirectory_page_whos_attending_url', 'route' => 'attendees'],
                'account' => ['option' => 'nexdirectory_page_account_url', 'route' => 'account'],
                'auth' => ['option' => 'nexdirectory_page_login_signup_url', 'route' => 'auth'],
                'claim' => ['option' => 'nexdirectory_claim_listing_page_url', 'route' => 'claim'],
            ];
            $route_context_map = (array) apply_filters(
                'nexdirectory_extension_frontend_route_context_map',
                $route_context_map,
                $profile_key,
                $this
            );
            foreach ($route_context_map as $context => $route_definition) {
                $route_option = (string) ($route_definition['option'] ?? '');
                $route_key = sanitize_key((string) ($route_definition['route'] ?? ''));
                if ($route_option === '' || $route_key === '') {
                    continue;
                }
                $route_url = $this->get_platform_page_url(
                    $route_option,
                    $this->get_profile_route_slugs($route_key, $profile_key),
                    $this->get_profile_route_default_url($route_key, $profile_key)
                );
                $route_page_id = absint((int) url_to_postid((string) $route_url));
                if ($route_page_id > 0 && $route_page_id === $page_id) {
                    return (string) $context;
                }
            }
        }

        $slug = nexdirectory_sanitize_title((string) $page->post_name);
        $slug_map = [
            'whats-new' => 'whats_new',
            'notifications' => 'notifications',
            'user-management' => 'user_management',
            'moderation-queue' => 'moderation',
            'organizers' => 'organizers',
        ];
        $slug_map = (array) apply_filters('nexdirectory_extension_frontend_slug_context_map', $slug_map, $this);
        return isset($slug_map[$slug]) ? (string) $slug_map[$slug] : '';
    }


    private function build_platform_page_item_list_schema($context, array $ui_labels, $limit = 10) {
        $context = sanitize_key((string) $context);
        $limit = max(1, min(30, (int) $limit));
        if ($context === '') {
            return [];
        }

        $query = null;
        if ($context === 'whats_new') {
            $query = new WP_Query([
                'post_type' => self::POST_TYPE,
                'post_status' => 'publish',
                'posts_per_page' => $limit,
                'orderby' => 'date',
                'order' => 'DESC',
                'ignore_sticky_posts' => true,
                'no_found_rows' => true,
            ]);
        } else {
            $profile_key = sanitize_key((string) ($ui_labels['profile_key'] ?? 'events'));
            $is_events_profile = $profile_key === 'events';
            $is_job_profile = $profile_key === 'job_board';
            $filters = [
                's' => '',
                'event_type' => '',
                'event_mode' => '',
                'event_price' => '',
                'event_tag' => '',
                'city' => '',
                'date_from' => '',
                'official' => '',
            ];
            if ($is_job_profile) {
                $filters['event_price'] = '';
            }
            if (!$is_events_profile) {
                $filters['official'] = '';
            }
            $query = $this->build_archive_query($filters, $limit, 1, 0);
        }

        if (!$query instanceof WP_Query || empty($query->posts)) {
            return [];
        }

        $elements = [];
        $position = 1;
        foreach ((array) $query->posts as $post_item) {
            $post_id = is_object($post_item) ? absint((int) ($post_item->ID ?? 0)) : absint((int) $post_item);
            if ($post_id <= 0 || get_post_status($post_id) !== 'publish') {
                continue;
            }
            $item_url = get_permalink($post_id);
            if (!is_string($item_url) || $item_url === '') {
                continue;
            }
            $item = [
                '@type' => 'Thing',
                '@id' => esc_url_raw($item_url),
                'name' => $this->normalize_schema_text(get_the_title($post_id), 140),
            ];
            $item_profile_key = $this->resolve_profile_key_for_post($post_id);
            if ($item_profile_key === 'events') {
                $item['@type'] = 'Event';
                $start_raw = (string) get_post_meta($post_id, 'nexdirectory_start', true);
                $start_ts = $start_raw !== '' ? strtotime($start_raw) : false;
                if ($start_ts) {
                    $item['startDate'] = gmdate('c', (int) $start_ts);
                }
            } elseif ($item_profile_key === 'job_board') {
                $item['@type'] = 'JobPosting';
                $posted_raw = (string) get_post_field('post_date', $post_id);
                $posted_ts = $posted_raw !== '' ? strtotime($posted_raw) : false;
                if ($posted_ts) {
                    $item['datePosted'] = gmdate('c', (int) $posted_ts);
                }
            } elseif ($item_profile_key === 'directory') {
                $item['@type'] = 'LocalBusiness';
            } elseif ($item_profile_key === 'marketplace') {
                $item['@type'] = 'Product';
            }

            $elements[] = [
                '@type' => 'ListItem',
                'position' => $position,
                'item' => $item,
            ];
            $position++;
        }
        wp_reset_postdata();

        if (empty($elements)) {
            return [];
        }

        return [
            '@type' => 'ItemList',
            'itemListOrder' => 'https://schema.org/ItemListOrderAscending',
            'numberOfItems' => count($elements),
            'itemListElement' => $elements,
        ];
    }


    private function build_platform_page_schema($context, array $ui_labels = []) {
        $context = sanitize_key((string) $context);
        if ($context === '') {
            return [];
        }
        $page_id = absint((int) get_queried_object_id());
        $url = $page_id > 0 ? get_permalink($page_id) : $this->current_url();
        if (!is_string($url) || $url === '') {
            $url = $this->current_url();
        }
        $url = esc_url_raw((string) $url);
        if ($url === '') {
            return [];
        }

        $title = $page_id > 0 ? $this->normalize_schema_text(get_the_title($page_id), 140) : '';
        if ($title === '') {
            $title = $this->normalize_schema_text((string) wp_get_document_title(), 140);
        }
        if ($title === '') {
            return [];
        }

        $description = '';
        if ($page_id > 0) {
            $excerpt = (string) get_post_field('post_excerpt', $page_id);
            $content = $this->normalize_legacy_shortcode_content((string) get_post_field('post_content', $page_id));
            $content_text = trim((string) wp_strip_all_tags(strip_shortcodes($content)));
            if ($excerpt !== '') {
                $description = $this->normalize_schema_text($excerpt, 340);
            } elseif ($content_text !== '') {
                $description = $this->normalize_schema_text($content_text, 340);
            }
        }
        if ($description === '') {
            $description_map = [
                'archive' => (string) ($ui_labels['archive_description'] ?? ''),
                'map' => (string) ($ui_labels['map_description'] ?? ''),
                'calendar' => (string) ($ui_labels['calendar_description'] ?? ''),
                'submit' => (string) ($ui_labels['submit_add_description'] ?? ''),
                'my_items' => (string) ($ui_labels['my_items_description'] ?? ''),
                'attending' => (string) ($ui_labels['attending_description'] ?? ''),
                'account' => (string) ($ui_labels['account_description'] ?? ''),
                'organizers' => (string) ($ui_labels['organizers_description'] ?? ''),
                'whats_new' => (string) ($ui_labels['whats_new_description'] ?? ''),
                'auth' => __('Login or create an account to access listing actions.', 'nexdirectory'),
                'claim' => __('Claim this listing if you are the legitimate owner.', 'nexdirectory'),
                'moderation' => __('Review pending listings and apply moderation actions.', 'nexdirectory'),
                'user_management' => __('Manage user access and listing permissions.', 'nexdirectory'),
                'notifications' => __('Review your platform notifications and updates.', 'nexdirectory'),
                'attendees' => __('Review users who saved or RSVPed this listing.', 'nexdirectory'),
            ];
            $description_map = (array) apply_filters(
                'nexdirectory_extension_schema_description_map',
                $description_map,
                $ui_labels,
                $this
            );
            $description = $this->normalize_schema_text((string) ($description_map[$context] ?? ''), 340);
        }

        $collection_contexts = ['archive', 'map', 'calendar', 'whats_new'];
        $schema_type = in_array($context, $collection_contexts, true) ? 'CollectionPage' : 'WebPage';
        if ($context === 'account') {
            $schema_type = 'ProfilePage';
        }

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => $schema_type,
            'name' => $title,
            'url' => $url,
            'inLanguage' => str_replace('_', '-', get_locale()),
            'isPartOf' => [
                '@type' => 'WebSite',
                'name' => sanitize_text_field((string) $this->get_white_label_brand_name()),
                'url' => esc_url_raw(home_url('/')),
            ],
        ];
        if ($description !== '') {
            $schema['description'] = $description;
        }

        if (in_array($context, $collection_contexts, true)) {
            $item_list = $this->build_platform_page_item_list_schema($context, $ui_labels, 12);
            if (!empty($item_list)) {
                $schema['mainEntity'] = $item_list;
            }
        }

        if (in_array($context, ['archive', 'map', 'calendar'], true)) {
            $schema['potentialAction'] = [
                '@type' => 'SearchAction',
                'target' => esc_url_raw((string) nexdirectory_add_query_arg(
                    $context === 'archive' ? 'nexdirectory_search' : 's',
                    '{search_term_string}',
                    $url
                )),
                'query-input' => 'required name=search_term_string',
            ];
        } elseif ($context === 'submit') {
            $schema['potentialAction'] = [
                '@type' => 'CreateAction',
                'name' => sprintf(
                    /* translators: %s: singular label */
                    __('Submit %s', 'nexdirectory'),
                    sanitize_text_field((string) ($ui_labels['singular'] ?? __('Listing', 'nexdirectory')))
                ),
                'target' => $url,
            ];
        } elseif ($context === 'auth') {
            $schema['potentialAction'] = [
                '@type' => 'LoginAction',
                'target' => $url,
            ];
        }

        return $schema;
    }


    private function build_platform_breadcrumb_schema($current_label = '') {
        $is_listing = is_singular(self::POST_TYPE);
        $page_context = $is_listing ? '' : $this->detect_platform_schema_page_context();
        $is_page_context = !$is_listing && $page_context !== '';
        if (!$is_listing && !$is_page_context) {
            return [];
        }

        $items = [];
        $items[] = [
            'label' => __('Home', 'nexdirectory'),
            'url' => esc_url_raw(home_url('/')),
        ];

        $current_label = $this->normalize_schema_text($current_label, 140);
        if ($is_listing) {
            $post_id = absint((int) get_queried_object_id());
            if ($post_id <= 0) {
                return [];
            }
            $profile_key = $this->resolve_profile_key_for_post($post_id);
            $ui_labels = $this->get_site_profile_ui_labels($profile_key);
            $plural_label = sanitize_text_field((string) ($ui_labels['plural'] ?? __('Listings', 'nexdirectory')));
            $items[] = [
                'label' => $this->normalize_schema_text($plural_label, 100),
                'url' => $this->get_platform_page_url(
                    'nexdirectory_page_events_url',
                    $this->get_profile_route_slugs('events', $profile_key),
                    $this->get_profile_route_default_url('events', $profile_key)
                ),
            ];
            $listing_title = $current_label !== '' ? $current_label : $this->normalize_schema_text(get_the_title($post_id), 140);
            if ($listing_title !== '') {
                $items[] = [
                    'label' => $listing_title,
                    'url' => get_permalink($post_id),
                ];
            }
        } else {
            $page_id = absint((int) get_queried_object_id());
            if ($page_id > 0 && is_singular()) {
                $ancestors = array_reverse((array) get_post_ancestors($page_id));
                foreach ($ancestors as $ancestor_id) {
                    $ancestor_id = absint((int) $ancestor_id);
                    if ($ancestor_id <= 0) {
                        continue;
                    }
                    $ancestor_label = $this->normalize_schema_text(get_the_title($ancestor_id), 120);
                    $ancestor_url = get_permalink($ancestor_id);
                    if ($ancestor_label === '' || !is_string($ancestor_url) || $ancestor_url === '') {
                        continue;
                    }
                    $items[] = [
                        'label' => $ancestor_label,
                        'url' => esc_url_raw((string) $ancestor_url),
                    ];
                }
                $page_label = $current_label !== '' ? $current_label : $this->normalize_schema_text(get_the_title($page_id), 140);
                if ($page_label !== '') {
                    $items[] = [
                        'label' => $page_label,
                        'url' => get_permalink($page_id),
                    ];
                }
            } else {
                $archive_title = $current_label !== '' ? $current_label : $this->normalize_schema_text((string) wp_get_document_title(), 140);
                if ($archive_title !== '') {
                    $items[] = [
                        'label' => $archive_title,
                        'url' => esc_url_raw((string) $this->current_url()),
                    ];
                }
            }
        }

        if (count($items) < 2) {
            return [];
        }

        $crumbs = [];
        $position = 1;
        foreach ($items as $item) {
            $label = $this->normalize_schema_text((string) ($item['label'] ?? ''), 140);
            $item_url = esc_url_raw((string) ($item['url'] ?? ''));
            if ($label === '') {
                continue;
            }
            if ($item_url === '') {
                $item_url = esc_url_raw((string) $this->current_url());
            }
            $crumbs[] = [
                '@type' => 'ListItem',
                'position' => $position,
                'item' => [
                    '@id' => $item_url,
                    'name' => $label,
                ],
            ];
            $position++;
        }

        if (count($crumbs) < 2) {
            return [];
        }

        return [
            '@context' => 'https://schema.org',
            '@type' => 'BreadcrumbList',
            'itemListElement' => $crumbs,
        ];
    }


    private function build_directory_listing_schema_payload($post_id, array $ui_labels = []) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return [];
        }

        $title = $this->normalize_schema_text(get_the_title($post_id), 180);
        $description = $this->normalize_schema_text((string) (get_post_field('post_excerpt', $post_id) ?: get_post_field('post_content', $post_id)), 2400);
        $permalink = esc_url_raw((string) get_permalink($post_id));
        if ($title === '' || $permalink === '') {
            return [];
        }

        $business_type = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_directory_business_type', true));
        $schema_type = 'LocalBusiness';
        if (in_array($business_type, ['professional-service', 'professional_service', 'service', 'consultancy', 'agency'], true)) {
            $schema_type = 'ProfessionalService';
        } elseif (in_array($business_type, ['restaurant', 'hospitality', 'food'], true)) {
            $schema_type = 'Restaurant';
        } elseif (in_array($business_type, ['health', 'clinic', 'wellness'], true)) {
            $schema_type = 'MedicalBusiness';
        }

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => $schema_type,
            'name' => $title,
            'url' => $permalink,
            'mainEntityOfPage' => $permalink,
        ];
        if ($description !== '') {
            $schema['description'] = $description;
        }

        $images = [];
        $cover = $this->get_event_cover_image_url($post_id, 'full', 'hero');
        if ($cover !== '') {
            $images[] = esc_url_raw((string) $cover);
        }
        foreach ((array) $this->get_event_gallery_image_urls($post_id, 'full') as $gallery_url) {
            $gallery_url = esc_url_raw((string) $gallery_url);
            if ($gallery_url !== '') {
                $images[] = $gallery_url;
            }
        }
        $images = array_values(array_unique(array_filter($images)));
        if (!empty($images)) {
            $schema['image'] = $images;
        }

        $phone = sanitize_text_field((string) get_post_meta($post_id, 'nexdirectory_contact_phone', true));
        $email = sanitize_email((string) get_post_meta($post_id, 'nexdirectory_contact_email', true));
        if ($phone !== '') {
            $schema['telephone'] = $phone;
        }
        if (is_email($email)) {
            $schema['email'] = $email;
        }

        $website = esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_external_url', true));
        if ($website === '') {
            $website = esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_directory_appointment_url', true));
        }
        if ($website !== '') {
            $schema['sameAs'] = [$website];
        }

        $address = trim((string) get_post_meta($post_id, 'nexdirectory_address', true));
        $city = trim((string) get_post_meta($post_id, 'nexdirectory_city', true));
        $country = trim((string) get_post_meta($post_id, 'nexdirectory_country', true));
        if ($address !== '' || $city !== '' || $country !== '') {
            $schema['address'] = [
                '@type' => 'PostalAddress',
                'streetAddress' => $this->normalize_schema_text($address, 180),
                'addressLocality' => $this->normalize_schema_text($city, 100),
                'addressCountry' => $this->normalize_schema_text($country, 100),
            ];
        }
        $lat = get_post_meta($post_id, 'nexdirectory_lat', true);
        $lng = get_post_meta($post_id, 'nexdirectory_lng', true);
        if (is_numeric($lat) && is_numeric($lng)) {
            $schema['geo'] = [
                '@type' => 'GeoCoordinates',
                'latitude' => (float) $lat,
                'longitude' => (float) $lng,
            ];
        }

        $service_area = $this->normalize_schema_text((string) get_post_meta($post_id, 'nexdirectory_directory_service_area', true), 180);
        if ($service_area !== '') {
            $schema['areaServed'] = $service_area;
        }
        $hours = $this->normalize_schema_text((string) get_post_meta($post_id, 'nexdirectory_directory_operating_hours', true), 220);
        if ($hours !== '') {
            $schema['openingHours'] = $hours;
        }
        $pricing = $this->normalize_schema_text((string) get_post_meta($post_id, 'nexdirectory_directory_pricing_summary', true), 120);
        if ($pricing !== '') {
            $schema['priceRange'] = $pricing;
        }
        $established_year = absint((int) get_post_meta($post_id, 'nexdirectory_directory_established_year', true));
        $current_year = (int) gmdate('Y') + 1;
        if ($established_year >= 1800 && $established_year <= $current_year) {
            $schema['foundingDate'] = sprintf('%04d-01-01', $established_year);
        }

        $contact_person = $this->normalize_schema_text((string) get_post_meta($post_id, 'nexdirectory_directory_contact_person', true), 120);
        if ($contact_person !== '') {
            $schema['contactPoint'] = [
                '@type' => 'ContactPoint',
                'name' => $contact_person,
                'contactType' => 'customer support',
            ];
            if (is_email($email)) {
                $schema['contactPoint']['email'] = $email;
            }
            if ($phone !== '') {
                $schema['contactPoint']['telephone'] = $phone;
            }
        }

        $type_terms = wp_get_post_terms($post_id, self::TAX_TYPE, ['fields' => 'names']);
        if (!is_wp_error($type_terms) && !empty($type_terms[0])) {
            $schema['category'] = $this->normalize_schema_text((string) $type_terms[0], 100);
        }
        $tag_terms = wp_get_post_terms($post_id, self::TAX_TAG, ['fields' => 'names']);
        if (is_wp_error($tag_terms)) {
            $tag_terms = [];
        }
        $services = $this->parse_delimited_values((string) get_post_meta($post_id, 'nexdirectory_directory_services', true));
        $keywords = [];
        foreach (array_merge((array) $tag_terms, (array) $services) as $keyword) {
            $keyword = $this->normalize_schema_text((string) $keyword, 80);
            if ($keyword !== '') {
                $keywords[] = $keyword;
            }
        }
        $keywords = array_values(array_unique($keywords));
        if (!empty($keywords)) {
            $schema['keywords'] = implode(', ', $keywords);
        }

        $document_schema_items = $this->build_supporting_document_schema_items($post_id, 8);
        if (!empty($document_schema_items)) {
            $schema['subjectOf'] = $document_schema_items;
        }

        return $schema;
    }


    private function map_market_condition_schema_url($condition) {
        $condition = strtolower(trim((string) $condition));
        if ($condition === '') {
            return '';
        }
        if (strpos($condition, 'new') !== false) {
            return 'https://schema.org/NewCondition';
        }
        if (strpos($condition, 'refurb') !== false) {
            return 'https://schema.org/RefurbishedCondition';
        }
        if (strpos($condition, 'used') !== false || strpos($condition, 'pre-owned') !== false || strpos($condition, 'preowned') !== false) {
            return 'https://schema.org/UsedCondition';
        }
        return '';
    }


    private function map_market_availability_schema_url($status) {
        $status = sanitize_key((string) $status);
        if (in_array($status, ['sold', 'sold_out', 'out_of_stock'], true)) {
            return 'https://schema.org/OutOfStock';
        }
        if (in_array($status, ['reserved', 'hold'], true)) {
            return 'https://schema.org/LimitedAvailability';
        }
        if (in_array($status, ['expired', 'inactive', 'removed'], true)) {
            return 'https://schema.org/Discontinued';
        }
        if (in_array($status, ['pending', 'draft'], true)) {
            return 'https://schema.org/PreOrder';
        }
        return 'https://schema.org/InStock';
    }


    private function build_marketplace_listing_schema_payload($post_id, array $ui_labels = []) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return [];
        }

        $title = $this->normalize_schema_text(get_the_title($post_id), 180);
        $description = $this->normalize_schema_text((string) (get_post_field('post_excerpt', $post_id) ?: get_post_field('post_content', $post_id)), 2400);
        $permalink = esc_url_raw((string) get_permalink($post_id));
        if ($title === '' || $permalink === '') {
            return [];
        }

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'Product',
            'name' => $title,
            'url' => $permalink,
            'mainEntityOfPage' => $permalink,
        ];
        if ($description !== '') {
            $schema['description'] = $description;
        }

        $images = [];
        $cover = $this->get_event_cover_image_url($post_id, 'full', 'hero');
        if ($cover !== '') {
            $images[] = esc_url_raw((string) $cover);
        }
        foreach ((array) $this->get_event_gallery_image_urls($post_id, 'full') as $gallery_url) {
            $gallery_url = esc_url_raw((string) $gallery_url);
            if ($gallery_url !== '') {
                $images[] = $gallery_url;
            }
        }
        $images = array_values(array_unique(array_filter($images)));
        if (!empty($images)) {
            $schema['image'] = $images;
        }

        $sku = $this->normalize_schema_text((string) get_post_meta($post_id, 'nexdirectory_market_sku', true), 80);
        if ($sku !== '') {
            $schema['sku'] = $sku;
        }
        $brand = $this->normalize_schema_text((string) get_post_meta($post_id, 'nexdirectory_market_brand', true), 100);
        if ($brand !== '') {
            $schema['brand'] = [
                '@type' => 'Brand',
                'name' => $brand,
            ];
        }
        $model = $this->normalize_schema_text((string) get_post_meta($post_id, 'nexdirectory_market_model', true), 100);
        if ($model !== '') {
            $schema['model'] = $model;
        }
        $year = absint((int) get_post_meta($post_id, 'nexdirectory_market_year', true));
        if ($year >= 1900 && $year <= ((int) gmdate('Y') + 2)) {
            $schema['releaseDate'] = sprintf('%04d-01-01', $year);
        }

        $type_terms = wp_get_post_terms($post_id, self::TAX_TYPE, ['fields' => 'names']);
        if (!is_wp_error($type_terms) && !empty($type_terms[0])) {
            $schema['category'] = $this->normalize_schema_text((string) $type_terms[0], 120);
        }
        $tag_terms = wp_get_post_terms($post_id, self::TAX_TAG, ['fields' => 'names']);
        if (is_wp_error($tag_terms)) {
            $tag_terms = [];
        }
        $keyword_values = [];
        foreach ((array) $tag_terms as $term_name) {
            $term_name = $this->normalize_schema_text((string) $term_name, 80);
            if ($term_name !== '') {
                $keyword_values[] = $term_name;
            }
        }
        $keyword_values = array_values(array_unique($keyword_values));
        if (!empty($keyword_values)) {
            $schema['keywords'] = implode(', ', $keyword_values);
        }

        $external_url = esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_external_url', true));
        $listing_status = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_market_listing_status', true));
        $condition = $this->normalize_schema_text((string) get_post_meta($post_id, 'nexdirectory_market_condition', true), 60);
        $condition_url = $this->map_market_condition_schema_url($condition);
        $price_raw = trim((string) get_post_meta($post_id, 'nexdirectory_price', true));
        $quantity = absint((int) get_post_meta($post_id, 'nexdirectory_market_quantity', true));
        $expires_raw = trim((string) get_post_meta($post_id, 'nexdirectory_market_expires_on', true));
        $expires_ts = $expires_raw !== '' ? strtotime($expires_raw . ' 23:59:00') : false;

        $currency = 'USD';
        if (stripos($price_raw, '£') !== false || stripos($price_raw, 'gbp') !== false) {
            $currency = 'GBP';
        } elseif (stripos($price_raw, '€') !== false || stripos($price_raw, 'eur') !== false) {
            $currency = 'EUR';
        } elseif (stripos($price_raw, 'cad') !== false) {
            $currency = 'CAD';
        } elseif (stripos($price_raw, 'aud') !== false) {
            $currency = 'AUD';
        }
        $price_value = null;
        if ($price_raw !== '' && preg_match('/\d+(?:\.\d+)?/', $price_raw, $price_match)) {
            $price_value = (float) $price_match[0];
        }

        $author_id = (int) get_post_field('post_author', $post_id);
        $author = $author_id > 0 ? get_user_by('id', $author_id) : null;
        $seller_name = $author instanceof WP_User ? $this->normalize_schema_text((string) $author->display_name, 120) : '';
        if ($seller_name === '') {
            $seller_name = sanitize_text_field((string) $this->get_white_label_brand_name());
        }
        $seller_type = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_market_seller_type', true));

        $offer = [
            '@type' => 'Offer',
            'url' => $external_url !== '' ? $external_url : $permalink,
            'availability' => $this->map_market_availability_schema_url($listing_status),
            'priceCurrency' => $currency,
            'seller' => [
                '@type' => $seller_type === 'business' ? 'Organization' : 'Person',
                'name' => $seller_name,
            ],
        ];
        if ($price_value !== null) {
            $offer['price'] = $price_value;
        }
        if ($condition_url !== '') {
            $offer['itemCondition'] = $condition_url;
        }
        if ($quantity > 0) {
            $offer['inventoryLevel'] = [
                '@type' => 'QuantitativeValue',
                'value' => $quantity,
            ];
        }
        if ($expires_ts) {
            $offer['priceValidUntil'] = gmdate('Y-m-d', (int) $expires_ts);
        }
        $schema['offers'] = $offer;

        $address = trim((string) get_post_meta($post_id, 'nexdirectory_address', true));
        $city = trim((string) get_post_meta($post_id, 'nexdirectory_city', true));
        $country = trim((string) get_post_meta($post_id, 'nexdirectory_country', true));
        $lat = get_post_meta($post_id, 'nexdirectory_lat', true);
        $lng = get_post_meta($post_id, 'nexdirectory_lng', true);
        if ($address !== '' || $city !== '' || $country !== '' || (is_numeric($lat) && is_numeric($lng))) {
            $location = [
                '@type' => 'Place',
                'name' => $title,
            ];
            if ($address !== '' || $city !== '' || $country !== '') {
                $location['address'] = [
                    '@type' => 'PostalAddress',
                    'streetAddress' => $this->normalize_schema_text($address, 180),
                    'addressLocality' => $this->normalize_schema_text($city, 100),
                    'addressCountry' => $this->normalize_schema_text($country, 100),
                ];
            }
            if (is_numeric($lat) && is_numeric($lng)) {
                $location['geo'] = [
                    '@type' => 'GeoCoordinates',
                    'latitude' => (float) $lat,
                    'longitude' => (float) $lng,
                ];
            }
            $schema['availableAtOrFrom'] = $location;
        }

        $delivery_methods = $this->parse_delimited_values((string) get_post_meta($post_id, 'nexdirectory_market_delivery_methods', true));
        $payment_methods = $this->normalize_schema_text((string) get_post_meta($post_id, 'nexdirectory_market_payment_methods', true), 180);
        $returns_accepted = (string) get_post_meta($post_id, 'nexdirectory_market_returns_accepted', true) === '1';
        $additional = [];
        if (!empty($delivery_methods)) {
            $delivery_clean = [];
            foreach ((array) $delivery_methods as $method) {
                $method = $this->normalize_schema_text((string) $method, 60);
                if ($method !== '') {
                    $delivery_clean[] = $method;
                }
            }
            if (!empty($delivery_clean)) {
                $additional[] = [
                    '@type' => 'PropertyValue',
                    'name' => __('Delivery Methods', 'nexdirectory'),
                    'value' => implode(', ', array_values(array_unique($delivery_clean))),
                ];
            }
        }
        if ($payment_methods !== '') {
            $additional[] = [
                '@type' => 'PropertyValue',
                'name' => __('Payment Methods', 'nexdirectory'),
                'value' => $payment_methods,
            ];
        }
        $additional[] = [
            '@type' => 'PropertyValue',
            'name' => __('Returns Accepted', 'nexdirectory'),
            'value' => $returns_accepted ? __('Yes', 'nexdirectory') : __('No', 'nexdirectory'),
        ];
        if (!empty($additional)) {
            $schema['additionalProperty'] = $additional;
        }

        $document_schema_items = $this->build_supporting_document_schema_items($post_id, 8);
        if (!empty($document_schema_items)) {
            $schema['subjectOf'] = $document_schema_items;
        }

        return $schema;
    }


    private function get_single_event_seo_meta_value($post_id, $meta_key, $max_length = 0) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            return '';
        }
        $value = trim((string) get_post_meta($post_id, (string) $meta_key, true));
        if ($value === '') {
            return '';
        }
        $value = trim((string) wp_strip_all_tags($value));
        if ($value === '') {
            return '';
        }
        $max_length = (int) $max_length;
        if ($max_length > 0) {
            if (function_exists('mb_substr')) {
                $value = mb_substr($value, 0, $max_length);
            } else {
                $value = substr($value, 0, $max_length);
            }
        }
        return trim((string) $value);
    }

    private function get_homepage_seo_option_value($option_key, $max_length = 0) {
        $option_key = sanitize_key((string) $option_key);
        if ($option_key === '') {
            return '';
        }
        $value = trim((string) get_option($option_key, ''));
        if ($value === '') {
            return '';
        }
        $value = trim((string) wp_strip_all_tags($value));
        if ($value === '') {
            return '';
        }
        $max_length = (int) $max_length;
        if ($max_length > 0) {
            if (function_exists('mb_substr')) {
                $value = mb_substr($value, 0, $max_length);
            } else {
                $value = substr($value, 0, $max_length);
            }
        }
        return trim((string) $value);
    }

    public function filter_home_document_title($title) {
        if (is_admin() || !is_front_page()) {
            return $title;
        }
        $custom_title = $this->get_homepage_seo_option_value('nexdirectory_home_meta_title', 60);
        if ($custom_title === '') {
            return $title;
        }
        return $custom_title;
    }

    public function output_homepage_meta_tags() {
        if (is_admin() || wp_doing_ajax() || is_feed() || is_trackback()) {
            return;
        }

        $favicon_url = esc_url_raw((string) get_option('nexdirectory_style_favicon_url', ''));
        if ($favicon_url !== '') {
            echo '<link rel="icon" href="' . esc_url($favicon_url) . '">' . "\n";
            echo '<link rel="shortcut icon" href="' . esc_url($favicon_url) . '">' . "\n";
            echo '<link rel="apple-touch-icon" href="' . esc_url($favicon_url) . '">' . "\n";
        }

        if (!is_front_page()) {
            return;
        }

        $custom_title = $this->get_homepage_seo_option_value('nexdirectory_home_meta_title', 60);
        $custom_description = $this->get_homepage_seo_option_value('nexdirectory_home_meta_description', 158);
        if ($custom_title === '' && $custom_description === '') {
            return;
        }

        $social_title = $custom_title !== '' ? $custom_title : sanitize_text_field((string) $this->get_white_label_brand_name());
        $social_description = $custom_description !== '' ? $custom_description : sanitize_text_field((string) get_bloginfo('description'));
        $canonical_url = esc_url_raw((string) home_url('/'));

        if ($custom_description !== '') {
            echo '<meta name="description" content="' . esc_attr($custom_description) . '">' . "\n";
        }
        if ($social_title !== '') {
            echo '<meta property="og:title" content="' . esc_attr($social_title) . '">' . "\n";
            echo '<meta name="twitter:title" content="' . esc_attr($social_title) . '">' . "\n";
        }
        if ($social_description !== '') {
            echo '<meta property="og:description" content="' . esc_attr($social_description) . '">' . "\n";
            echo '<meta name="twitter:description" content="' . esc_attr($social_description) . '">' . "\n";
        }
        if ($canonical_url !== '') {
            echo '<meta property="og:url" content="' . esc_attr($canonical_url) . '">' . "\n";
        }
    }


    public function filter_single_event_document_title($title) {
        if (is_admin() || !is_singular(self::POST_TYPE)) {
            return $title;
        }
        $post_id = absint((int) get_queried_object_id());
        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            return $title;
        }
        $custom_title = $this->get_single_event_seo_meta_value($post_id, 'nexdirectory_meta_title', 60);
        if ($custom_title === '') {
            return $title;
        }
        return $custom_title;
    }


    public function output_single_event_meta_tags() {
        if (is_admin() || wp_doing_ajax() || is_feed() || is_trackback()) {
            return;
        }
        if (!is_singular(self::POST_TYPE)) {
            return;
        }
        $post_id = absint((int) get_queried_object_id());
        if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE) {
            return;
        }

        $custom_title = $this->get_single_event_seo_meta_value($post_id, 'nexdirectory_meta_title', 60);
        $custom_description = $this->get_single_event_seo_meta_value($post_id, 'nexdirectory_meta_description', 158);
        if ($custom_title === '' && $custom_description === '') {
            return;
        }

        $canonical_url = esc_url_raw((string) get_permalink($post_id));
        $social_title = $custom_title !== '' ? $custom_title : sanitize_text_field((string) get_the_title($post_id));
        $social_description = $custom_description;
        if ($social_description === '') {
            $social_description = trim((string) nexdirectory_strip_all_tags((string) (get_post_field('post_excerpt', $post_id) ?: get_post_field('post_content', $post_id))));
            if (function_exists('mb_substr')) {
                $social_description = mb_substr((string) $social_description, 0, 158);
            } else {
                $social_description = substr((string) $social_description, 0, 158);
            }
            $social_description = trim((string) $social_description);
        }

        if ($custom_description !== '') {
            echo '<meta name="description" content="' . esc_attr($custom_description) . '">' . "\n";
        }
        if ($social_title !== '') {
            echo '<meta property="og:title" content="' . esc_attr($social_title) . '">' . "\n";
            echo '<meta name="twitter:title" content="' . esc_attr($social_title) . '">' . "\n";
        }
        if ($social_description !== '') {
            echo '<meta property="og:description" content="' . esc_attr($social_description) . '">' . "\n";
            echo '<meta name="twitter:description" content="' . esc_attr($social_description) . '">' . "\n";
        }
        if ($canonical_url !== '') {
            echo '<meta property="og:url" content="' . esc_attr($canonical_url) . '">' . "\n";
        }
    }


    public function output_event_schema() {
        if (is_admin() || wp_doing_ajax() || is_feed() || is_trackback() || (function_exists('is_robots') && is_robots())) {
            return;
        }

        $organization_schema = $this->build_global_organization_schema();
        $website_schema = $this->build_global_website_schema();
        $this->emit_json_ld_schema((array) $organization_schema);
        $this->emit_json_ld_schema((array) $website_schema);

        $is_single_listing = is_singular(self::POST_TYPE);
        $page_context = $is_single_listing ? '' : $this->detect_platform_schema_page_context();

        $post_id = 0;
        $ui_labels = [];
        if ($is_single_listing) {
            $post_id = absint((int) get_queried_object_id());
            if ($post_id <= 0 || get_post_type($post_id) !== self::POST_TYPE || get_post_status($post_id) !== 'publish') {
                return;
            }
            $resolved_profile_key = $this->resolve_profile_key_for_post($post_id);
            $ui_labels = $this->get_site_profile_ui_labels($resolved_profile_key);
        } else {
            $ui_labels = $this->get_site_profile_ui_labels();
        }

        $breadcrumb_schema = $this->build_platform_breadcrumb_schema($is_single_listing ? get_the_title($post_id) : '');
        if (empty($breadcrumb_schema)) {
            $breadcrumb_schema = $this->build_generic_breadcrumb_schema($is_single_listing ? get_the_title($post_id) : '');
        }
        $this->emit_json_ld_schema($breadcrumb_schema);

        if (!$is_single_listing) {
            $page_schema = $page_context !== ''
                ? $this->build_platform_page_schema($page_context, (array) $ui_labels)
                : $this->build_generic_page_schema();
            $this->emit_json_ld_schema((array) $page_schema);

            if (is_author()) {
                $author_object = get_queried_object();
                if ($author_object instanceof WP_User) {
                    $this->emit_json_ld_schema($this->build_person_schema_from_user($author_object, (string) $this->current_url()));
                } elseif (is_object($author_object) && isset($author_object->ID)) {
                    $this->emit_json_ld_schema($this->build_person_schema_from_user((int) $author_object->ID, (string) $this->current_url()));
                }
            }
            return;
        }

        $this->emit_json_ld_schema((array) $this->build_generic_page_schema());

        $listing_permalink = esc_url_raw((string) get_permalink($post_id));
        $listing_author_id = (int) get_post_field('post_author', $post_id);
        $listing_author = $listing_author_id > 0 ? get_user_by('id', $listing_author_id) : null;
        $listing_author_schema = $listing_author instanceof WP_User
            ? $this->build_person_schema_from_user($listing_author, $listing_permalink)
            : [];
        $this->emit_json_ld_schema((array) $listing_author_schema);

        $is_job_profile = (string) ($ui_labels['is_job_profile'] ?? '0') === '1';
        $is_directory_profile = (string) ($ui_labels['is_directory_profile'] ?? '0') === '1';
        $is_marketplace_profile = (string) ($ui_labels['is_marketplace_profile'] ?? '0') === '1';
        $is_events_profile = (string) ($ui_labels['is_events_profile'] ?? '1') === '1';
        if ($is_job_profile) {
            $title = get_the_title($post_id);
            $description_raw = (string) (get_post_field('post_excerpt', $post_id) ?: get_post_field('post_content', $post_id));
            $description = nexdirectory_strip_all_tags($description_raw);
            $permalink = get_permalink($post_id);
            $posted_raw = (string) get_post_field('post_date', $post_id);
            $posted_ts = strtotime($posted_raw);
            $posted_c = $posted_ts ? gmdate('c', (int) $posted_ts) : gmdate('c');

            $company_name = trim((string) get_post_meta($post_id, 'nexdirectory_company_name', true));
            $company_website = esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_job_company_website', true));
            if ($company_website === '') {
                $company_website = esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_external_url', true));
            }
            $job_type = trim((string) get_post_meta($post_id, 'nexdirectory_job_type', true));
            $job_work_mode = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_job_work_mode', true));
            $job_salary = trim((string) get_post_meta($post_id, 'nexdirectory_job_salary', true));
            $job_closing_raw = trim((string) get_post_meta($post_id, 'nexdirectory_job_closing_date', true));
            $job_closing_ts = $job_closing_raw !== '' ? strtotime($job_closing_raw . ' 23:59:00') : false;
            $job_apply_url = esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_job_apply_url', true));
            $job_apply_email = sanitize_email((string) get_post_meta($post_id, 'nexdirectory_job_application_email', true));
            $job_department = trim((string) get_post_meta($post_id, 'nexdirectory_job_department', true));
            $job_skills = trim((string) get_post_meta($post_id, 'nexdirectory_job_skills', true));
            $job_industry = trim((string) get_post_meta($post_id, 'nexdirectory_job_industry', true));

            $address = trim((string) get_post_meta($post_id, 'nexdirectory_address', true));
            $city = trim((string) get_post_meta($post_id, 'nexdirectory_city', true));
            $country = trim((string) get_post_meta($post_id, 'nexdirectory_country', true));
            $venue = trim((string) get_post_meta($post_id, 'nexdirectory_venue', true));
            $lat = get_post_meta($post_id, 'nexdirectory_lat', true);
            $lng = get_post_meta($post_id, 'nexdirectory_lng', true);

            $tag_terms = wp_get_post_terms($post_id, self::TAX_TAG, ['fields' => 'names']);
            $type_terms = wp_get_post_terms($post_id, self::TAX_TYPE, ['fields' => 'names']);
            $skills_terms = array_values(array_unique(array_filter(array_map('nexdirectory_sanitize_text', array_merge((array) $tag_terms, (array) $type_terms)))));
            if ($job_skills !== '') {
                $skills_terms[] = nexdirectory_sanitize_text($job_skills);
                $skills_terms = array_values(array_unique(array_filter($skills_terms)));
            }

            $organization_name = $company_name;
            if ($organization_name === '') {
                $author_id = (int) get_post_field('post_author', $post_id);
                $job_owner_profile = $author_id > 0
                    ? $this->get_public_organizer_profile($author_id, true)
                    : [];
                $organization_name = trim((string) ($job_owner_profile['name'] ?? ''));
                if ($organization_name === '') {
                    $organization_name = sanitize_text_field((string) $this->get_white_label_brand_name());
                }
            }

            $schema = [
                '@context' => 'https://schema.org',
                '@type' => 'JobPosting',
                'title' => $title,
                'description' => $description,
                'datePosted' => $posted_c,
                'url' => $permalink,
                'hiringOrganization' => [
                    '@type' => 'Organization',
                    'name' => $organization_name,
                ],
                'identifier' => [
                    '@type' => 'PropertyValue',
                    'name' => sanitize_text_field((string) $this->get_white_label_brand_name()),
                    'value' => (string) $post_id,
                ],
                'directApply' => ($job_apply_url !== '' || is_email($job_apply_email)),
            ];

            if ($company_website !== '') {
                $schema['hiringOrganization']['sameAs'] = $company_website;
            }
            if ($job_type !== '') {
                $schema['employmentType'] = $job_type;
            }
            if ($job_closing_ts) {
                $schema['validThrough'] = gmdate('c', (int) $job_closing_ts);
            }
            if ($job_department !== '') {
                $schema['industry'] = $job_department;
            } elseif ($job_industry !== '') {
                $schema['industry'] = $job_industry;
            }
            if (!empty($skills_terms)) {
                $schema['skills'] = implode(', ', $skills_terms);
            }

            if ($job_work_mode === 'remote') {
                $schema['jobLocationType'] = 'TELECOMMUTE';
                if ($country !== '') {
                    $schema['applicantLocationRequirements'] = [
                        '@type' => 'Country',
                        'name' => $country,
                    ];
                }
            } else {
                if ($job_work_mode === 'hybrid') {
                    $schema['jobLocationType'] = 'HYBRID';
                } elseif ($job_work_mode === 'on-site') {
                    $schema['jobLocationType'] = 'ON_SITE';
                }
                if ($address !== '' || $city !== '' || $country !== '' || $venue !== '') {
                    $location = [
                        '@type' => 'Place',
                        'name' => $venue !== '' ? $venue : $organization_name,
                        'address' => [
                            '@type' => 'PostalAddress',
                            'streetAddress' => $address,
                            'addressLocality' => $city,
                            'addressCountry' => $country,
                        ],
                    ];
                    if (is_numeric($lat) && is_numeric($lng)) {
                        $location['geo'] = [
                            '@type' => 'GeoCoordinates',
                            'latitude' => (float) $lat,
                            'longitude' => (float) $lng,
                        ];
                    }
                    $schema['jobLocation'] = $location;
                }
            }

            if ($job_salary !== '') {
                preg_match_all('/\d+(?:\.\d+)?/', $job_salary, $salary_matches);
                $amounts = isset($salary_matches[0]) ? array_map('floatval', (array) $salary_matches[0]) : [];
                $currency = 'USD';
                if (stripos($job_salary, '£') !== false || stripos($job_salary, 'gbp') !== false) {
                    $currency = 'GBP';
                } elseif (stripos($job_salary, '€') !== false || stripos($job_salary, 'eur') !== false) {
                    $currency = 'EUR';
                } elseif (stripos($job_salary, 'cad') !== false) {
                    $currency = 'CAD';
                } elseif (stripos($job_salary, 'aud') !== false) {
                    $currency = 'AUD';
                }
                if (!empty($amounts)) {
                    $value_payload = [
                        '@type' => 'QuantitativeValue',
                        'currency' => $currency,
                    ];
                    $min_salary = min($amounts);
                    $max_salary = max($amounts);
                    if ($min_salary > 0) {
                        $value_payload['minValue'] = $min_salary;
                    }
                    if ($max_salary > 0) {
                        $value_payload['maxValue'] = $max_salary;
                    }
                    if ($min_salary === $max_salary && $min_salary > 0) {
                        $value_payload['value'] = $min_salary;
                        unset($value_payload['minValue'], $value_payload['maxValue']);
                    }
                    $schema['baseSalary'] = [
                        '@type' => 'MonetaryAmount',
                        'currency' => $currency,
                        'value' => $value_payload,
                    ];
                }
            }

            $this->emit_json_ld_schema($schema);
            return;
        }

        if ($is_directory_profile) {
            $directory_schema = $this->build_directory_listing_schema_payload($post_id, (array) $ui_labels);
            $this->emit_json_ld_schema((array) $directory_schema);
            return;
        }

        if ($is_marketplace_profile) {
            $market_schema = $this->build_marketplace_listing_schema_payload($post_id, (array) $ui_labels);
            $this->emit_json_ld_schema((array) $market_schema);
            return;
        }

        if (!$is_events_profile) {
            return;
        }

        $start_raw = (string) get_post_meta($post_id, 'nexdirectory_start', true);
        $end_raw = (string) get_post_meta($post_id, 'nexdirectory_end', true);
        $timezone = $this->sanitize_timezone_value((string) get_post_meta($post_id, 'nexdirectory_timezone', true));
        if ($timezone === '') {
            $timezone = $this->sanitize_timezone_value(wp_timezone_string());
        }
        if ($timezone === '') {
            $timezone = 'UTC';
        }
        try {
            $event_tz = new DateTimeZone($timezone);
        } catch (Exception $e) {
            $event_tz = new DateTimeZone('UTC');
        }

        $start_dt = null;
        try {
            if ($start_raw !== '') {
                $start_dt = new DateTime($start_raw, $event_tz);
            }
        } catch (Exception $e) {
            $start_dt = null;
        }
        if (!$start_dt instanceof DateTime) {
            $start_unix = strtotime($start_raw);
            if (!$start_unix) {
                return;
            }
            $start_dt = new DateTime('@' . (int) $start_unix);
            $start_dt->setTimezone($event_tz);
        }

        $end_dt = null;
        try {
            if ($end_raw !== '') {
                $end_dt = new DateTime($end_raw, $event_tz);
            }
        } catch (Exception $e) {
            $end_dt = null;
        }
        if (!$end_dt instanceof DateTime || $end_dt <= $start_dt) {
            $end_dt = clone $start_dt;
            $end_dt->modify('+1 hour');
        }

        $start = $start_dt->getTimestamp();
        $end = $end_dt->getTimestamp();
        $address = trim((string) get_post_meta($post_id, 'nexdirectory_address', true));
        $city = trim((string) get_post_meta($post_id, 'nexdirectory_city', true));
        $country = trim((string) get_post_meta($post_id, 'nexdirectory_country', true));
        $venue = trim((string) get_post_meta($post_id, 'nexdirectory_venue', true));
        $lat = get_post_meta($post_id, 'nexdirectory_lat', true);
        $lng = get_post_meta($post_id, 'nexdirectory_lng', true);
        $is_official = $this->get_official_event_flag($post_id);
        $event_price_legacy = trim((string) get_post_meta($post_id, 'nexdirectory_price', true));
        $event_price = $this->get_event_price_from_ticket_meta($post_id, $event_price_legacy);
        $external_url = esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_external_url', true));
        $stream_url = esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_event_stream_url', true));
        $mode_terms = wp_get_post_terms($post_id, self::TAX_MODE, ['fields' => 'names']);
        $tag_terms = wp_get_post_terms($post_id, self::TAX_TAG, ['fields' => 'names']);
        $price_terms = wp_get_post_terms($post_id, self::TAX_PRICE, ['fields' => 'names']);
        $type_terms = wp_get_post_terms($post_id, self::TAX_TYPE, ['fields' => 'names']);
        $video_urls = $this->parse_multiline_urls((string) get_post_meta($post_id, 'nexdirectory_video_urls', true));
        $gallery_urls = $this->get_event_gallery_image_urls($post_id, 'thumbnail');
        $rsvp_count = count($this->get_event_attendees_by_status($post_id, 'yes'));
        $capacity = (int) get_post_meta($post_id, 'nexdirectory_capacity', true);

        $attendance_mode = 'https://schema.org/MixedEventAttendanceMode';
        $mode_blob = strtolower(implode(' ', array_map('strval', (array) $mode_terms)));
        $looks_online = strpos($mode_blob, 'online') !== false || strpos($mode_blob, 'video') !== false || strpos($mode_blob, 'stream') !== false;
        $looks_offline = strpos($mode_blob, 'in-person') !== false || strpos($mode_blob, 'in person') !== false || strpos($mode_blob, 'offline') !== false;
        if ($looks_online && !$looks_offline) {
            $attendance_mode = 'https://schema.org/OnlineEventAttendanceMode';
        } elseif ($looks_offline && !$looks_online) {
            $attendance_mode = 'https://schema.org/OfflineEventAttendanceMode';
        }

        $event_status = (time() > $end)
            ? 'https://schema.org/EventCompleted'
            : 'https://schema.org/EventScheduled';
        $availability = ($capacity > 0 && $rsvp_count >= $capacity)
            ? 'https://schema.org/SoldOut'
            : 'https://schema.org/InStock';
        $schema_organizer_profile = $listing_author_id > 0
            ? $this->get_public_organizer_profile($listing_author_id, true)
            : [];
        $organizer_name = (string) ($schema_organizer_profile['name'] ?? '');
        $organizer_name = $this->normalize_schema_text((string) $organizer_name, 140);
        if ($organizer_name === '') {
            $organizer_name = $this->normalize_schema_text((string) $this->get_white_label_brand_name(), 140);
        }
        $organizer_email = sanitize_email((string) ($schema_organizer_profile['email'] ?? ''));
        $event_name = $this->normalize_schema_text(get_the_title($post_id), 180);
        $event_description = $this->normalize_schema_text(
            (string) (get_post_field('post_excerpt', $post_id) ?: get_post_field('post_content', $post_id)),
            2400
        );
        if ($event_name === '') {
            return;
        }
        $event_url = $listing_permalink;
        if ($event_url === '') {
            $event_url = esc_url_raw((string) get_permalink($post_id));
        }
        if ($event_url === '') {
            return;
        }
        $event_id = $event_url . '#event';

        $organizer_payload = [];
        if (!empty($listing_author_schema['@id'])) {
            $organizer_payload = [
                '@id' => (string) $listing_author_schema['@id'],
            ];
            if ($organizer_name !== '') {
                $organizer_payload['name'] = $organizer_name;
            }
        } else {
            $organizer_payload = [
                '@type' => 'Person',
                'name' => $organizer_name,
            ];
            if (is_email($organizer_email)) {
                $organizer_payload['email'] = $organizer_email;
            }
        }

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'Event',
            '@id' => $event_id,
            'name' => $event_name,
            'description' => $event_description,
            'startDate' => $start_dt->format('c'),
            'endDate' => $end_dt->format('c'),
            'eventStatus' => $event_status,
            'eventAttendanceMode' => $attendance_mode,
            'url' => $event_url,
            'mainEntityOfPage' => $event_url,
            'inLanguage' => str_replace('_', '-', get_locale()),
            'isAccessibleForFree' => false,
            'organizer' => $organizer_payload,
        ];
        if (is_email($organizer_email) && isset($schema['organizer']['@id'])) {
            $schema['organizer']['email'] = $organizer_email;
        }

        $speaker_payloads = $this->build_event_speaker_schema_payloads($post_id, $event_url);
        $speaker_schemas = isset($speaker_payloads['schemas']) && is_array($speaker_payloads['schemas'])
            ? array_values((array) $speaker_payloads['schemas'])
            : [];
        $speaker_refs = isset($speaker_payloads['references']) && is_array($speaker_payloads['references'])
            ? array_values((array) $speaker_payloads['references'])
            : [];
        foreach ($speaker_schemas as $speaker_schema) {
            if (is_array($speaker_schema)) {
                $this->emit_json_ld_schema($speaker_schema);
            }
        }
        if (!empty($speaker_refs)) {
            $schema['performer'] = $speaker_refs;
        }

        if ($is_official) {
            $schema['eventSchedule'] = [
                '@type' => 'Schedule',
                'name' => 'Official Event',
            ];
        }

        $keywords = array_values(array_filter(array_map('nexdirectory_sanitize_text', (array) $tag_terms)));
        $about_terms = array_values(array_filter(array_map('nexdirectory_sanitize_text', array_merge((array) $type_terms, (array) $tag_terms))));
        if (!empty($keywords)) {
            $schema['keywords'] = implode(', ', $keywords);
        }
        if (!empty($about_terms)) {
            $schema['about'] = implode(', ', array_values(array_unique($about_terms)));
        }
        if (!empty($mode_terms)) {
            $schema['eventType'] = nexdirectory_sanitize_text(implode(' / ', array_map('strval', (array) $mode_terms)));
        }

        $sub_events = $this->build_event_sub_event_schema_items($post_id, $event_tz, $start_dt, $end_dt);
        if (!empty($sub_events)) {
            $schema['subEvent'] = $sub_events;
        }

        $image_urls = [];
        $cover_image = $this->get_event_cover_image_url($post_id, 'full', 'hero');
        if ($cover_image !== '') {
            $image_urls[] = $cover_image;
        }
        foreach (array_slice((array) $gallery_urls, 0, 12) as $gallery_url) {
            $gallery_url = esc_url_raw((string) $gallery_url);
            if ($gallery_url !== '') {
                $image_urls[] = $gallery_url;
            }
        }
        $image_urls = array_values(array_unique(array_filter($image_urls)));
        if (!empty($image_urls)) {
            $schema['image'] = $image_urls;
        }
        if ($attendance_mode === 'https://schema.org/OnlineEventAttendanceMode' && $stream_url !== '') {
            $schema['location'] = [
                '@type' => 'VirtualLocation',
                'url' => $stream_url,
            ];
        } elseif ($address !== '' || $city !== '' || $country !== '' || $venue !== '') {
            $location = [
                '@type' => 'Place',
                'name' => $venue !== '' ? $venue : $event_name,
                'address' => [
                    '@type' => 'PostalAddress',
                    'streetAddress' => $this->normalize_schema_text($address, 180),
                    'addressLocality' => $this->normalize_schema_text($city, 100),
                    'addressCountry' => $this->normalize_schema_text($country, 100),
                ],
            ];
            if (is_numeric($lat) && is_numeric($lng)) {
                $location['geo'] = [
                    '@type' => 'GeoCoordinates',
                    'latitude' => (float) $lat,
                    'longitude' => (float) $lng,
                ];
            }
            $schema['location'] = $location;
        }

        $offer_url = $external_url !== '' ? $external_url : get_permalink($post_id);
        $offer = [
            '@type' => 'Offer',
            'url' => $offer_url,
            'availability' => $availability,
        ];
        $currency = 'USD';
        if (stripos($event_price, '£') !== false || stripos($event_price, 'gbp') !== false) {
            $currency = 'GBP';
        } elseif (stripos($event_price, '€') !== false || stripos($event_price, 'eur') !== false) {
            $currency = 'EUR';
        } elseif (stripos($event_price, 'cad') !== false) {
            $currency = 'CAD';
        } elseif (stripos($event_price, 'aud') !== false) {
            $currency = 'AUD';
        }
        $is_free = false;
        if ($event_price !== '') {
            if (preg_match('/\d+(?:\.\d+)?/', $event_price, $match)) {
                $offer['price'] = $match[0];
            }
            if (stripos($event_price, 'free') !== false || in_array('Free', (array) $price_terms, true)) {
                $offer['price'] = '0';
                $is_free = true;
            }
            $offer['priceCurrency'] = $currency;
        } elseif (in_array('Free', (array) $price_terms, true)) {
            $offer['price'] = '0';
            $offer['priceCurrency'] = $currency;
            $is_free = true;
        }
        $schema['isAccessibleForFree'] = $is_free;
        $schema['offers'] = $offer;

        if (!empty($video_urls)) {
            $video_payload = [];
            foreach (array_slice((array) $video_urls, 0, 6) as $index => $video_url) {
                $video_url = esc_url_raw((string) $video_url);
                if ($video_url === '') {
                    continue;
                }
                $video_object = [
                    '@type' => 'VideoObject',
                    /* translators: %d: Placeholder value. */
                    'name' => sprintf(__('Event video %d', 'nexdirectory'), (int) $index + 1),
                    'url' => $video_url,
                ];
                $youtube_id = $this->extract_youtube_id_from_url($video_url);
                if ($youtube_id !== '') {
                    $video_object['embedUrl'] = 'https://www.youtube.com/embed/' . $youtube_id;
                    $video_object['thumbnailUrl'] = 'https://img.youtube.com/vi/' . $youtube_id . '/hqdefault.jpg';
                }
                $video_payload[] = $video_object;
            }
            if (count($video_payload) === 1) {
                $schema['video'] = $video_payload[0];
            } elseif (!empty($video_payload)) {
                $schema['video'] = $video_payload;
            }
        }

        $document_schema_items = $this->build_supporting_document_schema_items($post_id, 8);
        if (!empty($document_schema_items)) {
            $schema['subjectOf'] = $document_schema_items;
        }

        $this->emit_json_ld_schema($schema);
    }


    public function single_template($single) {
        global $post;
        if ($post && $post->post_type === self::POST_TYPE) {
            $template = plugin_dir_path(NEXDIRECTORY_PLUGIN_FILE) . 'templates/single-listing.php';
            if (is_readable($template)) {
                return $template;
            }
        }

        return $single;
    }


    private function extract_youtube_id_from_url($url) {
        $url = trim((string) $url);
        if ($url === '') {
            return '';
        }
        if (preg_match('/^[A-Za-z0-9_-]{11}$/', $url)) {
            return $url;
        }

        $parts = wp_parse_url($url);
        if (!is_array($parts) || empty($parts['host'])) {
            return '';
        }

        $host = strtolower((string) $parts['host']);
        $path = trim((string) ($parts['path'] ?? ''), '/');
        $query = (string) ($parts['query'] ?? '');

        if (strpos($host, 'youtu.be') !== false && $path !== '') {
            $candidate = explode('/', $path)[0];
            return preg_match('/^[A-Za-z0-9_-]{11}$/', $candidate) ? $candidate : '';
        }

        if (strpos($host, 'youtube.com') === false) {
            return '';
        }

        if ($query !== '') {
            parse_str($query, $query_args);
            $candidate = isset($query_args['v']) ? (string) $query_args['v'] : '';
            if (preg_match('/^[A-Za-z0-9_-]{11}$/', $candidate)) {
                return $candidate;
            }
        }

        $segments = $path !== '' ? explode('/', $path) : [];
        if (count($segments) >= 2 && in_array($segments[0], ['embed', 'shorts', 'live', 'v'], true)) {
            $candidate = (string) $segments[1];
            return preg_match('/^[A-Za-z0-9_-]{11}$/', $candidate) ? $candidate : '';
        }

        return '';
    }


    private function build_event_video_cards(array $video_urls, $fallback_image = '') {
        $cards = [];
        $fallback_image = esc_url((string) $fallback_image);
        foreach (array_values($video_urls) as $index => $raw_url) {
            $url = esc_url_raw((string) $raw_url);
            if ($url === '') {
                continue;
            }

            /* translators: %d: Placeholder value. */
            $title = sprintf(__('Video %d', 'nexdirectory'), $index + 1);
            $youtube_id = $this->extract_youtube_id_from_url($url);
            if ($youtube_id !== '') {
                $cards[] = [
                    'title' => $title,
                    'url' => esc_url($url),
                    'thumb' => esc_url('https://img.youtube.com/vi/' . rawurlencode($youtube_id) . '/hqdefault.jpg'),
                    'embed_url' => esc_url('https://www.youtube.com/embed/' . rawurlencode($youtube_id) . '?autoplay=1&rel=0'),
                ];
                continue;
            }

            if ($fallback_image === '') {
                continue;
            }
            $cards[] = [
                'title' => $title,
                'url' => esc_url($url),
                'thumb' => $fallback_image,
                'embed_url' => '',
            ];
        }

        return $cards;
    }


    private function get_supporting_document_embed_url($url) {
        $url = esc_url_raw((string) $url);
        if ($url === '') {
            return '';
        }
        $path = (string) wp_parse_url($url, PHP_URL_PATH);
        $ext = strtolower((string) pathinfo($path, PATHINFO_EXTENSION));
        if ($ext === 'pdf') {
            return esc_url($url);
        }
        return esc_url('https://docs.google.com/gview?embedded=1&url=' . rawurlencode($url));
    }


    private function build_event_supporting_document_cards(array $document_urls) {
        $cards = [];
        foreach (array_values($document_urls) as $index => $raw_url) {
            $url = esc_url_raw((string) $raw_url);
            if ($url === '') {
                continue;
            }
            $path = (string) wp_parse_url($url, PHP_URL_PATH);
            $filename = trim((string) basename($path));
            if ($filename === '') {
                /* translators: %d: Placeholder value. */
                $filename = sprintf(__('Document %d', 'nexdirectory'), $index + 1);
            }
            $extension = strtolower((string) pathinfo($filename, PATHINFO_EXTENSION));
            $label = $filename;
            if (strpos($filename, '.') !== false) {
                $label = (string) preg_replace('/\.[^.]+$/', '', $filename);
            }
            $label = trim((string) str_replace(['-', '_'], ' ', $label));
            if ($label === '') {
                /* translators: %d: Placeholder value. */
                $label = sprintf(__('Document %d', 'nexdirectory'), $index + 1);
            }
            $cards[] = [
                'title' => $label,
                'file_name' => $filename,
                'extension' => $extension !== '' ? strtoupper($extension) : __('FILE', 'nexdirectory'),
                'url' => esc_url($url),
                'embed_url' => $this->get_supporting_document_embed_url($url),
            ];
        }
        return $cards;
    }


    public function append_event_meta_to_single($content) {
        if (!is_singular(self::POST_TYPE) || !in_the_loop() || !is_main_query()) {
            return $content;
        }
        static $is_rendering_event_content = false;
        if ($is_rendering_event_content) {
            return $content;
        }
        $is_rendering_event_content = true;
        try {
            $post_id = get_the_ID();
            $content_html = (string) $content;
            if (trim((string) nexdirectory_strip_all_tags((string) $content_html)) === '') {
                $fallback_post_content = (string) get_post_field('post_content', $post_id);
                if ($fallback_post_content !== '') {
                    if (function_exists('do_blocks')) {
                        $fallback_post_content = (string) do_blocks($fallback_post_content);
                    }
                    $content_html = wpautop($fallback_post_content);
                }
            }
            $top_injected_html = '';
            $bottom_injected_html = '';
            $content_html = (string) preg_replace('/\[\/?nexdirectory_[^\]]*\]/i', '', $content_html);
            if (strpos($content_html, '[') !== false && strpos($content_html, ']') !== false) {
                $content_html = do_shortcode((string) $content_html);
            }
            $content_html = wp_kses_post((string) $content_html);
            $content_html = $this->add_nofollow_to_external_links((string) $content_html);

            $event_title = get_the_title($post_id);
            $excerpt = trim((string) get_the_excerpt($post_id));
            if ($excerpt === '') {
                $excerpt = wp_trim_words(nexdirectory_strip_all_tags((string) get_post_field('post_content', $post_id)), 26);
            }
            
        $start_raw = (string) get_post_meta($post_id, 'nexdirectory_start', true);
        $end_raw = (string) get_post_meta($post_id, 'nexdirectory_end', true);
        $start_ts = $start_raw !== '' ? strtotime($start_raw) : false;
        $end_ts = $end_raw !== '' ? strtotime($end_raw) : false;
        $start_has_time = $this->event_datetime_has_time($start_raw);
        $end_has_time = $this->event_datetime_has_time($end_raw);
        $start_display = $this->format_event_datetime_for_display($start_raw);
        $end_display = $this->format_event_datetime_for_display($end_raw);
        $timezone = (string) get_post_meta($post_id, 'nexdirectory_timezone', true);
        $venue = trim((string) preg_replace('/\s+/u', ' ', (string) get_post_meta($post_id, 'nexdirectory_venue', true)));
        $address = trim((string) preg_replace('/\s+/u', ' ', (string) get_post_meta($post_id, 'nexdirectory_address', true)));
        $city = trim((string) preg_replace('/\s+/u', ' ', (string) get_post_meta($post_id, 'nexdirectory_city', true)));
        $country = trim((string) preg_replace('/\s+/u', ' ', (string) get_post_meta($post_id, 'nexdirectory_country', true)));
        $lat = (string) get_post_meta($post_id, 'nexdirectory_lat', true);
        $lng = (string) get_post_meta($post_id, 'nexdirectory_lng', true);
        $capacity = (string) get_post_meta($post_id, 'nexdirectory_capacity', true);
        $price = (string) get_post_meta($post_id, 'nexdirectory_price', true);
        $price_simple = (string) get_post_meta($post_id, 'nexdirectory_price_simple', true);
        $contact_email = (string) get_post_meta($post_id, 'nexdirectory_contact_email', true);
        $contact_phone = (string) get_post_meta($post_id, 'nexdirectory_contact_phone', true);
        $external_url = (string) get_post_meta($post_id, 'nexdirectory_external_url', true);
        $external_click_metric = 'click_website';
        if ($external_url !== '' && preg_match('/(?:wa\.me|api\.whatsapp\.com|whatsapp)/i', (string) $external_url)) {
            $external_click_metric = 'click_whatsapp';
        }
        $stream_url = (string) get_post_meta($post_id, 'nexdirectory_event_stream_url', true);
        $youtube_one = (string) get_post_meta($post_id, 'nexdirectory_youtube_video_id_1', true);
        $youtube_two = (string) get_post_meta($post_id, 'nexdirectory_youtube_video_id_2', true);
        $recurrence = (string) get_post_meta($post_id, 'nexdirectory_recurrence', true);
        $recurrence_until = (string) get_post_meta($post_id, 'nexdirectory_recurrence_until', true);
        $recurrence_until_ts = $recurrence_until !== '' ? strtotime($recurrence_until) : false;
        $recurrence_until_display = $recurrence_until_ts ? wp_date(get_option('date_format'), $recurrence_until_ts) : $recurrence_until;
        $official = $this->get_official_event_flag($post_id);
        $classification_labels = $this->get_classification_labels();
        $resolved_profile_key = $this->resolve_profile_key_for_post($post_id);
        $ui_labels = $this->get_site_profile_ui_labels($resolved_profile_key);
        $profile_singular_label = sanitize_text_field((string) ($ui_labels['singular'] ?? __('Listing', 'nexdirectory')));
        $profile_singular_lower = function_exists('mb_strtolower')
            ? mb_strtolower($profile_singular_label, 'UTF-8')
            : strtolower($profile_singular_label);
        $is_events_profile = (string) ($ui_labels['is_events_profile'] ?? '1') === '1';
        $is_directory_profile = (string) ($ui_labels['is_directory_profile'] ?? '0') === '1';
        $is_job_profile = (string) ($ui_labels['is_job_profile'] ?? '0') === '1';
        $is_marketplace_profile = (string) ($ui_labels['is_marketplace_profile'] ?? '0') === '1';
        if ($is_events_profile) {
            $price = $this->get_event_price_from_ticket_meta($post_id, $price);
        }
        $single_visit_site_cta = sanitize_text_field((string) ($ui_labels['single_visit_site_cta'] ?? __('Visit Event Site', 'nexdirectory')));
        $single_apply_email_cta = sanitize_text_field((string) ($ui_labels['single_apply_email_cta'] ?? __('Apply by Email', 'nexdirectory')));
        $single_rsvp_join = sanitize_text_field((string) ($ui_labels['single_rsvp_join'] ?? __('RSVP / Join Waitlist', 'nexdirectory')));
        $single_rsvp_cancel = sanitize_text_field((string) ($ui_labels['single_rsvp_cancel'] ?? __('Cancel RSVP', 'nexdirectory')));
        $single_login_rsvp = sanitize_text_field((string) ($ui_labels['single_login_rsvp'] ?? __('Log In To RSVP', 'nexdirectory')));
        $single_owner_items_cta = sanitize_text_field((string) ($ui_labels['single_owner_items_cta'] ?? __('View all organizer events', 'nexdirectory')));
        $single_timeline_heading = sanitize_text_field((string) ($ui_labels['single_timeline_heading'] ?? __('A Day Of Action', 'nexdirectory')));
        $single_overview_heading = sanitize_text_field((string) ($ui_labels['single_overview_heading'] ?? __('Event Overview', 'nexdirectory')));
        $single_gallery_heading = sanitize_text_field((string) ($ui_labels['single_gallery_heading'] ?? __('Event Gallery', 'nexdirectory')));
        $single_videos_heading = sanitize_text_field((string) ($ui_labels['single_videos_heading'] ?? __('Event Videos', 'nexdirectory')));
        $single_cta_heading = sanitize_text_field((string) ($ui_labels['single_cta_heading'] ?? __('Secure Your Spot', 'nexdirectory')));
        $single_cta_copy = sanitize_text_field((string) ($ui_labels['single_cta_copy'] ?? __('Join the community and reserve your place in this event.', 'nexdirectory')));
        $single_report_cta = sanitize_text_field((string) ($ui_labels['single_report_cta'] ?? __('Report Event', 'nexdirectory')));
        $single_report_placeholder = sanitize_text_field((string) ($ui_labels['single_report_placeholder'] ?? __('Tell us what is wrong with this listing.', 'nexdirectory')));
        $single_contact_login = sanitize_text_field((string) ($ui_labels['single_contact_login'] ?? __('Please log in to contact the event holder.', 'nexdirectory')));
        $single_edit_cta = sanitize_text_field((string) ($ui_labels['single_edit_cta'] ?? __('Edit Event', 'nexdirectory')));
        $single_type_label = sanitize_text_field((string) ($ui_labels['type_label'] ?? __('Event Type', 'nexdirectory')));
        $single_mode_label = sanitize_text_field((string) ($ui_labels['mode_label'] ?? __('Mode', 'nexdirectory')));
        $single_price_range_label = sanitize_text_field((string) ($ui_labels['single_price_range_label'] ?? __('Price Range', 'nexdirectory')));
        $single_classification_label = sanitize_text_field((string) ($ui_labels['single_classification_label'] ?? __('Classification', 'nexdirectory')));
        $single_start_label = sanitize_text_field((string) ($ui_labels['single_start_label'] ?? __('Event Start Date', 'nexdirectory')));
        $single_end_label = sanitize_text_field((string) ($ui_labels['single_end_label'] ?? __('Event End Date', 'nexdirectory')));
        $single_timezone_label = sanitize_text_field((string) ($ui_labels['single_timezone_label'] ?? __('Timezone', 'nexdirectory')));
        $single_pattern_label = sanitize_text_field((string) ($ui_labels['single_pattern_label'] ?? __('Recurring Pattern', 'nexdirectory')));
        $single_until_label = sanitize_text_field((string) ($ui_labels['single_until_label'] ?? __('Recurs Until', 'nexdirectory')));
        $single_price_label = sanitize_text_field((string) ($ui_labels['single_price_label'] ?? __('Price', 'nexdirectory')));
        $single_capacity_label = sanitize_text_field((string) ($ui_labels['single_capacity_label'] ?? __('Capacity', 'nexdirectory')));
        $single_tags_label = sanitize_text_field((string) ($ui_labels['single_tags_label'] ?? __('Tags', 'nexdirectory')));
        $single_phone_label = sanitize_text_field((string) ($ui_labels['single_phone_label'] ?? __('Phone Number', 'nexdirectory')));
        $single_contact_email_label = sanitize_text_field((string) ($ui_labels['single_contact_email_label'] ?? __('Contact Email', 'nexdirectory')));
        $single_website_label = sanitize_text_field((string) ($ui_labels['single_website_label'] ?? __('Event Website', 'nexdirectory')));
        $single_video_label = sanitize_text_field((string) ($ui_labels['single_video_label'] ?? __('Video Stream URL', 'nexdirectory')));
        $single_directory_book_cta = sanitize_text_field((string) ($ui_labels['single_directory_book_cta'] ?? __('Book Appointment', 'nexdirectory')));
        $single_directory_whatsapp_cta = sanitize_text_field((string) ($ui_labels['single_directory_whatsapp_cta'] ?? __('Message on WhatsApp', 'nexdirectory')));
        $company_name = trim((string) get_post_meta($post_id, 'nexdirectory_company_name', true));
        $job_type = trim((string) get_post_meta($post_id, 'nexdirectory_job_type', true));
        $job_work_mode = trim((string) get_post_meta($post_id, 'nexdirectory_job_work_mode', true));
        $job_salary = trim((string) get_post_meta($post_id, 'nexdirectory_job_salary', true));
        $job_apply_url = esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_job_apply_url', true));
        $job_application_email = sanitize_email((string) get_post_meta($post_id, 'nexdirectory_job_application_email', true));
        $job_closing_date_raw = trim((string) get_post_meta($post_id, 'nexdirectory_job_closing_date', true));
        $job_closing_ts = $job_closing_date_raw !== '' ? strtotime($job_closing_date_raw . ' 23:59:00') : false;
        $job_closing_display = $job_closing_ts ? wp_date(get_option('date_format'), (int) $job_closing_ts) : '';
        $job_department = trim((string) get_post_meta($post_id, 'nexdirectory_job_department', true));
        $job_experience_level = trim((string) get_post_meta($post_id, 'nexdirectory_job_experience_level', true));
        $job_reporting_to = trim((string) get_post_meta($post_id, 'nexdirectory_job_reporting_to', true));
        $job_remote_policy = trim((string) get_post_meta($post_id, 'nexdirectory_job_remote_policy', true));
        $job_visa_sponsorship = (string) get_post_meta($post_id, 'nexdirectory_job_visa_sponsorship', true) === '1';
        $job_open_positions = absint((int) get_post_meta($post_id, 'nexdirectory_job_open_positions', true));
        $job_interview_process = trim((string) get_post_meta($post_id, 'nexdirectory_job_interview_process', true));
        $job_expected_start_date_raw = trim((string) get_post_meta($post_id, 'nexdirectory_job_expected_start_date', true));
        $job_expected_start_ts = $job_expected_start_date_raw !== '' ? strtotime($job_expected_start_date_raw . ' 00:00:00') : false;
        $job_expected_start_display = $job_expected_start_ts ? wp_date(get_option('date_format'), (int) $job_expected_start_ts) : '';
        $job_skills = trim((string) get_post_meta($post_id, 'nexdirectory_job_skills', true));
        $job_responsibilities = trim((string) get_post_meta($post_id, 'nexdirectory_job_responsibilities', true));
        $job_requirements = trim((string) get_post_meta($post_id, 'nexdirectory_job_requirements', true));
        $job_benefits = trim((string) get_post_meta($post_id, 'nexdirectory_job_benefits', true));
        $job_company_website = esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_job_company_website', true));
        $job_company_size = trim((string) get_post_meta($post_id, 'nexdirectory_job_company_size', true));
        $job_industry = trim((string) get_post_meta($post_id, 'nexdirectory_job_industry', true));
        $directory_business_type = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_directory_business_type', true));
        $directory_listing_status = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_directory_listing_status', true));
        $directory_contact_person = trim((string) get_post_meta($post_id, 'nexdirectory_directory_contact_person', true));
        $directory_service_area = trim((string) get_post_meta($post_id, 'nexdirectory_directory_service_area', true));
        $directory_operating_hours = trim((string) get_post_meta($post_id, 'nexdirectory_directory_operating_hours', true));
        $directory_response_time = trim((string) get_post_meta($post_id, 'nexdirectory_directory_response_time', true));
        $directory_established_year = absint((int) get_post_meta($post_id, 'nexdirectory_directory_established_year', true));
        $directory_pricing_summary = trim((string) get_post_meta($post_id, 'nexdirectory_directory_pricing_summary', true));
        $directory_appointment_url = esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_directory_appointment_url', true));
        $directory_whatsapp_url = esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_directory_whatsapp_url', true));
        $directory_services = trim((string) get_post_meta($post_id, 'nexdirectory_directory_services', true));
        $directory_highlights = trim((string) get_post_meta($post_id, 'nexdirectory_directory_highlights', true));
        $directory_certifications = trim((string) get_post_meta($post_id, 'nexdirectory_directory_certifications', true));
        $market_condition = trim((string) get_post_meta($post_id, 'nexdirectory_market_condition', true));
        $market_price_type = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_market_price_type', true));
        $market_quantity = absint((int) get_post_meta($post_id, 'nexdirectory_market_quantity', true));
        $market_sku = trim((string) get_post_meta($post_id, 'nexdirectory_market_sku', true));
        $market_brand = trim((string) get_post_meta($post_id, 'nexdirectory_market_brand', true));
        $market_model = trim((string) get_post_meta($post_id, 'nexdirectory_market_model', true));
        $market_year = absint((int) get_post_meta($post_id, 'nexdirectory_market_year', true));
        $market_seller_type = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_market_seller_type', true));
        $market_listing_status = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_market_listing_status', true));
        $market_expires_on = trim((string) get_post_meta($post_id, 'nexdirectory_market_expires_on', true));
        $market_expires_ts = $market_expires_on !== '' ? strtotime($market_expires_on . ' 23:59:00') : false;
        $market_expires_display = $market_expires_ts ? wp_date(get_option('date_format'), (int) $market_expires_ts) : '';
        $market_delivery_methods = preg_split('/[\s,]+/', (string) get_post_meta($post_id, 'nexdirectory_market_delivery_methods', true));
        if (!is_array($market_delivery_methods)) {
            $market_delivery_methods = [];
        }
        $market_delivery_methods = array_values(array_unique(array_filter(array_map('sanitize_key', $market_delivery_methods))));
        $market_returns_accepted = (string) get_post_meta($post_id, 'nexdirectory_market_returns_accepted', true) === '1';
        $market_accept_offers = (string) get_post_meta($post_id, 'nexdirectory_market_accept_offers', true) !== '0';
        $market_payment_methods = trim((string) get_post_meta($post_id, 'nexdirectory_market_payment_methods', true));
        $market_condition_notes = trim((string) get_post_meta($post_id, 'nexdirectory_market_condition_notes', true));
        $market_shipping_notes = trim((string) get_post_meta($post_id, 'nexdirectory_market_shipping_notes', true));
        if ($is_job_profile && $contact_email === '' && $job_application_email !== '') {
            $contact_email = $job_application_email;
        }
        if ($is_job_profile) {
            $single_visit_site_cta = __('Apply Now', 'nexdirectory');
            $single_apply_email_cta = __('Apply by Email', 'nexdirectory');
            $single_rsvp_join = __('Save Job', 'nexdirectory');
            $single_rsvp_cancel = __('Remove Saved Job', 'nexdirectory');
            $single_login_rsvp = __('Log In To Save Job', 'nexdirectory');
            $single_owner_items_cta = __('View all poster listing', 'nexdirectory');
            $single_timeline_heading = __('Application Timeline', 'nexdirectory');
            $single_overview_heading = __('Job Overview', 'nexdirectory');
            $single_gallery_heading = __('Company Gallery', 'nexdirectory');
            $single_videos_heading = __('Team & Culture', 'nexdirectory');
            $single_cta_heading = __('Apply For This Job', 'nexdirectory');
            $single_cta_copy = __('Review the role details and submit your application directly to the employer.', 'nexdirectory');
            $single_report_cta = __('Report Job Listing', 'nexdirectory');
            $single_contact_login = __('Please log in to contact the listing owner.', 'nexdirectory');
            $single_edit_cta = __('Edit Job', 'nexdirectory');
            $single_type_label = __('Department', 'nexdirectory');
            $single_mode_label = __('Location Type', 'nexdirectory');
            $single_price_range_label = __('Salary Range', 'nexdirectory');
            $single_classification_label = __('Employer Verification', 'nexdirectory');
            $single_start_label = __('Posted Date', 'nexdirectory');
            $single_end_label = __('Closing Date', 'nexdirectory');
            $single_pattern_label = __('Application Window', 'nexdirectory');
            $single_until_label = __('Apply Before', 'nexdirectory');
            $single_price_label = __('Salary', 'nexdirectory');
            $single_capacity_label = __('Remote Policy', 'nexdirectory');
            $single_tags_label = __('Skills / Keywords', 'nexdirectory');
            $single_phone_label = __('Phone', 'nexdirectory');
            $single_contact_email_label = __('Contact Email', 'nexdirectory');
            $single_website_label = __('Company Website', 'nexdirectory');
            $single_video_label = __('Company Video URL', 'nexdirectory');
            $single_status_label = __('Application Status', 'nexdirectory');
            $single_contact_section_heading = __('Apply', 'nexdirectory');
            $single_documents_heading = __('Job Documents', 'nexdirectory');
            $single_date_meta_label = __('Apply By', 'nexdirectory');
            $single_location_meta_label = __('Workplace', 'nexdirectory');
            $single_location_heading = __('Work Location', 'nexdirectory');
            $single_open_map_cta = __('View Workplace Map', 'nexdirectory');
        } elseif ($is_marketplace_profile) {
            $single_visit_site_cta = __('View Listing', 'nexdirectory');
            $single_owner_items_cta = __('View all seller listings', 'nexdirectory');
            $single_timeline_heading = '';
            $single_overview_heading = __('Overview', 'nexdirectory');
            $single_gallery_heading = __('Gallery', 'nexdirectory');
            $single_videos_heading = __('Videos', 'nexdirectory');
            $single_cta_heading = __('Contact Seller', 'nexdirectory');
            $single_cta_copy = __('Review the listing details and contact the seller directly.', 'nexdirectory');
            $single_report_cta = __('Report Listing', 'nexdirectory');
            $single_contact_login = __('Please log in to contact the seller.', 'nexdirectory');
            $single_edit_cta = __('Edit Listing', 'nexdirectory');
            $single_type_label = __('Listing Category', 'nexdirectory');
            $single_mode_label = __('Delivery', 'nexdirectory');
            $single_price_range_label = __('Price Type', 'nexdirectory');
            $single_classification_label = __('Seller Type', 'nexdirectory');
            $single_start_label = __('Listed Date', 'nexdirectory');
            $single_end_label = __('Expiry Date', 'nexdirectory');
            $single_pattern_label = __('Availability', 'nexdirectory');
            $single_until_label = __('Available Until', 'nexdirectory');
            $single_price_label = __('Asking Price', 'nexdirectory');
            $single_capacity_label = __('Quantity', 'nexdirectory');
            $single_tags_label = __('Keywords', 'nexdirectory');
            $single_phone_label = __('Seller Phone', 'nexdirectory');
            $single_contact_email_label = __('Seller Email', 'nexdirectory');
            $single_website_label = __('External Listing URL', 'nexdirectory');
            $single_video_label = __('Listing Video URL', 'nexdirectory');
            $single_status_label = __('Listing Status', 'nexdirectory');
            $single_contact_section_heading = __('Message Seller', 'nexdirectory');
            $single_documents_heading = __('Documents', 'nexdirectory');
            $single_date_meta_label = __('Listed', 'nexdirectory');
            $single_location_meta_label = __('Location', 'nexdirectory');
            $single_location_heading = __('Location', 'nexdirectory');
            $single_open_map_cta = __('Open Map', 'nexdirectory');
        } elseif ($is_directory_profile) {
            $single_visit_site_cta = __('Visit Website', 'nexdirectory');
            $single_owner_items_cta = __('View all owner listings', 'nexdirectory');
            $single_timeline_heading = '';
            $single_overview_heading = __('Overview', 'nexdirectory');
            $single_gallery_heading = __('Gallery', 'nexdirectory');
            $single_videos_heading = __('Videos', 'nexdirectory');
            $single_cta_heading = __('Contact Owner', 'nexdirectory');
            $single_cta_copy = __('Review this listing and contact the owner for the latest details.', 'nexdirectory');
            $single_report_cta = __('Report Listing', 'nexdirectory');
            $single_contact_login = __('Please log in to contact the listing owner.', 'nexdirectory');
            $single_edit_cta = __('Edit Listing', 'nexdirectory');
            $single_type_label = __('Listing Category', 'nexdirectory');
            $single_mode_label = __('Listing Status', 'nexdirectory');
            $single_price_range_label = __('Pricing', 'nexdirectory');
            $single_classification_label = __('Business Type', 'nexdirectory');
            $single_start_label = __('Listed Date', 'nexdirectory');
            $single_end_label = __('Updated Date', 'nexdirectory');
            $single_pattern_label = __('Availability', 'nexdirectory');
            $single_until_label = __('Available Until', 'nexdirectory');
            $single_price_label = __('Rate / Price', 'nexdirectory');
            $single_capacity_label = __('Service Area', 'nexdirectory');
            $single_tags_label = __('Keywords', 'nexdirectory');
            $single_phone_label = __('Phone', 'nexdirectory');
            $single_contact_email_label = __('Contact Email', 'nexdirectory');
            $single_website_label = __('Website', 'nexdirectory');
            $single_video_label = __('Video URL', 'nexdirectory');
            $single_status_label = __('Listing Status', 'nexdirectory');
            $single_contact_section_heading = __('Contact Owner', 'nexdirectory');
            $single_documents_heading = __('Documents', 'nexdirectory');
            $single_date_meta_label = __('Updated', 'nexdirectory');
            $single_location_meta_label = __('Location', 'nexdirectory');
            $single_location_heading = __('Location', 'nexdirectory');
            $single_open_map_cta = __('Open Map', 'nexdirectory');
        }
        $type_term_objects = wp_get_post_terms($post_id, self::TAX_TYPE, ['fields' => 'all']);
        if (is_wp_error($type_term_objects)) {
            $type_term_objects = [];
        }
        $mode_term_objects = wp_get_post_terms($post_id, self::TAX_MODE, ['fields' => 'all']);
        if (is_wp_error($mode_term_objects)) {
            $mode_term_objects = [];
        }
        $price_term_objects = wp_get_post_terms($post_id, self::TAX_PRICE, ['fields' => 'all']);
        if (is_wp_error($price_term_objects)) {
            $price_term_objects = [];
        }
        $tag_term_objects = wp_get_post_terms($post_id, self::TAX_TAG, ['fields' => 'all']);
        if (is_wp_error($tag_term_objects)) {
            $tag_term_objects = [];
        }
        $type_terms = array_map(static function ($term) {
            return (string) ($term->name ?? '');
        }, (array) $type_term_objects);
        $mode_terms = array_map(static function ($term) {
            return (string) ($term->name ?? '');
        }, (array) $mode_term_objects);
        $price_terms = array_map(static function ($term) {
            return (string) ($term->name ?? '');
        }, (array) $price_term_objects);
        $tag_terms = array_map(static function ($term) {
            return (string) ($term->name ?? '');
        }, (array) $tag_term_objects);
        $type_names = implode(', ', array_filter($type_terms));
        $mode_names = implode(', ', array_filter($mode_terms));
        $price_names = implode(', ', array_filter($price_terms));
        $tag_names = implode(', ', array_filter($tag_terms));
        $custom_form_fields = $this->get_post_custom_field_schema($post_id, true);
        $custom_field_values = $this->get_post_custom_field_values($post_id, $custom_form_fields);
        $custom_fields_display = [];
        foreach ($custom_form_fields as $field) {
            $field = is_array($field) ? $field : [];
            if ((string) ($field['show_in_event'] ?? '1') !== '1') {
                continue;
            }
            $field_id = sanitize_key((string) ($field['id'] ?? ''));
            if ($field_id === '') {
                continue;
            }
            $type = sanitize_key((string) ($field['type'] ?? 'text'));
            $label = sanitize_text_field((string) ($field['label'] ?? $field_id));
            if ($is_job_profile || $is_marketplace_profile || $is_directory_profile) {
                $label_lc = function_exists('mb_strtolower') ? mb_strtolower($label, 'UTF-8') : strtolower($label);
                if (
                    in_array($field_id, ['lead_sponsor', 'primary_partner', 'speaker_notes', 'owner_notes'], true)
                    || strpos($label_lc, 'owner notes') !== false
                    || strpos($label_lc, 'primary partner') !== false
                    || strpos($label_lc, 'event type') !== false
                    || strpos($label_lc, 'price range') !== false
                    || strpos($label_lc, 'classification') !== false
                    || strpos($label_lc, 'event start') !== false
                    || strpos($label_lc, 'event end') !== false
                    || strpos($label_lc, 'video stream') !== false
                    || strpos($label_lc, 'event website') !== false
                    || strpos($label_lc, 'timezone') !== false
                    || strpos($label_lc, 'capacity') !== false
                    || strpos($label_lc, 'a day of action') !== false
                    || strpos($label_lc, 'itinerary') !== false
                ) {
                    continue;
                }
            }
            $raw_value = isset($custom_field_values[$field_id]) ? $custom_field_values[$field_id] : '';
            if ($type === 'checkbox') {
                if ((string) $raw_value !== '1') {
                    continue;
                }
                $custom_fields_display[] = [
                    'label' => $label,
                    'render_type' => 'text',
                    'value' => __('Yes', 'nexdirectory'),
                ];
                continue;
            }
            if ($type === 'gallery') {
                $gallery_items = $this->parse_multiline_urls((string) $raw_value);
                if (empty($gallery_items)) {
                    continue;
                }
                $custom_fields_display[] = [
                    'label' => $label,
                    'render_type' => 'gallery',
                    'gallery' => $gallery_items,
                ];
                continue;
            }
            if ($type === 'image') {
                $image_url = esc_url((string) $raw_value);
                if ($image_url === '') {
                    continue;
                }
                $custom_fields_display[] = [
                    'label' => $label,
                    'render_type' => 'image',
                    'value' => $image_url,
                ];
                continue;
            }
            if ($type === 'file' || $type === 'url' || $type === 'email') {
                $value = trim((string) $raw_value);
                if ($value === '') {
                    continue;
                }
                $url = $type === 'email' ? 'mailto:' . sanitize_email($value) : esc_url_raw($value);
                if ($url === '') {
                    continue;
                }
                $custom_fields_display[] = [
                    'label' => $label,
                    'render_type' => 'link',
                    'value' => $value,
                    'url' => $url,
                ];
                continue;
            }
            if ($type === 'wysiwyg') {
                $value = trim((string) $raw_value);
                if ($value === '') {
                    continue;
                }
                $custom_fields_display[] = [
                    'label' => $label,
                    'render_type' => 'html',
                    'value' => $this->add_nofollow_to_external_links((string) wp_kses_post($value)),
                ];
                continue;
            }
            if ($type === 'date') {
                $value_ts = strtotime((string) $raw_value);
                $value = $value_ts ? wp_date(get_option('date_format'), $value_ts) : sanitize_text_field((string) $raw_value);
                if ($value === '') {
                    continue;
                }
                $custom_fields_display[] = [
                    'label' => $label,
                    'render_type' => 'text',
                    'value' => $value,
                ];
                continue;
            }
            if ($type === 'datetime-local') {
                $value_ts = strtotime((string) $raw_value);
                $value = $value_ts ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), $value_ts) : sanitize_text_field((string) $raw_value);
                if ($value === '') {
                    continue;
                }
                $custom_fields_display[] = [
                    'label' => $label,
                    'render_type' => 'text',
                    'value' => $value,
                ];
                continue;
            }
            $value = trim((string) preg_replace('/\s+/u', ' ', (string) $raw_value));
            if ($value === '') {
                continue;
            }
            $custom_fields_display[] = [
                'label' => $label,
                'render_type' => 'text',
                'value' => $value,
            ];
        }

        $gallery_urls = $this->parse_multiline_urls((string) get_post_meta($post_id, 'nexdirectory_gallery_urls', true));
        $video_urls = $this->parse_multiline_urls((string) get_post_meta($post_id, 'nexdirectory_video_urls', true));
        $supporting_doc_urls = $this->parse_multiline_urls((string) get_post_meta($post_id, 'nexdirectory_supporting_docs', true));
        if ($youtube_one !== '') {
            $video_urls[] = 'https://www.youtube.com/watch?v=' . $youtube_one;
        }
        if ($youtube_two !== '') {
            $video_urls[] = 'https://www.youtube.com/watch?v=' . $youtube_two;
        }
        if ($stream_url !== '') {
            $video_urls[] = $stream_url;
        }
        $video_urls = array_values(array_unique(array_filter($video_urls)));
        $gallery_display_urls = array_values(array_filter(array_map('esc_url', $gallery_urls), static function ($url) {
            return trim((string) $url) !== '';
        }));

        $cover_image = $this->get_event_cover_image_url($post_id, 'full', 'hero');
        if ($cover_image !== '') {
            $normalized_cover = untrailingslashit($cover_image);
            $gallery_display_urls = array_values(array_filter($gallery_display_urls, static function ($url) use ($normalized_cover) {
                return untrailingslashit((string) $url) !== $normalized_cover;
            }));
        }

        $location_bits = array_filter([$venue, $city, $country], static function ($value) {
            return trim((string) $value) !== '';
        });
        $location_line = trim((string) preg_replace('/\s+/u', ' ', implode(', ', $location_bits)));
        $location_full = trim((string) preg_replace('/\s+/u', ' ', implode(', ', array_filter([$venue, $address, $city, $country], static function ($value) {
            return trim((string) $value) !== '';
        }))));
        $map_link = '';
        if (is_numeric($lat) && is_numeric($lng)) {
            $map_link = nexdirectory_add_query_arg([
                'api' => '1',
                'query' => $lat . ',' . $lng,
            ], 'https://www.google.com/maps/search/');
        } elseif ($location_full !== '') {
            $map_link = nexdirectory_add_query_arg([
                'api' => '1',
                'query' => $location_full,
            ], 'https://www.google.com/maps/search/');
        }
        $map_embed_url = '';
        if (is_numeric($lat) && is_numeric($lng)) {
            $map_embed_url = 'https://maps.google.com/maps?q=' . rawurlencode($lat . ',' . $lng) . '&z=14&output=embed';
        } elseif ($location_full !== '') {
            $map_embed_url = 'https://maps.google.com/maps?q=' . rawurlencode($location_full) . '&z=14&output=embed';
        }
        $mode_value = $mode_names !== '' ? $mode_names : ($is_events_profile ? __('In-person Event', 'nexdirectory') : __('Standard', 'nexdirectory'));
        $price_value = $price_simple !== '' ? ucfirst($price_simple) : $price_names;
        if ($is_events_profile && $price !== '') {
            $price_value = $price;
        }
        $events_base_url = $this->get_platform_page_url(
            'nexdirectory_page_events_url',
            $this->get_profile_route_slugs('events', $resolved_profile_key),
            $this->get_profile_route_default_url('events', $resolved_profile_key)
        );
        $overview_chips = [];
        $seen_chip_labels = [];
        $add_overview_chip = static function ($label, $url, $primary = false) use (&$overview_chips, &$seen_chip_labels) {
            $clean_label = trim((string) $label);
            $clean_url = esc_url_raw((string) $url);
            if ($clean_label === '' || $clean_url === '') {
                return;
            }
            $hash = strtolower($clean_label);
            if (isset($seen_chip_labels[$hash])) {
                return;
            }
            $seen_chip_labels[$hash] = true;
            $overview_chips[] = [
                'label' => $clean_label,
                'url' => $clean_url,
                'primary' => $primary ? '1' : '0',
            ];
        };
        if ($is_events_profile) {
            $add_overview_chip(
                $official ? (string) ($classification_labels['official'] ?? __('Official Event', 'nexdirectory')) : (string) ($classification_labels['unofficial'] ?? __('Unofficial Event', 'nexdirectory')),
                nexdirectory_add_query_arg('official', $official ? '1' : '0', $events_base_url),
                true
            );
        }
        foreach ((array) $mode_term_objects as $term_obj) {
            if (!$term_obj instanceof WP_Term) {
                continue;
            }
            $add_overview_chip((string) $term_obj->name, nexdirectory_add_query_arg('event_mode', (string) $term_obj->slug, $events_base_url), false);
            if (count($overview_chips) >= 6) {
                break;
            }
        }
        foreach ((array) $type_term_objects as $term_obj) {
            if (!$term_obj instanceof WP_Term) {
                continue;
            }
            $add_overview_chip((string) $term_obj->name, nexdirectory_add_query_arg('event_type', (string) $term_obj->slug, $events_base_url), false);
            if (count($overview_chips) >= 6) {
                break;
            }
        }
        foreach ((array) $tag_term_objects as $term_obj) {
            if (!$term_obj instanceof WP_Term) {
                continue;
            }
            $add_overview_chip((string) $term_obj->name, nexdirectory_add_query_arg('event_tag', (string) $term_obj->slug, $events_base_url), false);
            if (count($overview_chips) >= 6) {
                break;
            }
        }

        $summary_cards = [];
        $detail_rows = [];
        if ($is_events_profile) {
            $summary_cards = array_filter([
                ['label' => $single_type_label, 'value' => $mode_value, 'compact' => true],
                ['label' => $single_price_range_label, 'value' => $price_value, 'compact' => true],
                ['label' => $single_classification_label, 'value' => $official ? (string) ($classification_labels['official_short'] ?? __('Official', 'nexdirectory')) : (string) ($classification_labels['unofficial_short'] ?? __('Unofficial', 'nexdirectory')), 'compact' => true],
            ], static function ($row) {
                return trim((string) ($row['value'] ?? '')) !== '';
            });
            if ($start_display !== '') {
                $detail_rows[] = ['label' => $single_start_label, 'value' => $start_display, 'compact' => false];
            }
            $same_day_event = $start_ts && (!$end_ts || wp_date('Y-m-d', $start_ts) === wp_date('Y-m-d', $end_ts));
            if ($end_display !== '' && !$same_day_event) {
                $detail_rows[] = ['label' => $single_end_label, 'value' => $end_display, 'compact' => false];
            }
            if ($timezone !== '') {
                $detail_rows[] = ['label' => $single_timezone_label, 'value' => $timezone, 'compact' => true];
            }
            if ($recurrence !== '' && $recurrence !== 'none') {
                $detail_rows[] = ['label' => $single_pattern_label, 'value' => ucfirst(str_replace('-', ' ', $recurrence)), 'compact' => false];
            }
            if ($type_names !== '') {
                $detail_rows[] = ['label' => $single_type_label, 'value' => $type_names, 'compact' => true];
            }
            if ($price !== '') {
                $detail_rows[] = ['label' => $single_price_label, 'value' => $price, 'compact' => true];
            }
            $capacity_int = is_numeric($capacity) ? (int) $capacity : 0;
            if ($capacity_int > 0) {
                $detail_rows[] = ['label' => $single_capacity_label, 'value' => (string) $capacity_int, 'compact' => true];
            }
            if ($tag_names !== '') {
                $detail_rows[] = ['label' => $single_tags_label, 'value' => $tag_names, 'compact' => false];
            }
            if ($recurrence_until_display !== '' && $recurrence !== '' && $recurrence !== 'none') {
                $detail_rows[] = ['label' => $single_until_label, 'value' => $recurrence_until_display, 'compact' => false];
            }
        } elseif ($is_job_profile) {
            $work_mode_display = $job_work_mode !== '' ? ucwords(str_replace('-', ' ', $job_work_mode)) : $mode_names;
            $summary_cards = array_filter([
                ['label' => __('Employment Type', 'nexdirectory'), 'value' => $job_type !== '' ? $job_type : $type_names, 'compact' => true],
                ['label' => __('Department', 'nexdirectory'), 'value' => $job_department !== '' ? $job_department : $type_names, 'compact' => true],
                ['label' => __('Location Type', 'nexdirectory'), 'value' => $work_mode_display, 'compact' => true],
            ], static function ($row) {
                return trim((string) ($row['value'] ?? '')) !== '';
            });
            if ($company_name !== '') {
                $detail_rows[] = ['label' => __('Company', 'nexdirectory'), 'value' => $company_name, 'compact' => true];
            }
            if ($job_experience_level !== '') {
                $detail_rows[] = ['label' => __('Experience Level', 'nexdirectory'), 'value' => $job_experience_level, 'compact' => true];
            }
            if ($job_reporting_to !== '') {
                $detail_rows[] = ['label' => __('Reporting Line', 'nexdirectory'), 'value' => $job_reporting_to, 'compact' => true];
            }
            if ($job_remote_policy !== '') {
                $detail_rows[] = ['label' => __('Remote Policy', 'nexdirectory'), 'value' => $job_remote_policy, 'compact' => true];
            }
            if ($job_salary !== '') {
                $detail_rows[] = ['label' => __('Salary', 'nexdirectory'), 'value' => $job_salary, 'compact' => true];
            }
            if ($job_company_size !== '') {
                $detail_rows[] = ['label' => __('Company Size', 'nexdirectory'), 'value' => $job_company_size, 'compact' => true];
            }
            if ($job_industry !== '') {
                $detail_rows[] = ['label' => __('Industry', 'nexdirectory'), 'value' => $job_industry, 'compact' => true];
            }
            if ($job_closing_display !== '') {
                $detail_rows[] = ['label' => __('Closing Date', 'nexdirectory'), 'value' => $job_closing_display, 'compact' => true];
            }
            if ($job_expected_start_display !== '') {
                $detail_rows[] = ['label' => __('Expected Start Date', 'nexdirectory'), 'value' => $job_expected_start_display, 'compact' => true];
            }
            $posted_ts = strtotime((string) get_post_field('post_date', $post_id));
            if ($posted_ts) {
                $detail_rows[] = ['label' => __('Posted On', 'nexdirectory'), 'value' => wp_date(get_option('date_format'), (int) $posted_ts), 'compact' => true];
            }
            if ($job_application_email !== '') {
                $detail_rows[] = ['label' => __('Application Email', 'nexdirectory'), 'value' => $job_application_email, 'url' => 'mailto:' . sanitize_email($job_application_email), 'compact' => false];
            }
            if ($job_apply_url !== '') {
                $detail_rows[] = ['label' => __('Application URL', 'nexdirectory'), 'value' => $job_apply_url, 'url' => $job_apply_url, 'metric' => 'click_website', 'compact' => false];
            }
            if ($job_company_website !== '' && untrailingslashit($job_company_website) !== untrailingslashit($job_apply_url)) {
                $detail_rows[] = ['label' => __('Company Website', 'nexdirectory'), 'value' => $job_company_website, 'url' => $job_company_website, 'metric' => 'click_website', 'compact' => false];
            }
            $skills_value = $job_skills !== '' ? $job_skills : $tag_names;
            if ($skills_value !== '') {
                $detail_rows[] = ['label' => $single_tags_label, 'value' => $skills_value, 'compact' => false];
            }
            $visa_label = $job_visa_sponsorship ? __('Yes', 'nexdirectory') : __('No', 'nexdirectory');
            $detail_rows[] = ['label' => __('Visa Sponsorship', 'nexdirectory'), 'value' => $visa_label, 'compact' => true];

            if ($job_responsibilities !== '') {
                $custom_fields_display[] = [
                    'label' => __('Responsibilities', 'nexdirectory'),
                    'render_type' => 'html',
                    'value' => $this->add_nofollow_to_external_links((string) wpautop(wp_kses_post($job_responsibilities))),
                ];
            }
            if ($job_requirements !== '') {
                $custom_fields_display[] = [
                    'label' => __('Requirements', 'nexdirectory'),
                    'render_type' => 'html',
                    'value' => $this->add_nofollow_to_external_links((string) wpautop(wp_kses_post($job_requirements))),
                ];
            }
            if ($job_benefits !== '') {
                $custom_fields_display[] = [
                    'label' => __('Benefits', 'nexdirectory'),
                    'render_type' => 'html',
                    'value' => $this->add_nofollow_to_external_links((string) wpautop(wp_kses_post($job_benefits))),
                ];
            }
            if ($job_interview_process !== '') {
                $custom_fields_display[] = [
                    'label' => __('Interview Process', 'nexdirectory'),
                    'render_type' => 'html',
                    'value' => $this->add_nofollow_to_external_links((string) wpautop(wp_kses_post($job_interview_process))),
                ];
            }
        } elseif ($is_directory_profile) {
            $directory_business_type_labels = [
                'independent-business' => __('Independent Business', 'nexdirectory'),
                'agency' => __('Agency', 'nexdirectory'),
                'private-owner' => __('Private Owner', 'nexdirectory'),
                'nonprofit' => __('Nonprofit / Community', 'nexdirectory'),
                'multi-location' => __('Multi-location Brand', 'nexdirectory'),
                'franchise' => __('Franchise', 'nexdirectory'),
            ];
            $directory_status_labels = [
                'active' => __('Active', 'nexdirectory'),
                'temporarily-closed' => __('Temporarily Closed', 'nexdirectory'),
                'permanently-closed' => __('Permanently Closed', 'nexdirectory'),
            ];
            $directory_business_type_display = (string) ($directory_business_type_labels[$directory_business_type] ?? '');
            $directory_status_display = (string) ($directory_status_labels[$directory_listing_status] ?? __('Active', 'nexdirectory'));

            $summary_cards = array_filter([
                ['label' => __('Business Type', 'nexdirectory'), 'value' => $directory_business_type_display, 'compact' => true],
                ['label' => __('Listing Status', 'nexdirectory'), 'value' => $directory_status_display, 'compact' => true],
                ['label' => __('Category', 'nexdirectory'), 'value' => $type_names, 'compact' => true],
            ], static function ($row) {
                return trim((string) ($row['value'] ?? '')) !== '';
            });
            $detail_rows[] = ['label' => __('Listing ID', 'nexdirectory'), 'value' => '#' . (string) $post_id, 'compact' => true];
            $posted_ts = strtotime((string) get_post_field('post_date', $post_id));
            if ($posted_ts) {
                $detail_rows[] = ['label' => __('Listed On', 'nexdirectory'), 'value' => wp_date(get_option('date_format'), (int) $posted_ts), 'compact' => true];
            }
            if ($company_name !== '') {
                $detail_rows[] = ['label' => __('Business Name', 'nexdirectory'), 'value' => $company_name, 'compact' => true];
            }
            if ($directory_contact_person !== '') {
                $detail_rows[] = ['label' => __('Contact Person', 'nexdirectory'), 'value' => $directory_contact_person, 'compact' => true];
            }
            $updated_ts = strtotime((string) get_post_field('post_modified', $post_id));
            if ($updated_ts) {
                $detail_rows[] = ['label' => __('Last Updated', 'nexdirectory'), 'value' => wp_date(get_option('date_format'), (int) $updated_ts), 'compact' => true];
            }
            if ($directory_service_area !== '') {
                $detail_rows[] = ['label' => __('Service Area', 'nexdirectory'), 'value' => $directory_service_area, 'compact' => true];
            }
            if ($directory_operating_hours !== '') {
                $detail_rows[] = ['label' => __('Operating Hours', 'nexdirectory'), 'value' => $directory_operating_hours, 'compact' => true];
            }
            if ($directory_response_time !== '') {
                $detail_rows[] = ['label' => __('Response Time', 'nexdirectory'), 'value' => $directory_response_time, 'compact' => true];
            }
            if ($directory_established_year > 0) {
                $detail_rows[] = ['label' => __('Established', 'nexdirectory'), 'value' => (string) $directory_established_year, 'compact' => true];
            }
            if ($directory_pricing_summary !== '') {
                $detail_rows[] = ['label' => __('Pricing Summary', 'nexdirectory'), 'value' => $directory_pricing_summary, 'compact' => false];
            }
            if ($tag_names !== '') {
                $detail_rows[] = ['label' => $single_tags_label, 'value' => $tag_names, 'compact' => false];
            }
            if ($directory_appointment_url !== '') {
                $detail_rows[] = ['label' => __('Book Appointment', 'nexdirectory'), 'value' => $directory_appointment_url, 'url' => $directory_appointment_url, 'metric' => 'click_website', 'compact' => false];
            }
            if ($directory_whatsapp_url !== '') {
                $detail_rows[] = ['label' => __('WhatsApp', 'nexdirectory'), 'value' => $directory_whatsapp_url, 'url' => $directory_whatsapp_url, 'metric' => 'click_whatsapp', 'compact' => false];
            }

            if ($directory_services !== '') {
                $custom_fields_display[] = [
                    'label' => __('Services Offered', 'nexdirectory'),
                    'render_type' => 'html',
                    'value' => $this->add_nofollow_to_external_links((string) wpautop(wp_kses_post($directory_services))),
                ];
            }
            if ($directory_highlights !== '') {
                $custom_fields_display[] = [
                    'label' => __('Highlights', 'nexdirectory'),
                    'render_type' => 'html',
                    'value' => $this->add_nofollow_to_external_links((string) wpautop(wp_kses_post($directory_highlights))),
                ];
            }
            if ($directory_certifications !== '') {
                $custom_fields_display[] = [
                    'label' => __('Certifications / Credentials', 'nexdirectory'),
                    'render_type' => 'html',
                    'value' => $this->add_nofollow_to_external_links((string) wpautop(wp_kses_post($directory_certifications))),
                ];
            }
        } elseif ($is_marketplace_profile) {
            $market_price_type_labels = [
                'fixed' => __('Fixed Price', 'nexdirectory'),
                'negotiable' => __('Negotiable', 'nexdirectory'),
                'auction' => __('Auction', 'nexdirectory'),
                'free' => __('Free', 'nexdirectory'),
            ];
            $market_status_labels = [
                'active' => __('Available', 'nexdirectory'),
                'reserved' => __('Reserved', 'nexdirectory'),
                'sold' => __('Sold', 'nexdirectory'),
                'expired' => __('Expired', 'nexdirectory'),
            ];
            $market_seller_labels = [
                'private' => __('Private Seller', 'nexdirectory'),
                'business' => __('Business Seller', 'nexdirectory'),
            ];
            $market_delivery_label_map = [
                'pickup' => __('Pickup', 'nexdirectory'),
                'shipping' => __('Shipping', 'nexdirectory'),
                'local-delivery' => __('Local Delivery', 'nexdirectory'),
                'digital' => __('Digital Delivery', 'nexdirectory'),
            ];
            $market_delivery_display = [];
            foreach ((array) $market_delivery_methods as $delivery_key) {
                if (isset($market_delivery_label_map[$delivery_key])) {
                    $market_delivery_display[] = (string) $market_delivery_label_map[$delivery_key];
                }
            }
            $market_price_type_display = (string) ($market_price_type_labels[$market_price_type] ?? '');
            $market_seller_type_display = (string) ($market_seller_labels[$market_seller_type] ?? '');
            $market_status_display = (string) ($market_status_labels[$market_listing_status] ?? __('Available', 'nexdirectory'));
            if ($market_expires_ts && $market_expires_ts < current_time('timestamp')) {
                $market_status_display = __('Expired', 'nexdirectory');
            }
            $summary_cards = array_filter([
                ['label' => __('Condition', 'nexdirectory'), 'value' => $market_condition, 'compact' => true],
                ['label' => __('Price Type', 'nexdirectory'), 'value' => $market_price_type_display, 'compact' => true],
                ['label' => __('Seller Type', 'nexdirectory'), 'value' => $market_seller_type_display, 'compact' => true],
                ['label' => __('Availability Status', 'nexdirectory'), 'value' => $market_status_display, 'compact' => true],
            ], static function ($row) {
                return trim((string) ($row['value'] ?? '')) !== '';
            });
            $posted_ts = strtotime((string) get_post_field('post_date', $post_id));
            $updated_ts = strtotime((string) get_post_field('post_modified', $post_id));
            $listing_views = 0;
            if ($this->analytics_table_exists()) {
                global $wpdb;
                $analytics_table = $this->get_analytics_table_name();
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $listing_views = (int) $wpdb->get_var($wpdb->prepare(
                    "SELECT COALESCE(SUM(value), 0) FROM {$analytics_table} WHERE event_id = %d AND metric = %s",
                    $post_id,
                    'listing_view'
                ));
            }
            $detail_rows[] = ['label' => __('Listing ID', 'nexdirectory'), 'value' => '#' . (string) $post_id, 'compact' => true];
            if ($posted_ts) {
                $detail_rows[] = ['label' => __('Posted On', 'nexdirectory'), 'value' => wp_date(get_option('date_format'), (int) $posted_ts), 'compact' => true];
            }
            if ($updated_ts) {
                $detail_rows[] = ['label' => __('Last Updated', 'nexdirectory'), 'value' => wp_date(get_option('date_format'), (int) $updated_ts), 'compact' => true];
            }
            if ($market_expires_display !== '') {
                $detail_rows[] = ['label' => __('Expires On', 'nexdirectory'), 'value' => $market_expires_display, 'compact' => true];
            }
            $detail_rows[] = ['label' => __('Views', 'nexdirectory'), 'value' => (string) number_format_i18n($listing_views), 'compact' => true];
            $detail_rows[] = ['label' => __('Accepts Offers', 'nexdirectory'), 'value' => $market_accept_offers ? __('Yes', 'nexdirectory') : __('No', 'nexdirectory'), 'compact' => true];
            if ($price !== '') {
                $detail_rows[] = ['label' => __('Asking Price', 'nexdirectory'), 'value' => $price, 'compact' => true];
            }
            if ($market_quantity > 0) {
                $detail_rows[] = ['label' => __('Quantity', 'nexdirectory'), 'value' => (string) $market_quantity, 'compact' => true];
            }
            if ($market_brand !== '') {
                $detail_rows[] = ['label' => __('Brand', 'nexdirectory'), 'value' => $market_brand, 'compact' => true];
            }
            if ($market_model !== '') {
                $detail_rows[] = ['label' => __('Model', 'nexdirectory'), 'value' => $market_model, 'compact' => true];
            }
            if ($market_year > 0) {
                $detail_rows[] = ['label' => __('Year', 'nexdirectory'), 'value' => (string) $market_year, 'compact' => true];
            }
            if ($market_sku !== '') {
                $detail_rows[] = ['label' => __('SKU', 'nexdirectory'), 'value' => $market_sku, 'compact' => true];
            }
            if ($type_names !== '') {
                $detail_rows[] = ['label' => __('Category', 'nexdirectory'), 'value' => $type_names, 'compact' => true];
            }
            if (!empty($market_delivery_display)) {
                $detail_rows[] = ['label' => __('Delivery', 'nexdirectory'), 'value' => implode(', ', $market_delivery_display), 'compact' => false];
            }
            if ($market_payment_methods !== '') {
                $detail_rows[] = ['label' => __('Payment Methods', 'nexdirectory'), 'value' => $market_payment_methods, 'compact' => false];
            }
            $detail_rows[] = ['label' => __('Returns Accepted', 'nexdirectory'), 'value' => $market_returns_accepted ? __('Yes', 'nexdirectory') : __('No', 'nexdirectory'), 'compact' => true];
            if ($tag_names !== '') {
                $detail_rows[] = ['label' => $single_tags_label, 'value' => $tag_names, 'compact' => false];
            }
            if ($market_condition_notes !== '') {
                $custom_fields_display[] = [
                    'label' => __('Condition Notes', 'nexdirectory'),
                    'render_type' => 'html',
                    'value' => $this->add_nofollow_to_external_links((string) wpautop(wp_kses_post($market_condition_notes))),
                ];
            }
            if ($market_shipping_notes !== '') {
                $custom_fields_display[] = [
                    'label' => __('Delivery / Collection Notes', 'nexdirectory'),
                    'render_type' => 'html',
                    'value' => $this->add_nofollow_to_external_links((string) wpautop(wp_kses_post($market_shipping_notes))),
                ];
            }
        } else {
            $summary_cards = array_filter([
                ['label' => $single_type_label, 'value' => $type_names, 'compact' => true],
                ['label' => $single_price_range_label, 'value' => $price_value, 'compact' => true],
            ], static function ($row) {
                return trim((string) ($row['value'] ?? '')) !== '';
            });
            if ($tag_names !== '') {
                $detail_rows[] = ['label' => $single_tags_label, 'value' => $tag_names, 'compact' => false];
            }
        }

        $summary_cards = array_map(static function ($row) {
            $row['value'] = trim((string) preg_replace('/\s+/u', ' ', (string) ($row['value'] ?? '')));
            $row['compact'] = !empty($row['compact']);
            return $row;
        }, $summary_cards);

        foreach ($this->get_custom_taxonomy_definitions_for_overview() as $taxonomy_row) {
            $taxonomy_slug = sanitize_key((string) ($taxonomy_row['slug'] ?? ''));
            if ($taxonomy_slug === '' || !taxonomy_exists($taxonomy_slug)) {
                continue;
            }
            if ($is_job_profile || $is_marketplace_profile || $is_directory_profile) {
                $taxonomy_label_lc = function_exists('mb_strtolower')
                    ? mb_strtolower((string) ($taxonomy_row['label'] ?? ''), 'UTF-8')
                    : strtolower((string) ($taxonomy_row['label'] ?? ''));
                if (
                    strpos($taxonomy_label_lc, 'event type') !== false
                    || strpos($taxonomy_label_lc, 'price range') !== false
                    || strpos($taxonomy_label_lc, 'classification') !== false
                    || strpos($taxonomy_label_lc, 'event start') !== false
                    || strpos($taxonomy_label_lc, 'event end') !== false
                    || strpos($taxonomy_label_lc, 'timezone') !== false
                    || strpos($taxonomy_label_lc, 'capacity') !== false
                    || strpos($taxonomy_label_lc, 'primary partner') !== false
                    || strpos($taxonomy_label_lc, 'owner notes') !== false
                ) {
                    continue;
                }
            }
            $term_names = wp_get_post_terms($post_id, $taxonomy_slug, ['fields' => 'names']);
            if (is_wp_error($term_names) || empty($term_names)) {
                continue;
            }
            $term_display = implode(', ', array_map('nexdirectory_sanitize_text', (array) $term_names));
            if (trim($term_display) === '') {
                continue;
            }
            $detail_rows[] = [
                'label' => sanitize_text_field((string) ($taxonomy_row['label'] ?? $taxonomy_slug)),
                'value' => $term_display,
                'compact' => false,
            ];
        }
        if (trim((string) $contact_phone) !== '' && !$is_marketplace_profile) {
            $detail_rows[] = ['label' => $single_phone_label, 'value' => $contact_phone, 'compact' => true];
        }
        if ($contact_email !== '' && !$is_marketplace_profile && !($is_job_profile && $job_application_email !== '' && strtolower((string) $contact_email) === strtolower((string) $job_application_email))) {
            $detail_rows[] = ['label' => $single_contact_email_label, 'value' => $contact_email, 'url' => 'mailto:' . sanitize_email($contact_email), 'compact' => false];
        }
        if ($external_url !== '' && !$is_marketplace_profile && !($is_job_profile && (
            ($job_apply_url !== '' && untrailingslashit($external_url) === untrailingslashit($job_apply_url))
            || ($job_company_website !== '' && untrailingslashit($external_url) === untrailingslashit($job_company_website))
        ))) {
            $detail_rows[] = [
                'label' => $single_website_label,
                'value' => $external_url,
                'url' => $external_url,
                'metric' => $external_click_metric,
                'compact' => false,
            ];
        }
        if ($is_events_profile && $stream_url !== '') {
            $detail_rows[] = ['label' => $single_video_label, 'value' => $stream_url, 'url' => $stream_url, 'compact' => false];
        }
        $detail_rows = array_values(array_filter($detail_rows, static function ($row) {
            return trim((string) ($row['value'] ?? '')) !== '';
        }));
        $detail_rows = array_map(static function ($row) {
            $row['value'] = trim((string) preg_replace('/\s+/u', ' ', (string) ($row['value'] ?? '')));
            $row['compact'] = !empty($row['compact']);
            return $row;
        }, $detail_rows);
        $overview_has_content = (
            trim((string) nexdirectory_strip_all_tags((string) $content_html)) !== ''
            || !empty($summary_cards)
            || !empty($overview_chips)
            || !empty($detail_rows)
        );

        $all_images = [];
        if ($cover_image !== '') {
            $all_images[] = $cover_image;
        }
        foreach ($gallery_urls as $gallery_url) {
            $all_images[] = esc_url((string) $gallery_url);
        }
        $all_images = array_values(array_unique(array_filter($all_images)));
        $video_cards = $this->build_event_video_cards($video_urls, $cover_image !== '' ? $cover_image : ($all_images[0] ?? ''));
        $supporting_document_cards = $this->build_event_supporting_document_cards($supporting_doc_urls);

        $sessions_rows = [];
        if ($is_events_profile) {
            $raw_sessions = json_decode((string) get_post_meta($post_id, 'nexdirectory_sessions', true), true);
            if (is_array($raw_sessions)) {
                foreach ($raw_sessions as $index => $session) {
                    if (!is_array($session)) {
                        continue;
                    }
                    $session_start_raw = (string) ($session['start'] ?? '');
                    $session_end_raw = (string) ($session['end'] ?? '');
                    $session_start = $session_start_raw !== '' ? strtotime($session_start_raw) : false;
                    $session_end = $session_end_raw !== '' ? strtotime($session_end_raw) : false;
                    $time_bits = [];
                    if ($session_start) {
                        if ($this->event_datetime_has_time($session_start_raw)) {
                            $time_bits[] = wp_date(get_option('time_format'), (int) $session_start);
                        } else {
                            $time_bits[] = wp_date(get_option('date_format'), (int) $session_start);
                        }
                    }
                    if ($session_end) {
                        if ($this->event_datetime_has_time($session_end_raw)) {
                            $time_bits[] = wp_date(get_option('time_format'), (int) $session_end);
                        } else {
                            $time_bits[] = wp_date(get_option('date_format'), (int) $session_end);
                        }
                    }
                    if ($session_start && $session_end && wp_date('Y-m-d', (int) $session_start) !== wp_date('Y-m-d', (int) $session_end)) {
                        $time_bits = [
                            $this->format_event_datetime_for_display($session_start_raw),
                            $this->format_event_datetime_for_display($session_end_raw),
                        ];
                    }

                    $day_label = sanitize_text_field((string) ($session['day'] ?? ''));
                    $day_key = '';
                    if ($session_start) {
                        $day_key = wp_date('Y-m-d', (int) $session_start);
                        if ($day_label === '') {
                            $day_label = wp_date(get_option('date_format'), (int) $session_start);
                        }
                    } elseif ($day_label === '' && $start_ts) {
                        $day_key = wp_date('Y-m-d', (int) $start_ts);
                        $day_label = wp_date(get_option('date_format'), (int) $start_ts);
                    } elseif ($day_label !== '' && preg_match('/^\d{4}\-\d{2}\-\d{2}$/', $day_label)) {
                        $day_key = $day_label;
                        $day_label = wp_date(get_option('date_format'), strtotime($day_label . ' 00:00:00'));
                    }

                    $attachments_raw = $session['attachments'] ?? [];
                    if (is_string($attachments_raw)) {
                        $attachments_raw = preg_split('/[\r\n,]+/', $attachments_raw) ?: [];
                    }
                    $attachments = [];
                    foreach ((array) $attachments_raw as $attachment_item) {
                        $attachment_value = trim((string) $attachment_item);
                        if ($attachment_value === '') {
                            continue;
                        }
                        if (is_numeric($attachment_value)) {
                            $attachment_id = absint((int) $attachment_value);
                            if ($attachment_id > 0) {
                                $resolved_url = wp_get_attachment_url($attachment_id);
                                if (is_string($resolved_url) && $resolved_url !== '') {
                                    $attachment_value = $resolved_url;
                                } else {
                                    continue;
                                }
                            }
                        }
                        if (preg_match('/^[A-Za-z0-9_-]{11}$/', $attachment_value)) {
                            $attachment_value = 'https://www.youtube.com/watch?v=' . $attachment_value;
                        } elseif (preg_match('/^(?:youtube\.com|youtu\.be|vimeo\.com)\//i', $attachment_value)) {
                            $attachment_value = 'https://' . $attachment_value;
                        } elseif (preg_match('/^[a-z0-9.-]+\.[a-z]{2,}(?:\/|$)/i', $attachment_value) && !preg_match('/^[a-z]+:\/\//i', $attachment_value)) {
                            $attachment_value = 'https://' . $attachment_value;
                        }
                        $attachment_url = esc_url_raw($attachment_value);
                        if ($attachment_url !== '') {
                            $attachments[] = $attachment_url;
                        }
                    }
                    $attachments = array_values(array_unique($attachments));

                    $sessions_rows[] = [
                        /* translators: %d: Placeholder value. */
                        'title' => !empty($session['title']) ? sanitize_text_field((string) $session['title']) : sprintf(__('Session %d', 'nexdirectory'), $index + 1),
                        'time' => implode(' - ', $time_bits),
                        'location' => sanitize_text_field((string) ($session['location'] ?? '')),
                        'speaker' => sanitize_text_field((string) ($session['speaker'] ?? '')),
                        'day_key' => $day_key,
                        'day_label' => $day_label,
                        'start_raw' => $session_start_raw,
                        'end_raw' => $session_end_raw,
                        'notes' => nexdirectory_sanitize_textarea((string) ($session['notes'] ?? '')),
                        'attachments' => $attachments,
                        'session_key' => sanitize_key((string) ($session['session_key'] ?? '')),
                    ];
                }
            }
        } elseif ($is_job_profile) {
            $posted_ts = strtotime((string) get_post_field('post_date', $post_id));
            if ($posted_ts) {
                $sessions_rows[] = [
                    'title' => __('Posted On', 'nexdirectory'),
                    'time' => wp_date(get_option('date_format'), (int) $posted_ts),
                    'location' => '',
                    'speaker' => '',
                ];
            }
            if ($job_closing_display !== '') {
                $sessions_rows[] = [
                    'title' => __('Application Deadline', 'nexdirectory'),
                    'time' => $job_closing_display,
                    'location' => '',
                    'speaker' => '',
                ];
            }
            if ($job_expected_start_display !== '') {
                $sessions_rows[] = [
                    'title' => __('Expected Start Date', 'nexdirectory'),
                    'time' => $job_expected_start_display,
                    'location' => '',
                    'speaker' => '',
                ];
            }
            if ($job_interview_process !== '') {
                $sessions_rows[] = [
                    'title' => __('Interview Process', 'nexdirectory'),
                    'time' => nexdirectory_sanitize_text($job_interview_process),
                    'location' => '',
                    'speaker' => '',
                ];
            }
        }
        $key_speakers_rows = $is_events_profile
            ? $this->parse_key_speakers((string) get_post_meta($post_id, 'nexdirectory_key_speakers', true))
            : [];

        $start = strtotime((string) get_post_meta($post_id, 'nexdirectory_start', true));
        $end = strtotime((string) get_post_meta($post_id, 'nexdirectory_end', true));
        if (!$end || $end <= $start) {
            if ($start && !$start_has_time) {
                $end = $start + DAY_IN_SECONDS;
            } else {
                $end = $start ? ($start + HOUR_IN_SECONDS) : time() + HOUR_IN_SECONDS;
            }
        }
        $calendar_url = nexdirectory_add_query_arg('nexdirectory_ics', $post_id, home_url('/'));
        $google_url = nexdirectory_add_query_arg([
            'action' => 'TEMPLATE',
            'text' => get_the_title($post_id),
            'dates' => gmdate('Ymd\THis\Z', $start ?: time()) . '/' . gmdate('Ymd\THis\Z', $end),
            'details' => nexdirectory_strip_all_tags(get_post_field('post_excerpt', $post_id) ?: get_post_field('post_content', $post_id)),
            'location' => $address !== '' ? $address : $city,
        ], 'https://calendar.google.com/calendar/render');

        $organizer_id = (int) get_post_field('post_author', $post_id);
        $organizer_profile = $organizer_id > 0
            ? $this->get_public_organizer_profile($organizer_id, true)
            : [];
        $verified = !empty($organizer_profile['verified']);
        $organizer_name = sanitize_text_field((string) ($organizer_profile['name'] ?? __('Listing owner', 'nexdirectory')));
        $organizer_email = sanitize_email((string) ($organizer_profile['email'] ?? ''));
        $organizer_events_url = esc_url_raw((string) ($organizer_profile['profile_url'] ?? ''));
        if ($organizer_events_url === '') {
            $organizer_events_url = nexdirectory_add_query_arg('organizer', $organizer_id, $events_base_url);
        }
        $organizer_member_since = sanitize_text_field((string) ($organizer_profile['member_since'] ?? ''));
        $organizer_listing_count = absint((int) ($organizer_profile['listing_count'] ?? 0));

        $viewer_logged_in = is_user_logged_in();
        $rsvp = $this->get_event_attendees_by_status($post_id, 'yes');
        $waitlist = $this->get_event_attendees_by_status($post_id, 'waitlist');
        $is_joined = $viewer_logged_in && (in_array(get_current_user_id(), $rsvp, true) || in_array(get_current_user_id(), $waitlist, true));
        $post_status = (string) get_post_status($post_id);
        $now_ts = current_time('timestamp');
        $show_rsvp_actions = $is_events_profile || $is_marketplace_profile;
        $show_calendar_actions = $is_events_profile;
        $show_timeline = ($is_events_profile || $is_job_profile) && !empty($sessions_rows);
        $show_key_speakers = $is_events_profile && !empty($key_speakers_rows);
        $platform_mode = sanitize_key((string) get_option('nexdirectory_platform_mode', 'user_generated'));
        $single_org_profile = $platform_mode === 'single_organisation' || $resolved_profile_key === 'events_single_organisation';
        $single_org_mode = $is_events_profile && $single_org_profile;
        $show_organizer_card = !$single_org_mode;

        if ($single_org_mode) {
            $show_rsvp_actions = false;
        }
        $capacity_int = is_numeric($capacity) ? (int) $capacity : 0;
        if ($post_status !== 'publish') {
            $status_text = ucfirst($post_status);
        } elseif ($is_marketplace_profile) {
            $market_status_map = [
                'active' => __('Available', 'nexdirectory'),
                'reserved' => __('Reserved', 'nexdirectory'),
                'sold' => __('Sold', 'nexdirectory'),
                'expired' => __('Expired', 'nexdirectory'),
            ];
            if ($market_expires_ts && $market_expires_ts < $now_ts) {
                $status_text = __('Expired', 'nexdirectory');
            } else {
                $status_text = (string) ($market_status_map[$market_listing_status] ?? __('Available', 'nexdirectory'));
            }
        } elseif ($is_job_profile && $job_closing_ts && $job_closing_ts < $now_ts) {
            $status_text = __('Applications Closed', 'nexdirectory');
        } elseif ($is_events_profile && $end_ts && $end_ts < $now_ts) {
            $status_text = __('Closed', 'nexdirectory');
        } elseif ($is_events_profile && $capacity_int > 0 && count($rsvp) >= $capacity_int) {
            $status_text = $is_events_profile ? __('Waitlist Only', 'nexdirectory') : __('At Capacity', 'nexdirectory');
        } elseif ($is_job_profile) {
            $status_text = __('Accepting Applications', 'nexdirectory');
        } elseif ($is_directory_profile) {
            $directory_status_map = [
                'active' => __('Active', 'nexdirectory'),
                'temporarily-closed' => __('Temporarily Closed', 'nexdirectory'),
                'permanently-closed' => __('Permanently Closed', 'nexdirectory'),
            ];
            $status_text = (string) ($directory_status_map[$directory_listing_status] ?? __('Active', 'nexdirectory'));
        } else {
            $status_text = $is_events_profile ? __('RSVP Open', 'nexdirectory') : __('Open', 'nexdirectory');
        }
        $owner_id = (int) get_post_field('post_author', $post_id);
        $current_user_id = is_user_logged_in() ? (int) get_current_user_id() : 0;
        $owner_event_messages_enabled = $owner_id > 0 ? (bool) $this->user_allows_event_messages($owner_id) : true;
        $is_owner = $current_user_id > 0 && $current_user_id === $owner_id;
        $can_edit_event = $current_user_id > 0 && $this->user_can_edit_event($post_id, $current_user_id);
        $can_duplicate_locked_demo = !$can_edit_event
            && $current_user_id > 0
            && method_exists($this, 'user_can_duplicate_event')
            && (bool) $this->user_can_duplicate_event($post_id, $current_user_id);
        $claim_enabled = (string) get_option('nexdirectory_enable_claim_listings', '0') === '1';
        $claimable = $claim_enabled && $this->is_event_claimable($post_id) && !$is_owner;
        $claim_status = '';
        if ($claimable && is_user_logged_in()) {
            $claim_status = $this->get_user_claim_status_for_event($post_id, get_current_user_id());
        }
        $claim_page_url = $this->get_platform_page_url(
            'nexdirectory_claim_listing_page_url',
            $this->get_profile_route_slugs('claim'),
            $this->get_profile_route_default_url('claim')
        );
        $claim_url = $claimable ? nexdirectory_add_query_arg('event_id', $post_id, $claim_page_url) : '';
        $import_source = sanitize_key((string) get_post_meta($post_id, 'nexdirectory_import_source', true));
        $is_plugin_generated_event = (string) get_post_meta($post_id, 'nexdirectory_generated_by_plugin', true) === '1' || $import_source === 'chrome_extension';
        $show_autogenerated_disclaimer = $is_plugin_generated_event && (string) get_option('nexdirectory_enable_autogenerated_disclaimer', '1') === '1';
        $autogenerated_disclaimer_text = nexdirectory_sanitize_textarea((string) get_option('nexdirectory_autogenerated_disclaimer_text', ''));
        if ($show_autogenerated_disclaimer && $autogenerated_disclaimer_text === '') {
            $site_name = nexdirectory_strip_all_tags((string) $this->get_white_label_brand_name());
            if ($site_name === '') {
                $site_name = __('This website', 'nexdirectory');
            }
            if ($is_events_profile) {
                $autogenerated_disclaimer_text = sprintf(
                    /* translators: %s: Placeholder value. */
                    __('This listing was auto-generated from a third-party source and event details may change without notice. Please verify date, time, location, and registration on the original host website. %s is not affiliated with the event host and cannot guarantee the accuracy of imported information. If you own this event, please claim the listing or contact us to request updates or removal.', 'nexdirectory'),
                    $site_name
                );
            } elseif ($is_job_profile) {
                $autogenerated_disclaimer_text = sprintf(
                    /* translators: %s: Placeholder value. */
                    __('This job listing was auto-generated from a third-party source and details may change without notice. Please verify role requirements, location, and application information on the original employer website. %s is not affiliated with the employer and cannot guarantee the accuracy of imported information. If you own this listing, please claim it or contact us to request updates or removal.', 'nexdirectory'),
                    $site_name
                );
            } else {
                $autogenerated_disclaimer_text = sprintf(
                    /* translators: 1: Placeholder value 1, 2: Placeholder value 2. */
                    __('This %1$s was auto-generated from a third-party source and details may change without notice. Please verify information on the original source website. %2$s is not affiliated with the original source and cannot guarantee the accuracy of imported information. If you own this listing, please claim it or contact us to request updates or removal.', 'nexdirectory'),
                    $profile_singular_lower,
                    $site_name
                );
            }
        }
        $autogenerated_source_url = $is_plugin_generated_event ? esc_url_raw((string) get_post_meta($post_id, 'nexdirectory_import_source_url', true)) : '';
        $edit_event_url = '';
        $edit_event_is_duplicate = false;
        if ($can_edit_event) {
            $edit_target_id = $post_id;
            $submit_event_url = $this->get_platform_page_url(
                'nexdirectory_page_submit_event_url',
                $this->get_profile_route_slugs('submit'),
                $this->get_profile_route_default_url('submit')
            );
            if ($submit_event_url !== '') {
                $edit_event_url = nexdirectory_add_query_arg([
                    'nexdirectory_tab' => 'submit',
                    'edit_event_id' => $edit_target_id,
                ], $submit_event_url) . '#submit';
            } else {
                $edit_event_url = '';
            }
        } elseif ($can_duplicate_locked_demo) {
            $edit_event_url = wp_nonce_url(
                nexdirectory_add_query_arg([
                    'action' => 'nexdirectory_duplicate_event',
                    'post' => $post_id,
                ], admin_url('admin.php')),
                'nexdirectory_duplicate_event',
                'nexdirectory_duplicate_nonce'
            );
            $edit_event_is_duplicate = true;
        }

        $hero_style_attr = $cover_image !== '' ? ' style="background-image:url(' . esc_url($cover_image) . ')"' : '';
        $hero_kicker_text = $this->get_event_hero_kicker_text();
        $hero_date_label = '';
        $show_hero_date_card = !$is_marketplace_profile;
        if ($is_marketplace_profile) {
            $hero_date_label = '';
        } elseif ($is_job_profile && $job_closing_ts) {
            $hero_date_label = sprintf(
                /* translators: %s: date */
                __('Closes %s', 'nexdirectory'),
                wp_date('M j, Y', (int) $job_closing_ts)
            );
        } elseif ($start_ts && $end_ts && wp_date('Y-m-d', (int) $end_ts) !== wp_date('Y-m-d', (int) $start_ts)) {
            $hero_date_label = (string) $this->format_event_date($post_id);
        } elseif ($start_ts) {
            $hero_date_label = wp_date('M j, Y', (int) $start_ts);
        } else {
            $posted_ts = strtotime((string) get_post_field('post_date', $post_id));
            $hero_date_label = $posted_ts ? wp_date('M j, Y', (int) $posted_ts) : __('Date TBD', 'nexdirectory');
        }
        $hero_location_label = $location_line !== '' ? $location_line : '';
        $job_application_deadline = $is_job_profile ? $job_closing_display : '';
        $local_time_start_iso = ($start && $start_has_time) ? gmdate('c', (int) $start) : '';
        $local_time_end_iso = ($end && $end_has_time) ? gmdate('c', (int) $end) : '';
        $login_url = $this->get_login_signup_url(get_permalink($post_id));
        $signup_url = $this->get_auth_screen_url('signup', get_permalink($post_id));
        $whos_attending_url = '';
        if ($is_events_profile) {
            $whos_attending_base_url = $this->get_platform_page_url(
                'nexdirectory_page_whos_attending_url',
                $this->get_profile_route_slugs('attendees', $resolved_profile_key),
                $this->get_profile_route_default_url('attendees', $resolved_profile_key)
            );
            if ($whos_attending_base_url !== '') {
                $whos_attending_url = nexdirectory_add_query_arg([
                    'event_id' => $post_id,
                    'status' => 'yes',
                ], $whos_attending_base_url);
            }
        }
        $event_webapp_link = apply_filters(
            'nexdirectory_event_webapp_link_context',
            [
                'enabled' => false,
                'url'     => '',
            ],
            $post_id,
            $current_user_id
        );
        $event_webapp_link = is_array($event_webapp_link) ? $event_webapp_link : [];
        $event_webapp_enabled = $is_events_profile && !empty($event_webapp_link['enabled']);
        $event_webapp_url = $event_webapp_enabled
            ? esc_url_raw((string) ($event_webapp_link['url'] ?? ''))
            : '';
        $single_open_event_app_cta = sanitize_text_field((string) ($ui_labels['single_open_event_app_cta'] ?? __('Download App', 'nexdirectory')));
        $primary_action_url = '';
        $primary_action_label = '';
        $primary_action_metric = '';
        if ($is_directory_profile && $directory_appointment_url !== '') {
            $primary_action_url = $directory_appointment_url;
            $primary_action_label = $single_directory_book_cta;
            $primary_action_metric = 'click_website';
        } elseif ($is_directory_profile && $directory_whatsapp_url !== '') {
            $primary_action_url = $directory_whatsapp_url;
            $primary_action_label = $single_directory_whatsapp_cta;
            $primary_action_metric = 'click_whatsapp';
        } elseif ($is_job_profile && $job_apply_url !== '') {
            $primary_action_url = $job_apply_url;
            $primary_action_label = $single_visit_site_cta;
            $primary_action_metric = 'click_website';
        } elseif ($is_job_profile && $job_application_email !== '') {
            $primary_action_url = 'mailto:' . sanitize_email($job_application_email);
            $primary_action_label = $single_apply_email_cta;
            $primary_action_metric = 'click_website';
        } elseif (!$is_events_profile && $external_url !== '') {
            $primary_action_url = $external_url;
            $primary_action_label = $single_visit_site_cta;
            $primary_action_metric = $external_click_metric;
        }
        $hero_layout = sanitize_key((string) get_option('nexdirectory_event_hero_layout', 'cinematic'));
        if (!in_array($hero_layout, ['cinematic', 'compact', 'split', 'minimal', 'immersive', 'spotlight'], true)) {
            $hero_layout = 'cinematic';
        }
        $page_layout = sanitize_key((string) get_option('nexdirectory_event_page_layout', 'modern'));
        if (in_array($page_layout, ['balanced', 'compact-aside', 'cards-heavy'], true)) {
            $page_layout = 'fifty-fifty';
        }
        if (!in_array($page_layout, ['modern', 'single-column', 'editorial', 'fifty-fifty', 'left-sidebar', 'zig-zag', 'full3', 'zen'], true)) {
            $page_layout = 'modern';
        }
        $event_gallery_type = method_exists($this, 'sanitize_event_gallery_type_option')
            ? $this->sanitize_event_gallery_type_option((string) get_option('nexdirectory_event_gallery_type', 'default'))
            : 'default';
        $reporting_enabled = (string) get_option('nexdirectory_enable_event_reporting', '1') === '1';
        if ($single_org_mode) {
            $reporting_enabled = false;
        }
        $report_nonce_html = wp_nonce_field('nexdirectory_report_event', 'nexdirectory_report_nonce', true, false);

        $event_markup = $this->render_frontend_template('single-event.php', [
            'post_id' => $post_id,
            'event_title' => $event_title,
            'event_subtitle' => $excerpt,
            'content_html' => $content_html,
            'hero_image' => $cover_image,
            'hero_style_attr' => $hero_style_attr,
            'hero_kicker_text' => $hero_kicker_text,
            'hero_layout' => $hero_layout,
            'page_layout' => $page_layout,
            'event_gallery_type' => $event_gallery_type,
            'hero_date_label' => $hero_date_label,
            'show_hero_date_card' => $show_hero_date_card,
            'hero_location_label' => $hero_location_label,
            'local_time_start_iso' => $local_time_start_iso,
            'local_time_end_iso' => $local_time_end_iso,
            'event_timezone_label' => $timezone,
            'summary_cards' => $summary_cards,
            'overview_chips' => $overview_chips,
            'detail_rows' => $detail_rows,
            'show_event_overview' => $overview_has_content,
            'custom_fields_display' => $custom_fields_display,
            'sessions' => $sessions_rows,
            'show_timeline' => $show_timeline,
            'key_speakers' => $key_speakers_rows,
            'show_key_speakers' => $show_key_speakers,
            'gallery_images' => $gallery_display_urls,
            'video_cards' => $video_cards,
            'supporting_documents' => $supporting_document_cards,
            'map_link' => $map_link,
            'map_embed_url' => $map_embed_url,
            'venue_label' => $venue !== '' ? $venue : $event_title,
            'location_full' => $location_full,
            'status_text' => $status_text,
            'job_application_deadline' => $job_application_deadline,
            'external_url' => esc_url($external_url),
            'external_click_metric' => $external_click_metric,
            'calendar_url' => esc_url($calendar_url),
            'google_url' => esc_url($google_url),
            'viewer_logged_in' => $viewer_logged_in,
            'rsvp_enabled' => $viewer_logged_in,
            'show_rsvp_actions' => $show_rsvp_actions,
            'single_org_mode' => $single_org_mode,
            'show_calendar_actions' => $show_calendar_actions,
            'rsvp_count' => count($rsvp),
            'waitlist_count' => count($waitlist),
            'whos_attending_url' => $whos_attending_url,
            'event_webapp_enabled' => $event_webapp_enabled,
            'event_webapp_url' => esc_url($event_webapp_url),
            'rsvp_intent' => $is_joined ? 'leave' : 'join',
            'rsvp_button_label' => $is_joined ? $single_rsvp_cancel : $single_rsvp_join,
            'rsvp_nonce_html' => wp_nonce_field('nexdirectory_rsvp_event', 'nexdirectory_rsvp_nonce', true, false),
            'primary_action_url' => esc_url($primary_action_url),
            'primary_action_label' => $primary_action_label,
            'primary_action_metric' => $primary_action_metric,
            'directory_appointment_url' => esc_url($directory_appointment_url),
            'directory_whatsapp_url' => esc_url($directory_whatsapp_url),
            'directory_contact_person' => $directory_contact_person,
            'login_url' => $login_url,
            'signup_url' => $signup_url,
            'edit_event_url' => $edit_event_url,
            'edit_event_is_duplicate' => $edit_event_is_duplicate,
            'claimable' => $claimable,
            'claim_status' => $claim_status,
            'claim_url' => $claim_url,
            'show_autogenerated_disclaimer' => $show_autogenerated_disclaimer,
            'autogenerated_disclaimer_text' => $autogenerated_disclaimer_text,
            'autogenerated_source_url' => esc_url($autogenerated_source_url),
            'organizer_name' => $organizer_name,
            'organizer_id' => $organizer_id,
            'organizer_email' => $organizer_email,
            'organizer_verified' => $verified,
            'organizer_events_url' => $organizer_events_url,
            'organizer_member_since' => $organizer_member_since,
            'organizer_listing_count' => $organizer_listing_count,
            'show_organizer_card' => $show_organizer_card,
            'owner_event_messages_enabled' => $owner_event_messages_enabled,
            'contact_nonce_html' => wp_nonce_field('nexdirectory_contact_holder', 'nexdirectory_contact_nonce', true, false),
            'contact_email' => $contact_email,
            'contact_phone' => $contact_phone,
            'market_accept_offers' => $market_accept_offers,
            'reporting_enabled' => $reporting_enabled,
            'report_nonce_html' => $report_nonce_html,
            'is_events_profile' => $is_events_profile,
            'is_job_profile' => $is_job_profile,
            'is_directory_profile' => $is_directory_profile,
            'is_marketplace_profile' => $is_marketplace_profile,
            'ui_labels' => $ui_labels,
            'single_overview_heading' => $single_overview_heading,
            'single_timeline_heading' => $single_timeline_heading,
            'single_gallery_heading' => $single_gallery_heading,
            'single_videos_heading' => $single_videos_heading,
            'single_cta_heading' => $single_cta_heading,
            'single_cta_copy' => $single_cta_copy,
            'single_visit_site_cta' => $single_visit_site_cta,
            'single_report_cta' => $single_report_cta,
            'single_report_placeholder' => $single_report_placeholder,
            'single_contact_login' => $single_contact_login,
            'single_owner_items_cta' => $single_owner_items_cta,
            'single_login_rsvp' => $single_login_rsvp,
            'single_edit_cta' => $single_edit_cta,
            'single_open_event_app_cta' => $single_open_event_app_cta,
        ]);

            return $this->escape_registered_callback_markup(
                $top_injected_html . (string) $this->render_notice() . (string) $event_markup . $bottom_injected_html
            );
        } finally {
            $is_rendering_event_content = false;
        }
    }
}
