/**
 * Contentosapp Studio Admin — Buy Extra Quotas modal controller (Sprint B.3.4).
 *
 * Public API:
 *   window.costudioAddonModal()       — open the modal (preferred entry)
 *   window.costudioAddonModal.open()  — same
 *   window.costudioAddonModal.close() — programmatic close
 *
 * Auto-init on DOMContentLoaded:
 *   - Wires close handlers (X button, overlay click, Escape key)
 *   - Wires "Buy Now" delegation on package cards
 *   - Wires "Try again" on the error state
 *   - Runs handleReturnFromCheckout() to react to ?addon_purchase=success/cancelled
 *     in the URL (the redirect that Stripe sends after a payment)
 *
 * State machine inside the modal body (#fos-addon-modal-content):
 *   skeleton  → grid       (success path on first open)
 *             → error      (network / server error fetching packages)
 *   grid      → redirecting (user clicked "Buy Now", waiting for Stripe URL)
 *             → error      (checkout creation failed; can retry)
 *   error     → skeleton   (user clicked "Try again")
 *
 * The whole content container is replaced on each transition — no
 * per-element state to track, no orphaned event listeners (jQuery
 * delegation on the modal root handles everything).
 */
/* global CostudioData */
(function ($) {
    'use strict';

    var data  = window.CostudioData || {};
    var ajax  = data.ajax_url || '';
    var nonce = data.nonce || '';
    var i18n  = data.i18n || {};
    function t(key, fallback) {
        return (typeof i18n[key] === 'string' && i18n[key].length) ? i18n[key] : fallback;
    }

    // ── Module state ─────────────────────────────────────────────────────
    var packagesCache = null;   // null = not fetched yet; array = fetched
    var fetchXHR      = null;   // jqXHR for in-flight packages fetch
    var checkoutXHR   = null;   // jqXHR for in-flight checkout creation
    var lastFocused   = null;   // restore focus on close
    var isOpen        = false;
    var redirecting   = false;  // true once we initiate window.location → Stripe
    var openOpts      = null;   // Sprint B.4.3 — opts passed to open(); null when modal opens in default mode

    // ── Open / close ─────────────────────────────────────────────────────
    /**
     * Open the modal. Optional opts object (Sprint B.4.3):
     *   {
     *     context: 'production_blocked',  // swaps default headline + appends &retry_production=1 to return URL
     *     headline: '...'                 // override the contextual headline copy
     *   }
     */
    function open(opts) {
        var $modal = $('#fos-addon-modal');
        if (!$modal.length) {
            // Partial not on this page — fall back to alert so the user
            // isn't left wondering why the button does nothing.
            window.alert(t('addon_buy_coming_soon', 'The purchase modal is not available on this page.'));
            return;
        }
        if (isOpen) { return; }

        openOpts = (opts && typeof opts === 'object') ? opts : null;

        lastFocused = document.activeElement;
        isOpen = true;
        $modal.show();
        $('body').addClass('fos-modal-open');

        // Render the context line according to opts. Default path shows the
        // "X extras + Y service quota credits" balance line; production_blocked path
        // shows a warning-toned call-to-action. Always re-rendered on each
        // open so the modal reflects the current opts (no stale state from
        // a previous open in a different context).
        renderContextLine(openOpts);

        // Move focus inside the modal for keyboard users (close button is
        // a safe initial target — always present, doesn't trigger anything
        // unintended if the user hits Enter).
        $('#fos-addon-modal-close').trigger('focus');

        // Lazy-load packages on first open. Cached for the rest of the
        // page lifetime (server transient gives us 1h on top of that).
        if (packagesCache === null) {
            fetchPackages();
        } else {
            renderGrid(packagesCache);
        }
    }

    function close() {
        if (!isOpen || redirecting) { return; }
        $('#fos-addon-modal').hide();
        $('body').removeClass('fos-modal-open');
        isOpen = false;
        openOpts = null;

        // Cancel anything in flight so it can't write to a closed modal.
        if (fetchXHR    && fetchXHR.readyState !== 4)    { try { fetchXHR.abort();    } catch (e) {} }
        if (checkoutXHR && checkoutXHR.readyState !== 4) { try { checkoutXHR.abort(); } catch (e) {} }
        fetchXHR = null;
        checkoutXHR = null;

        if (lastFocused && typeof lastFocused.focus === 'function') {
            try { lastFocused.focus(); } catch (e) {}
        }
        lastFocused = null;
    }

    // ── Fetch packages ───────────────────────────────────────────────────
    function fetchPackages() {
        renderSkeleton();

        fetchXHR = $.ajax({
            url: ajax,
            type: 'POST',
            dataType: 'json',
            data: {
                action: 'costudio_addon_packages',
                nonce:  nonce,
                force:  '0'
            }
        });

        fetchXHR.done(function (json) {
            if (!json || !json.success || !json.data || !Array.isArray(json.data.packages)) {
                renderError('generic');
                return;
            }
            if (json.data.packages.length === 0) {
                renderError('empty');
                return;
            }
            packagesCache = json.data;
            renderGrid(packagesCache);
        }).fail(function (jqXHR, textStatus) {
            if (textStatus === 'abort') { return; }
            renderError('network');
        });
    }

    // ── Buy click → checkout ────────────────────────────────────────────
    function buyPackage(packageId) {
        renderRedirecting();

        // Build return URLs from the current page so Stripe sends the user
        // back where they started. Strip existing query so we don't double-up
        // params on consecutive purchases in the same session.
        var pageUrl = window.location.origin + window.location.pathname;
        var qs = '?page=' + (new URLSearchParams(window.location.search).get('page') || '');
        // Preserve only the WP page param; everything else is noise for the
        // return trip. Stripe just redirects, doesn't reuse our query.
        var baseReturn = pageUrl + qs;

        // Sprint B.4.3 — When opened in production_blocked context, add the
        // retry flag so the production form auto-restores + re-submits after
        // the user returns. The success URL carries the flag; cancel URL
        // doesn't (no point retrying a production the user gave up on).
        var successQs = '&addon_purchase=success&pkg=' + encodeURIComponent(packageId);
        if (openOpts && openOpts.context === 'production_blocked') {
            successQs += '&retry_production=1';
        }

        checkoutXHR = $.ajax({
            url: ajax,
            type: 'POST',
            dataType: 'json',
            data: {
                action:             'costudio_addon_checkout',
                nonce:              nonce,
                package:            packageId,
                return_url_success: baseReturn + successQs,
                return_url_cancel:  baseReturn + '&addon_purchase=cancelled&pkg=' + encodeURIComponent(packageId)
            }
        });

        checkoutXHR.done(function (json) {
            if (json && json.success && json.data && json.data.checkout_url) {
                redirecting = true;
                // Don't bother closing the modal — we're navigating away.
                window.location.href = json.data.checkout_url;
                return;
            }
            // Server returned an error envelope: { success:false, data:{ code, message } }
            var code = (json && json.data && json.data.code) ? json.data.code : 'generic';
            renderError(code);
        }).fail(function (jqXHR, textStatus) {
            if (textStatus === 'abort') { return; }
            renderError('network');
        });
    }

    // ── Renderers (one per state) ────────────────────────────────────────
    function renderSkeleton() {
        var card = '<div class="fos-addon-pkg-card fos-addon-pkg-card--skeleton" aria-hidden="true"></div>';
        var html = '<div class="fos-addon-modal__skeleton">' +
                       '<div class="fos-addon-pkg-grid">' + card + card + card + card + '</div>' +
                   '</div>';
        $('#fos-addon-modal-content').html(html);
    }

    function renderGrid(payload) {
        var packages = payload.packages || [];
        var qtyLabel = t('addon_qty_label', 'extra credits');
        var buyLabel = t('addon_buy_now',   'Buy Now');

        // Compute savings vs the most expensive per-credit pack (the baseline).
        // "Save 20%" tells the user the actual value tradeoff in one glance —
        // much faster than mentally dividing $/qty across 4 cards.
        annotateSavings(packages);

        var html = '<div class="fos-addon-pkg-grid">';
        packages.forEach(function (pkg) {
            var classes = 'fos-addon-pkg-card';
            if (pkg.highlight) { classes += ' fos-addon-pkg-card--highlight'; }

            // Differentiated badge classes — popular signals emotional appeal
            // ("others chose this"), best_value signals rational appeal
            // ("math says this"). Different colors help users self-select.
            var badgeText  = '';
            var badgeClass = 'fos-addon-pkg-card__badge';
            if (pkg.badge === 'popular') {
                badgeText  = t('addon_badge_popular', 'Popular');
                badgeClass += ' fos-addon-pkg-card__badge--popular';
            } else if (pkg.badge === 'best_value') {
                badgeText  = t('addon_badge_best_value', 'Best value');
                badgeClass += ' fos-addon-pkg-card__badge--best-value';
            }

            html += '<div class="' + classes + '" data-package="' + escAttr(pkg.id) + '">';

            // Badge slot — always rendered so cards without a badge reserve
            // the same vertical space (qty number aligns horizontally across
            // the row instead of jumping up in cards that lack a badge).
            if (badgeText) {
                html += '<div class="' + badgeClass + '">' + escHtml(badgeText) + '</div>';
            } else {
                html += '<div class="fos-addon-pkg-card__badge fos-addon-pkg-card__badge--placeholder" aria-hidden="true">&nbsp;</div>';
            }

            html += '<div class="fos-addon-pkg-card__qty">' + escHtml(String(pkg.qty)) + '</div>';
            html += '<div class="fos-addon-pkg-card__qty-label">' + escHtml(qtyLabel) + '</div>';
            html += '<div class="fos-addon-pkg-card__price">' + escHtml(pkg.price_display) + '</div>';
            html += '<div class="fos-addon-pkg-card__unit">' + escHtml(pkg.unit_price_display) + '</div>';

            // Savings slot — same trick as the badge: always render so the
            // baseline pack (no savings) keeps the row's vertical rhythm
            // intact instead of leaving an asymmetric empty gap above the
            // Buy button compared to its neighbors.
            if (pkg._savings_pct > 0) {
                var savingsTxt = t('addon_savings', 'Save {percent}%').replace('{percent}', pkg._savings_pct);
                html += '<div class="fos-addon-pkg-card__savings">' + escHtml(savingsTxt) + '</div>';
            } else {
                html += '<div class="fos-addon-pkg-card__savings fos-addon-pkg-card__savings--placeholder" aria-hidden="true">&nbsp;</div>';
            }

            html += '<button type="button" class="fos-addon-pkg-card__buy" data-package="' + escAttr(pkg.id) + '">' + escHtml(buyLabel) + '</button>';
            html += '</div>';
        });
        html += '</div>';

        $('#fos-addon-modal-content').html(html);
    }

    // Mutates each pkg in-place adding _savings_pct (integer, 0-99). Compares
    // unit_price (cents/qty) against the highest in the catalog → that pack
    // gets 0% (no badge), all others get a positive Save X%.
    function annotateSavings(packages) {
        var maxUnit = 0;
        packages.forEach(function (p) {
            var unit = (p.qty > 0) ? (p.price_cents / p.qty) : 0;
            if (unit > maxUnit) { maxUnit = unit; }
        });
        packages.forEach(function (p) {
            var unit = (p.qty > 0) ? (p.price_cents / p.qty) : 0;
            p._savings_pct = (maxUnit > 0 && unit > 0 && unit < maxUnit)
                ? Math.round(((maxUnit - unit) / maxUnit) * 100)
                : 0;
        });
    }

    // Render the contextual "You have X extras + Y service quota credits left" line
    // above the grid. Reads the snapshot localized in CostudioData.addon_context
    // (Path A — server-cached transients exposed via wp_localize_script, no
    // extra round-trip on modal open).
    //
    // Sprint B.4.3 — accepts an `opts` override so the modal can be opened
    // in a specific context (e.g. production_blocked) with a warning-toned
    // headline replacing the default balance line.
    function renderContextLine(opts) {
        var $el = $('#fos-addon-modal-context');
        if (!$el.length) { return; }

        // B.4.3 override path — used when modal opens with a specific context
        // that warrants a different headline copy (warmer urgency tone).
        if (opts && opts.context === 'production_blocked') {
            var blockedCopy = opts.headline || t(
                'addon_context_blocked',
                'Your ContentOS Auto monthly quota is full. Choose a pack below to keep producing with the managed service.'
            );
            $el.attr('class', 'fos-addon-modal__context fos-addon-modal__context--blocked')
               .text(blockedCopy)
               .removeAttr('hidden');
            return;
        }

        var ctx = data.addon_context || {};
        if (!ctx.addon_enabled) {
            // Addons aren't enabled for this license — no balance line to show.
            // Hide in case it was previously visible from a different open.
            $el.attr('hidden', 'hidden').empty();
            return;
        }

        var extras = parseInt(ctx.addon_balance     || 0, 10);
        var plan   = parseInt(ctx.monthly_remaining || 0, 10);

        var html;
        var modifier = '';
        if (extras === 0 && plan === 0) {
            html = escHtml(t('addon_context_exhausted', 'You\'re out of credits — choose a pack below to keep producing.'));
            modifier = ' fos-addon-modal__context--exhausted';
        } else {
            // Template substitution. Numbers wrap in <strong> so they pop;
            // values pass through escHtml first to be safe even though they
            // come from server-side ints.
            var template = t('addon_context_dual', 'You have {extras_count} extras + {plan_count} service quota credits left this month.');
            html = escHtml(template)
                .replace('{extras_count}', '<strong>' + escHtml(String(extras)) + '</strong>')
                .replace('{plan_count}',   '<strong>' + escHtml(String(plan))   + '</strong>');
        }

        // Reset modifier classes then apply the active one (if any).
        $el.attr('class', 'fos-addon-modal__context' + modifier)
           .html(html)
           .removeAttr('hidden');
    }

    function renderError(code) {
        var msg = errorMessage(code);
        var html = '<div class="fos-addon-modal__error" role="alert">' +
                       '<div class="fos-addon-modal__error-icon" aria-hidden="true">⚠</div>' +
                       '<div class="fos-addon-modal__error-msg">' + escHtml(msg) + '</div>' +
                       '<button type="button" class="fos-addon-modal__retry-btn" id="fos-addon-modal-retry">' +
                           escHtml(t('addon_retry', 'Try again')) +
                       '</button>' +
                   '</div>';
        $('#fos-addon-modal-content').html(html);
    }

    function renderRedirecting() {
        var html = '<div class="fos-addon-modal__redirecting" role="status">' +
                       '<div class="fos-addon-modal__spinner" aria-hidden="true"></div>' +
                       '<div class="fos-addon-modal__redirecting-msg">' +
                           escHtml(t('addon_redirecting', 'Redirecting to secure checkout…')) +
                       '</div>' +
                   '</div>';
        $('#fos-addon-modal-content').html(html);
    }

    // ── Error message lookup ─────────────────────────────────────────────
    // Maps server-emitted error codes to user-facing copy. Anything not in
    // the map falls back to a generic message so the user is never stuck
    // with an opaque code.
    function errorMessage(code) {
        var map = {
            addons_disabled:       t('addon_err_disabled',     'Extra credits are not available right now.'),
            addons_not_configured: t('addon_err_unconfigured', 'There is a server configuration issue. Please contact support.'),
            plan_below_minimum:    t('addon_err_plan',         'Your current managed-service plan does not support extra credits.'),
            rate_limited:          t('addon_err_ratelimit',    'Too many attempts. Please wait a few minutes and try again.'),
            invalid_package:       t('addon_err_invalid_pkg',  'Selected package is not available. Please refresh and try again.'),
            invalid_return_url:    t('addon_err_url',          'Invalid return URL. Please refresh the page and try again.'),
            no_license:            t('addon_err_no_license',   'No active ContentOS Auto service license. Activate a service license before purchasing extra credits.'),
            network:               t('addon_err_network',      'Could not reach the server. Check your connection and try again.'),
            empty:                 t('addon_err_empty',        'No packages are available right now.'),
            generic:               t('addon_err_generic',      'Something went wrong. Please try again.')
        };
        return map[code] || map.generic;
    }

    // ── Return-from-checkout handler ─────────────────────────────────────
    // Runs once on every page load. Detects the ?addon_purchase=success or
    // =cancelled query that Stripe appended to the return URL, surfaces a
    // toast, kicks off a server-side resync (so the just-credited balance
    // shows up), then reloads the page so every UI piece picks up fresh
    // data — without re-triggering the toast (URL is cleaned first).
    function handleReturnFromCheckout() {
        if (!window.URLSearchParams) { return; } // ancient browser, give up gracefully
        var params = new URLSearchParams(window.location.search);
        var status = params.get('addon_purchase');
        if (!status) { return; }

        // Compute clean URL (without the addon_purchase tracker params).
        // Sprint B.4.4 — also strips retry_production: production.js relies
        // on the saved sessionStorage state (not the URL flag) to trigger
        // its auto-retry, so the flag's job ends here. Stripping it prevents
        // weird re-triggers if the user manually refreshes after the toast.
        params.delete('addon_purchase');
        params.delete('pkg');
        params.delete('session_id'); // some Stripe configs append it
        params.delete('retry_production');
        var newSearch = params.toString();
        var cleanUrl  = window.location.pathname + (newSearch ? '?' + newSearch : '') + window.location.hash;

        // Replace URL FIRST so the upcoming reload doesn't re-fire this handler.
        if (window.history && window.history.replaceState) {
            window.history.replaceState({}, document.title, cleanUrl);
        }

        if (status === 'success') {
            showToast('success', t('addon_purchase_success', '✨ Credits added! Refreshing in a moment…'));
            // Force a server-side resync to overwrite the cached transient
            // with the just-credited balance. Fire-and-forget — the reload
            // in 2.2s gives it plenty of time to complete (typical < 500ms).
            $.ajax({
                url: ajax,
                type: 'POST',
                data: { action: 'costudio_addon_resync_quota', nonce: nonce }
            });
            window.setTimeout(function () {
                window.location.reload();
            }, 2200);
        } else if (status === 'cancelled') {
            showToast('info', t('addon_purchase_cancelled', 'Purchase cancelled. No charge was made.'));
        }
    }

    // ── Toast (WP-native admin notice, prepended to .wrap) ───────────────
    function showToast(type, message) {
        var $wrap = $('.wrap').first();
        if (!$wrap.length) { return; } // no admin shell on this page

        var $toast = $(
            '<div class="notice notice-' + type + ' fos-addon-toast">' +
                '<p></p>' +
                '<button type="button" class="notice-dismiss">' +
                    '<span class="screen-reader-text">' + escHtml(t('addon_toast_dismiss', 'Dismiss this notice')) + '</span>' +
                '</button>' +
            '</div>'
        );
        $toast.find('p').text(message); // .text() escapes
        $wrap.prepend($toast);

        $toast.on('click', '.notice-dismiss', function () {
            $toast.fadeOut(160, function () { $toast.remove(); });
        });

        // Success toasts auto-dismiss; info/error stay until the user closes.
        if (type === 'success') {
            window.setTimeout(function () {
                $toast.fadeOut(400, function () { $toast.remove(); });
            }, 6000);
        }
    }

    // ── HTML escape helpers ──────────────────────────────────────────────
    function escHtml(s) {
        return $('<div/>').text(s == null ? '' : String(s)).html();
    }
    function escAttr(s) {
        return String(s == null ? '' : s)
            .replace(/&/g, '&amp;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');
    }

    // ── Wiring ───────────────────────────────────────────────────────────
    function init() {
        var $modal = $('#fos-addon-modal');
        if (!$modal.length) {
            // Modal partial isn't on this page. We still expose the global
            // open() (which alerts) and run handleReturnFromCheckout() since
            // the user might land on a non-modal page after Stripe.
            return;
        }

        // Sprint B.4.3 — Context line is now rendered per-open() call so it
        // can reflect the opts passed to that specific open (e.g.
        // production_blocked headline vs default balance line). Init no
        // longer pre-renders it.

        // Close: button, overlay click, Escape key
        $modal.on('click', '#fos-addon-modal-close', function (e) {
            e.preventDefault();
            close();
        });
        $modal.on('click', function (e) {
            // Click on the overlay (the .fos-modal element itself) closes.
            // Click inside .fos-modal__box bubbles up but target check
            // discriminates between them.
            if (e.target === $modal[0]) { close(); }
        });
        $(document).on('keydown.fosAddonModal', function (e) {
            if ((e.key === 'Escape' || e.keyCode === 27) && isOpen) { close(); }
        });

        // Buy click — delegated so it survives content swaps.
        $modal.on('click', '.fos-addon-pkg-card__buy', function (e) {
            e.preventDefault();
            var pkg = $(this).attr('data-package');
            if (pkg) { buyPackage(pkg); }
        });

        // Retry from error state — discard cache so we hit the server again.
        $modal.on('click', '#fos-addon-modal-retry', function (e) {
            e.preventDefault();
            packagesCache = null;
            fetchPackages();
        });
    }

    // ── Public API ───────────────────────────────────────────────────────
    var publicApi  = function () { open(); };
    publicApi.open  = open;
    publicApi.close = close;
    window.costudioAddonModal = publicApi;

    // ── Bootstrap ────────────────────────────────────────────────────────
    $(function () {
        init();
        handleReturnFromCheckout();
    });

})(jQuery);
