<?php
/**
 * Add New Product admin-page renderer.
 *
 * Strict-review fix #5 (KI-014): server-side renderer for the
 * §7 / §11.5 "Add New" admin page documented in
 * `specifications/admin-ui-spec.md` §7. The page closes the
 * Phase 8 P0 completion-condition gap flagged in §11.5
 * ("管理画面から商品を検索して追加") by mirroring the in-editor
 * provider-search loop landed for the Gutenberg blocks
 * (commits 7ffa110 + 26aefb7) on the wp-admin surface.
 *
 * The renderer ships a thin server-rendered shell:
 *
 *   - A provider tab strip (Amazon / 楽天市場 / Yahoo!ショッピング,
 *     spec §4.1 canonical keys).
 *   - A keyword input + Search button.
 *   - An empty results pane that the inline JS populates after
 *     the §8.1 `GET /clb/v1/products/search` round-trip.
 *   - A status / flash region the inline JS uses to surface
 *     per-row save outcomes.
 *
 * The interactive bits run entirely in vanilla JS (no JSX / no
 * `wp.element` build-step) and call `wp.apiFetch` (already
 * enqueued on every wp-admin page that loads `wp-api`); the JS
 * payload is emitted by {@see self::renderScript()} as one
 * `<script>` block at the bottom of the page so the surface
 * works on both classic and Gutenberg editor hosts.
 *
 * Capability gating is NOT performed by this renderer — the
 * page-render closure in
 * {@see \CommerceLinkBuilder\Presentation\Admin\AdminServiceProvider}
 * re-checks the §3.1 editor-tier capability via
 * {@see \CommerceLinkBuilder\Presentation\Admin\AdminCapability::PAGE_TOKEN_PRODUCTS_NEW}
 * and `wp_die()`'s on denial. The renderer just emits HTML.
 *
 * Flash-message support: when the page-render closure forwards
 * a `clb_status` query parameter (one of `'saved'` / `'error'`)
 * the renderer surfaces a persistent admin notice at the top of
 * the page so a redirect-after-POST flow lands cleanly. The JS
 * also surfaces inline per-row status without requiring a
 * server round-trip.
 *
 * Out of scope of this slice (deferred to follow-up KIs):
 *
 *   - Inspector controls (template / show flags) — those live
 *     on the Gutenberg block sidebar (KI-015), not on the Add
 *     New page.
 *   - Bulk-import flow (e.g. paste a list of ASIN / item codes
 *     and save them all at once).
 *   - Duplicate-detection UX beyond what the §8.1 endpoint's
 *     `is_saved` flag already surfaces.
 *
 * @package CommerceLinkBuilder\Presentation\Admin\Page
 */

declare(strict_types=1);

// phpcs:disable Generic.Files.LineLength.TooLong -- Inline admin JavaScript is intentionally compact and hand-authored.

namespace CommerceLinkBuilder\Presentation\Admin\Page;

/**
 * Renders the §7 Add New admin page to stdout.
 *
 * See the file-level docblock for scope, gating, and flash-
 * message behaviour.
 */
final class AddNewProductPageRenderer {

	/**
	 * Closed list of providers offered in the tab strip.
	 *
	 * Keyed by spec §4.1 canonical wire token; values are the
	 * human-readable label.
	 *
	 * @var array<string, string>
	 */
	private const PROVIDERS = array(
		'amazon'         => 'Amazon',
		'rakuten'        => '楽天市場',
		'yahoo_shopping' => 'Yahoo!ショッピング',
	);

	/**
	 * Closed set of `clb_status` query-parameter literals the
	 * page-render closure may forward.
	 *
	 * @var list<string>
	 */
	public const VALID_STATUS_FLASHES = array( 'saved', 'error' );

	/**
	 * Echo the §7 Add New admin page HTML to stdout.
	 *
	 * @param string|null $flash One of {@see self::VALID_STATUS_FLASHES}
	 *                           or null when no flash is active.
	 */
	public function render( ?string $flash = null ): void {
		echo '<div class="wrap clb-add-new-product">';
		echo '<h1>' . esc_html( '商品を追加' ) . '</h1>';

		echo '<p>'
			. esc_html(
				// phpcs:ignore Generic.Files.LineLength.MaxExceeded -- Long literal accepted as a single visible paragraph.
				'プロバイダーで商品を検索し、Save をクリックして保存してください。保存した商品は「商品一覧」に即時反映され、ブロックエディターから選択できます。'
			)
			. '</p>';

		$this->renderFlash( $flash );

		$this->renderForm();

		$this->renderResultsShell();

		$this->renderScript();

		echo '</div>';
	}

	/**
	 * Render the optional admin-notice flash banner.
	 *
	 * @param string|null $flash Whitelisted status literal or null.
	 */
	private function renderFlash( ?string $flash ): void {
		if ( $flash === null ) {
			return;
		}
		if ( ! in_array( $flash, self::VALID_STATUS_FLASHES, true ) ) {
			return;
		}

		if ( $flash === 'saved' ) {
			echo '<div class="notice notice-success is-dismissible"><p>'
				. esc_html( '商品を保存しました。' )
				. '</p></div>';
			return;
		}

		echo '<div class="notice notice-error is-dismissible"><p>'
			. esc_html( '保存に失敗しました。インラインエラーの詳細を確認してください。' )
			. '</p></div>';
	}

	/**
	 * Render the provider tab strip + keyword input + Search
	 * button.
	 *
	 * The form is intentionally `<form onsubmit="return false">`
	 * (interactive submit is handled in JS); a non-JS fallback
	 * is not implemented because the §8.1 search endpoint can
	 * only be exercised through `wp.apiFetch` with the editor
	 * nonce in this iteration.
	 */
	private function renderForm(): void {
		echo '<form id="clb-add-new-form" onsubmit="return false;" class="clb-add-new-form">';

		echo '<fieldset class="clb-add-new-providers" style="margin-bottom:12px;">';
		echo '<legend class="screen-reader-text">'
			. esc_html( 'プロバイダー' )
			. '</legend>';

		$first = true;
		foreach ( self::PROVIDERS as $key => $label ) {
			$inputId = 'clb-add-new-provider-' . $key;
			echo '<label for="' . esc_attr( $inputId ) . '" style="margin-right:12px;">';
			echo '<input type="radio" name="clb_provider" id="' . esc_attr( $inputId ) . '"'
				. ' value="' . esc_attr( $key ) . '"'
				. ( $first ? ' checked="checked"' : '' )
				. '> '
				. esc_html( $label );
			echo '</label>';
			$first = false;
		}

		echo '</fieldset>';

		echo '<p class="clb-add-new-search-box"'
			. ' style="display:flex;gap:6px;align-items:center;margin:0 0 12px 0;clear:both;">';
		echo '<label for="clb-add-new-keywords" class="screen-reader-text">'
			. esc_html( 'キーワード' )
			. '</label>';
		echo '<input type="search" id="clb-add-new-keywords" class="regular-text"'
			. ' placeholder="' . esc_attr( 'キーワード…' ) . '"'
			. ' spellcheck="false" autocorrect="off" autocapitalize="off" autocomplete="off"'
			. ' style="flex:1 1 auto;">';
		echo '<button type="button" id="clb-add-new-search" class="button button-primary">'
			. esc_html( '検索' )
			. '</button>';
		echo '</p>';

		echo '</form>';
	}

	/**
	 * Render the empty results-pane shell the inline JS
	 * populates after the §8.1 search round-trip.
	 */
	private function renderResultsShell(): void {
		echo '<div id="clb-add-new-status" class="clb-add-new-status"'
			. ' role="status" aria-live="polite" style="margin-bottom:8px;clear:both;"></div>';

		echo '<div id="clb-add-new-results" class="clb-add-new-results"'
			. ' style="border:1px solid #c3c4c7;background:#fff;border-radius:3px;'
			. 'min-height:48px;padding:0;clear:both;">';
		echo '<p class="description" style="margin:12px;">'
			. esc_html( 'キーワードを入力して検索をクリックすると、商品を取得します。' )
			. '</p>';
		echo '</div>';
	}

	/**
	 * Render the inline JS that drives the search + save loop.
	 *
	 * The script:
	 *
	 *   - Reads the selected provider radio + the keyword input.
	 *   - Calls `wp.apiFetch` against `/clb/v1/products/search`
	 *     to populate the results pane.
	 *   - Renders a per-row "Save" button that POSTs the row to
	 *     `/clb/v1/products` and surfaces "Saved (#N)" inline on
	 *     success, with a "View in Products" link to the saved
	 *     record.
	 *   - Falls back to a visible error message when
	 *     `wp.apiFetch` is unavailable (very old admin host).
	 *
	 * The script reuses the same buildSavePayload / extractSavedId
	 * helper logic the editor JS uses (commit 7ffa110 +
	 * commit 26aefb7) so the wp-admin and Gutenberg surfaces
	 * stay behaviour-equivalent.
	 *
	 * Emitted as a single `<script>` block; no minification step
	 * is applied because the page is admin-only and rarely
	 * loaded.
	 */
	private function renderScript(): void {
		// Resolve the products-list admin URL the per-row "View"
		// link points at, with a synthetic fallback so the link
		// still parses through esc_url() in pure-unit harnesses
		// where WordPress is not loaded.
		$productsAdminUrl = function_exists( 'admin_url' )
			? (string) admin_url( 'admin.php?page=clb-products' )
			: 'http://example.test/wp-admin/admin.php?page=clb-products';

		$productsAdminUrlJs = wp_json_encode( $productsAdminUrl );
		if ( ! is_string( $productsAdminUrlJs ) ) {
			$productsAdminUrlJs = '""';
		}

		// Server-emitted i18n strings the JS surfaces inline. Emitted
		// through wp_json_encode() so multi-byte strings round-trip
		// safely.
		$strings   = array(
			'enter_keyword'   => '検索キーワードを入力してください。',
			'searching'       => '検索中…',
			'no_results'      => '一致する商品が見つかりませんでした。',
			'search_failed'   => '検索に失敗しました。プロバイダー認証情報が設定済みか、edit_posts 権限があるかを確認してください。',
			'save_label'      => '保存',
			'saving_label'    => '保存中…',
			'saved_label'     => '保存しました (#%d)',
			'use_saved_label' => '保存済み (#%d)',
			'save_failed'     => '保存に失敗しました。',
			'invalid_row'     => 'この検索結果は必須フィールド (タイトル / 詳細 URL) が欠けているため保存できません。',
			'view_link'       => '商品一覧で表示',
			'no_apifetch'     => 'wp.apiFetch が読み込まれていないため、この画面から REST API にアクセスできません。wp-api スクリプトがエンキューされているか確認してください。',
		);
		$stringsJs = wp_json_encode( $strings );
		if ( ! is_string( $stringsJs ) ) {
			$stringsJs = '{}';
		}

		echo '<script>(function(){';
		// $stringsJs and $productsAdminUrlJs are emitted by wp_json_encode()
		// which already escapes for safe JS literal embedding (it returns
		// JSON-encoded values with special characters escaped). Running
		// them through esc_html()/esc_js() would re-escape the JSON quotes
		// and corrupt the literal.
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- See above.
		echo 'var STRINGS=' . $stringsJs . ';';
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- See above.
		echo 'var PRODUCTS_ADMIN_URL=' . $productsAdminUrlJs . ';';
		echo <<<'JS'
function format(tpl, n){return tpl.replace('%d', String(n));}
function statusLine(text, isError){
  var el=document.getElementById('clb-add-new-status');
  if(!el)return;
  el.textContent=text||'';
  el.style.color=isError?'#b32d2e':'';
}
function selectedProvider(){
  var radios=document.querySelectorAll('input[name="clb_provider"]');
  for(var i=0;i<radios.length;i++){if(radios[i].checked)return radios[i].value;}
  return '';
}
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 [];
}
function buildPayload(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 p={};
    if(typeof row.price.amount==='string'&&row.price.amount!=='')p.amount=row.price.amount;
    if(typeof row.price.currency==='string'&&row.price.currency!==''&&row.price.currency!=='UNKNOWN')p.currency=row.price.currency;
    if(typeof row.price.label==='string'&&row.price.label!=='')p.label=row.price.label;
    if(p.amount&&p.currency)snapshot.price=p;
  }
  return {provider:provider,provider_item_id:providerItemId,snapshot:snapshot};
}
function extractSavedId(response){
  if(response&&response.data){
    var d=response.data;
    if(typeof d.id==='number'&&d.id>0)return d.id;
    if(d.product&&typeof d.product.id==='number'&&d.product.id>0)return d.product.id;
  }
  if(response&&typeof response.id==='number'&&response.id>0)return response.id;
  return null;
}
function renderError(container, msg){
  container.innerHTML='';
  var p=document.createElement('p');
  p.className='description';
  p.style.color='#b32d2e';
  p.style.margin='12px';
  p.textContent=msg;
  container.appendChild(p);
}
function renderEmpty(container, msg){
  container.innerHTML='';
  var p=document.createElement('p');
  p.className='description';
  p.style.margin='12px';
  p.textContent=msg;
  container.appendChild(p);
}
function renderRow(row){
  var li=document.createElement('li');
  li.style.display='flex';
  li.style.alignItems='flex-start';
  li.style.gap='10px';
  li.style.padding='10px 12px';
  li.style.borderBottom='1px solid #eee';

  if(typeof row.image_primary_url==='string'&&row.image_primary_url!==''){
    var img=document.createElement('img');
    img.src=row.image_primary_url;
    img.alt='';
    img.style.width='56px';img.style.height='56px';img.style.objectFit='cover';
    img.style.flex='0 0 auto';img.style.borderRadius='3px';
    li.appendChild(img);
  } else {
    var ph=document.createElement('div');
    ph.style.width='56px';ph.style.height='56px';ph.style.background='#f0f0f0';
    ph.style.flex='0 0 auto';ph.style.borderRadius='3px';
    li.appendChild(ph);
  }

  var body=document.createElement('div');
  body.style.flex='1 1 auto';body.style.minWidth='0';
  var title=document.createElement('div');
  title.style.fontWeight='600';
  title.textContent=typeof row.title==='string'?row.title:'(no title)';
  body.appendChild(title);
  if(typeof row.brand==='string'&&row.brand!==''){
    var brand=document.createElement('div');brand.className='description';
    brand.style.fontSize='12px';brand.textContent=row.brand;body.appendChild(brand);
  }
  if(row.price&&typeof row.price.label==='string'&&row.price.label!==''){
    var price=document.createElement('div');price.style.fontSize='13px';
    price.style.marginTop='2px';price.textContent=row.price.label;body.appendChild(price);
  }
  if(row.links&&typeof row.links.detail_url==='string'){
    var linkLine=document.createElement('div');linkLine.style.fontSize='11px';
    linkLine.style.marginTop='2px';
    var a=document.createElement('a');a.href=row.links.detail_url;a.target='_blank';a.rel='noopener noreferrer';
    a.textContent=row.links.detail_url;linkLine.appendChild(a);body.appendChild(linkLine);
  }
  var rowStatus=document.createElement('div');
  rowStatus.style.fontSize='12px';rowStatus.style.marginTop='4px';
  body.appendChild(rowStatus);
  li.appendChild(body);

  var actions=document.createElement('div');
  actions.style.flex='0 0 auto';
  var btn=document.createElement('button');
  btn.type='button';btn.className='button button-primary';

  if(row.is_saved&&typeof row.id==='number'&&row.id>0){
    btn.textContent=format(STRINGS.use_saved_label, row.id);
    btn.disabled=true;
    actions.appendChild(btn);
    appendViewLink(actions, row.id);
  } else {
    btn.textContent=STRINGS.save_label;
    btn.addEventListener('click', function(){onSaveClick(row, btn, rowStatus, actions);});
    actions.appendChild(btn);
  }
  li.appendChild(actions);

  return li;
}
function appendViewLink(container, id){
  var sep=container.querySelector('.clb-view-link');
  if(sep)return;
  var a=document.createElement('a');
  a.className='clb-view-link';
  a.href=PRODUCTS_ADMIN_URL;
  a.style.marginLeft='8px';
  a.style.display='inline-block';
  a.style.lineHeight='28px';
  a.textContent=STRINGS.view_link;
  container.appendChild(a);
}
function onSaveClick(row, btn, rowStatus, actions){
  if(!window.wp||typeof window.wp.apiFetch!=='function'){
    rowStatus.style.color='#b32d2e';
    rowStatus.textContent=STRINGS.no_apifetch;
    return;
  }
  var payload=buildPayload(row);
  if(payload===null){
    rowStatus.style.color='#b32d2e';
    rowStatus.textContent=STRINGS.invalid_row;
    return;
  }
  btn.disabled=true;
  btn.textContent=STRINGS.saving_label;
  rowStatus.style.color='';
  rowStatus.textContent='';
  window.wp.apiFetch({path:'/clb/v1/products', method:'POST', data:payload}).then(function(response){
    var savedId=extractSavedId(response);
    if(savedId===null){
      btn.disabled=false;
      btn.textContent=STRINGS.save_label;
      rowStatus.style.color='#b32d2e';
      rowStatus.textContent=STRINGS.save_failed;
      return;
    }
    btn.textContent=format(STRINGS.saved_label, savedId);
    appendViewLink(actions, savedId);
  }).catch(function(err){
    btn.disabled=false;
    btn.textContent=STRINGS.save_label;
    rowStatus.style.color='#b32d2e';
    var msg=STRINGS.save_failed;
    if(err&&typeof err.message==='string'&&err.message!=='')msg=err.message;
    else if(err&&err.error&&typeof err.error.message==='string')msg=err.error.message;
    rowStatus.textContent=msg;
  });
}
function runSearch(){
  if(!window.wp||typeof window.wp.apiFetch!=='function'){
    statusLine(STRINGS.no_apifetch, true);
    return;
  }
  var input=document.getElementById('clb-add-new-keywords');
  var container=document.getElementById('clb-add-new-results');
  if(!input||!container)return;
  var keywords=(input.value||'').trim();
  if(keywords===''){
    statusLine(STRINGS.enter_keyword, true);
    return;
  }
  var provider=selectedProvider();
  if(provider===''){
    statusLine(STRINGS.enter_keyword, true);
    return;
  }
  statusLine(STRINGS.searching, false);
  container.innerHTML='';
  var path='/clb/v1/products/search?provider='+encodeURIComponent(provider)+'&keywords='+encodeURIComponent(keywords)+'&per_page=10';
  window.wp.apiFetch({path:path}).then(function(response){
    var items=extractItems(response);
    statusLine('', false);
    if(items.length===0){renderEmpty(container, STRINGS.no_results);return;}
    var ul=document.createElement('ul');
    ul.style.listStyle='none';ul.style.margin='0';ul.style.padding='0';
    items.forEach(function(row){ul.appendChild(renderRow(row));});
    container.innerHTML='';
    container.appendChild(ul);
  }).catch(function(err){
    var msg=STRINGS.search_failed;
    if(err&&typeof err.message==='string'&&err.message!=='')msg=err.message;
    else if(err&&err.error&&typeof err.error.message==='string')msg=err.error.message;
    statusLine('', false);
    renderError(container, msg);
  });
}
function init(){
  var btn=document.getElementById('clb-add-new-search');
  if(btn)btn.addEventListener('click', runSearch);
  var kw=document.getElementById('clb-add-new-keywords');
  if(kw)kw.addEventListener('keydown', function(e){if(e.key==='Enter'){e.preventDefault();runSearch();}});
}
if(document.readyState==='loading'){
  document.addEventListener('DOMContentLoaded', init);
} else {
  init();
}
JS;
		echo '})();</script>';
	}
}
