/**
 * NeccoSpeak — Contact Form 7 hand-off module (G38). Owner: impl-front.
 * See docs/phase6-design.md §2.6 / §8.4 / §8.4a / §8.5.
 *
 * Two layers that share one file (D8) because the "receiving" half must run
 * on pages where the chat widget itself is never mounted (the most common
 * real setup: the contact-form page is excluded from the widget, §8.4a):
 *
 *  (A) Send-out (needs `ctx`): on a `cf7_prefill`-typed CTA click
 *      (`module:cta:click`, F36 — payload is `{index,type,label}` only, so
 *      the CTA row itself is looked up from `ctx.config.features.cta.items`
 *      by index), writes a short-lived hand-off payload to
 *      `win.sessionStorage` under the fixed key `'neccospeak:cf7prefill'`
 *      (D8 — NOT `ctx.storage()`, whose F26 restUrl-hash namespace the
 *      receiving side, running without a mounted widget, cannot reproduce).
 *      The CTA's own `<a href>` (widget-cta.js, D7) does the actual same-tab
 *      navigation; this listener only has to finish writing before that
 *      default navigation happens, which it always does (synchronous click
 *      handler, §8.5).
 *  (B) Receive (no `ctx` required — D8/§8.4a): reads that key, validates
 *      version/TTL/path, fills matching `<input>`/`<textarea>` fields inside
 *      the page's first `form.wpcf7-form`, and always removes the key
 *      (success or failure). Runs both as part of module setup() (widget is
 *      mounted here too) AND, independently, whenever `window.neccoSpeakCf7`
 *      is present (the standalone enqueue path for pages where the widget is
 *      hidden, §8.4a) — a one-shot guard (`win.__neccoSpeakCf7Applied`)
 *      keeps the two entry points from ever running the body twice.
 *
 * ES5 IIFE taking `window` as `win` explicitly (never a bare `window`/
 * `document` global) so this file behaves identically whether it is loaded
 * as a NeccoSpeakModules entry or as the bare standalone script §8.4a
 * enqueues on form pages that never load widget-core.js at all.
 */
(function (win) {
  'use strict';

  var KEY = 'neccospeak:cf7prefill'; // fixed (§8.4 / D8) — not ctx.storage()'s namespaced key
  var VER = 1;
  var DEFAULT_TTL = 300000; // 5 minutes (NECCOSPEAK_CF7_HANDOFF_TTL_MS)
  var VALUE_MAX_CHARS = 2000; // NECCOSPEAK_CF7_VALUE_MAX_CHARS
  var FIELD_NAME_RE = /^[A-Za-z0-9_\-]{1,64}(\[\])?$/; // mirrors sanitize_cf7_map()'s server-side check (§8.3)

  // --- sessionStorage thin wrapper (ctx.storage() is not used — D8) --------
  function ss() {
    try { return win.sessionStorage || null; } catch (e) { return null; } // e.g. Safari private mode
  }
  function ssGet(k) { var s = ss(); try { return s ? s.getItem(k) : null; } catch (e) { return null; } }
  function ssSet(k, v) { var s = ss(); try { if (s) { s.setItem(k, v); } } catch (e) { /* swallow */ } }
  function ssDel(k) { var s = ss(); try { if (s) { s.removeItem(k); } } catch (e) { /* swallow */ } }

  function pathOf(url) {
    try { return new win.URL(url, win.location.href).pathname; } catch (e) { return ''; }
  }

  // Re-validates a field name against the same pattern the server enforced
  // at save time (§8.3) before it is ever used to build a `[name="…"]`
  // selector — an extra layer, not a trust boundary shortcut (§8.5 safety
  // notes table: "セレクタ組み立て前に同じ正規表現で再検証する").
  function cssEscapeName(name) {
    if (typeof name !== 'string' || !FIELD_NAME_RE.test(name)) { return null; }
    return name;
  }

  // Builds the `message` slot's fallback value from the conversation
  // (§8.4 "message の値"): the most recent user turn, machine-assembled —
  // Phase 6 deliberately does not ask the model to summarize (no extra API
  // call / cost). Returns '' when there is no user turn to quote, so the
  // caller skips the `message` field entirely rather than sending a
  // half-empty template.
  function buildMessage(ctx) {
    if (!ctx || !ctx.api || typeof ctx.api.getHistory !== 'function') { return ''; }
    var hist = ctx.api.getHistory() || [];
    var lastUser = '';
    for (var i = hist.length - 1; i >= 0; i--) {
      var h = hist[i];
      if (h && h.role === 'user' && typeof h.content === 'string' && h.content) { lastUser = h.content; break; }
    }
    if (!lastUser) { return ''; }
    var path = (win.location && typeof win.location.pathname === 'string') ? win.location.pathname : '';
    return 'Question: ' + lastUser + '\n\n(From the AI chat on ' + path + ')';
  }

  // =========================================================================
  // (A) Send-out — registered as a NeccoSpeakModules entry, needs ctx.
  // =========================================================================
  function setup(ctx) {
    ctx.on('module:cta:click', function (d) {
      if (!ctx.setting('enabled', false)) { return; } // F-1
      // Payload is exactly {index,type,label} (F36) — `d.item`/`d.value` do
      // not exist. The row itself must be looked up from config by index.
      if (!d || d.type !== 'cf7_prefill') { return; }

      var items = (ctx.config && ctx.config.features && ctx.config.features.cta)
        ? ctx.config.features.cta.items : null;
      if (!items || !items.length) { return; }
      var item = items[d.index];
      if (!item || item.type !== 'cf7_prefill') { return; } // index drift guard (F-6b)

      var u = pathOf(item.value);
      if (!u) { return; }

      var map = item.map || {};
      var seed = item.seed || {};
      var f = {};
      ['name', 'email', 'message'].forEach(function (slot) {
        var field = map[slot];
        if (!field) { return; } // unmapped slot: never sent
        var v = seed[slot] || ''; // seed (site-injected via the filter, §8.6) wins over conversation-derived
        if (!v && slot === 'message') { v = buildMessage(ctx); }
        if (!v) { return; }
        f[field] = String(v).slice(0, VALUE_MAX_CHARS);
      });
      if (!Object.keys(f).length) { return; }

      ssSet(KEY, JSON.stringify({ v: VER, t: Date.now(), u: u, f: f }));
      // The actual navigation is the CTA's own <a href> default action
      // (widget-cta.js, D7 — no target="_blank", so it stays same-tab and
      // sessionStorage survives). This listener runs synchronously before
      // that default action, so the write always lands first.
    });

    // In case this same page is also the form page (widget shown + form on
    // one page), try the receiving side too.
    applyPrefill(win, ctx);
  }

  // =========================================================================
  // (B) Receive — ctx-optional (D8 / §8.4a): must work on pages where the
  // widget is never mounted at all.
  // =========================================================================
  function applyPrefill(win, ctx) {
    if (win.__neccoSpeakCf7Applied) { return; } // one-shot guard (F-17)
    win.__neccoSpeakCf7Applied = true;

    var raw = ssGet(KEY);
    if (!raw) { return; }
    ssDel(KEY); // read once, remove always — success or failure (F-10)

    var ttlMs = (win.neccoSpeakCf7 && win.neccoSpeakCf7.ttlMs)
      || (ctx && ctx.setting ? ctx.setting('ttlMs', DEFAULT_TTL) : DEFAULT_TTL);

    var p;
    try { p = JSON.parse(raw); } catch (e) { return; }
    if (!p || p.v !== VER) { return; }
    if (typeof p.t !== 'number' || (Date.now() - p.t) > ttlMs) { return; }
    if (typeof p.u !== 'string' || !win.location || p.u !== win.location.pathname) { return; }
    if (!p.f || typeof p.f !== 'object') { return; }

    if (!win.document || typeof win.document.querySelectorAll !== 'function') { return; }
    var forms = win.document.querySelectorAll('form.wpcf7-form');
    if (!forms || !forms.length) { return; } // F-12
    var form = forms[0];
    var n = 0;
    Object.keys(p.f).forEach(function (fieldName) {
      var safe = cssEscapeName(fieldName);
      if (!safe) { return; } // F-13: malformed name — never build a selector from it
      var el = form.querySelector('[name="' + safe + '"]');
      if (!el) { return; }
      var tag = String(el.tagName || '').toLowerCase();
      if (tag !== 'input' && tag !== 'textarea') { return; } // F-14: select/checkbox excluded
      if (el.value) { return; } // F-11: never overwrite an existing value
      el.value = String(p.f[fieldName]).slice(0, VALUE_MAX_CHARS);
      ++n;
      try { el.dispatchEvent(new win.Event('input', { bubbles: true })); } catch (e) { /* swallow */ }
    });
    if (ctx && ctx.emit) { ctx.emit('module:cf7:prefilled', { fields: n }); }
  }

  // Registered as a module (send-out + widget-mounted-page receive side).
  win.NeccoSpeakModules = win.NeccoSpeakModules || [];
  win.NeccoSpeakModules.push({ id: 'cf7', setup: setup });

  // Standalone-script entry point (§8.4a): the widget is not mounted here at
  // all, so `NeccoSpeakModules`/`setup()` may never run — run the receiving
  // side directly, ctx-free, as soon as the DOM is ready.
  if (win.neccoSpeakCf7) {
    if (win.document && win.document.readyState === 'loading') {
      win.document.addEventListener('DOMContentLoaded', function () { applyPrefill(win, null); });
    } else {
      applyPrefill(win, null);
    }
  }
})(window);
