<?php
/**
 * Shared Vite asset enqueue helper.
 *
 * @package NimblyMail\SMTP
 *
 * @since 1.0.0
 */

namespace NimblyMail\SMTP\Admin;

defined( 'ABSPATH' ) || exit;

/**
 * Class AssetLoader
 *
 * Centralizes the dev-server + production-manifest enqueue pipeline used by
 * both the site-level Admin\Area and the multisite Admin\NetworkArea.
 *
 * @since 1.0.0
 * @since 1.0.3 Preloads the entry's imported JS chunks in production so the
 *              shared vendor bundles fetch in parallel with the entry instead
 *              of being discovered only after the entry is parsed.
 */
final class AssetLoader {

    /**
     * Script handles that must be emitted with type="module".
     *
     * Used by the `script_loader_tag` filter callers register on their own.
     *
     * @since 1.0.0
     *
     * @var string[]
     */
    public const MODULE_HANDLES = array(
        'nimblymail-smtp-vite-client',
        'nimblymail-smtp-dashboard',
        'nimblymail-smtp-settings',
        'nimblymail-smtp-logs',
        'nimblymail-smtp-queue',
        'nimblymail-smtp-tools',
        'nimblymail-smtp-deliverability',
        'nimblymail-smtp-wizard',
        'nimblymail-smtp-network-sites',
        'nimblymail-smtp-network-dashboard',
    );

    /**
     * Modulepreload URLs collected for the current request, keyed by URL.
     *
     * Keying by URL de-duplicates for free when more than one app is
     * enqueued on the same screen (e.g. a Pro wizard extension bundle
     * enqueued via the `nimblymail_wizard_enqueue_assets` hook) and shares a
     * vendor chunk with the core entry.
     *
     * @since 1.0.3
     *
     * @var array<string, true>
     */
    private static array $preload_urls = array();

    /**
     * Whether the `admin_head` preload printer has already been hooked for
     * this request.
     *
     * @since 1.0.3
     *
     * @var bool
     */
    private static bool $preload_printer_registered = false;

    /**
     * Enqueue a Vue app bundle.
     *
     * @since 1.0.0
     *
     * @param string $handle        Script handle.
     * @param string $entry         Source entry path (manifest key).
     * @param array  $localize_data Data passed to the JS window global.
     * @param bool   $is_hmr        Whether HMR is active.
     * @param string $dev_base_url  Vite dev-server base URL when HMR is active.
     *
     * @return void
     */
    public static function enqueue( string $handle, string $entry, array $localize_data, bool $is_hmr, string $dev_base_url ): void {
        if ( $is_hmr ) {
            self::enqueue_dev( $handle, $entry, $localize_data, $dev_base_url );
            return;
        }

        self::enqueue_production( $handle, $entry, $localize_data );
    }

    /**
     * Add type="module" to Vite-emitted script tags.
     *
     * Callers wire this up via `add_filter( 'script_loader_tag', ... )`.
     *
     * @since 1.0.0
     *
     * @param string $tag    The script tag HTML.
     * @param string $handle The script handle.
     *
     * @return string
     */
    public static function add_module_to_script_tag( string $tag, string $handle ): string {
        if ( in_array( $handle, self::MODULE_HANDLES, true ) ) {
            $tag = str_replace( ' src', ' type="module" src', $tag );
        }

        return $tag;
    }

    /**
     * Enqueue a bundle from the Vite dev server (HMR mode).
     *
     * @since 1.0.0
     *
     * @param string $handle        Script handle.
     * @param string $entry         Source entry path.
     * @param array  $localize_data Data passed to the JS window global.
     * @param string $dev_base_url  Vite dev-server base URL.
     *
     * @return void
     */
    private static function enqueue_dev( string $handle, string $entry, array $localize_data, string $dev_base_url ): void {
        wp_register_script(
            'nimblymail-smtp-vite-client',
            $dev_base_url . '/@vite/client',
            array(),
            (string) time(),
            true
        );
        wp_enqueue_script( 'nimblymail-smtp-vite-client' );

        wp_register_script(
            $handle,
            $dev_base_url . '/' . $entry,
            array( 'nimblymail-smtp-vite-client', 'wp-i18n' ),
            (string) time(),
            true
        );
        wp_enqueue_script( $handle );

        wp_localize_script( $handle, 'nimblymailSmtpApp', $localize_data );
        wp_set_script_translations( $handle, 'nimblymail-smtp', NIMBLYMAIL_PATH . 'languages' );
    }

    /**
     * Enqueue a bundle from the built Vite manifest.
     *
     * Walks the manifest import graph to collect every CSS file the entry
     * transitively depends on (Vite emits shared-component CSS into
     * separate chunks reachable only via `imports`).
     *
     * @since 1.0.0
     * @since 1.0.3 Also collects the entry's imported JS chunks (vendor
     *              bundles, shared components) and registers them for
     *              `<link rel="modulepreload">` output so they fetch in
     *              parallel with the entry rather than after it.
     *
     * @param string $handle        Script handle.
     * @param string $entry         Manifest key.
     * @param array  $localize_data Data passed to the JS window global.
     *
     * @return void
     */
    private static function enqueue_production( string $handle, string $entry, array $localize_data ): void {
        $manifest_path = NIMBLYMAIL_PATH . 'dist/.vite/manifest.json';

        if ( ! is_readable( $manifest_path ) ) {
            return;
        }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
        $manifest_raw = file_get_contents( $manifest_path );
        if ( false === $manifest_raw ) {
            return;
        }

        $manifest = json_decode( $manifest_raw, true );

        if ( ! is_array( $manifest ) || empty( $manifest[ $entry ] ) ) {
            return;
        }

        foreach ( array_keys( self::collect_css( $manifest, $entry ) ) as $key => $css_file ) {
            wp_enqueue_style(
                $handle . "-style-{$key}",
                NIMBLYMAIL_URL . 'dist/' . $css_file,
                array(),
                NIMBLYMAIL_VERSION
            );
        }

        wp_enqueue_script(
            $handle,
            NIMBLYMAIL_URL . 'dist/' . $manifest[ $entry ]['file'],
            array( 'wp-i18n' ),
            NIMBLYMAIL_VERSION,
            true
        );

        wp_localize_script( $handle, 'nimblymailSmtpApp', $localize_data );
        wp_set_script_translations( $handle, 'nimblymail-smtp', NIMBLYMAIL_PATH . 'languages' );

        foreach ( array_keys( self::collect_js_chunks( $manifest, $entry ) ) as $js_file ) {
            self::$preload_urls[ NIMBLYMAIL_URL . 'dist/' . $js_file ] = true;
        }

        self::register_preload_printer();
    }

    /**
     * Walk the Vite manifest to gather every CSS file the entry depends on.
     *
     * @since 1.0.0
     *
     * @param array  $manifest Decoded Vite manifest.
     * @param string $key      Manifest entry key.
     * @param array  $seen     Keys already visited (prevents cycles).
     *
     * @return array<string, true>
     */
    private static function collect_css( array $manifest, string $key, array &$seen = array() ): array {
        if ( isset( $seen[ $key ] ) || empty( $manifest[ $key ] ) ) {
            return array();
        }
        $seen[ $key ] = true;

        $node = $manifest[ $key ];
        $css  = array();

        if ( ! empty( $node['css'] ) ) {
            foreach ( $node['css'] as $file ) {
                $css[ $file ] = true;
            }
        }

        if ( ! empty( $node['imports'] ) ) {
            foreach ( $node['imports'] as $import_key ) {
                $css = array_merge( $css, self::collect_css( $manifest, $import_key, $seen ) );
            }
        }

        return $css;
    }

    /**
     * Walk the Vite manifest to gather every JS chunk the entry's `imports`
     * pull in, recursively.
     *
     * The entry's own file is deliberately excluded — it already has a
     * `<script type="module">` tag, so only its statically-imported chunks
     * (vendor bundles, shared components) need a preload hint. Kept as a
     * public pure function (no WP calls) so it can be unit-tested against a
     * plain manifest fixture, mirroring the Log Model build_search_clause()
     * approach.
     *
     * @since 1.0.3
     *
     * @param array  $manifest Decoded Vite manifest.
     * @param string $key      Manifest entry key.
     * @param array  $seen     Keys already visited (prevents cycles).
     *
     * @return array<string, true>
     */
    public static function collect_js_chunks( array $manifest, string $key, array &$seen = array() ): array {
        if ( isset( $seen[ $key ] ) || empty( $manifest[ $key ] ) ) {
            return array();
        }
        $seen[ $key ] = true;

        $node   = $manifest[ $key ];
        $chunks = array();

        if ( ! empty( $node['imports'] ) ) {
            foreach ( $node['imports'] as $import_key ) {
                if ( ! empty( $manifest[ $import_key ]['file'] ) ) {
                    $chunks[ $manifest[ $import_key ]['file'] ] = true;
                }
                $chunks = array_merge( $chunks, self::collect_js_chunks( $manifest, $import_key, $seen ) );
            }
        }

        return $chunks;
    }

    /**
     * Hook the modulepreload printer to `admin_head` once per request.
     *
     * Idempotent so calling `enqueue_production()` more than once on the
     * same screen (core entry + a Pro extension bundle) only registers the
     * printer a single time; the URLs themselves are already de-duplicated
     * by `$preload_urls` being keyed by URL.
     *
     * @since 1.0.3
     *
     * @return void
     */
    private static function register_preload_printer(): void {
        if ( self::$preload_printer_registered ) {
            return;
        }
        self::$preload_printer_registered = true;

        add_action( 'admin_head', array( self::class, 'print_preload_links' ) );
    }

    /**
     * Print `<link rel="modulepreload">` tags for every collected JS chunk.
     *
     * Hooked to `admin_head` rather than the `wp_preload_resources` filter:
     * that filter is only applied on the front-end `wp_head` hook (see
     * `wp-includes/default-filters.php`), which never fires on wp-admin
     * screens, so it would silently no-op here. `admin_head` runs before
     * WordPress prints the enqueued (footer) script tags, so the browser
     * sees these hints before it requests the entry module.
     *
     * @since 1.0.3
     *
     * @return void
     */
    public static function print_preload_links(): void {
        foreach ( array_keys( self::$preload_urls ) as $url ) {
            printf( '<link rel="modulepreload" href="%s" />' . "\n", esc_url( $url ) );
        }
    }
}
