<?php
/**
 * Legal Checkboxes module.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar\Modules\Compliance;

// Prevent direct access.
defined( 'ABSPATH' ) || exit;

/**
 * Legal Checkboxes module.
 *
 * Adds EU-required legal acceptance checkboxes to the WooCommerce checkout.
 * Supports both Classic (shortcode) and Blocks checkout.
 *
 * @since 1.0.0
 */
final class Legal_Checkboxes extends \Kramar\Module {

	/**
	 * {@inheritDoc}
	 */
	public function get_slug(): string {
		return 'legal-checkboxes';
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_category(): string {
		return 'compliance';
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_name(): string {
		return __( 'Legal Checkboxes', 'kramar' );
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_description(): string {
		return __( 'Add EU-required legal acceptance checkboxes to the WooCommerce checkout: terms, privacy, digital goods waiver, cookie policy, and a statutory guarantee notice.', 'kramar' );
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_default_settings(): array {
		return [
			'enabled'               => false,
			// Terms checkbox.
			'terms_enabled'         => true,
			'terms_required'        => true,
			'terms_label'           => __( 'I agree to the {terms_link} and the {revocation_link}', 'kramar' ),
			'terms_page_id'         => 0,
			'revocation_page_id'    => 0,
			// Hide WooCommerce's own terms & conditions checkbox to avoid a
			// duplicate acceptance box when a WC terms page is configured.
			// Defaults to on: with a WooCommerce Terms page configured, WC renders
			// its own required terms box on the classic checkout, so leaving this
			// off showed the customer two near-identical "I agree to the terms"
			// checkboxes out of the box. Only affects new installs — existing
			// sites keep whatever they already have stored.
			'hide_wc_terms'         => true,
			// Privacy checkbox.
			'privacy_enabled'       => true,
			'privacy_required'      => true,
			'privacy_label'         => __( 'I have read the {privacy_link}', 'kramar' ),
			'privacy_page_id'       => 0,
			// Digital goods waiver.
			'digital_waiver_enabled'  => true,
			'digital_waiver_required' => true,
			'digital_waiver_label'    => __( 'I agree to waive my right of withdrawal for digital products', 'kramar' ),
			// Cookie policy.
			'cookie_enabled'          => false,
			'cookie_required'         => true,
			'cookie_label'            => __( 'I have read the {cookie_link}', 'kramar' ),
			'cookie_page_id'          => 0,
			// Guarantee notice.
			'guarantee_enabled'     => true,
			'guarantee_text'        => __( 'All products come with a statutory guarantee of at least 2 years. Learn more in our {guarantee_link}.', 'kramar' ),
			'guarantee_page_id'     => 0,
		];
	}

	/**
	 * {@inheritDoc}
	 */
	public function sanitize_settings( array $input ): array {
		return [
			'enabled'                => ! empty( $input['enabled'] ),
			'terms_enabled'          => ! empty( $input['terms_enabled'] ),
			'terms_required'         => ! empty( $input['terms_required'] ),
			'terms_label'            => sanitize_text_field( $input['terms_label'] ?? __( 'I agree to the {terms_link} and the {revocation_link}', 'kramar' ) ),
			'terms_page_id'          => absint( $input['terms_page_id'] ?? 0 ),
			'revocation_page_id'     => absint( $input['revocation_page_id'] ?? 0 ),
			'hide_wc_terms'          => ! empty( $input['hide_wc_terms'] ),
			'privacy_enabled'        => ! empty( $input['privacy_enabled'] ),
			'privacy_required'       => ! empty( $input['privacy_required'] ),
			'privacy_label'          => sanitize_text_field( $input['privacy_label'] ?? __( 'I have read the {privacy_link}', 'kramar' ) ),
			'privacy_page_id'        => absint( $input['privacy_page_id'] ?? 0 ),
			'digital_waiver_enabled'  => ! empty( $input['digital_waiver_enabled'] ),
			'digital_waiver_required' => ! empty( $input['digital_waiver_required'] ),
			'digital_waiver_label'    => sanitize_text_field( $input['digital_waiver_label'] ?? __( 'I agree to waive my right of withdrawal for digital products', 'kramar' ) ),
			'cookie_enabled'          => ! empty( $input['cookie_enabled'] ),
			'cookie_required'         => ! empty( $input['cookie_required'] ),
			'cookie_label'            => sanitize_text_field( $input['cookie_label'] ?? __( 'I have read the {cookie_link}', 'kramar' ) ),
			'cookie_page_id'          => absint( $input['cookie_page_id'] ?? 0 ),
			'guarantee_enabled'      => ! empty( $input['guarantee_enabled'] ),
			'guarantee_text'         => sanitize_text_field( $input['guarantee_text'] ?? __( 'All products come with a statutory guarantee of at least 2 years. Learn more in our {guarantee_link}.', 'kramar' ) ),
			'guarantee_page_id'      => absint( $input['guarantee_page_id'] ?? 0 ),
		];
	}

	/**
	 * {@inheritDoc}
	 */
	protected function run(): void {
		// Blocks checkout: register additional checkout fields.
		// woocommerce_blocks_loaded has already fired by the time run() is called
		// (WC boots during plugins_loaded; Kramar's run() is called on init priority 0).
		// Call the registration method directly; it guards on function_exists() internally.
		$this->register_blocks_fields();

		// Classic checkout: render and validate checkboxes.
		add_action( 'woocommerce_review_order_before_submit', [ $this, 'render_classic_checkboxes' ], 9 );
		add_action( 'woocommerce_after_checkout_validation', [ $this, 'validate_classic_checkboxes' ], 10, 2 );

		// Avoid a duplicate terms checkbox: when WooCommerce's own terms page is
		// configured it renders its own required T&C box on the Classic checkout.
		// If Kramar's terms box is also enabled the customer sees two, so — when the
		// operator opts in — suppress WooCommerce's native terms UI.
		if (
			$this->s( 'terms_enabled' ) &&
			$this->s( 'hide_wc_terms' ) &&
			(int) get_option( 'woocommerce_terms_page_id', 0 ) > 0
		) {
			add_filter( 'woocommerce_checkout_show_terms', '__return_false' );
		}

		// Store consent on the order (both paths).
		add_action( 'woocommerce_checkout_order_processed', [ $this, 'store_classic_consent' ], 10, 3 );
		add_action( 'woocommerce_store_api_checkout_order_processed', [ $this, 'store_blocks_consent' ] );
	}

	// -------------------------------------------------------------------------
	// Blocks checkout
	// -------------------------------------------------------------------------

	/**
	 * Register WooCommerce Blocks additional checkout fields (WC 8.9+).
	 *
	 * @since 1.0.0
	 */
	public function register_blocks_fields(): void {
		if ( ! function_exists( 'woocommerce_register_additional_checkout_field' ) ) {
			return;
		}

		// Terms & Conditions.
		if ( $this->s( 'terms_enabled' ) ) {
			woocommerce_register_additional_checkout_field(
				[
					'id'       => 'kramar/terms-acceptance',
					'label'    => $this->resolve_label_plain( 'terms_label' ),
					'location' => 'order',
					'type'     => 'checkbox',
					'required' => (bool) $this->get_setting( 'terms_required' ),
				]
			);
		}

		// Privacy Policy.
		if ( $this->s( 'privacy_enabled' ) ) {
			woocommerce_register_additional_checkout_field(
				[
					'id'       => 'kramar/privacy-acceptance',
					'label'    => $this->resolve_label_plain( 'privacy_label' ),
					'location' => 'order',
					'type'     => 'checkbox',
					'required' => (bool) $this->get_setting( 'privacy_required' ),
				]
			);
		}

		// Digital Goods Waiver.
		if ( $this->s( 'digital_waiver_enabled' ) ) {
			$dw_required = (bool) $this->get_setting( 'digital_waiver_required' );
			woocommerce_register_additional_checkout_field(
				[
					'id'                => 'kramar/digital-waiver',
					'label'             => (string) $this->get_setting( 'digital_waiver_label' ),
					'location'          => 'order',
					'type'              => 'checkbox',
					'required'          => $dw_required,
					'validate_callback' => $dw_required ? [ $this, 'validate_blocks_digital_waiver' ] : null,
				]
			);
		}

		// Cookie Policy.
		if ( $this->s( 'cookie_enabled' ) ) {
			woocommerce_register_additional_checkout_field(
				[
					'id'       => 'kramar/cookie-acceptance',
					'label'    => $this->resolve_label_plain( 'cookie_label' ),
					'location' => 'order',
					'type'     => 'checkbox',
					'required' => (bool) $this->get_setting( 'cookie_required' ),
				]
			);
		}

		// Guarantee notice: injected via inline JS as an informational text element.
		if ( $this->s( 'guarantee_enabled' ) ) {
			add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_guarantee_notice' ] );
		}
	}

	/**
	 * Conditionally require the digital waiver in Blocks checkout.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $value Submitted field value.
	 * @param array<string, mixed> $field Field configuration.
	 * @return \WP_Error|true WP_Error on failure, true on success.
	 */
	public function validate_blocks_digital_waiver( mixed $value, array $field ): \WP_Error|true {
		if ( ! $this->cart_has_downloadable() ) {
			return true;
		}
		if ( empty( $value ) ) {
			return new \WP_Error(
				'kramar_digital_waiver',
				__( 'Please confirm the waiver of your right of withdrawal for digital products.', 'kramar' )
			);
		}
		return true;
	}

	/**
	 * Inject the guarantee notice text before the Blocks order button via inline JS.
	 *
	 * @since 1.0.0
	 */
	public function enqueue_guarantee_notice(): void {
		if ( ! function_exists( 'is_checkout' ) || ! is_checkout() ) {
			return;
		}

		if (
			! wp_script_is( 'wc-blocks-checkout', 'enqueued' ) &&
			! wp_script_is( 'wc-blocks-checkout', 'registered' )
		) {
			return;
		}

		$parts = $this->guarantee_text_parts();
		if ( '' === trim( $parts['before'] . $parts['label'] . $parts['after'] ) ) {
			return;
		}

		// The notice is built from DOM nodes rather than innerHTML: the link has
		// to be clickable (a page is configured, and the admin promises a link),
		// but the surrounding text is admin-supplied and never becomes markup.
		//
		// Several anchors are tried because WooCommerce renames these wrappers
		// between releases: this looked only for .wc-block-checkout__place-order,
		// which WooCommerce no longer renders, so the notice never appeared on a
		// block checkout at all and the poll ran forever. The attempt cap stops
		// the interval on a page that has no checkout block.
		wp_add_inline_script(
			'wc-blocks-checkout',
			sprintf(
				'try{document.addEventListener("DOMContentLoaded",function(){' .
				'var p=%s;' .
				'var s=[".wc-block-checkout__actions_row",".wc-block-checkout__actions",' .
				'".wc-block-components-checkout-place-order-button",".wc-block-checkout__place-order"];' .
				'var n=0;' .
				'var i=setInterval(function(){' .
				'if(++n>50||document.querySelector(".kramar-guarantee-notice")){clearInterval(i);return;}' .
				'var b=null;for(var k=0;k<s.length&&!b;k++){b=document.querySelector(s[k]);}' .
				'if(!b||!b.parentNode){return;}' .
				'clearInterval(i);' .
				'var d=document.createElement("p");' .
				'd.className="kramar-guarantee-notice";' .
				'd.style.cssText="font-size:.875em;margin:8px 0;color:#555;";' .
				'd.appendChild(document.createTextNode(p.before));' .
				'if(p.label){' .
				'if(p.url){var a=document.createElement("a");a.href=p.url;a.target="_blank";a.rel="noopener";' .
				'a.appendChild(document.createTextNode(p.label));d.appendChild(a);}' .
				'else{d.appendChild(document.createTextNode(p.label));}}' .
				'd.appendChild(document.createTextNode(p.after));' .
				'b.parentNode.insertBefore(d,b);' .
				'},200);});' .
				'}catch(e){}',
				// JSON_HEX_TAG so an admin-supplied "</script>" cannot close the
				// inline script element it is embedded in. JS decodes the escapes
				// back to the literal characters, so the text is unchanged.
				wp_json_encode( $parts, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT )
			)
		);
	}

	// -------------------------------------------------------------------------
	// Classic checkout
	// -------------------------------------------------------------------------

	/**
	 * Render legal checkboxes before the Place Order button (Classic checkout).
	 *
	 * @since 1.0.0
	 */
	public function render_classic_checkboxes(): void {
		echo '<div class="kramar-legal-checkboxes">';

		// Terms & Conditions.
		if ( $this->s( 'terms_enabled' ) ) {
			$this->render_classic_checkbox(
				'terms',
				$this->resolve_label_html( 'terms_label' ),
				(bool) $this->get_setting( 'terms_required' )
			);
		}

		// Privacy Policy.
		if ( $this->s( 'privacy_enabled' ) ) {
			$this->render_classic_checkbox(
				'privacy',
				$this->resolve_label_html( 'privacy_label' ),
				(bool) $this->get_setting( 'privacy_required' )
			);
		}

		// Digital Goods Waiver — only shown when cart has downloadable products.
		if ( $this->s( 'digital_waiver_enabled' ) && $this->cart_has_downloadable() ) {
			$this->render_classic_checkbox(
				'digital_waiver',
				esc_html( (string) $this->get_setting( 'digital_waiver_label' ) ),
				(bool) $this->get_setting( 'digital_waiver_required' )
			);
		}

		// Cookie Policy.
		if ( $this->s( 'cookie_enabled' ) ) {
			$this->render_classic_checkbox(
				'cookie',
				$this->resolve_label_html( 'cookie_label' ),
				(bool) $this->get_setting( 'cookie_required' )
			);
		}

		// Guarantee notice.
		if ( $this->s( 'guarantee_enabled' ) ) {
			$html = $this->resolve_guarantee_html();
			if ( '' !== $html ) {
				printf(
					'<p class="kramar-guarantee-notice" style="font-size:.875em;margin:8px 0;color:#555;">%s</p>',
					$html // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- resolve_guarantee_html() escapes every part it assembles.
				);
			}
		}

		echo '</div>';
	}

	/**
	 * Render a single Classic checkout checkbox.
	 *
	 * Required boxes carry WooCommerce's own .validate-required row class so the
	 * checkbox takes part in WooCommerce's field validation — highlighted on blur
	 * and when a submission is rejected — matching core's terms.php. Server-side
	 * validation in validate_classic_checkboxes() is what actually enforces consent.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key      Short key (e.g. 'terms').
	 * @param string $label    Already-escaped HTML label.
	 * @param bool   $required Whether the checkbox is required.
	 */
	private function render_classic_checkbox( string $key, string $label, bool $required ): void {
		$field_id    = 'kramar_legal_' . $key;
		$checked_val = isset( $_POST[ $field_id ] ) ? (bool) $_POST[ $field_id ] : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing

		// WooCommerce's checkout.js reads required-ness off the parent .form-row
		// (`$parent.is('.validate-required')`), not off the input, so without this
		// class the box was invisible to WooCommerce's validation entirely: no
		// highlight, no field-level error, just a notice at the top of a long page.
		// This is the same markup core's own terms.php uses.
		//
		// It does NOT stop the submission on the default Place order button — that
		// path posts straight to wc-ajax=checkout without consulting
		// .validate-required, and core's own terms checkbox has the same gap. What
		// it buys is the field-level treatment: the row is highlighted on blur and
		// when the server rejects it, and the server-side errors below carry the
		// input id so the message is attached to the checkbox rather than only
		// appearing in the banner.
		//
		// Deliberately NOT the HTML `required` attribute: core omits it too, because
		// a native required checkbox that a theme has hidden makes the browser
		// refuse to submit with "An invalid form control is not focusable" and no
		// visible error — a hard checkout break on some themes.
		$row_classes = 'form-row kramar-legal-checkbox' . ( $required ? ' validate-required' : '' );

		?>
		<p class="<?php echo esc_attr( $row_classes ); ?>">
			<label for="<?php echo esc_attr( $field_id ); ?>" class="woocommerce-form__label checkbox">
				<input
					type="checkbox"
					id="<?php echo esc_attr( $field_id ); ?>"
					name="<?php echo esc_attr( $field_id ); ?>"
					value="1"
					<?php checked( $checked_val ); ?>
					<?php if ( $required ) : ?>
					aria-required="true"
					<?php endif; ?>
				/>
				<span><?php echo $label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped HTML by caller (resolve_label_html / esc_html) ?></span>
				<?php if ( $required ) : ?>
				<abbr class="required" title="<?php esc_attr_e( 'required', 'kramar' ); ?>">*</abbr>
				<?php endif; ?>
			</label>
		</p>
		<?php
	}

	/**
	 * Validate Classic checkout legal checkboxes.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $data   Checkout field data (unused).
	 * @param \WP_Error            $errors Error collector.
	 */
	public function validate_classic_checkboxes( array $data, \WP_Error $errors ): void {
		// phpcs:disable WordPress.Security.NonceVerification.Missing

		// Whether a box was required is decided here, from settings and cart
		// contents — the same conditions render_classic_checkboxes() uses. It
		// previously also required a hidden "…_field" sentinel to be present in
		// the POST, which meant a request that simply omitted the sentinel
		// skipped every check: the order went through with none of the legally
		// required consent given and nothing recorded. The client does not get
		// to say whether consent was needed. That sentinel is no longer rendered
		// at all, so nothing can come to depend on it again.

		// Terms & Conditions.
		if (
			$this->s( 'terms_enabled' ) &&
			$this->get_setting( 'terms_required' ) &&
			empty( $_POST['kramar_legal_terms'] )
		) {
			$errors->add( 'kramar_terms', __( 'Please accept the terms and conditions and withdrawal policy.', 'kramar' ), [ 'id' => 'kramar_legal_terms' ] );
		}

		// Privacy Policy.
		if (
			$this->s( 'privacy_enabled' ) &&
			$this->get_setting( 'privacy_required' ) &&
			empty( $_POST['kramar_legal_privacy'] )
		) {
			$errors->add( 'kramar_privacy', __( 'Please confirm that you have read the privacy policy.', 'kramar' ), [ 'id' => 'kramar_legal_privacy' ] );
		}

		// Digital Goods Waiver.
		if (
			$this->s( 'digital_waiver_enabled' ) &&
			$this->get_setting( 'digital_waiver_required' ) &&
			$this->cart_has_downloadable() &&
			empty( $_POST['kramar_legal_digital_waiver'] )
		) {
			$errors->add( 'kramar_digital_waiver', __( 'Please confirm the waiver of your right of withdrawal for digital products.', 'kramar' ), [ 'id' => 'kramar_legal_digital_waiver' ] );
		}

		// Cookie Policy.
		if (
			$this->s( 'cookie_enabled' ) &&
			$this->get_setting( 'cookie_required' ) &&
			empty( $_POST['kramar_legal_cookie'] )
		) {
			$errors->add( 'kramar_cookie', __( 'Please confirm that you have read the cookie policy.', 'kramar' ), [ 'id' => 'kramar_legal_cookie' ] );
		}

		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}

	// -------------------------------------------------------------------------
	// Consent storage
	// -------------------------------------------------------------------------

	/**
	 * Store consent timestamps on the order for Classic checkout.
	 *
	 * @since 1.0.0
	 *
	 * @param int                  $order_id   Order ID.
	 * @param array<string, mixed> $posted     Posted checkout data (unused).
	 * @param \WC_Order            $order      Order object.
	 */
	public function store_classic_consent( int $order_id, array $posted, \WC_Order $order ): void {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		$now = current_time( 'mysql' );

		if ( $this->s( 'terms_enabled' ) && ! empty( $_POST['kramar_legal_terms'] ) ) {
			$order->update_meta_data( '_kramar_consent_terms_at', $now );
		}

		if ( $this->s( 'privacy_enabled' ) && ! empty( $_POST['kramar_legal_privacy'] ) ) {
			$order->update_meta_data( '_kramar_consent_privacy_at', $now );
		}

		if ( $this->s( 'digital_waiver_enabled' ) && ! empty( $_POST['kramar_legal_digital_waiver'] ) ) {
			$order->update_meta_data( '_kramar_consent_digital_waiver_at', $now );
		}

		if ( $this->s( 'cookie_enabled' ) && ! empty( $_POST['kramar_legal_cookie'] ) ) {
			$order->update_meta_data( '_kramar_consent_cookie_at', $now );
		}

		$order->save();
		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Store consent timestamps on the order for Blocks checkout.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order Order object.
	 */
	public function store_blocks_consent( \WC_Order $order ): void {
		$now = current_time( 'mysql' );

		if ( $this->s( 'terms_enabled' ) && $order->get_meta( 'kramar/terms-acceptance' ) ) {
			$order->update_meta_data( '_kramar_consent_terms_at', $now );
		}

		if ( $this->s( 'privacy_enabled' ) && $order->get_meta( 'kramar/privacy-acceptance' ) ) {
			$order->update_meta_data( '_kramar_consent_privacy_at', $now );
		}

		if ( $this->s( 'digital_waiver_enabled' ) && $order->get_meta( 'kramar/digital-waiver' ) ) {
			$order->update_meta_data( '_kramar_consent_digital_waiver_at', $now );
		}

		if ( $this->s( 'cookie_enabled' ) && $order->get_meta( 'kramar/cookie-acceptance' ) ) {
			$order->update_meta_data( '_kramar_consent_cookie_at', $now );
		}

		$order->save();
	}

	// -------------------------------------------------------------------------
	// Admin settings UI
	// -------------------------------------------------------------------------

	/**
	 * {@inheritDoc}
	 */
	public function render_extra_settings( string $option_name ): void {
		$slug = $this->get_slug();
		$cat  = $this->get_category();

		$terms_enabled          = $this->s( 'terms_enabled' );
		$terms_required         = $this->s( 'terms_required' );
		$terms_label            = (string) $this->get_setting( 'terms_label' );
		$terms_page_id          = (int) $this->get_setting( 'terms_page_id' );
		$revocation_page_id     = (int) $this->get_setting( 'revocation_page_id' );
		$privacy_enabled        = $this->s( 'privacy_enabled' );
		$privacy_required       = $this->s( 'privacy_required' );
		$privacy_label          = (string) $this->get_setting( 'privacy_label' );
		$privacy_page_id        = (int) $this->get_setting( 'privacy_page_id' );
		$digital_waiver_enabled  = $this->s( 'digital_waiver_enabled' );
		$digital_waiver_required = $this->s( 'digital_waiver_required' );
		$digital_waiver_label    = (string) $this->get_setting( 'digital_waiver_label' );
		$cookie_enabled          = $this->s( 'cookie_enabled' );
		$cookie_required         = $this->s( 'cookie_required' );
		$cookie_label            = (string) $this->get_setting( 'cookie_label' );
		$cookie_page_id         = (int) $this->get_setting( 'cookie_page_id' );
		$guarantee_enabled      = $this->s( 'guarantee_enabled' );
		$guarantee_text         = (string) $this->get_setting( 'guarantee_text' );
		$guarantee_page_id      = (int) $this->get_setting( 'guarantee_page_id' );

		$n = $option_name . '[' . $cat . '][' . $slug . ']';
		?>
		<div class="kramar-module__extra">
			<div class="kramar-settings-stack">

				<p><strong><?php esc_html_e( 'Checkboxes', 'kramar' ); ?></strong></p>

				<?php /* ---- Terms & Conditions ---- */ ?>
				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $n ); ?>[terms_enabled]"
							value="1"
							<?php checked( $terms_enabled ); ?>
						/>
						<?php esc_html_e( 'Enable Terms &amp; Conditions checkbox', 'kramar' ); ?>
					</label>
				</p>
				<p style="padding-left:24px;">
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $n ); ?>[terms_required]"
							value="1"
							<?php checked( $terms_required ); ?>
						/>
						<?php esc_html_e( 'Required', 'kramar' ); ?>
					</label>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Label:', 'kramar' ); ?>
						<br />
						<textarea
							name="<?php echo esc_attr( $n ); ?>[terms_label]"
							rows="2"
							style="width:100%;"
						><?php echo esc_textarea( $terms_label ); ?></textarea>
					</label>
					<span class="description"><?php esc_html_e( 'Placeholders: {terms_link}, {revocation_link}', 'kramar' ); ?></span>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Terms page (0 = auto-detect from WooCommerce settings):', 'kramar' ); ?>
						<?php
						// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_dropdown_pages() escapes its name attribute and option labels internally.
						wp_dropdown_pages(
							[
								'name'              => $n . '[terms_page_id]',
								'selected'          => $terms_page_id,
								'show_option_none'  => __( '— Auto-detect —', 'kramar' ),
								'option_none_value' => '0',
							]
						);
						// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
						?>
					</label>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Revocation / withdrawal policy page:', 'kramar' ); ?>
						<?php
						// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_dropdown_pages() escapes its name attribute and option labels internally.
						wp_dropdown_pages(
							[
								'name'              => $n . '[revocation_page_id]',
								'selected'          => $revocation_page_id,
								'show_option_none'  => __( '— Not set —', 'kramar' ),
								'option_none_value' => '0',
							]
						);
						// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
						?>
					</label>
				</p>
				<?php
				$this->render_link_warning( $terms_enabled, $terms_label, '{terms_link}', $this->resolve_page_id( 'terms' ) );
				$this->render_link_warning( $terms_enabled, $terms_label, '{revocation_link}', $revocation_page_id );
				?>
				<?php
				$wc_terms_page = (int) get_option( 'woocommerce_terms_page_id', 0 );
				if ( $terms_enabled && $wc_terms_page > 0 ) :
					?>
					<p style="padding-left:24px;">
						<label>
							<input
								type="checkbox"
								name="<?php echo esc_attr( $n ); ?>[hide_wc_terms]"
								value="1"
								<?php checked( $this->s( 'hide_wc_terms' ) ); ?>
							/>
							<?php esc_html_e( 'Hide WooCommerce\'s built-in terms &amp; conditions checkbox', 'kramar' ); ?>
						</label>
						<span class="description">
							<?php esc_html_e( 'A WooCommerce Terms page is configured, so WooCommerce renders its own required terms checkbox on the classic checkout — customers would see two. Enable this to suppress WooCommerce\'s box and keep only Kramar\'s.', 'kramar' ); ?>
						</span>
					</p>
				<?php endif; ?>

				<?php /* ---- Privacy Policy ---- */ ?>
				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $n ); ?>[privacy_enabled]"
							value="1"
							<?php checked( $privacy_enabled ); ?>
						/>
						<?php esc_html_e( 'Enable Privacy Policy checkbox', 'kramar' ); ?>
					</label>
				</p>
				<p style="padding-left:24px;">
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $n ); ?>[privacy_required]"
							value="1"
							<?php checked( $privacy_required ); ?>
						/>
						<?php esc_html_e( 'Required', 'kramar' ); ?>
					</label>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Label:', 'kramar' ); ?>
						<br />
						<textarea
							name="<?php echo esc_attr( $n ); ?>[privacy_label]"
							rows="2"
							style="width:100%;"
						><?php echo esc_textarea( $privacy_label ); ?></textarea>
					</label>
					<span class="description"><?php esc_html_e( 'Placeholder: {privacy_link}', 'kramar' ); ?></span>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Privacy policy page (0 = auto-detect from WordPress settings):', 'kramar' ); ?>
						<?php
						// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_dropdown_pages() escapes its name attribute and option labels internally.
						wp_dropdown_pages(
							[
								'name'              => $n . '[privacy_page_id]',
								'selected'          => $privacy_page_id,
								'show_option_none'  => __( '— Auto-detect —', 'kramar' ),
								'option_none_value' => '0',
							]
						);
						// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
						?>
					</label>
				</p>
				<?php $this->render_link_warning( $privacy_enabled, $privacy_label, '{privacy_link}', $this->resolve_page_id( 'privacy' ) ); ?>

				<?php /* ---- Digital Goods Waiver ---- */ ?>
				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $n ); ?>[digital_waiver_enabled]"
							value="1"
							<?php checked( $digital_waiver_enabled ); ?>
						/>
						<?php esc_html_e( 'Enable Digital Goods Waiver checkbox', 'kramar' ); ?>
					</label>
					<span class="description"><?php esc_html_e( 'Only shown when the cart contains downloadable products.', 'kramar' ); ?></span>
				</p>
				<p style="padding-left:24px;">
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $n ); ?>[digital_waiver_required]"
							value="1"
							<?php checked( $digital_waiver_required ); ?>
						/>
						<?php esc_html_e( 'Required', 'kramar' ); ?>
					</label>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Label:', 'kramar' ); ?>
						<br />
						<textarea
							name="<?php echo esc_attr( $n ); ?>[digital_waiver_label]"
							rows="2"
							style="width:100%;"
						><?php echo esc_textarea( $digital_waiver_label ); ?></textarea>
					</label>
				</p>

				<?php /* ---- Cookie Policy ---- */ ?>
				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $n ); ?>[cookie_enabled]"
							value="1"
							<?php checked( $cookie_enabled ); ?>
						/>
						<?php esc_html_e( 'Enable Cookie Policy checkbox', 'kramar' ); ?>
					</label>
				</p>
				<p style="padding-left:24px;">
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $n ); ?>[cookie_required]"
							value="1"
							<?php checked( $cookie_required ); ?>
						/>
						<?php esc_html_e( 'Required', 'kramar' ); ?>
					</label>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Label:', 'kramar' ); ?>
						<br />
						<textarea
							name="<?php echo esc_attr( $n ); ?>[cookie_label]"
							rows="2"
							style="width:100%;"
						><?php echo esc_textarea( $cookie_label ); ?></textarea>
					</label>
					<span class="description"><?php esc_html_e( 'Placeholder: {cookie_link}', 'kramar' ); ?></span>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Cookie policy page:', 'kramar' ); ?>
						<?php
						// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_dropdown_pages() escapes its name attribute and option labels internally.
						wp_dropdown_pages(
							[
								'name'              => $n . '[cookie_page_id]',
								'selected'          => $cookie_page_id,
								'show_option_none'  => __( '— Not set —', 'kramar' ),
								'option_none_value' => '0',
							]
						);
						// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
						?>
					</label>
				</p>
				<?php $this->render_link_warning( $cookie_enabled, $cookie_label, '{cookie_link}', $cookie_page_id ); ?>

				<p><strong><?php esc_html_e( 'Legal Guarantee Notice', 'kramar' ); ?></strong></p>

				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $n ); ?>[guarantee_enabled]"
							value="1"
							<?php checked( $guarantee_enabled ); ?>
						/>
						<?php esc_html_e( 'Show statutory guarantee notice at checkout', 'kramar' ); ?>
					</label>
					<span class="description"><?php esc_html_e( 'Informational text only — not a checkbox the customer must accept.', 'kramar' ); ?></span>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Notice text:', 'kramar' ); ?>
						<br />
						<textarea
							name="<?php echo esc_attr( $n ); ?>[guarantee_text]"
							rows="2"
							style="width:100%;"
						><?php echo esc_textarea( $guarantee_text ); ?></textarea>
					</label>
					<span class="description"><?php esc_html_e( 'Placeholder: {guarantee_link}', 'kramar' ); ?></span>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Guarantee / warranty information page:', 'kramar' ); ?>
						<?php
						// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_dropdown_pages() escapes its name attribute and option labels internally.
						wp_dropdown_pages(
							[
								'name'              => $n . '[guarantee_page_id]',
								'selected'          => $guarantee_page_id,
								'show_option_none'  => __( '— Not set —', 'kramar' ),
								'option_none_value' => '0',
							]
						);
						// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
						?>
					</label>
				</p>
				<?php $this->render_link_warning( $guarantee_enabled, $guarantee_text, '{guarantee_link}', $guarantee_page_id ); ?>

			</div>
		</div>
		<?php
	}

	/**
	 * Whether any enabled label still holds a link placeholder with no page behind it.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True when at least one active label would render a dead placeholder.
	 */
	public static function has_unlinked_placeholder(): bool {
		// Read the option directly rather than the plugin's cached settings: this
		// is called while rendering the dashboard, which can happen in the same
		// request as a settings save, and the cached copy is stale by then.
		$all      = get_option( \Kramar\Settings::OPTION_NAME, [] );
		$settings = is_array( $all ) ? ( $all['compliance']['legal-checkboxes'] ?? [] ) : [];

		if ( ! is_array( $settings ) || empty( $settings['enabled'] ) ) {
			return false;
		}

		// Each enabled label, the placeholder it may carry, and the page that
		// placeholder resolves to. Mirrors the inline warnings on the module's
		// own settings panel — the dashboard is where a shop owner checks
		// whether checkout is compliant, so a dead link has to fail there too.
		$checks = [
			[ 'terms_enabled', 'terms_label', '{terms_link}', (int) ( $settings['terms_page_id'] ?? 0 ) ?: (int) get_option( 'woocommerce_terms_page_id', 0 ) ],
			[ 'terms_enabled', 'terms_label', '{revocation_link}', (int) ( $settings['revocation_page_id'] ?? 0 ) ],
			[ 'privacy_enabled', 'privacy_label', '{privacy_link}', (int) ( $settings['privacy_page_id'] ?? 0 ) ?: (int) get_option( 'wp_page_for_privacy_policy', 0 ) ],
			[ 'cookie_enabled', 'cookie_label', '{cookie_link}', (int) ( $settings['cookie_page_id'] ?? 0 ) ],
			[ 'guarantee_enabled', 'guarantee_text', '{guarantee_link}', (int) ( $settings['guarantee_page_id'] ?? 0 ) ],
		];

		foreach ( $checks as [ $enabled_key, $label_key, $placeholder, $page_id ] ) {
			if ( empty( $settings[ $enabled_key ] ) || $page_id > 0 ) {
				continue;
			}
			if ( str_contains( (string) ( $settings[ $label_key ] ?? '' ), $placeholder ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Warn when an active label uses a link placeholder with no page behind it.
	 *
	 * Without a page the placeholder degrades to plain text — the customer sees
	 * "Revocation Policy" with nothing to click, which is not what an EU checkout
	 * is supposed to present.
	 *
	 * @since 1.0.0
	 *
	 * @param bool   $enabled     Whether the checkbox/notice is switched on.
	 * @param string $label       The label or notice text that may hold the placeholder.
	 * @param string $placeholder Placeholder tag, e.g. {revocation_link}.
	 * @param int    $page_id     Resolved page id (0 when unset).
	 */
	private function render_link_warning( bool $enabled, string $label, string $placeholder, int $page_id ): void {
		if ( ! $enabled || $page_id > 0 || ! str_contains( $label, $placeholder ) ) {
			return;
		}
		?>
		<p class="kramar-legal-warning" style="padding-left:24px;">
			<?php
			printf(
				/* translators: 1: placeholder tag such as {revocation_link}, 2: fallback text shown instead of the link */
				esc_html__( 'No page is selected for %1$s, so customers see the plain text "%2$s" with nothing to click. Pick a page above.', 'kramar' ),
				esc_html( $placeholder ),
				esc_html( self::fallback_label( $placeholder ) )
			);
			?>
		</p>
		<?php
	}

	// -------------------------------------------------------------------------
	// Helpers
	// -------------------------------------------------------------------------

	/**
	 * Shorthand boolean getter for this module's settings.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Setting key.
	 * @return bool Setting value as bool.
	 */
	private function s( string $key ): bool {
		return (bool) $this->get_setting( $key );
	}

	/**
	 * Get a setting value for this module.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Setting key.
	 * @return mixed Setting value.
	 */
	private function get_setting( string $key ): mixed {
		return $this->plugin->get_setting( $this->get_category(), $this->get_slug(), $key );
	}

	/**
	 * Check whether the current cart contains any downloadable products.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True when at least one downloadable product is in the cart.
	 */
	private function cart_has_downloadable(): bool {
		if ( ! WC()->cart ) {
			return false;
		}

		foreach ( WC()->cart->get_cart() as $item ) {
			$product = $item['data'] ?? null;
			if ( $product instanceof \WC_Product && $product->is_downloadable() ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Resolve a label's placeholders to page titles (plain text, for Blocks).
	 *
	 * @since 1.0.0
	 *
	 * @param string $label_key Setting key holding the label template.
	 * @return string Label with placeholders replaced by page titles.
	 */
	private function resolve_label_plain( string $label_key ): string {
		$label = (string) $this->get_setting( $label_key );

		$placeholders = [
			'{terms_link}'      => $this->resolve_page_id( 'terms' ),
			'{revocation_link}' => (int) $this->get_setting( 'revocation_page_id' ),
			'{privacy_link}'    => $this->resolve_page_id( 'privacy' ),
			'{cookie_link}'     => (int) $this->get_setting( 'cookie_page_id' ),
			'{guarantee_link}'  => (int) $this->get_setting( 'guarantee_page_id' ),
		];

		$replacements = [];
		foreach ( $placeholders as $tag => $page_id ) {
			$title = $this->page_title( $page_id );
			$replacements[ $tag ] = '' !== $title ? $title : self::fallback_label( $tag );
		}

		return str_replace( array_keys( $replacements ), array_values( $replacements ), $label );
	}

	/**
	 * Resolve a label's placeholders to HTML anchor tags (for Classic checkout).
	 *
	 * @since 1.0.0
	 *
	 * @param string $label_key Setting key holding the label template.
	 * @return string Label with placeholders replaced by escaped anchor HTML.
	 */
	private function resolve_label_html( string $label_key ): string {
		$label = esc_html( (string) $this->get_setting( $label_key ) );

		$placeholders = [
			'{terms_link}'      => $this->resolve_page_id( 'terms' ),
			'{revocation_link}' => (int) $this->get_setting( 'revocation_page_id' ),
			'{privacy_link}'    => $this->resolve_page_id( 'privacy' ),
			'{cookie_link}'     => (int) $this->get_setting( 'cookie_page_id' ),
			'{guarantee_link}'  => (int) $this->get_setting( 'guarantee_page_id' ),
		];

		$replacements = [];
		foreach ( $placeholders as $tag => $page_id ) {
			$link = $this->page_link( $page_id );
			$replacements[ $tag ] = '' !== $link ? $link : esc_html( self::fallback_label( $tag ) );
		}

		return str_replace( array_keys( $replacements ), array_values( $replacements ), $label );
	}

	/**
	 * Split the guarantee notice around {guarantee_link}.
	 *
	 * Both checkout paths need the same three pieces: the text on either side of
	 * the placeholder, and the link itself. Returning them separately is what
	 * lets Blocks build a real anchor without ever putting admin-supplied text
	 * through innerHTML.
	 *
	 * @since 1.0.3
	 *
	 * @return array{before: string, label: string, url: string, after: string}
	 */
	private function guarantee_text_parts(): array {
		$text    = (string) $this->get_setting( 'guarantee_text' );
		$page_id = (int) $this->get_setting( 'guarantee_page_id' );
		$title   = $this->page_title( $page_id );
		$url     = $page_id > 0 ? get_permalink( $page_id ) : false;

		if ( '' === $title ) {
			$title = self::fallback_label( '{guarantee_link}' );
			$url   = false;
		}

		$pieces = explode( '{guarantee_link}', $text, 2 );

		// No placeholder in the text: nothing to link, and nothing to append.
		if ( ! isset( $pieces[1] ) ) {
			return [
				'before' => $text,
				'label'  => '',
				'url'    => '',
				'after'  => '',
			];
		}

		return [
			'before' => $pieces[0],
			'label'  => $title,
			'url'    => false !== $url ? esc_url_raw( (string) $url ) : '',
			'after'  => $pieces[1],
		];
	}

	/**
	 * Resolve the guarantee notice with {guarantee_link} as an anchor.
	 *
	 * @since 1.0.3
	 *
	 * @return string Escaped HTML, or an empty string when there is nothing to show.
	 */
	private function resolve_guarantee_html(): string {
		$parts = $this->guarantee_text_parts();

		if ( '' === trim( $parts['before'] . $parts['label'] . $parts['after'] ) ) {
			return '';
		}

		$link = esc_html( $parts['label'] );
		if ( '' !== $parts['label'] && '' !== $parts['url'] ) {
			$link = sprintf(
				'<a href="%s" target="_blank" rel="noopener">%s</a>',
				esc_url( $parts['url'] ),
				esc_html( $parts['label'] )
			);
		}

		return esc_html( $parts['before'] ) . $link . esc_html( $parts['after'] );
	}

	/**
	 * Auto-detect a page ID from WordPress/WooCommerce settings when stored value is 0.
	 *
	 * @since 1.0.0
	 *
	 * @param string $type One of 'terms' or 'privacy'.
	 * @return int Resolved page ID (0 when not found).
	 */
	private function resolve_page_id( string $type ): int {
		$stored = (int) $this->get_setting( $type . '_page_id' );

		if ( $stored > 0 ) {
			return $stored;
		}

		return match ( $type ) {
			'terms'   => (int) get_option( 'woocommerce_terms_page_id', 0 ),
			'privacy' => (int) get_option( 'wp_page_for_privacy_policy', 0 ),
			default   => 0,
		};
	}

	/**
	 * Return the title of a WordPress page by ID.
	 *
	 * @since 1.0.0
	 *
	 * @param int $page_id Page ID (0 returns empty string).
	 * @return string Page title, or empty string when page is not found.
	 */
	private function page_title( int $page_id ): string {
		if ( $page_id <= 0 ) {
			return '';
		}

		$title = get_the_title( $page_id );
		return is_string( $title ) ? $title : '';
	}

	/**
	 * Human-readable fallback text for a link placeholder with no page behind it.
	 *
	 * @since 1.0.0
	 *
	 * @param string $placeholder Placeholder tag, e.g. {terms_link}.
	 * @return string Translated label, or an empty string for an unknown placeholder.
	 */
	private static function fallback_label( string $placeholder ): string {
		return match ( $placeholder ) {
			'{terms_link}'      => __( 'Terms & Conditions', 'kramar' ),
			'{revocation_link}' => __( 'Revocation Policy', 'kramar' ),
			'{privacy_link}'    => __( 'Privacy Policy', 'kramar' ),
			'{cookie_link}'     => __( 'Cookie Policy', 'kramar' ),
			'{guarantee_link}'  => __( 'Legal Guarantee', 'kramar' ),
			default             => '',
		};
	}

	/**
	 * Return an escaped HTML anchor for a WordPress page by ID.
	 *
	 * Falls back to the page title without a link when no valid URL is available.
	 *
	 * @since 1.0.0
	 *
	 * @param int $page_id Page ID.
	 * @return string Escaped anchor HTML, or escaped page title, or empty string.
	 */
	private function page_link( int $page_id ): string {
		if ( $page_id <= 0 ) {
			return '';
		}

		$url   = get_permalink( $page_id );
		$title = $this->page_title( $page_id );

		if ( false === $url || '' === $title ) {
			return esc_html( $title );
		}

		return sprintf(
			'<a href="%s" target="_blank" rel="noopener">%s</a>',
			esc_url( $url ),
			esc_html( $title )
		);
	}
}
