<?php
/**
 * Post FAQ module for BW AI Schema Pro
 *
 * Lets editors attach a question/answer list to any supported post type, render
 * it on the front-end at a configurable insertion point (theme-agnostic +
 * Kadence-aware), and emit FAQPage JSON-LD inline so the questions and answers
 * pick up as structured data.
 *
 * Storage:
 *   - _bw_schema_post_faq_items     (array of { question, answer })
 *   - _bw_schema_post_faq_heading   (per-post heading override, optional)
 *   - _bw_schema_post_faq_accordion ('on' | 'off' | '' for inherit-global)
 *   - bw_schema_post_faq_settings   (option: enabled, post_types[], etc.)
 *
 * Pattern adopted from the stackd-consulting child-theme implementation at
 * /srv/apps/stackdconsulting/wp-content/themes/total-child-theme/inc/post-faq.php,
 * generalized to work without requiring TotalWP. The renderer + schema halves
 * are intentionally kept independent of the existing BW_Schema_FAQ class
 * (which is for users who manually set a post's schema type to FAQPage) — this
 * module emits its own inline JSON-LD next to the rendered HTML so the data
 * and the visible markup stay in lockstep.
 *
 * @package BW_AI_Schema_Pro
 * @since 2.3.0
 */

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

class BW_Schema_Post_FAQ {

	const META_ITEMS      = '_bw_schema_post_faq_items';
	const META_HEADING    = '_bw_schema_post_faq_heading';
	const META_ACCORDION  = '_bw_schema_post_faq_accordion';
	const OPT_SETTINGS    = 'bw_schema_post_faq_settings';
	const NONCE_ACTION    = 'bw_schema_post_faq_save';
	const NONCE_FIELD     = 'bw_schema_post_faq_nonce';
	const SHORTCODE       = 'bw_schema_faq';

	/**
	 * Insertion modes. The label is what shows in the settings dropdown; the
	 * key is what's stored in the option. The actual hook each mode binds to
	 * is in get_insertion_hook_spec().
	 */
	public static function insertion_modes() {
		return array(
			'the_content'                => __( 'After the post content (universal — most themes)', 'bw-ai-schema-pro' ),
			'kadence_after_content'      => __( 'Kadence: after content, before comments', 'bw-ai-schema-pro' ),
			'kadence_after_entry_header' => __( 'Kadence: after the author byline / title area', 'bw-ai-schema-pro' ),
			'genesis_after_entry'        => __( 'Genesis: after the entry', 'bw-ai-schema-pro' ),
			'astra_entry_after'          => __( 'Astra: after the entry', 'bw-ai-schema-pro' ),
			'generate_after_content'     => __( 'GeneratePress: after content', 'bw-ai-schema-pro' ),
			'html_selector'              => __( 'Insert at an HTML element (CSS selector)', 'bw-ai-schema-pro' ),
			'custom'                     => __( 'Custom action hook (advanced)', 'bw-ai-schema-pro' ),
			'shortcode_only'             => __( 'Shortcode only — no auto-insertion', 'bw-ai-schema-pro' ),
		);
	}

	/**
	 * Selector positions, mapped to insertAdjacentElement's named positions.
	 */
	public static function selector_positions() {
		return array(
			'afterend'    => __( 'After the element (sibling, default)', 'bw-ai-schema-pro' ),
			'beforebegin' => __( 'Before the element (sibling)', 'bw-ai-schema-pro' ),
			'afterbegin'  => __( 'Inside the element, at the start', 'bw-ai-schema-pro' ),
			'beforeend'   => __( 'Inside the element, at the end', 'bw-ai-schema-pro' ),
		);
	}

	/**
	 * Em-based size preset for the heading. Empty string = inherit from theme.
	 */
	public static function heading_size_options() {
		return array(
			''        => __( 'Inherit from theme (default)', 'bw-ai-schema-pro' ),
			'0.9em'   => __( 'Smaller (0.9em)', 'bw-ai-schema-pro' ),
			'1em'     => __( 'Match body text (1em)', 'bw-ai-schema-pro' ),
			'1.1em'   => __( 'Slightly larger (1.1em)', 'bw-ai-schema-pro' ),
			'1.25em'  => __( 'Larger (1.25em)', 'bw-ai-schema-pro' ),
			'1.5em'   => __( 'Large (1.5em)', 'bw-ai-schema-pro' ),
			'2em'     => __( 'Hero (2em)', 'bw-ai-schema-pro' ),
		);
	}

	/**
	 * Map an insertion-mode key to the WordPress hook tuple it binds to.
	 * Returns array{ type: 'filter'|'action'|'none', name: string, priority: int }
	 * `type=none` means the renderer is shortcode-only (no auto-insert).
	 */
	private static function get_insertion_hook_spec( $mode ) {
		switch ( $mode ) {
			case 'kadence_after_content':
				return array( 'type' => 'action', 'name' => 'kadence_single_after_content', 'priority' => 20 );
			case 'kadence_after_entry_header':
				return array( 'type' => 'action', 'name' => 'kadence_single_after_entry_header', 'priority' => 20 );
			case 'genesis_after_entry':
				return array( 'type' => 'action', 'name' => 'genesis_after_entry', 'priority' => 12 );
			case 'astra_entry_after':
				return array( 'type' => 'action', 'name' => 'astra_entry_after', 'priority' => 10 );
			case 'generate_after_content':
				return array( 'type' => 'action', 'name' => 'generate_after_content', 'priority' => 10 );
			case 'html_selector':
				$sel = trim( (string) ( self::get_settings()['selector'] ?? '' ) );
				return $sel !== ''
					? array( 'type' => 'selector', 'name' => '', 'priority' => 0 )
					: array( 'type' => 'none', 'name' => '', 'priority' => 0 );
			case 'custom':
				$hook = trim( (string) ( self::get_settings()['custom_hook'] ?? '' ) );
				return $hook !== ''
					? array( 'type' => 'action', 'name' => $hook, 'priority' => 10 )
					: array( 'type' => 'none', 'name' => '', 'priority' => 0 );
			case 'shortcode_only':
				return array( 'type' => 'none', 'name' => '', 'priority' => 0 );
			case 'the_content':
			default:
				return array( 'type' => 'filter', 'name' => 'the_content', 'priority' => 20 );
		}
	}

	/**
	 * Init. Called from the plugin's bundled init hook.
	 */
	public static function init() {
		// Shortcode is always registered (cheap), even if FAQ is disabled.
		// When disabled, the shortcode returns ''.
		add_shortcode( self::SHORTCODE, array( __CLASS__, 'shortcode' ) );

		$settings = self::get_settings();

		if ( ! $settings['enabled'] ) {
			return;
		}

		if ( is_admin() ) {
			self::admin_init();
			return;
		}

		// Front-end: hook into the configured insertion point.
		$spec = self::get_insertion_hook_spec( $settings['insertion_mode'] );

		if ( 'filter' === $spec['type'] ) {
			add_filter( $spec['name'], array( __CLASS__, 'append_to_filter_content' ), $spec['priority'] );
		} elseif ( 'action' === $spec['type'] ) {
			add_action( $spec['name'], array( __CLASS__, 'echo_for_action' ), $spec['priority'] );
		} elseif ( 'selector' === $spec['type'] ) {
			// Hidden render + DOM-injection JS is the only mode that needs
			// extra client-side code, so we only hook it (and only emit the
			// JS) when this mode is selected. Other modes pay zero JS cost.
			add_action( 'wp_footer', array( __CLASS__, 'render_for_selector_mode' ), 5 );
		}
	}

	/* ---------------- Settings ---------------- */

	public static function default_heading() {
		return __( 'Frequently Asked Questions', 'bw-ai-schema-pro' );
	}

	public static function get_settings() {
		$saved = get_option( self::OPT_SETTINGS, array() );
		if ( ! is_array( $saved ) ) {
			$saved = array();
		}

		$defaults = array(
			'enabled'           => 0,
			'post_types'        => array( 'post' ),
			'default_heading'   => self::default_heading(),
			'default_accordion' => 1,
			'insertion_mode'    => 'the_content',
			'custom_hook'       => '',
			'selector'          => '',
			'selector_position' => 'afterend',
			'heading_color'     => '',
			'heading_size'      => '',
		);

		$merged = array_merge( $defaults, $saved );
		$merged['post_types'] = is_array( $merged['post_types'] ) ? array_map( 'sanitize_key', $merged['post_types'] ) : array( 'post' );
		$merged['enabled']           = (bool) $merged['enabled'];
		$merged['default_accordion'] = (bool) $merged['default_accordion'];
		$merged['default_heading']   = '' !== trim( (string) $merged['default_heading'] )
			? sanitize_text_field( $merged['default_heading'] )
			: self::default_heading();
		$merged['insertion_mode']    = sanitize_key( $merged['insertion_mode'] );
		if ( ! array_key_exists( $merged['insertion_mode'], self::insertion_modes() ) ) {
			$merged['insertion_mode'] = 'the_content';
		}
		$merged['custom_hook'] = sanitize_text_field( $merged['custom_hook'] );

		// Selector / position
		$merged['selector'] = sanitize_text_field( (string) $merged['selector'] );
		if ( ! array_key_exists( $merged['selector_position'], self::selector_positions() ) ) {
			$merged['selector_position'] = 'afterend';
		}

		// Heading color: empty or a valid hex (#rgb / #rrggbb / #rrggbbaa). If
		// it isn't one of those, just blank it — we don't want to emit invalid
		// inline CSS.
		$color = trim( (string) $merged['heading_color'] );
		if ( '' !== $color && ! preg_match( '/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i', $color ) ) {
			$color = '';
		}
		$merged['heading_color'] = $color;

		// Heading size: must be one of the named presets or blank.
		if ( ! array_key_exists( $merged['heading_size'], self::heading_size_options() ) ) {
			$merged['heading_size'] = '';
		}

		return $merged;
	}

	/**
	 * Sanitize-and-save settings. Called by the main settings page handler
	 * when its $_POST contains our `bw_schema_post_faq_*` fields.
	 */
	public static function save_settings_from_post() {
		$post_types = isset( $_POST['bw_schema_post_faq_post_types'] ) && is_array( $_POST['bw_schema_post_faq_post_types'] )
			? array_map( 'sanitize_key', wp_unslash( $_POST['bw_schema_post_faq_post_types'] ) )
			: array();

		$insertion_mode = isset( $_POST['bw_schema_post_faq_insertion_mode'] )
			? sanitize_key( wp_unslash( $_POST['bw_schema_post_faq_insertion_mode'] ) )
			: 'the_content';

		$new = array(
			'enabled'           => ! empty( $_POST['bw_schema_post_faq_enabled'] ) ? 1 : 0,
			'post_types'        => array_values( array_filter( $post_types ) ),
			'default_heading'   => sanitize_text_field( wp_unslash( $_POST['bw_schema_post_faq_default_heading'] ?? '' ) ),
			'default_accordion' => ! empty( $_POST['bw_schema_post_faq_default_accordion'] ) ? 1 : 0,
			'insertion_mode'    => $insertion_mode,
			'custom_hook'       => sanitize_text_field( wp_unslash( $_POST['bw_schema_post_faq_custom_hook'] ?? '' ) ),
			'selector'          => sanitize_text_field( wp_unslash( $_POST['bw_schema_post_faq_selector'] ?? '' ) ),
			'selector_position' => sanitize_key( wp_unslash( $_POST['bw_schema_post_faq_selector_position'] ?? 'afterend' ) ),
			'heading_color'     => ! empty( $_POST['bw_schema_post_faq_heading_color_override'] )
				? sanitize_text_field( wp_unslash( $_POST['bw_schema_post_faq_heading_color'] ?? '' ) )
				: '',
			'heading_size'      => sanitize_text_field( wp_unslash( $_POST['bw_schema_post_faq_heading_size'] ?? '' ) ),
		);

		if ( empty( $new['post_types'] ) ) {
			$new['post_types'] = array( 'post' );
		}

		if ( ! array_key_exists( $new['insertion_mode'], self::insertion_modes() ) ) {
			$new['insertion_mode'] = 'the_content';
		}

		update_option( self::OPT_SETTINGS, $new );
	}

	/* ---------------- Per-post resolution ---------------- */

	/**
	 * Items for a post — sanitized, in saved order, empty rows stripped.
	 * @return array<int, array{question:string, answer:string}>
	 */
	public static function get_items( $post_id ) {
		$raw = get_post_meta( $post_id, self::META_ITEMS, true );
		if ( ! is_array( $raw ) ) {
			return array();
		}
		$out = array();
		foreach ( $raw as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$q = isset( $row['question'] ) ? trim( (string) $row['question'] ) : '';
			$a = isset( $row['answer'] ) ? trim( (string) $row['answer'] ) : '';
			if ( '' === $q || '' === wp_strip_all_tags( $a ) ) {
				continue;
			}
			$out[] = array( 'question' => $q, 'answer' => $a );
		}
		return $out;
	}

	public static function resolve_heading( $post_id ) {
		$per_post = get_post_meta( $post_id, self::META_HEADING, true );
		if ( is_string( $per_post ) && '' !== trim( $per_post ) ) {
			return $per_post;
		}
		return self::get_settings()['default_heading'];
	}

	public static function resolve_accordion( $post_id ) {
		$per_post = get_post_meta( $post_id, self::META_ACCORDION, true );
		if ( 'on' === $per_post ) {
			return true;
		}
		if ( 'off' === $per_post ) {
			return false;
		}
		return (bool) self::get_settings()['default_accordion'];
	}

	/* ---------------- Front-end insertion ---------------- */

	/**
	 * Should this post output the FAQ on the front-end? Centralized so the
	 * filter, action, and shortcode paths agree.
	 */
	private static function should_render_for_current_post() {
		if ( ! is_singular() ) {
			return false;
		}
		$post = get_post();
		if ( ! $post ) {
			return false;
		}
		$settings = self::get_settings();
		if ( ! in_array( $post->post_type, $settings['post_types'], true ) ) {
			return false;
		}
		// Allow per-post disable.
		if ( 'off' === get_post_meta( $post->ID, '_bw_schema_post_faq_disable', true ) ) {
			return false;
		}
		// Don't double-render: if the shortcode already injected on this
		// post-render lifecycle, skip the auto-insert.
		if ( ! empty( $GLOBALS['bw_schema_post_faq_rendered'][ $post->ID ] ) ) {
			return false;
		}
		return self::get_items( $post->ID ) !== array();
	}

	/**
	 * the_content filter: append our markup AFTER the post body.
	 */
	public static function append_to_filter_content( $content ) {
		if ( ! in_the_loop() || ! is_main_query() ) {
			return $content;
		}
		if ( ! self::should_render_for_current_post() ) {
			return $content;
		}
		$post = get_post();
		$html = self::render_html( $post->ID );
		$GLOBALS['bw_schema_post_faq_rendered'][ $post->ID ] = true;
		return $content . $html;
	}

	/**
	 * Generic action callback: echo our markup directly. Used by the
	 * Kadence / Genesis / Astra / GeneratePress / custom-hook paths.
	 */
	public static function echo_for_action() {
		if ( ! self::should_render_for_current_post() ) {
			return;
		}
		$post = get_post();
		echo self::render_html( $post->ID );
		$GLOBALS['bw_schema_post_faq_rendered'][ $post->ID ] = true;
	}

	/**
	 * CSS-selector mode: render the FAQ hidden in the footer, then ship a
	 * tiny vanilla-JS snippet that finds the configured selector and moves
	 * the FAQ next to it via insertAdjacentElement. The FAQPage JSON-LD
	 * goes along for the ride inside the hidden container — JSON-LD is
	 * parsed from the DOM regardless of CSS display state, so schema
	 * validators still see it even before the JS runs.
	 *
	 * This is the only insertion mode that emits client-side JS.
	 */
	public static function render_for_selector_mode() {
		if ( ! self::should_render_for_current_post() ) {
			return;
		}
		$post     = get_post();
		$settings = self::get_settings();
		$selector = $settings['selector'];
		$position = $settings['selector_position'];

		if ( '' === $selector ) {
			return;
		}

		$faq_html = self::render_html( $post->ID );
		if ( '' === $faq_html ) {
			return;
		}

		// Hidden wrapper. `hidden` attribute = display:none + accessibility
		// hidden, which is what we want — the FAQ shouldn't paint at the
		// footer before the JS gets a chance to move it.
		echo '<div id="bw-schema-faq-deferred" hidden>' . $faq_html . '</div>';
		echo self::selector_injection_script( $selector, $position );

		$GLOBALS['bw_schema_post_faq_rendered'][ $post->ID ] = true;
	}

	/**
	 * Vanilla-JS DOM mover for the selector mode. Idempotent + cheap.
	 * Falls back to leaving the FAQ hidden if the selector doesn't match
	 * (so the page doesn't suddenly grow a FAQ at the bottom — better to
	 * stay invisible than appear in the wrong place).
	 */
	private static function selector_injection_script( $selector, $position ) {
		// Whitelist position to the four insertAdjacentElement values, just
		// in case something funky made it to the DB.
		if ( ! in_array( $position, array( 'afterend', 'beforebegin', 'afterbegin', 'beforeend' ), true ) ) {
			$position = 'afterend';
		}
		$selector_js = wp_json_encode( $selector );
		$position_js = wp_json_encode( $position );
		$js = <<<JS
(function(){
	function go(){
		var deferred = document.getElementById('bw-schema-faq-deferred');
		if (!deferred) { return; }
		var faq = deferred.firstElementChild;
		if (!faq) { return; }
		var target;
		try { target = document.querySelector({$selector_js}); }
		catch (e) { return; }
		if (!target) { return; }
		target.insertAdjacentElement({$position_js}, faq);
		faq.hidden = false;
		deferred.remove();
	}
	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', go);
	} else {
		go();
	}
})();
JS;
		return '<script>' . $js . '</script>';
	}

	/**
	 * Shortcode handler.
	 * Usage: [bw_schema_faq] (current post) or [bw_schema_faq post_id="123"].
	 */
	public static function shortcode( $atts ) {
		$atts = shortcode_atts( array( 'post_id' => 0 ), $atts, self::SHORTCODE );
		$post_id = (int) $atts['post_id'];
		if ( ! $post_id ) {
			$post = get_post();
			if ( ! $post ) {
				return '';
			}
			$post_id = $post->ID;
		}
		$settings = self::get_settings();
		if ( ! $settings['enabled'] ) {
			return '';
		}
		if ( self::get_items( $post_id ) === array() ) {
			return '';
		}
		$html = self::render_html( $post_id );
		$GLOBALS['bw_schema_post_faq_rendered'][ $post_id ] = true;
		return $html;
	}

	/* ---------------- Render ---------------- */

	/**
	 * Build the FAQ section markup + inline FAQPage JSON-LD for a single
	 * post. Returns '' if there are no items.
	 */
	public static function render_html( $post_id ) {
		$post_id = (int) $post_id;
		$items   = self::get_items( $post_id );
		if ( empty( $items ) ) {
			return '';
		}

		$heading   = self::resolve_heading( $post_id );
		$accordion = self::resolve_accordion( $post_id );
		$settings  = self::get_settings();
		$section_class = 'bw-schema-faq' . ( $accordion ? ' bw-schema-faq--accordion' : '' );
		$heading_id    = 'bw-schema-faq-heading-' . $post_id;

		// Optional heading style overrides — only emitted when set, so by
		// default the heading inherits the theme's H2 styles.
		$heading_inline_style = '';
		if ( '' !== $settings['heading_color'] ) {
			$heading_inline_style .= 'color:' . $settings['heading_color'] . ';';
		}
		if ( '' !== $settings['heading_size'] ) {
			$heading_inline_style .= 'font-size:' . $settings['heading_size'] . ';';
		}
		$heading_style_attr = '' !== $heading_inline_style
			? ' style="' . esc_attr( $heading_inline_style ) . '"'
			: '';

		$schema = array(
			'@context'   => 'https://schema.org',
			'@type'      => 'FAQPage',
			'mainEntity' => array(),
		);

		$html  = '<section class="' . esc_attr( $section_class ) . '" aria-labelledby="' . esc_attr( $heading_id ) . '">';
		$html .= self::inline_styles();
		$html .= '<h2 id="' . esc_attr( $heading_id ) . '" class="bw-schema-faq-heading"' . $heading_style_attr . '>' . esc_html( $heading ) . '</h2>';
		$html .= '<div class="bw-schema-faq-list">';

		foreach ( $items as $i => $item ) {
			$answer_html = wpautop( wp_kses_post( $item['answer'] ) );
			$panel_id    = 'bw-schema-faq-a-' . $post_id . '-' . $i;

			$html .= '<div class="bw-schema-faq-item">';
			if ( $accordion ) {
				$html .= '<h3 class="bw-schema-faq-q">';
				$html .= '<button type="button" class="bw-schema-faq-toggle" aria-expanded="false" aria-controls="' . esc_attr( $panel_id ) . '">';
				$html .= '<span class="bw-schema-faq-q-text">' . esc_html( $item['question'] ) . '</span>';
				$html .= '<span class="bw-schema-faq-icon" aria-hidden="true"></span>';
				$html .= '</button>';
				$html .= '</h3>';
				$html .= '<div id="' . esc_attr( $panel_id ) . '" class="bw-schema-faq-a" role="region" aria-labelledby="' . esc_attr( $heading_id ) . '" hidden>' . $answer_html . '</div>';
			} else {
				$html .= '<h3 class="bw-schema-faq-q">' . esc_html( $item['question'] ) . '</h3>';
				$html .= '<div class="bw-schema-faq-a">' . $answer_html . '</div>';
			}
			$html .= '</div>';

			$schema['mainEntity'][] = array(
				'@type'          => 'Question',
				'name'           => wp_strip_all_tags( $item['question'] ),
				'acceptedAnswer' => array(
					'@type' => 'Answer',
					'text'  => trim( wp_kses_post( $item['answer'] ) ),
				),
			);
		}

		$html .= '</div>';
		$html .= '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . '</script>';

		if ( $accordion ) {
			$html .= self::accordion_script();
		}

		$html .= '</section>';

		return $html;
	}

	/**
	 * Minimal styles — printed inline next to the section. Uses `currentColor`
	 * and `em` units so it inherits the surrounding theme's typography +
	 * link color. The class names are namespaced so no theme is going to
	 * accidentally collide. CSS is small enough that the duplication cost
	 * across pages is trivial; emitting inline avoids an extra HTTP request
	 * and the cache-busting headaches that come with a separate stylesheet.
	 */
	private static function inline_styles() {
		static $printed = false;
		if ( $printed ) {
			return '';
		}
		$printed = true;
		return '<style>'
			. '.bw-schema-faq{margin:2.5em 0;}'
			. '.bw-schema-faq .bw-schema-faq-heading{margin:0 0 1em;}'
			. '.bw-schema-faq .bw-schema-faq-item{margin:0 0 1.25em;padding:0 0 1.25em;border-bottom:1px solid currentColor;border-bottom-color:rgba(127,127,127,.2);}'
			. '.bw-schema-faq .bw-schema-faq-item:last-child{border-bottom:0;margin-bottom:0;padding-bottom:0;}'
			. '.bw-schema-faq .bw-schema-faq-q{margin:0 0 .5em;font-size:1.1em;line-height:1.35;}'
			. '.bw-schema-faq .bw-schema-faq-a > *:first-child{margin-top:0;}'
			. '.bw-schema-faq .bw-schema-faq-a > *:last-child{margin-bottom:0;}'
			// Accordion variant.
			. '.bw-schema-faq--accordion .bw-schema-faq-q{margin:0;}'
			. '.bw-schema-faq--accordion .bw-schema-faq-toggle{display:flex;align-items:center;justify-content:space-between;gap:.75em;width:100%;padding:0;margin:0;background:transparent;border:0;cursor:pointer;text-align:left;font:inherit;color:inherit;}'
			. '.bw-schema-faq--accordion .bw-schema-faq-toggle:focus-visible{outline:2px solid currentColor;outline-offset:2px;}'
			. '.bw-schema-faq--accordion .bw-schema-faq-icon{flex:0 0 auto;position:relative;width:.85em;height:.85em;}'
			. '.bw-schema-faq--accordion .bw-schema-faq-icon::before,.bw-schema-faq--accordion .bw-schema-faq-icon::after{content:"";position:absolute;background:currentColor;transition:transform .2s ease;}'
			. '.bw-schema-faq--accordion .bw-schema-faq-icon::before{top:calc(50% - 1px);left:0;width:100%;height:2px;}'
			. '.bw-schema-faq--accordion .bw-schema-faq-icon::after{top:0;left:calc(50% - 1px);width:2px;height:100%;}'
			. '.bw-schema-faq--accordion .bw-schema-faq-toggle[aria-expanded="true"] .bw-schema-faq-icon::after{transform:scaleY(0);}'
			. '.bw-schema-faq--accordion .bw-schema-faq-a{padding-top:.75em;}'
			. '.bw-schema-faq--accordion .bw-schema-faq-a[hidden]{display:none;}'
			. '</style>';
	}

	/**
	 * Accordion behavior: vanilla JS, no jQuery, no dependency on
	 * the surrounding theme. Idempotent — safe to render multiple times
	 * on a page; only the first instance binds listeners (via the dataset
	 * `bwSchemaFaqReady` flag).
	 */
	private static function accordion_script() {
		static $printed = false;
		if ( $printed ) {
			return '';
		}
		$printed = true;
		$js = <<<'JS'
(function(){
	function init(){
		var sections = document.querySelectorAll('.bw-schema-faq--accordion');
		Array.prototype.forEach.call(sections, function(section){
			if (section.dataset.bwSchemaFaqReady) { return; }
			section.dataset.bwSchemaFaqReady = '1';
			var toggles = section.querySelectorAll('.bw-schema-faq-toggle');
			Array.prototype.forEach.call(toggles, function(btn){
				btn.addEventListener('click', function(){
					var open = btn.getAttribute('aria-expanded') === 'true';
					Array.prototype.forEach.call(toggles, function(other){
						other.setAttribute('aria-expanded', 'false');
						var p = document.getElementById(other.getAttribute('aria-controls'));
						if (p) { p.hidden = true; }
					});
					if (!open) {
						btn.setAttribute('aria-expanded', 'true');
						var panel = document.getElementById(btn.getAttribute('aria-controls'));
						if (panel) { panel.hidden = false; }
					}
				});
			});
		});
	}
	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', init);
	} else {
		init();
	}
})();
JS;
		return '<script>' . $js . '</script>';
	}

	/* ---------------- Admin: metabox ---------------- */

	private static function admin_init() {
		add_action( 'add_meta_boxes', array( __CLASS__, 'register_metabox' ) );
		add_action( 'save_post', array( __CLASS__, 'save_metabox' ), 10, 2 );
		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin_assets' ) );
	}

	public static function register_metabox() {
		foreach ( self::get_settings()['post_types'] as $pt ) {
			if ( ! post_type_exists( $pt ) ) {
				continue;
			}
			add_meta_box(
				'bw_schema_post_faq',
				__( 'FAQ', 'bw-ai-schema-pro' ),
				array( __CLASS__, 'render_metabox' ),
				$pt,
				'normal',
				'default'
			);
		}
	}

	public static function enqueue_admin_assets( $hook ) {
		if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) {
			return;
		}
		$screen = get_current_screen();
		if ( ! $screen ) {
			return;
		}
		if ( ! in_array( $screen->post_type, self::get_settings()['post_types'], true ) ) {
			return;
		}
		wp_enqueue_script( 'jquery-ui-sortable' );

		$js = <<<'JS'
jQuery(function($){
	var $wrap = $('.bw-schema-faq-wrap');
	if (!$wrap.length) { return; }
	var $rows = $wrap.find('.bw-schema-faq-rows');
	$rows.sortable({ handle: '.bw-schema-faq-handle', placeholder: 'bw-schema-faq-placeholder', forcePlaceholderSize: true, axis: 'y' });
	$wrap.on('click', '.bw-schema-faq-add', function(){
		var tmpl = $('#tmpl-bw-schema-faq-row').html();
		$rows.append(tmpl);
	});
	$wrap.on('click', '.bw-schema-faq-remove', function(){
		$(this).closest('.bw-schema-faq-row').remove();
	});
});
JS;
		wp_add_inline_script( 'jquery-ui-sortable', $js );
	}

	private static function render_row( $question = '', $answer = '' ) {
		?>
		<div class="bw-schema-faq-row">
			<span class="bw-schema-faq-handle dashicons dashicons-menu" title="<?php esc_attr_e( 'Drag to reorder', 'bw-ai-schema-pro' ); ?>"></span>
			<div class="bw-schema-faq-fields">
				<input type="text" class="widefat bw-schema-faq-q" name="bw_schema_faq[question][]" value="<?php echo esc_attr( $question ); ?>" placeholder="<?php esc_attr_e( 'Question', 'bw-ai-schema-pro' ); ?>" />
				<textarea class="widefat bw-schema-faq-a" name="bw_schema_faq[answer][]" rows="4" placeholder="<?php esc_attr_e( 'Answer', 'bw-ai-schema-pro' ); ?>"><?php echo esc_textarea( $answer ); ?></textarea>
			</div>
			<button type="button" class="button-link bw-schema-faq-remove" aria-label="<?php esc_attr_e( 'Remove question', 'bw-ai-schema-pro' ); ?>">&times;</button>
		</div>
		<?php
	}

	public static function render_metabox( $post ) {
		wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD );
		$items      = self::get_items( $post->ID );
		$global     = self::get_settings();
		$heading    = get_post_meta( $post->ID, self::META_HEADING, true );
		$accordion  = get_post_meta( $post->ID, self::META_ACCORDION, true );
		$disabled   = 'off' === get_post_meta( $post->ID, '_bw_schema_post_faq_disable', true );
		$accordion_default_label = $global['default_accordion']
			? __( 'On', 'bw-ai-schema-pro' )
			: __( 'Off', 'bw-ai-schema-pro' );
		?>
		<div class="bw-schema-faq-wrap">
			<p class="description">
				<?php esc_html_e( 'Add questions and answers to show on this post. Drag the handle to reorder. Basic HTML (links, bold) is allowed in answers. Leave empty to show nothing.', 'bw-ai-schema-pro' ); ?>
			</p>

			<table class="form-table">
				<tr>
					<th scope="row"><label for="bw_schema_faq_heading"><?php esc_html_e( 'Heading', 'bw-ai-schema-pro' ); ?></label></th>
					<td>
						<input type="text" id="bw_schema_faq_heading" class="regular-text" name="bw_schema_faq_heading" value="<?php echo esc_attr( is_string( $heading ) ? $heading : '' ); ?>" placeholder="<?php echo esc_attr( $global['default_heading'] ); ?>" />
						<p class="description"><?php esc_html_e( 'Leave blank to use the site default shown in the placeholder.', 'bw-ai-schema-pro' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><label for="bw_schema_faq_accordion"><?php esc_html_e( 'Accordion style', 'bw-ai-schema-pro' ); ?></label></th>
					<td>
						<select id="bw_schema_faq_accordion" name="bw_schema_faq_accordion">
							<option value="" <?php selected( ! in_array( $accordion, array( 'on', 'off' ), true ) ); ?>>
								<?php
								/* translators: %s: site default value (On/Off) */
								printf( esc_html__( 'Use site default (%s)', 'bw-ai-schema-pro' ), esc_html( $accordion_default_label ) );
								?>
							</option>
							<option value="on" <?php selected( 'on', $accordion ); ?>><?php esc_html_e( 'On', 'bw-ai-schema-pro' ); ?></option>
							<option value="off" <?php selected( 'off', $accordion ); ?>><?php esc_html_e( 'Off', 'bw-ai-schema-pro' ); ?></option>
						</select>
					</td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Show on this post', 'bw-ai-schema-pro' ); ?></th>
					<td>
						<label>
							<input type="checkbox" name="bw_schema_faq_disable" value="off" <?php checked( $disabled ); ?> />
							<?php esc_html_e( 'Hide the FAQ on this post (overrides global enable)', 'bw-ai-schema-pro' ); ?>
						</label>
					</td>
				</tr>
			</table>

			<div class="bw-schema-faq-rows">
				<?php
				foreach ( $items as $item ) {
					self::render_row( $item['question'], $item['answer'] );
				}
				?>
			</div>
			<p>
				<button type="button" class="button bw-schema-faq-add"><?php esc_html_e( '+ Add question', 'bw-ai-schema-pro' ); ?></button>
			</p>
		</div>

		<script type="text/html" id="tmpl-bw-schema-faq-row">
			<?php self::render_row(); ?>
		</script>

		<style>
			.bw-schema-faq-row { display:flex; align-items:flex-start; gap:8px; padding:12px; margin-bottom:8px; background:#fff; border:1px solid #dcdcde; border-radius:4px; }
			.bw-schema-faq-handle { cursor:move; color:#8c8f94; margin-top:4px; }
			.bw-schema-faq-fields { flex:1; }
			.bw-schema-faq-fields .bw-schema-faq-q { margin-bottom:6px; font-weight:600; }
			.bw-schema-faq-remove { color:#b32d2e; font-size:20px; line-height:1; margin-top:2px; }
			.bw-schema-faq-remove:hover { color:#d63638; }
			.bw-schema-faq-placeholder { border:1px dashed #c3c4c7; background:#f0f0f1; border-radius:4px; margin-bottom:8px; }
		</style>
		<?php
	}

	public static function save_metabox( $post_id, $post ) {
		if ( ! isset( $_POST[ self::NONCE_FIELD ] ) ) {
			return;
		}
		if ( ! wp_verify_nonce( wp_unslash( $_POST[ self::NONCE_FIELD ] ), self::NONCE_ACTION ) ) {
			return;
		}
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}
		if ( ! in_array( $post->post_type, self::get_settings()['post_types'], true ) ) {
			return;
		}

		// Items.
		$questions = isset( $_POST['bw_schema_faq']['question'] ) && is_array( $_POST['bw_schema_faq']['question'] )
			? (array) $_POST['bw_schema_faq']['question']
			: array();
		$answers = isset( $_POST['bw_schema_faq']['answer'] ) && is_array( $_POST['bw_schema_faq']['answer'] )
			? (array) $_POST['bw_schema_faq']['answer']
			: array();

		$items = array();
		foreach ( $questions as $i => $q ) {
			$q_clean = sanitize_text_field( wp_unslash( $q ) );
			$a_clean = isset( $answers[ $i ] ) ? wp_kses_post( wp_unslash( $answers[ $i ] ) ) : '';
			if ( '' === trim( $q_clean ) && '' === trim( wp_strip_all_tags( $a_clean ) ) ) {
				continue;
			}
			$items[] = array( 'question' => $q_clean, 'answer' => $a_clean );
		}
		if ( $items ) {
			update_post_meta( $post_id, self::META_ITEMS, $items );
		} else {
			delete_post_meta( $post_id, self::META_ITEMS );
		}

		// Heading override.
		$heading = isset( $_POST['bw_schema_faq_heading'] ) ? sanitize_text_field( wp_unslash( $_POST['bw_schema_faq_heading'] ) ) : '';
		if ( '' !== $heading ) {
			update_post_meta( $post_id, self::META_HEADING, $heading );
		} else {
			delete_post_meta( $post_id, self::META_HEADING );
		}

		// Accordion override.
		$accordion = isset( $_POST['bw_schema_faq_accordion'] ) ? wp_unslash( $_POST['bw_schema_faq_accordion'] ) : '';
		if ( in_array( $accordion, array( 'on', 'off' ), true ) ) {
			update_post_meta( $post_id, self::META_ACCORDION, $accordion );
		} else {
			delete_post_meta( $post_id, self::META_ACCORDION );
		}

		// Per-post disable.
		if ( ! empty( $_POST['bw_schema_faq_disable'] ) && 'off' === wp_unslash( $_POST['bw_schema_faq_disable'] ) ) {
			update_post_meta( $post_id, '_bw_schema_post_faq_disable', 'off' );
		} else {
			delete_post_meta( $post_id, '_bw_schema_post_faq_disable' );
		}
	}
}
