// Phase 2 strict-review fix #3 (KI-013): editor-side surface for
// `affilun/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 for enabled providers
//      (Amazon / 楽天市場 / Yahoo!ショッピング / メルカリ) plus a keyword input.
//   2. On Search the editor calls `GET /affilun/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 `/affilun/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 /affilun/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. 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 selected-product preview now delegates to the
//     shared editor-preview helper so the REST call reaches the same
//     server-side renderer as the front-end dynamic block.
( 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 data = wp.data || null;
	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 = 'affilun';
	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' },
	];

	var DESIGN_PRESET_OPTIONS = [
		{ value: 'default', label: '標準' },
		{ value: 'flat', label: 'フラット' },
		{ value: 'outlined', label: 'アウトライン' },
		{ value: 'soft', label: 'ソフト' },
	];

	var COLOR_SCHEME_OPTIONS = [
		{ value: 'light', label: 'ライト' },
		{ value: 'dark', label: 'ダーク' },
		{ value: 'system', label: 'OS設定に連動' },
	];

	var BUTTON_LAYOUT_OPTIONS = [
		{ value: 'wrap', label: '横並び' },
		{ value: 'stacked', label: '縦積み' },
		{ value: 'full', label: '全幅' },
	];

	var BUTTON_SHAPE_OPTIONS = [
		{ value: 'square', label: '角丸なし' },
		{ value: 'rounded', label: '標準' },
		{ value: 'pill', label: 'ピル型' },
	];

	var BUTTON_TONE_OPTIONS = [
		{ value: 'brand', label: 'ブランドカラー' },
		{ value: 'muted', label: '控えめ' },
		{ value: 'monochrome', label: 'モノクロ' },
	];

	// 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!ショッピング' },
		{ key: 'mercari', label: 'メルカリ' },
	];

	/**
	 * 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.
	 */
	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 `/affilun/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 providerLabelForKey( providerKey ) {
		for ( var i = 0; i < PROVIDERS.length; i++ ) {
			if ( PROVIDERS[ i ].key === providerKey ) {
				return PROVIDERS[ i ].label;
			}
		}
		return providerKey;
	}

	function resultRowKey( row ) {
		return (
			( row && typeof row.provider === 'string' ? row.provider : '?' ) +
			':' +
			( row && typeof row.provider_item_id === 'string'
				? row.provider_item_id
				: '?' )
		);
	}

	function providerUrlPayloadFromRow( 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 detailUrl =
			row.links && typeof row.links.detail_url === 'string'
				? row.links.detail_url
				: '';
		if ( provider === '' || detailUrl === '' ) {
			return null;
		}
		var payload = {
			provider_type: provider,
			url: detailUrl,
			source: 'user',
		};
		if ( providerItemId !== '' ) {
			payload.provider_item_id = providerItemId;
		}
		return payload;
	}

	function buildSavePayload( row, titleOverride ) {
		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 : '';
		if (
			typeof titleOverride === 'string' &&
			titleOverride.trim() !== ''
		) {
			title = titleOverride.trim();
		}
		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,
		};
	}

	function normalizeEditorText( value ) {
		if ( typeof value === 'string' ) {
			return value;
		}
		if ( value && typeof value === 'object' ) {
			if ( typeof value.raw === 'string' ) {
				return value.raw;
			}
			if ( typeof value.rendered === 'string' ) {
				return value.rendered;
			}
		}
		return '';
	}

	function selectEditedPostStore() {
		if ( ! data || typeof data.select !== 'function' ) {
			return null;
		}

		try {
			var editor = data.select( 'core/editor' );
			if (
				editor &&
				typeof editor.getEditedPostAttribute === 'function'
			) {
				return editor;
			}
		} catch ( ignored ) {}

		try {
			var coreEditor = data.select( 'core/edit-post' );
			if (
				coreEditor &&
				typeof coreEditor.getEditedPostAttribute === 'function'
			) {
				return coreEditor;
			}
		} catch ( ignored ) {}

		return null;
	}

	function getEditedPostPayload() {
		var payload = { title: '', content: '' };
		var editor = selectEditedPostStore();
		if ( ! editor ) {
			return payload;
		}

		payload.title = normalizeEditorText(
			editor.getEditedPostAttribute( 'title' )
		);
		payload.content = normalizeEditorText(
			editor.getEditedPostAttribute( 'content' )
		);
		return payload;
	}

	function customButtonsToText( buttons ) {
		if ( ! Array.isArray( buttons ) ) {
			return '';
		}
		return buttons
			.filter( function ( button ) {
				return button && typeof button === 'object';
			} )
			.map( function ( button ) {
				var label =
					typeof button.label === 'string' ? button.label : '';
				var url = typeof button.url === 'string' ? button.url : '';
				var target =
					typeof button.target === 'string' ? button.target : '';
				var rel = typeof button.rel === 'string' ? button.rel : '';
				if ( target !== '' || rel !== '' ) {
					return label + '|' + url + '|' + target + '|' + rel;
				}
				return label + '|' + url;
			} )
			.join( '\n' );
	}

	function customButtonsFromText( value ) {
		if ( typeof value !== 'string' ) {
			return [];
		}
		return value
			.split( '\n' )
			.map( function ( line ) {
				var parts = line.split( '|' );
				var label = ( parts.shift() || '' ).trim();
				var url = ( parts.shift() || '' ).trim();
				var target = ( parts.shift() || '' ).trim();
				var rel = parts.join( '|' ).trim();
				return { label: label, url: url, target: target, rel: rel };
			} )
			.filter( function ( button ) {
				return button.label !== '' || button.url !== '';
			} );
	}

	function extractionData( response ) {
		return response && response.data && typeof response.data === 'object'
			? response.data
			: response || {};
	}

	function extractKeywordsFromResponse( response ) {
		var data = extractionData( response );
		if ( typeof data.search_keywords === 'string' ) {
			return data.search_keywords;
		}
		return '';
	}

	function extractStringListFromResponse( response, key ) {
		var data = extractionData( response );
		if ( ! Array.isArray( data[ key ] ) ) {
			return [];
		}
		return data[ key ].filter( function ( value ) {
			return typeof value === 'string' && value.trim() !== '';
		} );
	}

	function extractButtonLabelSuggestions( response ) {
		var data = extractionData( response );
		return data.button_label_suggestions &&
			typeof data.button_label_suggestions === 'object'
			? data.button_label_suggestions
			: {};
	}

	function messageFromAiExtractionError( err, fallback ) {
		var code = '';
		if ( err && typeof err.code === 'string' ) {
			code = err.code;
		} else if ( err && err.error && typeof err.error.code === 'string' ) {
			code = err.error.code;
		}

		var messages = {
			affilun_cloud_not_configured:
				'AI機能を使うには affilun Cloud を設定してください。設定画面で保存してください。',
			affilun_validation_failed:
				'投稿タイトルまたは本文を入力してから抽出してください。',
			affilun_cloud_plan_required:
				'この AI 機能を使うには affilun Cloud のプラン変更が必要です。',
			affilun_cloud_site_mismatch:
				'affilun Cloud の Site ID または API Key が現在のサイトと一致しません。設定画面で確認してください。',
			affilun_cloud_quota_exhausted:
				'affilun Cloud の利用上限に達しました。プランまたは利用状況を確認してください。',
			affilun_cloud_invalid_response:
				'affilun Cloud の応答を解析できませんでした。もう一度お試しください。',
			affilun_cloud_unavailable:
				'affilun Cloud に接続できませんでした。しばらく待ってから再試行してください。',
		};

		if ( code && messages[ code ] ) {
			return messages[ code ];
		}
		if ( err && typeof err.message === 'string' && err.message !== '' ) {
			return err.message;
		}
		if ( err && err.error && typeof err.error.message === 'string' ) {
			return err.error.message;
		}
		return fallback;
	}

	/**
	 * Extract a positive integer product id from the §4.2 success
	 * envelope returned by `POST /affilun/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 attributes = props.attributes || {};
		var setAttributes = props.setAttributes || function () {};

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

		var enabledProvidersState = useState( PROVIDERS );
		var enabledProviders = enabledProvidersState[ 0 ];
		var setEnabledProviders = enabledProvidersState[ 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 titleOverridesState = useState( {} );
		var titleOverrides = titleOverridesState[ 0 ];
		var setTitleOverrides = titleOverridesState[ 1 ];

		var associationState = useState( null );
		var association = associationState[ 0 ];
		var setAssociation = associationState[ 1 ];

		var cloudState = useState( { status: 'idle', configured: false } );
		var cloud = cloudState[ 0 ];
		var setCloud = cloudState[ 1 ];

		var aiExtractionState = useState( {
			status: 'idle',
			error: null,
			candidates: [],
			microcopySuggestions: [],
			buttonLabelSuggestions: {},
		} );
		var aiExtraction = aiExtractionState[ 0 ];
		var setAiExtraction = aiExtractionState[ 1 ];

		var apiAvailable = typeof apiFetch === 'function';

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

		useEffect(
			function () {
				if ( ! apiAvailable ) {
					return;
				}
				var cancelled = false;
				apiFetch( { path: '/affilun/v1/settings/public' } )
					.then( function ( response ) {
						if ( cancelled ) {
							return;
						}
						var publicSettings =
							response && response.data ? response.data : {};
						var enabledProviderKeys = Array.isArray(
							publicSettings.enabled_providers
						)
							? publicSettings.enabled_providers
							: [];
						var visibleProviders = PROVIDERS.filter(
							function ( provider ) {
								return (
									enabledProviderKeys.indexOf(
										provider.key
									) !== -1
								);
							}
						);
						if ( visibleProviders.length === 0 ) {
							visibleProviders = PROVIDERS;
						}
						setEnabledProviders( visibleProviders );
						setProviderKey( function ( currentProviderKey ) {
							return visibleProviders.some(
								function ( provider ) {
									return provider.key === currentProviderKey;
								}
							)
								? currentProviderKey
								: visibleProviders[ 0 ].key;
						} );

						var cloudConfigured = !! (
							publicSettings.cloud &&
							publicSettings.cloud.configured
						);
						setCloud( {
							status: 'ready',
							configured: cloudConfigured,
							source: cloudConfigured ? 'cloud' : '',
						} );
					} )
					.catch( function () {
						if ( ! cancelled ) {
							setCloud( { status: 'error', configured: false } );
						}
					} );
				return function () {
					cancelled = true;
				};
			},
			[ apiAvailable ]
		);

		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 =
					'/affilun/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 runAiKeywordExtraction = useCallback(
			function () {
				if ( ! apiAvailable ) {
					setAiExtraction( {
						status: 'error',
						error: __(
							'REST API クライアントが読み込まれていません。',
							DOMAIN
						),
					} );
					return;
				}

				var postPayload = getEditedPostPayload();
				if (
					postPayload.title.trim() === '' &&
					postPayload.content.trim() === ''
				) {
					setAiExtraction( {
						status: 'error',
						error: __(
							'投稿タイトルまたは本文を入力してから抽出してください。',
							DOMAIN
						),
					} );
					return;
				}

				setAiExtraction( {
					status: 'loading',
					error: null,
					candidates: [],
					microcopySuggestions: [],
					buttonLabelSuggestions: {},
				} );
				apiFetch( {
					path: '/affilun/v1/settings/ai/extract-keywords',
					method: 'POST',
					data: postPayload,
				} )
					.then( function ( response ) {
						var keywords =
							extractKeywordsFromResponse( response ).trim();
						if ( keywords === '' ) {
							setAiExtraction( {
								status: 'error',
								error: __(
									'キーワードを抽出できませんでした。',
									DOMAIN
								),
							} );
							return;
						}
						setQuery( keywords );
						setAiExtraction( {
							status: 'ok',
							error: null,
							candidates: extractStringListFromResponse(
								response,
								'candidates'
							),
							microcopySuggestions: extractStringListFromResponse(
								response,
								'microcopy_suggestions'
							),
							buttonLabelSuggestions:
								extractButtonLabelSuggestions( response ),
						} );
						if (
							queryInputRef &&
							queryInputRef.current &&
							typeof queryInputRef.current.focus === 'function'
						) {
							queryInputRef.current.focus();
						}
					} )
					.catch( function ( err ) {
						setAiExtraction( {
							status: 'error',
							error: messageFromAiExtractionError(
								err,
								__(
									'AIキーワード抽出に失敗しました。',
									DOMAIN
								)
							),
						} );
					} );
			},
			[ apiAvailable ]
		);

		function startAssociation( row ) {
			var queries = {};
			var baseQuery = ( query || '' ).trim();
			enabledProviders.forEach( function ( provider ) {
				if ( provider.key !== row.provider ) {
					queries[ provider.key ] = baseQuery;
				}
			} );
			setAssociation( {
				primary: row,
				queries: queries,
				results: {},
				selected: {},
				status: 'editing',
				error: null,
			} );

			if ( apiAvailable && baseQuery !== '' ) {
				enabledProviders.forEach( function ( provider ) {
					if ( provider.key === row.provider ) {
						return;
					}
					setAssociationProviderResult( provider.key, {
						status: 'loading',
						items: [],
						error: null,
					} );
					apiFetch( {
						path:
							'/affilun/v1/products/search?provider=' +
							encodeURIComponent( provider.key ) +
							'&keywords=' +
							encodeURIComponent( baseQuery ) +
							'&per_page=10',
					} )
						.then( function ( response ) {
							setAssociationProviderResult( provider.key, {
								status: 'ok',
								items: extractItems( response ),
								error: null,
							} );
						} )
						.catch( function ( err ) {
							var message = __( '検索に失敗しました。', DOMAIN );
							if (
								err &&
								typeof err.message === 'string' &&
								err.message !== ''
							) {
								message = err.message;
							}
							setAssociationProviderResult( provider.key, {
								status: 'error',
								items: [],
								error: message,
							} );
						} );
				} );
			}
		}

		function updateAssociationQuery( providerKey, value ) {
			setAssociation( function ( prev ) {
				if ( ! prev ) {
					return prev;
				}
				var nextQueries = {};
				Object.keys( prev.queries || {} ).forEach( function ( key ) {
					nextQueries[ key ] = prev.queries[ key ];
				} );
				nextQueries[ providerKey ] = value;
				return {
					primary: prev.primary,
					queries: nextQueries,
					results: prev.results || {},
					selected: prev.selected || {},
					status: prev.status || 'editing',
					error: prev.error || null,
				};
			} );
		}

		function setAssociationProviderResult( providerKey, result ) {
			setAssociation( function ( prev ) {
				if ( ! prev ) {
					return prev;
				}
				var nextResults = {};
				Object.keys( prev.results || {} ).forEach( function ( key ) {
					nextResults[ key ] = prev.results[ key ];
				} );
				nextResults[ providerKey ] = result;
				return {
					primary: prev.primary,
					queries: prev.queries || {},
					results: nextResults,
					selected: prev.selected || {},
					status: prev.status || 'editing',
					error: prev.error || null,
				};
			} );
		}

		function runAssociationSearch( providerKey ) {
			if ( ! association || ! apiAvailable ) {
				return;
			}
			var trimmed = (
				( association.queries || {} )[ providerKey ] || ''
			).trim();
			if ( trimmed === '' ) {
				setAssociationProviderResult( providerKey, {
					status: 'error',
					items: [],
					error: __( '検索キーワードを入力してください。', DOMAIN ),
				} );
				return;
			}
			setAssociationProviderResult( providerKey, {
				status: 'loading',
				items: [],
				error: null,
			} );
			apiFetch( {
				path:
					'/affilun/v1/products/search?provider=' +
					encodeURIComponent( providerKey ) +
					'&keywords=' +
					encodeURIComponent( trimmed ) +
					'&per_page=10',
			} )
				.then( function ( response ) {
					setAssociationProviderResult( providerKey, {
						status: 'ok',
						items: extractItems( response ),
						error: null,
					} );
				} )
				.catch( function ( err ) {
					var message = __( '検索に失敗しました。', DOMAIN );
					if (
						err &&
						typeof err.message === 'string' &&
						err.message !== ''
					) {
						message = err.message;
					}
					setAssociationProviderResult( providerKey, {
						status: 'error',
						items: [],
						error: message,
					} );
				} );
		}

		function chooseAssociationRow( providerKey, row ) {
			setAssociation( function ( prev ) {
				if ( ! prev ) {
					return prev;
				}
				var nextSelected = {};
				Object.keys( prev.selected || {} ).forEach( function ( key ) {
					nextSelected[ key ] = prev.selected[ key ];
				} );
				nextSelected[ providerKey ] = row;
				return {
					primary: prev.primary,
					queries: prev.queries || {},
					results: prev.results || {},
					selected: nextSelected,
					status: prev.status || 'editing',
					error: null,
				};
			} );
		}

		function confirmAssociation() {
			if ( ! association || ! apiAvailable ) {
				return;
			}
			var primary = association.primary;
			var primaryKey = resultRowKey( primary );
			var payload = buildSavePayload(
				primary,
				titleOverrides[ primaryKey ]
			);
			if ( payload === null ) {
				setAssociation( function ( prev ) {
					if ( ! prev ) {
						return prev;
					}
					return {
						primary: prev.primary,
						queries: prev.queries || {},
						results: prev.results || {},
						selected: prev.selected || {},
						status: 'error',
						error: __(
							'選択した商品に必須フィールドがありません。',
							DOMAIN
						),
					};
				} );
				return;
			}

			var providerUrls = [];
			var primaryUrl = providerUrlPayloadFromRow( primary );
			if ( primaryUrl !== null ) {
				providerUrls.push( primaryUrl );
			}
			Object.keys( association.selected || {} ).forEach(
				function ( providerKey ) {
					var urlPayload = providerUrlPayloadFromRow(
						association.selected[ providerKey ]
					);
					if ( urlPayload !== null ) {
						providerUrls.push( urlPayload );
					}
				}
			);
			payload.provider_urls = providerUrls;

			setAssociation( function ( prev ) {
				if ( ! prev ) {
					return prev;
				}
				return {
					primary: prev.primary,
					queries: prev.queries || {},
					results: prev.results || {},
					selected: prev.selected || {},
					status: 'saving',
					error: null,
				};
			} );

			apiFetch( {
				path: '/affilun/v1/products',
				method: 'POST',
				data: payload,
			} )
				.then( function ( response ) {
					var savedId = extractSavedId( response );
					if ( savedId === null ) {
						throw new Error(
							__(
								'保存は成功しましたが、商品 ID が含まれていません。',
								DOMAIN
							)
						);
					}
					setAssociation( null );
					onSelect( savedId, {
						title:
							typeof primary.title === 'string'
								? primary.title
								: '',
						provider:
							typeof primary.provider === 'string'
								? primary.provider
								: '',
					} );
				} )
				.catch( function ( err ) {
					var message = __( '保存に失敗しました。', DOMAIN );
					if (
						err &&
						typeof err.message === 'string' &&
						err.message !== ''
					) {
						message = err.message;
					}
					setAssociation( function ( prev ) {
						if ( ! prev ) {
							return prev;
						}
						return {
							primary: prev.primary,
							queries: prev.queries || {},
							results: prev.results || {},
							selected: prev.selected || {},
							status: 'error',
							error: message,
						};
					} );
				} );
		}

		var applyButtonLabelSuggestions = useCallback(
			function ( suggestions ) {
				var nextLabels = {};
				var current =
					attributes.buttonLabels &&
					typeof attributes.buttonLabels === 'object'
						? attributes.buttonLabels
						: {};
				Object.keys( current ).forEach( function ( key ) {
					nextLabels[ key ] = current[ key ];
				} );
				Object.keys( suggestions || {} ).forEach( function ( key ) {
					if (
						typeof suggestions[ key ] === 'string' &&
						suggestions[ key ] !== ''
					) {
						nextLabels[ key ] = suggestions[ key ];
					}
				} );
				setAttributes( { buttonLabels: nextLabels } );
			},
			[ attributes.buttonLabels, setAttributes ]
		);

		var handleSave = useCallback(
			function ( row ) {
				var hasAssociationProvider = enabledProviders.some(
					function ( provider ) {
						return provider.key !== row.provider;
					}
				);
				if ( hasAssociationProvider ) {
					startAssociation( row );
					return;
				}

				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, titleOverrides[ rowKey ] );
				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: '/affilun/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, titleOverrides, enabledProviders, query ]
		);

		var aiKeywordButton = cloud.configured
			? createElement(
					'button',
					{
						key: 'ai-keyword-extract',
						type: 'button',
						className: 'button button-secondary',
						disabled: aiExtraction.status === 'loading',
						onClick: runAiKeywordExtraction,
						style: { marginLeft: 'auto' },
					},
					aiExtraction.status === 'loading'
						? __( 'AI提案を生成中...', DOMAIN )
						: __( 'AIキーワード抽出', DOMAIN )
			  )
			: null;

		var providerTabs = createElement(
			'div',
			{
				className: 'affilun-product-card-providers',
				role: 'tablist',
				style: {
					display: 'flex',
					gap: '6px',
					marginBottom: '10px',
					flexWrap: 'wrap',
					alignItems: 'center',
				},
			},
			enabledProviders
				.map( function ( p ) {
					var isActive = p.key === providerKey;
					var activeStyle = {
						background: 'linear-gradient(135deg, #7c6cff, #ff8ec7)',
						color: '#fff',
						borderColor: 'transparent',
						borderRadius: '999px',
						fontWeight: '800',
						boxShadow: '0 12px 28px rgba(124,108,255,0.24)',
					};
					var inactiveStyle = {
						background: '#f5f6fa',
						color: '#2a2d3e',
						borderColor: 'transparent',
						borderRadius: '999px',
						fontWeight: '800',
					};
					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
					);
				} )
				.concat( aiKeywordButton ? [ aiKeywordButton ] : [] )
		);

		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 aiStatusLine = null;
		if ( aiExtraction.status === 'error' && aiExtraction.error !== null ) {
			aiStatusLine = createElement(
				'p',
				{ className: 'description', style: { color: '#b32d2e' } },
				aiExtraction.error
			);
		} else if ( aiExtraction.status === 'ok' ) {
			aiStatusLine = createElement(
				'p',
				{ className: 'description' },
				__(
					'AI提案を生成しました。検索キーワードを入力欄に設定しました。',
					DOMAIN
				)
			);
		}

		var aiSuggestionPanel = null;
		if ( aiExtraction.status === 'ok' ) {
			var candidateButtons = ( aiExtraction.candidates || [] ).map(
				function ( candidate ) {
					return createElement(
						'button',
						{
							key: 'candidate-' + candidate,
							type: 'button',
							className: 'button button-small',
							onClick: function () {
								setQuery( candidate );
							},
						},
						candidate
					);
				}
			);
			var microcopyButtons = (
				aiExtraction.microcopySuggestions || []
			).map( function ( copy ) {
				return createElement(
					'button',
					{
						key: 'microcopy-' + copy,
						type: 'button',
						className: 'button button-small',
						onClick: function () {
							setAttributes( { microcopyText: copy } );
						},
					},
					__( '訴求文を適用: ', DOMAIN ) + copy
				);
			} );
			var labelSuggestions = aiExtraction.buttonLabelSuggestions || {};
			var hasLabelSuggestions =
				Object.keys( labelSuggestions ).length > 0;
			var suggestionSections = [];
			if ( candidateButtons.length > 0 ) {
				suggestionSections.push(
					createElement(
						'div',
						{
							key: 'keyword-section',
							style: { marginBottom: '8px' },
						},
						createElement(
							'strong',
							{
								style: {
									display: 'block',
									marginBottom: '4px',
								},
							},
							__( '検索キーワード候補', DOMAIN )
						),
						candidateButtons
					)
				);
			}
			if ( microcopyButtons.length > 0 ) {
				suggestionSections.push(
					createElement(
						'div',
						{
							key: 'microcopy-section',
							style: { marginBottom: '8px' },
						},
						createElement(
							'strong',
							{
								style: {
									display: 'block',
									marginBottom: '4px',
								},
							},
							__( '商品カードの訴求文候補', DOMAIN )
						),
						microcopyButtons
					)
				);
			}
			if ( hasLabelSuggestions ) {
				suggestionSections.push(
					createElement(
						'div',
						{ key: 'cta-section', style: { marginBottom: '8px' } },
						createElement(
							'strong',
							{
								style: {
									display: 'block',
									marginBottom: '4px',
								},
							},
							__( 'CTAボタン文言候補', DOMAIN )
						),
						createElement(
							'button',
							{
								type: 'button',
								className: 'button button-small',
								onClick: function () {
									applyButtonLabelSuggestions(
										labelSuggestions
									);
								},
							},
							__( 'CTA文言を適用', DOMAIN )
						)
					)
				);
			}
			aiSuggestionPanel = createElement(
				'div',
				{
					style: {
						border: '1px solid #dcdcde',
						borderRadius: '8px',
						padding: '10px',
						marginBottom: '8px',
						background: '#fff',
					},
				},
				createElement(
					'p',
					{ className: 'description', style: { marginTop: 0 } },
					__(
						'記事文脈からキーワード候補を抽出しました。必要な候補だけ適用してください。',
						DOMAIN
					)
				),
				suggestionSections
			);
		}

		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 isAmazonFallbackTitle =
				row.provider === 'amazon' &&
				/^Amazonの商品ページ（ASIN:[^)]+）$/.test( title );
			var editableTitle = titleOverrides[ rowKey ] || 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: 'affilun-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,
						},
						editableTitle
					),
					isAmazonFallbackTitle
						? createElement( 'input', {
								type: 'text',
								className: 'regular-text',
								value: editableTitle,
								onChange: function ( e ) {
									var value = e.target.value;
									setTitleOverrides( function ( prev ) {
										var next = {};
										for ( var k in prev ) {
											if (
												Object.prototype.hasOwnProperty.call(
													prev,
													k
												)
											) {
												next[ k ] = prev[ k ];
											}
										}
										next[ rowKey ] = value;
										return next;
									} );
								},
								placeholder: __(
									'商品名または型番を入力',
									DOMAIN
								),
								style: {
									width: '100%',
									marginTop: '4px',
									fontSize: '12px',
								},
						  } )
						: null,
					isAmazonFallbackTitle
						? createElement(
								'div',
								{
									className: 'description',
									style: { fontSize: '11px' },
								},
								__(
									'Amazon Creators API が non-eligible のため、保存前に商品名を編集できます。',
									DOMAIN
								)
						  )
						: null,
					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
				)
			);
		} );

		if ( association !== null ) {
			var primaryRow = association.primary || {};
			var primaryImage =
				typeof primaryRow.image_primary_url === 'string' &&
				primaryRow.image_primary_url !== ''
					? primaryRow.image_primary_url
					: null;
			var primaryTitle =
				typeof primaryRow.title === 'string'
					? primaryRow.title
					: '(no title)';
			var remainingProviders = enabledProviders.filter(
				function ( provider ) {
					return provider.key !== primaryRow.provider;
				}
			);
			var associationSections = remainingProviders.map(
				function ( provider ) {
					var providerKey = provider.key;
					var sectionResult = ( association.results || {} )[
						providerKey
					] || {
						status: 'idle',
						items: [],
						error: null,
					};
					var selectedRow =
						( association.selected || {} )[ providerKey ] || null;
					var selectedTitle =
						selectedRow && typeof selectedRow.title === 'string'
							? selectedRow.title
							: '';
					var sectionRows = ( sectionResult.items || [] ).map(
						function ( row, idx ) {
							var imageUrl =
								typeof row.image_primary_url === 'string' &&
								row.image_primary_url !== ''
									? row.image_primary_url
									: null;
							var rowTitle =
								typeof row.title === 'string'
									? row.title
									: '(no title)';
							var brand =
								typeof row.brand === 'string' &&
								row.brand !== ''
									? row.brand
									: null;
							return createElement(
								'li',
								{
									key:
										'association-' +
										providerKey +
										'-' +
										idx,
									className: 'affilun-product-card-search-result',
									style: {
										display: 'flex',
										gap: '8px',
										alignItems: 'flex-start',
										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',
										{
											title: rowTitle,
											style: {
												fontWeight: '600',
												overflow: 'hidden',
												textOverflow: 'ellipsis',
												whiteSpace: 'nowrap',
											},
										},
										rowTitle
									),
									brand !== null
										? createElement(
												'div',
												{
													className: 'description',
													style: { fontSize: '12px' },
												},
												brand
										  )
										: null
								),
								createElement(
									'button',
									{
										type: 'button',
										className:
											'button button-primary button-small',
										onClick: function () {
											chooseAssociationRow(
												providerKey,
												row
											);
										},
										style: {
											flex: '0 0 auto',
											whiteSpace: 'nowrap',
										},
									},
									selectedRow &&
										resultRowKey( selectedRow ) ===
											resultRowKey( row )
										? __( '選択中', DOMAIN )
										: __( '選択', DOMAIN )
								)
							);
						}
					);

					return createElement(
						'section',
						{
							key: 'association-section-' + providerKey,
							style: { marginTop: '14px' },
						},
						createElement(
							'h4',
							{
								style: {
									display: 'inline-block',
									margin: '0 0 6px 0',
									padding: '4px 10px',
									borderRadius: '999px',
									background: '#f5f6fa',
								},
							},
							provider.label
						),
						selectedRow
							? createElement(
									'p',
									{
										className: 'description',
										style: { margin: '0 0 6px' },
									},
									__( '選択済み: ', DOMAIN ) + selectedTitle
							  )
							: null,
						createElement(
							'div',
							{
								style: {
									display: 'flex',
									gap: '6px',
									marginBottom: '8px',
								},
							},
							createElement( 'input', {
								type: 'search',
								className: 'regular-text',
								value:
									( association.queries || {} )[
										providerKey
									] || '',
								onChange: function ( e ) {
									updateAssociationQuery(
										providerKey,
										e.target.value
									);
								},
								onKeyDown: function ( e ) {
									if ( e.key === 'Enter' ) {
										e.preventDefault();
										runAssociationSearch( providerKey );
									}
								},
								style: { flex: '1 1 auto' },
							} ),
							createElement(
								'button',
								{
									type: 'button',
									className: 'button button-secondary',
									disabled:
										sectionResult.status === 'loading',
									onClick: function () {
										runAssociationSearch( providerKey );
									},
								},
								__( '検索', DOMAIN )
							)
						),
						sectionResult.status === 'loading'
							? createElement(
									'p',
									{ className: 'description' },
									__( '検索中…', DOMAIN )
							  )
							: null,
						sectionResult.status === 'error' && sectionResult.error
							? createElement(
									'p',
									{
										className: 'description',
										style: { color: '#b32d2e' },
									},
									sectionResult.error
							  )
							: null,
						sectionRows.length > 0
							? createElement(
									'ul',
									{
										className:
											'affilun-product-card-search-results',
										style: {
											listStyle: 'none',
											margin: 0,
											padding: 0,
											maxHeight: '220px',
											overflowY: 'auto',
											border: '1px solid #eee',
											borderRadius: '3px',
										},
									},
									sectionRows
							  )
							: null
					);
				}
			);

			return createElement(
				'div',
				{
					className: 'affilun-product-card-search-panel',
					style: {
						padding: '18px',
						border: '1px solid #eef0f6',
						borderRadius: '22px',
						background: 'rgba(255,255,255,0.92)',
						boxShadow: '0 14px 44px rgba(42,45,62,0.08)',
					},
				},
				createElement(
					'div',
					{
						style: {
							display: 'flex',
							gap: '10px',
							alignItems: 'center',
						},
					},
					primaryImage !== null
						? createElement( 'img', {
								src: primaryImage,
								alt: '',
								style: {
									width: '48px',
									height: '48px',
									objectFit: 'cover',
									borderRadius: '3px',
								},
						  } )
						: null,
					createElement(
						'div',
						{ style: { flex: '1 1 auto', minWidth: 0 } },
						createElement(
							'strong',
							{ title: primaryTitle },
							providerLabelForKey( primaryRow.provider ) +
								' / ' +
								primaryTitle
						),
						createElement(
							'div',
							{ className: 'description' },
							__(
								'残りのプロバイダーの商品ページを必要に応じて紐付けてください。',
								DOMAIN
							)
						)
					)
				),
				associationSections,
				association.status === 'error' && association.error
					? createElement(
							'p',
							{
								className: 'description',
								style: { color: '#b32d2e' },
							},
							association.error
					  )
					: null,
				createElement(
					'div',
					{
						style: {
							display: 'flex',
							gap: '8px',
							justifyContent: 'flex-end',
							marginTop: '14px',
						},
					},
					createElement(
						'button',
						{
							type: 'button',
							className: 'button button-secondary',
							disabled: association.status === 'saving',
							onClick: function () {
								setAssociation( null );
							},
						},
						__( '戻る', DOMAIN )
					),
					createElement(
						'button',
						{
							type: 'button',
							className: 'button button-primary',
							disabled: association.status === 'saving',
							onClick: confirmAssociation,
						},
						association.status === 'saving'
							? __( '保存中…', DOMAIN )
							: __( '確定', DOMAIN )
					)
				)
			);
		}

		return createElement(
			'div',
			{
				className: 'affilun-product-card-search-panel',
				style: {
					padding: '18px',
					border: '1px solid #eef0f6',
					borderRadius: '22px',
					background: 'rgba(255,255,255,0.92)',
					boxShadow: '0 14px 44px rgba(42,45,62,0.08)',
				},
			},
			providerTabs,
			aiStatusLine,
			aiSuggestionPanel,
			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: 'affilun-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 =
						'/affilun/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: 'affilun-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: 'affilun-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: 'affilun-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 || {};

		// Use the shared preview client so the editor preview reaches the same
		// REST payload shape as the front-end dynamic block.
		var template =
			typeof attributes.template === 'string' &&
			attributes.template !== ''
				? attributes.template
				: 'single-default-card';
		var attrSignature = JSON.stringify( attributes || {} );

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

		useEffect(
			function () {
				var previewClient = window.affilunEditorPreview || null;
				if (
					! previewClient ||
					typeof previewClient.requestSingleProductPreview !==
						'function' ||
					typeof apiFetch !== 'function' ||
					! idValue
				) {
					return;
				}
				var cancelled = false;
				setPreview( { status: 'loading', html: null, error: null } );
				previewClient
					.requestSingleProductPreview( {
						apiFetch: apiFetch,
						id: idValue,
						template: template,
						attributes: attributes,
					} )
					.then( function ( html ) {
						if ( cancelled ) {
							return;
						}
						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, attrSignature ]
		);

		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: 'affilun-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: 'affilun-product-card-selected',
				style: {
					padding: '12px',
					border: '1px solid #46b450',
					borderRadius: '3px',
					background: '#f4faf5',
				},
			},
			createElement(
				'h4',
				{ style: { marginTop: 0 } },
				__( 'affilun / 商品カード', DOMAIN )
			),
			previewBody,
			createElement(
				'button',
				{
					type: 'button',
					className: 'button button-secondary button-small',
					onClick: onClear,
				},
				__( '商品を変更', DOMAIN )
			)
		);
	}

	/**
	 * Render the Inspector sidebar panel for `affilun/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 TextControl = components.TextControl;
		var TextareaControl = components.TextareaControl;
		var ToggleControl = components.ToggleControl;
		if (
			typeof InspectorControls !== 'function' ||
			typeof PanelBody !== 'function' ||
			typeof SelectControl !== 'function' ||
			typeof TextControl !== 'function' ||
			typeof TextareaControl !== 'function' ||
			typeof ToggleControl !== 'function'
		) {
			return null;
		}

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

		function toggle( attrKey, label, help, defaultValue ) {
			var checked =
				typeof attributes[ attrKey ] === 'boolean'
					? attributes[ attrKey ]
					: defaultValue !== false;
			return createElement( ToggleControl, {
				key: 'toggle-' + attrKey,
				label: label,
				help: help,
				checked: checked,
				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( SelectControl, {
					label: __( 'デザインプリセット', DOMAIN ),
					help: __(
						'枠線・背景・影などの見た目を切り替えます。',
						DOMAIN
					),
					value:
						typeof attributes.templateVariant === 'string' &&
						attributes.templateVariant !== ''
							? attributes.templateVariant
							: '',
					options: [
						{ value: '', label: __( '設定デフォルト', DOMAIN ) },
					].concat( DESIGN_PRESET_OPTIONS ),
					onChange: function ( next ) {
						setAttributes( {
							templateVariant:
								String( next ) === ''
									? undefined
									: String( next ),
						} );
					},
				} ),
				createElement( SelectControl, {
					label: __( 'カラーモード', DOMAIN ),
					help: __(
						'カードの配色をライト、ダーク、OS設定連動から選びます。',
						DOMAIN
					),
					value:
						typeof attributes.colorScheme === 'string' &&
						attributes.colorScheme !== ''
							? attributes.colorScheme
							: '',
					options: [
						{ value: '', label: __( '設定デフォルト', DOMAIN ) },
					].concat( COLOR_SCHEME_OPTIONS ),
					onChange: function ( next ) {
						setAttributes( {
							colorScheme:
								String( next ) === ''
									? undefined
									: String( next ),
						} );
					},
				} ),
				createElement( SelectControl, {
					label: __( 'ボタン配置', DOMAIN ),
					value:
						typeof attributes.buttonLayout === 'string' &&
						attributes.buttonLayout !== ''
							? attributes.buttonLayout
							: '',
					options: [
						{ value: '', label: __( '設定デフォルト', DOMAIN ) },
					].concat( BUTTON_LAYOUT_OPTIONS ),
					onChange: function ( next ) {
						setAttributes( {
							buttonLayout:
								String( next ) === ''
									? undefined
									: String( next ),
						} );
					},
				} ),
				createElement( SelectControl, {
					label: __( 'ボタン形状', DOMAIN ),
					value:
						typeof attributes.buttonShape === 'string' &&
						attributes.buttonShape !== ''
							? attributes.buttonShape
							: '',
					options: [
						{ value: '', label: __( '設定デフォルト', DOMAIN ) },
					].concat( BUTTON_SHAPE_OPTIONS ),
					onChange: function ( next ) {
						setAttributes( {
							buttonShape:
								String( next ) === ''
									? undefined
									: String( next ),
						} );
					},
				} ),
				createElement( SelectControl, {
					label: __( 'ボタンカラー', DOMAIN ),
					value:
						typeof attributes.buttonTone === 'string' &&
						attributes.buttonTone !== ''
							? attributes.buttonTone
							: '',
					options: [
						{ value: '', label: __( '設定デフォルト', DOMAIN ) },
					].concat( BUTTON_TONE_OPTIONS ),
					onChange: function ( next ) {
						setAttributes( {
							buttonTone:
								String( next ) === ''
									? undefined
									: String( next ),
						} );
					},
				} ),
				createElement( TextareaControl, {
					label: __( 'カスタムボタン', DOMAIN ),
					help: __(
						'1行に1つ、ラベル|URL|target|rel の形式で入力します。target/rel は省略できます。',
						DOMAIN
					),
					value: customButtonsToText( attributes.customButtons ),
					onChange: function ( next ) {
						setAttributes( {
							customButtons: customButtonsFromText( next ),
						} );
					},
				} ),
				createElement( TextControl, {
					label: __( 'マイクロコピー', DOMAIN ),
					help: __(
						'セール文言など、ボタン付近に表示する短い訴求文です。',
						DOMAIN
					),
					value:
						typeof attributes.microcopyText === 'string'
							? attributes.microcopyText
							: '',
					onChange: function ( next ) {
						setAttributes( { microcopyText: String( next ) } );
					},
				} ),
				createElement( TextControl, {
					label: __( 'マイクロコピー開始', DOMAIN ),
					help: __( '任意。例: 2026-05-12 00:00', DOMAIN ),
					value:
						typeof attributes.microcopyStartAt === 'string'
							? attributes.microcopyStartAt
							: '',
					onChange: function ( next ) {
						setAttributes( { microcopyStartAt: String( next ) } );
					},
				} ),
				createElement( TextControl, {
					label: __( 'マイクロコピー終了', DOMAIN ),
					help: __( '任意。例: 2026-05-20 23:59', DOMAIN ),
					value:
						typeof attributes.microcopyEndAt === 'string'
							? attributes.microcopyEndAt
							: '',
					onChange: function ( next ) {
						setAttributes( { microcopyEndAt: String( next ) } );
					},
				} )
			),
			createElement(
				PanelBody,
				{
					title: __( 'A/B テスト', DOMAIN ),
					initialOpen: false,
				},
				toggle(
					'abTestingEnabled',
					__( 'A/B テストを有効化', DOMAIN ),
					__(
						'訪問者ごとに安定したボタン文言 / マイクロコピーのバリアントを表示します。',
						DOMAIN
					),
					false
				),
				createElement( TextControl, {
					label: __( 'バリアントB: ボタン文言', DOMAIN ),
					help: __( '空欄ならボタン文言は変更しません。', DOMAIN ),
					value:
						typeof attributes.abButtonLabelVariant === 'string'
							? attributes.abButtonLabelVariant
							: '',
					onChange: function ( next ) {
						setAttributes( {
							abButtonLabelVariant: String( next ),
						} );
					},
				} ),
				createElement( TextControl, {
					label: __( 'バリアントB: マイクロコピー', DOMAIN ),
					help: __(
						'空欄ならマイクロコピーは変更しません。',
						DOMAIN
					),
					value:
						typeof attributes.abMicrocopyVariant === 'string'
							? attributes.abMicrocopyVariant
							: '',
					onChange: function ( next ) {
						setAttributes( { abMicrocopyVariant: 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: 'affilun-product-card-editor' } )
			: { className: 'affilun-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,
					attributes: attributes,
					setAttributes: setAttributes,
				} ),
				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' ||
		typeof useEffect !== 'function' ||
		typeof useRef !== 'function'
	) {
		blocks.registerBlockType( 'affilun/product-card', {
			edit: function () {
				return createElement(
					'div',
					{ className: 'affilun-product-card-editor-placeholder' },
					__(
						'affilun / 商品カード (レガシーエディター — 検索 UI を利用するには WordPress を更新してください)。',
						DOMAIN
					)
				);
			},
			save: function () {
				return null;
			},
		} );
		return;
	}

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