<?php

declare(strict_types=1);

namespace Numinix\SeekmodoWordPress\Blocks;

/**
 * Sprint 11 PR 1 — Gutenberg block registration for the four
 * Seekmodo web components: search, recommend, chat, quiz.
 *
 * Design choices:
 *
 *   1. **Dynamic rendering.** Each block is a server-rendered
 *      "dynamic block" — the editor preview is a static React tree
 *      (built in `blocks/src/<element>/index.js`) but the front-end
 *      output is produced by {@see render()} below, which delegates
 *      to {@see AttributeSchema::render()}. This avoids any
 *      build-step dependency for the front-end output: the moment
 *      this PHP class is registered, every Seekmodo block on the
 *      site renders the matching `<seekmodo-...>` element, even
 *      before someone runs `npm run build` in `blocks/`.
 *
 *   2. **Conditional SDK enqueue.** The `@seekmodo/web-components`
 *      module bundle is only loaded on pages that actually contain
 *      a Seekmodo block (gated on `has_block()` over the four
 *      registered names). A WP site that installs the connector
 *      but never uses a block pays zero front-end bytes.
 *
 *   3. **Source-tree fallback.** {@see register()} prefers
 *      `blocks/build/<element>/block.json` (output of `npm run
 *      build`) but falls back to `blocks/src/<element>/block.json`
 *      so an in-tree checkout works without a build step. Release
 *      tarballs ship the `build/` tree.
 *
 *   4. **Editor script registration.** The editor JS sources live
 *      at `blocks/src/<element>/index.js` and are written in
 *      vanilla `wp.element.createElement` (no JSX, no bundler
 *      required). They are registered as named scripts here so
 *      `block.json`'s `editorScript` field can reference them by
 *      handle, sidestepping the `@wordpress/scripts`
 *      auto-resolution that would otherwise expect compiled
 *      `index.asset.php` files.
 */
final class Registry
{
    private const BLOCK_NAMES = [
        'search'    => 'seekmodo/search',
        'recommend' => 'seekmodo/recommend',
        'chat'      => 'seekmodo/chat',
        'quiz'      => 'seekmodo/quiz',
        'ask'       => 'seekmodo/ask',
    ];

    /**
     * Wire the block registration into the WP boot sequence. Called
     * from {@see \Numinix\SeekmodoWordPress\Bootstrap::boot()}.
     */
    public static function register(): void
    {
        if (function_exists('add_action')) {
            add_action('init', [self::class, 'registerBlocks']);
            add_action('wp_enqueue_scripts', [self::class, 'maybeEnqueueSdk']);
        }
    }

    /**
     * Run on `init`. Registers each of the four Seekmodo blocks via
     * `register_block_type_from_metadata()`.
     */
    public static function registerBlocks(): void
    {
        if (!function_exists('register_block_type_from_metadata')) {
            // Old WP — block API not available. Plugin still
            // activates; the four blocks just don't appear in the
            // inserter. Native search swap continues to work.
            return;
        }

        foreach (AttributeSchema::ELEMENTS as $element) {
            $metadata = self::resolveMetadataPath($element);
            if ($metadata === null) {
                continue;
            }

            self::registerEditorScript($element);

            register_block_type_from_metadata($metadata, [
                'render_callback' => static function (array $attrs) use ($element): string {
                    return self::render($element, $attrs);
                },
            ]);
        }
    }

    /**
     * Render callback — produces the front-end HTML for a single
     * block instance. Public for unit testing.
     *
     * @param array<string, mixed> $attrs
     */
    public static function render(string $element, array $attrs): string
    {
        return AttributeSchema::render($element, $attrs);
    }

    /**
     * Conditionally enqueue the `@seekmodo/web-components` SDK
     * module on the front-end when a page contains any of the
     * Seekmodo blocks. Defaults to the locally vendored ESM under
     * `assets/web-components/index.js` (WordPress.org requires no
     * remote runtime assets by default). Override via the
     * `seekmodo_web_components_src` filter if needed.
     */
    public static function maybeEnqueueSdk(): void
    {
        if (function_exists('is_admin') && is_admin()) {
            return;
        }
        if (!function_exists('has_block')) {
            return;
        }

        $present = false;
        foreach (self::BLOCK_NAMES as $blockName) {
            if (has_block($blockName)) {
                $present = true;
                break;
            }
        }
        if (!$present) {
            return;
        }

        self::enqueueSdk();
    }

    /**
     * Default URL for the vendored `@seekmodo/web-components` ESM.
     * Public for unit tests.
     */
    public static function defaultSdkSrc(): string
    {
        if (function_exists('plugins_url') && defined('SEEKMODO_WP_PLUGIN_FILE')) {
            return (string) plugins_url('assets/web-components/index.js', SEEKMODO_WP_PLUGIN_FILE);
        }
        if (defined('SEEKMODO_WP_PLUGIN_URL')) {
            return (string) SEEKMODO_WP_PLUGIN_URL . 'assets/web-components/index.js';
        }

        return 'assets/web-components/index.js';
    }

    /**
     * Enqueue the locally vendored `@seekmodo/web-components` module.
     * Used by Gutenberg block pages and by auto-injected PDP/cart
     * templates that have no Seekmodo blocks. Operators may override
     * the URL with the `seekmodo_web_components_src` filter.
     */
    public static function enqueueSdk(): void
    {
        if (function_exists('wp_script_is') && wp_script_is('seekmodo-web-components', 'enqueued')) {
            return;
        }

        $default = self::defaultSdkSrc();
        $src = function_exists('apply_filters')
            ? apply_filters('seekmodo_web_components_src', $default)
            : $default;

        if (function_exists('wp_enqueue_script')) {
            wp_enqueue_script(
                'seekmodo-web-components',
                (string) $src,
                [],
                defined('SEEKMODO_WP_VERSION') ? (string) SEEKMODO_WP_VERSION : '0.0.0',
                [
                    'strategy'  => 'defer',
                    'in_footer' => true,
                ]
            );
            // Promote to type=module — the vendored bundle is ESM.
            if (function_exists('add_filter')) {
                add_filter('script_loader_tag', [self::class, 'addModuleType'], 10, 3);
            }
        }
    }

    /**
     * Promote our SDK script tag to `type="module"` so the bundled
     * ESM imports resolve.
     */
    public static function addModuleType(string $tag, string $handle, string $src): string
    {
        if ($handle !== 'seekmodo-web-components') {
            return $tag;
        }

        return str_replace('<script ', '<script type="module" ', $tag);
    }

    /**
     * Locate the `block.json` for a given element, preferring the
     * built artifact in `blocks/build/<element>/` and falling back
     * to the in-source `blocks/src/<element>/` if the build hasn't
     * been run.
     */
    private static function resolveMetadataPath(string $element): ?string
    {
        if (!defined('SEEKMODO_WP_PLUGIN_DIR')) {
            return null;
        }

        $base = (string) SEEKMODO_WP_PLUGIN_DIR;

        $built = $base . 'blocks/build/' . $element . '/block.json';
        if (is_file($built)) {
            return $built;
        }

        $source = $base . 'blocks/src/' . $element . '/block.json';
        if (is_file($source)) {
            return $source;
        }

        return null;
    }

    /**
     * Register the editor JS for a given element under the canonical
     * handle name `seekmodo-block-<element>-editor`. The matching
     * `block.json` references this handle via its `editorScript`
     * field.
     */
    private static function registerEditorScript(string $element): void
    {
        if (!function_exists('wp_register_script') || !defined('SEEKMODO_WP_PLUGIN_URL')) {
            return;
        }

        $handle = 'seekmodo-block-' . $element . '-editor';
        $base   = (string) SEEKMODO_WP_PLUGIN_URL;
        $built  = $base . 'blocks/build/' . $element . '/index.js';
        $source = $base . 'blocks/src/' . $element . '/index.js';

        $localBuilt  = (defined('SEEKMODO_WP_PLUGIN_DIR') ? (string) SEEKMODO_WP_PLUGIN_DIR : '') . 'blocks/build/' . $element . '/index.js';
        $url         = is_file($localBuilt) ? $built : $source;

        wp_register_script(
            $handle,
            $url,
            ['wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n'],
            defined('SEEKMODO_WP_VERSION') ? (string) SEEKMODO_WP_VERSION : '0.0.0',
            true
        );
    }

    /**
     * Test helper — return the canonical block name for a given
     * element slug.
     */
    public static function blockNameFor(string $element): ?string
    {
        return self::BLOCK_NAMES[$element] ?? null;
    }
}
