<?php
/**
 * Server-side render for blocksify/accordion-item.
 *
 * Builds the full item DOM (header + body) at request time. Each item
 * self-renders + WP's normal recursive content-build joins these into
 * the parent accordion's `$content` arg — no double-render risk because
 * the parent uses `$content` directly (no manual children walk).
 *
 * Initial open state:
 *   Parent's `defaultOpenItem === item_id` drives initial state in
 *   both single- and multi-expand modes. The frontend Interactivity
 *   API store mutates `ctx.currentItem` at runtime when users click,
 *   but those mutations stay in the IA context object — they are
 *   never written back to post attributes (there's no `currentItem`
 *   attribute on the parent block, by design).
 *
 * @package Blocksify
 *
 * @var array    $attributes Block attributes.
 * @var string   $content    Inner blocks rendered (user's panel content).
 * @var WP_Block $block      Parsed block instance.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

$title        = isset( $attributes['title'] ) ? (string) $attributes['title'] : '';
$icon         = isset( $attributes['icon'] ) && is_array( $attributes['icon'] ) ? $attributes['icon'] : null;
$item_index   = isset( $attributes['index'] ) && is_numeric( $attributes['index'] ) ? (int) $attributes['index'] : 1;
$anchor       = isset( $attributes['anchor'] ) ? (string) $attributes['anchor'] : '';
$tag_name     = isset( $attributes['tagName'] ) ? (string) $attributes['tagName'] : 'div';
$user_class   = isset( $attributes['className'] ) ? trim( (string) $attributes['className'] ) : '';

// Wrapper tag allowlist (mirrors edit.js + Inspector options).
$allowed_item_tags = array( 'div', 'section', 'article', 'header', 'footer', 'main', 'aside', 'nav' );
$tag_name = in_array( $tag_name, $allowed_item_tags, true ) ? $tag_name : 'div';

// Read parent context (set by accordion/render.php via providesContext).
$parent_context     = isset( $block->context ) ? $block->context : array();
$parent_uid         = isset( $parent_context['blocksify/accordion/uid'] ) ? preg_replace( '/[^a-f0-9]/', '', (string) $parent_context['blocksify/accordion/uid'] ) : '';
$allow_multiple     = ! isset( $parent_context['blocksify/accordion/allowMultiple'] ) || true === $parent_context['blocksify/accordion/allowMultiple'];
$default_open_item  = isset( $parent_context['blocksify/accordion/defaultOpenItem'] ) ? $parent_context['blocksify/accordion/defaultOpenItem'] : null;
$indicator_type     = isset( $parent_context['blocksify/accordion/indicatorType'] ) ? (string) $parent_context['blocksify/accordion/indicatorType'] : 'plus-minus';
$title_tag          = isset( $parent_context['blocksify/accordion/titleTag'] ) ? (string) $parent_context['blocksify/accordion/titleTag'] : 'h4';
$title_align        = isset( $parent_context['blocksify/accordion/titleAlign'] ) ? (string) $parent_context['blocksify/accordion/titleAlign'] : '';
$show_icon          = ! empty( $parent_context['blocksify/accordion/showIcon'] );
$allowed_aligns     = array( 'left', 'center', 'right' );
$title_align        = in_array( $title_align, $allowed_aligns, true ) ? $title_align : '';

// Title-tag allowlist for the heading wrapper around the button.
$allowed_title_tags = array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'span', 'p' );
$title_tag = in_array( $title_tag, $allowed_title_tags, true ) ? $title_tag : 'h4';

// Resolve item_id by walking parent's parsed_block innerBlocks. Each
// position+1 is the canonical id; saved `index` attr is a hint, NOT
// authoritative.
$resolved_id = max( 1, (int) $item_index );

// Initial open state — driven by parent's "Open Default" attribute
// (`defaultOpenItem`). For visitors, the Interactivity API store
// then takes over runtime mutations; the saved attribute only seeds
// the initial render. Editor click-toggle UI state lives separately
// in the `blocksify/accordion-ui` data store, editor-only, NOT
// persisted — see `src/shared/stores/accordion-ui.js`.
$initial_open = ( null !== $default_open_item && (int) $default_open_item === $resolved_id );

// Indicator type allowlist.
$allowed_indicators = array( 'plus-minus', 'chevron', 'arrow', 'none' );
$indicator_type     = in_array( $indicator_type, $allowed_indicators, true ) ? $indicator_type : 'plus-minus';

// Compose item context — per-item `isOpen` + `itemId` for the
// Interactivity API store. JSON-encode + esc_attr below for the
// data-wp-context attr.
$item_context = array(
	'isOpen' => (bool) $initial_open,
	'itemId' => (int) $resolved_id,
);
$item_context_json = wp_json_encode( $item_context );
if ( false === $item_context_json ) {
	$item_context_json = '{}';
}

// ARIA IDs — derived from parent uid + this item's index. Parent uid is
// 8 hex chars, guaranteed unique per accordion-block-instance on the
// page; item index (1-based) is unique within one accordion. Together
// they yield a per-page-unique ID without persisting a uid attribute
// on each accordion-item. If parent_uid is missing (shouldn't happen
// in normal usage; defensive), emit empty IDs — aria links degrade
// gracefully when missing (screen readers fall back to proximity).
$header_id = '' !== $parent_uid ? 'bsy-accordion-' . $parent_uid . '-item-' . $resolved_id . '-header' : '';
$body_id   = '' !== $parent_uid ? 'bsy-accordion-' . $parent_uid . '-item-' . $resolved_id . '-body' : '';

$item_classes = array( 'bsy-accordion__item' );
if ( $initial_open ) {
	$item_classes[] = 'is-open';
}
if ( '' !== $user_class ) {
	$item_classes[] = $user_class;
}
$item_class_attr = esc_attr( implode( ' ', $item_classes ) );

// "Card-as-button" pattern — toggle behavior lives on the item wrapper
// itself (not on the inner `.bsy-accordion__header` row). Reason: when
// the user adds padding to the item (Inspector → Styling → Item Wrap →
// Padding), that padded surface should be part of the click target;
// hanging the click handler off the inner header would leave the
// padding ring visually inside the card but functionally dead.
// view.js's `actions.toggle` guards against clicks that originated
// inside `.bsy-accordion__body` so links / buttons inside the open
// panel don't accidentally collapse the item.
$item_attrs  = 'class="' . $item_class_attr . '"';
$item_attrs .= ' data-wp-context=\'' . esc_attr( $item_context_json ) . '\'';
$item_attrs .= ' data-wp-class--is-open="state.isItemOpen"';
$item_attrs .= ' role="button"';
$item_attrs .= ' tabindex="0"';
if ( '' !== $body_id ) {
	$item_attrs .= ' aria-controls="' . esc_attr( $body_id ) . '"';
}
$item_attrs .= ' aria-expanded="' . ( $initial_open ? 'true' : 'false' ) . '"';
$item_attrs .= ' data-wp-on--click="actions.toggle"';
$item_attrs .= ' data-wp-on--keydown="callbacks.onKeydown"';
$item_attrs .= ' data-wp-bind--aria-expanded="state.itemAriaExpanded"';
if ( '' !== $anchor ) {
	$item_attrs .= ' id="' . esc_attr( $anchor ) . '"';
}

// Header now carries no interactive role — just the title row layout.
// `id` stays so the body's `aria-labelledby` can still point at it.
// `is-open` class kept for any title-row-specific active styling.
$header_attrs  = 'class="bsy-accordion__header' . ( $initial_open ? ' is-open' : '' ) . '"';
if ( '' !== $header_id ) {
	$header_attrs .= ' id="' . esc_attr( $header_id ) . '"';
}
$header_attrs .= ' data-wp-class--is-open="state.isItemOpen"';

// Body uses `aria-hidden` (not the `hidden` attribute) so CSS transition
// can drive open/close — `hidden` triggers `display: none` which has no
// transition. `is-open` class on parent item drives the CSS animation
// state via `[item.is-open] > .body` selectors.
$body_attrs  = 'class="bsy-accordion__body"';
$body_attrs .= ' role="region"';
if ( '' !== $body_id ) {
	$body_attrs .= ' id="' . esc_attr( $body_id ) . '"';
}
if ( '' !== $header_id ) {
	$body_attrs .= ' aria-labelledby="' . esc_attr( $header_id ) . '"';
}
$body_attrs .= ' data-wp-bind--aria-hidden="state.itemHidden"';
$body_attrs .= ' aria-hidden="' . ( $initial_open ? 'false' : 'true' ) . '"';

// Icon — resolved by Blocksify_Icons (Lucide sparse bag, custom-svg
// paste, and inline-svg upload all flow through this). The `svg` field
// short-circuit honors legacy posts byte-for-byte.
//
// Gated on parent's `showIcon` context — picker stays usable in the
// editor regardless, but rendered output (frontend + editor preview)
// only appears when the Accordion parent's icon toggle is enabled.
$icon_html = '';
if ( $show_icon ) {
	$icon_svg = Blocksify_Icons::render( $icon );
	if ( '' !== $icon_svg ) {
		$icon_html = '<span class="bsy-accordion__icon" aria-hidden="true">' . $icon_svg . '</span>';
	} elseif ( $icon && isset( $icon['url'] ) && is_string( $icon['url'] ) && '' !== $icon['url'] ) {
		$icon_html = '<span class="bsy-accordion__icon" aria-hidden="true"><img src="' . esc_url( $icon['url'] ) . '" alt="" /></span>';
	}
}

// Indicator SVG — built by helper that mirrors the JS IndicatorSvg
// component byte-for-byte.
$indicator_html = blocksify_accordion_indicator_svg( $indicator_type );

$title_safe = wp_kses(
	$title,
	array(
		'strong' => array(),
		'em'     => array(),
		'b'      => array(),
		'i'      => array(),
	)
);
if ( '' === trim( wp_strip_all_tags( $title_safe ) ) ) {
	$title_safe = esc_html__( 'Accordion title', 'blocksify' );
}

// Title element IS the user-chosen tag (default h4 — h-tags inside
// <button> is technically invalid HTML but renders + is announced
// correctly by screen readers; matches Kadence / Spectra accordion
// convention. User can pick `span`/`div`/`p` for strict-validation
// cases.)
$item_tag_safe  = esc_attr( $tag_name );
$title_tag_safe = esc_attr( $title_tag );

// Icon is rendered INSIDE the title element so it inherits font-size
// (1em on the icon span = title's computed font-size). Indicator stays
// OUTSIDE the title (sibling of title in header).
$title_classes = array( 'bsy-accordion__title' );
if ( '' !== $title_align ) {
	$title_classes[] = 'is-align-' . $title_align;
}
$title_class_attr = esc_attr( implode( ' ', $title_classes ) );

// Icon + text + indicator ALL inside title element so indicator's
// `1em` SVG width resolves to the title's font-size (theme heading
// size), not the header's body font-size.
$title_content = '';
if ( '' !== $icon_html ) {
	$title_content .= $icon_html;
}
$title_content .= '<span class="bsy-accordion__title-text">' . $title_safe . '</span>';
if ( '' !== $indicator_html ) {
	$title_content .= '<span class="bsy-accordion__indicator" aria-hidden="true">' . $indicator_html . '</span>';
}

$header_inner = '<' . $title_tag_safe . ' class="' . $title_class_attr . '">'
	. $title_content
	. '</' . $title_tag_safe . '>';

// Body wrapping has THREE levels so the grid-collapse close trick AND
// the user-set body padding/border can coexist:
//   - `.bsy-accordion__body`        — grid container (`grid-template-rows:
//                                     0fr ↔ 1fr` for smooth open/close).
//   - `.bsy-accordion__body-inner`  — grid track CHILD with
//                                     `overflow:hidden` + `min-height:0`.
//                                     STRUCTURAL ONLY — adding padding
//                                     here would inflate the grid track
//                                     intrinsic size and prevent it from
//                                     collapsing to 0 when closed.
//   - `.bsy-accordion__body-content` — innermost wrapper. Padding /
//                                     background / border for the `body`
//                                     part land here. Sits inside the
//                                     overflow-hidden body-inner, so its
//                                     padding is clipped to 0 when the
//                                     parent grid track is 0fr (closed).
echo '<' . $item_tag_safe . ' ' . $item_attrs . '>'
	. '<div ' . $header_attrs . '>' . $header_inner . '</div>'
	. '<div ' . $body_attrs . '><div class="bsy-accordion__body-inner"><div class="bsy-accordion__body-content">' . $content . '</div></div></div>'
	. '</' . $item_tag_safe . '>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
