/**
 * TamRank — bulk alt text for media that is already in the library.
 *
 * Three buttons, one walk. "Preview" runs the whole library through the dry-run
 * route and shows the real values without writing anything; only after that does
 * "Fill in" unlock. "Undo" walks the attachments carrying our marker back.
 *
 * Preview and Fill are off while no image has an empty alt field — the server
 * counts that (`fillable`) and every route that can change it sends the fresh
 * count back, so a run that empties the queue switches them off and an undo
 * switches them on again, both without a reload.
 *
 * Every request carries the cursor the previous response handed back, so a
 * closed tab resumes at the attachment it stopped at instead of starting over.
 */
(function ($) {
    'use strict';

    var cfg  = window.tamrankAltBulk || {};
    var i18n = cfg.i18n || {};

    var $panel = $('#tr-alt-bulk-panel');
    if (!$panel.length) {
        return;
    }

    var $preview = $('#tr-alt-bulk-preview');
    var $run     = $('#tr-alt-bulk-run');
    var $undo    = $('#tr-alt-bulk-undo');
    var $status  = $('#tr-alt-bulk-status');
    var $result  = $('#tr-alt-bulk-result');
    var $resume  = $('#tr-alt-bulk-resume');
    var $filled  = $('#tr-alt-bulk-filled');
    var $nothing = $('#tr-alt-bulk-nothing');

    // The switch this panel hangs off. It saves itself — see the bottom of this
    // file — so the panel appears and disappears without a page load.
    var $toggle     = $('#tamrank_alt_bulk_enabled');
    var $tplToggle  = $('#tamrank_alt_templates_enabled');
    var $tplFields  = $('#tr-alt-templates-content');

    // What the last preview found, so the confirm can name a real number.
    var previewed = 0;

    // Whether there is anything to undo at all. The server renders the button
    // disabled when no attachment carries our marker, and re-enabling controls
    // after a request must not quietly undo that.
    var undoAvailable = !$undo.prop('disabled');

    // Whether there is anything to FILL at all — the count of images with an
    // empty alt field, rendered into the panel by the server and refreshed from
    // the closing batch of a run or an undo. Zero switches Preview and Fill off
    // and shows the line that says why.
    //
    // An unreadable attribute means "do not lock anything": a missing count is
    // never a reason to take a working button away.
    var fillable = parseInt($panel.attr('data-fillable'), 10);
    var canFill  = isNaN(fillable) || fillable > 0;

    function sprintf(template, values) {
        var i = 0;
        return String(template).replace(/%(\d+\$)?[ds]/g, function (match, position) {
            var index = position ? parseInt(position, 10) - 1 : i++;
            return typeof values[index] === 'undefined' ? match : values[index];
        });
    }

    // The branded dialog from tam-toast.js, which resolves a Promise where
    // window.confirm returned straight away. tam-rank-toast is a declared
    // dependency of this script, so the function is normally there; the
    // fallback only catches a half-loaded admin, where losing the confirmation
    // step altogether would be worse than a plain browser box.
    function askConfirm(title, message, confirmLabel, variant) {
        if (window.TamRank && typeof window.TamRank.confirm === 'function') {
            return window.TamRank.confirm({
                title:        title,
                message:      message,
                confirmLabel: confirmLabel,
                cancelLabel:  i18n.cancel,
                variant:      variant
            });
        }
        return Promise.resolve(window.confirm(title + '\n\n' + message));
    }

    /**
     * How much there is to undo, said in the two places that say it.
     *
     * The Undo label and the line above the buttons are one fact rendered
     * twice, so they are written from one place with one number. That number
     * always arrives from the server — `marked` on the closing batch of a walk,
     * `filled` from the save route — never from adding up batches here, because
     * a run only counts what IT wrote while the button names everything that
     * carries our marker.
     */
    function setFilled(count) {
        undoAvailable = count > 0;
        $undo.text(sprintf(i18n.undoBtn, [count]));
        $undo.prop('disabled', !undoAvailable);

        if (!$filled.length) {
            return;
        }
        if (count > 0) {
            $filled.find('.tr-alt-bulk-filled__text').text(sprintf(i18n.filled, [count]));
            $filled.removeAttr('hidden');
        } else {
            $filled.attr('hidden', 'hidden');
        }
    }

    /**
     * How much there is left to fill, and the line that says there is nothing.
     *
     * Same discipline as setFilled() above: the number arrives from the server —
     * `fillable` on the closing batch of a walk, or from the save route — never
     * from bookkeeping here. A run only knows what IT wrote; whether an empty
     * alt field is left anywhere is a question only the database can answer.
     */
    function setFillable(count) {
        fillable = count;
        canFill  = count > 0;
        $panel.attr('data-fillable', count);

        if (!$nothing.length) {
            return;
        }
        if (canFill) {
            $nothing.attr('hidden', 'hidden');
        } else {
            $nothing.removeAttr('hidden');
        }
    }

    /** One integer out of a response, or `fallback` when it is not there. */
    function numberFrom(data, key, fallback) {
        var value = parseInt(data[key], 10);
        return isNaN(value) ? fallback : value;
    }

    /** The count the server put in a finished walk, or `fallback` if it is not there. */
    function markedFrom(data, fallback) {
        return numberFrom(data, 'marked', fallback);
    }

    function busy(state) {
        // Nothing with an empty alt field means a preview is guaranteed to come
        // back with nothing, so there is no point in offering it.
        $preview.prop('disabled', state || !canFill);
        // "Fill in" stays locked until a preview has shown what would happen.
        $run.prop('disabled', state || previewed < 1 || !canFill);
        $undo.prop('disabled', state || !undoAvailable);
    }

    function fail(resp) {
        var message = i18n.error;
        if (resp && resp.responseJSON && resp.responseJSON.data && resp.responseJSON.data.message) {
            message = resp.responseJSON.data.message;
        }
        $status.text(message);
        busy(false);
    }

    function post(action, afterId) {
        return $.post(cfg.ajaxUrl, {
            action:   action,
            nonce:    cfg.nonce,
            after_id: afterId
        });
    }

    // ---------------------------------------------------------------- preview

    function renderPreview(totals, samples) {
        $result.empty();

        if (totals.fill < 1) {
            $result.append($('<p class="tr-field-hint"></p>').text(i18n.previewNone));
            return;
        }

        $result.append(
            $('<p class="tr-alt-bulk-summary"></p>').text(
                sprintf(i18n.previewHead, [totals.fill, totals.scanned])
            )
        );
        $result.append(
            $('<p class="tr-field-hint"></p>').text(
                sprintf(i18n.previewSkip, [totals.occupied, totals.empty, totals.unsaved])
            )
        );

        var $table = $('<table class="tr-table tr-alt-bulk-table"></table>');
        $table.append(
            $('<thead></thead>').append(
                $('<tr></tr>')
                    .append($('<th></th>').text(i18n.colImage))
                    .append($('<th></th>').text(i18n.colValue))
            )
        );

        var $body = $('<tbody></tbody>');
        $.each(samples, function (_, sample) {
            $body.append(
                $('<tr></tr>')
                    .append($('<td></td>').text(sample.name))
                    .append($('<td></td>').append($('<code></code>').text(sample.value)))
            );
        });
        $table.append($body);
        $result.append($table);
    }

    function walkPreview(afterId, totals, samples) {
        post('tamrank_alt_bulk_preview', afterId).done(function (resp) {
            if (!resp || !resp.success || !resp.data) {
                fail();
                return;
            }

            var data = resp.data;
            totals.scanned  += data.scanned;
            totals.fill     += data.filled;
            totals.occupied += data.skipped.occupied;
            totals.empty    += data.skipped.empty_result;
            totals.unsaved  += data.skipped.unsaved_parent;

            $.each(data.samples, function (_, sample) {
                if (samples.length < 12) {
                    samples.push(sample);
                }
            });

            if (!data.done) {
                $status.text(i18n.scanning + ' (' + totals.scanned + ')');
                walkPreview(data.next_after_id, totals, samples);
                return;
            }

            previewed = totals.fill;
            $status.text('');
            renderPreview(totals, samples);
            busy(false);
        }).fail(fail);
    }

    $preview.on('click', function (e) {
        e.preventDefault();
        // Same shape of guard as "Fill in" below: the button is rendered
        // disabled when nothing has an empty alt field, and this is what makes
        // that a rule rather than an appearance.
        if (!canFill) {
            return;
        }
        previewed = 0;
        busy(true);
        $result.empty();
        $status.text(i18n.scanning);
        walkPreview(0, { scanned: 0, fill: 0, occupied: 0, empty: 0, unsaved: 0 }, []);
    });

    // -------------------------------------------------------------------- run

    function walkRun(afterId, filled) {
        post('tamrank_alt_bulk_run', afterId).done(function (resp) {
            if (!resp || !resp.success || !resp.data) {
                fail();
                return;
            }

            var done = filled + resp.data.filled;

            if (!resp.data.done) {
                $status.text(sprintf(i18n.running, [done]));
                walkRun(resp.data.next_after_id, done);
                return;
            }

            previewed = 0;
            // The button and the line above it, from the count the server just
            // took — not from `done`, which is only what this run wrote.
            setFilled(markedFrom(resp.data, done));
            // And whether anything is still open. A run that emptied the queue
            // switches Preview and Fill off right here, without a reload.
            setFillable(numberFrom(resp.data, 'fillable', fillable));
            // Toast first, status line after: with no toast layer notify()
            // falls back to that same line, and the run's own message is the
            // one that should be left standing there.
            notify(sprintf(i18n.runToast, [done]), 'success');
            $status.text(sprintf(i18n.runDone, [done]));
            $result.empty();
            busy(false);
        }).fail(fail);
    }

    $run.on('click', function (e) {
        e.preventDefault();
        if (previewed < 1) {
            return;
        }
        // Reversible — the Undo button walks every marked attachment back — so
        // this is the neutral variant, not a warning.
        askConfirm(
            i18n.confirmRunTitle,
            sprintf(i18n.confirmRun, [previewed]),
            i18n.confirmRunLabel,
            'default'
        ).then(function (ok) {
            if (!ok) {
                return;
            }
            busy(true);
            $status.text(sprintf(i18n.running, [0]));
            // $run carries the resume cursor of an interrupted run; 0 otherwise.
            walkRun(parseInt($run.data('resume') || 0, 10), 0);
            $run.data('resume', 0);
        });
    });

    // ------------------------------------------------------------------- undo

    function walkUndo(afterId, cleared, kept) {
        post('tamrank_alt_bulk_undo', afterId).done(function (resp) {
            if (!resp || !resp.success || !resp.data) {
                fail();
                return;
            }

            var total = cleared + resp.data.cleared;
            var left  = kept + resp.data.kept;

            if (!resp.data.done) {
                $status.text(sprintf(i18n.undoing, [total]));
                walkUndo(resp.data.next_after_id, total, left);
                return;
            }

            // Every marker is gone now — the ones we emptied and the ones you
            // had edited — so the count the server reports back is 0, the
            // button says (0) and the line above it disappears. No toast here:
            // that disappearing line IS the confirmation.
            setFilled(markedFrom(resp.data, 0));
            // An undo puts alt fields back to empty, so it is the one walk that
            // hands Preview and Fill their work back.
            setFillable(numberFrom(resp.data, 'fillable', fillable));
            $status.text(sprintf(i18n.undoDone, [total, left]));
            busy(false);
        }).fail(fail);
    }

    $undo.on('click', function (e) {
        e.preventDefault();
        // Emptying fields is the one step here that takes something away, so
        // caution amber rather than the neutral icon.
        askConfirm(
            i18n.confirmUndoTitle,
            i18n.confirmUndo,
            i18n.confirmUndoLabel,
            'caution'
        ).then(function (ok) {
            if (!ok) {
                return;
            }
            busy(true);
            $status.text(sprintf(i18n.undoing, [0]));
            walkUndo(0, 0, 0);
        });
    });

    // -------------------------------------------- the switch saves itself ----
    //
    // Every other control on this page waits for the Save button. This one does
    // not, because the thing it reveals is a panel that reads the SAVED setting:
    // one card higher, "Convert existing images" sits there unconditionally, and
    // a switch that answers a click with "save your settings to use this" next
    // to it reads as broken. The trade — one control on the page behaving
    // differently from its neighbours — was made deliberately.

    /**
     * The four fields of this settings block, exactly as they stand right now.
     *
     * The whole block travels, not just the flag: the panel previews what the
     * alt pattern would write, and an unsaved pattern on screen with the old one
     * in the database would make that preview lie.
     */
    function readFormSettings() {
        return {
            enabled:        $tplToggle.is(':checked') ? 1 : 0,
            bulk_enabled:   $toggle.is(':checked') ? 1 : 0,
            alt_template:   $('#tamrank_alt_template').val() || '',
            title_template: $('#tamrank_title_template').val() || ''
        };
    }

    function notify(message, type) {
        if (window.TamRank && typeof window.TamRank.notify === 'function') {
            window.TamRank.notify(message, type);
            return;
        }
        // No toast layer: say it in the panel's own status line rather than
        // writing to the database in complete silence.
        $status.text(message);
    }

    // The pattern fields belong to both switches. Their normal sync lives in the
    // settings page's own inline script, on the same change event; this repeats
    // it only for the revert path, where the checkbox moves back under the
    // user's feet and no change event fires.
    function syncPatternFields() {
        if (!$tplFields.length) {
            return;
        }
        if ($tplToggle.is(':checked') || $toggle.is(':checked')) {
            $tplFields.removeAttr('hidden');
        } else {
            $tplFields.attr('hidden', 'hidden');
        }
    }

    /** Show the panel, and with numbers counted just now — not at page load. */
    function applySavedState(data) {
        if (data.bulk_enabled) {
            $panel.removeAttr('hidden');
        } else {
            $panel.attr('hidden', 'hidden');
        }

        setFilled(parseInt(data.filled, 10) || 0);
        setFillable(numberFrom(data, 'fillable', fillable));

        if (data.resume) {
            $resume.text(sprintf(i18n.resume, [parseInt(data.resume.filled, 10) || 0]));
            $resume.removeAttr('hidden');
            $run.data('resume', parseInt(data.resume.after_id, 10) || 0);
        } else {
            $resume.attr('hidden', 'hidden');
            $run.data('resume', 0);
        }

        // A fresh panel has not previewed anything yet, whatever the last one did.
        previewed = 0;
        $result.empty();
        $status.text('');
        busy(false);
    }

    $toggle.on('change', function () {
        var wanted = $toggle.is(':checked');

        // Locked for the round trip: a second flip mid-request would race the
        // first and the checkbox could end up disagreeing with the database.
        $toggle.prop('disabled', true);

        $.post(cfg.ajaxUrl, {
            action:   'tamrank_alt_settings_save',
            nonce:    cfg.settingsNonce,
            settings: readFormSettings()
        }).done(function (resp) {
            if (!resp || !resp.success || !resp.data) {
                revert(wanted, null);
                return;
            }
            applySavedState(resp.data);
            notify(wanted ? i18n.savedOn : i18n.savedOff, 'success');
        }).fail(function (xhr) {
            revert(wanted, xhr);
        }).always(function () {
            $toggle.prop('disabled', false);
        });
    });

    /**
     * The save failed, so put the checkbox back where it was and say so.
     *
     * A checkbox that stays where the user left it while the database says
     * something else is the one outcome worse than the failure itself.
     */
    function revert(wanted, xhr) {
        $toggle.prop('checked', !wanted);
        syncPatternFields();

        var message = i18n.saveFailed;
        if (xhr && xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
            message = xhr.responseJSON.data.message;
        }
        notify(message, 'error');
    }

    busy(false);
})(jQuery);
