<?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.

/**
 * Plugin Name: Nex Directory
 * Description: A flexible directory and events platform with unlimited submissions, moderation, maps, calendars, and member workspaces.
 * Plugin URI: https://nexdirectory.com/
 * Version: 1.4.7
 * Requires at least: 6.0
 * Requires PHP: 7.4
 * Author: Nex Directory
 * License: GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: nexdirectory
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
if ( !defined( 'NEXDIRECTORY_PLUGIN_FILE' ) ) {
    define( 'NEXDIRECTORY_PLUGIN_FILE', __FILE__ );
}
if ( !defined( 'NEXDIRECTORY_PRO_UPGRADE_URL' ) ) {
    define( 'NEXDIRECTORY_PRO_UPGRADE_URL', 'https://nexdirectory.com/pricing' );
}
if ( !defined( 'NEXDIRECTORY_CHROME_EXTENSION_URL' ) ) {
    define( 'NEXDIRECTORY_CHROME_EXTENSION_URL', 'https://chromewebstore.google.com/detail/dnijcccaedhkbbekcbacgbhldmminpip?utm_source=item-share-cb' );
}
if ( !defined( 'NEXDIRECTORY_PRO_ADDON_PLUGIN_FILE' ) ) {
    define( 'NEXDIRECTORY_PRO_ADDON_PLUGIN_FILE', 'nexdirectory-pro/nexdirectory-pro.php' );
}
if ( !defined( 'NEXDIRECTORY_DB_VERSION' ) ) {
    define( 'NEXDIRECTORY_DB_VERSION', '1.4.7' );
}
if ( !defined( 'NEXDIRECTORY_ROLE_SCHEMA_VERSION' ) ) {
    define( 'NEXDIRECTORY_ROLE_SCHEMA_VERSION', '1' );
}
if ( !class_exists( 'NEXDIRECTORY_Escaping', false ) ) {
    /**
     * Escaping helpers for plugin-generated markup.
     */
    final class NEXDIRECTORY_Escaping {
        /** @var string Per-request marker for enqueue-generated code blocks. */
        private static $trusted_code_block_token = '';

        /** @var array<string, bool> Exact clean blocks registered during this request. */
        private static $trusted_code_block_hashes = [];

        /**
         * Registers enqueue-generated script/style markup as trusted for this request.
         *
         * @param mixed $markup WordPress-generated asset markup.
         * @return string Markup carrying an unguessable per-request trust marker.
         */
        public static function register_trusted_code_block( $markup ) {
            if ( !is_string( $markup ) || trim( $markup ) === '' ) {
                return '';
            }
            if ( self::$trusted_code_block_token === '' ) {
                self::$trusted_code_block_token = wp_generate_password( 32, false, false );
            }
            $token = esc_attr( self::$trusted_code_block_token );
            return (string) preg_replace_callback(
                '#<(script|style)\b([^>]*)>.*?</\1>#is',
                static function ( $matches ) use ( $token ) {
                    $block = (string) ( $matches[0] ?? '' );
                    self::$trusted_code_block_hashes[hash( 'sha256', $block )] = true;
                    return (string) preg_replace(
                        '#^<(script|style)\b([^>]*)>#i',
                        '<$1$2 data-nexdirectory-code-token="' . $token . '">',
                        $block,
                        1
                    );
                },
                $markup
            );
        }

        /**
         * Escapes complex NexDirectory UI markup while preserving app bindings.
         *
         * @param mixed $markup Markup to escape.
         * @return string
         */
        public static function plugin_markup( $markup ) {
            $markup = (string) $markup;
            if ( $markup === '' ) {
                return '';
            }

	            $trusted_code_blocks = [];
	            $markup              = self::extract_trusted_code_blocks( $markup, $trusted_code_blocks );
	            $markup              = self::prepare_markup_for_wp_kses( $markup );
	            $escaped             = wp_kses( $markup, self::allowed_html( $markup ) );

            if ( $trusted_code_blocks ) {
                $escaped = strtr( $escaped, $trusted_code_blocks );
            }

            return $escaped;
        }

        /**
         * Preserves exact inline script/style blocks registered by WordPress enqueue APIs.
         *
         * wp_kses() is intentionally strict with script/style text, which corrupts JavaScript
         * operators such as >= and && when a late enqueue is printed inside returned markup.
         * Arbitrary markup cannot opt into this path by forging an element ID.
         *
         * @param string $markup Markup containing possible late-printed code blocks.
         * @param array<string, string> $trusted_code_blocks Placeholder map.
         * @return string
         */
        private static function extract_trusted_code_blocks( $markup, array &$trusted_code_blocks ) {
            return (string) preg_replace_callback(
                '#<(script|style)\b([^>]*)>.*?</\1>#is',
                static function ( $matches ) use ( &$trusted_code_blocks ) {
                    $block = (string) ( $matches[0] ?? '' );
                    $attributes = (string) ( $matches[2] ?? '' );
                    $has_valid_token = !(
                        self::$trusted_code_block_token === ''
                        || !preg_match( '/\bdata-nexdirectory-code-token\s*=\s*(["\'])(.*?)\1/i', $attributes, $token_match )
                        || !hash_equals( self::$trusted_code_block_token, (string) ( $token_match[2] ?? '' ) )
                    );

                    $clean_block = (string) preg_replace(
                        '/\sdata-nexdirectory-code-token\s*=\s*(["\']).*?\1/i',
                        '',
                        $block,
                        1
                    );
                    $clean_hash = hash( 'sha256', $clean_block );
                    if ( !$has_valid_token && !isset( self::$trusted_code_block_hashes[$clean_hash] ) ) {
                        return $block;
                    }
                    self::$trusted_code_block_hashes[$clean_hash] = true;
                    $placeholder = 'NEXDIRECTORY_TRUSTED_CODE_' . count( $trusted_code_blocks ) . '_PLACEHOLDER';
                    $trusted_code_blocks[$placeholder] = $clean_block;
                    return $placeholder;
                },
                $markup
            );
        }

        /**
         * Converts Alpine shorthand attributes to parser-friendly longhand names.
         *
         * @param mixed $markup Markup to normalize.
         * @return string
         */
	        public static function normalize_alpine_shorthand_attributes( $markup ) {
	            return (string) preg_replace_callback(
	                '/<([a-zA-Z][a-zA-Z0-9:-]*)([^<>]*)>/',
                static function ( $matches ) {
                    $attributes = (string) ( $matches[2] ?? '' );
                    $attributes = (string) preg_replace_callback(
                        '/(\s)([@:])([_a-zA-Z][-_a-zA-Z0-9:.]*)/',
                        static function ( $attribute_matches ) {
                            $prefix = $attribute_matches[2] === '@' ? 'x-on:' : 'x-bind:';
                            return (string) $attribute_matches[1] . $prefix . (string) $attribute_matches[3];
                        },
                        $attributes
                    );
                    return '<' . (string) $matches[1] . $attributes . '>';
                },
	                (string) $markup
	            );
	        }

	        /**
	         * Prepares plugin-owned markup for a direct wp_kses() call.
	         *
	         * @param mixed $markup Markup to prepare.
	         * @return string
	         */
	        public static function prepare_markup_for_wp_kses( $markup ) {
	            $markup = self::encode_angle_brackets_in_quoted_attributes( (string) $markup );
	            return self::normalize_alpine_shorthand_attributes( $markup );
	        }

	        /**
	         * Encodes tag-like operators inside quoted attributes before wp_kses() parses tags.
	         *
	         * @param mixed $markup Markup to normalize.
	         * @return string
	         */
	        private static function encode_angle_brackets_in_quoted_attributes( $markup ) {
	            return (string) preg_replace_callback(
	                '/<[a-zA-Z][a-zA-Z0-9:-]*(?:(?:"[^"]*"|\'[^\']*\'|[^\'">])*)>/s',
	                static function ( $matches ) {
	                    return (string) preg_replace_callback(
	                        '/=([\'"])(.*?)\1/s',
	                        static function ( $attribute_matches ) {
	                            $quote = (string) $attribute_matches[1];
	                            $value = str_replace(
	                                [ '<', '>' ],
	                                [ '&lt;', '&gt;' ],
	                                (string) $attribute_matches[2]
	                            );

	                            return '=' . $quote . $value . $quote;
	                        },
	                        (string) ( $matches[0] ?? '' )
	                    );
	                },
	                (string) $markup
	            );
	        }

	        /**
         * Returns the HTML allow-list for plugin-generated UI.
         *
         * @param mixed $markup Optional markup used to preserve dynamic app attributes.
         * @return array<string, array<string, bool>>
         */
        public static function allowed_html( $markup = '' ) {
            $allowed = wp_kses_allowed_html( 'post' );
            $additional_tags = [
                'article', 'aside', 'button', 'canvas', 'dialog', 'fieldset', 'figure',
                'body', 'footer', 'form', 'head', 'header', 'html', 'iframe', 'input',
                'label', 'legend', 'link', 'main', 'meta', 'nav', 'noscript', 'option',
                'picture', 'section', 'title',
                'select', 'source', 'svg', 'template', 'textarea', 'time',
                'video', 'circle', 'clippath', 'defs', 'g', 'line', 'path', 'polygon',
                'polyline', 'rect', 'symbol', 'use',
            ];
            $global_attributes = [
                'accept' => true,
                'accept-charset' => true,
                'action' => true,
                'allow' => true,
                'allowfullscreen' => true,
                'alt' => true,
                'aria-*' => true,
                'autocomplete' => true,
                'autoplay' => true,
                'checked' => true,
                'class' => true,
                'charset' => true,
                'cols' => true,
                'colspan' => true,
                'content' => true,
                'controls' => true,
                'cx' => true,
                'cy' => true,
                'd' => true,
                'data-*' => true,
                'datetime' => true,
                'decoding' => true,
                'disabled' => true,
                'enctype' => true,
                'fill' => true,
                'for' => true,
                'form' => true,
                'frameborder' => true,
                'height' => true,
                'hidden' => true,
                'href' => true,
                'http-equiv' => true,
                'id' => true,
                'lang' => true,
                'loading' => true,
                'loop' => true,
                'max' => true,
                'maxlength' => true,
                'media' => true,
                'method' => true,
                'min' => true,
                'multiple' => true,
                'muted' => true,
                'name' => true,
                'pattern' => true,
                'placeholder' => true,
                'playsinline' => true,
                'points' => true,
                'poster' => true,
                'preserveaspectratio' => true,
                'r' => true,
                'readonly' => true,
                'rel' => true,
                'referrerpolicy' => true,
                'required' => true,
                'role' => true,
                'rows' => true,
                'rowspan' => true,
                'rx' => true,
                'ry' => true,
                'sandbox' => true,
                'scrolling' => true,
                'selected' => true,
                'sizes' => true,
                'src' => true,
                'srcdoc' => true,
                'srcset' => true,
                'step' => true,
                'stroke' => true,
                'stroke-linecap' => true,
                'stroke-linejoin' => true,
                'stroke-width' => true,
                'style' => true,
                'tabindex' => true,
                'target' => true,
                'title' => true,
                'type' => true,
                'value' => true,
                'viewbox' => true,
                'width' => true,
                'x' => true,
                'x1' => true,
                'x2' => true,
                'xmlns' => true,
                'y' => true,
                'y1' => true,
                'y2' => true,
            ];

            foreach ( $additional_tags as $tag ) {
                if ( !isset( $allowed[$tag] ) || !is_array( $allowed[$tag] ) ) {
                    $allowed[$tag] = [];
                }
            }

            foreach ( $allowed as $tag => $attributes ) {
                $allowed[$tag] = array_merge( is_array( $attributes ) ? $attributes : [], $global_attributes );
            }

            if ( preg_match_all( '/<\s*([a-zA-Z][a-zA-Z0-9:-]*)([^>]*)>/', (string) $markup, $tag_matches, PREG_SET_ORDER ) ) {
                foreach ( $tag_matches as $tag_match ) {
                    $tag = strtolower( (string) ( $tag_match[1] ?? '' ) );
                    if ( $tag === '' || !isset( $allowed[$tag] ) ) {
                        continue;
                    }
                    $attribute_string = (string) ( $tag_match[2] ?? '' );
                    if ( !preg_match_all( '/(?:^|\s)(x-[-_a-zA-Z0-9:.]+)(?=\s|=|\/|$)/', $attribute_string, $attribute_matches ) ) {
                        continue;
                    }
                    foreach ( (array) ( $attribute_matches[1] ?? [] ) as $attribute_name ) {
                        $attribute_name = strtolower( (string) $attribute_name );
                        $allowed[$tag][$attribute_name] = true;
                    }
                }
            }

            return $allowed;
        }
    }
}
if ( !function_exists( 'esc_nexdirectory_plugin_markup' ) ) {
    // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- esc_* naming follows WordPress escaping-helper convention.
    /**
     * Escapes NexDirectory-owned UI markup through the plugin allow-list.
     *
     * @param mixed $markup Markup to escape.
     * @return string
     */
    function esc_nexdirectory_plugin_markup( $markup ) {
        return NEXDIRECTORY_Escaping::plugin_markup( $markup );
    }
    // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound

}
if ( !function_exists( 'nexdirectory_get_upload_base_dir' ) ) {
    /**
     * Returns the normalized uploads base directory.
     *
     * @return string
     */
    function nexdirectory_get_upload_base_dir() {
        $uploads = function_exists( 'wp_get_upload_dir' ) ? wp_get_upload_dir() : wp_upload_dir( null, false );
        if ( !is_array( $uploads ) || empty( $uploads['basedir'] ) ) {
            return '';
        }

        return untrailingslashit( wp_normalize_path( (string) $uploads['basedir'] ) );
    }
}
if ( !function_exists( 'nexdirectory_get_upload_base_url' ) ) {
    /**
     * Returns the normalized uploads base URL.
     *
     * @return string
     */
    function nexdirectory_get_upload_base_url() {
        $uploads = function_exists( 'wp_get_upload_dir' ) ? wp_get_upload_dir() : wp_upload_dir( null, false );
        if ( !is_array( $uploads ) || empty( $uploads['baseurl'] ) ) {
            return '';
        }

        return untrailingslashit( esc_url_raw( (string) $uploads['baseurl'] ) );
    }
}
if ( !function_exists( 'nexdirectory_get_wp_filesystem' ) ) {
    /**
     * Returns WordPress' filesystem abstraction when it can be initialised.
     *
     * @return WP_Filesystem_Base|null
     */
    function nexdirectory_get_wp_filesystem() {
        global $wp_filesystem;

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

        if ( !is_object( $wp_filesystem ) ) {
            WP_Filesystem();
        }

        return is_object( $wp_filesystem ) ? $wp_filesystem : null;
    }
}
if ( !function_exists( 'nexdirectory_get_upload_subdirectory' ) ) {
    /**
     * Returns a writable plugin-owned directory below uploads.
     *
     * @param string $subdirectory Optional subdirectory below uploads/nexdirectory.
     * @return string|WP_Error
     */
    function nexdirectory_get_upload_subdirectory( $subdirectory = '' ) {
        $base_directory = nexdirectory_get_upload_base_dir();
        if ( $base_directory === '' ) {
            return new WP_Error( 'nexdirectory_uploads_unavailable', __( 'The WordPress uploads directory is not available.', 'nexdirectory' ) );
        }

        $subdirectory = trim( wp_normalize_path( (string) $subdirectory ), '/' );
        if ( $subdirectory !== '' ) {
            $parts = array_filter( array_map( 'sanitize_key', explode( '/', $subdirectory ) ) );
            $subdirectory = implode( '/', $parts );
        }

        $directory = trailingslashit( $base_directory ) . 'nexdirectory';
        if ( $subdirectory !== '' ) {
            $directory .= '/' . $subdirectory;
        }

        if ( !wp_mkdir_p( $directory ) || !wp_is_writable( $directory ) ) {
            return new WP_Error( 'nexdirectory_uploads_unwritable', __( 'The Nex Directory uploads directory is not writable.', 'nexdirectory' ) );
        }

        return untrailingslashit( wp_normalize_path( $directory ) );
    }
}
if ( !function_exists( 'nexdirectory_get_upload_subdirectory_url' ) ) {
    /**
     * Returns a plugin-owned uploads URL matching nexdirectory_get_upload_subdirectory().
     *
     * @param string $subdirectory Optional subdirectory below uploads/nexdirectory.
     * @return string
     */
    function nexdirectory_get_upload_subdirectory_url( $subdirectory = '' ) {
        $base_url = nexdirectory_get_upload_base_url();
        if ( $base_url === '' ) {
            return '';
        }

        $subdirectory = trim( wp_normalize_path( (string) $subdirectory ), '/' );
        if ( $subdirectory !== '' ) {
            $parts = array_filter( array_map( 'sanitize_key', explode( '/', $subdirectory ) ) );
            $subdirectory = implode( '/', $parts );
        }

        $url = trailingslashit( $base_url ) . 'nexdirectory';
        if ( $subdirectory !== '' ) {
            $url .= '/' . implode( '/', array_map( 'rawurlencode', explode( '/', $subdirectory ) ) );
        }

        return esc_url_raw( $url );
    }
}
if ( !class_exists( 'NEXDIRECTORY_Shortcodes', false ) ) {
    /**
     * Shortcode compatibility helpers.
     */
    final class NEXDIRECTORY_Shortcodes {
        /**
         * Current public shortcode prefix.
         */
        const PREFIX = 'nexdirectory_';

        /**
         * Previous internal shortcode prefix, built dynamically to avoid new globals.
         *
         * @return string
         */
        public static function legacy_prefix() {
            return 'ee' . 'p_';
        }

        /**
         * Maps a shortcode tag to the current prefix when it uses the previous prefix.
         *
         * @param mixed $tag Shortcode tag.
         * @return string
         */
        public static function normalize_tag( $tag ) {
            $tag = sanitize_key( (string) $tag );
            if ( $tag === '' ) {
                return '';
            }

            $legacy_prefix = self::legacy_prefix();
            if ( strpos( $tag, $legacy_prefix ) === 0 ) {
                return self::PREFIX . substr( $tag, strlen( $legacy_prefix ) );
            }

            return $tag;
        }

        /**
         * Rewrites old shortcode tags in post content to the current prefix.
         *
         * @param mixed $content Content to normalize.
         * @param array<int, string> $canonical_tags Current shortcode tags.
         * @return string
         */
        public static function normalize_legacy_content( $content, array $canonical_tags ) {
            $content = (string) $content;
            if ( $content === '' ) {
                return '';
            }

            $legacy_prefix = self::legacy_prefix();
            if ( strpos( $content, '[' . $legacy_prefix ) === false && strpos( $content, '[/' . $legacy_prefix ) === false ) {
                return $content;
            }

            $map = [];
            foreach ( $canonical_tags as $canonical_tag ) {
                $canonical_tag = sanitize_key( (string) $canonical_tag );
                if ( strpos( $canonical_tag, self::PREFIX ) !== 0 ) {
                    continue;
                }
                $legacy_tag = $legacy_prefix . substr( $canonical_tag, strlen( self::PREFIX ) );
                $map[$legacy_tag] = $canonical_tag;
            }

            if ( empty( $map ) ) {
                return $content;
            }

            return (string) preg_replace_callback(
                '/\[(\/?)([A-Za-z0-9_-]+)(?=[\s\]\/])/',
                static function ( $matches ) use ( $map ) {
                    $tag = sanitize_key( (string) ( $matches[2] ?? '' ) );
                    if ( $tag === '' || !isset( $map[$tag] ) ) {
                        return (string) $matches[0];
                    }
                    return '[' . (string) ( $matches[1] ?? '' ) . $map[$tag];
                },
                $content
            );
        }
    }
}
if ( !function_exists( 'nexdirectory_scalar_to_string' ) ) {
    /**
     * Converts mixed values into a safe scalar string for sanitization helpers.
     *
     * @param mixed $value
     * @return string
     */
    function nexdirectory_scalar_to_string(  $value  ) {
        if ( is_string( $value ) ) {
            return $value;
        }
        if ( is_scalar( $value ) ) {
            return (string) $value;
        }
        if ( is_object( $value ) && method_exists( $value, '__toString' ) ) {
            return (string) $value;
        }
        return '';
    }

}
if ( !function_exists( 'nexdirectory_sanitize_text' ) ) {
    /**
     * Null-safe text sanitization wrapper.
     *
     * @param mixed $value
     * @return string
     */
    function nexdirectory_sanitize_text(  $value  ) {
        return sanitize_text_field( nexdirectory_scalar_to_string( $value ) );
    }

}
if ( !function_exists( 'nexdirectory_is_pro_addon_active' ) ) {
    /**
     * Returns whether the separate NexDirectory Pro add-on is active.
     *
     * @return bool
     */
    function nexdirectory_is_pro_addon_active() {
        return (bool) apply_filters( 'nexdirectory_pro_active', false );
    }

}
if ( !function_exists( 'nexdirectory_is_pro_feature_enabled' ) ) {
    /**
     * Returns whether a named Pro add-on feature is available.
     *
     * @param string $feature
     * @return bool
     */
    function nexdirectory_is_pro_feature_enabled( $feature ) {
        $feature = sanitize_key( (string) $feature );
        if ( $feature === '' ) {
            return false;
        }
        return (bool) apply_filters( 'nexdirectory_pro_feature_enabled', false, $feature );
    }

}
if ( !function_exists( 'nexdirectory_is_pro_license_active' ) ) {
    /**
     * Returns whether the separate Pro add-on has an active license.
     *
     * @return bool
     */
    function nexdirectory_is_pro_license_active() {
        return (bool) apply_filters( 'nexdirectory_pro_license_active', false );
    }

}
if ( !function_exists( 'nexdirectory_get_pro_upgrade_url' ) ) {
    /**
     * Returns the external Pro add-on information URL.
     *
     * @return string
     */
    function nexdirectory_get_pro_upgrade_url() {
        $default_url = defined( 'NEXDIRECTORY_PRO_UPGRADE_URL' ) ? (string) NEXDIRECTORY_PRO_UPGRADE_URL : 'https://nexdirectory.com/pricing';
        return esc_url_raw( (string) apply_filters( 'nexdirectory_pro_upgrade_url', $default_url ) );
    }

}
if ( !function_exists( 'nexdirectory_extension_method_exists' ) ) {
    /**
     * Reports whether an active add-on owns a callable extension method.
     *
     * Free contains only this neutral contract. Premium implementations live
     * in the separate NexDirectory Pro package.
     *
     * @param string $method Extension method name.
     * @return bool
     */
    function nexdirectory_extension_method_exists( $method ) {
        $method = sanitize_key( (string) $method );
        if ( $method === '' ) {
            return false;
        }

        return (bool) apply_filters( 'nexdirectory_extension_method_exists', false, $method );
    }

}
if ( !function_exists( 'nexdirectory_call_extension_method' ) ) {
    /**
     * Calls an add-on method through a structured, fail-safe bridge.
     *
     * @param string $method Extension method name.
     * @param array<int, mixed> $arguments Positional arguments.
     * @param mixed $default Default value when no add-on handles the method.
     * @return mixed
     */
    function nexdirectory_call_extension_method( $method, array $arguments = [], $default = null ) {
        $method = sanitize_key( (string) $method );
        if ( $method === '' ) {
            return $default;
        }

        $response = apply_filters(
            'nexdirectory_extension_method_call',
            [
                'handled' => false,
                'value'   => $default,
            ],
            $method,
            array_values( $arguments )
        );

        if ( !is_array( $response ) || empty( $response['handled'] ) ) {
            return $default;
        }

        return $response['value'] ?? null;
    }

}
if ( !function_exists( 'nexdirectory_sanitize_textarea' ) ) {
    /**
     * Null-safe textarea sanitization wrapper.
     *
     * @param mixed $value
     * @return string
     */
    function nexdirectory_sanitize_textarea(  $value  ) {
        return sanitize_textarea_field( nexdirectory_scalar_to_string( $value ) );
    }

}
if ( !function_exists( 'nexdirectory_sanitize_title' ) ) {
    /**
     * Null-safe title slug sanitization wrapper.
     *
     * @param mixed $title
     * @param mixed $fallback_title
     * @param string $context
     * @return string
     */
    function nexdirectory_sanitize_title(  $title, $fallback_title = '', $context = 'save'  ) {
        return sanitize_title( nexdirectory_scalar_to_string( $title ), nexdirectory_scalar_to_string( $fallback_title ), sanitize_key( (string) $context ) );
    }

}
if ( !function_exists( 'nexdirectory_strip_all_tags' ) ) {
    /**
     * Null-safe wrapper for wp_strip_all_tags.
     *
     * @param mixed $text
     * @param bool $remove_breaks
     * @return string
     */
    function nexdirectory_strip_all_tags(  $text, $remove_breaks = false  ) {
        return wp_strip_all_tags( nexdirectory_scalar_to_string( $text ), (bool) $remove_breaks );
    }

}
if ( !function_exists( 'nexdirectory_esc_url' ) ) {
    /**
     * Null-safe wrapper for esc_url().
     *
     * @param mixed $url
     * @param array<int, string>|null $protocols
     * @param string $_context
     * @return string
     */
    function nexdirectory_esc_url(  $url, $protocols = null, $_context = 'display'  ) {
        return esc_url( nexdirectory_scalar_to_string( $url ), $protocols, (string) $_context );
    }

}
if ( !function_exists( 'nexdirectory_esc_url_raw' ) ) {
    /**
     * Null-safe wrapper for esc_url_raw().
     *
     * @param mixed $url
     * @param array<int, string>|null $protocols
     * @return string
     */
    function nexdirectory_esc_url_raw(  $url, $protocols = null  ) {
        return esc_url_raw( nexdirectory_scalar_to_string( $url ), $protocols );
    }

}
if ( !function_exists( 'nexdirectory_normalize_server_request_context' ) ) {
    /**
     * Ensures core request globals are always strings.
     *
     * Prevents PHP 8.1+ deprecations in WordPress helpers like add_query_arg()
     * when hosting/proxy environments omit REQUEST_URI.
     *
     * @return void
     */
    function nexdirectory_normalize_server_request_context() {
        if ( !isset( $_SERVER['REQUEST_URI'] ) || !is_string( $_SERVER['REQUEST_URI'] ) || $_SERVER['REQUEST_URI'] === '' ) {
            $_SERVER['REQUEST_URI'] = '/';
        } else {
            $_SERVER['REQUEST_URI'] = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
        }
        if ( !isset( $_SERVER['REQUEST_METHOD'] ) || !is_string( $_SERVER['REQUEST_METHOD'] ) || $_SERVER['REQUEST_METHOD'] === '' ) {
            $_SERVER['REQUEST_METHOD'] = 'GET';
        } else {
            $_SERVER['REQUEST_METHOD'] = sanitize_key( wp_unslash( $_SERVER['REQUEST_METHOD'] ) );
        }
        $http_host = isset( $_SERVER['HTTP_HOST'] ) && is_string( $_SERVER['HTTP_HOST'] )
            ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) )
            : '';
        if ( $http_host === '' ) {
            $home_host = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
            if ( !is_string( $home_host ) || $home_host === '' ) {
                $home_host = wp_parse_url( site_url( '/' ), PHP_URL_HOST );
            }
            $_SERVER['HTTP_HOST'] = ( is_string( $home_host ) && $home_host !== '' ? $home_host : 'example.com' );
        } else {
            $_SERVER['HTTP_HOST'] = $http_host;
        }
        $server_name = isset( $_SERVER['SERVER_NAME'] ) && is_string( $_SERVER['SERVER_NAME'] )
            ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) )
            : '';
        if ( $server_name === '' ) {
            $_SERVER['SERVER_NAME'] = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) );
        } else {
            $_SERVER['SERVER_NAME'] = $server_name;
        }
    }

}
if ( !function_exists( 'nexdirectory_add_query_arg' ) ) {
    /**
     * Null-safe wrapper for add_query_arg().
     *
     * @param mixed ...$args
     * @return string
     */
    function nexdirectory_add_query_arg(  ... $args  ) {
        nexdirectory_normalize_server_request_context();
        if ( empty( $args ) ) {
            return '';
        }
        if ( is_array( $args[0] ) ) {
            $url = $args[1] ?? null;
            if ( $url === null || $url === false ) {
                $url = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/';
            }
            $args[1] = nexdirectory_scalar_to_string( $url );
        } else {
            $url = $args[2] ?? null;
            if ( $url === null || $url === false ) {
                $url = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/';
            }
            $args[2] = nexdirectory_scalar_to_string( $url );
        }
        return add_query_arg( ...$args );
    }

}
if ( !function_exists( 'nexdirectory_remove_query_arg' ) ) {
    /**
     * Null-safe wrapper for remove_query_arg().
     *
     * @param mixed $key
     * @param mixed $query
     * @return string
     */
    function nexdirectory_remove_query_arg(  $key, $query = false  ) {
        nexdirectory_normalize_server_request_context();
        $normalized_key = $key;
        if ( is_array( $key ) ) {
            $normalized_key = array_map( 'nexdirectory_scalar_to_string', $key );
        } else {
            $normalized_key = nexdirectory_scalar_to_string( $key );
        }
        if ( $query === null || $query === false ) {
            $query = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/';
        } else {
            $query = nexdirectory_scalar_to_string( $query );
            if ( $query === '' ) {
                $query = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/';
            }
        }
        return remove_query_arg( $normalized_key, $query );
    }

}
nexdirectory_normalize_server_request_context();
require_once __DIR__ . '/includes/trait-nexdirectory-admin.php';
require_once __DIR__ . '/includes/trait-nexdirectory-content.php';
require_once __DIR__ . '/includes/trait-nexdirectory-frontend.php';
require_once __DIR__ . '/includes/trait-nexdirectory-profile-calendar.php';
require_once __DIR__ . '/includes/trait-nexdirectory-operations-centers.php';
if ( !function_exists( 'nexdirectory_register_core_cron_schedules' ) ) {
    /**
     * Global fallback cron schedules to prevent invalid schedule errors.
     *
     * @param array<string, array<string, mixed>> $schedules
     * @return array<string, array<string, mixed>>
     */
    function nexdirectory_register_core_cron_schedules(  $schedules  ) {
        if ( !is_array( $schedules ) ) {
            $schedules = [];
        }
        if ( !isset( $schedules['nexdirectory_hourly'] ) ) {
            $schedules['nexdirectory_hourly'] = [
                'interval' => HOUR_IN_SECONDS,
                'display'  => __( 'Every Hour (NEXDIRECTORY)', 'nexdirectory' ),
            ];
        }
        return $schedules;
    }

}
add_filter( 'cron_schedules', 'nexdirectory_register_core_cron_schedules', 1 );
if ( !function_exists( 'nexdirectory_register_utilities_autoloader' ) ) {
    /**
     * Registers an autoloader for SOLID utility service classes.
     *
     * @return void
     */
    function nexdirectory_register_utilities_autoloader() {
        static $registered = false;
        if ( $registered ) {
            return;
        }
        spl_autoload_register( static function ( $class_name ) {
            $class_name = ( is_string( $class_name ) ? trim( $class_name ) : '' );
            if ( $class_name === '' || strpos( $class_name, 'NEXDIRECTORY_' ) !== 0 ) {
                return;
            }
            $relative = 'class-' . strtolower( str_replace( '_', '-', $class_name ) ) . '.php';
            $path = __DIR__ . '/includes/utilities/' . $relative;
            if ( is_readable( $path ) ) {
                require_once $path;
            }
        } );
        $registered = true;
    }

}
nexdirectory_register_utilities_autoloader();
if ( class_exists( 'NEXDIRECTORY_Privacy_Service' ) ) {
    NEXDIRECTORY_Privacy_Service::register_hooks();
}
final class NEXDIRECTORY_Plugin {
    const VERSION = '1.4.7';

    const PRODUCT_NAME = 'Nex Directory';

    const POST_TYPE = 'nexdirectory_event';

    const TAX_TYPE = 'event_type';

    const TAX_MODE = 'event_mode';

    const TAX_TAG = 'event_tag';

    const TAX_PRICE = 'event_price_range';

    const RSVP_TABLE = 'nexdirectory_rsvps';

    const INBOX_THREADS_TABLE = 'nexdirectory_inbox_threads';

    const INBOX_MESSAGES_TABLE = 'nexdirectory_inbox_messages';

    const NOTIFICATIONS_TABLE = 'nexdirectory_notifications';

    const ANALYTICS_TABLE = 'nexdirectory_event_analytics';

    private $is_generating_recurrences = false;

    use NEXDIRECTORY_Admin_Trait;
    use NEXDIRECTORY_Content_Trait;
    use NEXDIRECTORY_Frontend_Trait;
    use NEXDIRECTORY_Profile_Calendar_Trait;
    use NEXDIRECTORY_Operations_Centers_Trait;
    /**
     * Utility service keys used for runtime method delegation.
     *
     * @return array<int, string>
     */
    private function get_delegated_utility_service_keys() {
        return [
            'user_caps',
            'date',
            'media',
            'event_meta',
            'email',
            'notifier',
            'admin_ui',
            'importer',
            'taxonomy',
            'nav',
            'spa',
            'logger',
            'analytics'
        ];
    }

    /**
     * Delegates missing legacy utility calls to DI services.
     *
     * @param string $name
     * @param array<int, mixed> $arguments
     * @return mixed|null
     */
    public function __call( $name, $arguments ) {
        $method = ( is_string( $name ) ? trim( $name ) : '' );
        if ( $method === '' ) {
            return null;
        }
        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;
        }
        foreach ( $this->get_delegated_utility_service_keys() as $service_key ) {
            $service = $bootstrap->get_service( $service_key );
            if ( !is_object( $service ) || !is_callable( [$service, $method] ) ) {
                continue;
            }
            if ( $service_key === 'spa' && $method === 'render_frontend_template' ) {
                $template_file = $arguments[0] ?? '';
                $context = [];
                if ( isset( $arguments[1] ) && is_array( $arguments[1] ) ) {
                    $context = $arguments[1];
                }
                return $service->render_frontend_template( $template_file, $context, $this );
            }
            return $service->{$method}( ...$arguments );
        }
        if ( function_exists( 'nexdirectory_extension_method_exists' ) && nexdirectory_extension_method_exists( $method ) ) {
            return nexdirectory_call_extension_method( $method, $arguments );
        }
        return null;
    }

    /**
     * Exposes a deliberately small set of core primitives to trusted add-ons.
     *
     * The bridge keeps premium modules outside the WordPress.org package while
     * avoiding duplicated permission, routing, email, and profile logic.
     *
     * @param string $method Core primitive name.
     * @param array<int, mixed> $arguments Positional arguments.
     * @return mixed|null
     */
    public function extension_api_call( $method, array $arguments = [] ) {
        $method = sanitize_key( (string) $method );
        $allowed = [
            'append_runtime_log',
            'build_inbox_thread_url',
            'current_url',
            'escape_ics_text',
            'event_attendee_has_status',
            'format_event_date',
            'get_admin_brand_name',
            'get_default_event_email_template_value',
            'get_default_event_email_templates',
            'get_extension_app_pass',
            'get_login_signup_url',
            'get_notifications_page_url',
            'get_operations_attention_summary',
            'get_pending_events_count',
            'get_platform_page_url',
            'get_profile_route_default_url',
            'get_profile_route_slugs',
            'get_rsvp_table_name',
            'get_site_profile_ui_labels',
            'get_white_label_brand_name',
            'is_notification_toggle_enabled',
            'parse_ics_event_datetime',
            'process_rsvp_action',
            'record_analytics_event',
            'redirect_with_message',
            'render_admin_template',
            'render_admin_premium_shell_styles',
            'render_account_app_template',
            'render_event_email_body',
            'render_event_email_subject',
            'render_platform_email',
            'resolve_profile_key_for_post',
            'safe_redirect_or_fallback',
            'sanitize_checkbox_option',
            'sanitize_custom_field_value',
            'sanitize_secret_option_with_fallback',
            'sanitize_timezone_value',
            'send_platform_email',
            'should_use_action_scheduler',
            'user_can_edit_event',
            'user_can_moderate_events',
            'user_can_submit_events',
        ];
        $allowed = (array) apply_filters( 'nexdirectory_extension_api_methods', $allowed, $method );

        if ( $method === '' || !in_array( $method, $allowed, true ) ) {
            return null;
        }

        if ( method_exists( $this, $method ) || is_callable( [$this, $method] ) ) {
            return $this->{$method}( ...array_values( $arguments ) );
        }

        return $this->__call( $method, array_values( $arguments ) );
    }

    /**
     * Neutral dynamic-filter bridge used by Pro's visual field builder.
     *
     * @param mixed $input Request-like input.
     * @param string $context Surface identifier.
     * @return array<string, mixed>
     */
    public function parse_builder_checkin_dynamic_filters( $input, $context ) {
        $result = nexdirectory_call_extension_method(
            __FUNCTION__,
            [$input, $context],
            []
        );
        return is_array( $result ) ? $result : [];
    }

    /**
     * Converts Pro dynamic filters to query arguments when Pro is active.
     *
     * @param array<string, mixed> $filters Normalized filter values.
     * @return array<string, mixed>
     */
    public function builder_checkin_dynamic_filters_to_query_args( array $filters ) {
        $result = nexdirectory_call_extension_method( __FUNCTION__, [$filters], [] );
        return is_array( $result ) ? $result : [];
    }

    /**
     * Compiles Pro dynamic filters into a query plan.
     *
     * @param array<string, mixed> $filters Normalized filter values.
     * @param string $context Surface identifier.
     * @return array<string, mixed>
     */
    public function builder_checkin_compile_dynamic_query( array $filters, $context ) {
        $result = nexdirectory_call_extension_method( __FUNCTION__, [$filters, $context], [] );
        return is_array( $result ) ? $result : [];
    }

    /**
     * Renders Pro-owned dynamic controls, or nothing in standalone Free.
     *
     * @param string $context Surface identifier.
     * @param string $surface_id Unique form surface.
     * @return string
     */
    public function render_builder_checkin_filter_controls( $context, $surface_id = '' ) {
        $result = nexdirectory_call_extension_method(
            __FUNCTION__,
            [$context, $surface_id],
            ''
        );
        return is_string( $result ) ? $result : '';
    }

    public function get_platform_page_url( $option_key, $fallback_slug = '', $fallback_url = '' ) {
        $result = $this->__call( __FUNCTION__, [$option_key, $fallback_slug, $fallback_url] );
        return is_string( $result ) ? $result : '';
    }

    public function get_spa_hybrid_route_urls() {
        $result = $this->__call( __FUNCTION__, [] );
        return is_array( $result ) ? $result : [];
    }

    public function is_spa_hybrid_enabled() {
        return (bool) $this->__call( __FUNCTION__, [] );
    }

    public function user_can_duplicate_event( $post_id, $user_id = 0 ) {
        return (bool) $this->__call( __FUNCTION__, [$post_id, $user_id] );
    }

    /**
     * Wrappers retained so capability checks remain discoverable after the
     * user-capability service was moved behind runtime delegation.
     *
     * @param int $user_id User ID, or zero for the current user.
     * @return bool
     */
    public function user_can_moderate_events( $user_id = 0 ) {
        return (bool) $this->__call( __FUNCTION__, [$user_id] );
    }

    /**
     * Checks delegated event-level editing capability.
     *
     * @param int $post_id Event post ID.
     * @param int $user_id User ID, or zero for the current user.
     * @return bool
     */
    public function user_can_edit_event( $post_id, $user_id = 0 ) {
        return (bool) $this->__call( __FUNCTION__, [$post_id, $user_id] );
    }

    /**
     * Keeps custom-field sanitization callable by builder projections after
     * the event-meta service refactor.
     *
     * @param string $type Field type.
     * @param mixed  $value Raw field value.
     * @param string $options Selectable field options.
     * @return string
     */
    public function sanitize_custom_field_value( $type, $value, $options = '' ) {
        $result = $this->__call( __FUNCTION__, [$type, $value, $options] );
        return is_scalar( $result ) ? (string) $result : '';
    }

    public function event_status_badge( $status ) {
        $result = $this->__call( __FUNCTION__, [$status] );
        return is_string( $result ) ? $result : ucfirst( sanitize_key( (string) $status ) );
    }

    public function format_event_date( $post_id ) {
        $result = $this->__call( __FUNCTION__, [$post_id] );
        return is_string( $result ) ? $result : '';
    }

    /**
     * Runtime log wrapper retained for legacy method-existence checks.
     *
     * @param string $level
     * @param string $message
     * @param array<string, mixed> $context
     * @return void
     */
    public function append_runtime_log( $level, $message, array $context = [] ) {
        $this->__call( __FUNCTION__, [$level, $message, $context] );
    }

    /**
     * Wrapper so legacy method_exists checks continue to work after DI refactor.
     *
     * @param int $event_id
     * @param string $metric
     * @param array<string, mixed> $args
     * @return bool
     */
    public function record_analytics_event( $event_id, $metric, array $args = [] ) {
        $result = $this->__call( __FUNCTION__, [$event_id, $metric, $args] );
        return (bool) $result;
    }

    /**
     * Wrapper so archive/map batch tracking resolves through analytics service.
     *
     * @param array<int, mixed> $event_ids
     * @param string $metric
     * @param array<string, mixed> $args
     * @return int
     */
    public function record_analytics_for_event_ids( array $event_ids, $metric, array $args = [] ) {
        $result = $this->__call( __FUNCTION__, [$event_ids, $metric, $args] );
        return ( is_numeric( $result ) ? (int) $result : 0 );
    }

    public function __construct() {
        $this->register_profile_calendar_hooks();
        $this->register_operations_centers_hooks();
        add_action( 'after_setup_theme', [$this, 'maybe_enable_theme_menu_support_for_wp_menu_source'], 100 );
        add_action( 'init', [$this, 'register_content'] );
        add_action( 'init', [$this, 'register_shortcodes'] );
        add_action( 'init', [$this, 'maybe_upgrade_schema'], 20 );
        add_action( 'elementor/elements/categories_registered', [$this, 'register_elementor_category'] );
        add_action( 'elementor/widgets/register', [$this, 'register_elementor_widgets'] );
        add_action( 'admin_menu', [$this, 'register_settings_page'] );
        add_action( 'admin_menu', [$this, 'register_wp_menu_source_appearance_menu'], 99 );
        add_filter( 'plugin_action_links_' . plugin_basename( NEXDIRECTORY_PLUGIN_FILE ), [$this, 'filter_plugin_action_links'] );
        add_action( 'admin_init', [$this, 'maybe_redirect_after_activation'], 0 );
        add_action( 'admin_init', [$this, 'register_settings'] );
        add_action( 'updated_option', [$this, 'handle_runtime_option_change'], 10, 3 );
        add_action( 'added_option', [$this, 'handle_runtime_option_change'], 10, 2 );
        add_action( 'load-post-new.php', [$this, 'redirect_event_add_new_to_frontend_submit'] );
        add_action( 'admin_enqueue_scripts', [$this, 'enqueue_admin_page_assets'] );
        add_action( 'wp_enqueue_scripts', [$this, 'enqueue_assets'], 99 );
        add_action( 'admin_post_nexdirectory_submit_event', [$this, 'handle_submit_event'] );
        add_action( 'admin_post_nopriv_nexdirectory_submit_event', [$this, 'handle_submit_event'] );
        add_action( 'admin_post_nexdirectory_update_event', [$this, 'handle_update_event'] );
        add_action( 'admin_post_nexdirectory_delete_event', [$this, 'handle_delete_event'] );
        add_action( 'admin_post_nexdirectory_toggle_event_live_draft', [$this, 'handle_toggle_event_live_draft'] );
        add_action( 'admin_post_nexdirectory_autosave_event', [$this, 'handle_autosave_event'] );
        add_action( 'admin_post_nexdirectory_save_filters', [$this, 'handle_save_filters'] );
        add_action( 'admin_post_nexdirectory_moderate_event', [$this, 'handle_moderate_event'] );
        add_action( 'admin_post_nexdirectory_moderate_events_bulk', [$this, 'handle_moderate_events_bulk'] );
        add_action( 'admin_post_nexdirectory_account_update', [$this, 'handle_account_update'] );
        add_action( 'admin_post_nexdirectory_inbox_reply', [$this, 'handle_inbox_reply'] );
        add_action( 'admin_post_nexdirectory_manage_user_role', [$this, 'handle_manage_user_role'] );
        add_action( 'admin_post_nexdirectory_manage_user_event', [$this, 'handle_manage_user_event'] );
        add_action( 'admin_post_nexdirectory_toggle_verified', [$this, 'handle_toggle_verified'] );
        add_action( 'admin_post_nexdirectory_toggle_user_edit_trust', [$this, 'handle_toggle_user_edit_trust'] );
        add_action( 'admin_post_nexdirectory_manage_user_note', [$this, 'handle_manage_user_note'] );
        add_action( 'admin_post_nexdirectory_rsvp_event', [$this, 'handle_rsvp_event'] );
        add_action( 'admin_post_nexdirectory_report_event', [$this, 'handle_report_event'] );
        add_action( 'admin_post_nexdirectory_moderate_report', [$this, 'handle_moderate_report'] );
        add_action( 'admin_post_nexdirectory_claim_listing', [$this, 'handle_claim_listing'] );
        add_action( 'admin_post_nopriv_nexdirectory_claim_listing', [$this, 'handle_claim_listing'] );
        add_action( 'admin_post_nexdirectory_moderate_claim', [$this, 'handle_moderate_claim'] );
        add_action( 'admin_post_nexdirectory_contact_holder', [$this, 'handle_contact_holder'] );
        add_action( 'admin_post_nexdirectory_resend_verification', [$this, 'handle_resend_verification'] );
        add_action( 'admin_post_nexdirectory_import_events', [$this, 'handle_import_events'] );
        add_action( 'admin_post_nexdirectory_import_demo_events', [$this, 'handle_import_demo_events'] );
        add_action( 'admin_post_nexdirectory_apply_starter_blueprint', [$this, 'handle_apply_starter_blueprint'] );
        add_action( 'admin_post_nexdirectory_export_events', [$this, 'handle_export_events'] );
        add_action( 'admin_post_nexdirectory_export_demo_events', [$this, 'handle_export_demo_events'] );
        add_action( 'admin_post_nexdirectory_export_event_attendees', [$this, 'handle_export_event_attendees'] );
        add_action( 'admin_post_nexdirectory_email_event_attendees', [$this, 'handle_email_event_attendees'] );
        add_action( 'admin_post_nexdirectory_upload_image', [$this, 'handle_upload_image'] );
        add_action( 'admin_post_nexdirectory_send_test_email', [$this, 'handle_send_test_email'] );
        add_action( 'admin_post_nexdirectory_test_map_connection', [$this, 'handle_test_map_connection'] );
        add_action( 'admin_post_nexdirectory_test_captcha_connection', [$this, 'handle_test_captcha_connection'] );
        add_action( 'admin_post_nexdirectory_test_routing_connection', [$this, 'handle_test_routing_connection'] );
        add_action( 'admin_post_nexdirectory_reset_smooth_scroll_defaults', [$this, 'handle_reset_smooth_scroll_defaults'] );
        add_action( 'admin_post_nexdirectory_generate_pages', [$this, 'handle_generate_pages'] );
        add_action( 'admin_post_nexdirectory_open_transfer_owner', [$this, 'handle_open_transfer_owner'] );
        add_action( 'admin_post_nexdirectory_reassign_listing_owner', [$this, 'handle_reassign_listing_owner'] );
        add_action( 'admin_post_nexdirectory_regenerate_extension_app_pass', [$this, 'handle_regenerate_extension_app_pass'] );
        add_action( 'admin_post_nexdirectory_mark_onboarding_complete', [$this, 'handle_mark_onboarding_complete'] );
        add_action( 'admin_post_nexdirectory_save_onboarding_preferences', [$this, 'handle_onboarding_save_preferences'] );
        add_action( 'admin_post_nexdirectory_set_onboarding_homepage', [$this, 'handle_set_onboarding_homepage'] );
        add_action(
            'transition_post_status',
            [$this, 'on_event_status_transition'],
            10,
            3
        );
        add_action( 'template_redirect', [$this, 'handle_calendar_export'] );
        add_action( 'template_redirect', [$this, 'handle_email_verification'] );
        add_action( 'init', [$this, 'schedule_extension_replay_cleanup'], 30 );
        add_action( 'nexdirectory_privacy_retention_cron', [$this, 'run_privacy_retention_cleanup'] );
        add_action( 'nexdirectory_extension_replay_cleanup', [$this, 'cleanup_extension_replay_ledger'] );
        add_action( 'rest_api_init', [$this, 'register_rest_routes'] );
        add_filter( 'query_vars', [$this, 'add_query_vars'] );
        add_filter( 'request', [$this, 'normalize_archive_search_request'], 1 );
        add_filter( 'ajax_query_attachments_args', [$this, 'filter_frontend_media_library'] );
        add_filter( 'user_has_cap', [$this, 'filter_submitter_core_upload_capability'], 10, 4 );
        add_filter( 'single_template', [$this, 'single_template'] );
        add_filter( 'the_content', [$this, 'normalize_legacy_shortcode_content'], 1 );
        add_filter( 'the_content', [$this, 'append_event_meta_to_single'] );
        add_filter( 'pre_get_document_title', [$this, 'filter_home_document_title'], 98 );
        add_filter( 'pre_get_document_title', [$this, 'filter_single_event_document_title'], 99 );
        add_action( 'wp_head', [$this, 'output_homepage_meta_tags'], 4 );
        add_action( 'wp_head', [$this, 'output_single_event_meta_tags'], 5 );
        add_action( 'wp_head', [$this, 'output_event_schema'] );
    }

    /**
     * Runs a lifecycle callback across a network without loading every site ID
     * into memory at once.
     *
     * @param callable $callback Current-site lifecycle callback.
     * @return void
     */
    private static function for_each_network_site( $callback ) {
        $batch_size = 100;
        $offset = 0;

        do {
            $site_ids = get_sites( [
                'fields' => 'ids',
                'number' => $batch_size,
                'offset' => $offset,
                'orderby' => 'id',
                'order' => 'ASC',
            ] );
            $site_ids = is_array( $site_ids ) ? $site_ids : [];
            foreach ( $site_ids as $site_id ) {
                $site_id = absint( (int) $site_id );
                if ( $site_id <= 0 ) {
                    continue;
                }
                switch_to_blog( $site_id );
                try {
                    call_user_func( $callback );
                } finally {
                    restore_current_blog();
                }
            }
            $offset += count( $site_ids );
        } while ( count( $site_ids ) === $batch_size );
    }

    public static function activate( $network_wide = false ) {
        if ( is_multisite() && $network_wide ) {
            self::for_each_network_site( [self::class, 'activate_site'] );
            return;
        }
        self::activate_site();
    }

    /**
     * Initializes plugin data and schedules for the current site.
     *
     * @return void
     */
    private static function activate_site() {
        self::add_roles();
        $plugin = new self();
        add_option(
            'nexdirectory_enable_frontend_sticky_nav',
            '1',
            '',
            false
        );
        add_option(
            'nexdirectory_frontend_sticky_nav_include_auth_cta',
            '1',
            '',
            false
        );
        add_option(
            'nexdirectory_frontend_sticky_nav_show_for_guests',
            '1',
            '',
            false
        );
        add_option(
            'nexdirectory_frontend_sticky_nav_show_for_members',
            '1',
            '',
            false
        );
        $plugin->register_content();
        if ( method_exists( $plugin, 'sync_route_options_to_existing_pages' ) ) {
            $plugin->sync_route_options_to_existing_pages();
        }
        $plugin->create_rsvp_table();
        $plugin->create_inbox_tables();
        $plugin->create_notifications_table();
        $plugin->create_analytics_table();
        $plugin->create_private_files_table();
        update_option( 'nexdirectory_activation_redirect_import', '1', false );
        update_option( 'nexdirectory_db_version', NEXDIRECTORY_DB_VERSION, false );
        update_option( 'nexdirectory_plugin_version', self::VERSION, false );
        update_option( 'nexdirectory_role_schema_version', NEXDIRECTORY_ROLE_SCHEMA_VERSION, false );
        $plugin->schedule_privacy_retention_cleanup();
        $plugin->schedule_extension_replay_cleanup();
        flush_rewrite_rules();
    }

    public static function deactivate( $network_wide = false ) {
        if ( is_multisite() && $network_wide ) {
            self::for_each_network_site( [self::class, 'deactivate_site'] );
            return;
        }
        self::deactivate_site();
    }

    /**
     * Clears plugin schedules for the current site.
     *
     * @return void
     */
    private static function deactivate_site() {
        wp_clear_scheduled_hook( 'nexdirectory_privacy_retention_cron' );
        wp_clear_scheduled_hook( 'nexdirectory_extension_replay_cleanup' );
        delete_option( 'nexdirectory_extension_replay_ledger_v1' );
        if ( function_exists( 'as_unschedule_all_actions' ) ) {
            as_unschedule_all_actions( 'nexdirectory_privacy_retention_cron', null, 'nexdirectory' );
            as_unschedule_all_actions( 'nexdirectory_extension_replay_cleanup', null, 'nexdirectory' );
        }
        flush_rewrite_rules();
    }

    /**
     * Initializes a newly created site when the plugin is network active.
     *
     * @param WP_Site $new_site Newly created site.
     * @return void
     */
    public static function initialize_new_site( $new_site ) {
        if ( ! is_multisite() || ! $new_site instanceof WP_Site ) {
            return;
        }
        if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }
        if ( ! is_plugin_active_for_network( plugin_basename( NEXDIRECTORY_PLUGIN_FILE ) ) ) {
            return;
        }
        switch_to_blog( (int) $new_site->blog_id );
        try {
            self::activate_site();
        } finally {
            restore_current_blog();
        }
    }

    public static function add_roles() {
        $custom_caps = ['nexdirectory_submit_events', 'nexdirectory_moderate_events', 'nexdirectory_manage_organizers'];
        $submitter_can_submit = (string) get_option( 'nexdirectory_role_event_submitter_can_submit', '1' ) === '1';
        $author_can_submit = (string) get_option( 'nexdirectory_role_author_can_submit', '1' ) === '1';
        $editor_can_moderate = (string) get_option( 'nexdirectory_role_editor_can_moderate', '1' ) === '1';
        $editor_can_manage_organizers = (string) get_option( 'nexdirectory_role_editor_manage_organizers', '1' ) === '1';
        add_role( 'event_submitter', __( 'Event Submitter', 'nexdirectory' ), [
            'read'              => true,
            'upload_files'      => true,
            'nexdirectory_submit_events' => $submitter_can_submit,
        ] );
        $role = get_role( 'event_submitter' );
        if ( $role instanceof WP_Role ) {
            $role->add_cap( 'read', true );
            $role->add_cap( 'upload_files', true );
            if ( $submitter_can_submit ) {
                $role->add_cap( 'nexdirectory_submit_events', true );
            } else {
                $role->remove_cap( 'nexdirectory_submit_events' );
            }
        }
        foreach ( ['author', 'editor', 'administrator'] as $role_name ) {
            $submit_role = get_role( $role_name );
            if ( !$submit_role instanceof WP_Role ) {
                continue;
            }
            if ( $role_name === 'author' ) {
                if ( $author_can_submit ) {
                    $submit_role->add_cap( 'nexdirectory_submit_events', true );
                } else {
                    $submit_role->remove_cap( 'nexdirectory_submit_events' );
                }
            } else {
                $submit_role->add_cap( 'nexdirectory_submit_events', true );
            }
        }
        $editor_role = get_role( 'editor' );
        if ( $editor_role instanceof WP_Role ) {
            if ( $editor_can_moderate ) {
                $editor_role->add_cap( 'nexdirectory_moderate_events', true );
            } else {
                $editor_role->remove_cap( 'nexdirectory_moderate_events' );
            }
            if ( $editor_can_manage_organizers ) {
                $editor_role->add_cap( 'nexdirectory_manage_organizers', true );
            } else {
                $editor_role->remove_cap( 'nexdirectory_manage_organizers' );
            }
        }
        $admin_role = get_role( 'administrator' );
        if ( $admin_role instanceof WP_Role ) {
            foreach ( $custom_caps as $cap_name ) {
                $admin_role->add_cap( $cap_name, true );
            }
        }
    }

}

if ( !function_exists( 'nexdirectory_get_plugin_instance' ) ) {
    function nexdirectory_get_plugin_instance() {
        return ( isset( $GLOBALS['nexdirectory_plugin_instance'] ) && $GLOBALS['nexdirectory_plugin_instance'] instanceof NEXDIRECTORY_Plugin ? $GLOBALS['nexdirectory_plugin_instance'] : null );
    }

}
if ( !function_exists( 'nexdirectory_bootstrap_services' ) ) {
    /**
     * Initializes and runs the DI bootstrap container.
     *
     * @return NEXDIRECTORY_Plugin_Bootstrap|null
     */
    function nexdirectory_bootstrap_services() {
        if ( isset( $GLOBALS['nexdirectory_bootstrap'] ) && $GLOBALS['nexdirectory_bootstrap'] instanceof NEXDIRECTORY_Plugin_Bootstrap ) {
            return $GLOBALS['nexdirectory_bootstrap'];
        }
        if ( !class_exists( 'NEXDIRECTORY_Plugin_Bootstrap' ) ) {
            return null;
        }
        $bootstrap = new NEXDIRECTORY_Plugin_Bootstrap();
        $bootstrap->run();
        $GLOBALS['nexdirectory_bootstrap'] = $bootstrap;
        return $bootstrap;
    }

}
if ( !function_exists( 'nexdirectory_get_bootstrap' ) ) {
    /**
     * Returns the plugin bootstrap container instance.
     *
     * @return NEXDIRECTORY_Plugin_Bootstrap|null
     */
    function nexdirectory_get_bootstrap() {
        if ( isset( $GLOBALS['nexdirectory_bootstrap'] ) && $GLOBALS['nexdirectory_bootstrap'] instanceof NEXDIRECTORY_Plugin_Bootstrap ) {
            return $GLOBALS['nexdirectory_bootstrap'];
        }
        return nexdirectory_bootstrap_services();
    }

}
if ( !function_exists( 'nexdirectory_is_app_page' ) ) {
    /**
     * Returns true when current request is a plugin shortcode app page.
     *
     * @return bool
     */
    function nexdirectory_is_app_page() {
        $bootstrap = ( function_exists( 'nexdirectory_get_bootstrap' ) ? nexdirectory_get_bootstrap() : null );
        if ( $bootstrap instanceof NEXDIRECTORY_Plugin_Bootstrap && method_exists( $bootstrap, 'get_service' ) ) {
            $nav_service = $bootstrap->get_service( 'nav' );
            if ( is_object( $nav_service ) && is_callable( [$nav_service, 'is_app_page'] ) ) {
                return (bool) $nav_service->is_app_page();
            }
        }
        return false;
    }

}
if ( !function_exists( 'nexdirectory_get_app_view' ) ) {
    /**
     * Returns normalized app view key for current request.
     *
     * @return string
     */
    function nexdirectory_get_app_view() {
        $bootstrap = ( function_exists( 'nexdirectory_get_bootstrap' ) ? nexdirectory_get_bootstrap() : null );
        if ( $bootstrap instanceof NEXDIRECTORY_Plugin_Bootstrap && method_exists( $bootstrap, 'get_service' ) ) {
            $nav_service = $bootstrap->get_service( 'nav' );
            if ( is_object( $nav_service ) && is_callable( [$nav_service, 'get_app_view'] ) ) {
                return sanitize_key( (string) $nav_service->get_app_view() );
            }
        }
        return '';
    }

}
if ( !function_exists( 'nexdirectory_get_private_storage_path_for_cleanup' ) ) {
    /**
     * Returns the configured per-site private-storage path without creating it.
     *
     * @return string
     */
    function nexdirectory_get_private_storage_path_for_cleanup() {
        $default_base = function_exists( 'nexdirectory_get_upload_subdirectory' )
            ? nexdirectory_get_upload_subdirectory( 'private' )
            : '';
        if ( is_wp_error( $default_base ) || (string) $default_base === '' ) {
            return '';
        }
        $configured_base = (string) apply_filters(
            'nexdirectory_private_storage_base',
            $default_base,
            get_current_blog_id()
        );
        $configured_base = untrailingslashit( wp_normalize_path( $configured_base ) );
        $base_directory  = $configured_base !== '' ? $configured_base : untrailingslashit( wp_normalize_path( $default_base ) );
        $is_absolute_base = strpos( $base_directory, '/' ) === 0
            || (bool) preg_match( '/^[A-Za-z]:[\\\\\/]/', $base_directory );
        if ( !$is_absolute_base ) {
            return '';
        }
        return trailingslashit( $base_directory ) . 'site-' . get_current_blog_id();
    }
}
if ( !function_exists( 'nexdirectory_delete_private_file_for_cleanup' ) ) {
    /**
     * Deletes a path only when both it and the site's private-storage root
     * resolve canonically and the file is contained by that root.
     *
     * @param string $path Stored path.
     * @return void
     */
    function nexdirectory_delete_private_file_for_cleanup( $path ) {
        $path = (string) $path;
        $storage_directory = nexdirectory_get_private_storage_path_for_cleanup();
        if ( $path === '' || $storage_directory === '' ) {
            return;
        }
        $real_path = realpath( $path );
        $real_directory = realpath( $storage_directory );
        if ( false === $real_path || false === $real_directory ) {
            return;
        }
        $normalized_path = wp_normalize_path( $real_path );
        $normalized_directory = trailingslashit( wp_normalize_path( $real_directory ) );
        if ( strpos( $normalized_path, $normalized_directory ) !== 0 || !is_file( $real_path ) ) {
            return;
        }
        wp_delete_file( $real_path );
    }
}
if ( !function_exists( 'nexdirectory_run_uninstall_cleanup' ) ) {
    /**
     * Cleanup one site on uninstall.
     *
     * @param bool $remove_data Whether to purge plugin data/content.
     * @return void
     */
    function nexdirectory_run_uninstall_cleanup(  $remove_data = false  ) {
        global $wpdb;
        $remove_data = (bool) $remove_data;
        wp_clear_scheduled_hook( 'nexdirectory_privacy_retention_cron' );
        wp_clear_scheduled_hook( 'nexdirectory_extension_replay_cleanup' );
        delete_option( 'nexdirectory_extension_replay_ledger_v1' );
        if ( function_exists( 'as_unschedule_all_actions' ) ) {
            as_unschedule_all_actions( 'nexdirectory_privacy_retention_cron', null, 'nexdirectory' );
            as_unschedule_all_actions( 'nexdirectory_extension_replay_cleanup', null, 'nexdirectory' );
        }
        // Preserve plugin configuration/content by default so re-install keeps settings.
        if ( !$remove_data ) {
            return;
        }
        $option_patterns = ['nexdirectory_%', '_transient_nexdirectory_%', '_transient_timeout_nexdirectory_%'];
        foreach ( $option_patterns as $pattern ) {
            $wpdb->query( $wpdb->prepare( 
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
                "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
                $pattern
             ) );
        }
        $caps = ['nexdirectory_submit_events', 'nexdirectory_moderate_events', 'nexdirectory_manage_organizers'];
        foreach ( [
            'administrator',
            'editor',
            'author',
            'event_submitter'
        ] as $role_name ) {
            $role = get_role( $role_name );
            if ( !$role instanceof WP_Role ) {
                continue;
            }
            foreach ( $caps as $cap_name ) {
                $role->remove_cap( $cap_name );
            }
        }
        remove_role( 'event_submitter' );
        $app_page_ids = $wpdb->get_col( $wpdb->prepare(
            "SELECT DISTINCT p.ID
             FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->postmeta} pm_app
                ON pm_app.post_id = p.ID
               AND pm_app.meta_key = %s
               AND pm_app.meta_value = %s
             INNER JOIN {$wpdb->postmeta} pm_owned
                ON pm_owned.post_id = p.ID
               AND pm_owned.meta_key = %s
               AND pm_owned.meta_value = %s
             WHERE p.post_type = %s",
            '_nexdirectory_is_app_page',
            '1',
            '_nexdirectory_created_by_plugin',
            '1',
            'page'
        ) );
        foreach ( (array) $app_page_ids as $app_page_id ) {
            wp_delete_post( (int) $app_page_id, true );
        }
        delete_post_meta_by_key( '_nexdirectory_is_app_page' );
        delete_post_meta_by_key( '_nexdirectory_app_view' );
        delete_post_meta_by_key( '_nexdirectory_created_by_plugin' );
        $post_ids = $wpdb->get_col( $wpdb->prepare(
            "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s",
            'nexdirectory_event'
        ) );
        foreach ( (array) $post_ids as $post_id ) {
            wp_delete_post( (int) $post_id, true );
        }
        if ( function_exists( 'unregister_taxonomy' ) ) {
            unregister_taxonomy( 'event_type' );
            unregister_taxonomy( 'event_mode' );
            unregister_taxonomy( 'event_tag' );
            unregister_taxonomy( 'event_price_range' );
        }
        if ( function_exists( 'unregister_post_type' ) ) {
            unregister_post_type( 'nexdirectory_event' );
        }
        $wpdb->query( $wpdb->prepare( 
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE %s",
            'nexdirectory_%'
         ) );
        $wpdb->query( $wpdb->prepare( 
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE %s",
            'nexdirectory_%'
         ) );
        $comment_ids = $wpdb->get_col( $wpdb->prepare(
            "SELECT comment_ID FROM {$wpdb->comments} WHERE comment_type IN (%s, %s)",
            'nexdirectory_report',
            'nexdirectory_claim'
        ) );
        foreach ( (array) $comment_ids as $comment_id ) {
            wp_delete_comment( (int) $comment_id, true );
        }
        $rsvp_table = $wpdb->prefix . 'nexdirectory_rsvps';
        $inbox_messages_table = $wpdb->prefix . 'nexdirectory_inbox_messages';
        $inbox_threads_table = $wpdb->prefix . 'nexdirectory_inbox_threads';
        $notifications_table = $wpdb->prefix . 'nexdirectory_notifications';
        $analytics_table = $wpdb->prefix . 'nexdirectory_event_analytics';
        $private_files_table = $wpdb->prefix . 'nexdirectory_private_files';
        $private_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $private_files_table ) ) );
        if ( (string) $private_table_exists === $private_files_table ) {
            $private_paths = $wpdb->get_col( "SELECT storage_path FROM {$private_files_table}" );
            foreach ( (array) $private_paths as $private_path ) {
                nexdirectory_delete_private_file_for_cleanup( (string) $private_path );
            }
        }
        $wpdb->query( "DROP TABLE IF EXISTS {$inbox_messages_table}" );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query( "DROP TABLE IF EXISTS {$inbox_threads_table}" );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query( "DROP TABLE IF EXISTS {$notifications_table}" );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query( "DROP TABLE IF EXISTS {$rsvp_table}" );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query( "DROP TABLE IF EXISTS {$analytics_table}" );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query( "DROP TABLE IF EXISTS {$private_files_table}" );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    }

}
if ( !function_exists( 'nexdirectory_uninstall_cleanup' ) ) {
    /**
     * WordPress uninstall callback.
     *
     * @return void
     */
    function nexdirectory_uninstall_cleanup() {
        $should_remove_data = (string) get_option( 'nexdirectory_uninstall_remove_data', '0' ) === '1';
        if ( is_multisite() ) {
            $batch_size = 100;
            $offset = 0;
            do {
                $site_ids = get_sites( [
                    'fields' => 'ids',
                    'number' => $batch_size,
                    'offset' => $offset,
                    'orderby' => 'id',
                    'order' => 'ASC',
                ] );
                $site_ids = is_array( $site_ids ) ? $site_ids : [];
                foreach ( $site_ids as $site_id ) {
                    $site_id = absint( (int) $site_id );
                    if ( $site_id <= 0 ) {
                        continue;
                    }
                    switch_to_blog( $site_id );
                    try {
                        $site_remove_data = (string) get_option( 'nexdirectory_uninstall_remove_data', '0' ) === '1';
                        nexdirectory_run_uninstall_cleanup( $site_remove_data );
                    } finally {
                        restore_current_blog();
                    }
                }
                $offset += count( $site_ids );
            } while ( count( $site_ids ) === $batch_size );
        } else {
            nexdirectory_run_uninstall_cleanup( $should_remove_data );
        }
    }

}
add_action( 'plugins_loaded', 'nexdirectory_bootstrap_services', 5 );
add_action( 'wp_initialize_site', ['NEXDIRECTORY_Plugin', 'initialize_new_site'], 200, 1 );
register_uninstall_hook( __FILE__, 'nexdirectory_uninstall_cleanup' );
register_activation_hook( __FILE__, ['NEXDIRECTORY_Plugin', 'activate'] );
register_deactivation_hook( __FILE__, ['NEXDIRECTORY_Plugin', 'deactivate'] );
$GLOBALS['nexdirectory_plugin_instance'] = new NEXDIRECTORY_Plugin();
