<?php
/**
 * TitanCart Admin Asset Manager.
 *
 * Enqueues scripts and styles for the React admin SPA.
 * Uses WordPress's built-in React (wp-element) as the React runtime.
 *
 * @package TitanCart\Admin
 */

namespace TitanCart\Admin;

use TitanCart\Services\SettingsService;

defined( 'ABSPATH' ) || exit;

class Assets {

    public function __construct() {
        add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
    }

    /**
     * Enqueue React SPA assets on TitanCart admin pages.
     *
     * @param string $hook Current admin page hook.
     */
    public function enqueue( string $hook ): void {
        // Load on our top-level page AND all TitanCart sub-pages.
        // Sub-page hooks are formatted as: {parent_slug}_page_{sub_slug}
        // Since our sub-slugs contain '#', WP sanitises them — catch both patterns.
        $is_tc_page = $hook === 'toplevel_page_titancart'
            || strpos( $hook, 'titancart' ) !== false;

        if ( ! $is_tc_page ) {
            return;
        }

        // ── KI-109 root cause (S467): keep wp-emoji off the SPA screen ────────
        // wp-emoji's MutationObserver rewrites emoji characters inside text nodes
        // into <img class="emoji"> (active whenever WP's feature test decides the
        // browser can't render its current emoji set — true on macOS Chrome for
        // WP 7.0's Emoji 17 probe). The SPA renders emoji in text ("⚡ Test
        // Connection", "✅", "💳" …); once wp-emoji swaps such a text node for
        // img+text, React's fiber still holds the detached node, and the next
        // sibling insertion throws `NotFoundError: insertBefore` → Page Error.
        // Only REAL interactions trip it because the observer runs async.
        // `wp-exclude-emoji` cannot help: twemoji's doNotParse only filters
        // descendants of the parse root, and the observer starts at the text
        // node's own parent. Core does exactly this for the block editor
        // (wp-admin/edit-form-blocks.php). Scoped to TitanCart screens only.
        remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
        remove_action( 'admin_print_styles', 'print_emoji_styles' );
        wp_dequeue_style( 'wp-emoji-styles' );

        // ── React SPA bundle ─────────────────────────────────────────────────
        $asset_file = TITANCART_PATH . 'admin/build/titancart-admin.asset.php';
        $js_file    = TITANCART_PATH . 'admin/build/titancart-admin.js';

        // Clear PHP stat cache so filemtime reflects the actual file, not an opcached stale value.
        if ( file_exists( $js_file ) ) {
            clearstatcache( true, $js_file );
        }

        $asset      = file_exists( $asset_file )
            ? require $asset_file
            : [
                'dependencies' => [ 'wp-element', 'wp-api-fetch', 'wp-i18n' ],
                'version'      => file_exists( $js_file ) ? (string) filemtime( $js_file ) : TITANCART_VERSION,
            ];

        wp_enqueue_script(
            'titancart-admin',
            TITANCART_ADMIN_URL . 'titancart-admin.js',
            $asset['dependencies'],
            $asset['version'],
            true // Load in footer.
        );

        // ── SPA shell chrome CSS (WP.org R1 🅱1) ─────────────────────────────
        // Suppresses WP's default admin page padding so the full-height SPA can
        // manage its own layout (moved here from AdminController::render_spa so
        // it rides the enqueue API instead of an inline style-tag echo).
        wp_register_style( 'titancart-admin-shell', false, [], TITANCART_VERSION );
        wp_enqueue_style( 'titancart-admin-shell' );
        wp_add_inline_style(
            'titancart-admin-shell',
            '#wpcontent, #wpbody-content { padding: 0 !important; } .wrap { margin: 0 !important; }'
        );

        // ── Inline config for the React app ──────────────────────────────────
        wp_localize_script(
            'titancart-admin',
            'TitanCartConfig',
            [
                'version'        => TITANCART_VERSION,
                'apiUrl'         => rest_url( 'titancart/v1' ),
                'nonce'          => wp_create_nonce( 'wp_rest' ),
                'edition'         => titancart_core_edition(),
                'isPro'           => titancart_is_pro(),
                // Pro-only built-in extension slugs (the "4th seam"). Lets the
                // Extensions admin + the nav lock+badge Pro extensions (e.g. SEO,
                // which has no matching core capability key) without hard-coding
                // the split in JS. Inert while isPro === true.
                'proExtensions'   => class_exists( '\\TitanCart\\Core\\Edition' )
                    ? \TitanCart\Core\Edition::pro_extensions()
                    : [],
                // ── Phone-home posture (KI-36, S358) ─────────────────────────
                // TRUE only in the WordPress.org "TitanCart Lite" package, which
                // pins the constant and physically strips src/REST/License/ plus
                // the marketplace/installer regions of ExtensionsController (see
                // that file's strip markers). Those routes 404 in that build, and
                // the SPA must not render — or fetch — the surfaces backed by them.
                //
                // Deliberately does not spell either strip-marker token: this file
                // is staged into both packages and the build's text assertions
                // cannot tell a comment from code. See pattern #443.
                //
                // This is NOT a Pro/Lite upsell flag. Do not conflate it with
                // isPro: a Pro store whose licence has lapsed is !isPro but its
                // routes still exist. The question here is "does this route
                // exist in this package", which only the build can answer.
                //
                // FALSE on every live store today (the constant defaults false
                // in titancart.php), so this key is inert everywhere but Lite.
                //
                // Load-bearing beyond cosmetics: readme.txt promises "no
                // outbound connections of any kind". The SPA gates its four
                // marketplace fetches on this flag — see navVisible() and
                // loadMarketplace() in admin/build/titancart-admin.js.
                'noPhoneHome'     => defined( 'TITANCART_NO_PHONEHOME' ) && TITANCART_NO_PHONEHOME,
                'adminUrl'       => admin_url( 'admin.php?page=titancart' ),
                // S267 (#7/KI-15): front-end home URL so the order-detail "Print"
                // button can open the standalone branded invoice at
                // /order-invoice/{number}/ (admin is authorized server-side; no key needed).
                'siteUrl'        => home_url(),
                'assetsUrl'      => TITANCART_ASSETS_URL,
                'locale'         => get_locale(),
                'currency'       => $this->get_currency_config(),
                // S120 polish: flat alias for legacy admin JS that still reads
                // `Config.currency_symbol` directly. Prevents the whole admin
                // backend from falling through to '$' when primary != USD.
                'currency_symbol' => \TitanCart\Services\CurrencyService::primary_symbol(),
                'extensions'     => $this->get_extension_statuses(),
                'adminPerPage'   => SettingsService::int( 'appearance', 'admin_elements_per_page', 50 ),
                'dateFormat'     => SettingsService::get( 'appearance', 'date_format', 'mdy' ),
                'timeFormat'     => SettingsService::get( 'appearance', 'time_format', '24' ),
                'timezone'       => SettingsService::get( 'appearance', 'timezone', 'America/New_York' ),
                'companyName'    => SettingsService::get( 'company', 'company_name', get_bloginfo( 'name' ) ),
                // When ON, the product editor allows saving with a blank Product
                // Code (the server generates one). Mirrors the server-side gate in
                // ProductsController::create_item() so the SPA's required-field
                // check doesn't block a save the server would happily accept.
                'productCodeAutogenerate' => SettingsService::bool( 'general', 'product_code_autogenerate', false ),
                'subscriptionsActive' => defined( 'TCS_VERSION' ),
                // Theme Options card/page visibility — true only when the active
                // TitanCart theme advertises per-preset controls via the
                // `titancart_theme_options_schema` filter. Engine-agnostic: with no
                // theme registered the filter returns empty and the card is hidden.
                'themeOptionsAvailable' => ! empty( apply_filters( 'titancart_theme_options_schema', [] ) ),
                // S157 — Marketplace plugin signals. Two distinct keys so the
                // SPA can render an "Marketplace (license required)" state
                // (card visible, form locked) when the plugin is installed
                // but unlicensed. `Installed` controls card/menu visibility;
                // `LicenseActive` controls whether form controls are editable.
                // The license-active check short-circuits via && so the static
                // method is only called when the constant is defined (= plugin
                // is loaded and the autoloader can resolve the class).
                'marketplaceInstalled'     => defined( 'TCMP_VERSION' ),
                'marketplaceLicenseActive' => defined( 'TCMP_VERSION' ) && \TitanCartMarketplace\MarketplacePlugin::license_active(),
                'marketplaceApiUrl'        => defined( 'TCMP_VERSION' ) ? rest_url( 'titancart-marketplace/v1' ) : '',
                // S221 — Wishlist+ plugin signals. Same two-flag shape as Marketplace:
                // `Installed` gates nav/page visibility (card shown, possibly locked);
                // `LicenseActive` gates whether the enable toggle can be flipped on.
                // The && short-circuit keeps the static call guarded behind the
                // constant so the autoloader is only asked to resolve the class when
                // the plugin is actually loaded.
                'wishlistPlusInstalled'     => defined( 'TCWP_VERSION' ),
                'wishlistPlusLicenseActive' => defined( 'TCWP_VERSION' ) && \TitanCartWishlistPlus\WishlistPlusPlugin::license_active(),
                'wishlistPlusApiUrl'        => defined( 'TCWP_VERSION' ) ? rest_url( 'titancart-wishlist/v1' ) : '',
                // S225 — Product Data Feeds plugin signals. Same two-flag shape as
                // Wishlist+/Marketplace: `Installed` gates the Growth → Data Feeds
                // nav/page; `LicenseActive` gates whether the master enable toggle
                // can be flipped on. Surfaced HERE (not via a filter) because core
                // does not run an apply_filters('titancart_config', …) over this
                // localized config — every extension surfaces its keys directly in
                // this array. The && short-circuit keeps the static call guarded
                // behind the constant so the autoloader only resolves the class when
                // the plugin is actually loaded.
                'productFeedsInstalled'     => defined( 'TCPF_VERSION' ),
                'productFeedsLicenseActive' => defined( 'TCPF_VERSION' ) && \TitanCartProductFeeds\ProductFeedsPlugin::license_active(),
                'productFeedsApiUrl'        => defined( 'TCPF_VERSION' ) ? rest_url( 'titancart-feeds/v1' ) : '',
                // S228 — Abandoned Cart Recovery plugin signals. Same three-flag shape
                // as Data Feeds/Wishlist+: `Installed` gates the Growth → Cart Recovery
                // nav/page; `LicenseActive` gates whether the master enable toggle can be
                // flipped on. Surfaced HERE (not via the bootstrap's titancart_config
                // filter, which core never applies — S225 finding); every extension
                // surfaces its keys directly in this array. The && short-circuit keeps the
                // static call guarded behind the constant so the autoloader only resolves
                // the class when the plugin is actually loaded.
                'abandonedCartInstalled'     => defined( 'TCAC_VERSION' ),
                'abandonedCartLicenseActive' => defined( 'TCAC_VERSION' ) && \TitanCartAbandonedCart\AbandonedCartPlugin::license_active(),
                'abandonedCartApiUrl'        => defined( 'TCAC_VERSION' ) ? rest_url( 'titancart-cart-recovery/v1' ) : '',
                // S231 — SMS Notifications plugin signals. Same three-flag shape as
                // Data Feeds/Wishlist+/Cart Recovery: `Installed` gates the Growth → SMS
                // Notifications nav/page; `LicenseActive` gates whether the master enable
                // toggle can be flipped on. Surfaced HERE (not via the bootstrap's inert
                // titancart_config filter — S225 finding); the && short-circuit keeps the
                // static call guarded behind the constant so the autoloader only resolves
                // the class when the plugin is actually loaded.
                'smsNotificationsInstalled'     => defined( 'TCSMS_VERSION' ),
                'smsNotificationsLicenseActive' => defined( 'TCSMS_VERSION' ) && \TitanCartSmsNotifications\SmsNotificationsPlugin::license_active(),
                'smsNotificationsApiUrl'        => defined( 'TCSMS_VERSION' ) ? rest_url( 'titancart-sms/v1' ) : '',
                // S233 — Affiliate Marketing plugin signals. Same three-flag shape as
                // SMS Notifications/Cart Recovery/Data Feeds: `Installed` gates the
                // Growth → Affiliates nav/page; `LicenseActive` gates whether the
                // master enable toggle can be flipped on. Surfaced HERE (not via the
                // bootstrap's inert titancart_config filter — S225 finding); the &&
                // short-circuit keeps the static call guarded behind the constant so
                // the autoloader only resolves the class when the plugin is loaded.
                'affiliateMarketingInstalled'     => defined( 'TCAF_VERSION' ),
                'affiliateMarketingLicenseActive' => defined( 'TCAF_VERSION' ) && \TitanCartAffiliateMarketing\AffiliatePlugin::license_active(),
                'affiliateMarketingApiUrl'        => defined( 'TCAF_VERSION' ) ? rest_url( 'titancart-affiliate/v1' ) : '',
                // S329 — Tip / Donation plugin signals. Same three-flag shape as
                // Affiliates/SMS/Cart Recovery: `Installed` gates the Growth →
                // Tips & Donations nav/page; `LicenseActive` gates whether the master
                // enable toggle can be flipped on. Surfaced HERE (not via the
                // bootstrap's inert titancart_config filter — S225 finding); the &&
                // short-circuit keeps the static call guarded behind the constant so
                // the autoloader only resolves the class when the plugin is loaded.
                'tipDonationInstalled'     => defined( 'TCTIP_VERSION' ),
                'tipDonationLicenseActive' => defined( 'TCTIP_VERSION' ) && \TitanCartTipDonation\TipDonationPlugin::license_active(),
                'tipDonationApiUrl'        => defined( 'TCTIP_VERSION' ) ? rest_url( 'titancart-tip/v1' ) : '',
                // S334 — Printful POD plugin signals (roadmap #14). Same three-flag
                // shape as Tip/Donation above: `Installed` gates the Growth → Printful
                // nav/page; `LicenseActive` gates whether the connect/import actions are
                // usable. Surfaced HERE (not via the bootstrap's inert titancart_config
                // filter — S225 finding); the && short-circuit keeps the static call
                // guarded behind the constant so the autoloader only resolves the class
                // when the plugin is loaded.
                'printfulInstalled'     => defined( 'TCPFUL_VERSION' ),
                'printfulLicenseActive' => defined( 'TCPFUL_VERSION' ) && \TitanCartPrintful\Plugin::license_active(),
                'printfulApiUrl'        => defined( 'TCPFUL_VERSION' ) ? rest_url( 'titancart-printful/v1' ) : '',
                // S313 - TitanCart Essentials (FREE bundle). No *LicenseActive /
                // *ApiUrl keys: it is free (no license) and registers its REST
                // under the core titancart/v1 namespace (/essentials/*), so the
                // SPA reaches it via the existing apiFetch + nonce. This single
                // Installed flag (defined when the plugin is active) gates the
                // Settings card + the Essentials hub page.
                'essentialsInstalled' => defined( 'TCE_VERSION' ),
                'productImageShape'  => SettingsService::get( 'thumbnails', 'product_image_shape', 'tall' ),
                // S120: Resolve usergroup IDs by handle so the Bulk Buys editor can map
                // its three hardcoded labels (All / Guests / Registered users) to the
                // integer IDs the schema actually stores. Future sessions can replace the
                // hardcoded labels with a dynamic dropdown driven by tc_usergroups.
                'usergroupIds'       => $this->get_usergroup_ids_by_handle(),
            ]
        );

        // ── Admin styles ──────────────────────────────────────────────────────
        if ( file_exists( TITANCART_PATH . 'admin/build/titancart-admin.css' ) ) {
            wp_enqueue_style(
                'titancart-admin',
                TITANCART_ADMIN_URL . 'titancart-admin.css',
                [],
                $asset['version']
            );
        }

        // ── WP Media Library (for image pickers in categories, etc.) ──────────
        wp_enqueue_media();

        // ── Featured product toggle injection ─────────────────────────────────
        // The compiled React SPA doesn't include a Featured toggle on the product
        // edit form. Inject one via DOM observation + REST API.
        wp_add_inline_script( 'titancart-admin', self::featured_toggle_script(), 'after' );

        // ── WP components style (for any @wordpress/components usage) ─────────
        wp_enqueue_style( 'wp-components' );
    }

    /**
     * Build the currency config object for the React app.
     *
     * Returns a row from tc_currencies for the active primary currency.
     * Routed through CurrencyService so the same source-of-truth feeds
     * both the storefront and the admin SPA.
     *
     * @return array<string, mixed>
     */
    private function get_currency_config(): array {
        $primary = \TitanCart\Services\CurrencyService::primary();
        return [
            'code'          => (string) ( $primary['code']          ?? 'USD' ),
            'symbol'        => (string) ( $primary['symbol']        ?? '$' ),
            'symbol_after'  => (int)    ( $primary['symbol_after']  ?? 0 ),
            'decimals'      => (int)    ( $primary['decimals']      ?? 2 ),
            'thousands_sep' => (string) ( $primary['thousands_sep'] ?? ',' ),
            'decimal_sep'   => (string) ( $primary['decimal_sep']   ?? '.' ),
        ];
    }

    /**
     * Resolve the seeded usergroup handles ('guest', 'customer', 'wholesale') to their
     * actual integer IDs from tc_usergroups. The Bulk Buys editor uses this map to
     * convert its three hardcoded label options to integer usergroup_id values for
     * the prices payload.
     *
     * Returned shape:
     *   [
     *     'guest'     => 1,
     *     'customer'  => 2,
     *     'wholesale' => 3,
     *   ]
     *
     * Missing handles are simply absent from the map (the JS side falls back to 0).
     *
     * @return array<string, int>
     */
    private function get_usergroup_ids_by_handle(): array {
        global $wpdb;
        $table = $wpdb->prefix . TITANCART_TABLE_PREFIX . 'usergroups';

        $rows = $wpdb->get_results(
            "SELECT handle, id FROM {$table} WHERE status = 'active'",
            ARRAY_A
        );

        $map = [];
        if ( is_array( $rows ) ) {
            foreach ( $rows as $r ) {
                if ( ! empty( $r['handle'] ) ) {
                    $map[ $r['handle'] ] = (int) $r['id'];
                }
            }
        }
        return $map;
    }

    /**
     * Get extension status map: { id => 'active'|'disabled' }.
     *
     * Reads persisted statuses from tc_settings (section = 'extensions'),
     * falls back to built-in defaults for any not yet saved.
     *
     * @return array<string, string>
     */
    private function get_extension_statuses(): array {
        $defaults = [
            'bestsellers' => 'active',   'reviews'     => 'active',
            'gdpr'        => 'active',   'gift-cards'  => 'active',
            'recaptcha'   => 'active',   'sitemap'     => 'active',
            'maps'        => 'active',   'newsletters' => 'disabled',
            'bundles'     => 'active',   'variations'  => 'active',
            'rewards'     => 'active',   'rma'         => 'disabled',
            'seo'         => 'active',
            'advanced-import'    => 'active',   'price-per-unit'      => 'active',
            'social-login'       => 'active',   'wishlist'            => 'active',
            'social-buttons'     => 'disabled', 'catalog-mode'        => 'disabled',
            'age-verification'   => 'disabled', 'access-restriction'  => 'disabled',
        ];

        $db = titancart_db();
        $rows = $db->getHash(
            "SELECT `key`, value FROM {$db->table('settings')} WHERE section = ?s",
            'extensions'
        );

        foreach ( $defaults as $id => $default ) {
            if ( isset( $rows[ $id ] ) ) {
                $defaults[ $id ] = $rows[ $id ];
            }
        }

        return $defaults;
    }

    /**
     * Inline JS that injects a "Featured" toggle into the product edit form.
     *
     * Uses a MutationObserver to detect when the Status radio group renders,
     * then appends a checkbox that reads/writes the `featured` field via REST.
     */
    private static function featured_toggle_script(): string {
        return <<<'JS'
(function(){
    var API   = (window.TitanCartConfig||{}).apiUrl || '/wp-json/titancart/v1';
    var NONCE = (window.TitanCartConfig||{}).nonce  || '';
    var injected = null;

    function injectFeatured() {
        var match = window.location.hash.match(/^#\/products\/(\d+)/);
        if (!match) { injected = null; return; }
        var pid = match[1];
        if (injected === pid) return;
        if (document.getElementById('tc-featured-row')) { injected = pid; return; }

        // Find the Status radio group via its unique radio input name.
        var radio = document.querySelector('input[name="status_active"]');
        if (!radio) return;

        // Walk up to the field row (div with grid-template-columns).
        var fieldRow = radio;
        for (var i = 0; i < 6; i++) {
            fieldRow = fieldRow.parentElement;
            if (!fieldRow) return;
            var s = fieldRow.style;
            if (s && s.display === 'grid' && s.gridTemplateColumns) break;
        }
        if (!fieldRow || !fieldRow.parentElement) return;

        injected = pid;

        // Create a matching grid row for the Featured toggle.
        var row = document.createElement('div');
        row.id = 'tc-featured-row';
        row.style.cssText = fieldRow.style.cssText;
        row.innerHTML =
            '<div style="font-size:14px;color:#737373;padding-top:8px;text-align:right;line-height:1.4;font-weight:500;">Featured</div>' +
            '<div><label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;padding-top:6px;">' +
                '<input type="checkbox" id="tc-featured-cb" style="width:18px;height:18px;cursor:pointer;accent-color:#2563EB;">' +
                '<span>Show in the Featured section on the homepage</span>' +
            '</label></div>';

        fieldRow.parentNode.insertBefore(row, fieldRow.nextSibling);

        var cb = document.getElementById('tc-featured-cb');

        fetch(API + '/products/' + pid, {
            headers: { 'X-WP-Nonce': NONCE }
        }).then(function(r){ return r.json(); }).then(function(p) {
            cb.checked = parseInt(p.featured, 10) === 1;
        }).catch(function(){});

        cb.addEventListener('change', function() {
            fetch(API + '/products/' + pid, {
                method: 'PUT',
                headers: { 'Content-Type':'application/json', 'X-WP-Nonce': NONCE },
                body: JSON.stringify({ featured: this.checked ? 1 : 0 })
            }).catch(function(){});
        });
    }

    window.addEventListener('hashchange', function(){ injected = null; setTimeout(injectFeatured, 600); });
    setInterval(injectFeatured, 1500);
})();
JS;
    }
}
