// Phase 2 strict-review fix #3 (KI-013): editor-side surface for
// `clb/product-card` that completes the spec §8.4 "in-editor product
// search" loop.
//
// Pipeline (matches gutenberg-block-spec.md §8.1 / §8.4):
//
//   1. The editor surfaces a provider tab strip (Amazon / 楽天市場 /
//      Yahoo!ショッピング) plus a keyword input.
//   2. On Search the editor calls `GET /clb/v1/products/search` with
//      `provider` + `keywords` (debounced + Enter / button driven).
//      The REST endpoint returns the §6.3 / §8.1 search-result row
//      shape -- title / brand / image / price / detail_url / is_saved.
//   3. Each result row exposes a "Save & Use" action that POSTs the
//      row to `/clb/v1/products` (or skips the POST when the row's
//      `is_saved` flag is true) and then writes the persisted product
//      id into the block's `productId` attribute. The editor preview
//      switches to a "Selected product #N" summary card, with a
//      "Change product" affordance to return to the search panel.
//   4. A saved-product picker calls `GET /clb/v1/products?search=...`
//      so existing records can be selected by title instead of by
//      manually entering numeric IDs.
//
// Rendered entirely through `wp.element.createElement()` so the
// block ships without a JSX build step. Mirrors the
// dependency-tolerant pattern established by `clb/product-grid`'s
// editor: short-circuits when wp.blocks / wp.element are missing
// so a stripped-down editor host cannot break the bootstrap.
//
// Out of scope of this slice (deferred to follow-up KIs):
//   - Inspector controls for showPrice / showBrand / showDescription
//     / showProviderBadge / template selector (KI-015).
//   - Server-rendered preview integration (spec §8.9 / Phase 7 P0
//     "preview"). The editor preview is a static summary; the
//     front-end render call still drives the canonical HTML.
(function () {
  if (typeof window === "undefined" || !window.wp) {
    return;
  }

  var wp = window.wp;
  var blocks = wp.blocks;
  var element = wp.element;
  var apiFetch = wp.apiFetch;
  var blockEditor = wp.blockEditor || null;
  var components = wp.components || null;
  var i18n = wp.i18n || {
    __: function (s) {
      return s;
    },
  };

  if (!blocks || !element || typeof blocks.registerBlockType !== "function") {
    return;
  }

  var __ = i18n.__;
  var createElement = element.createElement;
  var useState = element.useState;
  var useCallback = element.useCallback;
  var useEffect = element.useEffect;
  var useRef = element.useRef;
  var Fragment = element.Fragment || "div";
  var DOMAIN = "commerce-link-builder";
  var SEARCH_DEBOUNCE_MS = 250;
  var SAVED_SEARCH_PAGE_SIZE = 10;

  // Spec §5 テンプレート採用許可セット (Phase 5 P1)。
  // `single-fallback-card` は fallback path 専用のため
  // セレクタより除外。KI-015 docblock 参照。
  var TEMPLATE_OPTIONS = [
    { value: "single-default-card", label: "デフォルトカード" },
    { value: "single-compact-card", label: "コンパクトカード" },
    { value: "single-minimal-cta", label: "ミニマル CTA" },
  ];

  // Spec §4.1 canonical provider keys. The REST `/products/search`
  // `provider` query parameter accepts these tokens plus the
  // special `'all'` cross-provider token (omitted here because the
  // editor surface is single-provider per search).
  var PROVIDERS = [
    { key: "amazon", label: "Amazon" },
    { key: "rakuten", label: "楽天市場" },
    { key: "yahoo_shopping", label: "Yahoo!ショッピング" },
  ];

  /**
   * Coerce a raw `productId` attribute (mixed shape per the
   * historical editor JS that stored it as either int or
   * stringified int) into a positive integer or null.
   */
  function normalizeId(raw) {
    if (typeof raw === "number" && raw > 0 && Math.floor(raw) === raw) {
      return raw;
    }
    if (typeof raw === "string" && raw.trim() !== "") {
      var n = parseInt(raw.trim(), 10);
      if (!isNaN(n) && n > 0) {
        return n;
      }
    }
    return null;
  }

  /**
   * Pull the search-result list out of a §4.2 success envelope or
   * any of the historical flatter shapes the REST surface has
   * returned across phases. Mirrors the resilient parser
   * `clb/product-grid`'s editor uses.
   */
  function extractItems(response) {
    if (response && response.data && Array.isArray(response.data.items)) {
      return response.data.items;
    }
    if (response && Array.isArray(response.items)) {
      return response.items;
    }
    if (response && Array.isArray(response.data)) {
      return response.data;
    }
    if (Array.isArray(response)) {
      return response;
    }
    return [];
  }

  /**
   * Build the §8.4 POST `/clb/v1/products` body for a single
   * search-result row. Returns null when the row lacks a field
   * the spec marks as required (provider / provider_item_id /
   * title / detail_url) so the caller can surface a row-level
   * error instead of issuing a doomed request.
   */
  function buildSavePayload(row) {
    if (!row || typeof row !== "object") {
      return null;
    }
    var provider = typeof row.provider === "string" ? row.provider : "";
    var providerItemId =
      typeof row.provider_item_id === "string" ? row.provider_item_id : "";
    var title = typeof row.title === "string" ? row.title : "";
    var detailUrl =
      row.links && typeof row.links.detail_url === "string"
        ? row.links.detail_url
        : "";
    if (
      provider === "" ||
      providerItemId === "" ||
      title === "" ||
      detailUrl === ""
    ) {
      return null;
    }

    var snapshot = {
      title: title,
      detail_url: detailUrl,
    };
    if (typeof row.brand === "string" && row.brand !== "") {
      snapshot.brand = row.brand;
    }
    if (
      typeof row.image_primary_url === "string" &&
      row.image_primary_url !== ""
    ) {
      snapshot.image_primary_url = row.image_primary_url;
    }
    if (row.price && typeof row.price === "object") {
      var priceFragment = {};
      if (typeof row.price.amount === "string" && row.price.amount !== "") {
        priceFragment.amount = row.price.amount;
      }
      if (
        typeof row.price.currency === "string" &&
        row.price.currency !== "" &&
        row.price.currency !== "UNKNOWN"
      ) {
        priceFragment.currency = row.price.currency;
      }
      if (typeof row.price.label === "string" && row.price.label !== "") {
        priceFragment.label = row.price.label;
      }
      if (priceFragment.amount && priceFragment.currency) {
        snapshot.price = priceFragment;
      }
    }

    return {
      provider: provider,
      provider_item_id: providerItemId,
      snapshot: snapshot,
    };
  }

  /**
   * Extract a positive integer product id from the §4.2 success
   * envelope returned by `POST /clb/v1/products`.
   *
   * The controller stamps `data.id` (the persisted record's PK)
   * and `data.product.id` (via the summary mapper); fall back
   * across both shapes so a future envelope tweak does not break
   * the editor surface.
   */
  function extractSavedId(response) {
    if (response && response.data) {
      var data = response.data;
      if (typeof data.id === "number" && data.id > 0) {
        return data.id;
      }
      if (
        data.product &&
        typeof data.product.id === "number" &&
        data.product.id > 0
      ) {
        return data.product.id;
      }
    }
    if (response && typeof response.id === "number" && response.id > 0) {
      return response.id;
    }
    return null;
  }

  /**
   * Render the search-driven product-selection panel.
   *
   * Owns the local search state (query / provider / loading /
   * results / per-row save status) so the parent edit() function
   * stays thin. Calls `onSelect(id, summary)` once the row's
   * save round-trip succeeds, leaving attribute persistence to
   * the parent.
   */
  function SearchPanel(props) {
    var onSelect = props.onSelect;

    var providerState = useState(PROVIDERS[0].key);
    var providerKey = providerState[0];
    var setProviderKey = providerState[1];

    var queryState = useState("");
    var query = queryState[0];
    var setQuery = queryState[1];
    var queryInputRef = typeof useRef === "function" ? useRef(null) : null;

    var resultsState = useState({ status: "idle", items: [], error: null });
    var results = resultsState[0];
    var setResults = resultsState[1];

    // Per-row save status keyed by `${provider}:${provider_item_id}`
    // so independent rows can show their own pending / error /
    // saved state without re-rendering the whole result list.
    var rowStatusState = useState({});
    var rowStatus = rowStatusState[0];
    var setRowStatus = rowStatusState[1];

    var apiAvailable = typeof apiFetch === "function";

    useEffect(function () {
      if (
        queryInputRef &&
        queryInputRef.current &&
        typeof queryInputRef.current.focus === "function"
      ) {
        queryInputRef.current.focus();
      }
    }, []);

    var runSearch = useCallback(
      function () {
        if (!apiAvailable) {
          setResults({
            status: "error",
            items: [],
            error: __(
              "REST API クライアント (wp.apiFetch) がこのエディターホストで読み込まれていません。",
              DOMAIN,
            ),
          });
          return;
        }

        var trimmed = (query || "").trim();
        if (trimmed === "") {
          setResults({
            status: "error",
            items: [],
            error: __("検索キーワードを入力してください。", DOMAIN),
          });
          return;
        }

        setResults({ status: "loading", items: [], error: null });

        var path =
          "/clb/v1/products/search?provider=" +
          encodeURIComponent(providerKey) +
          "&keywords=" +
          encodeURIComponent(trimmed) +
          "&per_page=10";

        apiFetch({ path: path })
          .then(function (response) {
            var items = extractItems(response);
            setResults({
              status: "ok",
              items: items,
              error: null,
            });
          })
          .catch(function (err) {
            var message = __(
              "検索に失敗しました。プロバイダー認証情報が設定済みか、edit_posts 権限があるかを確認してください。",
              DOMAIN,
            );
            if (err && typeof err.message === "string" && err.message !== "") {
              message = err.message;
            } else if (
              err &&
              err.error &&
              typeof err.error.message === "string"
            ) {
              message = err.error.message;
            }
            setResults({ status: "error", items: [], error: message });
          });
      },
      [apiAvailable, providerKey, query],
    );

    var handleSave = useCallback(
      function (row) {
        var rowKey =
          (typeof row.provider === "string" ? row.provider : "?") +
          ":" +
          (typeof row.provider_item_id === "string"
            ? row.provider_item_id
            : "?");

        // Already-saved row: skip the POST, just adopt the id.
        if (row.is_saved && typeof row.id === "number" && row.id > 0) {
          onSelect(row.id, {
            title: typeof row.title === "string" ? row.title : "",
            provider: typeof row.provider === "string" ? row.provider : "",
          });
          return;
        }

        if (!apiAvailable) {
          setRowStatus(function (prev) {
            var next = {};
            for (var k in prev) {
              if (Object.prototype.hasOwnProperty.call(prev, k)) {
                next[k] = prev[k];
              }
            }
            next[rowKey] = {
              status: "error",
              error: __(
                "wp.apiFetch が読み込まれていないため保存できません。",
                DOMAIN,
              ),
            };
            return next;
          });
          return;
        }

        var payload = buildSavePayload(row);
        if (payload === null) {
          setRowStatus(function (prev) {
            var next = {};
            for (var k in prev) {
              if (Object.prototype.hasOwnProperty.call(prev, k)) {
                next[k] = prev[k];
              }
            }
            next[rowKey] = {
              status: "error",
              error: __(
                "この検索結果は必須フィールド (タイトル / 詳細 URL) が欠けているため保存できません。",
                DOMAIN,
              ),
            };
            return next;
          });
          return;
        }

        setRowStatus(function (prev) {
          var next = {};
          for (var k in prev) {
            if (Object.prototype.hasOwnProperty.call(prev, k)) {
              next[k] = prev[k];
            }
          }
          next[rowKey] = { status: "saving", error: null };
          return next;
        });

        apiFetch({
          path: "/clb/v1/products",
          method: "POST",
          data: payload,
        })
          .then(function (response) {
            var savedId = extractSavedId(response);
            if (savedId === null) {
              setRowStatus(function (prev) {
                var next = {};
                for (var k in prev) {
                  if (Object.prototype.hasOwnProperty.call(prev, k)) {
                    next[k] = prev[k];
                  }
                }
                next[rowKey] = {
                  status: "error",
                  error: __(
                    "保存は成功しましたが、レスポンスに商品 ID が含まれていません。",
                    DOMAIN,
                  ),
                };
                return next;
              });
              return;
            }

            setRowStatus(function (prev) {
              var next = {};
              for (var k in prev) {
                if (Object.prototype.hasOwnProperty.call(prev, k)) {
                  next[k] = prev[k];
                }
              }
              next[rowKey] = { status: "saved", error: null };
              return next;
            });

            onSelect(savedId, {
              title: typeof row.title === "string" ? row.title : "",
              provider: typeof row.provider === "string" ? row.provider : "",
            });
          })
          .catch(function (err) {
            var message = __("保存に失敗しました。", DOMAIN);
            if (err && typeof err.message === "string" && err.message !== "") {
              message = err.message;
            } else if (
              err &&
              err.error &&
              typeof err.error.message === "string"
            ) {
              message = err.error.message;
            }
            setRowStatus(function (prev) {
              var next = {};
              for (var k in prev) {
                if (Object.prototype.hasOwnProperty.call(prev, k)) {
                  next[k] = prev[k];
                }
              }
              next[rowKey] = { status: "error", error: message };
              return next;
            });
          });
      },
      [apiAvailable, onSelect],
    );

    var providerTabs = createElement(
      "div",
      {
        className: "clb-product-card-providers",
        role: "tablist",
        style: {
          display: "flex",
          gap: "6px",
          marginBottom: "10px",
          flexWrap: "wrap",
          alignItems: "center",
        },
      },
      PROVIDERS.map(function (p) {
        var isActive = p.key === providerKey;
        // Inline style overrides so the active provider is
        // unmistakable even when the WP admin .button-primary /
        // .button-secondary base styles render at low contrast in
        // the editor iframe (which lacks the wp-admin color
        // scheme).
        var activeStyle = {
          background: "#2271b1",
          color: "#fff",
          borderColor: "#2271b1",
          fontWeight: "700",
          boxShadow: "0 0 0 1px #2271b1",
        };
        var inactiveStyle = {
          background: "#f6f7f7",
          color: "#2c3338",
          borderColor: "#dcdcde",
          fontWeight: "500",
        };
        return createElement(
          "button",
          {
            key: "provider-" + p.key,
            type: "button",
            role: "tab",
            "aria-selected": isActive ? "true" : "false",
            className:
              "button " + (isActive ? "button-primary" : "button-secondary"),
            onClick: function () {
              setProviderKey(p.key);
            },
            style: isActive ? activeStyle : inactiveStyle,
            title: isActive
              ? __("現在の検索対象プロバイダー", DOMAIN)
              : __("このプロバイダーに切り替え", DOMAIN),
          },
          (isActive ? "✓ " : "") + p.label,
        );
      }),
    );

    var statusLine = null;
    if (results.status === "loading") {
      statusLine = createElement(
        "p",
        { className: "description" },
        __("検索中…", DOMAIN),
      );
    } else if (results.status === "error" && results.error !== null) {
      statusLine = createElement(
        "p",
        { className: "description", style: { color: "#b32d2e" } },
        results.error,
      );
    } else if (results.status === "ok" && results.items.length === 0) {
      statusLine = createElement(
        "p",
        { className: "description" },
        __("一致する商品が見つかりませんでした。", DOMAIN),
      );
    }

    var resultElements = results.items.map(function (row, idx) {
      var rowKey =
        (typeof row.provider === "string" ? row.provider : "?") +
        ":" +
        (typeof row.provider_item_id === "string" ? row.provider_item_id : "?");
      var status = rowStatus[rowKey] || { status: "idle", error: null };

      var imageUrl =
        typeof row.image_primary_url === "string" &&
        row.image_primary_url !== ""
          ? row.image_primary_url
          : null;
      var title = typeof row.title === "string" ? row.title : "(no title)";
      var brand =
        typeof row.brand === "string" && row.brand !== "" ? row.brand : null;
      var priceLabel =
        row.price &&
        typeof row.price.label === "string" &&
        row.price.label !== ""
          ? row.price.label
          : null;
      var detailUrl =
        row.links && typeof row.links.detail_url === "string"
          ? row.links.detail_url
          : null;

      var saveLabel = __("選択", DOMAIN);
      if (row.is_saved && typeof row.id === "number" && row.id > 0) {
        saveLabel = __("使用中 (#" + row.id + ")", DOMAIN);
      } else if (status.status === "saving") {
        saveLabel = __("保存中…", DOMAIN);
      } else if (status.status === "saved") {
        saveLabel = __("選択済み", DOMAIN);
      }

      return createElement(
        "li",
        {
          key: "result-" + idx + "-" + rowKey,
          className: "clb-product-card-search-result",
          style: {
            display: "flex",
            alignItems: "flex-start",
            gap: "8px",
            padding: "8px",
            borderBottom: "1px solid #eee",
          },
        },
        imageUrl !== null
          ? createElement("img", {
              src: imageUrl,
              alt: "",
              style: {
                width: "48px",
                height: "48px",
                objectFit: "cover",
                flex: "0 0 auto",
                borderRadius: "3px",
              },
            })
          : createElement("div", {
              style: {
                width: "48px",
                height: "48px",
                background: "#f0f0f0",
                flex: "0 0 auto",
                borderRadius: "3px",
              },
            }),
        createElement(
          "div",
          {
            style: {
              flex: "1 1 auto",
              minWidth: 0,
            },
          },
          createElement(
            "div",
            {
              style: {
                fontWeight: "600",
                overflow: "hidden",
                textOverflow: "ellipsis",
                whiteSpace: "nowrap",
              },
              title: title,
            },
            title,
          ),
          brand !== null
            ? createElement(
                "div",
                { className: "description", style: { fontSize: "12px" } },
                brand,
              )
            : null,
          priceLabel !== null
            ? createElement(
                "div",
                { style: { fontSize: "13px", marginTop: "2px" } },
                priceLabel,
              )
            : null,
          detailUrl !== null
            ? createElement(
                "div",
                { style: { fontSize: "11px", marginTop: "2px" } },
                createElement(
                  "a",
                  {
                    href: detailUrl,
                    target: "_blank",
                    rel: "noopener noreferrer",
                  },
                  __("プロバイダーで表示", DOMAIN),
                ),
              )
            : null,
          status.status === "error" && status.error !== null
            ? createElement(
                "div",
                {
                  className: "description",
                  style: { color: "#b32d2e", marginTop: "4px" },
                },
                status.error,
              )
            : null,
        ),
        createElement(
          "button",
          {
            type: "button",
            className: "button button-primary button-small",
            disabled: status.status === "saving" || status.status === "saved",
            onClick: function () {
              handleSave(row);
            },
            style: { flex: "0 0 auto", whiteSpace: "nowrap" },
          },
          saveLabel,
        ),
      );
    });

    return createElement(
      "div",
      {
        className: "clb-product-card-search-panel",
        style: {
          padding: "12px",
          border: "1px solid #ccd0d4",
          borderRadius: "3px",
          background: "#fff",
        },
      },
      createElement(
        "h4",
        { style: { marginTop: 0 } },
        __("Commerce Link Builder / 商品カード", DOMAIN),
      ),
      createElement(
        "p",
        { className: "description" },
        __(
          "ボタンでプロバイダーを選んで検索し、「選択」をクリックしてください。",
          DOMAIN,
        ),
      ),
      providerTabs,
      createElement(
        "div",
        {
          style: {
            display: "flex",
            gap: "6px",
            marginBottom: "8px",
          },
        },
        createElement("input", {
          ref: queryInputRef,
          type: "search",
          className: "regular-text",
          placeholder: __("キーワード…", DOMAIN),
          autoFocus: true,
          spellCheck: false,
          autoCorrect: "off",
          autoCapitalize: "off",
          autoComplete: "off",
          value: query,
          onChange: function (e) {
            setQuery(e.target.value);
          },
          onKeyDown: function (e) {
            if (e.key === "Enter") {
              e.preventDefault();
              runSearch();
            }
          },
          style: { flex: "1 1 auto" },
        }),
        createElement(
          "button",
          {
            type: "button",
            className: "button button-secondary",
            disabled: results.status === "loading",
            onClick: runSearch,
          },
          __("検索", DOMAIN),
        ),
      ),
      statusLine,
      resultElements.length > 0
        ? createElement(
            "ul",
            {
              className: "clb-product-card-search-results",
              style: {
                listStyle: "none",
                margin: "0 0 12px 0",
                padding: 0,
                maxHeight: "320px",
                overflowY: "auto",
                border: "1px solid #eee",
                borderRadius: "3px",
              },
            },
            resultElements,
          )
        : null,
      !apiAvailable
        ? createElement(
            "p",
            { className: "description", style: { fontStyle: "italic" } },
            __("このエディターホストでは REST 検索を利用できません。", DOMAIN),
          )
        : null,
    );
  }

  /**
   * Debounced REST search for already-saved products.
   */
  function useSavedProductSearch(query) {
    var initial = { items: [], status: "idle" };
    var s = useState(initial);
    var state = s[0];
    var setState = s[1];

    useEffect(
      function () {
        var trimmed = (query || "").trim();
        if (trimmed === "") {
          setState({ items: [], status: "idle" });
          return undefined;
        }

        if (!apiFetch || typeof apiFetch !== "function") {
          setState({ items: [], status: "unavailable" });
          return undefined;
        }

        setState({ items: [], status: "loading" });
        var cancelled = false;

        var timer = window.setTimeout(function () {
          var path =
            "/clb/v1/products?search=" +
            encodeURIComponent(trimmed) +
            "&per_page=" +
            SAVED_SEARCH_PAGE_SIZE;

          apiFetch({ path: path })
            .then(function (response) {
              if (cancelled) {
                return;
              }

              var rawItems = extractItems(response);
              var items = [];
              for (var i = 0; i < rawItems.length; i++) {
                var row = rawItems[i];
                if (!row) {
                  continue;
                }
                var id = parseInt(row.id, 10);
                if (isNaN(id) || id <= 0) {
                  continue;
                }
                items.push({
                  id: id,
                  title: typeof row.title === "string" ? row.title : "",
                  provider:
                    typeof row.provider === "string" ? row.provider : "",
                  image_primary_url:
                    typeof row.image_primary_url === "string"
                      ? row.image_primary_url
                      : "",
                });
              }

              setState({ items: items, status: "ok" });
            })
            .catch(function () {
              if (cancelled) {
                return;
              }
              setState({ items: [], status: "error" });
            });
        }, SEARCH_DEBOUNCE_MS);

        return function () {
          cancelled = true;
          window.clearTimeout(timer);
        };
      },
      [query],
    );

    return state;
  }

  /**
   * Render the saved-product picker used by the card block.
   */
  function SavedProductSearchPanel(props) {
    var onSelect = props.onSelect;
    var queryState = useState("");
    var query = queryState[0];
    var setQuery = queryState[1];
    var search = useSavedProductSearch(query);

    var statusLine = null;
    if (search.status === "loading") {
      statusLine = createElement(
        "p",
        { className: "description" },
        __("検索中…", DOMAIN),
      );
    } else if (search.status === "error") {
      statusLine = createElement(
        "p",
        { className: "description", style: { color: "#b32d2e" } },
        __(
          "検索に失敗しました。商品 REST エンドポイントに到達可能か、edit_posts 権限があるかを確認してください。",
          DOMAIN,
        ),
      );
    } else if (search.status === "unavailable") {
      statusLine = createElement(
        "p",
        { className: "description" },
        __(
          "REST API クライアント (wp.apiFetch) がこのエディターホストで読み込まれていません。",
          DOMAIN,
        ),
      );
    } else if (
      search.status === "ok" &&
      search.items.length === 0 &&
      query.trim() !== ""
    ) {
      statusLine = createElement(
        "p",
        { className: "description" },
        __("一致する商品が見つかりませんでした。", DOMAIN),
      );
    }

    var resultElements = search.items.map(function (result) {
      var label =
        (result.title || "#" + result.id) +
        " — " +
        (result.provider || __("不明", DOMAIN));
      var imageUrl =
        typeof result.image_primary_url === "string" &&
        result.image_primary_url !== ""
          ? result.image_primary_url
          : null;

      return createElement(
        "li",
        {
          key: "saved-result-" + result.id,
          className: "clb-product-card-saved-result",
          style: {
            display: "flex",
            alignItems: "flex-start",
            gap: "8px",
            padding: "6px",
            borderBottom: "1px solid #eee",
          },
        },
        imageUrl !== null
          ? createElement("img", {
              src: imageUrl,
              alt: "",
              style: {
                width: "40px",
                height: "40px",
                objectFit: "cover",
                flex: "0 0 auto",
                borderRadius: "3px",
              },
            })
          : createElement("div", {
              style: {
                width: "40px",
                height: "40px",
                background: "#f0f0f0",
                flex: "0 0 auto",
                borderRadius: "3px",
              },
            }),
        createElement(
          "span",
          {
            style: {
              flex: "1 1 auto",
              overflow: "hidden",
              textOverflow: "ellipsis",
            },
            title: label,
          },
          label,
        ),
        createElement(
          "button",
          {
            type: "button",
            className: "button button-small",
            onClick: function () {
              onSelect(result.id, {
                title: result.title,
                provider: result.provider,
              });
            },
            style: { flex: "0 0 auto" },
          },
          __("選択", DOMAIN),
        ),
      );
    });

    return createElement(
      "div",
      {
        className: "clb-product-card-saved-search",
        style: {
          padding: "12px",
          border: "1px dashed #ccd0d4",
          borderRadius: "3px",
          background: "#fafafa",
          marginTop: "12px",
        },
      },
      createElement(
        "h5",
        { style: { marginTop: 0 } },
        __("登録済み商品", DOMAIN),
      ),
      createElement("input", {
        type: "search",
        className: "regular-text",
        placeholder: __("タイトルで登録済み商品を検索…", DOMAIN),
        spellCheck: false,
        autoCorrect: "off",
        autoCapitalize: "off",
        autoComplete: "off",
        value: query,
        onChange: function (e) {
          setQuery(e.target.value);
        },
        style: { width: "100%", marginBottom: "8px" },
      }),
      statusLine,
      resultElements.length > 0
        ? createElement(
            "ul",
            {
              className: "clb-product-card-saved-results",
              style: {
                listStyle: "none",
                margin: "0",
                padding: 0,
                maxHeight: "200px",
                overflowY: "auto",
                border: "1px solid #eee",
                borderRadius: "3px",
              },
            },
            resultElements,
          )
        : null,
    );
  }

  /**
   * Render the "selected product" summary card the editor shows
   * after a successful save (or after the editor opens a post
   * that already has `productId` persisted).
   */
  function SelectedProductSummary(props) {
    var idValue = props.idValue;
    var summary = props.summary;
    var onClear = props.onClear;
    var attributes = props.attributes || {};

    // Subset of block.json attributes that PreviewProductCardUseCase
    // actually honors (§10.1 permissive whitelist).
    var template =
      typeof attributes.template === "string" && attributes.template !== ""
        ? attributes.template
        : "single-default-card";
    var showPrice = attributes.showPrice !== false;
    var showBrand = attributes.showBrand !== false;
    var showDescription = attributes.showDescription === true;
    var showProviderBadge = attributes.showProviderBadge === true;

    var previewState = useState({
      status: "idle",
      html: null,
      error: null,
    });
    var preview = previewState[0];
    var setPreview = previewState[1];

    useEffect(
      function () {
        if (typeof apiFetch !== "function" || !idValue) {
          return;
        }
        var cancelled = false;
        setPreview({ status: "loading", html: null, error: null });
        apiFetch({
          path: "/clb/v1/editor/preview",
          method: "POST",
          data: {
            kind: "single",
            productIds: [String(idValue)],
            template: template,
            attrs: {
              show_price: showPrice,
              show_brand: showBrand,
              show_description: showDescription,
              show_provider_badge: showProviderBadge,
            },
          },
        })
          .then(function (response) {
            if (cancelled) {
              return;
            }
            var html =
              response &&
              response.data &&
              typeof response.data.html === "string"
                ? response.data.html
                : null;
            if (html === null || html === "") {
              setPreview({
                status: "error",
                html: null,
                error: __("プレビューHTMLを取得できませんでした。", DOMAIN),
              });
              return;
            }
            setPreview({ status: "ready", html: html, error: null });
          })
          .catch(function (err) {
            if (cancelled) {
              return;
            }
            var msg =
              err && err.message
                ? err.message
                : __("プレビューの取得に失敗しました。", DOMAIN);
            setPreview({ status: "error", html: null, error: msg });
          });
        return function () {
          cancelled = true;
        };
      },
      [
        idValue,
        template,
        showPrice,
        showBrand,
        showDescription,
        showProviderBadge,
      ],
    );

    var fallbackLabel =
      summary && summary.title && summary.title !== ""
        ? summary.title + " (#" + idValue + ")"
        : __("保存済み商品 #" + idValue, DOMAIN);

    var previewBody;
    if (preview.status === "ready" && preview.html !== null) {
      previewBody = createElement("div", {
        className: "clb-product-card-preview",
        style: { margin: "0 0 12px 0" },
        dangerouslySetInnerHTML: { __html: preview.html },
      });
    } else if (preview.status === "loading") {
      previewBody = createElement(
        "p",
        { className: "description", style: { margin: "0 0 12px 0" } },
        __("プレビューを読み込み中…", DOMAIN),
      );
    } else if (preview.status === "error") {
      previewBody = createElement(
        "div",
        { style: { margin: "0 0 12px 0" } },
        createElement(
          "p",
          { style: { fontWeight: "600", margin: "0 0 4px 0" } },
          fallbackLabel,
        ),
        createElement(
          "p",
          {
            className: "description",
            style: { margin: 0, color: "#b32d2e" },
          },
          preview.error,
        ),
      );
    } else {
      // apiFetch unavailable (e.g., very old editor host).
      previewBody = createElement(
        "p",
        { style: { fontWeight: "600", margin: "0 0 12px 0" } },
        fallbackLabel,
      );
    }

    return createElement(
      "div",
      {
        className: "clb-product-card-selected",
        style: {
          padding: "12px",
          border: "1px solid #46b450",
          borderRadius: "3px",
          background: "#f4faf5",
        },
      },
      createElement(
        "h4",
        { style: { marginTop: 0 } },
        __("Commerce Link Builder / 商品カード", DOMAIN),
      ),
      previewBody,
      createElement(
        "button",
        {
          type: "button",
          className: "button button-secondary button-small",
          onClick: onClear,
        },
        __("商品を変更", DOMAIN),
      ),
    );
  }

  /**
   * Render the Inspector sidebar panel for `clb/product-card`
   * (KI-015): template selector + show-flag toggles.
   *
   * Returns null when wp.blockEditor / wp.components are not
   * available (very old editor host or preview iframe). The
   * editor still works without the sidebar; the block just
   * falls back to the attribute defaults declared in block.json.
   */
  function InspectorPanel(props) {
    if (!blockEditor || !components) {
      return null;
    }
    var InspectorControls = blockEditor.InspectorControls;
    var PanelBody = components.PanelBody;
    var SelectControl = components.SelectControl;
    var ToggleControl = components.ToggleControl;
    if (
      typeof InspectorControls !== "function" ||
      typeof PanelBody !== "function" ||
      typeof SelectControl !== "function" ||
      typeof ToggleControl !== "function"
    ) {
      return null;
    }

    var attributes = props.attributes || {};
    var setAttributes = props.setAttributes || function () {};

    function toggle(attrKey, label, help) {
      return createElement(ToggleControl, {
        key: "toggle-" + attrKey,
        label: label,
        help: help,
        checked: attributes[attrKey] !== false,
        onChange: function (next) {
          var update = {};
          update[attrKey] = !!next;
          setAttributes(update);
        },
      });
    }

    return createElement(
      InspectorControls,
      {},
      createElement(
        PanelBody,
        {
          title: __("テンプレート", DOMAIN),
          initialOpen: true,
        },
        createElement(SelectControl, {
          label: __("カードテンプレート", DOMAIN),
          help: __(
            "表示時にサーバー側で描画されるビジュアルバリエーションを選択します。",
            DOMAIN,
          ),
          value:
            typeof attributes.template === "string" &&
            attributes.template !== ""
              ? attributes.template
              : "single-default-card",
          options: TEMPLATE_OPTIONS,
          onChange: function (next) {
            setAttributes({ template: String(next) });
          },
        }),
      ),
      createElement(
        PanelBody,
        {
          title: __("表示オプション", DOMAIN),
          initialOpen: true,
        },
        toggle(
          "showPrice",
          __("価格を表示", DOMAIN),
          __("タイトル下にスナップショットの価格ラベルを表示します。", DOMAIN),
        ),
        toggle(
          "showBrand",
          __("ブランドを表示", DOMAIN),
          __("タイトル下にブランド名を表示します。", DOMAIN),
        ),
        toggle(
          "showDescription",
          __("説明を表示", DOMAIN),
          __(
            "スナップショットの説明ブロックを表示します (デフォルトではオフ、カードをコンパクトに保つため)。",
            DOMAIN,
          ),
        ),
        toggle(
          "showProviderBadge",
          __("プロバイダーバッジを表示", DOMAIN),
          __(
            "それぞれの商品に「Amazon」 / 「楽天」 / 「Yahoo!」バッジを付与します。",
            DOMAIN,
          ),
        ),
        toggle(
          "showDisclaimer",
          __("ディスクレーマーを表示", DOMAIN),
          __(
            "アフィリエイト記載上のディスクレーマーを表示します (推奨、デフォルトオン)。",
            DOMAIN,
          ),
        ),
      ),
      createElement(
        PanelBody,
        {
          title: __("リンク設定", DOMAIN),
          initialOpen: false,
        },
        toggle(
          "openInNewTab",
          __("新しいタブで開く", DOMAIN),
          __('アフィリエイトリンクに target="_blank" を付与します。', DOMAIN),
        ),
        toggle(
          "nofollow",
          __('rel="nofollow" を追加', DOMAIN),
          __('アフィリエイトリンクに rel="nofollow" を付与します。', DOMAIN),
        ),
        toggle(
          "sponsored",
          __('rel="sponsored" を追加', DOMAIN),
          __('アフィリエイトリンクに rel="sponsored" を付与します。', DOMAIN),
        ),
        toggle(
          "trackingEnabled",
          __("トラッキングを有効化", DOMAIN),
          __(
            "アフィリエイト URL に設定済みのトラッキングパラメータを付与します。",
            DOMAIN,
          ),
        ),
      ),
    );
  }

  function ProductCardEditor(props) {
    var attributes = props.attributes || {};
    var setAttributes = props.setAttributes || function () {};

    // Gutenberg's `useBlockProps()` returns the props (including a
    // canonical data-block id, role, draggable handlers, focus /
    // selection behaviour) the block-editor needs to attach to the
    // outermost element of edit() so the block is selectable,
    // movable and deletable like any core block. Without it the
    // block renders correctly but the toolbar / mover / Delete-key
    // affordances do not engage. Falls back to a plain props bag
    // when wp.blockEditor is unavailable (preview iframe / very
    // old editor host) so the surface still renders.
    var useBlockProps =
      blockEditor && typeof blockEditor.useBlockProps === "function"
        ? blockEditor.useBlockProps
        : null;
    var rootProps = useBlockProps
      ? useBlockProps({ className: "clb-product-card-editor" })
      : { className: "clb-product-card-editor" };

    var currentId = normalizeId(attributes.productId);

    // Cache the most recently selected row's title / provider so
    // the summary card can render something better than just
    // "#N" right after the save round-trip. Not persisted into
    // the post -- a fresh editor session for an existing post
    // renders the summary as "Saved product #N" until the
    // front-end render call picks up the live snapshot.
    var summaryState = useState(null);
    var summary = summaryState[0];
    var setSummary = summaryState[1];

    var handleSelect = useCallback(
      function (id, summaryFromRow) {
        setAttributes({ productId: id });
        setSummary(summaryFromRow);
      },
      [setAttributes],
    );

    var handleClear = useCallback(
      function () {
        setAttributes({ productId: undefined });
        setSummary(null);
      },
      [setAttributes],
    );

    if (currentId !== null) {
      return createElement(
        Fragment,
        {},
        createElement(InspectorPanel, {
          attributes: attributes,
          setAttributes: setAttributes,
        }),
        createElement(
          "div",
          rootProps,
          createElement(SelectedProductSummary, {
            idValue: currentId,
            summary: summary,
            attributes: attributes,
            onClear: handleClear,
          }),
        ),
      );
    }

    return createElement(
      Fragment,
      {},
      createElement(InspectorPanel, {
        attributes: attributes,
        setAttributes: setAttributes,
      }),
      createElement(
        "div",
        rootProps,
        createElement(SearchPanel, { onSelect: handleSelect }),
        createElement(SavedProductSearchPanel, { onSelect: handleSelect }),
      ),
    );
  }

  // Editor host predates the hooks API on wp.element: register a
  // minimal placeholder so the block still appears in the
  // inserter rather than failing silently.
  if (typeof useState !== "function" || typeof useCallback !== "function") {
    blocks.registerBlockType("clb/product-card", {
      edit: function () {
        return createElement(
          "div",
          { className: "clb-product-card-editor-placeholder" },
          __(
            "Commerce Link Builder / 商品カード (レガシーエディター — 検索 UI を利用するには WordPress を更新してください)。",
            DOMAIN,
          ),
        );
      },
      save: function () {
        return null;
      },
    });
    return;
  }

  blocks.registerBlockType("clb/product-card", {
    edit: ProductCardEditor,
    save: function () {
      // Server-side rendered: no static save markup.
      return null;
    },
  });
})();
