<?php

declare(strict_types=1);

namespace FecaMailshots\Admin;

use FecaMailshots\Application\MailshotService;
use FecaMailshots\WordPress\WordPressFacade;

final class MailshotsAdminPage
{
    use AdminRequestHelpers;

    private const CAPABILITY = 'edit_pages';
    private const RESULT_OPTION_KEY = 'feca_mailshots_mailshots_ui_result';
    private const DRAFT_OPTION_KEY = 'feca_mailshots_mailshots_ui_draft';
    private const NONCE_ACTION = 'feca_mailshots_mailshots';

    /** @var callable(): MailshotService */
    private $serviceFactory;
    private WordPressFacade $wp;

    /** @param callable(): MailshotService $serviceFactory */
    public function __construct(callable $serviceFactory, WordPressFacade $wp)
    {
        $this->serviceFactory = $serviceFactory;
        $this->wp = $wp;
    }

    public function register(): void
    {
        $this->wp->addAction('admin_menu', [$this, 'registerMenu']);
        $this->wp->addAction('admin_post_feca_mailshots_mailshots_api', [$this, 'handleApi']);
        $this->wp->addAction('admin_post_feca_mailshots_mailshots_ui_save', [$this, 'handleUiSave']);
        $this->wp->addAction('admin_post_feca_mailshots_mailshots_ui_delete', [$this, 'handleUiDelete']);
        $this->wp->addAction('admin_post_feca_mailshots_mailshots_ui_duplicate', [$this, 'handleUiDuplicate']);
    }

    public function registerMenu(): void
    {
        $this->wp->addSubmenuPage('feca-mailshot', 'Mailshots', 'Mailshots', self::CAPABILITY, 'feca-mailshots-mailshots', [$this, 'render']);
    }

    public function render(): void
    {
        if (!$this->wp->currentUserCan(self::CAPABILITY)) {
            echo 'Permission denied';
            return;
        }

        $sortBy = (string) ($this->wp->requestParam('sort_by', 'purpose') ?? 'purpose');
        if (!in_array($sortBy, ['purpose', 'data_source', 'subject'], true)) {
            $sortBy = 'purpose';
        }
        $sortDirection = strtolower((string) ($this->wp->requestParam('sort_direction', 'asc') ?? 'asc'));
        if (!in_array($sortDirection, ['asc', 'desc'], true)) {
            $sortDirection = 'asc';
        }
        $items = $this->sortedMailshotItems($this->service()->list(), $sortBy, $sortDirection);
        $dataSources = $this->service()->dataSourceNames();
        sort($dataSources, SORT_NATURAL | SORT_FLAG_CASE);
        $attachmentNames = $this->service()->attachmentNames();
        sort($attachmentNames, SORT_NATURAL | SORT_FLAG_CASE);

        $draftRaw = $this->wp->getOption(self::DRAFT_OPTION_KEY, null);
        $draft = is_array($draftRaw) ? $draftRaw : null;
        $requestedEditId = (int) ($this->wp->requestParam('edit_id', '0') ?? '0');
        $draftId = is_array($draft) && isset($draft['id']) ? (int) $draft['id'] : 0;
        $useDraft = is_array($draft) && isset($draft['form']) && (
            $requestedEditId <= 0 || ($draftId > 0 && $draftId === $requestedEditId)
        );
        $editId = $requestedEditId;
        if ($editId <= 0 && $useDraft && $draftId > 0) {
            $editId = $draftId;
        }
        $editItem = null;
        foreach ($items as $row) {
            if ((int) ($row['id'] ?? 0) === $editId) {
                $editItem = $row;
                break;
            }
        }

        $selectedAttachments = [];
        $decoded = json_decode((string) ($editItem['AttachmentNames'] ?? '[]'), true);
        if (is_array($decoded)) {
            foreach ($decoded as $v) {
                $s = trim((string) $v);
                if ($s !== '') {
                    $selectedAttachments[] = $s;
                }
            }
        }
        $selectedAttachments = array_values(array_unique($selectedAttachments));

        $form = [
            'Purpose' => (string) ($editItem['Purpose'] ?? ''),
            'DataSource' => (string) ($editItem['DataSource'] ?? ''),
            'CC' => (string) ($editItem['CC'] ?? ''),
            'BCC' => (string) ($editItem['BCC'] ?? ''),
            'Subject' => (string) ($editItem['Subject'] ?? ''),
            'Message' => (string) ($editItem['Message'] ?? ''),
            'PDFAttachment' => (string) ($editItem['PDFAttachment'] ?? ''),
            'PDFFilenameDerivedFrom' => (string) ($editItem['PDFFilenameDerivedFrom'] ?? ''),
            'RecipientEmailField' => (string) ($editItem['RecipientEmailField'] ?? ''),
            'ReplyTo' => (string) ($editItem['ReplyTo'] ?? ''),
        ];
        if ($useDraft && is_array($draft) && isset($draft['form']) && is_array($draft['form'])) {
            foreach (['Purpose', 'DataSource', 'CC', 'BCC', 'Subject', 'Message', 'PDFAttachment', 'PDFFilenameDerivedFrom', 'RecipientEmailField', 'ReplyTo'] as $key) {
                if (array_key_exists($key, $draft['form'])) {
                    $form[$key] = (string) $draft['form'][$key];
                }
            }
            $csv = trim((string) ($draft['form']['AttachmentNamesCsv'] ?? ''));
            if ($csv !== '') {
                $selectedAttachments = array_values(array_unique(array_filter(array_map('trim', explode(',', $csv)), static fn(string $v): bool => $v !== '')));
            } else {
                $selectedAttachments = [];
            }
        }

        $dataSource = (string) ($form['DataSource'] ?? '');
        $tokenData = $dataSource !== '' ? $this->service()->tokenInsertionData($dataSource) : ['tokens' => [], 'errors' => []];
        $pdfFieldOptions = [];
        foreach (($tokenData['tokens'] ?? []) as $t) {
            if (is_array($t) && isset($t['field'])) {
                $pdfFieldOptions[] = (string) $t['field'];
            }
        }

        $result = $this->consumeOptionArray(self::RESULT_OPTION_KEY);
        $action = htmlspecialchars($this->wp->adminUrl('admin-post.php'));
        $pluginMainFile = dirname(__DIR__, 2) . '/feca_mailshots_plugin.php';
        $joditCssUrl = function_exists('plugins_url')
            ? \plugins_url('assets/vendor/jodit/jodit.min.css', $pluginMainFile)
            : '';
        $joditJsUrl = function_exists('plugins_url')
            ? \plugins_url('assets/vendor/jodit/jodit.min.js', $pluginMainFile)
            : '';
        $aceJsUrl = function_exists('plugins_url')
            ? \plugins_url('assets/vendor/ace/ace.js', $pluginMainFile)
            : '';
        $aceBaseUrl = function_exists('plugins_url')
            ? \plugins_url('assets/vendor/ace', $pluginMainFile)
            : '';

        echo '<div class="wrap feca-mailshots-admin"><h1>Mailshots</h1>';
        echo $this->renderAdminUiStyles();
        $isSaveError = (($result['context'] ?? '') === 'mailshot_save') && empty($result['ok']);
        if ($result !== null && !$isSaveError && (!is_array($draft) || !empty($result['ok']))) {
            $ok = !empty($result['ok']);
            $bannerClass = $ok ? 'feca-banner-success' : 'feca-banner-error';
            $successText = 'Mailshot saved.';
            if (($result['context'] ?? '') === 'mailshot_duplicate') {
                $successText = 'Mailshot duplicated.';
            } elseif (($result['context'] ?? '') === 'mailshot_delete') {
                $successText = 'Mailshot deleted.';
            }
            echo '<div class="feca-banner ' . $bannerClass . '">';
            echo '<strong>' . ($ok ? htmlspecialchars($successText) : 'Mailshot action failed.') . '</strong>';
            if (!empty($result['errors']) && is_array($result['errors'])) {
                echo '<p class="feca-banner-note">' . htmlspecialchars(implode('; ', $result['errors'])) . '</p>';
            }
            echo '</div>';
        }

        echo '<p class="feca-button-row"><button type="button" class="button button-primary" id="ms-open-new">New Mailshot</button></p>';
        echo '<div id="ms-editor-modal" class="feca-modal-overlay">';
        echo '<div class="feca-modal-shell feca-modal-shell-wide">';
        echo '<form method="post" action="' . $action . '" id="mailshot-editor" class="feca-form">';
        echo '<h2 class="feca-modal-title">' . ($editId > 0 ? 'Edit Mailshot' : 'New Mailshot') . '</h2>';
        echo $this->modalErrorHtml($result);
        echo '<input type="hidden" name="action" value="feca_mailshots_mailshots_ui_save">';
        echo $this->hiddenNonceField(self::NONCE_ACTION);
        echo '<input type="hidden" name="sort_by" value="' . htmlspecialchars($sortBy, ENT_QUOTES) . '">';
        echo '<input type="hidden" name="sort_direction" value="' . htmlspecialchars($sortDirection, ENT_QUOTES) . '">';
        if ($editId > 0) {
            echo '<input type="hidden" name="id" value="' . $editId . '" id="ms-editor-id">';
        } else {
            echo '<input type="hidden" name="id" value="" id="ms-editor-id">';
        }

        echo '<table class="form-table" role="presentation">';
        echo '<tr><th scope="row"><label for="ms_purpose">Purpose</label></th><td><input class="regular-text" type="text" id="ms_purpose" name="Purpose" value="' . htmlspecialchars($form['Purpose'], ENT_QUOTES) . '"></td>';
        echo '<th scope="row"><label for="ms_ds">Data Source</label></th><td><select id="ms_ds" name="DataSource"><option value="">Select data source</option>';
        foreach ($dataSources as $ds) {
            $sel = $ds === $form['DataSource'] ? ' selected' : '';
            echo '<option value="' . htmlspecialchars($ds, ENT_QUOTES) . '"' . $sel . '>' . htmlspecialchars($ds) . '</option>';
        }
        echo '</select></td></tr>';

        echo '<tr><th scope="row">Available Tokens</th><td colspan="3"><div id="ms-token-controls"></div><p class="description" id="ms-token-help">Select a data source to view tokens.</p></td></tr>';

        echo '<tr><th scope="row"><label for="ms_subject">Subject</label></th><td colspan="3"><textarea id="ms_subject" name="Subject" rows="2" class="large-text">' . htmlspecialchars($form['Subject']) . '</textarea></td></tr>';
        echo '<tr><th scope="row">Message</th><td><button type="button" class="button" id="ms-edit-message">Edit Message</button> <span class="description" id="ms-message-meta"></span><textarea id="ms_message" name="Message" rows="10" class="large-text code feca-hidden">' . htmlspecialchars($form['Message']) . '</textarea><div id="ms-message-snippet" class="description feca-banner-note"></div></td>';
        echo '<th scope="row">PDF Attachment</th><td><button type="button" class="button" id="ms-edit-pdf">Edit PDF Attachment</button> <span class="description" id="ms-pdf-meta"></span><textarea id="ms_pdfa" name="PDFAttachment" rows="8" class="large-text code feca-hidden">' . htmlspecialchars($form['PDFAttachment']) . '</textarea><div id="ms-pdf-snippet" class="description feca-banner-note"></div></td></tr>';

        echo '<tr><th scope="row"><label for="ms_pdfname">PDF Filename Derived From</label></th><td><select id="ms_pdfname" name="PDFFilenameDerivedFrom"><option value="">(none)</option>';
        foreach ($pdfFieldOptions as $f) {
            $sel = $f === $form['PDFFilenameDerivedFrom'] ? ' selected' : '';
            echo '<option value="' . htmlspecialchars($f, ENT_QUOTES) . '"' . $sel . '>' . htmlspecialchars($f) . '</option>';
        }
        echo '</select></td>';
        echo '<th scope="row"><label for="ms_email_field">Recipient Email Field</label></th><td><select id="ms_email_field" name="RecipientEmailField"><option value="">Select field</option>';
        foreach ($pdfFieldOptions as $f) {
            $sel = $f === $form['RecipientEmailField'] ? ' selected' : '';
            echo '<option value="' . htmlspecialchars($f, ENT_QUOTES) . '"' . $sel . '>' . htmlspecialchars($f) . '</option>';
        }
        echo '</select><p class="description">Optional. Set this for run/send workflows; leave blank for PDF-only workflows.</p>';
        echo '<div id="ms-recipient-warning" class="feca-warning-inline">Mailshot does not have a specified RecipientEmailField, it cannot be used to send a mailshot.</div>';
        echo '</td></tr>';

        echo '<tr><th scope="row">Attachments</th><td colspan="3">';
        echo '<select id="ms_attachment_pick"><option value="">Select attachment</option>';
        foreach ($attachmentNames as $name) {
            echo '<option value="' . htmlspecialchars($name, ENT_QUOTES) . '">' . htmlspecialchars($name) . '</option>';
        }
        echo '</select> <button type="button" class="button button-small" id="ms_attachment_add">Add Selected Attachment</button>';
        echo '<ul id="ms_attachment_list" class="feca-banner-note">';
        foreach ($selectedAttachments as $name) {
            echo '<li data-name="' . htmlspecialchars($name, ENT_QUOTES) . '">' . htmlspecialchars($name) . ' <button type="button" class="button-link-delete ms-att-remove">Remove</button></li>';
        }
        echo '</ul>';
        echo '<input type="hidden" name="AttachmentNamesCsv" id="ms_attachment_csv" value="' . htmlspecialchars(implode(', ', $selectedAttachments), ENT_QUOTES) . '">';
        echo '</td></tr>';

        echo '<tr><th scope="row"><label for="ms_cc">CC</label></th><td><input class="regular-text" type="text" id="ms_cc" name="CC" value="' . htmlspecialchars($form['CC'], ENT_QUOTES) . '"></td>';
        echo '<th scope="row"><label for="ms_bcc">BCC</label></th><td><input class="regular-text" type="text" id="ms_bcc" name="BCC" value="' . htmlspecialchars($form['BCC'], ENT_QUOTES) . '"></td></tr>';
        echo '<tr><th scope="row"><label for="ms_reply">Reply-To</label></th><td colspan="3"><input class="regular-text" type="text" id="ms_reply" name="ReplyTo" value="' . htmlspecialchars($form['ReplyTo'], ENT_QUOTES) . '"></td></tr>';
        echo '</table>';

        echo '<p class="feca-button-row"><button type="submit" class="button button-primary">Save</button> ';
        echo '<button type="button" class="button" id="ms-close-editor">Quit</button> ';
        echo '</p>';
        echo '</form>';

        echo '<div id="ms-template-modal" class="feca-modal-overlay-high">';
        echo '<div id="ms-template-panel" class="feca-template-panel">';
        echo '<h3 id="ms-template-title" class="feca-template-title">Template Editor</h3>';
        echo '<div id="ms-template-error" class="feca-template-error"></div>';
        echo '<div id="ms-template-token-controls" class="feca-template-token-controls"></div>';
        echo '<div id="ms-template-pdf-assets" class="feca-template-assets"><label>PDF Asset <select id="ms_pdf_asset_pick"><option value="">Select PDF asset</option></select></label> <button type="button" class="button button-small" id="ms_pdf_asset_insert">Insert Asset Token</button><p class="description">Inserts Twig helper token, example: {{ pdf_asset(\'logo_asset\') }}.</p></div>';
        echo '<link rel="stylesheet" href="' . htmlspecialchars($joditCssUrl, ENT_QUOTES) . '">';
        echo '<script src="' . htmlspecialchars($joditJsUrl, ENT_QUOTES) . '"></script>';
        echo '<script src="' . htmlspecialchars($aceJsUrl, ENT_QUOTES) . '"></script>';
        echo '<div id="ms-template-editor-wrap" class="feca-template-editor-wrap">';
        echo '<textarea id="ms-template-jodit" rows="14" class="large-text code feca-template-editor"></textarea>';
        echo '</div>';
        echo '<p class="feca-template-actions"><button type="button" class="button button-primary" id="ms-template-save">Save</button> <button type="button" class="button" id="ms-template-close">Quit</button></p>';
        echo '</div></div>';
        echo '</div></div>';

        echo '<h2>Existing Mailshots</h2>';
        echo '<form method="get" action="' . htmlspecialchars($this->wp->adminUrl('admin.php')) . '" class="feca-form">';
        echo '<input type="hidden" name="page" value="feca-mailshots-mailshots">';
        if ($editId > 0) {
            echo '<input type="hidden" name="edit_id" value="' . $editId . '">';
        }
        echo '<div class="feca-control-row">';
        echo '<div class="feca-control feca-control-min-260">';
        echo '<label for="ms_sort_by"><strong>Sort by</strong></label>';
        echo '<select id="ms_sort_by" name="sort_by">';
        foreach (['purpose' => 'Purpose', 'data_source' => 'Data Source', 'subject' => 'Subject'] as $value => $label) {
            $selected = $sortBy === $value ? ' selected' : '';
            echo '<option value="' . htmlspecialchars($value, ENT_QUOTES) . '"' . $selected . '>' . htmlspecialchars($label) . '</option>';
        }
        echo '</select>';
        echo '</div>';
        echo '<div class="feca-control feca-control-min-180">';
        echo '<label for="ms_sort_direction"><strong>Direction</strong></label>';
        echo '<select id="ms_sort_direction" name="sort_direction">';
        foreach (['asc' => 'Ascending', 'desc' => 'Descending'] as $value => $label) {
            $selected = $sortDirection === $value ? ' selected' : '';
            echo '<option value="' . htmlspecialchars($value, ENT_QUOTES) . '"' . $selected . '>' . htmlspecialchars($label) . '</option>';
        }
        echo '</select>';
        echo '</div>';
        echo '<div class="feca-control"><label>&nbsp;</label><button class="button" type="submit">Apply</button></div>';
        echo '</div>';
        echo '</form>';
        echo '<div class="feca-scroll-frame"><div class="feca-scroll-pane">';
        echo '<table class="widefat striped"><thead><tr><th>Purpose</th><th>Data Source</th><th>Subject</th><th>Actions</th></tr></thead><tbody>';
        foreach ($items as $row) {
            $id = (int) ($row['id'] ?? 0);
            $editUrl = $this->wp->adminUrl('admin.php?page=feca-mailshots-mailshots&edit_id=' . $id . '&sort_by=' . rawurlencode($sortBy) . '&sort_direction=' . rawurlencode($sortDirection));
            echo '<tr>';
            echo '<td class="feca-truncate-220">' . htmlspecialchars((string) ($row['Purpose'] ?? '')) . '</td>';
            echo '<td class="feca-truncate-220">' . htmlspecialchars((string) ($row['DataSource'] ?? '')) . '</td>';
            echo '<td class="feca-truncate-360">' . htmlspecialchars((string) ($row['Subject'] ?? '')) . '</td>';
            echo '<td><a class="button button-small" href="' . htmlspecialchars($editUrl) . '">Edit</a> ';
            echo '<form method="post" action="' . $action . '" class="feca-inline-form">';
            echo '<input type="hidden" name="action" value="feca_mailshots_mailshots_ui_duplicate">';
            echo $this->hiddenNonceField(self::NONCE_ACTION);
            echo '<input type="hidden" name="id" value="' . $id . '">';
            echo '<input type="hidden" name="sort_by" value="' . htmlspecialchars($sortBy, ENT_QUOTES) . '">';
            echo '<input type="hidden" name="sort_direction" value="' . htmlspecialchars($sortDirection, ENT_QUOTES) . '">';
            echo '<button type="submit" class="button button-small">Duplicate</button>';
            echo '</form> ';
            echo '<form method="post" action="' . $action . '" class="feca-inline-form">';
            echo '<input type="hidden" name="action" value="feca_mailshots_mailshots_ui_delete">';
            echo $this->hiddenNonceField(self::NONCE_ACTION);
            echo '<input type="hidden" name="id" value="' . $id . '">';
            echo '<input type="hidden" name="sort_by" value="' . htmlspecialchars($sortBy, ENT_QUOTES) . '">';
            echo '<input type="hidden" name="sort_direction" value="' . htmlspecialchars($sortDirection, ENT_QUOTES) . '">';
            echo '<button type="submit" class="button button-small feca-button-danger" onclick="return confirm(\'Delete this mailshot?\');">Delete</button>';
            echo '</form></td>';
            echo '</tr>';
        }
        if ($items === []) {
            echo '<tr><td colspan="4">No mailshots found.</td></tr>';
        }
        echo '</tbody></table>';
        echo '</div></div>';

        echo '<script>
            (function () {
                var editorModal = document.getElementById("ms-editor-modal");
                var openNew = document.getElementById("ms-open-new");
                var closeEditor = document.getElementById("ms-close-editor");
                var editor = document.getElementById("mailshot-editor");
                if (!editor) { return; }

                var adminApiBase = ' . json_encode($this->wp->adminUrl('admin-post.php?action=feca_mailshots_mailshots_api'), JSON_UNESCAPED_SLASHES) . ';
                var pdfApiBase = ' . json_encode($this->wp->adminUrl('admin-post.php?action=feca_mailshots_pdf_assets_api'), JSON_UNESCAPED_SLASHES) . ';
                var aceBaseUrl = ' . json_encode($aceBaseUrl, JSON_UNESCAPED_SLASHES) . ';
                var currentTokens = ' . json_encode(array_values(array_filter((array) ($tokenData['tokens'] ?? []), static fn($t): bool => is_array($t))), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ';
                var lastTarget = null;
                var isDirty = false;
                var cleanState = "";
                var snapshotEditorState = function () {
                    var parts = [];
                    editor.querySelectorAll("input,select,textarea").forEach(function (el) {
                        var key = el.id || el.name || "";
                        parts.push(key + "=" + String(el.value || ""));
                    });
                    return parts.join("\n");
                };
                var setCleanState = function () {
                    cleanState = snapshotEditorState();
                    isDirty = false;
                };
                var markDirty = function () {
                    isDirty = snapshotEditorState() !== cleanState;
                };
                var confirmDiscard = function () {
                    markDirty();
                    if (!isDirty) { return true; }
                    return window.confirm("You have unsaved changes. Close without saving?");
                };

                var dsSelect = document.getElementById("ms_ds");
                var tokenControls = document.getElementById("ms-token-controls");
                var tokenHelp = document.getElementById("ms-token-help");
                var pdfNameSelect = document.getElementById("ms_pdfname");
                var emailSelect = document.getElementById("ms_email_field");
                var subjectField = document.getElementById("ms_subject");
                var messageField = document.getElementById("ms_message");
                var pdfField = document.getElementById("ms_pdfa");

                var templateModal = document.getElementById("ms-template-modal");
                var templateTitle = document.getElementById("ms-template-title");
                var templateClose = document.getElementById("ms-template-close");
                var templateSave = document.getElementById("ms-template-save");
                var templateJodit = document.getElementById("ms-template-jodit");
                var templateError = document.getElementById("ms-template-error");
                var templateTokenControls = document.getElementById("ms-template-token-controls");
                var templatePdfAssets = document.getElementById("ms-template-pdf-assets");
                var pdfAssetPick = document.getElementById("ms_pdf_asset_pick");
                var pdfAssetInsert = document.getElementById("ms_pdf_asset_insert");
                var openMessageEditor = document.getElementById("ms-edit-message");
                var openPdfEditor = document.getElementById("ms-edit-pdf");
                var messageSnippet = document.getElementById("ms-message-snippet");
                var messageMeta = document.getElementById("ms-message-meta");
                var pdfSnippet = document.getElementById("ms-pdf-snippet");
                var pdfMeta = document.getElementById("ms-pdf-meta");
                var recipientWarning = document.getElementById("ms-recipient-warning");
                if (window.ace && window.ace.config) {
                    window.ace.config.set("basePath", aceBaseUrl);
                    window.ace.config.set("modePath", aceBaseUrl);
                    window.ace.config.set("themePath", aceBaseUrl);
                    window.ace.config.set("workerPath", aceBaseUrl);
                }

                var activeTemplateField = null;
                var templateDirty = false;
                var templateJoditInstance = null;
                var clearTemplateError = function () {
                    if (!templateError) { return; }
                    templateError.style.display = "none";
                    templateError.textContent = "";
                };
                var setTemplateError = function (message) {
                    if (!templateError) { return; }
                    templateError.style.display = "block";
                    templateError.textContent = String(message || "Template editor error.");
                };

                var cleanTokenLabel = function (value) {
                    var text = String(value || "");
                    text = text.replace(/{{\\s*|\\s*}}/g, "");
                    text = text.replace(/fenedgec_members_/gi, "");
                    return text.trim();
                };
                var tokenFormatOptions = [
                    { value: "", label: "Raw value" },
                    { value: "date(\'l j F Y\')", label: "Long Date" },
                    { value: "date(\'j M Y\')", label: "Short Date" },
                    { value: "date(\'Y-m-d\')", label: "ISO Date" }
                ];
                var twigTokenFor = function (tokenData, format) {
                    var name = String((tokenData && tokenData.token_name) || "").trim();
                    if (!name) {
                        return String((tokenData && tokenData.token) || "");
                    }
                    var fmt = String(format || "").trim();
                    return "{{ " + name + (fmt ? " | " + fmt : "") + " }}";
                };
                var stripHtml = function (html) {
                    var s = String(html || "");
                    s = s.replace(/<style[\\s\\S]*?<\\/style>/gi, " ");
                    s = s.replace(/<script[\\s\\S]*?<\\/script>/gi, " ");
                    s = s.replace(/<[^>]+>/g, " ");
                    s = s.replace(/\\s+/g, " ").trim();
                    return s;
                };
                var getTemplateHtmlValue = function () {
                    if (!templateJoditInstance) {
                        throw new Error("Template editor is not initialized.");
                    }
                    if (typeof templateJoditInstance.synchronizeValues !== "function") {
                        throw new Error("Template editor cannot synchronize values.");
                    }
                    templateJoditInstance.synchronizeValues();
                    if (typeof templateJoditInstance.value !== "undefined") {
                        return String(templateJoditInstance.value || "");
                    }
                    if (typeof templateJoditInstance.getEditorValue === "function") {
                        return String(templateJoditInstance.getEditorValue() || "");
                    }
                    throw new Error("Template editor value API is unavailable.");
                };
                var setTemplateHtmlValue = function (value) {
                    var htmlValue = String(value || "");
                    if (!templateJoditInstance) {
                        if (templateJodit) { templateJodit.value = htmlValue; }
                        return;
                    }
                    if (typeof templateJoditInstance.value !== "undefined") {
                        templateJoditInstance.value = htmlValue;
                    } else if (typeof templateJoditInstance.setEditorValue === "function") {
                        templateJoditInstance.setEditorValue(htmlValue);
                    }
                    if (templateJodit) { templateJodit.value = htmlValue; }
                    try {
                        if (typeof templateJoditInstance.synchronizeValues === "function") {
                            templateJoditInstance.synchronizeValues();
                        }
                    } catch (e) {
                    }
                };
                var scrubEditorArtifacts = function (value) {
                    var htmlValue = String(value || "");
                    htmlValue = htmlValue.replace(/<span\b[^>]*\bdata-mce-type=(["\'])bookmark\1[^>]*>[\s\S]*?<\/span>/gi, "");
                    htmlValue = htmlValue.replace(/<span\b[^>]*\bclass=(["\'])mce_SELRES_(?:start|end)\1[^>]*>[\s\S]*?<\/span>/gi, "");
                    htmlValue = htmlValue.replace(/\uFEFF/g, "");
                    return htmlValue;
                };
                var joditContainer = function () {
                    if (!templateJodit) { return null; }
                    var el = templateJodit.nextElementSibling;
                    if (el && String(el.className || "").indexOf("jodit-container") >= 0) {
                        return el;
                    }
                    return null;
                };
                var ensureJoditEditor = function () {
                    if (!templateJodit || templateJoditInstance) { return !!templateJoditInstance; }
                    if (!(window.Jodit && typeof window.Jodit.make === "function")) { return false; }
                    templateJoditInstance = window.Jodit.make("#ms-template-jodit", {
                        // Keep authored HTML/CSS isolated so template <style> does not leak into admin UI.
                        iframe: true,
                        defaultMode: window.Jodit.MODE_SOURCE,
                        height: "100%",
                        minHeight: 320,
                        allowResizeY: false,
                        useSplitMode: false,
                        sourceEditor: "ace",
                        toolbarAdaptive: false,
                        toolbarSticky: false,
                        buttons: ["bold", "italic", "ul", "ol", "|", "source"]
                    });
                    if (templateJoditInstance && templateJoditInstance.events && typeof templateJoditInstance.events.on === "function") {
                        templateJoditInstance.events.on("change", function () { templateDirty = true; });
                    }
                    return !!templateJoditInstance;
                };
                var showJodit = function () {
                    if (templateJodit) { templateJodit.style.display = "none"; }
                    var jc = joditContainer();
                    if (jc) {
                        jc.style.display = "block";
                        jc.style.height = "100%";
                    }
                };
                var tokenFieldOptions = function (tokens) {
                    var out = [];
                    (tokens || []).forEach(function (t) {
                        if (!t || typeof t !== "object") { return; }
                        var field = String(t.field || "").trim();
                        if (field && out.indexOf(field) === -1) { out.push(field); }
                    });
                    return out;
                };
                var repopulateFieldSelect = function (sel, values, emptyLabel) {
                    if (!sel) { return; }
                    var prev = sel.value || "";
                    sel.innerHTML = "";
                    var first = document.createElement("option");
                    first.value = "";
                    first.textContent = emptyLabel;
                    sel.appendChild(first);
                    values.forEach(function (v) {
                        var o = document.createElement("option");
                        o.value = v;
                        o.textContent = v;
                        sel.appendChild(o);
                    });
                    if (prev && values.indexOf(prev) !== -1) {
                        sel.value = prev;
                    } else {
                        sel.value = "";
                    }
                };
                var insertIntoField = function (field, text) {
                    if (!field || !text) { return; }
                    var start = field.selectionStart || 0;
                    var end = field.selectionEnd || 0;
                    var value = field.value || "";
                    field.value = value.substring(0, start) + text + value.substring(end);
                    field.focus();
                    var next = start + text.length;
                    field.setSelectionRange(next, next);
                    markDirty();
                };
                var insertToken = function (token) {
                    if (!token) { return; }
                    if (templateModal && templateModal.style.display === "block") {
                        if (templateJoditInstance) {
                            if (templateJoditInstance.s && typeof templateJoditInstance.s.insertHTML === "function") {
                                templateJoditInstance.s.insertHTML(token);
                            } else {
                                templateJoditInstance.value = String(templateJoditInstance.value || "") + token;
                            }
                        } else if (templateJodit) {
                            insertIntoField(templateJodit, token);
                        }
                        templateDirty = true;
                        return;
                    }
                    if (!lastTarget) {
                        lastTarget = subjectField;
                    }
                    insertIntoField(lastTarget, token);
                };
                var renderTokenControlsInto = function (container) {
                    if (!container) { return; }
                    container.innerHTML = "";
                    if (!currentTokens || currentTokens.length === 0) {
                        return;
                    }
                    var chooser = document.createElement("span");
                    chooser.className = "feca-token-format-controls";
                    chooser.style.display = "inline-block";
                    chooser.style.marginRight = "8px";
                    chooser.style.marginBottom = "6px";
                    var sel = document.createElement("select");
                    sel.innerHTML = "<option value=\\"\\">Select field token</option>";
                    currentTokens.forEach(function (t, idx) {
                        var label = cleanTokenLabel(String((t && (t.field || t.token)) || ""));
                        var o = document.createElement("option");
                        o.value = String(idx);
                        o.textContent = label || String((t && t.token) || "");
                        sel.appendChild(o);
                    });
                    var formatSel = document.createElement("select");
                    tokenFormatOptions.forEach(function (fmt) {
                        var o = document.createElement("option");
                        o.value = fmt.value;
                        o.textContent = fmt.label;
                        formatSel.appendChild(o);
                    });
                    formatSel.style.marginLeft = "6px";
                    var btn = document.createElement("button");
                    btn.type = "button";
                    btn.className = "button button-small";
                    btn.textContent = "Insert";
                    btn.style.marginLeft = "6px";
                    btn.addEventListener("click", function () {
                        var idx = parseInt(sel.value || "-1", 10);
                        if (idx < 0 || !currentTokens[idx]) { return; }
                        insertToken(twigTokenFor(currentTokens[idx], formatSel.value || ""));
                    });
                    chooser.appendChild(sel);
                    chooser.appendChild(formatSel);
                    chooser.appendChild(btn);
                    container.appendChild(chooser);
                    if (currentTokens.length > 10) {
                        return;
                    }
                    currentTokens.forEach(function (t) {
                        var token = String((t && t.token) || "");
                        var label = cleanTokenLabel(String((t && (t.field || t.token)) || ""));
                        var b = document.createElement("button");
                        b.type = "button";
                        b.className = "button button-small";
                        b.style.marginRight = "6px";
                        b.style.marginBottom = "6px";
                        b.textContent = label || token;
                        b.title = token;
                        b.addEventListener("click", function () { insertToken(token); });
                        container.appendChild(b);
                    });
                };
                var refreshTokenUi = function () {
                    renderTokenControlsInto(tokenControls);
                    renderTokenControlsInto(templateTokenControls);
                    var fields = tokenFieldOptions(currentTokens);
                    repopulateFieldSelect(pdfNameSelect, fields, "(none)");
                    repopulateFieldSelect(emailSelect, fields, "Select field");
                    if (tokenHelp) {
                        tokenHelp.textContent = fields.length ? "Insert tokens into Subject, Message or PDF Attachment editors." : "Select a data source to view tokens.";
                    }
                    updateRecipientWarning();
                };
                var updateRecipientWarning = function () {
                    if (!recipientWarning || !emailSelect) { return; }
                    recipientWarning.style.display = (String(emailSelect.value || "").trim() === "") ? "block" : "none";
                };
                var fetchTokensForDataSource = function (name) {
                    if (!name) {
                        currentTokens = [];
                        refreshTokenUi();
                        clearTemplateError();
                        return;
                    }
                    fetch(adminApiBase + "&op=tokens&data_source=" + encodeURIComponent(name), { credentials: "same-origin" })
                        .then(function (r) { return r.json(); })
                        .then(function (j) {
                            currentTokens = (j && j.tokens && Array.isArray(j.tokens)) ? j.tokens : [];
                            refreshTokenUi();
                            clearTemplateError();
                        })
                        .catch(function (e) {
                            if (tokenHelp) { tokenHelp.textContent = "Token lookup failed. Resolve this error before continuing."; }
                            setTemplateError("Token lookup failed for selected data source: " + (e && e.message ? e.message : "unknown error"));
                        });
                };
                var updateTemplateMeta = function () {
                    if (messageField && messageMeta) { messageMeta.textContent = (messageField.value || "").length + " chars"; }
                    if (pdfField && pdfMeta) { pdfMeta.textContent = (pdfField.value || "").length + " chars"; }
                    if (messageSnippet && messageField) {
                        var msg = stripHtml(messageField.value || "");
                        messageSnippet.textContent = msg ? msg.substring(0, 160) + (msg.length > 160 ? "..." : "") : "No message content yet.";
                    }
                    if (pdfSnippet && pdfField) {
                        var pdf = stripHtml(pdfField.value || "");
                        pdfSnippet.textContent = pdf ? pdf.substring(0, 160) + (pdf.length > 160 ? "..." : "") : "No PDF template content yet.";
                    }
                };
                var escapeTwigDoubleQuotedString = function (value) {
                    var s = String(value || "");
                    s = s.split(String.fromCharCode(92)).join(String.fromCharCode(92, 92));
                    s = s.split(String.fromCharCode(34)).join(String.fromCharCode(92, 34));
                    return s;
                };
                var loadPdfAssets = function () {
                    if (!pdfAssetPick) { return; }
                    pdfAssetPick.innerHTML = "<option value=\\"\\">Loading...</option>";
                    fetch(pdfApiBase + "&op=list", { credentials: "same-origin" })
                        .then(function (r) { return r.json(); })
                        .then(function (j) {
                            var items = (j && j.items && Array.isArray(j.items)) ? j.items : [];
                            pdfAssetPick.innerHTML = "<option value=\\"\\">Select PDF asset</option>";
                            items.forEach(function (row) {
                                var name = String((row && row.name) || "").trim();
                                if (!name) { return; }
                                var o = document.createElement("option");
                                o.value = name;
                                o.textContent = name;
                                pdfAssetPick.appendChild(o);
                            });
                            clearTemplateError();
                        })
                        .catch(function (e) {
                            pdfAssetPick.innerHTML = "<option value=\\"\\">Select PDF asset</option>";
                            setTemplateError("PDF asset lookup failed: " + (e && e.message ? e.message : "unknown error"));
                        });
                };
                var openTemplateEditor = function (field, title, isPdf) {
                    if (!field || !templateModal) { return; }
                    activeTemplateField = field;
                    templateTitle.textContent = title;
                    templateDirty = false;
                    clearTemplateError();
                    if (!ensureJoditEditor() || !templateJoditInstance) {
                        throw new Error("Jodit editor is required for template editing but failed to load.");
                    }
                    showJodit();
                    setTemplateHtmlValue(field.value || "");
                    templatePdfAssets.style.display = isPdf ? "block" : "none";
                    if (isPdf) { loadPdfAssets(); }
                    templateModal.style.display = "block";
                };
                var closeTemplateEditor = function () {
                    if (!templateModal) { return; }
                    if (templateDirty && !window.confirm("Discard changes in template editor?")) { return; }
                    templateModal.style.display = "none";
                };

                editor.querySelectorAll("input,select,textarea").forEach(function (el) {
                    el.addEventListener("input", markDirty);
                    el.addEventListener("change", markDirty);
                });
                editor.addEventListener("submit", function () { setCleanState(); });

                if (subjectField) {
                    subjectField.addEventListener("focus", function () { lastTarget = subjectField; });
                }
                if (dsSelect) {
                    dsSelect.addEventListener("change", function () {
                        fetchTokensForDataSource(dsSelect.value || "");
                        markDirty();
                    });
                }
                if (emailSelect) {
                    emailSelect.addEventListener("change", function () {
                        updateRecipientWarning();
                        markDirty();
                    });
                }

                if (openMessageEditor && messageField) {
                    openMessageEditor.addEventListener("click", function () {
                        try {
                            openTemplateEditor(messageField, "Edit Message", false);
                        } catch (e) {
                            setTemplateError(e && e.message ? e.message : "Unable to open message editor.");
                        }
                    });
                }
                if (openPdfEditor && pdfField) {
                    openPdfEditor.addEventListener("click", function () {
                        try {
                            openTemplateEditor(pdfField, "Edit PDF Attachment", true);
                        } catch (e) {
                            setTemplateError(e && e.message ? e.message : "Unable to open PDF attachment editor.");
                        }
                    });
                }
                if (templateClose) { templateClose.addEventListener("click", closeTemplateEditor); }
                if (templateSave) {
                    var saveTemplateNow = function () {
                        if (!activeTemplateField) { return; }
                        var rawValue = getTemplateHtmlValue();
                        activeTemplateField.value = scrubEditorArtifacts(rawValue);
                        templateDirty = false;
                        clearTemplateError();
                        templateModal.style.display = "none";
                        updateTemplateMeta();
                        markDirty();
                    };
                    var saveTemplate = function (ev) {
                        if (ev && typeof ev.preventDefault === "function") { ev.preventDefault(); }
                        try {
                            if (templateJoditInstance && typeof templateJoditInstance.synchronizeValues === "function") {
                                templateJoditInstance.synchronizeValues();
                            }
                        } catch (e) {
                        }
                        try {
                            saveTemplateNow();
                        } catch (e) {
                            setTemplateError(e && e.message ? e.message : "Template save failed.");
                        }
                    };
                    templateSave.addEventListener("click", saveTemplate);
                }
                if (pdfAssetInsert && pdfAssetPick) {
                    pdfAssetInsert.addEventListener("click", function () {
                        var raw = pdfAssetPick.value || "";
                        if (!raw) { return; }
                        var token = "{{ pdf_asset(\\"" + escapeTwigDoubleQuotedString(raw) + "\\") }}";
                        insertToken(token);
                    });
                }

                if (openNew && editorModal) {
                    openNew.addEventListener("click", function () {
                        if (!confirmDiscard()) { return; }
                        var idInput = document.getElementById("ms-editor-id");
                        if (idInput) { idInput.value = ""; }
                        ["ms_purpose","ms_ds","ms_subject","ms_message","ms_pdfa","ms_cc","ms_bcc","ms_reply","ms_pdfname","ms_email_field"].forEach(function (id) {
                            var el = document.getElementById(id);
                            if (el) { el.value = ""; }
                        });
                        var list = document.getElementById("ms_attachment_list");
                        if (list) { list.innerHTML = ""; }
                        var csv = document.getElementById("ms_attachment_csv");
                        if (csv) { csv.value = ""; }
                        currentTokens = [];
                        refreshTokenUi();
                        updateRecipientWarning();
                        updateTemplateMeta();
                        setCleanState();
                        editorModal.style.display = "block";
                    });
                }
                if (closeEditor && editorModal) {
                    closeEditor.addEventListener("click", function () {
                        if (!confirmDiscard()) { return; }
                        editorModal.style.display = "none";
                    });
                }

                var list = document.getElementById("ms_attachment_list");
                var pick = document.getElementById("ms_attachment_pick");
                var addBtn = document.getElementById("ms_attachment_add");
                var hidden = document.getElementById("ms_attachment_csv");
                if (list && pick && addBtn && hidden) {
                    var sync = function () {
                        var names = [];
                        list.querySelectorAll("li").forEach(function (li) {
                            var n = li.getAttribute("data-name") || "";
                            if (n && names.indexOf(n) === -1) { names.push(n); }
                        });
                        hidden.value = names.join(", ");
                    };
                    list.addEventListener("click", function (ev) {
                        if (ev.target && ev.target.classList.contains("ms-att-remove")) {
                            var li = ev.target.closest("li");
                            if (li) { li.remove(); sync(); markDirty(); }
                        }
                    });
                    addBtn.addEventListener("click", function () {
                        var value = pick.value || "";
                        if (!value) { return; }
                        var exists = false;
                        list.querySelectorAll("li").forEach(function (li) {
                            if ((li.getAttribute("data-name") || "") === value) { exists = true; }
                        });
                        if (exists) { return; }
                        var li = document.createElement("li");
                        li.setAttribute("data-name", value);
                        li.appendChild(document.createTextNode(value + " "));
                        var rm = document.createElement("button");
                        rm.type = "button";
                        rm.className = "button-link-delete ms-att-remove";
                        rm.textContent = "Remove";
                        li.appendChild(rm);
                        list.appendChild(li);
                        sync();
                        markDirty();
                    });
                    sync();
                }

                refreshTokenUi();
                updateRecipientWarning();
                updateTemplateMeta();
                setCleanState();

                var hasEdit = ' . ($editId > 0 ? 'true' : 'false') . ';
                var hasDraft = ' . ($useDraft ? 'true' : 'false') . ';
                var hasResult = ' . ($result !== null ? 'true' : 'false') . ';
                var resultOk = ' . (!empty($result['ok']) ? 'true' : 'false') . ';
                if ((hasEdit || hasDraft) && editorModal && (!hasResult || !resultOk)) {
                    editorModal.style.display = "block";
                    setCleanState();
                }
            })();
        </script>';

        echo '</div>';
    }

    public function handleUiSave(): void
    {
        if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
            return;
        }
        $idRaw = trim((string) ($this->wp->requestParam('id', '') ?? ''));
        $id = $idRaw === '' ? null : (int) $idRaw;
        $attachmentCsv = (string) ($this->wp->requestParam('AttachmentNamesCsv', '') ?? '');
        $attachmentNames = array_values(array_filter(array_map('trim', explode(',', $attachmentCsv)), static fn(string $v): bool => $v !== ''));

        $formDraft = [
            'Purpose' => (string) ($this->wp->requestParam('Purpose', '') ?? ''),
            'DataSource' => (string) ($this->wp->requestParam('DataSource', '') ?? ''),
            'CC' => (string) ($this->wp->requestParam('CC', '') ?? ''),
            'BCC' => (string) ($this->wp->requestParam('BCC', '') ?? ''),
            'Subject' => (string) ($this->wp->requestParam('Subject', '') ?? ''),
            'Message' => (string) ($this->wp->requestParam('Message', '') ?? ''),
            'PDFAttachment' => (string) ($this->wp->requestParam('PDFAttachment', '') ?? ''),
            'AttachmentNamesCsv' => $attachmentCsv,
            'PDFFilenameDerivedFrom' => (string) ($this->wp->requestParam('PDFFilenameDerivedFrom', '') ?? ''),
            'RecipientEmailField' => (string) ($this->wp->requestParam('RecipientEmailField', '') ?? ''),
            'ReplyTo' => (string) ($this->wp->requestParam('ReplyTo', '') ?? ''),
        ];
        $payload = [
            'Purpose' => $formDraft['Purpose'],
            'DataSource' => $formDraft['DataSource'],
            'CC' => $formDraft['CC'],
            'BCC' => $formDraft['BCC'],
            'Subject' => $formDraft['Subject'],
            'Message' => $formDraft['Message'],
            'PDFAttachment' => $formDraft['PDFAttachment'],
            'AttachmentNames' => json_encode($attachmentNames, JSON_UNESCAPED_SLASHES),
            'PDFFilenameDerivedFrom' => $formDraft['PDFFilenameDerivedFrom'],
            'RecipientEmailField' => $formDraft['RecipientEmailField'],
            'ReplyTo' => $formDraft['ReplyTo'],
        ];
        $result = $this->service()->save($id, $payload);
        $result['context'] = 'mailshot_save';
        $this->persistResultAndDraft(
            self::RESULT_OPTION_KEY,
            self::DRAFT_OPTION_KEY,
            $result,
            ['id' => $id, 'form' => $formDraft]
        );
        $editId = $id;
        if (!empty($result['ok']) && isset($result['id'])) {
            $editId = (int) $result['id'];
        }
        $url = $this->mailshotsPageUrl($editId !== null ? (int) $editId : 0);
        if (!headers_sent()) {
            header('Location: ' . $url, true, 302);
            exit;
        }
    }

    public function handleUiDelete(): void
    {
        if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
            return;
        }
        $id = (int) ($this->wp->requestParam('id', '0') ?? '0');
        $result = ['ok' => true];
        try {
            if ($id > 0) {
                $this->service()->delete($id);
            }
        } catch (\Throwable $e) {
            $result = ['ok' => false, 'errors' => [$e->getMessage()], 'context' => 'mailshot_delete'];
        }
        $result['context'] = 'mailshot_delete';
        $this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
        if (!headers_sent()) {
            header('Location: ' . $this->mailshotsPageUrl(), true, 302);
            exit;
        }
    }

    public function handleUiDuplicate(): void
    {
        if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
            return;
        }
        $id = (int) ($this->wp->requestParam('id', '0') ?? '0');
        $result = ['ok' => false, 'errors' => ['Mailshot ID is required.'], 'context' => 'mailshot_duplicate'];
        try {
            if ($id > 0) {
                $result = $this->service()->duplicate($id);
                $result['context'] = 'mailshot_duplicate';
            }
        } catch (\Throwable $e) {
            $result = ['ok' => false, 'errors' => [$e->getMessage()], 'context' => 'mailshot_duplicate'];
        }
        $this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
        $editId = !empty($result['ok']) && isset($result['id']) ? (int) $result['id'] : 0;
        $url = $this->mailshotsPageUrl($editId);
        if (!headers_sent()) {
            header('Location: ' . $url, true, 302);
            exit;
        }
    }

    public function handleApi(): void
    {
        if (!$this->wp->currentUserCan(self::CAPABILITY)) {
            $this->wp->sendJson(['ok' => false, 'error' => 'Permission denied'], 403);
            return;
        }

        $op = (string) ($this->wp->requestParam('op', 'list') ?? 'list');

        try {
            if ($op === 'list') {
                $this->wp->sendJson(['ok' => true, 'items' => $this->service()->list()]);
                return;
            }

            if ($op === 'data_sources') {
                $this->wp->sendJson(['ok' => true, 'items' => $this->service()->dataSourceNames()]);
                return;
            }

            if ($op === 'attachment_names') {
                $this->wp->sendJson(['ok' => true, 'items' => $this->service()->attachmentNames()]);
                return;
            }

            if ($op === 'tokens') {
                $dataSource = (string) ($this->wp->requestParam('data_source', '') ?? '');
                $this->wp->sendJson(['ok' => true] + $this->service()->tokenInsertionData($dataSource));
                return;
            }

            if ($op === 'last_run') {
                $mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
                $this->wp->sendJson(['ok' => true, 'items' => $this->service()->lastRun($mailshotId)]);
                return;
            }

            if ($op === 'clear_last_run') {
                if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
                    return;
                }
                $mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
                $this->service()->clearLastRun($mailshotId);
                $this->wp->sendJson(['ok' => true]);
                return;
            }

            if ($op === 'save') {
                if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
                    return;
                }
                $idRaw = $this->wp->requestParam('id', '');
                $id = $idRaw === '' ? null : (int) $idRaw;
                $payload = [
                    'Purpose' => (string) ($this->wp->requestParam('Purpose', '') ?? ''),
                    'DataSource' => (string) ($this->wp->requestParam('DataSource', '') ?? ''),
                    'CC' => (string) ($this->wp->requestParam('CC', '') ?? ''),
                    'BCC' => (string) ($this->wp->requestParam('BCC', '') ?? ''),
                    'Subject' => (string) ($this->wp->requestParam('Subject', '') ?? ''),
                    'Message' => (string) ($this->wp->requestParam('Message', '') ?? ''),
                    'PDFAttachment' => (string) ($this->wp->requestParam('PDFAttachment', '') ?? ''),
                    'AttachmentNames' => (string) ($this->wp->requestParam('AttachmentNames', '[]') ?? '[]'),
                    'PDFFilenameDerivedFrom' => (string) ($this->wp->requestParam('PDFFilenameDerivedFrom', '') ?? ''),
                    'RecipientEmailField' => (string) ($this->wp->requestParam('RecipientEmailField', '') ?? ''),
                    'ReplyTo' => (string) ($this->wp->requestParam('ReplyTo', '') ?? ''),
                ];
                $this->wp->sendJson($this->service()->save($id, $payload));
                return;
            }

            if ($op === 'delete') {
                if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
                    return;
                }
                $id = (int) ($this->wp->requestParam('id', '0') ?? '0');
                $this->service()->delete($id);
                $this->wp->sendJson(['ok' => true]);
                return;
            }

            if ($op === 'duplicate') {
                if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
                    return;
                }
                $id = (int) ($this->wp->requestParam('id', '0') ?? '0');
                $this->wp->sendJson($this->service()->duplicate($id));
                return;
            }

            $this->wp->sendJson(['ok' => false, 'error' => 'Unknown operation'], 400);
        } catch (\Throwable $e) {
            $this->wp->sendJson(['ok' => false, 'error' => $e->getMessage()], 500);
        }
    }

    private function service(): MailshotService
    {
        return ($this->serviceFactory)();
    }

    /** @param list<array<string,mixed>> $items @return list<array<string,mixed>> */
    private function sortedMailshotItems(array $items, string $sortBy, string $sortDirection): array
    {
        $fieldMap = [
            'purpose' => 'Purpose',
            'data_source' => 'DataSource',
            'subject' => 'Subject',
        ];
        $field = $fieldMap[$sortBy] ?? 'Purpose';
        $direction = $sortDirection === 'desc' ? -1 : 1;
        usort($items, static function (array $a, array $b) use ($field, $direction): int {
            $left = trim((string) ($a[$field] ?? ''));
            $right = trim((string) ($b[$field] ?? ''));
            $cmp = strnatcasecmp($left, $right);
            if ($cmp === 0) {
                $cmp = ((int) ($a['id'] ?? 0)) <=> ((int) ($b['id'] ?? 0));
            }
            return $cmp * $direction;
        });
        return $items;
    }

    private function mailshotsPageUrl(int $editId = 0): string
    {
        $sortBy = (string) ($this->wp->requestParam('sort_by', 'purpose') ?? 'purpose');
        if (!in_array($sortBy, ['purpose', 'data_source', 'subject'], true)) {
            $sortBy = 'purpose';
        }
        $sortDirection = strtolower((string) ($this->wp->requestParam('sort_direction', 'asc') ?? 'asc'));
        if (!in_array($sortDirection, ['asc', 'desc'], true)) {
            $sortDirection = 'asc';
        }

        $url = 'admin.php?page=feca-mailshots-mailshots&sort_by=' . rawurlencode($sortBy) . '&sort_direction=' . rawurlencode($sortDirection);
        if ($editId > 0) {
            $url .= '&edit_id=' . $editId;
        }
        return $this->wp->adminUrl($url);
    }

    private function cleanTokenLabel(string $value): string
    {
        $label = trim($value);
        if ($label === '') {
            return $label;
        }
        $label = str_replace(['{{', '}}'], '', $label);
        $label = preg_replace('/\bfenedgec_members_/i', '', $label) ?? $label;
        return trim($label);
    }

}
