/**
 * TamRank PRO Feature Tour
 *
 * Cross-page guided tour engine. Reads step config from tamrankTour
 * (localized via PHP) and persists progress in localStorage.
 */
(function($) {
    'use strict';

    /**
     * Fill one placeholder without letting the value expand.
     *
     * Copy of the canonical helper in assets/js/tamrank-onboarding.js; read the
     * docblock there for why String.replace() with a STRING replacement mangles
     * any value carrying $&, $`, $' or $$, and why this is a copy instead of a
     * shared global. `token` may be a string or a RegExp; a FUNCTION replacement
     * is exempt from that expansion in both cases. Keep the body identical.
     */
    function fillPlaceholder(tpl, token, value) {
        var literal = String(value);
        return String(tpl).replace(token, function () { return literal; });
    }

    var STORAGE_KEY = 'tamrank_tour_step';
    var config      = window.tamrankTour || {};
    var steps       = config.steps || [];
    var currentPage = config.currentPage || '';
    var i18n        = config.i18n || {};
    var totalSteps  = steps.length;

    if (!totalSteps) return;

    /* ── Step numbering ─────────────────────────────────────── */

    // A step flagged `uncounted` (the FREE welcome card) announces the numbered
    // steps rather than being one of them, so it is left out of "Step X of Y".
    // countedNumber[i] holds the number to show for step i; countedTotal is the
    // count the welcome copy promises.
    var countedTotal  = 0;
    var countedNumber = [];
    for (var si = 0; si < steps.length; si++) {
        if (!steps[si].uncounted) countedTotal++;
        countedNumber[si] = countedTotal;
    }

    // Returns '' for an uncounted step, so its header stays empty.
    function stepLabel(index) {
        if (steps[index] && steps[index].uncounted) return '';
        return fillPlaceholder(
            fillPlaceholder(i18n.step_of || 'Step %1$s of %2$s', '%1$s', countedNumber[index]),
            '%2$s', countedTotal);
    }

    /* ── Live-tracking state ────────────────────────────────── */

    // References to the currently anchored step, used to keep the tooltip and
    // pulse glued to the target while the user scrolls or resizes.
    var activeTarget    = null;
    var activeTooltip   = null;
    var activePreferred = 'bottom';
    var rafPending      = false;

    function reposition() {
        if (!activeTarget || !activeTarget.length) return;

        var el = activeTarget[0];
        // Target gone from the DOM (e.g. tab/panel re-render) — stop tracking.
        if (!document.body.contains(el)) return;

        var rect = el.getBoundingClientRect();

        // Pulse ring follows the live viewport rect (position: fixed).
        $('.tr-tour__pulse').css({
            top:    rect.top,
            left:   rect.left,
            width:  rect.width,
            height: rect.height
        });

        if (activeTooltip && activeTooltip.length) {
            positionTooltip(activeTooltip, activeTarget, activePreferred);
        }
    }

    // rAF-throttled handler so scroll/resize repositioning stays smooth.
    function onScrollResize() {
        if (rafPending) return;
        rafPending = true;
        window.requestAnimationFrame(function() {
            rafPending = false;
            reposition();
        });
    }

    function startTracking() {
        // Capture phase so inner scroll containers (e.g. the Gutenberg
        // meta-box panel) trigger repositioning too, not just window scroll.
        window.addEventListener('scroll', onScrollResize, true);
        window.addEventListener('resize', onScrollResize);
    }

    function stopTracking() {
        window.removeEventListener('scroll', onScrollResize, true);
        window.removeEventListener('resize', onScrollResize);
        activeTarget    = null;
        activeTooltip   = null;
    }

    /* ── SVG icons ──────────────────────────────────────────── */

    var icons = {
        sparkles: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.5 5.5L19 10l-5.5 1.5L12 17l-1.5-5.5L5 10l5.5-1.5L12 3z"/><path d="M18 14l.75 2.25L21 17l-2.25.75L18 20l-.75-2.25L15 17l2.25-.75L18 14z"/></svg>',
        image: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>',
        code: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>'
    };

    var alertCircle = '<svg class="tr-tour__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>';

    /* ── State ──────────────────────────────────────────────── */

    function getStep() {
        var s = parseInt(localStorage.getItem(STORAGE_KEY), 10);
        return isNaN(s) ? -1 : s;
    }

    function setStep(n) {
        localStorage.setItem(STORAGE_KEY, n);
    }

    function clearStep() {
        localStorage.removeItem(STORAGE_KEY);
    }

    // A target is anchorable only when it is actually laid out. A display:none
    // element (or one with a hidden ancestor — e.g. the SEO meta box inside a
    // collapsed Gutenberg meta-box drawer) has offsetParent === null and a zero
    // rect, which would otherwise pin the tooltip to the viewport corner.
    function isAnchorable($el) {
        var el = $el && $el[0];
        if (!el || el.offsetParent === null) return false;
        var rect = el.getBoundingClientRect();
        return rect.width > 1 && rect.height > 1;
    }

    // Close WordPress' native "Welcome to the block editor" guide so it does not
    // overlap the tour on a fresh editor visit. Guarded so it no-ops off the
    // editor and on WP versions without these stores. Setting the preference
    // both closes it if open and keeps it from re-appearing.
    function dismissWpWelcomeGuide() {
        if (!window.wp || !wp.data) return;
        try {
            var prefs = wp.data.select('core/preferences');
            if (prefs && prefs.get('core/edit-post', 'welcomeGuide')) {
                wp.data.dispatch('core/preferences').set('core/edit-post', 'welcomeGuide', false);
            }
        } catch (e) {}
        try {
            var editPost = wp.data.select('core/edit-post');
            if (editPost && typeof editPost.isFeatureActive === 'function' &&
                editPost.isFeatureActive('welcomeGuide')) {
                wp.data.dispatch('core/edit-post').toggleFeature('welcomeGuide');
            }
        } catch (e) {}
    }

    /* ── DOM helpers ────────────────────────────────────────── */

    function cleanup() {
        stopTracking();
        $('.tr-tour__backdrop, .tr-tour__tooltip, .tr-tour__card, .tr-tour__pulse').remove();
        $('.tr-tour__highlighted').removeClass('tr-tour__highlighted');
        $('.tr-tour__lifted').removeClass('tr-tour__lifted tr-tour__lifted--static');
    }

    function scrollTo($el, callback) {
        var el = $el[0];
        if (el && el.scrollIntoView) {
            el.scrollIntoView({ behavior: 'smooth', block: 'center' });
            // Wait for smooth scroll to finish before positioning tooltip
            setTimeout(callback, 400);
        } else {
            var top = $el.offset().top - ($(window).height() / 3);
            $('html, body').animate({ scrollTop: Math.max(0, top) }, 300, callback);
        }
    }

    /* ── Positioning ────────────────────────────────────────── */

    // Tooltip + pulse use position:fixed, so all coordinates here are
    // viewport-relative (getBoundingClientRect, no scroll offset added).
    function positionTooltip($tooltip, $target, preferred) {
        var tRect = $target[0].getBoundingClientRect();
        var gap   = 16;
        var tipW  = $tooltip.outerWidth();
        var tipH  = $tooltip.outerHeight();
        var winW  = $(window).width();
        var winH  = $(window).height();

        var pos = preferred;

        // Flip if not enough room
        if (pos === 'bottom' && tRect.bottom + gap + tipH > winH) pos = 'top';
        if (pos === 'top' && tRect.top - gap - tipH < 0) pos = 'bottom';
        if (pos === 'right' && tRect.right + gap + tipW > winW) pos = 'left';
        if (pos === 'left' && tRect.left - gap - tipW < 0) pos = 'right';

        var css = {};

        switch (pos) {
            case 'bottom':
                css.top  = tRect.bottom + gap;
                css.left = tRect.left + (tRect.width / 2) - (tipW / 2);
                break;
            case 'top':
                css.top  = tRect.top - tipH - gap;
                css.left = tRect.left + (tRect.width / 2) - (tipW / 2);
                break;
            case 'right':
                css.top  = tRect.top + (tRect.height / 2) - (tipH / 2);
                css.left = tRect.right + gap;
                break;
            case 'left':
                css.top  = tRect.top + (tRect.height / 2) - (tipH / 2);
                css.left = tRect.left - tipW - gap;
                break;
        }

        // Keep within the viewport so the tooltip stays reachable even when the
        // target is scrolled near an edge.
        css.left = Math.max(16, Math.min(css.left, winW - tipW - 16));
        css.top  = Math.max(16, Math.min(css.top, winH - tipH - 16));

        $tooltip.css(css).attr('data-position', pos);
    }

    /* ── Render tooltip step ────────────────────────────────── */

    // Open the classic SEO meta box so the editor step has something to anchor
    // to. In the block editor those boxes live in a drawer at the bottom of the
    // canvas that WordPress keeps collapsed, and in the code editor they are not
    // rendered at all — either way the target sits in the DOM without layout.
    // Same recipe as the floating widget's "View full analysis" button in
    // admin/meta-boxes.php; keep the two in step when one of them changes.
    //
    // Retried until the target is laid out, because on post-new.php the tour can
    // fire before Gutenberg has mounted the drawer at all, and a single early
    // attempt would then open nothing.
    function prepareMetaBox(step, done) {
        var tries        = 0;
        var switchedMode = false;

        (function attempt() {
            // 1. The code editor ("text" mode) does not render classic meta
            //    boxes. Switch back once; re-dispatching every round would
            //    fight a user who deliberately switches away.
            if (!switchedMode) {
                try {
                    var editPost = window.wp && wp.data && wp.data.select('core/edit-post');
                    if (editPost && typeof editPost.getEditorMode === 'function' &&
                        editPost.getEditorMode() === 'text') {
                        wp.data.dispatch('core/edit-post').switchEditorMode('visual');
                        switchedMode = true;
                    }
                } catch (e) {}
            }

            // 2. Expand the meta-box drawer. Its toggle is the aria-expanded
            //    button that sits outside the liner, so this stays independent
            //    of the (translated) label on it.
            try {
                var $liner = $('.edit-post-meta-boxes-main__liner, .editor-meta-boxes-area__liner');
                if ($liner.length && $liner.css('display') === 'none') {
                    var $drawerToggle = $('.edit-post-meta-boxes-main, .editor-meta-boxes-area')
                        .find('button[aria-expanded="false"]')
                        .filter(function() {
                            return $(this).closest('.edit-post-meta-boxes-main__liner, .editor-meta-boxes-area__liner').length === 0;
                        }).first();
                    if ($drawerToggle.length) {
                        $drawerToggle.trigger('click');
                    }
                }
            } catch (e) {}

            // 3. Open the postbox itself when WordPress remembered it collapsed.
            //    Click its own handle first, so postboxes.js flips the class,
            //    updates aria-expanded and remembers the new state. Removing
            //    the class ourselves *and* clicking would toggle it twice and
            //    land back on closed, with only a forced inline display to
            //    keep the box visible.
            var $postbox = $('#tam-rank-seo');
            if ($postbox.length && $postbox.hasClass('closed')) {
                var $handle = $postbox.find('.handlediv').first();
                if ($handle.length) {
                    $handle.trigger('click');
                }
                // Belt to those braces: no handle, or a click that did not take.
                if ($postbox.hasClass('closed')) {
                    $postbox.removeClass('closed');
                    $postbox.find('.inside').show();
                }
            }

            var $el = $(step.target).first();
            if (($el.length && isAnchorable($el)) || ++tries >= 25) {
                done();
                return;
            }
            setTimeout(attempt, 150);
        })();
    }

    var preparers = { metabox: prepareMetaBox };

    // Optional pre-render activation so the target is actually visible:
    //  - step.hash:    a settings pill-tab (anchor) to activate
    //  - step.click:   any element to click first (e.g. a dashboard sub-tab)
    //  - step.prepare: a named routine for targets a click cannot reach
    function preActivate(step, done) {
        var acted = false;

        if (step.hash) {
            var $tab = $('a.tr-pill[href="' + step.hash + '"]');
            if ($tab.length && !$tab.hasClass('is-active')) {
                $tab.trigger('click');
            }
            acted = true;
        }
        if (step.click) {
            $(step.click).first().trigger('click');
            acted = true;
        }

        var after = function() {
            // Let the tab/panel switch paint before the target is re-queried.
            setTimeout(done, acted ? 150 : 0);
        };

        if (step.prepare && preparers[step.prepare]) {
            preparers[step.prepare](step, after);
        } else {
            after();
        }
    }

    // Wait for the target to be laid out before deciding it cannot be shown.
    // A panel switch paints asynchronously, so a single synchronous check would
    // send a perfectly good step to the fallback card. Calls back with the
    // element, or with null when it never turns up in the DOM at all.
    function waitForTarget(selector, done) {
        var tries = 0;
        (function poll() {
            var $el = $(selector).first();
            if ($el.length && isAnchorable($el)) {
                done($el);
                return;
            }
            if (++tries >= 10) {
                done($el.length ? $el : null);
                return;
            }
            setTimeout(poll, 100);
        })();
    }

    function showTooltip(step, index) {
        preActivate(step, function() {
            waitForTarget(step.target, function($target) {
                // Not in the DOM at all (a panel this install does not have):
                // move on rather than talk about something that is not there.
                if (!$target) {
                    next(index);
                    return;
                }
                renderTooltip($target, step, index);
            });
        });
    }

    // True when the backdrop, not the target, is what the user sees at the
    // target's centre.
    function backdropCoversTarget($target) {
        if (!$target.length || !$('.tr-tour__backdrop').length) return false;

        var rect = $target[0].getBoundingClientRect();
        var x = Math.min(Math.max(rect.left + rect.width / 2, 1), window.innerWidth - 1);
        var y = Math.min(Math.max(rect.top + rect.height / 2, 1), window.innerHeight - 1);

        var onTop = document.elementFromPoint(x, y);
        return !!(onTop && $(onTop).closest('.tr-tour__backdrop').length);
    }

    // Raise every ancestor that opens its own stacking context, so the target's
    // own z-index can actually reach above the backdrop. cleanup() takes the
    // classes off again.
    function liftStackingAncestors($target) {
        var el = $target[0].parentElement;

        while (el && el !== document.body && el !== document.documentElement) {
            var cs = window.getComputedStyle(el);
            var opens = cs.zIndex !== 'auto' ||
                cs.transform !== 'none' ||
                cs.filter !== 'none' ||
                cs.perspective !== 'none' ||
                cs.contain === 'paint' || cs.contain === 'layout' ||
                parseFloat(cs.opacity) < 1 ||
                (cs.willChange && /transform|opacity|filter/.test(cs.willChange));

            if (opens) {
                $(el).addClass('tr-tour__lifted');
                // z-index does nothing on a statically positioned box.
                if (cs.position === 'static') {
                    $(el).addClass('tr-tour__lifted--static');
                }
            }
            el = el.parentElement;
        }
    }

    // A z-index only lifts an element inside its own stacking context, so a
    // highlighted box can end up UNDER the full-screen backdrop however high we
    // set its z-index. Measure it rather than guess, then escalate: first lift
    // the containers that trap it, and if that still leaves the backdrop on
    // top, undo the lift and drop the backdrop instead. The highlight ring and
    // pulse then carry the step; clicking outside no longer ends the tour,
    // which the Skip button still does.
    //
    // The block editor always lands in that second case, measured on
    // post-new.php: the chain from the meta box up is liner (isolation:
    // isolate) -> meta-boxes-main (z-index) -> skeleton__content (isolation:
    // isolate) -> editor-editor-interface (position: fixed). Lifting all of
    // those means lifting the whole editor above the backdrop, which dims
    // nothing anyway, so we leave WordPress' layout alone and lose the dim.
    function ensureHighlightOnTop($target) {
        if (!backdropCoversTarget($target)) return;

        liftStackingAncestors($target);

        if (backdropCoversTarget($target)) {
            $('.tr-tour__lifted').removeClass('tr-tour__lifted tr-tour__lifted--static');
            $('.tr-tour__backdrop').remove();
        }
    }

    function renderTooltip($target, step, index) {
        // Target is in the DOM but not laid out (e.g. the SEO meta box inside a
        // collapsed Gutenberg meta-box drawer): show a centered card instead of
        // pinning a broken tooltip to the corner.
        if (!isAnchorable($target)) {
            showFallbackCard(step, index);
            return;
        }

        cleanup();

        // Backdrop
        $('<div class="tr-tour__backdrop"></div>').appendTo('body');

        // Highlight target
        $target.addClass('tr-tour__highlighted');

        // Pulse ring — positioned by reposition() once layout settles.
        $('<div class="tr-tour__pulse"></div>').appendTo('body');

        // Tooltip
        var isLast = (index === totalSteps - 1);

        var html = '<div class="tr-tour__tooltip">' +
            '<div class="tr-tour__tooltip-header">' +
                '<span class="tr-tour__counter">' + stepLabel(index) + '</span>' +
            '</div>' +
            '<h3 class="tr-tour__title">' + step.title + '</h3>' +
            '<p class="tr-tour__text">' + step.text + '</p>' +
            '<div class="tr-tour__actions">' +
                '<button type="button" class="tr-tour__btn-skip">' + (i18n.skip || 'Skip') + '</button>' +
                '<button type="button" class="tr-tour__btn-next">' + (isLast ? (i18n.finish || 'Get started!') : (i18n.next || 'Next') + ' &rarr;') + '</button>' +
            '</div>' +
        '</div>';

        var $tooltip = $(html).appendTo('body');

        // Register as the active anchor so scroll/resize keep it aligned.
        activeTarget    = $target;
        activeTooltip   = $tooltip;
        activePreferred = step.position || 'bottom';

        scrollTo($target, function() {
            reposition();
            ensureHighlightOnTop($target);
            $tooltip.addClass('is-visible');
            startTracking();
        });

        // Events
        $tooltip.find('.tr-tour__btn-next').on('click', function() {
            if (isLast) { finish(); } else { next(index); }
        });
        $tooltip.find('.tr-tour__btn-skip').on('click', finish);
        $('.tr-tour__backdrop').on('click', finish);
    }

    /* ── Render card step (floating, no target) ─────────────── */

    // Used for two things: a step that has no target by design (the FREE
    // welcome card) and the fallback for a tooltip step whose target cannot be
    // anchored. Next advances the tour and only finishes on the last step, so
    // neither use can cut the tour short. `nextLabel` / `skipLabel` let a step
    // name its own buttons — the welcome card says "Start the tour" and "Skip
    // the tour" rather than the generic Next/Skip.
    function showCard(step, index) {
        cleanup();

        $('<div class="tr-tour__backdrop"></div>').appendTo('body');

        var isLast = (index === totalSteps - 1);

        var featuresHtml = '';
        if (step.features && step.features.length) {
            featuresHtml = '<ul class="tr-tour__features">';
            for (var i = 0; i < step.features.length; i++) {
                var f = step.features[i];
                var icon = icons[f.icon] || '';
                featuresHtml += '<li class="tr-tour__feature">' +
                    '<div class="tr-tour__feature-icon">' + icon + '</div>' +
                    '<div class="tr-tour__feature-body">' +
                        '<strong>' + f.label + '</strong>' +
                        '<span>' + f.desc + '</span>' +
                    '</div>' +
                '</li>';
            }
            featuresHtml += '</ul>';
        }

        var nextText = step.nextLabel ||
            (isLast ? (i18n.finish || 'Get started!') : (i18n.next || 'Next') + ' &rarr;');
        // A step that names its own skip label means it, so that button gets
        // the weight to match (see the modifier in tamrank-tour.css).
        var skipText  = step.skipLabel || (i18n.skip || 'Skip');
        var skipClass = 'tr-tour__btn-skip' + (step.skipLabel ? ' tr-tour__btn-skip--stated' : '');

        // An uncounted step has no number to show, so it gets no header row at
        // all rather than an empty one holding open its margin.
        var label      = stepLabel(index);
        var headerHtml = label
            ? '<div class="tr-tour__tooltip-header"><span class="tr-tour__counter">' + label + '</span></div>'
            : '';

        var html = '<div class="tr-tour__card">' +
            headerHtml +
            '<h3 class="tr-tour__title">' + step.title + '</h3>' +
            '<p class="tr-tour__text">' + step.text + '</p>' +
            featuresHtml +
            '<div class="tr-tour__actions">' +
                '<button type="button" class="' + skipClass + '">' + skipText + '</button>' +
                '<button type="button" class="tr-tour__btn-next">' + nextText + '</button>' +
            '</div>' +
        '</div>';

        var $card = $(html).appendTo('body');

        // Small delay for animation
        setTimeout(function() { $card.addClass('is-visible'); }, 50);

        $card.find('.tr-tour__btn-next').on('click', function() {
            if (isLast) { finish(); } else { next(index); }
        });
        $card.find('.tr-tour__btn-skip').on('click', finish);
        $('.tr-tour__backdrop').on('click', finish);
    }

    // Centered fallback for a tooltip step whose target cannot be anchored.
    function showFallbackCard(step, index) {
        showCard(step, index);
    }

    /* ── Navigation ─────────────────────────────────────────── */

    function show(index) {
        if (index < 0 || index >= totalSteps) {
            finish();
            return;
        }

        var step = steps[index];

        // Wrong page? Navigate there.
        if (step.page && step.page !== currentPage) {
            setStep(index);
            var url = config.urls[step.page.replace('tam-rank-', '')] || config.urls.dashboard;
            window.location.href = url;
            return;
        }

        setStep(index);

        if (step.type === 'card') {
            showCard(step, index);
        } else {
            showTooltip(step, index);
        }
    }

    function next(currentIndex) {
        show(currentIndex + 1);
    }

    function finish() {
        cleanup();
        clearStep();

        // Persist server-side (per tour type, so free/pro dismiss independently)
        $.post(config.ajaxUrl, {
            action:   'tam_rank_tour_done',
            tourType: config.tourType || '',
            nonce:    config.nonce
        });

        // Redirect to dashboard if not already there
        if (currentPage !== 'tam-rank-dashboard') {
            window.location.href = config.urls.dashboard;
        }
    }

    /* ── Init ───────────────────────────────────────────────── */

    $(function() {
        var step = getStep();

        // No active tour
        if (step < 0) return;

        // The editor step navigates to post-new.php, where WP shows its own
        // welcome guide. Dismiss it now and once more after the editor mounts
        // so it never overlaps the tour.
        dismissWpWelcomeGuide();
        setTimeout(dismissWpWelcomeGuide, 600);

        // Small delay to let page render settle
        setTimeout(function() {
            show(step);
        }, 500);
    });

})(jQuery);
