<?php
/**
 * Refund & Return (Withdrawal) module.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar\Modules\Orders;

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

/**
 * Refund & Return module.
 *
 * Implements Art. 6:230oa BW (NL, effective June 19 2026) — the "herroepingsknop"
 * requirement. Provides a standalone withdrawal page accessible without an account,
 * HMAC-signed email links, a two-step confirmation flow, custom order statuses,
 * 4 WooCommerce emails, and an admin order metabox.
 *
 * @since 1.0.0
 */
final class Refund_Return extends \Kramar\Module {

	/** @since 1.0.2 */
	private const WITHDRAWAL_PAGE_OPTION = 'kramar_withdrawal_page_id';

	/** @since 1.0.2 */
	private const WITHDRAWAL_PAGE_LOCK = 'kramar_withdrawal_page_lock';


	/**
	 * POST action for the return action form (approve/reject/info).
	 *
	 * @since 1.0.0
	 */
	private const RETURN_ACTION = 'kramar_return_action';

	/**
	 * Nonce action for the shortcode withdrawal form.
	 *
	 * @since 1.0.0
	 */
	private const SHORTCODE_NONCE = 'kramar_withdrawal_form';

	/**
	 * Transient prefix for guest session tokens on the withdrawal page.
	 *
	 * @since 1.0.0
	 */
	private const TRANSIENT_PREFIX = 'kramar_wdr_';

	// -------------------------------------------------------------------------
	// Module Identity
	// -------------------------------------------------------------------------

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

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

	/** {@inheritDoc} */
	public function get_name(): string {
		return __( 'Refund & Return (Withdrawal)', 'kramar' );
	}

	/** {@inheritDoc} */
	public function get_description(): string {
		return __( 'EU-compliant withdrawal/return request system (Art. 6:230oa BW). Adds a standalone cancellation page, HMAC-signed links, a two-step confirmation flow, custom order statuses, and admin management tools.', 'kramar' );
	}

	// -------------------------------------------------------------------------
	// Settings
	// -------------------------------------------------------------------------

	/** {@inheritDoc} */
	public function get_default_settings(): array {
		return [
			'enabled'                    => false,
			'withdrawal_page_id'         => 0,
			'button_label'               => __( 'Cancel order', 'kramar' ),
			'email_link_text'            => __( 'Cancel this order', 'kramar' ),
			'email_link_in_emails'       => true,
			'confirm_button_label'       => __( 'Confirm cancellation', 'kramar' ),
			'return_instructions_page_id' => 0,
			'css_button'                 => 'btn btn-primary',
			'css_input'                  => 'form-control',
			'css_select'                 => 'form-select',
			'css_label'                  => 'form-label',
		];
	}

	/** {@inheritDoc} */
	public function sanitize_settings( array $input ): array {
		return [
			'enabled'                    => ! empty( $input['enabled'] ),
			'withdrawal_page_id'         => absint( $input['withdrawal_page_id'] ?? 0 ),
			'button_label'               => sanitize_text_field( $input['button_label'] ?? __( 'Cancel order', 'kramar' ) ),
			'email_link_text'            => sanitize_text_field( $input['email_link_text'] ?? __( 'Cancel this order', 'kramar' ) ),
			'email_link_in_emails'       => ! empty( $input['email_link_in_emails'] ),
			'confirm_button_label'       => sanitize_text_field( $input['confirm_button_label'] ?? __( 'Confirm cancellation', 'kramar' ) ),
			'return_instructions_page_id' => absint( $input['return_instructions_page_id'] ?? 0 ),
			'css_button'                 => sanitize_text_field( $input['css_button'] ?? 'btn btn-primary' ),
			'css_input'                  => sanitize_text_field( $input['css_input'] ?? 'form-control' ),
			'css_select'                 => sanitize_text_field( $input['css_select'] ?? 'form-select' ),
			'css_label'                  => sanitize_text_field( $input['css_label'] ?? 'form-label' ),
		];
	}

	// -------------------------------------------------------------------------
	// Bootstrap
	// -------------------------------------------------------------------------

	/** {@inheritDoc} */
	protected function run(): void {
		// Custom statuses — must run at init pri 9, before WC finalises its list.
		add_action( 'init', [ $this, 'register_return_statuses' ], 9 );
		add_filter( 'wc_order_statuses', [ $this, 'add_return_statuses_to_wc' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );

		// Withdrawal page auto-creation + verification email registration.
		add_action( 'init', [ $this, 'maybe_create_withdrawal_page' ], 20 );
		add_action( 'init', [ $this, 'ensure_verification_email' ], 20 );

		// Shortcode for the withdrawal flow.
		add_shortcode( 'kramar_withdrawal', [ $this, 'render_withdrawal_shortcode' ] );

		// My Account button.
		add_filter( 'woocommerce_my_account_my_orders_actions', [ $this, 'add_my_account_cancel_button' ], 10, 2 );

		// Email classes.
		add_filter( 'woocommerce_email_classes', [ $this, 'register_email_classes' ] );

		// Resolve the {withdrawal_link} / {withdrawal_deadline} merge fields
		// (registered in the Email Manager palette but otherwise never filled).
		add_filter( 'kramar_email_merge_field_values', [ $this, 'resolve_merge_fields' ], 10, 2 );

		// Put the withdrawal link in the order confirmation itself. Required by
		// Directive (EU) 2023/2673 since 19 June 2026: the confirmation has to
		// carry a withdrawal function the customer can complete in two steps.
		//
		// Two mechanisms, deliberately. The button is a visible block in the
		// email builder's default template, so the shop can see it, move it and
		// reword it — a legally required element that only exists as invisible
		// injection is one nobody can review. The hook is the floor underneath:
		// it covers templates saved before this existed, templates the button
		// was deleted from, and the case where the Email module is switched off
		// entirely and WooCommerce renders its own template. When the template
		// carries {withdrawal_link} the hook stands down, so there is never a
		// second link.
		add_filter( 'kramar_email_default_blocks', [ $this, 'add_withdrawal_block_to_defaults' ], 10, 2 );
		add_action( 'woocommerce_email_after_order_table', [ $this, 'inject_withdrawal_link_in_email' ], 20, 4 );

		// Notify the customer when the refund for an approved withdrawal completes
		// (covers automatic and manual refunds).
		add_action( 'woocommerce_order_refunded', [ $this, 'on_order_refunded' ], 10, 2 );

		// Email triggers on status transitions.
		add_action( 'woocommerce_order_status_return-requested', [ $this, 'trigger_withdrawal_emails' ], 10, 2 );
		add_action( 'woocommerce_order_status_return-approved', [ $this, 'trigger_approved_email' ], 10, 2 );
		add_action( 'woocommerce_order_status_return-rejected', [ $this, 'trigger_rejected_email' ], 10, 2 );

		// Admin metabox — CPT and HPOS.
		add_action( 'add_meta_boxes', [ $this, 'register_metabox' ] );

		// PRG handler for admin metabox actions.
		add_action( 'admin_post_' . self::RETURN_ACTION, [ $this, 'handle_return_action' ] );

	}

	// -------------------------------------------------------------------------
	// Custom Order Statuses
	// -------------------------------------------------------------------------

	/**
	 * Register the three return-related order statuses with WordPress.
	 *
	 * @since 1.0.0
	 */
	/**
	 * Build a nooped-plural array for register_post_status()'s `label_count`
	 * from a runtime status label, mirroring _n_noop()'s return shape without
	 * passing a variable to a gettext function (which the translation parser
	 * cannot extract statically).
	 *
	 * @since 1.0.0
	 *
	 * @param string $label Status label.
	 * @return array<int|string, string|null>
	 */
	private static function make_label_count( string $label ): array {
		$format = $label . ' <span class="count">(%s)</span>';
		return [
			0          => $format,
			1          => $format,
			'singular' => $format,
			'plural'   => $format,
			'context'  => null,
			'domain'   => 'kramar',
		];
	}

	public function register_return_statuses(): void {
		$statuses = [
			'wc-return-requested' => __( 'Return Requested', 'kramar' ),
			'wc-return-approved'  => __( 'Return Approved', 'kramar' ),
			'wc-return-rejected'  => __( 'Return Rejected', 'kramar' ),
		];

		foreach ( $statuses as $slug => $label ) {
			register_post_status(
				$slug,
				[
					'label'                    => $label,
					'public'                   => true,
					'exclude_from_search'      => false,
					'show_in_admin_all_list'   => true,
					'show_in_admin_status_list' => true,
					// Built by hand rather than _n_noop() because $label is a runtime
					// value here, not a source literal the translation parser could
					// extract. The labels themselves stay translatable via the __()
					// calls in $statuses above.
					'label_count'              => self::make_label_count( $label ),
				]
			);
		}
	}

	/**
	 * Add the return statuses to WooCommerce's order status list.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, string> $statuses Existing WC statuses.
	 * @return array<string, string> Extended statuses.
	 */
	public function add_return_statuses_to_wc( array $statuses ): array {
		$statuses['wc-return-requested'] = __( 'Return Requested', 'kramar' );
		$statuses['wc-return-approved']  = __( 'Return Approved', 'kramar' );
		$statuses['wc-return-rejected']  = __( 'Return Rejected', 'kramar' );
		return $statuses;
	}

	/**
	 * Enqueue return-status badge CSS on order and Kramar admin screens.
	 *
	 * @since 1.0.0
	 */
	public function enqueue_admin_assets(): void {
		$screen = get_current_screen();
		if (
			! $screen ||
			( ! in_array( $screen->id, [ 'edit-shop_order', 'shop_order', 'woocommerce_page_wc-orders' ], true ) &&
			! str_contains( $screen->id, 'kramar' ) )
		) {
			return;
		}

		wp_enqueue_style( 'kramar-admin', KRAMAR_URL . 'assets/css/admin.css', [], KRAMAR_VERSION );
		wp_add_inline_style(
			'kramar-admin',
			'.order-status.status-return-requested{background:#f0ad4e;color:#fff;}' .
			'.order-status.status-return-approved{background:#5cb85c;color:#fff;}' .
			'.order-status.status-return-rejected{background:#d9534f;color:#fff;}'
		);
	}

	// -------------------------------------------------------------------------
	// Withdrawal Page
	// -------------------------------------------------------------------------

	/**
	 * Create the withdrawal page on first enable if it doesn't exist yet.
	 *
	 * @since 1.0.0
	 */
	public function maybe_create_withdrawal_page(): void {
		// Hooked on `init` (fires on front-end too); only ever create the page
		// during a privileged admin request. Once created it persists, so the
		// front-end shortcode still resolves for everyone.
		if ( ! is_admin() || wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) {
			return;
		}

		$page_id = (int) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'withdrawal_page_id' );

		if ( $page_id > 0 && self::page_is_usable( (int) $page_id ) ) {
			// Mirror it into the dedicated option now, while the settings copy is
			// still intact — that copy is the one another module can clobber.
			$this->remember_withdrawal_page( $page_id );
			return;
		}

		// The same protection the tracking page needed, which this twin never got.
		// The id lived only inside the shared kramar_settings option, which several
		// modules read-modify-write wholesale; one of them saving a copy it read
		// before this page existed resets the id to 0, and this runs on every admin
		// page load, so the next load makes another page. On the tracking page that
		// produced eighteen of them.
		$known = (int) get_option( self::WITHDRAWAL_PAGE_OPTION, 0 );
		if ( $known > 0 && self::page_is_usable( (int) $known ) ) {
			$this->remember_withdrawal_page( $known );
			return;
		}

		// Adopt a page that already carries the shortcode rather than adding one.
		$existing = get_posts(
			[
				'post_type'      => 'page',
				'post_status'    => [ 'publish', 'draft', 'pending', 'private' ],
				'posts_per_page' => 1,
				'fields'         => 'ids',
				's'              => '[kramar_withdrawal]',
			]
		);
		if ( ! empty( $existing[0] ) ) {
			$this->remember_withdrawal_page( (int) $existing[0] );
			return;
		}

		// Only one request may create the page.
		// The lock covers a few lines, but a request that dies in between —
		// timeout, fatal, out of memory — would leave it set forever, and the
		// page could then never be created again with nothing to tell an admin
		// why. A stale lock silently disabling the feature is worse than the
		// duplicate pages it exists to prevent, so it expires.
		$lock = get_option( self::WITHDRAWAL_PAGE_LOCK, false );
		if ( false !== $lock && ( time() - (int) $lock ) > MINUTE_IN_SECONDS ) {
			delete_option( self::WITHDRAWAL_PAGE_LOCK );
			$lock = false;
		}

		if ( false !== $lock || false === add_option( self::WITHDRAWAL_PAGE_LOCK, time(), '', false ) ) {
			return;
		}

		$page_id = wp_insert_post(
			[
				'post_title'   => __( 'Cancel order', 'kramar' ),
				// Wrapped as a shortcode block so it renders in block themes too.
				'post_content' => '<!-- wp:shortcode -->[kramar_withdrawal]<!-- /wp:shortcode -->',
				'post_status'  => 'publish',
				'post_type'    => 'page',
			]
		);

		delete_option( self::WITHDRAWAL_PAGE_LOCK );

		if ( $page_id && ! is_wp_error( $page_id ) ) {
			$this->remember_withdrawal_page( (int) $page_id );
		}
	}

	/**
	 * Record the withdrawal page id in both places it is read from.
	 *
	 * Its own option is authoritative — nothing else rewrites it wholesale —
	 * while the settings copy keeps the Modules screen in step.
	 *
	 * @since 1.0.2
	 */
	private function remember_withdrawal_page( int $page_id ): void {
		if ( $page_id <= 0 ) {
			return;
		}

		update_option( self::WITHDRAWAL_PAGE_OPTION, $page_id, false );

		$settings = get_option( \Kramar\Settings::OPTION_NAME, [] );
		if ( ! is_array( $settings ) ) {
			$settings = [];
		}

		if ( (int) ( $settings['orders']['refund-return']['withdrawal_page_id'] ?? 0 ) === $page_id ) {
			return;
		}

		$settings['orders']['refund-return']['withdrawal_page_id'] = $page_id;
		update_option( \Kramar\Settings::OPTION_NAME, $settings );
	}

	/**
	 * Return the full URL to the withdrawal page.
	 *
	 * @since 1.0.0
	 *
	 * @return string URL, or empty string if not configured.
	 */
	public static function get_withdrawal_url(): string {
		$settings = get_option( \Kramar\Settings::OPTION_NAME, [] );
		$page_id  = (int) ( $settings['orders']['refund-return']['withdrawal_page_id'] ?? 0 );
		return $page_id > 0 ? (string) get_permalink( $page_id ) : '';
	}

	/**
	 * Build the signed withdrawal/cancel link for a specific order.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order Order.
	 * @return string URL, or empty string if the withdrawal page is not set.
	 */
	public static function get_order_withdrawal_url( \WC_Order $order ): string {
		$base = self::get_withdrawal_url();
		if ( '' === $base ) {
			return '';
		}
		return add_query_arg(
			[
				'order_id' => $order->get_id(),
				'token'    => self::generate_token( $order->get_id(), $order->get_billing_email() ),
			],
			$base
		);
	}

	/**
	 * Resolve {withdrawal_link} and {withdrawal_deadline} for an order. These
	 * tags are listed in the Email Manager palette but were never filled.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, string> $replacements Tag → value map.
	 * @param \WC_Order             $order        Order.
	 * @return array<string, string>
	 */
	public function resolve_merge_fields( array $replacements, \WC_Order $order ): array {
		$replacements['{withdrawal_link}'] = self::get_order_withdrawal_url( $order );

		// Same signed URL, under the name the verification template uses. It has
		// to be a real merge field: the template is rendered by the Email Manager
		// (builder preview included), which knows nothing about this module, so an
		// unresolved {verify_url} would come out of esc_url() as "http://verify_url".
		$replacements['{verify_url}'] = self::get_order_withdrawal_url( $order );

		$anchor = $order->get_date_completed() ?: $order->get_date_created();
		$replacements['{withdrawal_deadline}'] = $anchor
			? wp_date( wc_date_format(), $anchor->getTimestamp() + ( 14 * DAY_IN_SECONDS ) )
			: '';

		return $replacements;
	}

	/**
	 * The WC_Email ids whose confirmations carry the withdrawal link.
	 *
	 * @since 1.0.3
	 *
	 * @param \WC_Order|null $order Order being emailed, when there is one.
	 * @return string[]
	 */
	private function withdrawal_link_email_ids( ?\WC_Order $order = null ): array {
		/**
		 * Emails that carry the withdrawal link.
		 *
		 * @since 1.0.3
		 *
		 * @param string[]       $ids   WC_Email ids.
		 * @param \WC_Order|null $order Order being emailed, if any.
		 */
		return (array) apply_filters(
			'kramar_withdrawal_link_emails',
			[ 'customer_processing_order', 'customer_on_hold_order', 'customer_completed_order' ],
			$order
		);
	}

	/**
	 * Add the withdrawal button to the default template of the confirmation
	 * emails, so it is visible and editable in the email builder.
	 *
	 * Placed after the order table, matching where the hook puts it, so moving
	 * between the two mechanisms does not move the button.
	 *
	 * @since 1.0.3
	 *
	 * @param array<int, array<string, mixed>> $blocks   Default body blocks.
	 * @param string                           $email_id WC_Email id.
	 * @return array<int, array<string, mixed>>
	 */
	public function add_withdrawal_block_to_defaults( array $blocks, string $email_id ): array {
		if ( ! in_array( $email_id, $this->withdrawal_link_email_ids(), true ) ) {
			return $blocks;
		}

		// No page, no link. A button block resolving to an empty URL would be
		// worse than none, and the module's own settings screen already warns
		// when the page is missing.
		if ( '' === self::get_withdrawal_url() ) {
			return $blocks;
		}

		$label = trim( (string) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'email_link_text' ) );
		if ( '' === $label ) {
			$label = __( 'Cancel this order', 'kramar' );
		}

		$button = [
			'type'     => 'button',
			'id'       => 'default_withdrawal_btn',
			'settings' => [
				'text'   => $label,
				'url'    => '{withdrawal_link}',
				'color'  => '#ffffff',
				'bg'     => '#2c3338',
				'align'  => 'center',
				'radius' => '4',
			],
		];

		$at = null;
		foreach ( $blocks as $i => $block ) {
			if ( 'order-table' === ( $block['type'] ?? '' ) ) {
				$at = $i + 1;
			}
		}

		if ( null === $at ) {
			$blocks[] = $button;
			return $blocks;
		}

		array_splice( $blocks, $at, 0, [ $button ] );

		return $blocks;
	}

	/**
	 * Print the withdrawal link inside customer order emails.
	 *
	 * The module generated a signed link and offered a setting for its wording,
	 * but nothing ever emitted it: the link reached nobody. Directive (EU)
	 * 2023/2673 has required since 19 June 2026 that the order confirmation
	 * itself carry a withdrawal function, so this runs on WooCommerce's standard
	 * after-order-table hook — which the Email Manager's block templates also
	 * fire, so the link appears under both rendering paths.
	 *
	 * @since 1.0.3
	 *
	 * @param \WC_Order|mixed $order         Order being emailed.
	 * @param bool            $sent_to_admin Whether this copy goes to the shop.
	 * @param bool            $plain_text    Whether the email is plain text.
	 * @param \WC_Email|mixed $email         Email being sent.
	 */
	public function inject_withdrawal_link_in_email( $order, $sent_to_admin = false, $plain_text = false, $email = null ): void {
		if ( ! $order instanceof \WC_Order || $sent_to_admin ) {
			return;
		}

		if ( ! $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'email_link_in_emails' ) ) {
			return;
		}

		/**
		 * Suppress the automatic link for this render.
		 *
		 * The Email Manager sets this while rendering a template that carries
		 * {withdrawal_link} itself, so a shop that placed the link in the builder
		 * gets exactly one — theirs — with no setting to remember.
		 *
		 * @since 1.0.3
		 *
		 * @param bool $suppress Whether to skip the automatic link.
		 */
		if ( apply_filters( 'kramar_suppress_withdrawal_link', false ) ) {
			return;
		}

		$email_id = $email instanceof \WC_Email ? (string) $email->id : '';

		if ( ! in_array( $email_id, $this->withdrawal_link_email_ids( $order ), true ) ) {
			return;
		}

		// Nothing left to withdraw from — offering the link would only confuse.
		if ( $order->has_status( [ 'cancelled', 'refunded', 'failed', 'return-requested', 'return-approved', 'return-rejected' ] ) ) {
			return;
		}

		$url = self::get_order_withdrawal_url( $order );
		if ( '' === $url ) {
			return;
		}

		$label = trim( (string) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'email_link_text' ) );
		if ( '' === $label ) {
			$label = __( 'Cancel this order', 'kramar' );
		}

		if ( $plain_text ) {
			echo "\n" . esc_html( $label ) . ":\n" . esc_url_raw( $url ) . "\n";
			return;
		}

		printf(
			'<p style="margin:16px 0;"><a href="%1$s" style="display:inline-block;padding:12px 24px;background:#2c3338;color:#ffffff;text-decoration:none;border-radius:4px;font-weight:600;">%2$s</a></p>',
			esc_url( $url ),
			esc_html( $label )
		);
	}

	// -------------------------------------------------------------------------
	// HMAC Token Helpers
	// -------------------------------------------------------------------------

	/**
	 * Generate an HMAC token for an order + billing email pair.
	 *
	 * @since 1.0.0
	 *
	 * @param int    $order_id Order ID.
	 * @param string $email    Billing email address.
	 * @return string Hex HMAC.
	 */
	public static function generate_token( int $order_id, string $email ): string {
		return hash_hmac( 'sha256', $order_id . '|' . $email, wp_salt( 'auth' ) );
	}

	/**
	 * Verify an HMAC token for an order + billing email pair.
	 *
	 * @since 1.0.0
	 *
	 * @param int    $order_id Order ID.
	 * @param string $email    Billing email address.
	 * @param string $token    Token to verify.
	 * @return bool True if valid.
	 */
	public static function verify_token( int $order_id, string $email, string $token ): bool {
		return hash_equals( self::generate_token( $order_id, $email ), $token );
	}

	// -------------------------------------------------------------------------
	// Shortcode — Withdrawal Flow
	// -------------------------------------------------------------------------

	/**
	 * Render the [kramar_withdrawal] shortcode.
	 *
	 * Handles a three-step flow:
	 *   Step 0 — Guest identification (order number + email).
	 *   Step 1 — Withdrawal declaration with order summary.
	 *   Step 2 — Final confirmation.
	 *
	 * @since 1.0.0
	 *
	 * @return string HTML output.
	 */
	public function render_withdrawal_shortcode(): string {
		ob_start();

		// Determine whether we arrived via a signed email link.
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$url_order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0;
		$url_token    = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : '';
		// phpcs:enable

		$token_verified = false;
		$linked_order   = null;

		if ( $url_order_id > 0 && '' !== $url_token ) {
			$order = wc_get_order( $url_order_id );
			if ( $order instanceof \WC_Order ) {
				$billing_email = $order->get_billing_email();
				if ( self::verify_token( $url_order_id, $billing_email, $url_token ) ) {
					$token_verified = true;
					$linked_order   = $order;
				}
			}
		}

		// Handle POST submissions.
		// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- nonce verified in each individual step handler; REQUEST_METHOD is server-controlled
		if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['kramar_withdrawal_step'] ) ) {
			$this->handle_withdrawal_post( $linked_order, $token_verified );
			return (string) ob_get_clean();
		}

		// Render appropriate step.
		if ( $token_verified && $linked_order instanceof \WC_Order ) {
			$this->render_withdrawal_step1( $linked_order );
		} else {
			$this->render_withdrawal_step0();
		}

		return (string) ob_get_clean();
	}

	/**
	 * Handle withdrawal form POST data and route to the correct step.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order|null $linked_order Order from HMAC link, or null.
	 * @param bool           $token_verified Whether the URL token is valid.
	 */
	private function handle_withdrawal_post( ?\WC_Order $linked_order, bool $token_verified ): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified in each individual step handler (handle_identify_step, handle_declare_step, handle_confirm_step)
		$step = sanitize_key( $_POST['kramar_withdrawal_step'] ?? '' );

		if ( 'identify' === $step ) {
			$this->handle_identify_step();
			return;
		}

		if ( 'declare' === $step ) {
			$this->handle_declare_step( $linked_order, $token_verified );
			return;
		}

		if ( 'confirm' === $step ) {
			$this->handle_confirm_step( $linked_order, $token_verified );
			return;
		}

		$this->render_withdrawal_step0();
	}

	/**
	 * Step 0: Render the guest identification form.
	 *
	 * @since 1.0.0
	 */
	private function render_withdrawal_step0( string $error = '' ): void {
		$css = $this->get_css_classes();

		$a = wp_rand( 2, 9 );
		$b = wp_rand( 1, 9 );
		// Stateless captcha: the token is an HMAC of the correct answer, so no
		// per-render transient is written and the page stays cacheable. Mirrors the
		// Back-in-Stock captcha. See L5.
		$captcha_token = hash_hmac( 'sha256', (string) ( $a + $b ), wp_salt( 'auth' ) );

		?>
		<div class="kramar-withdrawal kramar-withdrawal--step-0">
			<?php if ( '' !== $error ) : ?>
				<div class="kramar-withdrawal__notice"><?php echo esc_html( $error ); ?></div>
			<?php endif; ?>
			<form method="post" class="kramar-withdrawal__form">
				<?php wp_nonce_field( self::SHORTCODE_NONCE, 'kramar_withdrawal_nonce' ); ?>
				<input type="hidden" name="kramar_withdrawal_step" value="identify" />
				<input type="hidden" name="kramar_captcha_token" value="<?php echo esc_attr( $captcha_token ); ?>" />

				<div class="kramar-withdrawal__field mb-3">
					<label for="kramar-order-number" class="<?php echo esc_attr( $css['label'] ); ?>"><?php esc_html_e( 'Order number', 'kramar' ); ?></label>
					<input
						type="text"
						id="kramar-order-number"
						name="kramar_order_number"
						required
						class="<?php echo esc_attr( $css['input'] ); ?>"
						autocomplete="off"
					/>
				</div>
				<div class="kramar-withdrawal__field mb-3">
					<label for="kramar-billing-email" class="<?php echo esc_attr( $css['label'] ); ?>"><?php esc_html_e( 'Billing email address', 'kramar' ); ?></label>
					<input
						type="email"
						id="kramar-billing-email"
						name="kramar_billing_email"
						required
						class="<?php echo esc_attr( $css['input'] ); ?>"
						autocomplete="email"
					/>
				</div>
				<div class="kramar-withdrawal__field mb-3">
					<label for="kramar-captcha" class="<?php echo esc_attr( $css['label'] ); ?>">
						<?php
						printf(
							/* translators: 1: first number, 2: second number */
							esc_html__( 'What is %1$d + %2$d?', 'kramar' ),
							(int) $a,
							(int) $b
						);
						?>
					</label>
					<input
						type="number"
						id="kramar-captcha"
						name="kramar_captcha"
						required
						class="<?php echo esc_attr( $css['input'] ); ?>"
						autocomplete="off"
						style="max-width:120px;"
					/>
				</div>

				<div class="kramar-withdrawal__field">
					<button type="submit" class="<?php echo esc_attr( $css['button'] ); ?>">
						<?php esc_html_e( 'Continue', 'kramar' ); ?>
					</button>
				</div>
			</form>
		</div>
		<?php
	}

	/**
	 * Handle Step 0 POST — validate order + email, then show Step 1.
	 *
	 * @since 1.0.0
	 */
	private function handle_identify_step(): void {
		if ( ! isset( $_POST['kramar_withdrawal_nonce'] ) ||
			! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['kramar_withdrawal_nonce'] ) ), self::SHORTCODE_NONCE )
		) {
			$this->render_withdrawal_step0( __( 'Security check failed. Please try again.', 'kramar' ) );
			return;
		}

		if ( $this->is_rate_limited() ) {
			$this->render_withdrawal_step0( __( 'Too many attempts. Please try again in a few minutes.', 'kramar' ) );
			return;
		}

		$captcha_ans   = absint( $_POST['kramar_captcha'] ?? 0 );
		$captcha_token = sanitize_text_field( wp_unslash( $_POST['kramar_captcha_token'] ?? '' ) );
		$expected      = hash_hmac( 'sha256', (string) $captcha_ans, wp_salt( 'auth' ) );

		if ( '' === $captcha_token || ! hash_equals( $expected, $captcha_token ) ) {
			$this->record_failed_attempt();
			$this->render_withdrawal_step0( __( 'Incorrect answer. Please try again.', 'kramar' ) );
			return;
		}

		$raw_number    = sanitize_text_field( wp_unslash( $_POST['kramar_order_number'] ?? '' ) );
		$billing_email = sanitize_email( wp_unslash( $_POST['kramar_billing_email'] ?? '' ) );
		$raw_number    = ltrim( $raw_number, '#' );

		$order   = $this->find_order_by_number( $raw_number );
		$matched = $order instanceof \WC_Order
			&& strtolower( $order->get_billing_email() ) === strtolower( $billing_email );

		if ( $matched ) {
			$this->send_verification_email( $order );
		} else {
			$this->record_failed_attempt();
		}

		$this->render_email_sent_notice();
	}

	/**
	 * Step 1: Render the withdrawal declaration form.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order The order being withdrawn (already verified via HMAC).
	 */
	private function render_withdrawal_step1( \WC_Order $order ): void {
		$current_status = $order->get_status();
		if ( in_array( $current_status, [ 'return-requested', 'return-approved', 'return-rejected' ], true ) ) {
			$this->render_error( __( 'A withdrawal request has already been submitted for this order.', 'kramar' ) );
			return;
		}

		if ( ! $this->is_within_withdrawal_period( $order ) ) {
			$this->render_error( __( 'The 14-day withdrawal period for this order has passed.', 'kramar' ) );
			return;
		}

		$confirm_label = (string) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'confirm_button_label' );
		$css           = $this->get_css_classes();

		$reason_options = [
			''                   => __( '-- no reason required --', 'kramar' ),
			'changed-mind'       => __( 'Changed my mind', 'kramar' ),
			'ordered-by-mistake' => __( 'Ordered by mistake', 'kramar' ),
			'found-cheaper'      => __( 'Found a better price elsewhere', 'kramar' ),
			'delivery-too-long'  => __( 'Delivery time too long', 'kramar' ),
			'other'              => __( 'Other', 'kramar' ),
		];

		$instructions_page_id = (int) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'return_instructions_page_id' );
		?>
		<div class="kramar-withdrawal kramar-withdrawal--step-1">
			<?php if ( $instructions_page_id > 0 ) : ?>
				<p>
					<?php
					printf(
						/* translators: 1: opening <a>, 2: closing </a> */
						esc_html__( 'Please read our %1$sreturn instructions%2$s before proceeding.', 'kramar' ),
						'<a href="' . esc_url( (string) get_permalink( $instructions_page_id ) ) . '">',
						'</a>'
					);
					?>
				</p>
			<?php endif; ?>

			<h3><?php esc_html_e( 'Your order', 'kramar' ); ?></h3>
			<table class="kramar-withdrawal__order-summary table">
				<tbody>
					<?php foreach ( $order->get_items() as $item ) : ?>
						<tr>
							<td><?php echo esc_html( $item->get_name() ); ?></td>
							<td><?php echo esc_html( $item->get_quantity() ); ?> &times;</td>
							<td><?php echo wp_kses_post( wc_price( $item->get_total() ) ); ?></td>
						</tr>
					<?php endforeach; ?>
					<tr>
						<th colspan="2"><?php esc_html_e( 'Total', 'kramar' ); ?></th>
						<td><?php echo wp_kses_post( $order->get_formatted_order_total() ); ?></td>
					</tr>
				</tbody>
			</table>

			<form method="post" class="kramar-withdrawal__form">
				<?php wp_nonce_field( self::SHORTCODE_NONCE, 'kramar_withdrawal_nonce' ); ?>
				<input type="hidden" name="kramar_withdrawal_step" value="confirm" />
				<input type="hidden" name="kramar_order_id" value="<?php echo esc_attr( (string) $order->get_id() ); ?>" />
					<input type="hidden" name="kramar_billing_email" value="<?php echo esc_attr( $order->get_billing_email() ); ?>" />
					<input type="hidden" name="kramar_token" value="<?php echo esc_attr( self::generate_token( $order->get_id(), $order->get_billing_email() ) ); ?>" />

				<div class="kramar-withdrawal__field mb-3">
					<label for="kramar-return-reason" class="<?php echo esc_attr( $css['label'] ); ?>"><?php esc_html_e( 'Reason (optional)', 'kramar' ); ?></label>
					<select id="kramar-return-reason" name="kramar_return_reason" class="<?php echo esc_attr( $css['select'] ); ?>">
						<?php foreach ( $reason_options as $val => $label ) : ?>
							<option value="<?php echo esc_attr( $val ); ?>"><?php echo esc_html( $label ); ?></option>
						<?php endforeach; ?>
					</select>
				</div>

				<p class="kramar-withdrawal__declaration-notice">
					<?php
					esc_html_e(
						'By clicking the button below you confirm that you wish to withdraw from the purchase contract for the above order.',
						'kramar'
					);
					?>
				</p>

				<div class="kramar-withdrawal__field">
					<button type="submit" class="<?php echo esc_attr( $css['button'] ); ?>">
						<?php echo esc_html( $confirm_label ?: __( 'Confirm cancellation', 'kramar' ) ); ?>
					</button>
				</div>
			</form>
		</div>
		<?php
	}

	/**
	 * Handle Step 2 POST — verify ownership, set status, store meta, trigger emails.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order|null $linked_order Order from HMAC link, or null.
	 * @param bool           $token_verified Whether the URL HMAC token is valid.
	 */
	private function handle_declare_step( ?\WC_Order $linked_order, bool $token_verified ): void {
		// Unused step kept for extensibility; confirm step handles submission.
		$this->render_error( __( 'Unexpected state. Please start over.', 'kramar' ) );
	}

	/**
	 * Handle the confirmation POST and finalise the withdrawal.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order|null $linked_order Order from HMAC link, or null.
	 * @param bool           $token_verified Whether the URL HMAC token is valid.
	 */
	private function handle_confirm_step( ?\WC_Order $linked_order, bool $token_verified ): void {
		if ( ! isset( $_POST['kramar_withdrawal_nonce'] ) ||
			! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['kramar_withdrawal_nonce'] ) ), self::SHORTCODE_NONCE )
		) {
			$this->render_error( __( 'Security check failed. Please try again.', 'kramar' ) );
			return;
		}

		$order_id = absint( $_POST['kramar_order_id'] ?? 0 );
		$order    = wc_get_order( $order_id );

		if ( ! $order instanceof \WC_Order ) {
			$this->render_error( __( 'Order not found.', 'kramar' ) );
			return;
		}

		$billing_email = sanitize_email( wp_unslash( $_POST['kramar_billing_email'] ?? '' ) );
		$token         = sanitize_text_field( wp_unslash( $_POST['kramar_token'] ?? '' ) );
		$ownership_ok  = strtolower( $order->get_billing_email() ) === strtolower( $billing_email )
			&& self::verify_token( $order_id, $billing_email, $token );

		if ( ! $ownership_ok ) {
			$this->render_error( __( 'Could not verify order ownership. Please try again.', 'kramar' ) );
			return;
		}

		// Guard: only process once.
		if ( in_array( $order->get_status(), [ 'return-requested', 'return-approved', 'return-rejected' ], true ) ) {
			$this->render_error( __( 'A withdrawal request has already been submitted for this order.', 'kramar' ) );
			return;
		}

		$declaration = __( 'I hereby declare that I withdraw from my purchase contract for the above order.', 'kramar' );
		$timestamp   = current_time( 'mysql' );
		$reason      = sanitize_text_field( wp_unslash( $_POST['kramar_return_reason'] ?? '' ) );

		$order->update_meta_data( '_kramar_return_requested_at', $timestamp );
		$order->update_meta_data( '_kramar_return_reason', $reason );
		$order->update_meta_data( '_kramar_return_declaration', $declaration );
		$order->save_meta_data();

		$order->update_status( 'return-requested', __( 'Customer submitted a withdrawal request.', 'kramar' ) );

		$this->render_withdrawal_confirmation( $order, $declaration, $timestamp );
	}

	/**
	 * Render the success confirmation after a withdrawal is submitted.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order       The order.
	 * @param string    $declaration The declaration text stored.
	 * @param string    $timestamp   MySQL datetime of submission.
	 */
	private function render_withdrawal_confirmation( \WC_Order $order, string $declaration, string $timestamp ): void {
		?>
		<div class="kramar-withdrawal kramar-withdrawal--confirmed">
			<p class="kramar-withdrawal__success">
				<?php esc_html_e( 'Your withdrawal request has been received. You will receive a confirmation by email.', 'kramar' ); ?>
			</p>
			<dl class="kramar-withdrawal__declaration-summary">
				<dt><?php esc_html_e( 'Order', 'kramar' ); ?></dt>
				<dd>#<?php echo esc_html( (string) $order->get_order_number() ); ?></dd>
				<dt><?php esc_html_e( 'Declaration', 'kramar' ); ?></dt>
				<dd><?php echo esc_html( $declaration ); ?></dd>
				<dt><?php esc_html_e( 'Submitted', 'kramar' ); ?></dt>
				<dd><?php echo esc_html( $timestamp ); ?></dd>
			</dl>
		</div>
		<?php
	}

	private const VERIFY_EMAIL_ID = 'kramar_withdrawal_verify';

	public function ensure_verification_email(): void {
		// The config entry and the template blocks live in two different options
		// and are checked separately — an install that has one but not the other
		// would otherwise never get the missing half.
		$custom = get_option( 'kramar_custom_emails', [] );
		if ( ! is_array( $custom ) ) {
			$custom = [];
		}

		if ( ! isset( $custom[ self::VERIFY_EMAIL_ID ] ) ) {
			$custom[ self::VERIFY_EMAIL_ID ] = [
				'title'               => __( 'Withdrawal Verification', 'kramar' ),
				'description'         => __( 'Sent when a customer requests to cancel an order. Contains a verification link they must click to confirm their identity.', 'kramar' ),
				'subject'             => __( 'Verify your withdrawal request for order #{order_number}', 'kramar' ),
				'enabled'             => true,
				'recipient_type'      => 'customer',
				'trigger_description' => __( 'Customer submits withdrawal form', 'kramar' ),
				'context'             => 'order',
				'source'              => 'kramar',
			];

			update_option( 'kramar_custom_emails', $custom );
		}

		$overrides = get_option( 'kramar_email_overrides', [] );
		if ( ! is_array( $overrides ) ) {
			$overrides = [];
		}

		$blocks = $overrides[ self::VERIFY_EMAIL_ID ]['blocks'] ?? [];

		if ( empty( $blocks ) ) {
			$overrides[ self::VERIFY_EMAIL_ID ]['blocks'] = [
				[
					'type'     => 'heading',
					'settings' => [ 'text' => __( 'Verify your identity', 'kramar' ), 'level' => 'h2', 'align' => 'left' ],
				],
				[
					'type'     => 'text',
					// The renderer reads a text block from `content`; seeding it under
					// `text` (as this template used to) renders an empty paragraph.
					'settings' => [
						'content' => __( 'You (or someone on your behalf) requested to cancel order <strong>#{order_number}</strong>. To continue, please verify your identity by clicking the button below.', 'kramar' ),
					],
				],
				[
					'type'     => 'button',
					'settings' => [ 'text' => __( 'Verify and continue', 'kramar' ), 'url' => '{verify_url}', 'align' => 'center' ],
				],
				[
					'type'     => 'text',
					'settings' => [
						'content' => __( 'If you did not request this, you can safely ignore this email.', 'kramar' ),
						'size'    => 'small',
					],
				],
			];
			update_option( 'kramar_email_overrides', $overrides );
			return;
		}

		// Repair templates seeded with the wrong key before this was fixed —
		// those paragraphs render blank, so the customer gets a bare button.
		$repaired = false;
		foreach ( $blocks as $i => $block ) {
			if ( 'text' !== ( $block['type'] ?? '' ) ) {
				continue;
			}
			$settings = $block['settings'] ?? [];
			if ( isset( $settings['text'] ) && '' === (string) ( $settings['content'] ?? '' ) ) {
				$settings['content'] = $settings['text'];
				unset( $settings['text'] );
				$blocks[ $i ]['settings'] = $settings;
				$repaired                 = true;
			}
		}

		if ( $repaired ) {
			$overrides[ self::VERIFY_EMAIL_ID ]['blocks'] = $blocks;
			update_option( 'kramar_email_overrides', $overrides );
		}
	}

	private function render_email_sent_notice(): void {
		?>
		<div class="kramar-withdrawal kramar-withdrawal--email-sent">
			<p class="kramar-withdrawal__success">
				<?php esc_html_e( 'If your order number and email address match, you will receive a verification email shortly. Please check your inbox and follow the link to continue.', 'kramar' ); ?>
			</p>
		</div>
		<?php
	}

	private function send_verification_email( \WC_Order $order ): void {
		$status = $order->get_status();
		if ( in_array( $status, [ 'return-requested', 'return-approved', 'return-rejected' ], true ) ) {
			return;
		}
		if ( ! $this->is_within_withdrawal_period( $order ) ) {
			return;
		}

		$to = $order->get_billing_email();
		if ( empty( $to ) ) {
			return;
		}

		$email_mgr = null;
		foreach ( $this->plugin->get_modules() as $m ) {
			if ( $m instanceof \Kramar\Modules\Email\Email_Manager ) {
				$email_mgr = $m;
				break;
			}
		}

		// Preferred path: send it as the WC_Email the Email Manager already
		// registers for this entry. That is what applies the on/off switch on the
		// Emails screen, the subject set in the Email Builder, and the
		// from/reply-to/CC/BCC settings — none of which a hand-rolled wp_mail()
		// call ever consulted. {verify_url} is resolved as a merge field (see
		// resolve_merge_fields()), so the template needs no pre-processing.
		if ( $email_mgr && $email_mgr->is_enabled() && $email_mgr->is_available() ) {
			$email_mgr->send_custom_email( self::VERIFY_EMAIL_ID, $order );
			return;
		}

		// Fallback: the Email Manager module is switched off, but this link is the
		// only way the customer can continue the withdrawal they just started, so
		// send a plain-text copy rather than silently dropping it.
		$custom = get_option( 'kramar_custom_emails', [] );
		if ( empty( $custom[ self::VERIFY_EMAIL_ID ]['enabled'] ) ) {
			return;
		}

		$url     = self::get_order_withdrawal_url( $order );
		$subject = (string) ( $custom[ self::VERIFY_EMAIL_ID ]['subject'] ?? '' );
		$subject = str_replace( '{order_number}', (string) $order->get_order_number(), $subject );
		if ( '' === $subject ) {
			$subject = __( 'Verify your withdrawal request', 'kramar' );
		}

		$body = sprintf(
			/* translators: 1: order number, 2: verification URL */
			__( "You requested to cancel order #%1\$s.\n\nOpen this link to verify your identity and continue:\n\n%2\$s\n\nIf you did not request this, you can ignore this email.", 'kramar' ),
			$order->get_order_number(),
			$url
		);

		wp_mail( $to, wp_specialchars_decode( $subject ), $body );
	}

	private function render_error( string $message ): void {
		?>
		<div class="kramar-withdrawal kramar-withdrawal--error">
			<p class="kramar-withdrawal__error"><?php echo esc_html( $message ); ?></p>
		</div>
		<?php
	}

	// -------------------------------------------------------------------------
	// 14-Day Withdrawal Period Check
	// -------------------------------------------------------------------------

	/**
	 * Check whether the order is still within the 14-day withdrawal window.
	 *
	 * Uses date_completed when set, falls back to date_created.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order The order to check.
	 * @return bool True if within 14 days.
	 */
	/**
	 * Find an order by its displayed order number.
	 *
	 * Checks formatted sequential number meta first, then raw sequential number,
	 * then falls back to WC order ID for stores without sequential numbering.
	 *
	 * @since 1.0.0
	 *
	 * @param string $number The order number as entered by the customer.
	 * @return \WC_Order|null The matched order, or null.
	 */
	/**
	 * @since 1.0.0
	 * @return array{button: string, input: string, select: string, label: string}
	 */
	private function get_client_ip(): string {
		return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0' ) );
	}

	private function is_rate_limited(): bool {
		$key     = 'kramar_wdr_fails_' . md5( $this->get_client_ip() );
		$attempts = (int) get_transient( $key );
		return $attempts >= 5;
	}

	private function record_failed_attempt(): void {
		$key      = 'kramar_wdr_fails_' . md5( $this->get_client_ip() );
		$attempts = (int) get_transient( $key );
		set_transient( $key, $attempts + 1, 5 * MINUTE_IN_SECONDS );
	}

	private function get_css_classes(): array {
		return [
			'button' => (string) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'css_button' ),
			'input'  => (string) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'css_input' ),
			'select' => (string) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'css_select' ),
			'label'  => (string) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'css_label' ),
		];
	}

	private function find_order_by_number( string $number ): ?\WC_Order {
		if ( '' === $number ) {
			return null;
		}

		$orders = wc_get_orders( [
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- intentional lookup by custom order number.
			'meta_key'   => '_order_number_formatted',
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- intentional lookup by custom order number.
			'meta_value' => $number,
			'limit'      => 1,
			'return'     => 'ids',
		] );

		if ( ! empty( $orders ) ) {
			$order = wc_get_order( $orders[0] );
			return $order instanceof \WC_Order ? $order : null;
		}

		if ( ctype_digit( $number ) ) {
			$orders = wc_get_orders( [
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- intentional lookup by custom order number.
				'meta_key'   => '_order_number',
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- intentional lookup by custom order number.
			'meta_value' => $number,
				'limit'      => 1,
				'return'     => 'ids',
			] );

			if ( ! empty( $orders ) ) {
				$order = wc_get_order( $orders[0] );
				return $order instanceof \WC_Order ? $order : null;
			}

			$order = wc_get_order( (int) $number );
			return $order instanceof \WC_Order ? $order : null;
		}

		return null;
	}

	private function is_within_withdrawal_period( \WC_Order $order ): bool {
		$days = (int) apply_filters( 'kramar_withdrawal_period_days', 14, $order );

		/**
		 * When the 14-day withdrawal period starts for this order.
		 *
		 * Art. 6:230o BW (and the Consumer Rights Directive) run the period from
		 * the day the consumer takes physical possession of the goods, not from
		 * the order date. WooCommerce's date_completed is the closest signal we
		 * have — but a shop that fulfils using its own custom statuses, which
		 * this plugin's Custom Order Statuses module actively encourages, may
		 * never set it.
		 *
		 * Falling back to date_created, as this used to, starts the clock at
		 * checkout and hands the customer a shorter window than the law gives
		 * them. Treat "not yet delivered" as "the period has not started",
		 * which keeps the request open rather than expiring it early. Shops that
		 * track delivery elsewhere can return that date here.
		 *
		 * @since 1.0.2
		 *
		 * @param \WC_DateTime|null $start Delivery/completion date, or null if unknown.
		 * @param \WC_Order         $order The order.
		 */
		$start = apply_filters( 'kramar_withdrawal_period_start', $order->get_date_completed(), $order );

		if ( ! $start instanceof \WC_DateTime ) {
			return true; // no evidence of delivery — the clock hasn't started
		}

		return ( time() - $start->getTimestamp() ) < $days * DAY_IN_SECONDS;
	}

	// -------------------------------------------------------------------------
	// My Account Integration
	// -------------------------------------------------------------------------

	/**
	 * Add a "Cancel order" button to the My Account orders list.
	 *
	 * Only shown for orders within the 14-day period that don't already have a
	 * return status active.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, array<string, string>> $actions Existing action buttons.
	 * @param \WC_Order                            $order   The order object.
	 * @return array<string, array<string, string>> Modified actions.
	 */
	public function add_my_account_cancel_button( array $actions, \WC_Order $order ): array {
		$return_statuses = [ 'return-requested', 'return-approved', 'return-rejected' ];
		if ( in_array( $order->get_status(), $return_statuses, true ) ) {
			return $actions;
		}

		if ( ! $this->is_within_withdrawal_period( $order ) ) {
			return $actions;
		}

		$withdrawal_url = self::get_withdrawal_url();
		if ( '' === $withdrawal_url ) {
			return $actions;
		}

		$token = self::generate_token( $order->get_id(), $order->get_billing_email() );

		$button_label = (string) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'button_label' );

		$actions['kramar-cancel'] = [
			'url'  => add_query_arg(
				[
					'order_id' => $order->get_id(),
					'token'    => $token,
				],
				$withdrawal_url
			),
			'name' => $button_label ?: __( 'Cancel order', 'kramar' ),
		];

		return $actions;
	}

	// -------------------------------------------------------------------------
	// Email Classes
	// -------------------------------------------------------------------------

	/**
	 * Register the four withdrawal email classes with WooCommerce's mailer.
	 *
	 * The class files are loaded here; they will be created separately.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, \WC_Email> $emails Existing WC email instances.
	 * @return array<string, \WC_Email> Extended list.
	 */
	public function register_email_classes( array $emails ): array {
		require_once __DIR__ . '/class-kramar-email-withdrawal-confirmation.php';
		require_once __DIR__ . '/class-kramar-email-withdrawal-request.php';
		require_once __DIR__ . '/class-kramar-email-withdrawal-approved.php';
		require_once __DIR__ . '/class-kramar-email-withdrawal-rejected.php';
		require_once __DIR__ . '/class-kramar-email-withdrawal-info-request.php';
		require_once __DIR__ . '/class-kramar-email-withdrawal-refunded.php';

		$emails['Kramar_Email_Withdrawal_Confirmation'] = new Kramar_Email_Withdrawal_Confirmation();
		$emails['Kramar_Email_Withdrawal_Request']      = new Kramar_Email_Withdrawal_Request();
		$emails['Kramar_Email_Withdrawal_Approved']     = new Kramar_Email_Withdrawal_Approved();
		$emails['Kramar_Email_Withdrawal_Rejected']     = new Kramar_Email_Withdrawal_Rejected();
		$emails['Kramar_Email_Withdrawal_Info_Request'] = new Kramar_Email_Withdrawal_Info_Request();
		$emails['Kramar_Email_Withdrawal_Refunded']     = new Kramar_Email_Withdrawal_Refunded();

		return $emails;
	}

	/**
	 * Fire customer confirmation + admin notification emails when status → return-requested.
	 *
	 * @since 1.0.0
	 *
	 * @param int       $order_id Order ID.
	 * @param \WC_Order $order    Order object.
	 */
	public function trigger_withdrawal_emails( int $order_id, \WC_Order $order ): void {
		$mailer = WC()->mailer();
		$emails = $mailer->get_emails();

		if ( isset( $emails['Kramar_Email_Withdrawal_Confirmation'] ) ) {
			$emails['Kramar_Email_Withdrawal_Confirmation']->trigger( $order_id, $order );
		}

		if ( isset( $emails['Kramar_Email_Withdrawal_Request'] ) ) {
			$emails['Kramar_Email_Withdrawal_Request']->trigger( $order_id, $order );
		}
	}

	/**
	 * Fire approval email when status → return-approved.
	 *
	 * @since 1.0.0
	 *
	 * @param int       $order_id Order ID.
	 * @param \WC_Order $order    Order object.
	 */
	public function trigger_approved_email( int $order_id, \WC_Order $order ): void {
		$mailer = WC()->mailer();
		$emails = $mailer->get_emails();

		if ( isset( $emails['Kramar_Email_Withdrawal_Approved'] ) ) {
			$emails['Kramar_Email_Withdrawal_Approved']->trigger( $order_id, $order );
		}
	}

	/**
	 * Fire rejection email when status → return-rejected.
	 *
	 * @since 1.0.0
	 *
	 * @param int       $order_id Order ID.
	 * @param \WC_Order $order    Order object.
	 */
	public function trigger_rejected_email( int $order_id, \WC_Order $order ): void {
		$mailer = WC()->mailer();
		$emails = $mailer->get_emails();

		if ( isset( $emails['Kramar_Email_Withdrawal_Rejected'] ) ) {
			$emails['Kramar_Email_Withdrawal_Rejected']->trigger( $order_id, $order );
		}
	}

	// -------------------------------------------------------------------------
	// Admin Order Metabox
	// -------------------------------------------------------------------------

	/**
	 * Register the withdrawal metabox for both the legacy CPT order screen and
	 * the HPOS order screen.
	 *
	 * @since 1.0.0
	 */
	public function register_metabox(): void {
		$screens = [ 'shop_order', 'woocommerce_page_wc-orders' ];
		foreach ( $screens as $screen ) {
			add_meta_box(
				'kramar-withdrawal-request',
				__( 'Withdrawal Request', 'kramar' ),
				[ $this, 'render_metabox_content' ],
				$screen,
				'side',
				'default'
			);
		}
	}

	/**
	 * Render the metabox body.
	 *
	 * Accepts either a WP_Post (legacy CPT) or a WC_Order (HPOS) — WooCommerce
	 * passes the order object to metabox callbacks on the HPOS order screen.
	 *
	 * @since 1.0.0
	 *
	 * @param int|\WP_Post|\WC_Order|null $post_or_id Post/order object (or ID).
	 * @param mixed                       $metabox    Metabox data (unused).
	 */
	public function render_metabox_content( $post_or_id, mixed $metabox = null ): void {
		if ( $post_or_id instanceof \WC_Order ) {
			$order = $post_or_id;
		} else {
			$order = wc_get_order( is_object( $post_or_id ) ? (int) $post_or_id->ID : (int) $post_or_id );
		}

		if ( ! $order instanceof \WC_Order ) {
			return;
		}

		$order_id = $order->get_id();

		$requested_at = $order->get_meta( '_kramar_return_requested_at' );
		$declaration  = $order->get_meta( '_kramar_return_declaration' );
		$reason       = $order->get_meta( '_kramar_return_reason' );
		$current_status = $order->get_status();

		if ( '' === $requested_at ) {
			echo '<p class="description">' . esc_html__( 'No withdrawal request submitted.', 'kramar' ) . '</p>';
			return;
		}

		?>
		<dl style="margin:0 0 12px;">
			<dt><strong><?php esc_html_e( 'Submitted', 'kramar' ); ?></strong></dt>
			<dd><?php echo esc_html( $requested_at ); ?></dd>

			<?php if ( '' !== $reason ) : ?>
				<dt><strong><?php esc_html_e( 'Reason', 'kramar' ); ?></strong></dt>
				<dd><?php echo esc_html( $reason ); ?></dd>
			<?php endif; ?>

			<dt><strong><?php esc_html_e( 'Declaration', 'kramar' ); ?></strong></dt>
			<dd><?php echo esc_html( $declaration ); ?></dd>

			<dt><strong><?php esc_html_e( 'Status', 'kramar' ); ?></strong></dt>
			<dd><?php echo esc_html( wc_get_order_status_name( $current_status ) ); ?></dd>
		</dl>
		<?php

		if ( in_array( $current_status, [ 'return-approved', 'return-rejected' ], true ) ) {
			$actioned_by = $order->get_meta( '_kramar_return_actioned_by' );
			$actioned_at = $order->get_meta( '_kramar_return_actioned_at' );
			if ( '' !== $actioned_at ) {
				echo '<p class="description">';
				printf(
					/* translators: 1: username, 2: datetime */
					esc_html__( 'Actioned by %1$s on %2$s.', 'kramar' ),
					esc_html( $actioned_by ),
					esc_html( $actioned_at )
				);
				echo '</p>';
			}
			return;
		}

		// Show action buttons for pending requests.
		//
		// NOTE: the order edit screen (both legacy CPT and HPOS) wraps the whole
		// page in a single <form>. A nested <form> here would be dropped by the
		// HTML parser, so the buttons would silently submit WC's order form instead
		// of our PRG handler. We therefore render form-free controls and submit an
		// isolated form (appended to <body>, outside the order form) via JS.
		$form_url = admin_url( 'admin-post.php' );
		$nonce    = wp_create_nonce( 'kramar_return_action_' . $order_id );
		?>
		<div
			class="kramar-return-actions"
			data-action-url="<?php echo esc_url( $form_url ); ?>"
			data-post-action="<?php echo esc_attr( self::RETURN_ACTION ); ?>"
			data-order-id="<?php echo esc_attr( (string) $order_id ); ?>"
			data-nonce="<?php echo esc_attr( $nonce ); ?>"
		>
			<p>
				<button type="button" data-kramar-return="approve" class="button button-primary button-small">
					<?php esc_html_e( 'Approve', 'kramar' ); ?>
				</button>
				&nbsp;
				<button type="button" data-kramar-return="info" class="button button-small">
					<?php esc_html_e( 'Request Info', 'kramar' ); ?>
				</button>
			</p>

			<p>
				<label for="kramar-info-message-<?php echo esc_attr( (string) $order_id ); ?>">
					<?php esc_html_e( 'Message for "Request Info" (optional)', 'kramar' ); ?>
				</label>
				<textarea
					id="kramar-info-message-<?php echo esc_attr( (string) $order_id ); ?>"
					name="kramar_info_message"
					rows="2"
					class="widefat"
				></textarea>
			</p>

			<p>
				<label for="kramar-reject-reason-<?php echo esc_attr( (string) $order_id ); ?>">
					<?php esc_html_e( 'Rejection reason (required to reject)', 'kramar' ); ?>
				</label>
				<textarea
					id="kramar-reject-reason-<?php echo esc_attr( (string) $order_id ); ?>"
					name="kramar_reject_reason"
					rows="3"
					class="widefat"
				></textarea>
			</p>
			<p>
				<button type="button" data-kramar-return="reject" class="button button-small">
					<?php esc_html_e( 'Reject', 'kramar' ); ?>
				</button>
			</p>
		</div>
		<script>
		( function () {
			if ( window.kramarReturnActionsBound ) {
				return;
			}
			window.kramarReturnActionsBound = true;
			document.addEventListener( 'click', function ( e ) {
				var btn = e.target.closest( '[data-kramar-return]' );
				if ( ! btn ) {
					return;
				}
				e.preventDefault();
				var wrap = btn.closest( '.kramar-return-actions' );
				if ( ! wrap ) {
					return;
				}
				var act = btn.getAttribute( 'data-kramar-return' );
				var reject = wrap.querySelector( '[name="kramar_reject_reason"]' );
				if ( 'reject' === act && reject && '' === reject.value.trim() ) {
					reject.focus();
					return;
				}
				var form = document.createElement( 'form' );
				form.method = 'post';
				form.action = wrap.getAttribute( 'data-action-url' );
				form.style.display = 'none';
				var add = function ( name, value ) {
					var i = document.createElement( 'input' );
					i.type = 'hidden';
					i.name = name;
					i.value = value;
					form.appendChild( i );
				};
				add( 'action', wrap.getAttribute( 'data-post-action' ) );
				add( 'kramar_order_id', wrap.getAttribute( 'data-order-id' ) );
				add( 'kramar_return_nonce', wrap.getAttribute( 'data-nonce' ) );
				add( 'kramar_return_action', act );
				var info = wrap.querySelector( '[name="kramar_info_message"]' );
				if ( info ) {
					add( 'kramar_info_message', info.value );
				}
				if ( reject ) {
					add( 'kramar_reject_reason', reject.value );
				}
				document.body.appendChild( form );
				form.submit();
			} );
		}() );
		</script>
		<?php
	}

	// -------------------------------------------------------------------------
	// Admin Return Action Handler (PRG)
	// -------------------------------------------------------------------------

	/**
	 * Handle admin metabox form submissions (approve / reject / info).
	 *
	 * Hooked on `admin_post_kramar_return_action`. Verifies nonce + capability,
	 * then processes the action and redirects back (PRG).
	 *
	 * @since 1.0.0
	 */
	public function handle_return_action(): void {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die( esc_html__( 'You do not have permission to manage return requests.', 'kramar' ) );
		}

		$order_id = absint( $_POST['kramar_order_id'] ?? 0 );

		check_admin_referer( 'kramar_return_action_' . $order_id, 'kramar_return_nonce' );

		$order = wc_get_order( $order_id );
		if ( ! $order instanceof \WC_Order ) {
			wp_safe_redirect( wp_get_referer() ?: admin_url() );
			exit;
		}

		$action = sanitize_key( $_POST['kramar_return_action'] ?? '' );

		// A decided request is not decided again. The metabox hides the buttons
		// once a decision is made, but that is only the UI: the nonce stays valid
		// for hours and is not single-use, so a stale tab, a double-click or a
		// resubmitted POST could run approve after reject, or approve twice —
		// and approve now genuinely refunds through the gateway, so twice means
		// paying twice.
		$decided = [ 'return-approved', 'return-rejected' ];
		if ( in_array( $action, [ 'approve', 'reject' ], true ) && in_array( $order->get_status(), $decided, true ) ) {
			$order->add_order_note(
				__( 'Ignored a repeated decision on a withdrawal request that was already settled.', 'kramar' )
			);
			$action = '';
		}

		if ( 'approve' === $action ) {
			$this->process_approve( $order );
		} elseif ( 'reject' === $action ) {
			$this->process_reject( $order );
		} elseif ( 'info' === $action ) {
			$this->process_info_request( $order );
		}

		$redirect = get_edit_post_link( $order_id, 'url' );
		if ( ! $redirect ) {
			$redirect = admin_url( 'admin.php?page=wc-orders&action=edit&id=' . $order_id );
		}

		wp_safe_redirect( $redirect );
		exit;
	}

	/**
	 * Process an approve action.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order The order being approved.
	 */
	private function process_approve( \WC_Order $order ): void {
		$order->update_meta_data( '_kramar_return_actioned_by', wp_get_current_user()->user_login );
		$order->update_meta_data( '_kramar_return_actioned_at', current_time( 'mysql' ) );

		// Snapshot the configured return-instructions page URL so the approval
		// email can link to it (the email reads _kramar_return_instructions_url).
		$instructions_page_id = (int) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'return_instructions_page_id' );
		$instructions_url     = $instructions_page_id > 0 ? (string) get_permalink( $instructions_page_id ) : '';
		$order->update_meta_data( '_kramar_return_instructions_url', $instructions_url );

		$order->save_meta_data();

		$order->update_status( 'return-approved', __( 'Return approved by shop manager.', 'kramar' ) );

		// Approving does not move money, deliberately. Refunds go through
		// WooCommerce's own refund screen — the module's "How it works" panel
		// walks the admin through exactly that, and it is the flow that handles
		// part-refunds, restocking, and refunding via the gateway or manually.
		// A short-lived opt-in setting here duplicated that badly: it created the
		// refund record and the Refunded status while never calling the gateway,
		// so the customer was told the money was on its way and it never was.
	}

	/**
	 * Process a reject action.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order The order being rejected.
	 */
	private function process_reject( \WC_Order $order ): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce already verified by check_admin_referer() in handle_return_action() before this method is called
		$reason = sanitize_textarea_field( wp_unslash( $_POST['kramar_reject_reason'] ?? '' ) );
		if ( '' === $reason ) {
			// Require a reason — silently bail; the form label communicates this.
			return;
		}

		$order->update_meta_data( '_kramar_return_actioned_by', wp_get_current_user()->user_login );
		$order->update_meta_data( '_kramar_return_actioned_at', current_time( 'mysql' ) );
		$order->update_meta_data( '_kramar_return_reject_reason', $reason );
		$order->save_meta_data();

		$order->update_status(
			'return-rejected',
			/* translators: %s: rejection reason */
			sprintf( __( 'Return rejected: %s', 'kramar' ), $reason )
		);
	}

	/**
	 * Process a "request more info" action.
	 *
	 * Adds a customer-visible order note prompting them to reply with more detail.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order The order.
	 */
	private function process_info_request( \WC_Order $order ): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce already verified by check_admin_referer() in handle_return_action() before this method is called
		$message = sanitize_textarea_field( wp_unslash( $_POST['kramar_info_message'] ?? '' ) );
		if ( '' === $message ) {
			$message = __( 'We need additional information to process your withdrawal request. Please contact us with the details of your request.', 'kramar' );
		}

		// Store the message so the info-request email can render it (the email
		// reads _kramar_return_info_message).
		$order->update_meta_data( '_kramar_return_info_message', $message );
		$order->save_meta_data();

		// Recorded as a private note, not a customer note. A customer note makes
		// WooCommerce send its own "A note has been added to your order" email,
		// which arrived alongside Kramar's own info-request email — two messages
		// seconds apart about one action.
		$order->add_order_note(
			$message,
			false, // is_customer_note — Kramar sends its own email for this
			true   // added_by_user
		);

		// Send the dedicated info-request email.
		$mailer = WC()->mailer();
		if ( isset( $mailer->emails['Kramar_Email_Withdrawal_Info_Request'] ) ) {
			$mailer->emails['Kramar_Email_Withdrawal_Info_Request']->trigger( $order->get_id(), $order );
		}
	}

	/**
	 * Send the "refund completed" email once when an approved withdrawal/return
	 * order is refunded (automatic or manual).
	 *
	 * @since 1.0.0
	 *
	 * @param int $order_id  Order ID.
	 * @param int $refund_id Refund ID.
	 */
	public function on_order_refunded( int $order_id, int $refund_id ): void {
		$order = wc_get_order( $order_id );
		if ( ! $order instanceof \WC_Order ) {
			return;
		}

		// Only for Kramar withdrawal/return orders.
		if ( 'return-approved' !== $order->get_status() && '' === (string) $order->get_meta( '_kramar_return_actioned_by' ) ) {
			return;
		}

		// Only announce completion once the order is FULLY refunded — a partial
		// (manual) refund should not fire the "refund completed" email prematurely.
		if ( (float) $order->get_total_refunded() < (float) $order->get_total() ) {
			return;
		}

		// Idempotency — send once.
		if ( $order->get_meta( '_kramar_refund_email_sent' ) ) {
			return;
		}

		$mailer = WC()->mailer();
		if ( isset( $mailer->emails['Kramar_Email_Withdrawal_Refunded'] ) ) {
			$mailer->emails['Kramar_Email_Withdrawal_Refunded']->trigger( $order_id, $order );
			$order->update_meta_data( '_kramar_refund_email_sent', current_time( 'mysql', true ) );
			$order->save();
		}
	}

	// -------------------------------------------------------------------------
	// Admin Settings UI
	// -------------------------------------------------------------------------

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

		$withdrawal_page_id          = (int) $this->plugin->get_setting( $cat, $slug, 'withdrawal_page_id' );
		$button_label                = (string) $this->plugin->get_setting( $cat, $slug, 'button_label' );
		$email_link_text             = (string) $this->plugin->get_setting( $cat, $slug, 'email_link_text' );
		$email_link_in_emails        = (bool) $this->plugin->get_setting( $cat, $slug, 'email_link_in_emails' );
		$confirm_button_label        = (string) $this->plugin->get_setting( $cat, $slug, 'confirm_button_label' );
		$return_instructions_page_id = (int) $this->plugin->get_setting( $cat, $slug, 'return_instructions_page_id' );
		$css                         = $this->get_css_classes();

		$field = static function ( string $key ) use ( $option_name, $cat, $slug ): string {
			return sprintf( '%s[%s][%s][%s]', $option_name, $cat, $slug, $key );
		};
		?>
		<div class="kramar-module__extra" style="flex-direction:column;align-items:flex-start;">
			<div class="kramar-settings-stack">

				<div style="background:#f8f9fa;border:1px solid #e4e4e7;border-radius:4px;padding:12px 16px;max-width:680px;font-size:13px;line-height:1.6;color:#50575e;">
					<strong style="color:#1d2327;"><?php esc_html_e( 'How it works', 'kramar' ); ?></strong>
					<ol style="margin:6px 0 0;padding-left:20px;">
						<li><?php esc_html_e( 'Customer follows the link in their order confirmation, or enters their order number and email on the withdrawal page.', 'kramar' ); ?></li>
						<li><?php esc_html_e( 'A verification email is sent (no data is revealed until the email is confirmed).', 'kramar' ); ?></li>
						<li><?php esc_html_e( 'Customer clicks the link and confirms — order status becomes "Return Requested" and you receive an admin notification.', 'kramar' ); ?></li>
						<li><?php esc_html_e( 'You review the request and change the order status to "Return Approved" or "Return Rejected".', 'kramar' ); ?></li>
						<li><?php esc_html_e( 'The customer automatically receives an approval or rejection email.', 'kramar' ); ?></li>
						<li><?php esc_html_e( 'If approved: open the order, click "Refund", select items/amounts, click "Refund via [gateway]" or "Refund manually", and set the order status to "Refunded".', 'kramar' ); ?></li>
					</ol>
					<p style="margin:8px 0 0;color:#1d2327;"><?php esc_html_e( 'Nothing is automatic — the module handles the request and emails, you decide whether to approve and how to refund.', 'kramar' ); ?></p>
				</div>

				<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px 20px;max-width:680px;">
					<div>
						<label for="kramar-rr-withdrawal-page"><strong><?php esc_html_e( 'Withdrawal page', 'kramar' ); ?></strong></label><br />
						<?php
						// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_dropdown_pages() escapes its name attribute and option labels internally.
						wp_dropdown_pages(
							[
								'id'               => 'kramar-rr-withdrawal-page',
								'name'             => $field( 'withdrawal_page_id' ),
								'selected'         => $withdrawal_page_id,
								'show_option_none' => __( '-- auto-create --', 'kramar' ),
								'option_none_value' => '0',
							]
						);
						// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
						?>
					</div>
					<div>
						<label for="kramar-rr-instructions-page"><strong><?php esc_html_e( 'Return instructions page', 'kramar' ); ?></strong></label><br />
						<?php
						// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_dropdown_pages() escapes its name attribute and option labels internally.
						wp_dropdown_pages(
							[
								'id'               => 'kramar-rr-instructions-page',
								'name'             => $field( 'return_instructions_page_id' ),
								'selected'         => $return_instructions_page_id,
								'show_option_none' => __( '-- none --', 'kramar' ),
								'option_none_value' => '0',
							]
						);
						// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
						?>
					</div>

					<div>
						<label for="kramar-rr-button-label"><strong><?php esc_html_e( 'My Account button', 'kramar' ); ?></strong></label><br />
						<input type="text" id="kramar-rr-button-label" name="<?php echo esc_attr( $field( 'button_label' ) ); ?>" value="<?php echo esc_attr( $button_label ); ?>" class="regular-text" style="width:100%;" />
					</div>
					<div>
						<label for="kramar-rr-confirm-label"><strong><?php esc_html_e( 'Confirm button', 'kramar' ); ?></strong></label><br />
						<input type="text" id="kramar-rr-confirm-label" name="<?php echo esc_attr( $field( 'confirm_button_label' ) ); ?>" value="<?php echo esc_attr( $confirm_button_label ); ?>" class="regular-text" style="width:100%;" />
					</div>

					<div style="grid-column:span 2;">
						<label for="kramar-rr-email-link-text"><strong><?php esc_html_e( 'Email link text', 'kramar' ); ?></strong></label><br />
						<input type="text" id="kramar-rr-email-link-text" name="<?php echo esc_attr( $field( 'email_link_text' ) ); ?>" value="<?php echo esc_attr( $email_link_text ); ?>" class="regular-text" style="width:100%;" />
						<span class="description"><?php esc_html_e( 'Link text in transactional emails pointing to the withdrawal page.', 'kramar' ); ?></span>
						<p style="margin:8px 0 0;">
							<label>
								<input type="checkbox" name="<?php echo esc_attr( $field( 'email_link_in_emails' ) ); ?>" value="1" <?php checked( $email_link_in_emails ); ?> />
								<?php esc_html_e( 'Add this link to order confirmation emails', 'kramar' ); ?>
							</label><br />
							<span class="description"><?php esc_html_e( 'Required by Directive (EU) 2023/2673 since 19 June 2026. The button is part of the confirmation templates — move or reword it in the email builder. Deleting it from a template adds it back automatically; untick here to remove it entirely.', 'kramar' ); ?></span>
						</p>
					</div>
				</div>

				<div style="margin-top:16px;padding-top:12px;border-top:1px solid #e4e4e7;">
					<p style="margin:0 0 8px;"><strong><?php esc_html_e( 'Front-end CSS classes', 'kramar' ); ?></strong>
						<span class="description">&mdash; <?php esc_html_e( 'Bootstrap 5 defaults', 'kramar' ); ?></span>
					</p>
					<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:8px;max-width:680px;">
						<div>
							<label for="kramar-rr-css-button"><?php esc_html_e( 'Button', 'kramar' ); ?></label><br />
							<input type="text" id="kramar-rr-css-button" name="<?php echo esc_attr( $field( 'css_button' ) ); ?>" value="<?php echo esc_attr( $css['button'] ); ?>" style="width:100%;" placeholder="btn btn-primary" />
						</div>
						<div>
							<label for="kramar-rr-css-input"><?php esc_html_e( 'Input', 'kramar' ); ?></label><br />
							<input type="text" id="kramar-rr-css-input" name="<?php echo esc_attr( $field( 'css_input' ) ); ?>" value="<?php echo esc_attr( $css['input'] ); ?>" style="width:100%;" placeholder="form-control" />
						</div>
						<div>
							<label for="kramar-rr-css-select"><?php esc_html_e( 'Select', 'kramar' ); ?></label><br />
							<input type="text" id="kramar-rr-css-select" name="<?php echo esc_attr( $field( 'css_select' ) ); ?>" value="<?php echo esc_attr( $css['select'] ); ?>" style="width:100%;" placeholder="form-select" />
						</div>
						<div>
							<label for="kramar-rr-css-label"><?php esc_html_e( 'Label', 'kramar' ); ?></label><br />
							<input type="text" id="kramar-rr-css-label" name="<?php echo esc_attr( $field( 'css_label' ) ); ?>" value="<?php echo esc_attr( $css['label'] ); ?>" style="width:100%;" placeholder="form-label" />
						</div>
					</div>
				</div>

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