<?php
/**
 * Gutenberg blocks bootstrap (isolated from core shortcode classes).
 *
 * @package MovieEngine
 */

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

/**
 * Registers Movie Engine blocks and editor assets. Front output uses existing shortcodes via do_shortcode().
 */
final class Movie_Engine_Gutenberg
{
    public const LIBRARY_HANDLE = 'movie-engine-library-block-editor';

    private const LIBRARY_REL_PATH = 'gutenberg/blocks/library';

    public const SEARCH_BAR_HANDLE = 'movie-engine-search-bar-block-editor';

    private const SEARCH_BAR_REL_PATH = 'gutenberg/blocks/search-bar';

    public const HERO_HANDLE = 'movie-engine-hero-block-editor';

    private const HERO_REL_PATH = 'gutenberg/blocks/hero';

    public const LIVE_SEARCH_HANDLE = 'movie-engine-live-search-block-editor';

    private const LIVE_SEARCH_REL_PATH = 'gutenberg/blocks/live-search';

    public const PERSON_LIST_HANDLE = 'movie-engine-person-list-block-editor';

    private const PERSON_LIST_REL_PATH = 'gutenberg/blocks/person-list';

    public const USER_MENU_HANDLE = 'movie-engine-user-menu-block-editor';

    private const USER_MENU_REL_PATH = 'gutenberg/blocks/user-menu';

    public const GRID_HANDLE = 'movie-engine-grid-block-editor';

    private const GRID_REL_PATH = 'gutenberg/blocks/grid';

    public const SEARCH_HANDLE = 'movie-engine-search-block-editor';

    private const SEARCH_REL_PATH = 'gutenberg/blocks/search';

    public const TAXONOMIES_HANDLE = 'movie-engine-taxonomies-block-editor';

    private const TAXONOMIES_REL_PATH = 'gutenberg/blocks/taxonomies';

    public static function init(): void
    {
        add_filter('block_categories_all', [self::class, 'register_block_category'], 5, 2);
        add_action('init', [self::class, 'register_blocks'], 20);
        add_action('enqueue_block_editor_assets', [self::class, 'localize_library_defaults'], 9);
    }

    /**
     * @param array<int,array<string,mixed>> $categories Default categories.
     * @param mixed                           $editor_context Editor context (WP 5.8+).
     * @return array<int,array<string,mixed>>
     */
    public static function register_block_category($categories, $editor_context = null)
    {
        return array_merge(
            [
                [
                    'slug' => 'movie-engine',
                    'title' => __('Movie Engine', 'cine-movie-engine'),
                    'icon' => null,
                ],
            ],
            is_array($categories) ? $categories : []
        );
    }

    public static function register_blocks(): void
    {
        if (!function_exists('register_block_type')) {
            return;
        }

        $preview_leaf = \MovieEngine\Frontend\Assets::register_block_editor_preview_styles();

        self::register_editor_block(
            self::LIBRARY_REL_PATH,
            self::LIBRARY_HANDLE,
            [self::class, 'render_library_block'],
            $preview_leaf
        );

        self::register_editor_block(
            self::SEARCH_BAR_REL_PATH,
            self::SEARCH_BAR_HANDLE,
            [self::class, 'render_search_bar_block'],
            $preview_leaf
        );

        self::register_editor_block(
            self::HERO_REL_PATH,
            self::HERO_HANDLE,
            [self::class, 'render_hero_block'],
            $preview_leaf
        );

        self::register_editor_block(
            self::LIVE_SEARCH_REL_PATH,
            self::LIVE_SEARCH_HANDLE,
            [self::class, 'render_live_search_block'],
            $preview_leaf
        );

        self::register_editor_block(
            self::PERSON_LIST_REL_PATH,
            self::PERSON_LIST_HANDLE,
            [self::class, 'render_person_list_block'],
            $preview_leaf
        );

        self::register_editor_block(
            self::USER_MENU_REL_PATH,
            self::USER_MENU_HANDLE,
            [self::class, 'render_user_menu_block'],
            $preview_leaf
        );

        self::register_editor_block(
            self::GRID_REL_PATH,
            self::GRID_HANDLE,
            [self::class, 'render_grid_block'],
            $preview_leaf
        );

        self::register_editor_block(
            self::SEARCH_REL_PATH,
            self::SEARCH_HANDLE,
            [self::class, 'render_search_block'],
            $preview_leaf
        );

        self::register_editor_block(
            self::TAXONOMIES_REL_PATH,
            self::TAXONOMIES_HANDLE,
            [self::class, 'render_taxonomies_block'],
            $preview_leaf
        );
    }

    /**
     * Register one block from gutenberg/blocks/{name}/ (build/index.js + block.json).
     *
     * @param callable $render_callback function( array $attributes, string $content, \WP_Block $block ): string
     */
    private static function register_editor_block(string $relPath, string $handle, callable $render_callback, string $previewLeaf): void
    {
        $block_dir = trailingslashit(MOVIE_ENGINE_PATH) . $relPath;
        $build_js = $block_dir . '/build/index.js';
        $build_css = $block_dir . '/build/index.css';
        $build_asset = $block_dir . '/build/index.asset.php';

        if (!is_readable($build_js) || !is_readable($build_asset)) {
            return;
        }

        $asset = include $build_asset;
        $dependencies = isset($asset['dependencies']) && is_array($asset['dependencies'])
            ? $asset['dependencies']
            : [];
        $version = isset($asset['version']) ? $asset['version'] : MOVIE_ENGINE_VERSION;

        wp_register_script(
            $handle,
            MOVIE_ENGINE_URL . $relPath . '/build/index.js',
            $dependencies,
            $version,
            true
        );

        $style_handle = $handle . '-editor-style';
        if (is_readable($build_css)) {
            wp_register_style(
                $style_handle,
                MOVIE_ENGINE_URL . $relPath . '/build/index.css',
                [$previewLeaf],
                $version
            );
        }

        if (function_exists('wp_set_script_translations')) {
            wp_set_script_translations($handle, 'cine-movie-engine', MOVIE_ENGINE_PATH . 'languages');
        }

        $args = [
            'render_callback' => $render_callback,
        ];

        if (wp_script_is($handle, 'registered')) {
            $args['editor_script'] = $handle;
        }
        if (wp_style_is($style_handle, 'registered')) {
            $args['editor_style'] = $style_handle;
        }

        register_block_type($block_dir, $args);
    }

    /**
     * Design grid defaults for library / person-list inspector RangeControl hints.
     *
     * @return array<string,int>
     */
    private static function get_design_defaults_payload(): array
    {
        $design = get_option('movie_engine_design_settings', []);
        if (!is_array($design)) {
            $design = [];
        }

        return [
            'archiveColumnsDesktop' => isset($design['archive_columns_desktop']) ? (int) $design['archive_columns_desktop'] : 4,
            'archiveColumnsLaptop' => isset($design['archive_columns_laptop']) ? (int) $design['archive_columns_laptop'] : 4,
            'archiveColumnsTablet' => isset($design['archive_columns_tablet']) ? (int) $design['archive_columns_tablet'] : 3,
            'archiveColumnsMobile' => isset($design['archive_columns_mobile']) ? (int) $design['archive_columns_mobile'] : 2,
            'archiveItemsPerPage' => isset($design['archive_items_per_page']) ? (int) $design['archive_items_per_page'] : 12,
            'searchItemsPerPage' => isset($design['search_items_per_page']) ? (int) $design['search_items_per_page'] : 12,
        ];
    }

    /**
     * Pass design defaults for library and person-list block inspectors.
     */
    public static function localize_library_defaults(): void
    {
        $payload = self::get_design_defaults_payload();

        if (wp_script_is(self::LIBRARY_HANDLE, 'registered')) {
            wp_localize_script(self::LIBRARY_HANDLE, 'movieEngineLibraryDefaults', $payload);
        }

        if (wp_script_is(self::PERSON_LIST_HANDLE, 'registered')) {
            wp_localize_script(self::PERSON_LIST_HANDLE, 'movieEnginePersonListDefaults', $payload);
        }

        if (wp_script_is(self::GRID_HANDLE, 'registered')) {
            wp_localize_script(self::GRID_HANDLE, 'movieEngineLibraryDefaults', $payload);
        }

        if (wp_script_is(self::SEARCH_HANDLE, 'registered')) {
            wp_localize_script(self::SEARCH_HANDLE, 'movieEngineLibraryDefaults', $payload);
        }
    }

    /**
     * @param array<string,mixed> $attributes Block attributes.
     */
    public static function render_library_block($attributes, $content, $block): string
    {
        $attributes = is_array($attributes) ? $attributes : [];
        $shortcode = self::build_library_shortcode_string($attributes);
        return do_shortcode($shortcode);
    }

    /**
     * @param array<string,mixed> $attributes Block attributes.
     */
    public static function render_search_bar_block($attributes, $content, $block): string
    {
        $attributes = is_array($attributes) ? $attributes : [];
        $shortcode = self::build_search_bar_shortcode_string($attributes);
        return do_shortcode($shortcode);
    }

    /**
     * @param array<string,mixed> $attributes Block attributes.
     */
    public static function render_hero_block($attributes, $content, $block): string
    {
        $attributes = is_array($attributes) ? $attributes : [];
        $shortcode = self::build_hero_shortcode_string($attributes);
        return do_shortcode($shortcode);
    }

    /**
     * @param array<string,mixed> $attributes Block attributes.
     */
    public static function render_live_search_block($attributes, $content, $block): string
    {
        $attributes = is_array($attributes) ? $attributes : [];
        $shortcode = self::build_live_search_shortcode_string($attributes);
        return do_shortcode($shortcode);
    }

    /**
     * @param array<string,mixed> $attributes Block attributes.
     */
    public static function render_person_list_block($attributes, $content, $block): string
    {
        $attributes = is_array($attributes) ? $attributes : [];
        $shortcode = self::build_person_list_shortcode_string($attributes);
        return do_shortcode($shortcode);
    }

    /**
     * @param array<string,mixed> $attributes Block attributes.
     */
    public static function render_user_menu_block($attributes, $content, $block): string
    {
        $attributes = is_array($attributes) ? $attributes : [];
        $shortcode = self::build_user_menu_shortcode_string($attributes);
        return do_shortcode($shortcode);
    }

    /**
     * @param array<string,mixed> $attributes Block attributes.
     */
    public static function render_grid_block($attributes, $content, $block): string
    {
        $attributes = is_array($attributes) ? $attributes : [];
        $shortcode = self::build_movie_grid_shortcode_string($attributes);
        return do_shortcode($shortcode);
    }

    /**
     * @param array<string,mixed> $attributes Block attributes.
     */
    public static function render_search_block($attributes, $content, $block): string
    {
        $attributes = is_array($attributes) ? $attributes : [];
        $shortcode = self::build_movie_search_shortcode_string($attributes);
        return do_shortcode($shortcode);
    }

    /**
     * @param array<string,mixed> $attributes Block attributes.
     */
    public static function render_taxonomies_block($attributes, $content, $block): string
    {
        $attributes = is_array($attributes) ? $attributes : [];
        $shortcode = self::build_taxonomies_shortcode_string($attributes);
        return do_shortcode($shortcode);
    }

    /**
     * Maps block attributes to [movie_engine_search_bar] (Shortcodes::render_search_bar).
     *
     * @param array<string,mixed> $attributes Block attributes.
     */
    private static function build_search_bar_shortcode_string(array $attributes): string
    {
        $parts = [];

        $appendStr = static function ($sc_key, $value) use (&$parts): void {
            if ($value === null) {
                return;
            }
            $s = trim((string) $value);
            if ($s === '') {
                return;
            }
            $parts[] = sprintf('%s="%s"', $sc_key, esc_attr($s));
        };

        $appendStr('placeholder', $attributes['placeholder'] ?? null);
        $appendStr('button_text', $attributes['buttonText'] ?? null);
        $appendStr('custom_class', $attributes['customClass'] ?? null);

        $inner = implode(' ', $parts);

        return '[movie_engine_search_bar' . ($inner !== '' ? ' ' . $inner : '') . ']';
    }

    /**
     * Maps block attributes to [movie_engine_library] shortcode (parity with Shortcodes::render_library).
     *
     * @param array<string,mixed> $attributes Block attributes.
     */
    private static function build_library_shortcode_string(array $attributes): string
    {
        return self::build_library_like_shortcode_string('movie_engine_library', $attributes, true);
    }

    /**
     * [movie_engine_grid] — same attributes as library; PHP forces show_search off.
     *
     * @param array<string,mixed> $attributes Block attributes.
     */
    private static function build_movie_grid_shortcode_string(array $attributes): string
    {
        return self::build_library_like_shortcode_string('movie_engine_grid', $attributes, false);
    }

    /**
     * [movie_engine_search] — same attributes as library; PHP forces show_search on.
     *
     * @param array<string,mixed> $attributes Block attributes.
     */
    private static function build_movie_search_shortcode_string(array $attributes): string
    {
        return self::build_library_like_shortcode_string('movie_engine_search', $attributes, false);
    }

    /**
     * Shared builder for library / grid / search shortcodes (grid & search omit show_search; PHP overrides).
     *
     * @param array<string,mixed> $attributes Block attributes.
     */
    private static function build_library_like_shortcode_string(string $tag, array $attributes, bool $include_show_search_attr): string
    {
        $parts = [];

        $appendInt = static function ($key, $value) use (&$parts): void {
            $v = (int) $value;
            if ($v > 0) {
                $parts[] = sprintf('%s="%d"', $key, $v);
            }
        };

        $appendStr = static function ($sc_key, $value) use (&$parts): void {
            if ($value === null) {
                return;
            }
            $s = trim((string) $value);
            if ($s === '') {
                return;
            }
            $parts[] = sprintf('%s="%s"', $sc_key, esc_attr($s));
        };

        $appendBoolIfDiff = static function ($sc_key, $value, bool $default) use (&$parts): void {
            $v = (bool) $value;
            if ($v === $default) {
                return;
            }
            $parts[] = sprintf('%s="%s"', $sc_key, $v ? 'true' : 'false');
        };

        if (isset($attributes['limit'])) {
            $appendInt('limit', $attributes['limit']);
        }
        foreach (['columns' => 'columns', 'columnsLaptop' => 'columns_laptop', 'columnsTablet' => 'columns_tablet', 'columnsMobile' => 'columns_mobile'] as $attr => $sc) {
            if (isset($attributes[$attr])) {
                $appendInt($sc, $attributes[$attr]);
            }
        }

        foreach (['postType' => 'post_type', 'genre' => 'genre', 'year' => 'year', 'language' => 'language', 'ids' => 'ids', 'access' => 'access'] as $attr => $sc) {
            if (!empty($attributes[$attr])) {
                $appendStr($sc, $attributes[$attr]);
            }
        }

        $orderby = isset($attributes['orderby']) ? (string) $attributes['orderby'] : 'date';
        $order = isset($attributes['order']) ? (string) $attributes['order'] : 'DESC';
        if ($orderby !== 'date') {
            $appendStr('orderby', $orderby);
        }
        if ($order !== 'DESC') {
            $appendStr('order', $order);
        }

        if (!empty($attributes['queryType'])) {
            $appendStr('type', $attributes['queryType']);
        }

        $layout = isset($attributes['layout']) ? (string) $attributes['layout'] : 'grid';
        if (!in_array($layout, ['grid', 'slider'], true)) {
            $layout = 'grid';
        }
        if ($layout !== 'grid') {
            $appendStr('layout', $layout);
        }

        $appendStr('title', $attributes['title'] ?? '');

        if (!empty($attributes['viewAllUrl'])) {
            $appendStr('view_all_url', $attributes['viewAllUrl']);
        }
        if (!empty($attributes['viewAllText'])) {
            $appendStr('view_all_text', $attributes['viewAllText']);
        }

        $bool_map = [
            'showFilters' => ['show_filters', false],
            'enableAjax' => ['enable_ajax', true],
            'showViewAll' => ['show_view_all', true],
            'pagination' => ['pagination', true],
            'autoplay' => ['autoplay', false],
            'loop' => ['loop', false],
            'pauseOnHover' => ['pause_on_hover', true],
            'showDots' => ['show_dots', false],
            'showRating' => ['show_rating', true],
            'showYear' => ['show_year', true],
            'showQuality' => ['show_quality', true],
            'showType' => ['show_type', true],
            'showPrice' => ['show_price', false],
            'showAccessLabel' => ['show_access_label', false],
            'showFree' => ['show_free', false],
            'showBadges' => ['show_badges', true],
            'showPremiumBadge' => ['show_premium_badge', true],
            'showPpvBadge' => ['show_ppv_badge', true],
        ];
        if ($include_show_search_attr) {
            $bool_map = ['showSearch' => ['show_search', false]] + $bool_map;
        }
        foreach ($bool_map as $attr => $meta) {
            if (array_key_exists($attr, $attributes)) {
                $appendBoolIfDiff($meta[0], $attributes[$attr], $meta[1]);
            }
        }

        $filter_layout = isset($attributes['filterLayout']) ? (string) $attributes['filterLayout'] : 'sidebar';
        if ($filter_layout !== 'sidebar' && in_array($filter_layout, ['sidebar', 'top'], true)) {
            $appendStr('filter_layout', $filter_layout);
        }

        if (isset($attributes['autoplaySpeed'])) {
            $speed = (int) $attributes['autoplaySpeed'];
            if ($speed > 0 && $speed !== 3000) {
                $parts[] = sprintf('autoplay_speed="%d"', $speed);
            }
        }

        // Use isset + trim (not empty): empty('0') is true in PHP, so gap="0" must still be passed.
        if (isset($attributes['gap']) && trim((string) $attributes['gap']) !== '') {
            $appendStr('gap', $attributes['gap']);
        }

        $ratio = isset($attributes['ratio']) ? (string) $attributes['ratio'] : 'portrait';
        if ($ratio !== 'portrait') {
            $appendStr('ratio', $ratio);
        }
        if ($ratio === 'custom' && !empty($attributes['customRatio'])) {
            $appendStr('custom_ratio', $attributes['customRatio']);
        }

        $card_style = isset($attributes['cardStyle']) ? (string) $attributes['cardStyle'] : 'classic';
        if ($card_style !== 'classic') {
            $appendStr('style', $card_style);
        }

        if (!empty($attributes['customClass'])) {
            $appendStr('custom_class', $attributes['customClass']);
        }

        if (!empty($attributes['backgroundColor'])) {
            $appendStr('background_color', $attributes['backgroundColor']);
        }

        $inner = implode(' ', $parts);

        return '[' . $tag . ($inner !== '' ? ' ' . $inner : '') . ']';
    }

    /**
     * Maps block attributes to [movie_engine_hero] (Shortcodes::render_hero).
     *
     * @param array<string,mixed> $attributes Block attributes.
     */
    private static function build_hero_shortcode_string(array $attributes): string
    {
        $parts = [];

        $appendStr = static function ($sc_key, $value) use (&$parts): void {
            if ($value === null) {
                return;
            }
            $s = trim((string) $value);
            if ($s === '') {
                return;
            }
            $parts[] = sprintf('%s="%s"', $sc_key, esc_attr($s));
        };

        $appendBoolIfDiff = static function ($sc_key, $value, bool $default) use (&$parts): void {
            $v = (bool) $value;
            if ($v === $default) {
                return;
            }
            $parts[] = sprintf('%s="%s"', $sc_key, $v ? 'true' : 'false');
        };

        if (isset($attributes['limit'])) {
            $lim = (int) $attributes['limit'];
            if ($lim > 0 && $lim !== 5) {
                $parts[] = sprintf('limit="%d"', $lim);
            }
        }

        $type = isset($attributes['type']) ? (string) $attributes['type'] : 'trending';
        if ($type !== 'trending') {
            $appendStr('type', $type);
        }

        if (!empty($attributes['ids'])) {
            $appendStr('ids', $attributes['ids']);
        }

        $default_post_types = 'movie_engine_movie,movie_engine_series';
        $post_type = isset($attributes['postType']) ? trim((string) $attributes['postType']) : $default_post_types;
        if ($post_type !== '' && $post_type !== $default_post_types) {
            $appendStr('post_type', $post_type);
        }

        $style = isset($attributes['style']) ? (string) $attributes['style'] : 'style1';
        if ($style !== 'style1') {
            $appendStr('style', $style);
        }

        $height = isset($attributes['height']) ? trim((string) $attributes['height']) : '700px';
        if ($height !== '' && $height !== '700px') {
            $appendStr('height', $height);
        }

        if (!empty($attributes['customClass'])) {
            $appendStr('custom_class', $attributes['customClass']);
        }

        if (!empty($attributes['genre'])) {
            $appendStr('genre', $attributes['genre']);
        }
        if (!empty($attributes['year'])) {
            $appendStr('year', $attributes['year']);
        }

        $orderby = isset($attributes['orderby']) ? trim((string) $attributes['orderby']) : '';
        if ($orderby !== '') {
            $appendStr('orderby', $orderby);
        }

        $order = isset($attributes['order']) ? (string) $attributes['order'] : 'DESC';
        if ($order !== 'DESC') {
            $appendStr('order', $order);
        }

        if (isset($attributes['descriptionLength'])) {
            $dl = (int) $attributes['descriptionLength'];
            if ($dl > 0 && $dl !== 30) {
                $parts[] = sprintf('description_length="%d"', $dl);
            }
        }

        if (isset($attributes['starringLimit'])) {
            $sl = (int) $attributes['starringLimit'];
            if ($sl > 0 && $sl !== 6) {
                $parts[] = sprintf('starring_limit="%d"', $sl);
            }
        }

        if (isset($attributes['genresLimit'])) {
            $gl = (int) $attributes['genresLimit'];
            if ($gl > 0 && $gl !== 3) {
                $parts[] = sprintf('genres_limit="%d"', $gl);
            }
        }

        if (isset($attributes['autoplaySpeed'])) {
            $sp = (int) $attributes['autoplaySpeed'];
            if ($sp > 0 && $sp !== 5000) {
                $parts[] = sprintf('autoplay_speed="%d"', $sp);
            }
        }

        if (!empty($attributes['playButtonText'])) {
            $appendStr('play_button_text', $attributes['playButtonText']);
        }
        if (!empty($attributes['trailerButtonText'])) {
            $appendStr('trailer_button_text', $attributes['trailerButtonText']);
        }

        $bool_map = [
            'autoplay' => ['autoplay', true],
            'loop' => ['loop', true],
            'pauseOnHover' => ['pause_on_hover', true],
            'fullwidth' => ['fullwidth', false],
            'showNav' => ['show_nav', true],
            'showDots' => ['show_dots', true],
            'showRating' => ['show_rating', true],
            'showImdbBadge' => ['show_imdb_badge', true],
            'showDescription' => ['show_description', true],
            'showTrailer' => ['show_trailer', true],
            'showMeta' => ['show_meta', true],
            'showStarring' => ['show_starring', true],
            'showGenres' => ['show_genres', true],
            'showPlayButton' => ['show_play_button', true],
            'showWatchlist' => ['show_watchlist', true],
        ];
        foreach ($bool_map as $attr => $meta) {
            if (array_key_exists($attr, $attributes)) {
                $appendBoolIfDiff($meta[0], $attributes[$attr], $meta[1]);
            }
        }

        $inner = implode(' ', $parts);

        return '[movie_engine_hero' . ($inner !== '' ? ' ' . $inner : '') . ']';
    }

    /**
     * Maps block attributes to [movie_engine_live_search] (Shortcodes::render_live_search).
     *
     * @param array<string,mixed> $attributes Block attributes.
     */
    private static function build_live_search_shortcode_string(array $attributes): string
    {
        $parts = [];

        $appendStr = static function ($sc_key, $value) use (&$parts): void {
            if ($value === null) {
                return;
            }
            $s = trim((string) $value);
            if ($s === '') {
                return;
            }
            $parts[] = sprintf('%s="%s"', $sc_key, esc_attr($s));
        };

        $appendBoolIfDiff = static function ($sc_key, $value, bool $default) use (&$parts): void {
            $v = (bool) $value;
            if ($v === $default) {
                return;
            }
            $parts[] = sprintf('%s="%s"', $sc_key, $v ? 'true' : 'false');
        };

        $appendStr('placeholder', $attributes['placeholder'] ?? null);
        if (array_key_exists('showIcon', $attributes)) {
            $appendBoolIfDiff('show_icon', $attributes['showIcon'], true);
        }
        $appendStr('custom_class', $attributes['customClass'] ?? null);

        $inner = implode(' ', $parts);

        return '[movie_engine_live_search' . ($inner !== '' ? ' ' . $inner : '') . ']';
    }

    /**
     * Maps block attributes to [movie_engine_person_list] (Shortcodes::render_person_list).
     *
     * @param array<string,mixed> $attributes Block attributes.
     */
    private static function build_person_list_shortcode_string(array $attributes): string
    {
        $parts = [];

        $appendStr = static function ($sc_key, $value) use (&$parts): void {
            if ($value === null) {
                return;
            }
            $s = trim((string) $value);
            if ($s === '') {
                return;
            }
            $parts[] = sprintf('%s="%s"', $sc_key, esc_attr($s));
        };

        $appendBoolIfDiff = static function ($sc_key, $value, bool $default) use (&$parts): void {
            $v = (bool) $value;
            if ($v === $default) {
                return;
            }
            $parts[] = sprintf('%s="%s"', $sc_key, $v ? 'true' : 'false');
        };

        if (isset($attributes['limit'])) {
            $lim = (int) $attributes['limit'];
            if ($lim > 0 && $lim !== 24) {
                $parts[] = sprintf('limit="%d"', $lim);
            }
        }

        foreach (['columns' => 'columns', 'columnsLaptop' => 'columns_laptop', 'columnsTablet' => 'columns_tablet', 'columnsMobile' => 'columns_mobile'] as $attr => $sc) {
            if (!empty($attributes[$attr])) {
                $v = (int) $attributes[$attr];
                if ($v > 0) {
                    $parts[] = sprintf('%s="%d"', $sc, $v);
                }
            }
        }

        $orderby = isset($attributes['orderby']) ? (string) $attributes['orderby'] : 'title';
        if ($orderby !== 'title') {
            $appendStr('orderby', $orderby);
        }

        $order = isset($attributes['order']) ? (string) $attributes['order'] : 'ASC';
        if ($order !== 'ASC') {
            $appendStr('order', $order);
        }

        if (!empty($attributes['department'])) {
            $appendStr('department', $attributes['department']);
        }
        if (!empty($attributes['search'])) {
            $appendStr('search', $attributes['search']);
        }

        if (array_key_exists('enableAjax', $attributes)) {
            $appendBoolIfDiff('enable_ajax', $attributes['enableAjax'], true);
        }
        if (array_key_exists('showFilters', $attributes)) {
            $appendBoolIfDiff('show_filters', $attributes['showFilters'], false);
        }

        $filter_layout = isset($attributes['filterLayout']) ? (string) $attributes['filterLayout'] : 'sidebar';
        if ($filter_layout !== 'sidebar' && in_array($filter_layout, ['sidebar', 'top'], true)) {
            $appendStr('filter_layout', $filter_layout);
        }

        $ratio = isset($attributes['ratio']) ? (string) $attributes['ratio'] : 'portrait';
        if ($ratio !== 'portrait') {
            $appendStr('ratio', $ratio);
        }
        if ($ratio === 'custom' && !empty($attributes['customRatio'])) {
            $appendStr('custom_ratio', $attributes['customRatio']);
        }

        $card_style = isset($attributes['cardStyle']) ? (string) $attributes['cardStyle'] : 'classic';
        if ($card_style !== 'classic') {
            $appendStr('style', $card_style);
        }

        $layout = isset($attributes['layout']) ? (string) $attributes['layout'] : 'grid';
        if ($layout !== 'grid' && in_array($layout, ['grid', 'slider'], true)) {
            $appendStr('layout', $layout);
        }

        if (array_key_exists('autoplay', $attributes)) {
            $appendBoolIfDiff('autoplay', $attributes['autoplay'], false);
        }
        if (isset($attributes['autoplaySpeed'])) {
            $sp = (int) $attributes['autoplaySpeed'];
            if ($sp > 0 && $sp !== 3000) {
                $parts[] = sprintf('autoplay_speed="%d"', $sp);
            }
        }
        if (array_key_exists('loop', $attributes)) {
            $appendBoolIfDiff('loop', $attributes['loop'], false);
        }
        if (array_key_exists('pauseOnHover', $attributes)) {
            $appendBoolIfDiff('pause_on_hover', $attributes['pauseOnHover'], true);
        }
        if (array_key_exists('showDots', $attributes)) {
            $appendBoolIfDiff('show_dots', $attributes['showDots'], false);
        }

        // Use isset + trim (not empty): empty('0') is true in PHP, so gap="0" must still be passed.
        if (isset($attributes['gap']) && trim((string) $attributes['gap']) !== '') {
            $appendStr('gap', $attributes['gap']);
        }
        if (!empty($attributes['customClass'])) {
            $appendStr('custom_class', $attributes['customClass']);
        }

        $inner = implode(' ', $parts);

        return '[movie_engine_person_list' . ($inner !== '' ? ' ' . $inner : '') . ']';
    }

    /**
     * Maps block attributes to [movie_engine_user_menu] (Shortcodes::render_user_menu).
     *
     * @param array<string,mixed> $attributes Block attributes.
     */
    private static function build_user_menu_shortcode_string(array $attributes): string
    {
        $parts = [];

        $appendStr = static function ($sc_key, $value) use (&$parts): void {
            if ($value === null) {
                return;
            }
            $s = trim((string) $value);
            if ($s === '') {
                return;
            }
            $parts[] = sprintf('%s="%s"', $sc_key, esc_attr($s));
        };

        $appendStr('custom_class', $attributes['customClass'] ?? null);

        $inner = implode(' ', $parts);

        return '[movie_engine_user_menu' . ($inner !== '' ? ' ' . $inner : '') . ']';
    }

    /**
     * Maps block attributes to [movie_engine_taxonomies] (Shortcodes::render_taxonomies).
     *
     * @param array<string,mixed> $attributes Block attributes.
     */
    private static function build_taxonomies_shortcode_string(array $attributes): string
    {
        $parts = [];

        $appendStr = static function ($sc_key, $value) use (&$parts): void {
            if ($value === null) {
                return;
            }
            $s = trim((string) $value);
            if ($s === '') {
                return;
            }
            $parts[] = sprintf('%s="%s"', $sc_key, esc_attr($s));
        };

        $appendBoolIfDiff = static function ($sc_key, $value, bool $default) use (&$parts): void {
            $v = (bool) $value;
            if ($v === $default) {
                return;
            }
            $parts[] = sprintf('%s="%s"', $sc_key, $v ? 'true' : 'false');
        };

        $type = isset($attributes['type']) ? (string) $attributes['type'] : 'genre';
        if ($type !== 'genre') {
            $appendStr('type', $type);
        }

        $layout = isset($attributes['layout']) ? (string) $attributes['layout'] : 'pills';
        if ($layout !== 'pills') {
            $appendStr('layout', $layout);
        }

        $appendStr('title', $attributes['title'] ?? '');

        if (array_key_exists('showViewAll', $attributes)) {
            $appendBoolIfDiff('show_view_all', $attributes['showViewAll'], false);
        }

        if (!empty($attributes['viewAllUrl'])) {
            $appendStr('view_all_url', $attributes['viewAllUrl']);
        }
        if (!empty($attributes['viewAllText'])) {
            $appendStr('view_all_text', $attributes['viewAllText']);
        }

        $col_defaults = [
            'columns' => 4,
            'columnsLaptop' => 4,
            'columnsTablet' => 3,
            'columnsMobile' => 2,
        ];
        foreach ($col_defaults as $attr => $def) {
            $sc = [
                'columns' => 'columns',
                'columnsLaptop' => 'columns_laptop',
                'columnsTablet' => 'columns_tablet',
                'columnsMobile' => 'columns_mobile',
            ][$attr];
            if (isset($attributes[$attr])) {
                $v = (int) $attributes[$attr];
                if ($v > 0 && $v !== $def) {
                    $parts[] = sprintf('%s="%d"', $sc, $v);
                }
            }
        }

        if (isset($attributes['limit'])) {
            $lim = (int) $attributes['limit'];
            if ($lim > 0) {
                $parts[] = sprintf('limit="%d"', $lim);
            }
        }

        $orderby = isset($attributes['orderby']) ? (string) $attributes['orderby'] : 'name';
        if ($orderby !== 'name') {
            $appendStr('orderby', $orderby);
        }

        $order = isset($attributes['order']) ? (string) $attributes['order'] : 'ASC';
        if ($order !== 'ASC') {
            $appendStr('order', $order);
        }

        if (array_key_exists('hideEmpty', $attributes)) {
            $appendBoolIfDiff('hide_empty', $attributes['hideEmpty'], true);
        }
        if (array_key_exists('showCount', $attributes)) {
            $appendBoolIfDiff('show_count', $attributes['showCount'], true);
        }

        if (isset($attributes['gap'])) {
            $g = trim((string) $attributes['gap']);
            if ($g !== '' && $g !== '15') {
                $appendStr('gap', $g);
            }
        }

        if (array_key_exists('autoplay', $attributes)) {
            $appendBoolIfDiff('autoplay', $attributes['autoplay'], false);
        }
        if (isset($attributes['autoplaySpeed'])) {
            $sp = (int) $attributes['autoplaySpeed'];
            if ($sp > 0 && $sp !== 3000) {
                $parts[] = sprintf('autoplay_speed="%d"', $sp);
            }
        }
        if (array_key_exists('loop', $attributes)) {
            $appendBoolIfDiff('loop', $attributes['loop'], false);
        }
        if (array_key_exists('showDots', $attributes)) {
            $appendBoolIfDiff('show_dots', $attributes['showDots'], false);
        }
        if (array_key_exists('pauseOnHover', $attributes)) {
            $appendBoolIfDiff('pause_on_hover', $attributes['pauseOnHover'], true);
        }
        if (array_key_exists('showImage', $attributes)) {
            $appendBoolIfDiff('show_image', $attributes['showImage'], true);
        }

        if (!empty($attributes['include'])) {
            $appendStr('include', $attributes['include']);
        }
        if (!empty($attributes['exclude'])) {
            $appendStr('exclude', $attributes['exclude']);
        }

        $ratio = isset($attributes['ratio']) ? (string) $attributes['ratio'] : 'landscape';
        if ($ratio !== 'landscape') {
            $appendStr('ratio', $ratio);
        }
        if ($ratio === 'custom' && !empty($attributes['customRatio'])) {
            $appendStr('custom_ratio', $attributes['customRatio']);
        }

        if (!empty($attributes['customClass'])) {
            $appendStr('custom_class', $attributes['customClass']);
        }

        $inner = implode(' ', $parts);

        return '[movie_engine_taxonomies' . ($inner !== '' ? ' ' . $inner : '') . ']';
    }
}

Movie_Engine_Gutenberg::init();
