<?php

namespace MovieEngine\Frontend;

if (! defined("ABSPATH")) {
    exit; // Exit if accessed directly
}

class Shortcodes {

    public function __construct() {
        add_shortcode('movie_engine_grid', [$this, 'render_movie_grid']);
        add_shortcode('movie_engine_search', [$this, 'render_movie_search']);
        add_shortcode('movie_engine_library', [$this, 'render_library']);
        add_shortcode('movie_engine_search_bar', [$this, 'render_search_bar']);
        add_shortcode('movie_engine_live_search', [$this, 'render_live_search']);

        add_shortcode('movie_engine_person_list', [$this, 'render_person_list']);
        add_shortcode('movie_engine_user_menu', [$this, 'render_user_menu']);

        add_shortcode('movie_engine_genres', [$this, 'render_genres']);
        add_shortcode('movie_engine_years', [$this, 'render_years']);
        add_shortcode('movie_engine_taxonomy_terms', [$this, 'render_taxonomy_terms']);
        add_shortcode('movie_engine_taxonomies', [$this, 'render_taxonomies']);
        add_shortcode('movie_engine_hero', [$this, 'render_hero']);
    }

    /**
     * Sanitize a CSS color for inline background-color on the library container.
     *
     * @param mixed $color Raw value from shortcode/block.
     * @return string Safe color or empty string.
     */
    private static function sanitize_library_background_color($color) {
        $color = trim((string) $color);
        if ($color === '') {
            return '';
        }

        $hex = sanitize_hex_color($color);
        if ($hex) {
            return $hex;
        }

        // Block editor palette / theme.json presets.
        if (preg_match('/^var\(--[a-zA-Z0-9\-_,\s]+\)$/', $color)) {
            return $color;
        }

        if (preg_match('/^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(,\s*[\d.]+\s*)?\)$/', $color)) {
            return $color;
        }

        if (preg_match('/^hsla?\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*(,\s*[\d.]+\s*)?\)$/', $color)) {
            return $color;
        }

        return '';
    }

    /**
     * Normalize shortcode gap to a non-negative integer pixel value (no "px" suffix).
     *
     * @param mixed $gap Raw attribute.
     * @return string Empty if unset/invalid, otherwise digits only.
     */
    private static function normalize_gap_px($gap) {
        $gap = trim((string) $gap);
        if ($gap === '') {
            return '';
        }
        $gap = preg_replace('/\s*px\s*$/i', '', $gap);
        if (!is_numeric($gap)) {
            return '';
        }
        $n = (int) round((float) $gap);
        return $n >= 0 ? (string) $n : '';
    }

    public function render_movie_grid($atts) {
        $atts['show_search'] = 'false';
        return $this->render_library($atts);
    }


    public function render_movie_search($atts) {
        $atts['show_search'] = 'true';
        return $this->render_library($atts);
    }

    /**
     * Unified Library Shortcode
     * [movie_engine_library post_type="movie_engine_movie" columns="5" show_search="true" limit="20"]
     */
    public function render_library($atts) {
        $design_settings = get_option('movie_engine_design_settings', []);

        $is_search = (isset($atts['show_search']) && filter_var($atts['show_search'], FILTER_VALIDATE_BOOLEAN)) || isset($_GET['s']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended

        // Allow real output for REST block preview (ServerSideRender); still show placeholder in classic admin.
        $is_rest_block_preview = defined('REST_REQUEST') && REST_REQUEST;
        if (is_admin() && (!defined('DOING_AJAX') || !DOING_AJAX) && !$is_rest_block_preview) {
            return '<div class="movie-engine-shortcode-placeholder" style="padding: 20px; border: 1px dashed #ccc; text-align: center; background: #f9f9f9;">' . esc_html__('Movie Library: [movie_engine_library]', 'cine-movie-engine') . '</div>';
        }

        if ($is_search) {
            $setting_key = 'search_items_per_page';
        } else {
            $setting_key = 'archive_items_per_page';
        }

        $default_limit = isset($design_settings[$setting_key]) ? intval($design_settings[$setting_key]) : 12;

        $atts = shortcode_atts([
            'post_type'      => '', // movie_engine_movie, movie_engine_series, movie_engine_season, movie_engine_episode, etc.
            'limit'          => $default_limit,
            'columns'        => $design_settings['archive_columns_desktop'] ?? 4,
            'columns_laptop' => $design_settings['archive_columns_laptop'] ?? 4,
            'columns_tablet' => $design_settings['archive_columns_tablet'] ?? 3,
            'columns_mobile' => $design_settings['archive_columns_mobile'] ?? 2,
            'show_search'    => 'false', // true, false
            'orderby'        => 'date',
            'order'          => 'DESC',
            'genre'          => '', // genre slug
            'year'           => '', // year slug
            'show_filters'   => 'false', // true, false
            'filter_layout'  => 'sidebar', // sidebar, top
            'enable_ajax'    => 'true',
            'layout'         => 'grid', // grid, slider
            'type'           => '', // trending, new_releases, upcoming, top_rated , popular, upcoming, top_rated, etc.
            'title'          => '', // title
            'show_view_all'  => 'true',
            'view_all_url'   => '', // url
            'view_all_text'  => esc_html__('View All', 'cine-movie-engine'),
            'pagination'     => 'true', // true, false
            'ids'            => '', // post ids
            'language'       => '', // language slug
            'autoplay'       => 'false',
            'autoplay_speed' => '3000',
            'loop'           => 'false',
            'pause_on_hover' => 'true',
            'show_dots'      => 'false',
            'gap'            => '',
            'show_rating'    => 'true',
            'show_year'      => 'true',
            'show_quality'   => 'true',
            'show_type'      => 'true',
            'access'         => 'false',
            'show_price'     => 'false',
            'show_access_label' => 'false',
            'show_free'      => 'false',
            'show_badges'    => 'true',
            'show_premium_badge' => 'true',
            'show_ppv_badge' => 'true',
            'ratio'          => 'portrait',
            'custom_ratio'   => '',
            'style'          => 'classic', // classic, modern, grid, list, slider
            'custom_class'   => '',
            'background_color' => '', // hex, rgb/rgba, or var(--wp--preset--color--...)
        ], $atts);

        ob_start();

        $show_search = filter_var($atts['show_search'], FILTER_VALIDATE_BOOLEAN);
        $columns = intval($atts['columns']);
        if ($columns < 2 || $columns > 15) $columns = 4;
        $columns_laptop = intval($atts['columns_laptop']);
        $columns_tablet = intval($atts['columns_tablet']);
        $columns_mobile = intval($atts['columns_mobile']);
        $locked_post_type = $atts['post_type'];
        $post_type = $locked_post_type;
        if (!empty($post_type) && !is_array($post_type) && strpos($post_type, ',') !== false) {
            $post_type = array_map('trim', explode(',', $post_type));
        }

        if (empty($post_type)) {
            $post_type = isset($_GET['me_post_type']) ? sanitize_text_field(wp_unslash($_GET['me_post_type'])) : ['movie_engine_movie', 'movie_engine_series']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        }

        $preselected_genre = $atts['genre'];
        $preselected_year = $atts['year'];
        $preselected_orderby = $atts['orderby'];
        $show_filters = filter_var($atts['show_filters'], FILTER_VALIDATE_BOOLEAN);
        $layout = $atts['layout'];
        $type = $atts['type'];
        $show_view_all = filter_var($atts['show_view_all'], FILTER_VALIDATE_BOOLEAN);
        $pagination_enabled = filter_var($atts['pagination'], FILTER_VALIDATE_BOOLEAN);

        if ($show_search && !$show_filters) {
            \MovieEngine\Utils\TemplateLoader::get_template_part('archive/filter-bar', '', [
                'locked_post_type'    => $locked_post_type,
                'preselected_genre'   => $preselected_genre,
                'preselected_year'    => $preselected_year,
                'preselected_orderby' => $preselected_orderby,
            ]);
        } elseif (!$show_filters) {
            $pt_val = is_array($post_type) ? implode(',', (array)$post_type) : $post_type;
            echo '<input type="hidden" class="movie-engine-filter-type" value="' . esc_attr($pt_val) . '">';
        }

        if (!empty($preselected_genre)) echo '<input type="hidden" name="movie_engine_genre" value="' . esc_attr($preselected_genre) . '">';
        if (!empty($preselected_year)) echo '<input type="hidden" name="movie_engine_year" value="' . esc_attr($preselected_year) . '">';
        if (!empty($preselected_orderby)) echo '<input type="hidden" name="orderby" value="' . esc_attr($preselected_orderby) . '">';

        $filter_layout = $atts['filter_layout'];
        $enable_ajax = filter_var($atts['enable_ajax'], FILTER_VALIDATE_BOOLEAN);

        $container_attrs = 'data-ajax="' . ($enable_ajax ? '1' : '0') . '"';
        if ($enable_ajax) {
            $container_attrs .= ' data-action="movie_engine_filter_movies"';
            $pt_val = is_array($post_type) ? implode(',', (array) $post_type) : $post_type;
            $container_attrs .= ' data-post_type="' . esc_attr($pt_val) . '"';
        }
        $container_class = 'movie-engine-library-container movie-engine-plugin ' . esc_attr($atts['custom_class']) . ' ' . ($show_filters ? 'me-has-filters me-filter-' . esc_attr($filter_layout) : '');
        if ($enable_ajax) {
            $container_class .= ' movie-engine-ajax-container';
        }

        $bg_style = '';
        $safe_bg = self::sanitize_library_background_color($atts['background_color'] ?? '');
        if ($safe_bg !== '') {
            $bg_style = ' style="background-color: ' . esc_attr($safe_bg) . ';"';
        }
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $container_class and $container_attrs built with esc_attr for values
        echo '<div class="' . esc_attr($container_class) . '" ' . $container_attrs . $bg_style . '>';

        if ($show_filters && $filter_layout === 'sidebar') {
            \MovieEngine\Utils\TemplateLoader::get_template_part('archive/filter-sidebar', '', [
                'locked_post_type' => $locked_post_type,
            ]);
        }

        echo '<div class="movie-engine-library-content">';

        if ($show_filters && $filter_layout === 'top') {
            \MovieEngine\Utils\TemplateLoader::get_template_part('archive/filter-sidebar', '', [
                'locked_post_type' => $locked_post_type,
                'collapse_all'     => true,
            ]);
        }

        if ($show_filters) {
            \MovieEngine\Utils\TemplateLoader::get_template_part('archive/active-filters', '', [
                'show_search' => $show_search
            ]);
        }

        $paged = (get_query_var('paged')) ? get_query_var('paged') : (isset($_GET['paged']) ? absint(wp_unslash($_GET['paged'])) : 1); // phpcs:ignore WordPress.Security.NonceVerification.Recommended


        $view_all_url = '';
        if (!empty($atts['title'])) {
            $view_all_url = !empty($atts['view_all_url']) ? $atts['view_all_url'] : '';
            if (empty($view_all_url) && $show_view_all) {
                if (!empty($design_settings['movie_archive_page'])) {
                    $view_all_url = get_permalink($design_settings['movie_archive_page']);
                } elseif (!empty($design_settings['browse_page'])) {
                    $view_all_url = get_permalink($design_settings['browse_page']);
                } else {
                    $view_all_url = get_post_type_archive_link('movie_engine_movie');
                }

                if ($type === 'trending') {
                    $view_all_url = add_query_arg('orderby', 'views', $view_all_url);
                } elseif ($type === 'upcoming') {
                    $view_all_url = add_query_arg('orderby', 'upcoming', $view_all_url);
                }
            }
        }

        $query_args = [
            'post_type'      => $post_type,
            'posts_per_page' => intval($atts['limit']),
            'paged'          => $paged,
            's'              => isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            'post_status'    => 'publish'
        ];

        if (!empty($type)) {
            switch ($type) {
                case 'trending':
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                    $query_args['meta_key'] = '_movie_engine_views';
                    $query_args['orderby'] = 'meta_value_num';
                    $query_args['order'] = 'DESC';
                    break;
                case 'new_releases':
                    $query_args['orderby'] = 'date';
                    $query_args['order'] = 'DESC';
                    break;
                case 'upcoming':
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                    $query_args['meta_key'] = '_movie_engine_release_date';
                    $query_args['orderby'] = 'meta_value';
                    $query_args['order'] = 'ASC';
                    $query_args['meta_query'][] = [
                        'key'     => '_movie_engine_release_date',
                        'value'   => gmdate('Y-m-d'),
                        'compare' => '>',
                        'type'    => 'DATE'
                    ];
                    break;
                case 'top_rated':
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                    $query_args['meta_key'] = '_movie_engine_rating';
                    $query_args['orderby'] = 'meta_value_num';
                    $query_args['order'] = 'DESC';
                    break;
            }
        }

        if (!empty($_GET['me_genre'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $query_args['tax_query'][] = [
                'taxonomy' => 'movie_engine_genre',
                'field'    => 'slug',
                'terms'    => array_map('sanitize_text_field', wp_unslash((array)$_GET['me_genre'])), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
                'operator' => 'IN'
            ];
        } elseif (!empty($_GET['movie_engine_genre'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $query_args['tax_query'][] = [
                'taxonomy' => 'movie_engine_genre',
                'field'    => 'slug',
                'terms'    => sanitize_text_field(wp_unslash($_GET['movie_engine_genre'])), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            ];
        } elseif (!empty($atts['genre'])) {
            $query_args['tax_query'][] = [
                'taxonomy' => 'movie_engine_genre',
                'field'    => 'slug',
                'terms'    => sanitize_text_field($atts['genre']),
            ];
        }

        if (!empty($_GET['me_year'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $query_args['tax_query'][] = [
                'taxonomy' => 'movie_engine_year',
                'field'    => 'slug',
                'terms'    => array_map('sanitize_text_field', wp_unslash((array)$_GET['me_year'])), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
                'operator' => 'IN'
            ];
        } elseif (!empty($_GET['movie_engine_year'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $query_args['tax_query'][] = [
                'taxonomy' => 'movie_engine_year',
                'field'    => 'slug',
                'terms'    => sanitize_text_field(wp_unslash($_GET['movie_engine_year'])), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            ];
        } elseif (!empty($atts['year'])) {
            $query_args['tax_query'][] = [
                'taxonomy' => 'movie_engine_year',
                'field'    => 'slug',
                'terms'    => sanitize_text_field($atts['year']),
            ];
        }

        if (!empty($_GET['me_language'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $query_args['tax_query'][] = [
                'taxonomy' => 'movie_engine_language',
                'field'    => 'slug',
                'terms'    => array_map('sanitize_text_field', wp_unslash((array)$_GET['me_language'])), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
                'operator' => 'IN'
            ];
        } elseif (!empty($atts['language'])) {
            $query_args['tax_query'][] = [
                'taxonomy' => 'movie_engine_language',
                'field'    => 'slug',
                'terms'    => sanitize_text_field($atts['language']),
            ];
        }

        if (!empty($_GET['me_quality'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $qualities = array_map('sanitize_text_field', wp_unslash((array)$_GET['me_quality'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $query_args['tax_query'][] = [
                'relation' => 'OR',
                [
                    'taxonomy' => 'movie_engine_quality',
                    'field'    => 'slug',
                    'terms'    => $qualities,
                    'operator' => 'IN'
                ],
                [
                    'taxonomy' => 'movie_engine_tv_quality',
                    'field'    => 'slug',
                    'terms'    => $qualities,
                    'operator' => 'IN'
                ]
            ];
        }

        $meta_query = ['relation' => 'AND'];

        $is_pro = apply_filters('movie_engine_is_pro', false);
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- GET based filtering, sanitization happens below
        $access_val = ($is_pro && !empty($_GET['me_access'])) ? (array)wp_unslash($_GET['me_access']) : (($is_pro && !empty($atts['access'])) ? explode(',', $atts['access']) : []);
        if (!empty($access_val)) {
            $access = array_map('sanitize_text_field', wp_unslash($access_val));
            $access_queries = ['relation' => 'OR'];
            foreach ($access as $type_access) {
                $type_access = trim($type_access);
                if ($type_access === 'free') {
                    $access_queries[] = [
                        'relation' => 'OR',
                        ['key' => '_movie_engine_is_premium', 'value' => '1', 'compare' => '!='],
                        ['key' => '_movie_engine_is_premium', 'compare' => 'NOT EXISTS']
                    ];
                } elseif ($type_access === 'premium') {
                    $access_queries[] = ['key' => '_movie_engine_is_premium', 'value' => '1', 'compare' => '='];
                } elseif ($type_access === 'rent') {
                    $access_queries[] = ['key' => '_movie_engine_rent_price', 'value' => 0, 'compare' => '>', 'type' => 'NUMERIC'];
                } elseif ($type_access === 'sale') {
                    $access_queries[] = ['key' => '_movie_engine_sale_price', 'value' => 0, 'compare' => '>', 'type' => 'NUMERIC'];
                }
            }
            $meta_query[] = $access_queries;
        }

        if (!empty($_GET['me_duration'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $duration = sanitize_text_field(wp_unslash($_GET['me_duration'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            if (is_array($duration)) $duration = end($duration);
            switch ($duration) {
                case 'under_1':
                    $meta_query[] = ['key' => '_movie_engine_runtime', 'value' => 60, 'compare' => '<', 'type' => 'NUMERIC'];
                    break;
                case '1_2':
                    $meta_query[] = ['key' => '_movie_engine_runtime', 'value' => [60, 120], 'compare' => 'BETWEEN', 'type' => 'NUMERIC'];
                    break;
                case '2_3':
                    $meta_query[] = ['key' => '_movie_engine_runtime', 'value' => [120, 180], 'compare' => 'BETWEEN', 'type' => 'NUMERIC'];
                    break;
                case 'above_3':
                case 'over_2': // legacy alias
                    $meta_query[] = ['key' => '_movie_engine_runtime', 'value' => 180, 'compare' => '>', 'type' => 'NUMERIC'];
                    break;
            }
        }

        if (!empty($_GET['me_rating'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $rating = sanitize_text_field(wp_unslash($_GET['me_rating'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            if (is_array($rating)) $rating = end($rating);
            $meta_query[] = ['key' => '_movie_engine_rating', 'value' => floatval($rating), 'compare' => '>=', 'type' => 'NUMERIC'];
        }

        if (count($meta_query) > 1) {
            // phpcs:ignore WordPress.DB.SlowDBQuery
            $query_args['meta_query'] = $meta_query;
        }

        if (!empty($_GET['orderby'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $orderby = sanitize_text_field(wp_unslash($_GET['orderby'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            if ($orderby === 'rating') {
                // phpcs:ignore WordPress.DB.SlowDBQuery
                $query_args['meta_key'] = '_movie_engine_rating';
                $query_args['orderby'] = 'meta_value_num';
                $query_args['order'] = 'DESC';
            } elseif ($orderby === 'views') {
                // phpcs:ignore WordPress.DB.SlowDBQuery
                $query_args['meta_key'] = '_movie_engine_views';
                $query_args['orderby'] = 'meta_value_num';
                $query_args['order'] = 'DESC';
            } elseif ($orderby === 'title') {
                $query_args['orderby'] = 'title';
                $query_args['order'] = 'ASC';
            } else {
                $query_args['orderby'] = 'date';
                $query_args['order'] = 'DESC';
            }
        } else {
            $query_args['orderby'] = sanitize_text_field($atts['orderby']);
            $query_args['order']   = sanitize_text_field($atts['order']);
        }

        if (!empty($atts['ids'])) {
            $query_args['post__in'] = array_map('intval', explode(',', $atts['ids']));
            $query_args['orderby'] = 'post__in';
        }

        $query_args = apply_filters('movie_engine_library_args', $query_args, $atts);

        $transient_key = 'me_q_' . md5(serialize($query_args));
        $cached_ids = get_transient($transient_key);

        if (false !== $cached_ids && is_array($cached_ids)) {
            $query = new \WP_Query([
                'post_type'           => $query_args['post_type'],
                'post__in'            => empty($cached_ids) ? [0] : $cached_ids,
                'orderby'             => 'post__in',
                'posts_per_page'      => count($cached_ids) ?: 1,
                'ignore_sticky_posts' => true
            ]);
            $query->max_num_pages = get_transient($transient_key . '_pages') ?: 1;
            $query->found_posts   = get_transient($transient_key . '_total') ?: count($cached_ids);
        } else {
            $query = new \WP_Query($query_args);
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            if (empty($_GET) || (count($_GET) === 1 && isset($_GET['paged']))) {
                set_transient($transient_key, wp_list_pluck($query->posts, 'ID'), HOUR_IN_SECONDS * 2);
                set_transient($transient_key . '_pages', $query->max_num_pages, HOUR_IN_SECONDS * 2);
                set_transient($transient_key . '_total', $query->found_posts, HOUR_IN_SECONDS * 2);
            }
        }

        $grid_vars_style = "--me-grid-columns-desktop: {$columns}; --me-grid-columns-laptop: {$columns_laptop}; --me-grid-columns-tablet: {$columns_tablet}; --me-grid-columns-mobile: {$columns_mobile}; ";
        $grid_vars_style .= "--grid-columns: {$columns}; --grid-columns-laptop: {$columns_laptop}; --grid-columns-tablet: {$columns_tablet}; --grid-columns-mobile: {$columns_mobile};";

        $gap_norm = self::normalize_gap_px($atts['gap'] ?? '');
        $gap_css = ($gap_norm !== '') ? ' gap: ' . esc_attr($gap_norm) . 'px;' : '';
        $grid_items_style = $grid_vars_style . $gap_css;

        $slider_grid_style = $grid_items_style . " display: flex !important; flex-wrap: nowrap !important; overflow-x: auto !important;";

        do_action('movie_engine_before_library_grid', $atts);

        if ($query->have_posts()) {
            $section_classes = 'movie-engine-section';
            $section_attrs = '';

            if ($layout === 'slider') {
                $section_classes .= ' movie-engine-slider-enabled me-slider-layout';
                $section_attrs = sprintf(
                    'data-autoplay="%s" data-speed="%s" data-loop="%s" data-pause="%s" data-dots="%s"',
                    esc_attr($atts['autoplay']),
                    esc_attr($atts['autoplay_speed']),
                    esc_attr($atts['loop']),
                    esc_attr($atts['pause_on_hover']),
                    esc_attr($atts['show_dots'])
                );
            } else {
                $section_classes .= ' me-grid-layout';
            }

            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            echo '<section class="' . esc_attr($section_classes) . '" style="' . esc_attr($grid_vars_style) . '" ' . $section_attrs . '>';

            if (!empty($atts['title'])) {
                echo '<div class="movie-engine-section-title me-section-header">';
                echo '<h2>' . esc_html($atts['title']) . '</h2>';
                if ($show_view_all && !empty($view_all_url)) {
                    echo '<a href="' . esc_url($view_all_url) . '" class="movie-engine-view-all me-view-all">';
                    echo esc_html($atts['view_all_text']) . ' <span class="fa-solid fa-angle-right"></span>';
                    echo '</a>';
                }
                echo '</div>';
            }

            if ($layout === 'slider') {
                echo '<div class="movie-engine-items-grid" style="' . esc_attr($slider_grid_style) . '">';
            } else {
                echo '<div class="movie-engine-items-grid" style="' . esc_attr($grid_items_style) . '">';
            }

            $args = [
                'show_rating'       => filter_var($atts['show_rating'], FILTER_VALIDATE_BOOLEAN),
                'show_year'         => filter_var($atts['show_year'], FILTER_VALIDATE_BOOLEAN),
                'show_quality'      => filter_var($atts['show_quality'], FILTER_VALIDATE_BOOLEAN),
                'show_type'         => filter_var($atts['show_type'], FILTER_VALIDATE_BOOLEAN),
                'show_price'        => $is_pro && filter_var($atts['show_price'], FILTER_VALIDATE_BOOLEAN),
                'show_access_label' => $is_pro && filter_var($atts['show_access_label'], FILTER_VALIDATE_BOOLEAN),
                'show_free'         => filter_var($atts['show_free'], FILTER_VALIDATE_BOOLEAN),
                'show_badges'       => $is_pro && filter_var($atts['show_badges'], FILTER_VALIDATE_BOOLEAN),
                'show_premium_badge' => $is_pro && filter_var($atts['show_premium_badge'], FILTER_VALIDATE_BOOLEAN),
                'show_ppv_badge'     => $is_pro && filter_var($atts['show_ppv_badge'], FILTER_VALIDATE_BOOLEAN),
                'style'              => sanitize_text_field($atts['style']),
            ];

            $ratio = sanitize_text_field($atts['ratio']);
            $custom_ratio = trim((string) $atts['custom_ratio']);

            // If explicit custom_ratio is provided, prefer it.
            if (!empty($custom_ratio)) {
                $args['ratio'] = 'custom';
            } elseif ($ratio === 'custom') {
                $args['ratio'] = 'custom';
                $custom_ratio = '2/3';
            } elseif (strpos($ratio, '/') !== false) {
                $custom_ratio = $ratio;
                $args['ratio'] = 'custom';
            } else {
                $args['ratio'] = $ratio;
            }
            $args['custom_ratio'] = $custom_ratio;

            while ($query->have_posts()) {
                $query->the_post();
                \MovieEngine\Utils\TemplateLoader::get_template_part('loop/content-movie', '', $args);
            }

            echo '</div>';
            echo '</section>';

            if ($pagination_enabled && $layout !== 'slider') {
                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- kses_post sanitizes HTML output
                echo \MovieEngine\Utils\Helpers::kses_post(\MovieEngine\Utils\Helpers::render_pagination($paged, $query->max_num_pages));
            }

            wp_reset_postdata();
        } else {
            echo '<p class="movie-engine-no-results">' . esc_html__('No items found.', 'cine-movie-engine') . '</p>';
        }

        do_action('movie_engine_after_library_grid', $atts);

        echo '</div>';
        echo '</div>';

        $html = ob_get_clean();
        return $html;
    }


    public function render_search_bar($atts) {
        $atts = shortcode_atts([
            'placeholder' => esc_html__('Search movies & TV series...', 'cine-movie-engine'),
            'button_text' => esc_html__('Search', 'cine-movie-engine'),
            'custom_class' => '',
        ], $atts);

        ob_start();
?>
        <div class="movie-engine-search-bar-wrapper movie-engine-plugin <?php echo esc_attr($atts['custom_class']); ?>">
            <form role="search" method="get" class="movie-engine-search-form" action="<?php echo esc_url(home_url('/')); ?>">
                <input type="hidden" name="post_type" value="movie_engine_movie">
                <div class="movie-engine-search-input-wrapper">
                    <input
                        type="search"
                        class="movie-engine-search-field"
                        placeholder="<?php echo esc_attr($atts['placeholder']); ?>"
                        value="<?php echo esc_attr(get_search_query()); ?>"
                        name="s"
                        required>
                    <button type="submit" class="movie-engine-search-submit">
                        <span class="dashicons dashicons-search"></span>
                        <span class="movie-engine-search-text"><?php echo esc_html($atts['button_text']); ?></span>
                    </button>
                </div>
            </form>
        </div>
    <?php
        return ob_get_clean();
    }

    public function render_live_search($atts) {
        $atts = shortcode_atts([
            'placeholder' => esc_html__('Search movies & TV series...', 'cine-movie-engine'),
            'show_icon'   => 'true',
            'custom_class' => '',
        ], $atts);

        ob_start();
    ?>
        <div class="movie-engine-live-search-wrapper movie-engine-plugin <?php echo esc_attr($atts['custom_class']); ?>">
            <div class="movie-engine-ls-input-group">
                <?php if (filter_var($atts['show_icon'], FILTER_VALIDATE_BOOLEAN)) : ?>
                    <span class="dashicons dashicons-search me-ls-icon"></span>
                <?php endif; ?>
                <input type="text" class="movie-engine-ls-input" placeholder="<?php echo esc_attr($atts['placeholder']); ?>" autocomplete="off">
                <div class="movie-engine-ls-loader me-hidden">
                    <div class="movie-engine-spinner"></div>
                </div>
            </div>
            <div class="movie-engine-live-search-results me-hidden"></div>
        </div>
<?php
        return ob_get_clean();
    }


    /**
     * Person List Shortcode
     * [movie_engine_person_list limit="24" columns="6"]
     */
    public function render_person_list($atts) {
        if (is_admin() && (!defined('DOING_AJAX') || !DOING_AJAX)) {
            return '<div class="movie-engine-shortcode-placeholder" style="padding: 20px; border: 1px dashed #ccc; text-align: center; background: #f9f9f9;">' . esc_html__('Person List: [movie_engine_person_list]', 'cine-movie-engine') . '</div>';
        }

        $design_settings = get_option('movie_engine_design_settings', []);

        $atts = shortcode_atts([
            'limit'          => 24,
            'columns'        => $design_settings['archive_columns_desktop'] ?? 6,
            'columns_laptop' => $design_settings['archive_columns_laptop'] ?? 4,
            'columns_tablet' => $design_settings['archive_columns_tablet'] ?? 3,
            'columns_mobile' => $design_settings['archive_columns_mobile'] ?? 2,
            'orderby'        => 'title',
            'order'          => 'ASC',
            'department'     => '',
            'search'         => '',
            'enable_ajax'    => 'true',
            'show_filters'   => 'false',
            'filter_layout'  => 'sidebar',
            'ratio'          => 'portrait',
            'custom_ratio'   => '',
            'style'          => 'classic',
            'layout'         => 'grid',
            'autoplay'       => 'false',
            'autoplay_speed' => '3000',
            'loop'           => 'false',
            'pause_on_hover' => 'true',
            'show_dots'      => 'false',
            'gap'            => '',
            'custom_class'   => '',
        ], $atts);

        ob_start();

        $columns = intval($atts['columns']);
        if ($columns < 2 || $columns > 15) $columns = 6;
        $columns_laptop = intval($atts['columns_laptop']);
        $columns_tablet = intval($atts['columns_tablet']);
        $columns_mobile = intval($atts['columns_mobile']);
        $limit = intval($atts['limit']);
        $show_filters = filter_var($atts['show_filters'], FILTER_VALIDATE_BOOLEAN);

        $paged = (get_query_var('paged')) ? get_query_var('paged') : (isset($_GET['paged']) ? absint(wp_unslash($_GET['paged'])) : 1); // phpcs:ignore WordPress.Security.NonceVerification.Recommended


        $html_header = ob_get_clean();
        ob_start();

        $query_args = [
            'post_type'      => 'movie_engine_person',
            'posts_per_page' => $limit,
            'paged'          => $paged,
            'post_status'    => 'publish',
            'orderby'        => sanitize_text_field($atts['orderby']),
            'order'          => sanitize_text_field($atts['order']),
        ];

        if (!empty($atts['search']) || !empty($_GET['s'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $query_args['s'] = !empty($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : sanitize_text_field($atts['search']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        }

        $department = '';
        if (!empty($_GET['movie_engine_known_for'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            // Could be array or string
            $req_dept = map_deep(wp_unslash($_GET['movie_engine_known_for']), 'sanitize_text_field'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            if (is_array($req_dept)) {
                $department = $req_dept;
            } else {
                $department = sanitize_text_field($req_dept);
            }
        } elseif (!empty($atts['department'])) {
            $department = sanitize_text_field($atts['department']);
        }

        if (!empty($department)) {
            if (is_array($department)) {
                $meta_query_dept = ['relation' => 'OR'];
                foreach ($department as $dept) {
                    $meta_query_dept[] = [
                        'key'     => '_movie_engine_known_for',
                        'value'   => sanitize_text_field($dept),
                        'compare' => '='
                    ];
                }
                $query_args['meta_query'][] = $meta_query_dept;
            } else {
                $query_args['meta_query'][] = [
                    'key'     => '_movie_engine_known_for',
                    'value'   => $department,
                    'compare' => '='
                ];
            }
        }

        $query = new \WP_Query($query_args);

        // Grid variables (CSS vars cascade; gap on section does not affect card grid — apply on .movie-engine-items-grid).
        $grid_vars_style = "--me-grid-columns-desktop: {$columns}; --me-grid-columns-laptop: {$columns_laptop}; --me-grid-columns-tablet: {$columns_tablet}; --me-grid-columns-mobile: {$columns_mobile}; ";
        $grid_vars_style .= "--grid-columns: {$columns}; --grid-columns-laptop: {$columns_laptop}; --grid-columns-tablet: {$columns_tablet}; --grid-columns-mobile: {$columns_mobile};";
        $gap_norm = self::normalize_gap_px($atts['gap'] ?? '');
        $gap_css = ($gap_norm !== '') ? ' gap: ' . esc_attr($gap_norm) . 'px;' : '';
        $grid_items_style = $grid_vars_style . $gap_css;
        $slider_grid_style = $grid_items_style . " display: flex !important; flex-wrap: nowrap !important; overflow-x: auto !important;";

        // Ajax Data Attributes
        $ajax_data = '';
        if (filter_var($atts['enable_ajax'], FILTER_VALIDATE_BOOLEAN)) {
            $ajax_data .= ' data-action="movie_engine_filter_movies"';
            $ajax_data .= ' data-post_type="movie_engine_person"';
            $ajax_data .= ' data-orderby="' . esc_attr($atts['orderby']) . '"';
            $ajax_data .= ' data-order="' . esc_attr($atts['order']) . '"';
            $ajax_data .= ' data-posts_per_page="' . esc_attr($limit) . '"';
        }

        $filter_layout = $atts['filter_layout'];
        echo '<div class="movie-engine-library-container movie-engine-person-library movie-engine-plugin ' . esc_attr($atts['custom_class']) . ' ' . esc_attr($show_filters ? 'me-has-filters me-filter-' . $filter_layout : '') . ' ' . esc_attr(filter_var($atts['enable_ajax'], FILTER_VALIDATE_BOOLEAN) ? 'movie-engine-ajax-container' : '') . '" ' . $ajax_data . ' data-ajax="1">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        echo '<input type="hidden" class="movie-engine-filter-type" value="movie_engine_person">';

        if ($show_filters && $filter_layout === 'sidebar') {
            $movie_engine_departments = \MovieEngine\Utils\Helpers::get_unique_meta_values('movie_engine_person', '_movie_engine_known_for');
            $movie_engine_active_departments = isset($_GET['movie_engine_known_for']) ? (array)map_deep(wp_unslash($_GET['movie_engine_known_for']), 'sanitize_text_field') : []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

            $movie_engine_genders = \MovieEngine\Utils\Helpers::get_unique_meta_values('movie_engine_person', '_movie_engine_gender');
            $movie_engine_active_genders = isset($_GET['me_gender']) ? (array)map_deep(wp_unslash($_GET['me_gender']), 'sanitize_text_field') : []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

            $movie_engine_birth_years = [];
            $birthdays = \MovieEngine\Utils\Helpers::get_unique_meta_values('movie_engine_person', '_movie_engine_birthday');
            $current_year = (int)gmdate('Y');
            foreach ($birthdays as $birthday) {
                $timestamp = strtotime($birthday);
                if ($timestamp) {
                    $year = (int)gmdate('Y', $timestamp);
                    if ($year > 1800 && $year <= $current_year && !in_array($year, $movie_engine_birth_years)) {
                        $movie_engine_birth_years[] = $year;
                    }
                }
            }
            rsort($movie_engine_birth_years);
            $movie_engine_active_years = isset($_GET['me_birth_year']) ? (array)map_deep(wp_unslash($_GET['me_birth_year']), 'sanitize_text_field') : []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

            \MovieEngine\Utils\TemplateLoader::get_template_part('archive/filter-person-sidebar', '', [
                'movie_engine_departments'        => $movie_engine_departments,
                'movie_engine_active_departments' => $movie_engine_active_departments,
                'movie_engine_genders'            => $movie_engine_genders,
                'movie_engine_active_genders'     => $movie_engine_active_genders,
                'movie_engine_birth_years'        => $movie_engine_birth_years,
                'movie_engine_active_years'       => $movie_engine_active_years,
                'collapse_all'                    => true,
            ]);
        }

        echo '<div class="movie-engine-library-content">';

        if ($show_filters && $filter_layout === 'top') {
            $movie_engine_departments = \MovieEngine\Utils\Helpers::get_unique_meta_values('movie_engine_person', '_movie_engine_known_for');
            $movie_engine_active_departments = isset($_GET['movie_engine_known_for']) ? (array)map_deep(wp_unslash($_GET['movie_engine_known_for']), 'sanitize_text_field') : []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

            $movie_engine_genders = \MovieEngine\Utils\Helpers::get_unique_meta_values('movie_engine_person', '_movie_engine_gender');
            $movie_engine_active_genders = isset($_GET['me_gender']) ? (array)map_deep(wp_unslash($_GET['me_gender']), 'sanitize_text_field') : []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

            $movie_engine_birth_years = [];
            $birthdays = \MovieEngine\Utils\Helpers::get_unique_meta_values('movie_engine_person', '_movie_engine_birthday');
            $current_year = (int)gmdate('Y');
            foreach ($birthdays as $birthday) {
                $timestamp = strtotime($birthday);
                if ($timestamp) {
                    $year = (int)gmdate('Y', $timestamp);
                    if ($year > 1800 && $year <= $current_year && !in_array($year, $movie_engine_birth_years)) {
                        $movie_engine_birth_years[] = $year;
                    }
                }
            }
            rsort($movie_engine_birth_years);
            $movie_engine_active_years = isset($_GET['me_birth_year']) ? (array)map_deep(wp_unslash($_GET['me_birth_year']), 'sanitize_text_field') : []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

            \MovieEngine\Utils\TemplateLoader::get_template_part('archive/filter-person-sidebar', '', [
                'movie_engine_departments'        => $movie_engine_departments,
                'movie_engine_active_departments' => $movie_engine_active_departments,
                'movie_engine_genders'            => $movie_engine_genders,
                'movie_engine_active_genders'     => $movie_engine_active_genders,
                'movie_engine_birth_years'        => $movie_engine_birth_years,
                'movie_engine_active_years'       => $movie_engine_active_years,
            ]);
        }

        $layout = $atts['layout'];
        if ($query->have_posts()) {
            $section_classes = 'movie-engine-section';
            $section_attrs = '';

            if ($layout === 'slider') {
                $section_classes .= ' movie-engine-slider-enabled me-slider-layout';
                $section_attrs = sprintf(
                    'data-autoplay="%s" data-speed="%s" data-loop="%s" data-pause="%s" data-dots="%s"',
                    esc_attr($atts['autoplay']),
                    esc_attr($atts['autoplay_speed']),
                    esc_attr($atts['loop']),
                    esc_attr($atts['pause_on_hover']),
                    esc_attr($atts['show_dots'])
                );
            } else {
                $section_classes .= ' me-grid-layout';
            }

            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            echo '<section class="' . esc_attr($section_classes) . '" style="' . esc_attr($grid_vars_style) . '" ' . $section_attrs . '>';
        }

        if ($show_filters) {
            \MovieEngine\Utils\TemplateLoader::get_template_part('archive/active-filters', '', [
                'show_search' => true,
                'locked_post_type' => 'movie_engine_person'
            ]);
        }

        if ($query->have_posts()) {
            if ($layout === 'slider') {
                echo '<div class="movie-engine-items-grid" style="' . esc_attr($slider_grid_style) . '">';
            } else {
                echo '<div class="movie-engine-items-grid movie-engine-person-grid" style="' . esc_attr($grid_items_style) . '">';
            }

            $args = [
                'style' => sanitize_text_field($atts['style']),
            ];

            $ratio = sanitize_text_field($atts['ratio']);
            $custom_ratio = trim((string) $atts['custom_ratio']);

            if (!empty($custom_ratio)) {
                $args['ratio'] = 'custom';
            } elseif ($ratio === 'custom') {
                $args['ratio'] = 'custom';
                $custom_ratio = '2/3';
            } elseif (strpos($ratio, '/') !== false) {
                $custom_ratio = $ratio;
                $args['ratio'] = 'custom';
            } else {
                $args['ratio'] = $ratio;
            }
            $args['custom_ratio'] = $custom_ratio;

            while ($query->have_posts()) {
                $query->the_post();
                \MovieEngine\Utils\TemplateLoader::get_template_part('loop/content-person', '', $args);
            }
            echo '</div>';

            echo '</section>';

            if ($layout !== 'slider') {
                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- kses_post sanitizes HTML output
                echo \MovieEngine\Utils\Helpers::kses_post(\MovieEngine\Utils\Helpers::render_pagination($paged, $query->max_num_pages));
            }

            wp_reset_postdata();
        } else {
            echo '<p class="movie-engine-no-results">' . esc_html__('No persons found.', 'cine-movie-engine') . '</p>';
        }

        echo '</div>';
        echo '</div>';

        $html = ob_get_clean();
        $full_html = $html_header . $html;
        return $full_html;
    }
    /**
     * Render User Profile Menu for Header
     */
    public function render_user_menu($atts) {
        $atts = shortcode_atts([
            'custom_class' => '',
        ], $atts);

        if (!is_user_logged_in()) {
            return '<div class="movie-engine-guest-menu ' . esc_attr($atts['custom_class']) . '">
                <a href="' . wp_login_url() . '" class="movie-engine-btn me-btn-login">' . esc_html__('Sign In', 'cine-movie-engine') . '</a>
            </div>';
        }

        $user_id = get_current_user_id();
        $user = get_userdata($user_id);

        $custom_avatar = get_user_meta($user_id, 'movie_engine_avatar_url', true);
        $avatar_url = $custom_avatar ? $custom_avatar : get_avatar_url($user_id, ['size' => 100]);

        $design_settings = get_option('movie_engine_design_settings', []);
        $dashboard_page_id = isset($design_settings['dashboard_page']) ? $design_settings['dashboard_page'] : '';
        $dashboard_url = $dashboard_page_id ? get_permalink($dashboard_page_id) : home_url('/dashboard');

        ob_start();
        \MovieEngine\Utils\TemplateLoader::get_template_part('global/user-menu', '', [
            'user' => $user,
            'avatar_url' => $avatar_url,
            'dashboard_page_id' => $dashboard_page_id,
            'atts' => $atts
        ]);
        return ob_get_clean();
    }

    /**
     * Unified Taxonomy Shortcode
     * [movie_engine_taxonomies type="genre" layout="grid" columns="4"]
     */
    public function render_taxonomies($atts) {
        $atts = shortcode_atts([
            'type'           => 'genre',
            'layout'         => 'pills', // pills, grid, slider
            'title'          => '',
            'show_view_all'  => 'false',
            'view_all_url'   => '',
            'view_all_text'  => esc_html__('View All', 'cine-movie-engine'),
            'columns'        => 4,
            'columns_laptop' => 4,
            'columns_tablet' => 3,
            'columns_mobile' => 2,
            'limit'          => 0,
            'orderby'        => 'name',
            'order'          => 'ASC',
            'hide_empty'     => 'true',
            'show_count'     => 'true',
            'gap'            => '15',
            'autoplay'       => 'false',
            'autoplay_speed' => '3000',
            'loop'           => 'false',
            'show_dots'      => 'false',
            'pause_on_hover' => 'true',
            'show_image'     => 'true',
            'include'        => '',
            // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
            'exclude'        => '',
            'custom_class'   => '',
            'ratio'          => 'landscape',
            'custom_ratio'   => '',
        ], $atts);

        // Allow real output for REST block preview (ServerSideRender); still show placeholder in classic admin.
        $is_rest_block_preview = defined('REST_REQUEST') && REST_REQUEST;
        if (is_admin() && (!defined('DOING_AJAX') || !DOING_AJAX) && !$is_rest_block_preview) {
            return '<div class="movie-engine-shortcode-placeholder" style="padding: 20px; border: 1px dashed #ccc; text-align: center; background: #f9f9f9;">' . esc_html__('Taxonomies: [movie_engine_taxonomies]', 'cine-movie-engine') . '</div>';
        }

        // Map shortcode 'type' to actual taxonomy
        $tax_map = [
            'genre'    => 'movie_engine_genre',
            'year'     => 'movie_engine_year',
            'tag'      => 'movie_engine_tag',
            'country'  => 'movie_engine_country',
            'language' => 'movie_engine_language',
            'studio'   => 'movie_engine_studio',
            'network'  => 'movie_engine_network',
            'quality'  => 'movie_engine_quality',
            'actor'    => 'movie_engine_actor',
            'director' => 'movie_engine_director',
            'creator'  => 'movie_engine_creator',
        ];

        $taxonomy = $tax_map[$atts['type']] ?? $atts['type'];
        $layout = $atts['layout'];
        $limit = intval($atts['limit']);
        $limit = $limit > 0 ? $limit : '';

        $terms_args = [
            'taxonomy'   => $taxonomy,
            'number'     => $limit,
            'orderby'    => sanitize_text_field($atts['orderby']),
            'order'      => sanitize_text_field($atts['order']),
            'hide_empty' => filter_var($atts['hide_empty'], FILTER_VALIDATE_BOOLEAN),
        ];

        if (!empty($atts['include'])) {
            $terms_args['include'] = array_map('intval', explode(',', $atts['include']));
            $terms_args['orderby'] = 'include';
        }

        if (!empty($atts['exclude'])) {
            // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Explictly defined by user in shortcode
            $terms_args['exclude'] = array_map('intval', explode(',', $atts['exclude']));
        }

        $terms = get_terms($terms_args);

        if (is_wp_error($terms) || empty($terms)) {
            return '';
        }

        $columns = intval($atts['columns']);
        $columns_laptop = intval($atts['columns_laptop']);
        $columns_tablet = intval($atts['columns_tablet']);
        $columns_mobile = intval($atts['columns_mobile']);
        $grid_vars_style = "--me-grid-columns-desktop: {$columns}; --me-grid-columns-laptop: {$columns_laptop}; --me-grid-columns-tablet: {$columns_tablet}; --me-grid-columns-mobile: {$columns_mobile}; ";
        $grid_vars_style .= "--grid-columns: {$columns}; --grid-columns-laptop: {$columns_laptop}; --grid-columns-tablet: {$columns_tablet}; --grid-columns-mobile: {$columns_mobile};";
        $gap_norm = self::normalize_gap_px($atts['gap'] ?? '');
        $gap_css = ($gap_norm !== '') ? ' gap: ' . esc_attr($gap_norm) . 'px;' : '';
        $grid_items_style = $grid_vars_style . $gap_css;

        ob_start();

        $custom_class = !empty($atts['custom_class']) ? ' ' . esc_attr($atts['custom_class']) : '';
        echo '<div class="movie-engine-taxonomies-wrapper movie-engine-plugin movie-engine-taxonomy-shortcode' . esc_attr($custom_class) . '">';

        if ($layout === 'slider' || $layout === 'grid') {
            $section_classes = 'movie-engine-section';
            $section_attrs = '';
            if ($layout === 'slider') {
                $section_classes .= ' movie-engine-slider-enabled me-slider-layout';
                $section_attrs = sprintf(
                    'data-autoplay="%s" data-speed="%s" data-loop="%s" data-pause="%s" data-dots="%s"',
                    esc_attr($atts['autoplay']),
                    esc_attr($atts['autoplay_speed']),
                    esc_attr($atts['loop']),
                    esc_attr($atts['pause_on_hover']),
                    esc_attr($atts['show_dots'])
                );
            } else {
                $section_classes .= ' me-grid-layout';
            }

            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            echo '<section class="' . esc_attr($section_classes) . '" style="' . esc_attr($grid_vars_style) . '" ' . $section_attrs . '>';
        }

        // Header
        $design_settings = get_option('movie_engine_design_settings', []);
        $view_all_url = $atts['view_all_url'];
        if (filter_var($atts['show_view_all'], FILTER_VALIDATE_BOOLEAN) && empty($view_all_url)) {
            if (!empty($design_settings['movie_archive_page'])) {
                $view_all_url = get_permalink($design_settings['movie_archive_page']);
            } elseif (!empty($design_settings['browse_page'])) {
                $view_all_url = get_permalink($design_settings['browse_page']);
            } else {
                $view_all_url = get_post_type_archive_link('movie_engine_movie');
            }
        }

        if (!empty($atts['title'])) {
            echo '<div class="movie-engine-section-title me-section-header">';
            echo '<h2>' . esc_html($atts['title']) . '</h2>';
            if (filter_var($atts['show_view_all'], FILTER_VALIDATE_BOOLEAN) && !empty($view_all_url)) {
                echo '<a href="' . esc_url($view_all_url) . '" class="movie-engine-view-all me-view-all">';
                echo esc_html($atts['view_all_text']) . ' <span class="fa-solid fa-angle-right"></span>';
                echo '</a>';
            }
            echo '</div>';
        }

        if ($layout === 'pills') {
            echo '<div class="movie-engine-genres-widget">';
        } elseif ($layout === 'slider') {
            $slider_grid_style = $grid_items_style . " display: flex !important; flex-wrap: nowrap !important; overflow-x: auto !important;";
            echo '<div class="movie-engine-items-grid" style="' . esc_attr($slider_grid_style) . '">';
        } else {
            echo '<div class="movie-engine-items-grid me-term-grid" style="' . esc_attr($grid_items_style) . '">';
        }

        foreach ($terms as $term) {
            $link = get_term_link($term);
            if ($layout === 'pills') {
                echo '<a href="' . esc_url($link) . '" class="me-genre-tag">';
                echo esc_html($term->name);
                if (filter_var($atts['show_count'], FILTER_VALIDATE_BOOLEAN)) {
                    echo ' <span class="count">(' . esc_html($term->count) . ')</span>';
                }
                echo '</a>';
            } else {
                $term_image = '';
                $show_image = filter_var($atts['show_image'], FILTER_VALIDATE_BOOLEAN);

                if ($show_image) {
                    $post_with_image = get_posts([
                        'post_type'      => ['movie_engine_movie', 'movie_engine_series'],
                        'posts_per_page' => 1,
                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
                        'tax_query'      => [
                            [
                                'taxonomy' => $term->taxonomy,
                                'field'    => 'term_id',
                                'terms'    => $term->term_id,
                            ],
                        ],
                    ]);
                    if (!empty($post_with_image)) {
                        $term_image = get_the_post_thumbnail_url($post_with_image[0]->ID, 'medium');
                    }
                }

                $ratio = sanitize_text_field($atts['ratio']);
                $custom_ratio_style = ($ratio === 'custom' && !empty($atts['custom_ratio'])) ? ' style="--me-custom-ratio: ' . esc_attr($atts['custom_ratio']) . ';"' : '';

                // Card layout
                $card_class = $term_image ? 'me-term-card has-image' : 'me-term-card';
                $card_class .= ' me-ratio-' . $ratio;

                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                echo '<a href="' . esc_url($link) . '" class="' . esc_attr($card_class) . '" ' . $custom_ratio_style . '>';

                if ($term_image) {
                    echo '<div class="me-term-card-bg" style="background-image: url(' . esc_url($term_image) . ');"></div>';
                    echo '<div class="me-term-card-overlay"></div>';
                } else {
                    echo '<div class="me-term-card-icon"><span class="fa-solid fa-folder"></span></div>';
                }

                echo '<div class="me-term-card-content">';
                echo '<h4 class="me-term-name">' . esc_html($term->name) . '</h4>';
                if (filter_var($atts['show_count'], FILTER_VALIDATE_BOOLEAN)) {
                    /* translators: %s: number of items */
                    $count_text = sprintf(_n('%s Item', '%s Items', $term->count, 'cine-movie-engine'), number_format_i18n($term->count));
                    echo '<span class="me-term-count">' . esc_html($count_text) . '</span>';
                }
                echo '</div>';
                echo '</a>';
            }
        }

        echo '</div>';

        if ($layout === 'slider' || $layout === 'grid') {
            echo '</section>';
        }

        echo '</div>';

        return ob_get_clean();
    }

    /**
     * Render Genres list/cloud
     */
    public function render_genres($atts) {
        if (!isset($atts['type'])) $atts['type'] = 'genre';
        return $this->render_taxonomies($atts);
    }

    /**
     * Render Years list/cloud
     */
    public function render_years($atts) {
        if (!isset($atts['type'])) $atts['type'] = 'year';
        if (!isset($atts['order'])) $atts['order'] = 'DESC';
        return $this->render_taxonomies($atts);
    }

    /**
     * Render Generic Taxonomy Terms
     */
    public function render_taxonomy_terms($atts) {
        return $this->render_taxonomies($atts);
    }

    /**
     * Render Advanced Hero Section
     *
     * @param array $atts Shortcode attributes.
     * @return string
     *
     * Optional params: limit, type, ids, post_type, style, height, custom_class,
     *   autoplay, autoplay_speed, loop, pause_on_hover, show_nav, show_dots,
     *   show_rating, show_imdb_badge, show_description, description_length, show_trailer, show_meta,
     *   show_starring, show_genres, starring_limit, genres_limit, show_play_button,
     *   play_button_text, trailer_button_text, genre, year, orderby, order.
     */
    public function render_hero($atts) {
        $atts = shortcode_atts([
            'limit'              => 5,
            'type'               => 'trending', // trending, new_releases, upcoming, top_rated, popular, etc.
            'ids'                => '', // post ids
            'post_type'          => 'movie_engine_movie,movie_engine_series', // post types
            'style'              => 'style1', // style1, style2, style3
            'height'             => '700px', // px,vh
            'custom_class'       => '', // custom class
            'autoplay'           => 'true', // true, false
            'autoplay_speed'     => '5000', // 5000, 10000, 15000, 20000, 25000, 30000
            'loop'               => 'true', // true, false
            'pause_on_hover'     => 'true', // true, false
            'fullwidth'          => 'false', // true, false
            'show_nav'           => 'true', // true, false
            'show_dots'          => 'true', // true, false
            'show_rating'        => 'true', // true, false
            'show_imdb_badge'    => 'true', // true, false
            'show_description'   => 'true', // true, false
            'description_length' => 30, // 30, 50, 100, 150, 200
            'show_trailer'       => 'true', // true, false
            'show_meta'          => 'true', // true, false
            'show_starring'      => 'true', // true, false
            'show_genres'        => 'true', // true, false
            'starring_limit'     => 6, // 2,3,4,6,7,10
            'genres_limit'       => 3, // 2,3,4,6,7,10
            'show_play_button'   => 'true', // true, false
            'play_button_text'   => esc_html__('Watch Now', 'cine-movie-engine'),
            'trailer_button_text' => esc_html__('Trailer', 'cine-movie-engine'),
            'genre'              => '', // genre slug
            'year'               => '', // year
            'orderby'            => '', // order by
            'order'              => 'DESC', // order
            'show_watchlist'     => 'true', // true, false
        ], $atts);

        if (is_admin() && (!defined('DOING_AJAX') || !DOING_AJAX)) {
            return '<div class="movie-engine-shortcode-placeholder" style="padding: 20px; border: 1px dashed #ccc; text-align: center; background: #f9f9f9;">' . esc_html__('Hero Section: [movie_engine_hero]', 'cine-movie-engine') . '</div>';
        }

        $limit = intval($atts['limit']);
        $post_types = explode(',', $atts['post_type']);
        $post_types = array_map('trim', $post_types);

        $query_args = [
            'post_type'      => $post_types,
            'posts_per_page' => $limit,
            'post_status'    => 'publish',
        ];

        if (!empty($atts['ids'])) {
            $query_args['post__in'] = array_map('intval', explode(',', $atts['ids']));
            $query_args['orderby'] = 'post__in';
        } else {
            switch ($atts['type']) {
                case 'trending':
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                    $query_args['meta_key'] = '_movie_engine_views';
                    $query_args['orderby'] = 'meta_value_num';
                    $query_args['order'] = 'DESC';
                    break;
                case 'new_releases':
                    $query_args['orderby'] = 'date';
                    $query_args['order'] = 'DESC';
                    break;
                case 'top_rated':
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                    $query_args['meta_key'] = '_movie_engine_rating';
                    $query_args['orderby'] = 'meta_value_num';
                    $query_args['order'] = 'DESC';
                    break;
                case 'upcoming':
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                    $query_args['meta_key'] = '_movie_engine_release_date';
                    $query_args['orderby'] = 'meta_value';
                    $query_args['order'] = 'ASC';
                    $query_args['meta_query'][] = [
                        'key'     => '_movie_engine_release_date',
                        'value'   => gmdate('Y-m-d'),
                        'compare' => '>',
                        'type'    => 'DATE'
                    ];
                    break;
                default:
                    if (!empty($atts['orderby'])) {
                        $query_args['orderby'] = sanitize_text_field($atts['orderby']);
                        $query_args['order'] = in_array(strtoupper($atts['order']), ['ASC', 'DESC'], true) ? strtoupper($atts['order']) : 'DESC';
                        if (in_array($query_args['orderby'], ['meta_value_num', 'meta_value'], true)) {
                            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                            $query_args['meta_key'] = $query_args['orderby'] === 'meta_value_num' ? '_movie_engine_rating' : '_movie_engine_release_date';
                        }
                    } else {
                        $query_args['orderby'] = 'date';
                        $query_args['order'] = 'DESC';
                    }
                    break;
            }
        }

        if (!empty($atts['genre'])) {
            $genre_slugs = array_map('trim', explode(',', $atts['genre']));
            $query_args['tax_query'][] = [
                'taxonomy' => 'movie_engine_genre',
                'field'    => 'slug',
                'terms'    => $genre_slugs,
                'operator' => count($genre_slugs) > 1 ? 'IN' : '=',
            ];
        }
        if (!empty($atts['year'])) {
            $year_slugs = array_map('trim', explode(',', $atts['year']));
            $query_args['tax_query'][] = [
                'taxonomy' => 'movie_engine_year',
                'field'    => 'slug',
                'terms'    => $year_slugs,
                'operator' => count($year_slugs) > 1 ? 'IN' : '=',
            ];
        }

        $query = new \WP_Query($query_args);

        if (!$query->have_posts()) {
            return '';
        }

        // Flag so Design::render_details_modal() outputs share/playlist/login modals on this page
        $GLOBALS['movie_engine_needs_interaction_modals'] = true;

        ob_start();
        $items = $query->posts;

        \MovieEngine\Utils\TemplateLoader::get_template_part('global/hero', '', ['items' => $items, 'atts' => $atts]);

        wp_reset_postdata();

        return ob_get_clean();
    }
}
