<?php

declare(strict_types=1);

namespace FecaMailshots\Admin;

use FecaMailshots\WordPress\WordPressFacade;

final class DataSourcesAdminPage
{
    use AdminRequestHelpers;

    private const CAPABILITY = 'edit_pages';
    private const RESULT_OPTION_KEY = 'feca_mailshots_data_sources_ui_result';
    private const DRAFT_OPTION_KEY = 'feca_mailshots_data_sources_ui_draft';
    private const NONCE_ACTION = 'feca_mailshots_data_sources';

    /** @var callable(): \FecaMailshots\Application\DataSourceService */
    private $serviceFactory;
    private WordPressFacade $wp;

    /** @param callable(): \FecaMailshots\Application\DataSourceService $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_menu', [$this, 'hideDuplicateRootSubmenu'], 999);
        $this->wp->addAction('admin_post_feca_mailshots_data_sources_api', [$this, 'handleApi']);
        $this->wp->addAction('rest_api_init', [$this, 'registerRestRoutes']);
        $this->wp->addAction('admin_post_feca_mailshots_data_sources_ui_save', [$this, 'handleUiSave']);
        $this->wp->addAction('admin_post_feca_mailshots_data_sources_ui_delete', [$this, 'handleUiDelete']);
        $this->wp->addAction('admin_post_feca_mailshots_data_sources_ui_duplicate', [$this, 'handleUiDuplicate']);
        $this->wp->addAction('admin_post_feca_mailshots_data_sources_ui_validate', [$this, 'handleUiValidate']);
        $this->wp->addAction('admin_post_feca_mailshots_data_sources_ui_preview', [$this, 'handleUiPreview']);
    }

    public function registerMenu(): void
    {
        $this->wp->addMenuPage('FECA Mailshots', 'FECA Mailshots', self::CAPABILITY, 'feca-mailshot', [$this, 'renderRoot']);
        $this->wp->addSubmenuPage('feca-mailshot', 'Mailshot Data Sources', 'Data Sources', self::CAPABILITY, 'feca-mailshot-data-sources', [$this, 'render']);
    }

    public function hideDuplicateRootSubmenu(): void
    {
        if (function_exists('remove_submenu_page')) {
            remove_submenu_page('feca-mailshot', 'feca-mailshot');
        }
    }

    public function renderRoot(): void
    {
        $target = $this->wp->adminUrl('admin.php?page=feca-mailshots-mailshots');
        if (!headers_sent()) {
            header('Location: ' . $target, true, 302);
            exit;
        }
        echo '<div class="wrap"><h1>FECA Mailshots</h1><p><a href="' . htmlspecialchars($target) . '">Open Mailshots</a></p></div>';
    }

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

        $action = htmlspecialchars($this->wp->adminUrl('admin-post.php'));
        $filter = trim((string) ($this->wp->requestParam('q', '') ?? ''));
        $sort = (string) ($this->wp->requestParam('sort', 'name') ?? 'name');
        if (!in_array($sort, ['name', 'updated_desc', 'updated_asc'], true)) {
            $sort = 'name';
        }

        $items = $this->filteredAndSortedItems($this->service()->list(), $filter, $sort);
        $draft = $this->consumeOptionArray(self::DRAFT_OPTION_KEY);
        $requestedEditId = (int) ($this->wp->requestParam('edit_id', '0') ?? '0');
        $draftId = is_array($draft) && isset($draft['id']) ? (int) $draft['id'] : 0;
        $useDraft = is_array($draft) && (
            $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;
            }
        }
        $name = is_array($editItem) ? (string) ($editItem['name'] ?? '') : '';
        $dsl = is_array($editItem) ? (string) ($editItem['dsl_text'] ?? '') : '';
        if ($useDraft && is_array($draft)) {
            $name = (string) ($draft['name'] ?? $name);
            $dsl = (string) ($draft['dsl_text'] ?? $dsl);
        }
        $result = $this->consumeOptionArray(self::RESULT_OPTION_KEY);
        $sourceFieldsMap = $this->service()->sourceFields();
        $schemaList = $this->service()->listSchemas();

        echo '<div class="wrap feca-mailshots-admin">';
        echo $this->renderAdminUiStyles();
        echo '<h1>Mailshot Data Sources</h1>';

        if ($result !== null) {
            $ok = !empty($result['ok']);
            $title = $ok ? 'Action succeeded.' : 'Action failed.';
            $bannerClass = $ok ? 'feca-banner-success' : 'feca-banner-error';
            echo '<div id="ds-result-banner" class="feca-banner ' . $bannerClass . '">';
            echo '<strong>' . htmlspecialchars($title) . '</strong>';
            if (!empty($result['errors']) && is_array($result['errors'])) {
                echo '<p class="feca-banner-note">' . htmlspecialchars(implode('; ', $result['errors'])) . '</p>';
            }
            if (!empty($result['warnings']) && is_array($result['warnings'])) {
                echo '<p class="feca-banner-note">Warnings: ' . htmlspecialchars(implode('; ', $result['warnings'])) . '</p>';
            }
            if (isset($result['count'])) {
                echo '<p class="feca-banner-note">Preview count: ' . (int) $result['count'] . '</p>';
            }
            echo '</div>';
            echo '<script>(function(){var n=document.getElementById("ds-result-banner");if(!n){return;}window.setTimeout(function(){if(n&&n.parentNode){n.parentNode.removeChild(n);}},30000);})();</script>';
        }

        echo '<div class="feca-panel"><strong>Total data sources:</strong> ' . count($items) . '</div>';

        echo '<form method="get" action="' . htmlspecialchars($this->wp->adminUrl('admin.php')) . '" class="feca-form">';
        echo '<input type="hidden" name="page" value="feca-mailshot-data-sources">';
        echo '<div class="feca-control-row">';
        echo '<div class="feca-control feca-control-min-360">';
        echo '<label for="ds_filter_q"><strong>Filter</strong></label>';
        echo '<input id="ds_filter_q" class="regular-text" type="text" name="q" value="' . htmlspecialchars($filter, ENT_QUOTES) . '" placeholder="Name or sentence">';
        echo '</div>';
        echo '<div class="feca-control feca-control-min-260">';
        echo '<label for="ds_filter_sort"><strong>Sort</strong></label>';
        echo '<select id="ds_filter_sort" name="sort">';
        foreach (['name' => 'Name', 'updated_desc' => 'Updated (newest)', 'updated_asc' => 'Updated (oldest)'] as $value => $label) {
            $selected = $sort === $value ? ' selected' : '';
            echo '<option value="' . htmlspecialchars($value, ENT_QUOTES) . '"' . $selected . '>' . htmlspecialchars($label) . '</option>';
        }
        echo '</select>';
        echo '</div>';
        echo '<div class="feca-control"><button class="button" type="submit">Apply</button></div>';
        echo '</div>';
        echo '</form>';

        echo '<p class="feca-button-row"><button type="button" class="button button-primary" id="ds-open-new">New Data Source</button></p>';
        echo '<div id="ds-editor-modal" class="feca-modal-overlay">';
        echo '<div class="feca-modal-shell">';
        echo '<form method="post" action="' . $action . '" class="feca-form" id="ds-editor-form">';
        echo '<h2 class="feca-modal-title">' . ($editId > 0 ? 'Edit Data Source' : 'New Data Source') . '</h2>';
        echo $this->modalErrorHtml($result);
        echo $this->hiddenNonceField(self::NONCE_ACTION);
        if ($editId > 0) {
            echo '<input type="hidden" name="id" value="' . $editId . '" id="ds-editor-id">';
        } else {
            echo '<input type="hidden" name="id" value="" id="ds-editor-id">';
        }
        echo '<table class="form-table" role="presentation">';
        echo '<tr><th scope="row"><label for="ds_name">Name</label></th><td><input class="regular-text" type="text" id="ds_name" name="name" value="' . htmlspecialchars($name, ENT_QUOTES) . '" data-initial="' . htmlspecialchars($name, ENT_QUOTES) . '"></td></tr>';
        echo '<tr><th scope="row"><label for="ds_dsl">DSL Sentence</label></th><td><textarea id="ds_dsl" name="dsl_text" rows="7" class="large-text code" data-initial="' . htmlspecialchars($dsl, ENT_QUOTES) . '">' . htmlspecialchars($dsl) . '</textarea></td></tr>';
        echo '</table>';
        echo '<p class="feca-button-row">';
        echo '<button type="submit" class="button button-primary" name="action" value="feca_mailshots_data_sources_ui_save">Save</button> ';
        echo '<button type="button" class="button" id="ds-close-editor">Quit</button> ';
        echo '<button type="button" class="button" id="ds-validate-btn">Validate Sentence</button> ';
        echo '<button type="button" class="button" id="ds-preview-btn">Preview Recipients</button> ';
        echo '<button type="button" class="button" id="ds-build-dsl-open">Build DSL</button> ';
        echo '</p>';
        echo '<div id="ds-inline-result" class="feca-banner feca-banner-muted feca-hidden"></div>';
        echo '<div id="ds-inline-preview" class="feca-banner feca-hidden"></div>';
        echo '</form>';
        echo '</div></div>';

        echo '<div id="ds-builder-modal" class="feca-modal-overlay-high">';
        echo '<div class="feca-builder-shell">';
        echo '<h2 class="feca-modal-title">Build DSL</h2>';
        echo '<p class="feca-banner-note">Construct a DSL sentence from sources and constraints.</p>';
        echo '<div class="feca-builder-grid">';
        echo '<div class="feca-builder-col-left">';
        echo '<h3 class="feca-modal-title">Data Sources</h3>';
        echo '<label><input type="checkbox" class="ds-source-built" value="contacts"> contacts</label><br>';
        echo '<label><input type="checkbox" class="ds-source-built" value="accounts"> accounts</label><br>';
        echo '<label><input type="checkbox" class="ds-source-built" value="renewals"> renewals</label><br>';
        echo '<label><input type="checkbox" class="ds-source-built" value="grants"> grants</label><br>';
        echo '<label><input type="checkbox" class="ds-source-built" value="advertisers"> advertisers</label><br>';
        echo '<label><input type="checkbox" class="ds-source-built" value="ads"> ads</label><br>';
        echo '<label><input type="checkbox" class="ds-source-built" value="pages"> pages</label><br>';
        echo '<label><input type="checkbox" class="ds-source-built" value="articles"> articles</label><br>';
        echo '<label><input type="checkbox" class="ds-source-built" value="issues"> issues</label><br>';
        echo '<label><input type="checkbox" class="ds-source-built" value="invoices"> invoices</label><br><br>';
        echo '<strong>Add custom source</strong><br>';
        echo '<label>Schema <select id="ds-builder-schema"><option value="">Select schema</option>';
        foreach ($schemaList as $schema) {
            echo '<option value="' . htmlspecialchars((string) $schema, ENT_QUOTES) . '">' . htmlspecialchars((string) $schema) . '</option>';
        }
        echo '</select></label> ';
        echo '<label>Table <select id="ds-builder-table"><option value="">Select table</option></select></label> ';
        echo '<button type="button" class="button button-small" id="ds-builder-add-custom">Add</button>';
        echo '<ul id="ds-builder-custom-list"></ul>';
        echo '</div>';
        echo '<div class="feca-builder-col-right">';
        echo '<h3 class="feca-modal-title">Constraints / Filters</h3>';
        echo '<div id="ds-builder-rows"></div>';
        echo '<p class="feca-button-row"><button type="button" class="button button-small" id="ds-builder-add-row">Add Constraint Row</button></p>';
        echo '</div></div>';
        echo '<div class="feca-builder-output">';
        echo '<strong>Generated DSL</strong>';
        echo '<textarea id="ds-builder-output" rows="4" class="large-text code" readonly></textarea>';
        echo '<p id="ds-builder-error" class="feca-error-text"></p>';
        echo '<p id="ds-builder-table-error" class="feca-error-text"></p>';
        echo '</div>';
        echo '<p class="feca-button-row">';
        echo '<button type="button" class="button button-primary" id="ds-builder-apply">Save</button> ';
        echo '<button type="button" class="button" id="ds-builder-cancel">Quit</button>';
        echo '</p></div></div>';

        if (is_array($result) && isset($result['rows']) && is_array($result['rows'])) {
            $rows = $result['rows'];
            echo '<div class="feca-panel">';
            echo '<h2 class="feca-modal-title">Preview (first ' . count($rows) . ' rows)</h2>';
            if ($rows === []) {
                echo '<p>No rows returned.</p>';
            } else {
                $columns = [];
                foreach ($rows as $r) {
                    if (!is_array($r)) {
                        continue;
                    }
                    foreach (array_keys($r) as $k) {
                        $columns[(string) $k] = true;
                    }
                }
                $columns = array_keys($columns);
                echo '<table class="widefat striped"><thead><tr>';
                foreach ($columns as $col) {
                    echo '<th>' . htmlspecialchars($col) . '</th>';
                }
                echo '</tr></thead><tbody>';
                foreach ($rows as $r) {
                    if (!is_array($r)) {
                        continue;
                    }
                    echo '<tr>';
                    foreach ($columns as $col) {
                        echo '<td>' . htmlspecialchars((string) ($r[$col] ?? '')) . '</td>';
                    }
                    echo '</tr>';
                }
                echo '</tbody></table>';
            }
            echo '</div>';
        }

        echo '<h2>Existing Data Sources</h2>';
        echo '<div class="feca-scroll-frame"><div class="feca-scroll-pane">';
        echo '<table class="widefat striped"><thead><tr><th>ID</th><th>Name</th><th>DSL Sentence</th><th>Updated At</th><th>Actions</th></tr></thead><tbody>';
        foreach ($items as $row) {
            $id = (int) ($row['ID'] ?? 0);
            $editUrl = $this->wp->adminUrl('admin.php?page=feca-mailshot-data-sources&edit_id=' . $id . '&q=' . rawurlencode($filter) . '&sort=' . rawurlencode($sort));
            echo '<tr>';
            echo '<td>' . $id . '</td>';
            echo '<td>' . htmlspecialchars((string) ($row['name'] ?? '')) . '</td>';
            echo '<td class="feca-truncate-420"><code>' . htmlspecialchars((string) ($row['dsl_text'] ?? '')) . '</code></td>';
            echo '<td>' . htmlspecialchars((string) ($row['updated_at'] ?? '')) . '</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_data_sources_ui_duplicate">';
            echo $this->hiddenNonceField(self::NONCE_ACTION);
            echo '<input type="hidden" name="id" value="' . $id . '">';
            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_data_sources_ui_delete">';
            echo $this->hiddenNonceField(self::NONCE_ACTION);
            echo '<input type="hidden" name="id" value="' . $id . '">';
            echo '<button type="submit" class="button button-small feca-button-danger" onclick="return confirm(\'Delete this data source?\');">Delete</button>';
            echo '</form></td>';
            echo '</tr>';
        }
        if ($items === []) {
            echo '<tr><td colspan="5">No data sources found.</td></tr>';
        }
        echo '</tbody></table>';
        echo '</div></div>';
        echo '<script>';
        echo 'window.fecaDataSourcesBuilderConfig = ' . json_encode([
            'sourceFields' => $sourceFieldsMap,
            'restBase' => '/wp-json/mailshots/v1/data-sources',
            'adminPostApi' => $this->wp->adminUrl('admin-post.php?action=feca_mailshots_data_sources_api'),
        ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ';';
        echo '(function(){';
        echo 'var cfg=window.fecaDataSourcesBuilderConfig||{};';
        echo 'var dslInput=document.getElementById("ds_dsl");';
        echo 'var modal=document.getElementById("ds-builder-modal");';
        echo 'var openBtn=document.getElementById("ds-build-dsl-open");';
        echo 'var cancelBtn=document.getElementById("ds-builder-cancel");';
        echo 'var applyBtn=document.getElementById("ds-builder-apply");';
        echo 'var addRowBtn=document.getElementById("ds-builder-add-row");';
        echo 'var rowsWrap=document.getElementById("ds-builder-rows");';
        echo 'var out=document.getElementById("ds-builder-output");';
        echo 'var err=document.getElementById("ds-builder-error");';
        echo 'var tableErr=document.getElementById("ds-builder-table-error");';
        echo 'var editorModal=document.getElementById("ds-editor-modal");';
        echo 'var editorForm=document.getElementById("ds-editor-form");';
        echo 'var validateBtn=document.getElementById("ds-validate-btn");';
        echo 'var previewBtn=document.getElementById("ds-preview-btn");';
        echo 'var inlineResult=document.getElementById("ds-inline-result");';
        echo 'var inlinePreview=document.getElementById("ds-inline-preview");';
        echo 'var openNew=document.getElementById("ds-open-new");';
        echo 'var closeEditor=document.getElementById("ds-close-editor");';
        echo 'var editorId=document.getElementById("ds-editor-id");';
        echo 'var editorName=document.getElementById("ds_name");';
        echo 'var nonceField=editorForm?editorForm.querySelector("input[name=\"_wpnonce\"]"):null;';
        echo 'var builtChecks=[].slice.call(document.querySelectorAll(".ds-source-built"));';
        echo 'var customList=document.getElementById("ds-builder-custom-list");';
        echo 'var schemaSel=document.getElementById("ds-builder-schema");';
        echo 'var tableSel=document.getElementById("ds-builder-table");';
        echo 'var addCustomBtn=document.getElementById("ds-builder-add-custom");';
        echo 'var customSources=[];';
        echo 'var sourceOrder=[];';
        echo 'var constraints=[];';
        echo 'var isDirty=false;';
        echo 'function confirmDiscard(){if(!isDirty){return true;}return window.confirm("You have unsaved changes. Close without saving?");}';
        echo 'if(editorForm){editorForm.querySelectorAll("input,select,textarea").forEach(function(el){el.addEventListener("input",function(){isDirty=true;});el.addEventListener("change",function(){isDirty=true;});});editorForm.addEventListener("submit",function(){isDirty=false;});}';
        echo 'var filters=[["selected-renewal","Renewal is selected","none"],["pending-renewal","Renewal is pending","none"],["primary-contact","Contact is primary","none"],["fen1-contact","Contact is FEN1","none"],["member-or-affiliate-or-parish-council","Account is member/affiliate/parish council","none"],["account-has-article-in-issue","Account has article in issue","issue"],["selected","Advertiser is selected","none"],["issue","Issue is","issue"],["pending-invoice","Invoice is pending","none"],["selected-invoice","Invoice is selected","none"],["invoice-ids","Invoice ID is one of","ids"]];';
        echo 'function selectedSources(){var selected={};builtChecks.forEach(function(c){if(c.checked){selected[c.value]=true;}});customSources.forEach(function(v){selected[v]=true;});var s=[];sourceOrder.forEach(function(src){if(selected[src]&&s.indexOf(src)===-1){s.push(src);}});builtChecks.forEach(function(c){if(c.checked&&s.indexOf(c.value)===-1){s.push(c.value);}});customSources.forEach(function(v){if(s.indexOf(v)===-1){s.push(v);}});return s;}';
        echo 'function noteSourceSelected(src){if(src&&sourceOrder.indexOf(src)===-1){sourceOrder.push(src);}}';
        echo 'function noteSourceDeselected(src){sourceOrder=sourceOrder.filter(function(v){return v!==src;});}';
        echo 'function updateCustomList(){customList.innerHTML="";customSources.forEach(function(src,i){var li=document.createElement("li");li.textContent=src+" ";var b=document.createElement("button");b.type="button";b.className="button-link-delete";b.textContent="Remove";b.onclick=function(){noteSourceDeselected(src);customSources.splice(i,1);updateCustomList();renderRows();updateDsl();};li.appendChild(b);customList.appendChild(li);});}';
        echo 'function fetchTables(schema){tableSel.innerHTML="<option value=\"\">Loading...</option>";tableSel.style.color="#1d2327";if(tableErr){tableErr.textContent="";}var pickLabel=function(t){if(typeof t==="string"){return t.trim();}if(t===null||t===undefined){return "";}if(typeof t==="number"){return String(t);}if(typeof t==="object"){var direct=[t.table_name,t.name,t.table,t.label];for(var i=0;i<direct.length;i++){var dv=String(direct[i]||"").trim();if(dv){return dv;}}var keys=Object.keys(t||{});for(var k=0;k<keys.length;k++){var key=String(keys[k]||"").trim();if(/^Tables_in_/i.test(key)){var vv=String(t[key]||"").trim();if(vv){return vv;}}}var vals=Object.values(t||{});for(var j=0;j<vals.length;j++){var v=String(vals[j]||"").trim();if(v){return v;}}}return "";};var fill=function(items){tableSel.innerHTML="<option value=\"\">Select table</option>";var invalid=0;(items||[]).forEach(function(t){var label=pickLabel(t);if(!label){invalid++;return;}var o=document.createElement("option");o.value=label;o.textContent=label;tableSel.appendChild(o);});if((items||[]).length===0&&tableErr){tableErr.textContent="No tables returned for this schema. Check DB grants: SELECT and SHOW VIEW on schema tables.";}if(invalid>0&&tableErr){tableErr.textContent="Received "+invalid+" table entries without names; showing only valid table names.";}};var showErr=function(msg){tableSel.innerHTML="<option value=\"\">Select table</option>";if(tableErr){tableErr.textContent=msg||"Table lookup failed. Resolve the error before continuing.";}if(err){err.textContent="Table lookup failed: "+(msg||"unknown error");}};var u=(cfg.adminPostApi||"/wp-admin/admin-post.php?action=feca_mailshots_data_sources_api")+"&op=tables&schema="+encodeURIComponent(schema);fetch(u,{credentials:"same-origin"}).then(function(r){if(!r.ok){throw new Error("API "+r.status);}return r.json();}).then(function(j){if(!j||j.ok===false){throw new Error((j&&j.error)||"API error");}fill((j&&j.items)||[]);}).catch(function(e){showErr(e&&e.message?e.message:"Need MySQL grants on the selected schema.");});}';
        echo 'function sourceFields(source){var map=cfg.sourceFields||{};if(map[source]){return map[source];}if(source&&source.indexOf(".")!==-1){var u=(cfg.adminPostApi||"/wp-admin/admin-post.php?action=feca_mailshots_data_sources_api")+"&op=source_fields&source="+encodeURIComponent(source);fetch(u,{credentials:"same-origin"}).then(function(r){if(!r.ok){throw new Error("API "+r.status);}return r.json();}).then(function(j){if(!j||j.ok===false){throw new Error((j&&j.error)||"API error");}var fields=(j&&j.fields)||[];if(fields&&fields.length){map[source]=fields;cfg.sourceFields=map;renderRows();updateDsl();return;}if(err){err.textContent="Source field lookup returned no fields for "+source+".";}}).catch(function(e){if(err){err.textContent="Source field lookup failed for "+source+": "+(e&&e.message?e.message:"unknown error");}});}return [];}';
        echo 'function mkSelect(options,value){var s=document.createElement("select");options.forEach(function(opt){var o=document.createElement("option");o.value=opt[0];o.textContent=opt[1];if(opt[0]===value){o.selected=true;}s.appendChild(o);});return s;}';
        echo 'function filterArgType(name){for(var i=0;i<filters.length;i++){if(filters[i][0]===name){return filters[i][2]||"none";}}return "none";}';
        echo 'function addConstraint(){constraints.push({kind:"filter",negate:false,filter:"selected-renewal",filterArg:"",lhsSource:"",lhsField:"",op:"=",rhsMode:"literal",rhsLiteral:"",rhsSource:"",rhsField:""});renderRows();updateDsl();}';
        echo 'function renderRows(){rowsWrap.innerHTML="";constraints.forEach(function(row,idx){var box=document.createElement("div");box.className="feca-builder-row";var top=document.createElement("div");var not=document.createElement("input");not.type="checkbox";not.checked=!!row.negate;not.onchange=function(){row.negate=not.checked;updateDsl();};top.appendChild(not);top.appendChild(document.createTextNode(" NOT "));var kind=mkSelect([["filter","Predefined Filter"],["compare","Field Comparison"]],row.kind);kind.onchange=function(){row.kind=kind.value;renderRows();updateDsl();};top.appendChild(kind);var rem=document.createElement("button");rem.type="button";rem.className="button-link-delete";rem.textContent="Remove";rem.onclick=function(){constraints.splice(idx,1);renderRows();updateDsl();};top.appendChild(rem);box.appendChild(top);if(row.kind==="filter"){var f=mkSelect(filters,row.filter);f.className="feca-builder-filter-select";f.onchange=function(){row.filter=f.value;renderRows();updateDsl();};box.appendChild(f);var argType=filterArgType(row.filter);if(argType!=="none"){var arg=document.createElement("input");arg.type="text";arg.value=row.filterArg||"";arg.placeholder=argType==="ids"?"IDs, comma separated":"Issue ID";arg.oninput=function(){row.filterArg=arg.value;updateDsl();};box.appendChild(arg);}}else{var srcs=selectedSources().map(function(s){return [s,s];});if(srcs.length===0){srcs=[["","Select source"]];}else{srcs.unshift(["","Select source"]);}var lhsS=mkSelect(srcs,row.lhsSource);lhsS.onchange=function(){row.lhsSource=lhsS.value;row.lhsField="";renderRows();updateDsl();};box.appendChild(lhsS);var lhsFields=(row.lhsSource?sourceFields(row.lhsSource):[]).map(function(f){return [f,f]});lhsFields.unshift(["","Field"]);var lhsF=mkSelect(lhsFields,row.lhsField);lhsF.onchange=function(){row.lhsField=lhsF.value;updateDsl();};box.appendChild(lhsF);var op=mkSelect([["=","="],["!=","!="],["contains","contains"],["starts-with","starts-with"],["ends-with","ends-with"],["in","in"],["blank","is blank"]],row.op);op.onchange=function(){row.op=op.value;renderRows();updateDsl();};box.appendChild(op);if(row.op==="blank"){rowsWrap.appendChild(box);return;}var mode=mkSelect([["literal","Literal"],["field","Field ref"]],row.rhsMode);mode.onchange=function(){row.rhsMode=mode.value;renderRows();updateDsl();};box.appendChild(mode);if(row.rhsMode==="literal"){var input=document.createElement("input");input.type="text";input.value=row.rhsLiteral||"";input.placeholder="Value";input.oninput=function(){row.rhsLiteral=input.value;updateDsl();};box.appendChild(input);}else{var rhsS=mkSelect(srcs,row.rhsSource);rhsS.onchange=function(){row.rhsSource=rhsS.value;row.rhsField="";renderRows();updateDsl();};box.appendChild(rhsS);var rhsFields=(row.rhsSource?sourceFields(row.rhsSource):[]).map(function(f){return [f,f]});rhsFields.unshift(["","Field"]);var rhsF=mkSelect(rhsFields,row.rhsField);rhsF.onchange=function(){row.rhsField=rhsF.value;updateDsl();};box.appendChild(rhsF);}}rowsWrap.appendChild(box);});}';
        echo 'function quote(v){if(v==="true"||v==="false"||/^\\d+$/.test(v)){return v;}return "\'"+String(v).replace(/\'/g,"")+"\'";}';
        echo 'function isBareIdent(v){var s=String(v||"");if(!s){return false;}var first=s.charAt(0);if(!((first>="A"&&first<="Z")||(first>="a"&&first<="z")||first==="_")){return false;}for(var i=1;i<s.length;i++){var ch=s.charAt(i);if(!((ch>="A"&&ch<="Z")||(ch>="a"&&ch<="z")||(ch>="0"&&ch<="9")||ch==="_"||ch==="-")){return false;}}return true;}';
        echo 'function ident(v){var s=String(v||"");return isBareIdent(s)?s:("`"+s.split("`").join("``")+"`");}';
        echo 'function unquoteIdent(v){var s=String(v||"");if(s.length>=2&&s.charAt(0)==="`"&&s.charAt(s.length-1)==="`"){return s.substring(1,s.length-1).split("``").join("`");}return s;}';
        echo 'function fieldRef(source,field){return ident(source)+"."+ident(field);}';
        echo 'function updateDsl(){err.textContent="";var srcs=selectedSources();if(srcs.length===0){out.value="";err.textContent="Select at least one data source.";return;}var base=srcs.join(" and ");var terms=[];for(var i=0;i<constraints.length;i++){var r=constraints[i];var t="";if(r.kind==="filter"){if(!r.filter){continue;}var argType=filterArgType(r.filter);if(argType==="none"){t=r.filter;}else{var raw=String(r.filterArg||"").trim();if(!raw){continue;}if(argType==="ids"){var ids=raw.split(",").map(function(v){return v.trim();}).filter(function(v){return /^\\d+$/.test(v);});if(ids.length===0){continue;}t=r.filter+"("+ids.join(", ")+")";}else{if(!/^\\d+$/.test(raw)){continue;}t=r.filter+"("+raw+")";}}}else{if(!r.lhsSource||!r.lhsField){continue;}var lhs=fieldRef(r.lhsSource,r.lhsField);if(r.op==="blank"){t=lhs+" blank";}else if(r.rhsMode==="field"){if(!r.rhsSource||!r.rhsField){continue;}var rhs=fieldRef(r.rhsSource,r.rhsField);if(r.op==="in"){t=lhs+" in ("+rhs+")";}else{t=lhs+" "+r.op+" "+rhs;}}else{if((r.rhsLiteral||"")===""){continue;}if(r.op==="in"){t=lhs+" in ("+quote(r.rhsLiteral)+")";}else{t=lhs+" "+r.op+" "+quote(r.rhsLiteral);}}}if(r.negate&&t!==""){t="not ("+t+")";}if(t!==""){terms.push(t);}}out.value=base+(terms.length>0?" where "+terms.join(" and "):"");}';
        echo 'function resetBuilder(){builtChecks.forEach(function(c){c.checked=false;});customSources=[];sourceOrder=[];constraints=[];updateCustomList();renderRows();updateDsl();}';
        echo 'function splitAndTerms(text){var out=[];var cur="";var depth=0;var inQuote=false;for(var i=0;i<text.length;i++){var ch=text.charAt(i);if(ch==="\'"){inQuote=!inQuote;cur+=ch;continue;}if(!inQuote){if(ch==="("){depth++;cur+=ch;continue;}if(ch===")"){if(depth>0){depth--;}cur+=ch;continue;}if(depth===0&&text.substring(i,i+5)===" and "){if(cur.trim()){out.push(cur.trim());}cur="";i+=4;continue;}}cur+=ch;}if(cur.trim()){out.push(cur.trim());}return out;}';
        echo 'function splitFieldRef(text){var s=String(text||"").trim();var idx=s.lastIndexOf(".");if(idx<=0||idx>=s.length-1){return null;}return {source:unquoteIdent(s.substring(0,idx)),field:unquoteIdent(s.substring(idx+1))};}';
        echo 'function parseLiteralValue(text){var v=String(text||"").trim();if(!v){return null;}if(v.length>=2&&v.charAt(0)==="\'"&&v.charAt(v.length-1)==="\'"){return v.substring(1,v.length-1);}if(v==="true"||v==="false"||/^\\d+$/.test(v)){return v;}return null;}';
        echo 'function parseCompareTerm(term){var rawTerm=String(term||"").trim();var bm=/^(.+?)\\s+blank$/i.exec(rawTerm);if(bm){var blankRef=splitFieldRef(bm[1]);if(!blankRef){return null;}return {kind:"compare",negate:false,filter:"selected-renewal",lhsSource:blankRef.source,lhsField:blankRef.field,op:"blank",rhsMode:"literal",rhsLiteral:"",rhsSource:"",rhsField:""};}var m=/^(.+?)\\s+(=|!=|contains|starts-with|ends-with|in)\\s+(.+)$/.exec(rawTerm);if(!m){return null;}var lhsRef=splitFieldRef(m[1]);if(!lhsRef){return null;}var op=String(m[2]||"");var rhsRaw=String(m[3]||"").trim();if(!rhsRaw){return null;}var row={kind:"compare",negate:false,filter:"selected-renewal",lhsSource:lhsRef.source,lhsField:lhsRef.field,op:op,rhsMode:"literal",rhsLiteral:"",rhsSource:"",rhsField:""};if(op==="in"){if(rhsRaw.charAt(0)!=="("||rhsRaw.charAt(rhsRaw.length-1)!==")"){return null;}rhsRaw=rhsRaw.substring(1,rhsRaw.length-1).trim();if(!rhsRaw){return null;}}var rhsRef=splitFieldRef(rhsRaw);if(rhsRef){row.rhsMode="field";row.rhsSource=rhsRef.source;row.rhsField=rhsRef.field;return row;}var literal=parseLiteralValue(rhsRaw);if(literal===null){return null;}row.rhsLiteral=literal;return row;}';
        echo 'function prefillFromDsl(){var text=(dslInput&&dslInput.value?dslInput.value:"").trim();if(!text){return;}var lower=text.toLowerCase();var whereIndex=lower.indexOf(" where ");var srcPart=whereIndex>=0?text.substring(0,whereIndex):text;var wherePart=whereIndex>=0?text.substring(whereIndex+7).trim():"";var bits=srcPart.split(" and ");sourceOrder=[];bits.map(function(v){return v.trim();}).forEach(function(src){if(!src){return;}noteSourceSelected(src);var built=false;builtChecks.forEach(function(c){if(c.value===src){c.checked=true;built=true;}});if(!built&&customSources.indexOf(src)===-1){customSources.push(src);}});constraints=[];if(wherePart){var known={};filters.forEach(function(f){known[f[0]]=true;});splitAndTerms(wherePart).forEach(function(term){var t=(term||"").trim();if(!t){return;}var negate=false;var lt=t.toLowerCase();if(lt.indexOf("not (")===0&&t.charAt(t.length-1)===")"){negate=true;t=t.substring(5,t.length-1).trim();}else if(lt.indexOf("not ")===0){negate=true;t=t.substring(4).trim();}if(known[t]){constraints.push({kind:"filter",negate:negate,filter:t,filterArg:"",lhsSource:"",lhsField:"",op:"=",rhsMode:"literal",rhsLiteral:"",rhsSource:"",rhsField:""});return;}var fm=/^([a-z0-9_-]+)\\(([^)]*)\\)$/i.exec(t);if(fm&&known[fm[1]]){constraints.push({kind:"filter",negate:negate,filter:fm[1],filterArg:fm[2],lhsSource:"",lhsField:"",op:"=",rhsMode:"literal",rhsLiteral:"",rhsSource:"",rhsField:""});return;}var parsed=parseCompareTerm(t);if(parsed){parsed.negate=negate;constraints.push(parsed);}});}updateCustomList();renderRows();updateDsl();}';
        echo 'if(schemaSel){schemaSel.onchange=function(){if(schemaSel.value){fetchTables(schemaSel.value);}else{tableSel.innerHTML="<option value=\"\">Select table</option>";}};if(schemaSel.value){fetchTables(schemaSel.value);}}';
        echo 'if(addCustomBtn){addCustomBtn.onclick=function(){if(!schemaSel.value||!tableSel.value){return;}var src=schemaSel.value+"."+tableSel.value;if(customSources.indexOf(src)===-1){customSources.push(src);}noteSourceSelected(src);updateCustomList();renderRows();updateDsl();};}';
        echo 'builtChecks.forEach(function(c){c.onchange=function(){if(c.checked){noteSourceSelected(c.value);}else{noteSourceDeselected(c.value);}renderRows();updateDsl();};});';
        echo 'if(addRowBtn){addRowBtn.onclick=addConstraint;}';
        echo 'if(openBtn){openBtn.onclick=function(){resetBuilder();prefillFromDsl();modal.style.display="block";};}';
        echo 'if(cancelBtn){cancelBtn.onclick=function(){modal.style.display="none";};}';
        echo 'if(applyBtn){applyBtn.onclick=function(){if(dslInput){dslInput.value=out.value;}modal.style.display="none";};}';
        echo 'if(openNew){openNew.onclick=function(){if(!confirmDiscard()){return;}if(editorId){editorId.value="";}if(editorName){editorName.value="";}if(dslInput){dslInput.value="";}isDirty=false;editorModal.style.display="block";};}';
        echo 'if(closeEditor){closeEditor.onclick=function(){if(!confirmDiscard()){return;}editorModal.style.display="none";};}';
        echo 'function esc(v){return String(v||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");}';
        echo 'function showInlineResult(ok,title,lines){if(!inlineResult){return;}inlineResult.style.display="block";inlineResult.style.borderColor=ok?"#8bc34a":"#ef9a9a";inlineResult.style.background=ok?"#f1f8e9":"#ffebee";var h="<strong>"+esc(title)+"</strong>";if(lines&&lines.length){h+="<ul style=\"margin:8px 0 0 18px;\">"+lines.map(function(x){return "<li>"+esc(x)+"</li>";}).join("")+"</ul>";}inlineResult.innerHTML=h;}';
        echo 'if(validateBtn){validateBtn.onclick=function(){var dsl=(dslInput&&dslInput.value?dslInput.value:"");var payload=new URLSearchParams();payload.set("dsl_text",dsl);if(nonceField&&nonceField.value){payload.set("_wpnonce",String(nonceField.value));}fetch((cfg.adminPostApi||"/wp-admin/admin-post.php?action=feca_mailshots_data_sources_api")+"&op=validate",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:payload.toString()}).then(function(r){return r.json();}).then(function(j){if(!j||j.ok===false){showInlineResult(false,"Validation failed",[j&&j.error?j.error:"Unknown validation error"]);return;}var errors=(j.errors||[]);var warnings=(j.warnings||[]);if(errors.length===0){showInlineResult(true,"Validation passed",warnings.length?warnings:["No errors"]);}else{showInlineResult(false,"Validation failed",errors.concat(warnings));}}).catch(function(e){showInlineResult(false,"Validation failed",[e&&e.message?e.message:"Request error"]);});};}';
        echo 'if(previewBtn){previewBtn.onclick=function(){var dsl=(dslInput&&dslInput.value?dslInput.value:"");var payload=new URLSearchParams();payload.set("dsl_text",dsl);payload.set("limit","50");if(nonceField&&nonceField.value){payload.set("_wpnonce",String(nonceField.value));}fetch((cfg.adminPostApi||"/wp-admin/admin-post.php?action=feca_mailshots_data_sources_api")+"&op=preview",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:payload.toString()}).then(function(r){return r.json();}).then(function(j){if(!j||j.ok===false){showInlineResult(false,"Preview failed",[j&&j.error?j.error:"Unknown preview error"]);if(inlinePreview){inlinePreview.style.display="none";}return;}var errors=(j.errors||[]);var warnings=(j.warnings||[]);if(errors.length){showInlineResult(false,"Preview failed",errors.concat(warnings));if(inlinePreview){inlinePreview.style.display="none";}return;}showInlineResult(true,"Preview ready",["Count: "+(j.count||0)].concat(warnings));if(!inlinePreview){return;}var rows=(j.rows||[]);if(!rows.length){inlinePreview.style.display="block";inlinePreview.innerHTML="<p style=\"margin:0;\">No rows returned.</p>";return;}var cols=[];var seen={};rows.forEach(function(r){Object.keys(r||{}).forEach(function(k){if(!seen[k]){seen[k]=1;cols.push(k);}});});var html="<table class=\"widefat striped\"><thead><tr>"+cols.map(function(c){return "<th>"+esc(c)+"</th>";}).join("")+"</tr></thead><tbody>";rows.forEach(function(r){html+="<tr>"+cols.map(function(c){return "<td>"+esc((r&&r[c]!==undefined)?r[c]:"")+"</td>";}).join("")+"</tr>";});html+="</tbody></table>";inlinePreview.style.display="block";inlinePreview.innerHTML=html;}).catch(function(e){showInlineResult(false,"Preview failed",[e&&e.message?e.message:"Request error"]);if(inlinePreview){inlinePreview.style.display="none";}});};}';
        echo 'var hasEdit=' . ($editId > 0 ? 'true' : 'false') . ';';
        echo 'var hasDraft=' . ($useDraft ? 'true' : 'false') . ';';
        echo 'var hasResult=' . ($result !== null ? 'true' : 'false') . ';';
        echo 'var resultOk=' . (!empty($result['ok']) ? 'true' : 'false') . ';';
        echo 'if((hasEdit||hasDraft)&&editorModal&&(!hasResult||!resultOk)){editorModal.style.display="block";isDirty=false;}';
        echo '})();';
        echo '</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;
        $name = (string) ($this->wp->requestParam('name', '') ?? '');
        $dsl = (string) ($this->wp->requestParam('dsl_text', '') ?? '');
        $result = $this->service()->save(
            $id,
            $name,
            $dsl
        );
        $this->persistResultAndDraft(
            self::RESULT_OPTION_KEY,
            self::DRAFT_OPTION_KEY,
            $result,
            ['id' => $id, 'name' => $name, 'dsl_text' => $dsl]
        );
        $this->redirectToEditId($result, $id);
    }

    public function handleUiValidate(): void
    {
        if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
            return;
        }
        $dsl = (string) ($this->wp->requestParam('dsl_text', '') ?? '');
        $name = (string) ($this->wp->requestParam('name', '') ?? '');
        $validation = $this->service()->validateDsl($dsl);
        $result = [
            'ok' => ($validation['errors'] ?? []) === [],
            'errors' => $validation['errors'] ?? [],
            'warnings' => $validation['warnings'] ?? [],
        ];
        $this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
        $idRaw = trim((string) ($this->wp->requestParam('id', '') ?? ''));
        $id = $idRaw === '' ? null : (int) $idRaw;
        $this->wp->updateOption(self::DRAFT_OPTION_KEY, ['id' => $id, 'name' => $name, 'dsl_text' => $dsl]);
        $this->redirectToEditId($result, $id);
    }

    public function handleUiPreview(): void
    {
        if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
            return;
        }
        $dsl = (string) ($this->wp->requestParam('dsl_text', '') ?? '');
        $name = (string) ($this->wp->requestParam('name', '') ?? '');
        $preview = $this->service()->preview($dsl, 50);
        $result = [
            'ok' => ($preview['errors'] ?? []) === [],
            'errors' => $preview['errors'] ?? [],
            'warnings' => $preview['warnings'] ?? [],
            'count' => (int) ($preview['count'] ?? 0),
            'rows' => is_array($preview['rows'] ?? null) ? $preview['rows'] : [],
        ];
        $this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
        $idRaw = trim((string) ($this->wp->requestParam('id', '') ?? ''));
        $id = $idRaw === '' ? null : (int) $idRaw;
        $this->wp->updateOption(self::DRAFT_OPTION_KEY, ['id' => $id, 'name' => $name, 'dsl_text' => $dsl]);
        $this->redirectToEditId($result, $id);
    }

    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()]];
        }
        $this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
        if (!headers_sent()) {
            header('Location: ' . $this->wp->adminUrl('admin.php?page=feca-mailshot-data-sources'), 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' => ['Data source ID is required.']];
        try {
            if ($id > 0) {
                $result = $this->service()->duplicate($id);
            }
        } catch (\Throwable $e) {
            $result = ['ok' => false, 'errors' => [$e->getMessage()]];
        }
        $this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
        $editId = !empty($result['ok']) && isset($result['id']) ? (int) $result['id'] : 0;
        $url = $this->wp->adminUrl('admin.php?page=feca-mailshot-data-sources' . ($editId > 0 ? '&edit_id=' . $editId : ''));
        if (!headers_sent()) {
            header('Location: ' . $url, true, 302);
            exit;
        }
    }

    public function handleApi(): void
    {
        if (!$this->enforceCapabilityOrJson(self::CAPABILITY)) {
            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 === 'get') {
                $id = (int) ($this->wp->requestParam('id', '0') ?? '0');
                $item = $id > 0 ? $this->service()->get($id) : null;
                if ($item === null) {
                    $this->wp->sendJson(['ok' => false, 'error' => 'Data source not found'], 404);
                    return;
                }
                $this->wp->sendJson(['ok' => true, 'item' => $item]);
                return;
            }
            if ($op === 'fields') {
                $this->wp->sendJson(['ok' => true, 'fields' => $this->service()->sourceFields()]);
                return;
            }
            if ($op === 'schemas') {
                $this->wp->sendJson(['ok' => true, 'items' => $this->service()->listSchemas()]);
                return;
            }
            if ($op === 'tables') {
                $schema = (string) ($this->wp->requestParam('schema', '') ?? '');
                $this->wp->sendJson(['ok' => true, 'items' => $this->service()->listTables($schema)]);
                return;
            }
            if ($op === 'source_fields') {
                $source = (string) ($this->wp->requestParam('source', '') ?? '');
                $this->wp->sendJson(['ok' => true, 'source' => $source, 'fields' => $this->service()->sourceFieldsForSource($source)]);
                return;
            }
            if ($op === 'validate') {
                $dsl = (string) ($this->wp->requestParam('dsl_text', '') ?? '');
                $this->wp->sendJson(['ok' => true] + $this->service()->validateDsl($dsl));
                return;
            }
            if ($op === 'preview') {
                $dsl = (string) ($this->wp->requestParam('dsl_text', '') ?? '');
                $limit = (int) ($this->wp->requestParam('limit', '50') ?? '50');
                $this->wp->sendJson(['ok' => true] + $this->service()->preview($dsl, $limit));
                return;
            }
            if ($op === 'save') {
                if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
                    return;
                }
                $idRaw = $this->wp->requestParam('id', '');
                $id = $idRaw === '' ? null : (int) $idRaw;
                $name = (string) ($this->wp->requestParam('name', '') ?? '');
                $dsl = (string) ($this->wp->requestParam('dsl_text', '') ?? '');
                $this->wp->sendJson($this->service()->save($id, $name, $dsl));
                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);
        }
    }

    public function registerRestRoutes(): void
    {
        if (!function_exists('register_rest_route')) {
            return;
        }

        register_rest_route('mailshots/v1', '/data-sources', [
            'methods' => 'GET',
            'callback' => [$this, 'restList'],
            'permission_callback' => [$this, 'restCanRead'],
        ]);
        register_rest_route('mailshots/v1', '/data-sources', [
            'methods' => 'POST',
            'callback' => [$this, 'restCreate'],
            'permission_callback' => [$this, 'restCanManage'],
        ]);
        register_rest_route('mailshots/v1', '/data-sources/(?P<id>\d+)', [
            'methods' => 'GET',
            'callback' => [$this, 'restGetOne'],
            'permission_callback' => [$this, 'restCanRead'],
        ]);
        register_rest_route('mailshots/v1', '/data-sources/(?P<id>\d+)', [
            'methods' => 'PUT',
            'callback' => [$this, 'restUpdate'],
            'permission_callback' => [$this, 'restCanManage'],
        ]);
        register_rest_route('mailshots/v1', '/data-sources/(?P<id>\d+)', [
            'methods' => 'DELETE',
            'callback' => [$this, 'restDelete'],
            'permission_callback' => [$this, 'restCanManage'],
        ]);
        register_rest_route('mailshots/v1', '/data-sources/validate', [
            'methods' => 'POST',
            'callback' => [$this, 'restValidate'],
            'permission_callback' => [$this, 'restCanRead'],
        ]);
        register_rest_route('mailshots/v1', '/data-sources/preview', [
            'methods' => 'POST',
            'callback' => [$this, 'restPreview'],
            'permission_callback' => [$this, 'restCanRead'],
        ]);
        register_rest_route('mailshots/v1', '/data-sources/(?P<id>\d+)/preview', [
            'methods' => 'POST',
            'callback' => [$this, 'restPreviewById'],
            'permission_callback' => [$this, 'restCanRead'],
        ]);
        register_rest_route('mailshots/v1', '/data-sources/schemas', [
            'methods' => 'GET',
            'callback' => [$this, 'restSchemas'],
            'permission_callback' => [$this, 'restCanRead'],
        ]);
        register_rest_route('mailshots/v1', '/data-sources/tables', [
            'methods' => 'GET',
            'callback' => [$this, 'restTables'],
            'permission_callback' => [$this, 'restCanRead'],
        ]);
        register_rest_route('mailshots/v1', '/data-sources/source-fields', [
            'methods' => 'GET',
            'callback' => [$this, 'restSourceFields'],
            'permission_callback' => [$this, 'restCanRead'],
        ]);
    }

    private function service(): \FecaMailshots\Application\DataSourceService
    {
        return ($this->serviceFactory)();
    }

    public function restCanRead(): bool
    {
        return $this->wp->currentUserCan(self::CAPABILITY);
    }

    public function restCanManage(): bool
    {
        return $this->wp->currentUserCan(self::CAPABILITY);
    }

    /** @return array<string,mixed> */
    public function restList()
    {
        return ['ok' => true, 'items' => $this->service()->list()];
    }

    /** @return array<string,mixed> */
    public function restGetOne($request)
    {
        $id = (int) $this->reqParam($request, 'id', 0);
        $item = $id > 0 ? $this->service()->get($id) : null;
        if ($item === null) {
            return ['ok' => false, 'error' => 'Data source not found'];
        }
        return ['ok' => true, 'item' => $item];
    }

    /** @return array<string,mixed> */
    public function restCreate($request)
    {
        $name = (string) $this->reqParam($request, 'name', '');
        $dsl = (string) $this->reqParam($request, 'dsl_text', '');
        return $this->service()->save(null, $name, $dsl);
    }

    /** @return array<string,mixed> */
    public function restUpdate($request)
    {
        $id = (int) $this->reqParam($request, 'id', 0);
        $name = (string) $this->reqParam($request, 'name', '');
        $dsl = (string) $this->reqParam($request, 'dsl_text', '');
        return $this->service()->save($id > 0 ? $id : null, $name, $dsl);
    }

    /** @return array<string,mixed> */
    public function restDelete($request)
    {
        $id = (int) $this->reqParam($request, 'id', 0);
        try {
            if ($id > 0) {
                $this->service()->delete($id);
            }
            return ['ok' => true];
        } catch (\Throwable $e) {
            return ['ok' => false, 'error' => $e->getMessage()];
        }
    }

    /** @return array<string,mixed> */
    public function restValidate($request)
    {
        $dsl = (string) $this->reqParam($request, 'dsl_text', '');
        return ['ok' => true] + $this->service()->validateDsl($dsl);
    }

    /** @return array<string,mixed> */
    public function restPreview($request)
    {
        $dsl = (string) $this->reqParam($request, 'dsl_text', '');
        $limit = (int) $this->reqParam($request, 'limit', 50);
        return ['ok' => true] + $this->service()->preview($dsl, $limit);
    }

    /** @return array<string,mixed> */
    public function restPreviewById($request)
    {
        $id = (int) $this->reqParam($request, 'id', 0);
        $item = $id > 0 ? $this->service()->get($id) : null;
        if (!is_array($item)) {
            return ['ok' => false, 'error' => 'Data source not found'];
        }
        $dsl = (string) ($item['dsl_text'] ?? '');
        $limit = (int) $this->reqParam($request, 'limit', 50);
        return ['ok' => true] + $this->service()->preview($dsl, $limit);
    }

    /** @return array<string,mixed> */
    public function restSchemas()
    {
        return ['ok' => true, 'items' => $this->service()->listSchemas()];
    }

    /** @return array<string,mixed> */
    public function restTables($request)
    {
        $schema = (string) $this->reqParam($request, 'schema', '');
        return ['ok' => true, 'items' => $this->service()->listTables($schema)];
    }

    /** @return array<string,mixed> */
    public function restSourceFields($request)
    {
        $source = (string) $this->reqParam($request, 'source', '');
        if ($source === '') {
            return ['ok' => true, 'fields' => $this->service()->sourceFields()];
        }
        return ['ok' => true, 'source' => $source, 'fields' => $this->service()->sourceFieldsForSource($source)];
    }

    /** @param list<array<string,mixed>> $items @return list<array<string,mixed>> */
    private function filteredAndSortedItems(array $items, string $filter, string $sort): array
    {
        if ($filter !== '') {
            $needle = strtolower($filter);
            $items = array_values(array_filter($items, static function (array $row) use ($needle): bool {
                $name = strtolower((string) ($row['name'] ?? ''));
                $dsl = strtolower((string) ($row['dsl_text'] ?? ''));
                return strpos($name, $needle) !== false || strpos($dsl, $needle) !== false;
            }));
        }

        usort($items, static function (array $a, array $b) use ($sort): int {
            if ($sort === 'updated_desc' || $sort === 'updated_asc') {
                $av = (string) ($a['updated_at'] ?? '');
                $bv = (string) ($b['updated_at'] ?? '');
                $cmp = strcmp($av, $bv);
                return $sort === 'updated_desc' ? -$cmp : $cmp;
            }
            return strcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
        });

        return $items;
    }

    /** @param array<string,mixed> $result */
    private function redirectToEditId(array $result, ?int $currentEditId): void
    {
        $editId = $currentEditId;
        if (!empty($result['ok']) && isset($result['id'])) {
            $editId = (int) $result['id'];
        }
        $url = $this->wp->adminUrl('admin.php?page=feca-mailshot-data-sources' . ($editId !== null && $editId > 0 ? '&edit_id=' . $editId : ''));
        if (!headers_sent()) {
            header('Location: ' . $url, true, 302);
            exit;
        }
    }

    /**
     * @param mixed $request
     * @param mixed $default
     * @return mixed
     */
    private function reqParam($request, string $key, $default = null)
    {
        if (is_object($request) && method_exists($request, 'get_param')) {
            $value = $request->get_param($key);
            return $value === null ? $default : $value;
        }
        if (is_array($request) && array_key_exists($key, $request)) {
            return $request[$key];
        }
        return $default;
    }
}
