/**
 * NeccoSpeak - Admin settings helper.
 * Plain JS IIFE, no build step. Loaded only on the plugin settings page
 * (enqueued by neccospeak.php, admin_enqueue_scripts, hook_suffix gated).
 *
 * Reads window.neccoSpeakAdmin = {
 *   restUrl,           // esc_url_raw( rest_url( 'neccospeak/v1/' ) )
 *   nonce,             // wp_create_nonce( 'wp_rest' )
 *   homeUrl,           // esc_url_raw( home_url( '/' ) ) — optional; falls
 *                      // back to window.location.origin + '/' when absent
 *                      // (design rev.2 §10.1 C4 / Appendix A "297-313").
 *   i18n: { fetching, fetched, failed, noKey, setKeyFirst, fetchedSummary,
 *           diagChecking, diagOk, diagFail, diagForbidden, diagNotSet,
 *           presetConfirm, presetApplied, faqMax,
 *           mediaTitle, mediaButton, mediaSelect, mediaRemove, repeatMax,
 *           confirmDelete, confirmRollback, exporting }
 *         (esc_html__ done; see includes/settings.php neccospeak_admin_i18n())
 * }
 *
 * Sections in this file:
 *  1. Fetch-models flow (unchanged from earlier versions).
 *  2. G18 Prompt tab preview mode toggle (text/realtime radios).
 *  3. G4 Diagnostics tab client-side reachability checks (C1/C2/C2b/C4) —
 *     these must run from the browser (not PHP) to actually exercise
 *     anonymous / omit-credentials reachability, matching what a real
 *     visitor's request looks like.
 *  4. Confirm() guard for forms marked data-neccospeak-confirm (clear/re-wrap
 *     key actions in the Diagnostics tab).
 *  5. G11 Prompt tab: FAQ repeating rows (add/remove, monotonic index
 *     counter) and industry-preset apply (with an overwrite confirmation).
 *  6. Phase 4 (design phase4 §8.3/§8.4/§8.5/§4.3): a GENERIC repeater
 *     (initRepeaters(), used by the Actions tab's cta_list — the existing
 *     FAQ repeater above is NOT migrated onto this, design phase4 D2), a
 *     generic wp.media image picker (initMediaPickers()), wp-color-picker
 *     wiring (initColorPickers()), and small show/hide toggles for the
 *     header-gradient and custom-icon rows on the Appearance tab.
 *  7. Phase 5 (design phase5 §14): a generic "exporting…" status swap
 *     (initExportButtons()) for any button marked data-neccospeak-export —
 *     the Logs tab's CSV export (impl-viewers, includes/admin-logs.php) opts
 *     in via that attribute alone, no JS of its own required. The "delete
 *     all" / "roll back" confirmations those screens need reuse section 4's
 *     EXISTING data-neccospeak-confirm mechanism (unchanged here) with the
 *     new confirmDelete/confirmRollback i18n strings.
 *  8. 0.3.0-dev.42 撤去記録: この項にあった initStreamSelfTest()
 *     (Diagnostics タブの SSE セルフテスト G29, 行 #neccospeak-diag-i1) と
 *     checkDiagC1() (GET {restUrl}diag) は、両者を出していた旧診断タブ
 *     (includes/admin-diagnostics.php neccospeak_render_diagnostics_tab())
 *     が agent-ui の menu callback 差し替えで到達不能になったため、
 *     サーバ側のルート/ハンドラごと削除した。
 *     initRagSync() drives the Content tab's "Rebuild index" button (G26) —
 *     sequential admin-ajax.php POSTs (action=neccospeak_rag_sync,
 *     impl-rag), resumable via the cursor the server hands back.
 *     initToolToggles() dims (never disables — submission must still carry
 *     the individual tool checkboxes even while the master switch is off,
 *     design phase6 §5.8) the Content tab's per-tool checkboxes when "Enable
 *     tools" is unchecked. initCf7MapRows() shows/hides a cta_list row's
 *     CF7 field-mapping inputs (G38) based on that row's own type <select>,
 *     via event delegation so it also covers rows the generic repeater
 *     (section 6) clones after page load.
 *
 * Security: the API key never reaches the browser. /v1/models runs the
 * admin's stored key server-side only; here we receive model ids only.
 */
(function () {
  'use strict';

  var cfg = window.neccoSpeakAdmin;
  if (!cfg || !cfg.restUrl) {
    return;
  }

  // ---- i18n: server-localized strings with English fallbacks -------------
  var I18N = (cfg.i18n && typeof cfg.i18n === 'object') ? cfg.i18n : {};
  var I18N_FALLBACK = {
    fetching: 'Fetching available models…',
    fetched: 'Models loaded.',
    failed: 'Failed to fetch the model list.',
    noKey: 'No API key is set.',
    setKeyFirst: 'Please save your API key first.',
    fetchedSummary: '(chat %1$s / realtime %2$s)',
    diagChecking: 'Checking…',
    diagOk: 'OK',
    diagFail: 'Unreachable',
    diagForbidden: 'Blocked (403) — the same-origin check failed.',
    diagNotSet: 'Not set',
    presetConfirm: 'This will overwrite the prompt fields you have already filled in. Continue?',
    presetApplied: 'Preset applied. Review the fields, then click Save Changes.',
    faqMax: 'The maximum number of FAQ entries has been reached.',
    mediaTitle: 'Choose an image',
    mediaButton: 'Use this image',
    mediaSelect: 'Select image',
    mediaRemove: 'Remove image',
    repeatMax: 'The maximum number of actions has been reached.',
    confirmDelete: 'This will permanently delete all matching entries. This cannot be undone. Continue?',
    confirmRollback: 'Roll back to this version? Your current published settings will be replaced (except the API key).',
    exporting: 'Exporting…',
    sseChecking: 'Checking…',
    sseOk: 'Working',
    sseBuffered: 'Buffered by the server',
    sseUnsupported: 'Not available',
    ragSyncing: 'Indexing… %1$s / %2$s',
    ragSyncDone: 'Indexing finished: %1$s chunks from %2$s pages.',
    ragSyncFailed: 'Indexing failed. Please try again.',
    ragSyncConfirm: 'Rebuild the content index? This can take a while on large sites.'
  };

  function t(key) {
    var v = I18N[key];
    if (typeof v === 'string' && v) {
      return v;
    }
    return I18N_FALLBACK[key] != null ? I18N_FALLBACK[key] : '';
  }

  function ready(fn) {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', fn);
    } else {
      fn();
    }
  }

  // ---- DOM helpers --------------------------------------------------------
  function setStatus(text, isError) {
    var statusEl = document.getElementById('neccospeak-fetch-status');
    if (!statusEl) {
      return;
    }
    statusEl.textContent = text || ''; // textContent: XSS-safe
    if (isError) {
      statusEl.setAttribute('data-state', 'error');
    } else {
      statusEl.removeAttribute('data-state');
    }
  }

  // Append model ids into a <datalist>, skipping values already present
  // (de-duplicates against existing static fallback options).
  // Returns the number of NEW options added.
  function appendOptions(datalistId, ids) {
    var list = document.getElementById(datalistId);
    if (!list || !ids || !ids.length) {
      return 0;
    }

    // Collect existing option values to avoid duplicates.
    var existing = {};
    var opts = list.getElementsByTagName('option');
    var i;
    for (i = 0; i < opts.length; i++) {
      var ev = opts[i].value != null && opts[i].value !== ''
        ? opts[i].value
        : opts[i].textContent;
      if (typeof ev === 'string' && ev) {
        existing[ev] = true;
      }
    }

    var added = 0;
    for (i = 0; i < ids.length; i++) {
      var id = ids[i];
      if (typeof id !== 'string' || !id) {
        continue;
      }
      if (existing[id]) {
        continue;
      }
      existing[id] = true;
      var opt = document.createElement('option');
      opt.value = id; // attribute assignment: safe, no HTML parsing
      list.appendChild(opt);
      added++;
    }
    return added;
  }

  function toArray(v) {
    return Object.prototype.toString.call(v) === '[object Array]' ? v : [];
  }

  // ---- Fetch flow ---------------------------------------------------------
  function fetchModels(btn) {
    var url = cfg.restUrl + 'models';
    var headers = {};
    if (cfg.nonce) {
      headers['X-WP-Nonce'] = cfg.nonce;
    }

    if (btn) {
      btn.disabled = true;
    }
    setStatus(t('fetching'), false);

    fetch(url, {
      method: 'GET',
      headers: headers,
      credentials: 'same-origin'
    }).then(function (res) {
      return res.text().then(function (raw) {
        if (!res.ok) {
          // 400/409: API key not set on the server. 403: not permitted.
          var msg = t('failed');
          if (res.status === 400 || res.status === 409) {
            msg = t('noKey');
          }
          // Surface a server-provided message without trusting HTML:
          // PHP returns {error:"…"}; WP_Error responses use {message:"…"}.
          try {
            var ej = JSON.parse(raw);
            if (ej && typeof ej.error === 'string' && ej.error) {
              msg = ej.error;
            } else if (ej && typeof ej.message === 'string' && ej.message) {
              msg = ej.message;
            }
          } catch (e) {}
          throw new Error(msg);
        }
        var data;
        try {
          data = JSON.parse(raw);
        } catch (e) {
          throw new Error(t('failed'));
        }
        return data || {};
      });
    }).then(function (data) {
      var chat = toArray(data.chat);
      var realtime = toArray(data.realtime);
      var added = 0;
      added += appendOptions('neccospeak-model-list', chat);
      added += appendOptions('neccospeak-realtime-model-list', realtime);
      // Status: "<fetched> (chat N / realtime M)". The count summary is
      // localized via the fetchedSummary template (%1$s = chat, %2$s = realtime).
      var counts = t('fetchedSummary')
        .replace('%1$s', String(chat.length))
        .replace('%2$s', String(realtime.length));
      setStatus(t('fetched') + ' ' + counts, false);
    }).catch(function (err) {
      setStatus(err && err.message ? err.message : t('failed'), true);
    }).then(function () {
      if (btn) {
        btn.disabled = false;
      }
    });
  }

  // ---- G18: Prompt tab preview mode toggle --------------------------------
  // Both <div class="neccospeak-preview-panel" data-mode="text|realtime">
  // panels are always in the DOM (server-rendered degrade for JS-disabled
  // browsers, design rev.2 §11); this only toggles which one is visible.
  function initPromptPreview() {
    var radios = document.querySelectorAll('input[name="neccospeak-preview-mode"]');
    if (!radios.length) {
      return;
    }

    function apply() {
      var mode = 'text';
      var i;
      for (i = 0; i < radios.length; i++) {
        if (radios[i].checked) {
          mode = radios[i].value;
          break;
        }
      }
      var panels = document.querySelectorAll('.neccospeak-preview-panel');
      for (i = 0; i < panels.length; i++) {
        if (panels[i].getAttribute('data-mode') === mode) {
          panels[i].removeAttribute('hidden');
        } else {
          panels[i].setAttribute('hidden', 'hidden');
        }
      }
    }

    for (var j = 0; j < radios.length; j++) {
      radios[j].addEventListener('change', apply);
    }
    apply();
  }

  // ---- G4: Diagnostics tab client-side reachability checks ----------------
  // Rows are pre-rendered by includes/admin-diagnostics.php with a
  // "Checking…" placeholder and, for the rows handled here, an id
  // (neccospeak-diag-c1 / -c2 / -c2b / -c4) plus a nested
  // ".neccospeak-diag-state" span that this code fills in.
  function setDiagRow(rowId, state, text) {
    var row = document.getElementById(rowId);
    if (!row) {
      return;
    }
    row.setAttribute('data-state', state);
    var span = row.querySelector('.neccospeak-diag-state');
    if (span) {
      span.textContent = text;
      span.className = 'neccospeak-diag-state neccospeak-diag-state-' + state;
    }
  }


  // C2 (GET) / C2b (POST): {restUrl}ping — public route, deliberately
  // credentials:'omit' so the request matches what an anonymous visitor's
  // browser actually sends (design rev.2 §0.2.3-d / §10.4).
  function checkDiagPing(rowId, method) {
    if (!document.getElementById(rowId)) {
      return;
    }
    var reqInit = { method: method, credentials: 'omit' };
    if (method === 'POST') {
      reqInit.headers = { 'Content-Type': 'application/json' };
      reqInit.body = '{}';
    }
    fetch(cfg.restUrl + 'ping', reqInit).then(function (res) {
      if (res.ok) {
        setDiagRow(rowId, 'ok', t('diagOk'));
      } else if (res.status === 403) {
        setDiagRow(rowId, 'error', t('diagForbidden'));
      } else {
        setDiagRow(rowId, 'error', t('diagFail'));
      }
    }).catch(function () {
      setDiagRow(rowId, 'error', t('diagFail'));
    });
  }

  // C4: HEAD homeUrl, credentials:'omit', read the Referrer-Policy response
  // header. cfg.homeUrl is populated by neccospeak.php's admin localize once
  // that wiring lands (design rev.2 Appendix A "297-313"); until then this
  // falls back to the current origin, which is correct for the common case
  // of the admin screen and the site sharing an origin.
  function checkDiagC4() {
    var rowId = 'neccospeak-diag-c4';
    if (!document.getElementById(rowId)) {
      return;
    }
    var homeUrl = (cfg.homeUrl && typeof cfg.homeUrl === 'string' && cfg.homeUrl)
      ? cfg.homeUrl
      : (window.location.origin + '/');
    fetch(homeUrl, { method: 'HEAD', credentials: 'omit' }).then(function (res) {
      var rp = res.headers.get('Referrer-Policy');
      if (rp === 'no-referrer') {
        setDiagRow(rowId, 'ok', rp);
      } else if (rp) {
        setDiagRow(rowId, 'info', rp);
      } else {
        setDiagRow(rowId, 'info', t('diagNotSet'));
      }
    }).catch(function () {
      setDiagRow(rowId, 'warn', t('diagFail'));
    });
  }

  function initDiagnostics() {
    checkDiagPing('neccospeak-diag-c2', 'GET');
    checkDiagPing('neccospeak-diag-c2b', 'POST');
    checkDiagC4();
  }

  // ---- Confirm() guard for destructive admin-post forms --------------------
  // includes/admin-diagnostics.php renders the clear-key form with a
  // data-neccospeak-confirm="<localized message>" attribute (esc_attr__ done
  // server-side); this just wires window.confirm() to it.
  function initConfirmForms() {
    var forms = document.querySelectorAll('form[data-neccospeak-confirm]');
    for (var i = 0; i < forms.length; i++) {
      forms[i].addEventListener('submit', function (e) {
        var msg = this.getAttribute('data-neccospeak-confirm');
        if (msg && !window.confirm(msg)) { // eslint-disable-line no-alert
          e.preventDefault();
        }
      });
    }
  }

  // ---- G11: FAQ repeating rows (design rev.2 §4.4) -------------------------
  // The prototype <tbody id="neccospeak-faq-proto"> holds one disabled,
  // hidden <tr> template whose inputs use the literal name placeholder
  // "__i__" (see includes/admin-tabs.php). Adding a row clones it, swaps
  // __i__ for a MONOTONICALLY INCREASING counter (never rows.length -- see
  // the docblock on addRow() below), and removes "disabled" so the fields
  // are actually submitted.
  var OPTION_PREFIX = 'neccospeak_options';

  function fieldSelector(key) {
    return '[name="' + OPTION_PREFIX + '[' + key + ']"]';
  }

  var neccoSpeakFaq = null; // Populated by initFaqRows(); read by initPresets().

  function initFaqRows() {
    var table = document.querySelector('.neccospeak-faq-table');
    var rowsBody = document.getElementById('neccospeak-faq-rows');
    var protoBody = document.getElementById('neccospeak-faq-proto');
    var addBtn = document.getElementById('neccospeak-faq-add');
    var statusEl = document.getElementById('neccospeak-faq-status');
    if (!table || !rowsBody || !protoBody || !addBtn) {
      return;
    }

    var max = parseInt(table.getAttribute('data-neccospeak-faq-max'), 10);
    if (!max || max < 1) {
      max = 30;
    }
    // The counter starts at the number of rows the server already rendered
    // (data-neccospeak-faq-next) and only ever increases. Reusing
    // rowsBody.children.length instead would reassign an index that a
    // just-removed row used to hold; on submit PHP would then see two rows
    // sharing one array key and silently keep only the last one (design
    // rev.2 §4.4 C13).
    var nextIndex = parseInt(table.getAttribute('data-neccospeak-faq-next'), 10);
    if (!nextIndex || nextIndex < 0) {
      nextIndex = 0;
    }

    function setStatusText(text) {
      if (statusEl) {
        statusEl.textContent = text || ''; // textContent: XSS-safe
      }
    }

    function protoRow() {
      return protoBody.querySelector('.neccospeak-faq-row');
    }

    // q / a are optional prefill values (used by the preset-apply flow).
    function addRow(q, a) {
      if (rowsBody.querySelectorAll('.neccospeak-faq-row').length >= max) {
        setStatusText(t('faqMax'));
        return null;
      }
      var proto = protoRow();
      if (!proto) {
        return null;
      }
      var row = proto.cloneNode(true);
      var idx = nextIndex;
      nextIndex += 1;

      var fields = row.querySelectorAll('input, textarea');
      var i;
      for (i = 0; i < fields.length; i++) {
        var field = fields[i];
        var name = field.getAttribute('name');
        if (name) {
          field.setAttribute('name', name.replace('__i__', String(idx)));
        }
        field.removeAttribute('disabled');
      }

      var qEl = row.querySelector('[name$="[q]"]');
      var aEl = row.querySelector('[name$="[a]"]');
      if (qEl) {
        qEl.value = typeof q === 'string' ? q : '';
      }
      if (aEl) {
        aEl.value = typeof a === 'string' ? a : '';
      }

      rowsBody.appendChild(row);
      setStatusText('');
      return row;
    }

    // Used only by the preset-apply flow (§6.6): all existing rows are
    // removed and the counter is reset to 0, which is safe ONLY because
    // every row is being torn down at the same time -- nothing is left
    // behind to collide with.
    function resetRows() {
      while (rowsBody.firstChild) {
        rowsBody.removeChild(rowsBody.firstChild);
      }
      nextIndex = 0;
    }

    addBtn.addEventListener('click', function () {
      addRow();
    });

    // Event delegation: rows are added dynamically, so the listener lives on
    // the always-present tbody rather than on each row's remove button.
    rowsBody.addEventListener('click', function (e) {
      var target = e.target;
      if (!target || !target.className) {
        return;
      }
      if ((' ' + target.className + ' ').indexOf(' neccospeak-faq-remove ') === -1) {
        return;
      }
      var row = target;
      while (row && (!row.className || (' ' + row.className + ' ').indexOf(' neccospeak-faq-row ') === -1)) {
        row = row.parentNode;
      }
      if (row && row.parentNode === rowsBody) {
        rowsBody.removeChild(row);
      }
    });

    neccoSpeakFaq = { addRow: addRow, resetRows: resetRows };
  }

  // ---- G11: preset apply (design rev.2 §6.6) -------------------------------
  function faqRowsHaveContent() {
    var rowsBody = document.getElementById('neccospeak-faq-rows');
    if (!rowsBody) {
      return false;
    }
    var fields = rowsBody.querySelectorAll('[name$="[q]"], [name$="[a]"]');
    for (var i = 0; i < fields.length; i++) {
      if (fields[i].value && fields[i].value.replace(/^\s+|\s+$/g, '') !== '') {
        return true;
      }
    }
    return false;
  }

  // Whether the CURRENT form state already holds something a preset would
  // overwrite for this key -- used to decide whether to show the overwrite
  // confirmation. Selects are intentionally excluded: they always hold some
  // enum value (never really "empty"), so overwriting one is low-stakes.
  function isPresetTargetFilled(key) {
    if (key === 'prompt_faq') {
      return faqRowsHaveContent();
    }
    if (key === 'prompt_styles') {
      var boxes = document.querySelectorAll('[name="' + OPTION_PREFIX + '[prompt_styles][]"]');
      for (var i = 0; i < boxes.length; i++) {
        if (boxes[i].checked) {
          return true;
        }
      }
      return false;
    }
    var el = document.querySelector(fieldSelector(key));
    if (!el || el.tagName === 'SELECT') {
      return false;
    }
    return typeof el.value === 'string' && el.value.replace(/^\s+|\s+$/g, '') !== '';
  }

  function applyPresetStyles(list) {
    var boxes = document.querySelectorAll('[name="' + OPTION_PREFIX + '[prompt_styles][]"]');
    for (var i = 0; i < boxes.length; i++) {
      boxes[i].checked = !!list && list.indexOf(boxes[i].value) !== -1;
    }
  }

  function applyPresetFaq(rows) {
    if (!neccoSpeakFaq) {
      return;
    }
    neccoSpeakFaq.resetRows();
    if (!rows || !rows.length) {
      return;
    }
    for (var i = 0; i < rows.length; i++) {
      var row = rows[i];
      if (row && typeof row === 'object') {
        neccoSpeakFaq.addRow(row.q, row.a);
      }
    }
  }

  function applyPreset(values) {
    for (var key in values) {
      if (!Object.prototype.hasOwnProperty.call(values, key)) {
        continue;
      }
      if (key === 'prompt_faq') {
        applyPresetFaq(values[key]);
      } else if (key === 'prompt_styles') {
        applyPresetStyles(values[key]);
      } else {
        var el = document.querySelector(fieldSelector(key));
        if (el) {
          el.value = values[key];
        }
      }
    }
  }

  function initPresets() {
    var buttons = document.querySelectorAll('.neccospeak-preset');
    if (!buttons.length) {
      return;
    }
    var statusEl = document.getElementById('neccospeak-preset-status');

    function onPresetClick() {
      var raw = this.getAttribute('data-neccospeak-preset');
      var values;
      try {
        values = JSON.parse(raw);
      } catch (e) {
        return;
      }
      if (!values || typeof values !== 'object') {
        return;
      }

      var anyFilled = false;
      for (var key in values) {
        if (Object.prototype.hasOwnProperty.call(values, key) && isPresetTargetFilled(key)) {
          anyFilled = true;
          break;
        }
      }
      if (anyFilled && !window.confirm(t('presetConfirm'))) { // eslint-disable-line no-alert
        return;
      }

      applyPreset(values);
      if (statusEl) {
        statusEl.textContent = t('presetApplied'); // textContent: XSS-safe
      }
    }

    for (var i = 0; i < buttons.length; i++) {
      buttons[i].addEventListener('click', onPresetClick);
    }
  }

  // ---- Phase 4: change-event bridge for the G6 live preview ---------------
  // wp-color-picker (Iris) writes the underlying <input>'s .value directly
  // but never dispatches an 'input'/'change' DOM event, so anything (like
  // the G6 live preview, design phase4 §11.5) listening for real user input
  // on that field would otherwise never fire. This synthesizes one.
  function scheduleThemeChanged(el) {
    if (!el) {
      return;
    }
    var evt;
    try {
      evt = new Event('input', { bubbles: true });
    } catch (e) {
      // Very old browsers without the Event constructor: degrade silently.
      // wp-color-picker itself requires a modern browser anyway.
      return;
    }
    el.dispatchEvent(evt);
  }

  // ---- Phase 4 (design phase4 §8.5): wp-color-picker -----------------------
  function initColorPickers() {
    if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.wpColorPicker) {
      return;
    }
    var $ = window.jQuery;
    $('[data-neccospeak-color]').wpColorPicker({
      change: function (event, ui) {
        scheduleThemeChanged(this);
      },
      clear: function () {
        scheduleThemeChanged(this);
      }
    });
  }

  // ---- Phase 4 (design phase4 §8.4): generic wp.media image picker --------
  function initMediaPickers() {
    var wrappers = document.querySelectorAll('[data-neccospeak-media]');
    if (!wrappers.length) {
      return;
    }

    for (var i = 0; i < wrappers.length; i++) {
      (function (wrapper) {
        var input = wrapper.querySelector('[data-neccospeak-media-input]');
        var preview = wrapper.querySelector('[data-neccospeak-media-preview]');
        var selectBtn = wrapper.querySelector('[data-neccospeak-media-select]');
        var removeBtn = wrapper.querySelector('[data-neccospeak-media-remove]');
        if (!input) {
          return;
        }

        // Sets the visible on-page <img> only. Deliberately independent from
        // data-neccospeak-media-url (design phase4 review fix): that
        // attribute stays at the production-fidelity size (e.g. 'large' for
        // the watermark, up to 1024px) for the G6 live preview, while the
        // small inline preview in the .form-table always uses a 'thumbnail'
        // URL — see thumbUrlFromAttachment() below and
        // includes/admin-tabs.php's neccospeak_render_media_field(), which
        // renders the initial page load's <img> the same way.
        function setPreview(url) {
          if (!preview) {
            return;
          }
          if (url) {
            preview.src = url;
            preview.removeAttribute('hidden');
          } else {
            preview.src = '';
            preview.setAttribute('hidden', 'hidden');
          }
        }

        // Resolves the small on-page preview from a wp.media attachment's
        // own 'thumbnail' size, independent of data-neccospeak-media-size
        // (which drives data-neccospeak-media-url instead — see setPreview()
        // above).
        function thumbUrlFromAttachment(attachment) {
          if (attachment && attachment.sizes && attachment.sizes.thumbnail && attachment.sizes.thumbnail.url) {
            return attachment.sizes.thumbnail.url;
          }
          return (attachment && attachment.url) || '';
        }

        if (removeBtn) {
          removeBtn.addEventListener('click', function () {
            input.value = '0';
            input.setAttribute('data-neccospeak-media-url', '');
            setPreview('');
            scheduleThemeChanged(input);
          });
        }

        if (!selectBtn || !window.wp || !window.wp.media) {
          return; // wp.media unavailable: the number input still works standalone.
        }

        var frame = null;
        selectBtn.addEventListener('click', function (e) {
          e.preventDefault();
          if (frame) {
            frame.open();
            return;
          }
          frame = window.wp.media({
            title: t('mediaTitle'),
            button: { text: t('mediaButton') },
            multiple: false,
            library: { type: 'image' }
          });
          frame.on('select', function () {
            var selection = frame.state().get('selection').first();
            if (!selection) {
              return;
            }
            var attachment = selection.toJSON();
            // Request the same size includes/admin-tabs.php's
            // neccospeak_render_media_field() rendered this field with (and
            // that includes/frontend-config.php's
            // neccospeak_widget_attachment_url() requests for this same
            // field), so data-neccospeak-media-url (the G6 live preview's
            // source) matches what production shows instead of always the
            // 'medium' size (design phase4 §11.5 review fix). The visible
            // on-page <img> is set separately, from the 'thumbnail' size,
            // via thumbUrlFromAttachment() (design phase4 review fix).
            var size = input.getAttribute('data-neccospeak-media-size') || 'medium';
            var url = (attachment.sizes && attachment.sizes[size] && attachment.sizes[size].url)
              ? attachment.sizes[size].url
              : attachment.url;
            input.value = String(attachment.id);
            input.setAttribute('data-neccospeak-media-url', url || '');
            setPreview(thumbUrlFromAttachment(attachment));
            scheduleThemeChanged(input);
          });
          frame.open();
        });
      })(wrappers[i]);
    }
  }

  // ---- Phase 4 (design phase4 §8.3, D2): generic repeater ------------------
  // Same contract as initFaqRows() above (monotonic index counter, __i__
  // placeholder, disabled prototype row) but namespaced by
  // data-neccospeak-repeat="<name>" so multiple independent repeaters can
  // coexist on one page without their add/remove handlers cross-firing.
  // Deliberately NOT shared code with initFaqRows() (design phase4 D2): the
  // FAQ table must never gain these generic data attributes, or it would be
  // double-bound by both implementations.
  function initRepeaters() {
    var tables = document.querySelectorAll('[data-neccospeak-repeat]');
    for (var i = 0; i < tables.length; i++) {
      initOneRepeater(tables[i]);
    }
  }

  function initOneRepeater(table) {
    var name = table.getAttribute('data-neccospeak-repeat');
    if (!name) {
      return;
    }
    var rowsBody = table.querySelector('[data-neccospeak-repeat-rows]');
    var protoBody = table.querySelector('[data-neccospeak-repeat-proto]');
    var addBtn = document.querySelector('[data-neccospeak-repeat-add="' + name + '"]');
    var statusEl = document.querySelector('[data-neccospeak-repeat-status="' + name + '"]');
    if (!rowsBody || !protoBody || !addBtn) {
      return;
    }

    var max = parseInt(table.getAttribute('data-neccospeak-repeat-max'), 10);
    if (!max || max < 1) {
      max = 8;
    }
    // Monotonically increasing — never rows.length (a just-removed row's
    // index must never be reused, same C13 rationale as the FAQ repeater).
    var nextIndex = parseInt(table.getAttribute('data-neccospeak-repeat-next'), 10);
    if (!nextIndex || nextIndex < 0) {
      nextIndex = 0;
    }

    function setStatusText(text) {
      if (statusEl) {
        statusEl.textContent = text || ''; // textContent: XSS-safe
      }
    }

    function protoRow() {
      return protoBody.firstElementChild;
    }

    function rowCount() {
      return rowsBody.children.length;
    }

    function addRow() {
      if (rowCount() >= max) {
        setStatusText(t('repeatMax'));
        return null;
      }
      var proto = protoRow();
      if (!proto) {
        return null;
      }
      var row = proto.cloneNode(true);
      var idx = nextIndex;
      nextIndex += 1;

      var fields = row.querySelectorAll('input, select, textarea');
      for (var i = 0; i < fields.length; i++) {
        var field = fields[i];
        var fname = field.getAttribute('name');
        if (fname) {
          field.setAttribute('name', fname.replace('__i__', String(idx)));
        }
        field.removeAttribute('disabled');
      }

      rowsBody.appendChild(row);
      setStatusText('');
      return row;
    }

    addBtn.addEventListener('click', function () {
      addRow();
    });

    rowsBody.addEventListener('click', function (e) {
      var target = e.target;
      if (!target || !target.className) {
        return;
      }
      if ((' ' + target.className + ' ').indexOf(' neccospeak-repeat-remove ') === -1) {
        return;
      }
      var row = target;
      while (row && row.parentNode !== rowsBody) {
        row = row.parentNode;
        if (!row) {
          return;
        }
      }
      rowsBody.removeChild(row);
    });
  }

  // ---- Phase 4 (design phase4 §4.3): header gradient field show/hide ------
  function initHeaderStyleToggle() {
    var select = document.querySelector('[data-neccospeak-header-style]');
    if (!select) {
      return;
    }
    var rows = document.querySelectorAll('[data-neccospeak-header-gradient-row]');

    function apply() {
      var isGradient = select.value === 'gradient';
      for (var i = 0; i < rows.length; i++) {
        if (isGradient) {
          rows[i].removeAttribute('hidden');
        } else {
          rows[i].setAttribute('hidden', 'hidden');
        }
      }
    }

    select.addEventListener('change', apply);
    apply();
  }

  // ---- Phase 4 (design phase4 §5.1/§6.5): custom-icon field show/hide -----
  // Same pattern applied to both the launcher icon select and the home-view
  // icon select: the "custom image" media picker row is only relevant when
  // "Custom image" is the chosen icon.
  function initCustomIconToggle(selectAttr, rowAttr) {
    var select = document.querySelector('[' + selectAttr + ']');
    if (!select) {
      return;
    }
    var rows = document.querySelectorAll('[' + rowAttr + ']');

    function apply() {
      var isCustom = select.value === 'custom';
      for (var i = 0; i < rows.length; i++) {
        if (isCustom) {
          rows[i].removeAttribute('hidden');
        } else {
          rows[i].setAttribute('hidden', 'hidden');
        }
      }
    }

    select.addEventListener('change', apply);
    apply();
  }

  // ---- Phase 5 (design phase5 §14): "exporting…" status for CSV export ----
  // Opt-in via data-neccospeak-export="<css selector of a status element>"
  // (falls back to a sibling .neccospeak-export-status if the selector is
  // empty/omitted or resolves to nothing). The export itself is a normal
  // admin-post.php form submission (a file download), so this never
  // preventDefault()s — it only gives the admin visible feedback while the
  // browser starts the download.
  function initExportButtons() {
    var buttons = document.querySelectorAll('[data-neccospeak-export]');
    for (var i = 0; i < buttons.length; i++) {
      buttons[i].addEventListener('click', function () {
        var sel = this.getAttribute('data-neccospeak-export');
        var status = null;
        if (sel) {
          try {
            status = document.querySelector(sel);
          } catch (e) { status = null; }
        }
        if (!status && this.parentNode) {
          status = this.parentNode.querySelector('.neccospeak-export-status');
        }
        if (status) {
          status.textContent = t('exporting'); // textContent: XSS-safe
        }
      });
    }
  }


  // ---- Phase 6 (design phase6 §6.3/§11.4): G26 "Rebuild index" -------------
  function initRagSync() {
    var btn = document.getElementById('neccospeak-rag-sync-btn');
    if (!btn) {
      return;
    }
    var progress = document.getElementById('neccospeak-rag-sync-progress');
    var bar = document.getElementById('neccospeak-rag-sync-bar');
    var statusEl = document.getElementById('neccospeak-rag-sync-status');
    var nonce = btn.getAttribute('data-neccospeak-rag-nonce') || '';
    var ajaxUrl = (cfg.ajaxUrl && typeof cfg.ajaxUrl === 'string' && cfg.ajaxUrl)
      ? cfg.ajaxUrl
      : (window.location.origin + '/wp-admin/admin-ajax.php');

    function setStatusText(text) {
      if (statusEl) {
        statusEl.textContent = text || ''; // textContent: XSS-safe
      }
    }

    function step(cursor, reset) {
      var body = 'action=neccospeak_rag_sync&nonce=' + encodeURIComponent(nonce)
        + '&cursor=' + encodeURIComponent(cursor)
        + (reset ? '&reset=1' : '');
      fetch(ajaxUrl, {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: body
      }).then(function (res) {
        return res.json();
      }).then(function (data) {
        var d = (data && typeof data === 'object' && data.data && typeof data.data === 'object') ? data.data : data;
        if (!data || !data.success || !d || typeof d !== 'object') {
          setStatusText(t('ragSyncFailed'));
          btn.disabled = false;
          return;
        }
        var total = typeof d.total === 'number' ? d.total : 0;
        var processed = typeof d.processed === 'number' ? d.processed : 0;
        var pct = total > 0 ? Math.round((processed / total) * 100) : 100;
        if (bar) {
          bar.value = Math.max(0, Math.min(100, pct));
        }
        if (d.done) {
          var chunks = typeof d.chunks === 'number' ? d.chunks : 0;
          setStatusText(t('ragSyncDone').replace('%1$s', String(chunks)).replace('%2$s', String(processed)));
          btn.disabled = false;
          return;
        }
        setStatusText(t('ragSyncing').replace('%1$s', String(processed)).replace('%2$s', String(total)));
        step(typeof d.cursor === 'number' ? d.cursor : 0, false);
      }).catch(function () {
        setStatusText(t('ragSyncFailed'));
        btn.disabled = false;
      });
    }

    btn.addEventListener('click', function () {
      if (!window.confirm(t('ragSyncConfirm'))) { // eslint-disable-line no-alert
        return;
      }
      btn.disabled = true;
      if (progress) {
        progress.removeAttribute('hidden');
      }
      if (bar) {
        bar.value = 0;
      }
      setStatusText(t('ragSyncing').replace('%1$s', '0').replace('%2$s', '…'));
      step(0, true);
    });
  }

  // ---- Phase 6 (design phase6 §5.8): G16 tools master/target dimming -------
  // Deliberately never sets `.disabled` on the target fields: their checked
  // state must still be submitted (and therefore preserved) even while the
  // master switch is unchecked — the server-side neccospeak_tools_enabled()
  // gate is what actually decides whether tools run, not this UI affordance.
  function initToolToggles() {
    var master = document.querySelector('[data-neccospeak-master-toggle="tools"]');
    if (!master) {
      return;
    }
    var targets = document.querySelectorAll('[data-neccospeak-toggle-target="tools"]');

    function apply() {
      for (var i = 0; i < targets.length; i++) {
        targets[i].style.opacity = master.checked ? '' : '0.6';
      }
    }

    master.addEventListener('change', apply);
    apply();
  }

  // ---- Phase 6 (design phase6 §8.2/§11.4): G38 CF7 map-field rows ----------
  // Event delegation on the cta_list repeater table so this also covers rows
  // the generic repeater (section 6 above) clones after page load, without
  // needing its own re-init hook.
  function initCf7MapRows() {
    var table = document.querySelector('table[data-neccospeak-repeat="cta"]');
    if (!table) {
      return;
    }

    function applyRow(select) {
      var row = select;
      while (row && row.parentNode && row.tagName !== 'TR') {
        row = row.parentNode;
      }
      if (!row || row.tagName !== 'TR') {
        return;
      }
      var mapDiv = row.querySelector('[data-neccospeak-cta-cf7-map]');
      if (!mapDiv) {
        return;
      }
      if (select.value === 'cf7_prefill') {
        mapDiv.removeAttribute('hidden');
      } else {
        mapDiv.setAttribute('hidden', 'hidden');
      }
    }

    table.addEventListener('change', function (e) {
      var target = e.target;
      if (target && target.hasAttribute && target.hasAttribute('data-neccospeak-cta-type-select')) {
        applyRow(target);
      }
    });
  }

  // ---- Init ---------------------------------------------------------------
  ready(function () {
    var btn = document.getElementById('neccospeak-fetch-models');
    if (btn) {
      btn.addEventListener('click', function (e) {
        e.preventDefault();
        fetchModels(btn);
      });
    }
    initPromptPreview();
    initDiagnostics();
    initConfirmForms();
    initFaqRows();
    initPresets();
    initColorPickers();
    initMediaPickers();
    initRepeaters();
    initHeaderStyleToggle();
    initCustomIconToggle('data-neccospeak-launcher-icon', 'data-neccospeak-launcher-icon-custom-row');
    initCustomIconToggle('data-neccospeak-home-icon', 'data-neccospeak-home-icon-custom-row');
    initExportButtons();
    initRagSync();
    initToolToggles();
    initCf7MapRows();
  });
})();
