<?php
/**
 * Professional Account Flow module.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar\Modules\Account;

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

/**
 * Professional Account Flow module.
 *
 * Makes the WooCommerce account/login experience feel like a modern
 * marketplace, with minimal friction and full GDPR compliance.
 *
 * Phase 1 — Works with both Classic and Block checkout:
 *   1. Post-checkout guest account creation (opt-in CTA on thank-you page; GDPR: not pre-ticked).
 *   2. Signed guest order tracking via HMAC token (no login required).
 *   3. Auto-login after account creation (`woocommerce_created_customer`).
 *   4. Login redirect control (`woocommerce_login_redirect`).
 *   5. Enhanced My Account dashboard: last orders, reorder, completeness nudges.
 *
 * Phase 2 — Classic checkout only (Blocks do not fire `woocommerce_login_form_*`):
 *   6. Email-first login form: enter email → reveal password or registration.
 *
 * @since 1.0.0
 */
final class Account_Flow extends \Kramar\Module {

	/**
	 * Query var used by the guest tracking endpoint.
	 *
	 * @since 1.0.0
	 */
	private const TRACK_QUERY_VAR = 'kramar-track';

	/**
	 * AJAX action for the post-checkout account-creation form.
	 *
	 * @since 1.0.0
	 */
	private const AJAX_CREATE_ACCOUNT = 'kramar_create_account';

	/**
	 * AJAX action for the email-first login check.
	 *
	 * @since 1.0.0
	 */
	private const AJAX_EMAIL_CHECK = 'kramar_email_check';

	/**
	 * AJAX action for the reorder.
	 *
	 * @since 1.0.0
	 */
	private const AJAX_REORDER = 'kramar_reorder';

	// -------------------------------------------------------------------------
	// Module identity
	// -------------------------------------------------------------------------

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

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

	/** {@inheritDoc} */
	public function get_name(): string {
		return __( 'Professional Account Flow', 'kramar' );
	}

	/** {@inheritDoc} */
	public function get_description(): string {
		return __( 'Modern account flow: post-checkout opt-in account creation (GDPR: not pre-ticked), signed guest order tracking, auto-login, login redirect control, and an enhanced My Account dashboard with reorder buttons. Email-first login (Phase 2) is classic checkout only — Blocks do not fire the required PHP hooks.', 'kramar' );
	}

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

	/** {@inheritDoc} */
	public function get_default_settings(): array {
		return [
			'enabled'               => false,
			'guest_account_cta'     => true,
			'auto_login'            => true,
			'login_redirect'        => 'referer',  // 'referer' | 'myaccount' | 'shop' | 'custom'
			'login_redirect_url'    => '',
			'guest_tracking'        => true,
			'tracking_page_id'      => 0,
			'enhanced_dashboard'    => true,
			'email_first_login'     => false,
		];
	}

	/** {@inheritDoc} */
	public function sanitize_settings( array $input ): array {
		$redirect = sanitize_key( $input['login_redirect'] ?? 'referer' );
		if ( ! in_array( $redirect, [ 'referer', 'myaccount', 'shop', 'custom' ], true ) ) {
			$redirect = 'referer';
		}

		return [
			'enabled'               => ! empty( $input['enabled'] ),
			'guest_account_cta'     => ! empty( $input['guest_account_cta'] ),
			'auto_login'            => ! empty( $input['auto_login'] ),
			'login_redirect'        => $redirect,
			'login_redirect_url'    => esc_url_raw( $input['login_redirect_url'] ?? '' ),
			'guest_tracking'        => ! empty( $input['guest_tracking'] ),
			'tracking_page_id'      => absint( $input['tracking_page_id'] ?? 0 ),
			'enhanced_dashboard'    => ! empty( $input['enhanced_dashboard'] ),
			'email_first_login'     => ! empty( $input['email_first_login'] ),
		];
	}

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

	/** {@inheritDoc} */
	protected function run(): void {

		// ── Phase 1: works in Classic AND Blocks ──────────────────────────────

		// 1. Guest tracking: register the query var and rewrite rule.
		if ( $this->setting( 'guest_tracking' ) ) {
			add_action( 'init', [ $this, 'register_tracking_endpoint' ] );
			add_action( 'template_redirect', [ $this, 'maybe_render_tracking_page' ] );
			add_action( 'woocommerce_new_order', [ $this, 'stamp_tracking_token' ], 10, 2 );
			add_action( 'woocommerce_email_after_order_table', [ $this, 'inject_tracking_link_in_email' ], 10, 4 );

			// {tracking_link} merge field (the after-order-table injection is
			// discarded under Kramar builder templates).
			add_filter( 'kramar_email_merge_fields', [ $this, 'register_tracking_merge_field' ], 10, 2 );
			add_filter( 'kramar_email_merge_field_values', [ $this, 'resolve_tracking_merge_field' ], 10, 2 );

			// Editable tracking page + shortcode + manual (email-confirm) lookup.
			add_action( 'admin_init', [ $this, 'maybe_create_tracking_page' ] );
			add_shortcode( 'kramar_order_tracking', [ $this, 'render_tracking_shortcode' ] );
			add_filter( 'wp_robots', [ $this, 'tracking_page_robots' ] );
		}

		// 2. Post-checkout account CTA (thank-you page).
		if ( $this->setting( 'guest_account_cta' ) ) {
			add_action( 'woocommerce_thankyou', [ $this, 'maybe_show_account_cta' ], 20 );
			// nopriv: the thank-you page is visited as a guest.
			add_action( 'wp_ajax_nopriv_' . self::AJAX_CREATE_ACCOUNT, [ $this, 'ajax_create_account' ] );
			add_action( 'wp_ajax_' . self::AJAX_CREATE_ACCOUNT, [ $this, 'ajax_create_account' ] );
		}

		// 3. Auto-login after WC creates a customer (fires in Classic + Blocks).
		if ( $this->setting( 'auto_login' ) ) {
			add_action( 'woocommerce_created_customer', [ $this, 'auto_login_after_registration' ], 10, 3 );
		}

		// 4. Login redirect.
		add_filter( 'woocommerce_login_redirect', [ $this, 'filter_login_redirect' ], 10, 2 );

		// 5. Enhanced dashboard.
		if ( $this->setting( 'enhanced_dashboard' ) ) {
			add_action( 'woocommerce_account_dashboard', [ $this, 'render_enhanced_dashboard' ] );
			add_filter( 'woocommerce_my_account_my_orders_actions', [ $this, 'add_reorder_button' ], 10, 2 );
			add_action( 'wp_ajax_' . self::AJAX_REORDER, [ $this, 'ajax_reorder' ] );
			// Non-JS fallback for the orders-list "Reorder" link (?kramar_reorder=ID).
			add_action( 'template_redirect', [ $this, 'maybe_handle_reorder_url' ] );
		}

		// ── Phase 2: classic checkout only ────────────────────────────────────

		// 6. Email-first login (classic only — Blocks don't fire this hook).
		if ( $this->setting( 'email_first_login' ) ) {
			add_action( 'woocommerce_login_form_start', [ $this, 'render_email_first_field' ] );
			add_action( 'wp_ajax_nopriv_' . self::AJAX_EMAIL_CHECK, [ $this, 'ajax_email_check' ] );
		}

		// Enqueue frontend assets.
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_assets' ] );
	}

	// -------------------------------------------------------------------------
	// Helper
	// -------------------------------------------------------------------------

	/**
	 * Shorthand for reading own settings.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Setting key.
	 * @return mixed
	 */
	private function setting( string $key ): mixed {
		return $this->plugin->get_setting( $this->get_category(), $this->get_slug(), $key );
	}

	// -------------------------------------------------------------------------
	// Feature 2 — Guest tracking endpoint
	// -------------------------------------------------------------------------

	/**
	 * Register the `kramar-track` query var and its rewrite rule.
	 *
	 * URL shape: /?kramar-track=1&order_id=X&token=Y
	 * (No flush needed per request — we use a simple query var.)
	 *
	 * @since 1.0.0
	 */
	public function register_tracking_endpoint(): void {
		add_rewrite_tag( '%' . self::TRACK_QUERY_VAR . '%', '([^&]+)' );
	}

	/**
	 * Generate an HMAC token for a given order.
	 *
	 * Token covers order_id + order_key to make it impossible to enumerate orders
	 * by ID alone.
	 *
	 * @since 1.0.0
	 *
	 * @param int    $order_id  WooCommerce order ID.
	 * @param string $order_key WooCommerce order key.
	 * @return string Hex HMAC-SHA256 token.
	 */
	public static function generate_tracking_token( int $order_id, string $order_key ): string {
		return hash_hmac( 'sha256', $order_id . '|' . $order_key, wp_salt( 'auth' ) );
	}

	/**
	 * Verify a tracking token for a given order.
	 *
	 * @since 1.0.0
	 *
	 * @param int    $order_id  Order ID.
	 * @param string $order_key Order key.
	 * @param string $token     Token to verify.
	 * @return bool True if the token is valid.
	 */
	public static function verify_tracking_token( int $order_id, string $order_key, string $token ): bool {
		return hash_equals( self::generate_tracking_token( $order_id, $order_key ), $token );
	}

	/**
	 * Build the signed tracking URL for a given order.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order The order.
	 * @return string Fully-qualified tracking URL.
	 */
	public static function get_tracking_url( \WC_Order $order ): string {
		$token = self::generate_tracking_token( $order->get_id(), $order->get_order_key() );
		$args  = [
			'order_id' => $order->get_id(),
			'token'    => $token,
		];

		// Prefer the editable tracking page when one exists.
		$settings = get_option( \Kramar\Settings::OPTION_NAME, [] );
		$page_id  = (int) ( $settings['account']['account-flow']['tracking_page_id'] ?? 0 );
		if ( $page_id > 0 && get_post( $page_id ) ) {
			return add_query_arg( $args, (string) get_permalink( $page_id ) );
		}

		$args[ self::TRACK_QUERY_VAR ] = '1';
		return add_query_arg( $args, home_url( '/' ) );
	}

	/**
	 * Register the {tracking_link} merge field in the Email Manager picker.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, array<string, mixed>> $fields  Merge field groups.
	 * @param string                              $context Email context.
	 * @return array<string, array<string, mixed>>
	 */
	public function register_tracking_merge_field( array $fields, string $context ): array {
		if ( 'order' === $context && isset( $fields['kramar']['fields'] ) ) {
			$fields['kramar']['fields']['{tracking_link}'] = __( 'Order Tracking Link', 'kramar' );
		}
		return $fields;
	}

	/**
	 * Resolve {tracking_link} for an order.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, string> $replacements Tag → value map.
	 * @param \WC_Order             $order        Order.
	 * @return array<string, string>
	 */
	public function resolve_tracking_merge_field( array $replacements, \WC_Order $order ): array {
		$replacements['{tracking_link}'] = self::get_tracking_url( $order );
		return $replacements;
	}

	/**
	 * Stamp an HMAC tracking token on a new order and store it in order meta.
	 *
	 * @since 1.0.0
	 *
	 * @param int       $order_id Order ID.
	 * @param \WC_Order $order    Order object.
	 */
	public function stamp_tracking_token( int $order_id, \WC_Order $order ): void {
		// Only stamp once.
		if ( '' !== (string) $order->get_meta( '_kramar_order_track_token' ) ) {
			return;
		}
		$token = self::generate_tracking_token( $order_id, $order->get_order_key() );
		$order->update_meta_data( '_kramar_order_track_token', $token );
		$order->save_meta_data();
	}

	/**
	 * If the current request is a valid guest-tracking URL, render the tracking
	 * page and exit. Rejects requests with missing or wrong tokens.
	 *
	 * @since 1.0.0
	 */
	public function maybe_render_tracking_page(): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- intentional public read-only endpoint; token acts as HMAC authorization.
		if ( ! get_query_var( self::TRACK_QUERY_VAR ) && empty( $_GET[ self::TRACK_QUERY_VAR ] ) ) {
			return;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$order_id = absint( $_GET['order_id'] ?? 0 );
		$token    = sanitize_text_field( wp_unslash( $_GET['token'] ?? '' ) );
		// phpcs:enable

		$order = $order_id > 0 ? wc_get_order( $order_id ) : null;

		if (
			! $order instanceof \WC_Order ||
			! self::verify_tracking_token( $order_id, $order->get_order_key(), $token )
		) {
			// Gracefully show "order not found" without leaking why.
			$this->render_tracking_page( null );
			exit;
		}

		$this->render_tracking_page( $order );
		exit;
	}

	/**
	 * Inject a signed "Track your order" link into WooCommerce order emails.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order         The order.
	 * @param bool      $sent_to_admin Whether this email is for the admin.
	 * @param bool      $plain_text    Whether this is a plain-text email.
	 * @param \WC_Email $email         The email object.
	 */
	public function inject_tracking_link_in_email( \WC_Order $order, bool $sent_to_admin, bool $plain_text, \WC_Email $email ): void {
		if ( $sent_to_admin ) {
			return;
		}

		// Only send a tracking link to guest customers.
		if ( $order->get_customer_id() > 0 ) {
			return;
		}

		$url = self::get_tracking_url( $order );

		if ( $plain_text ) {
			/* translators: %s: order tracking URL */
			printf( "\n" . esc_html__( 'Track your order: %s', 'kramar' ) . "\n", esc_url( $url ) );
			return;
		}
		?>
		<p style="margin:16px 0;font-size:14px;">
			<a href="<?php echo esc_url( $url ); ?>" style="color:#7f54b3;">
				<?php esc_html_e( 'Track your order', 'kramar' ); ?>
			</a>
		</p>
		<?php
	}

	/**
	 * Render the guest order tracking page.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order|null $order Verified order, or null if invalid.
	 */
	private function render_tracking_page( ?\WC_Order $order ): void {
		// Raw-link fallback wrapper (used when no editable page exists). The
		// shortcode path renders inside the theme's page template instead.
		add_filter( 'wp_robots', 'wp_robots_no_robots' );
		get_header();
		echo '<div style="max-width:680px;margin:40px auto;padding:0 16px;">';
		echo wp_kses_post( $this->tracking_summary_html( $order ) );
		echo '</div>';
		get_footer();
	}

	/**
	 * Build the order-tracking summary HTML (or an invalid-link notice).
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order|null $order Verified order, or null if invalid.
	 * @return string HTML.
	 */
	private function tracking_summary_html( ?\WC_Order $order ): string {
		ob_start();
		?>
		<div class="kramar-track-order">
			<?php if ( null === $order ) : ?>
				<h2><?php esc_html_e( 'Order not found', 'kramar' ); ?></h2>
				<p><?php esc_html_e( 'The order tracking link you used is invalid or has expired. Please check your email for the correct link.', 'kramar' ); ?></p>
			<?php else : ?>
				<h1>
					<?php
					printf(
						/* translators: %s: order number */
						esc_html__( 'Order #%s', 'kramar' ),
						esc_html( $order->get_order_number() )
					);
					?>
				</h1>

				<p>
					<strong><?php esc_html_e( 'Status:', 'kramar' ); ?></strong>
					<?php echo esc_html( wc_get_order_status_name( $order->get_status() ) ); ?>
				</p>

				<p>
					<strong><?php esc_html_e( 'Date:', 'kramar' ); ?></strong>
					<?php
					$date = $order->get_date_created();
					echo esc_html( $date ? wp_date( get_option( 'date_format' ), $date->getTimestamp() ) : '—' );
					?>
				</p>

				<table style="width:100%;border-collapse:collapse;margin-top:16px;">
					<thead>
						<tr>
							<th style="text-align:left;padding:8px 0;border-bottom:1px solid #e0e0e0;"><?php esc_html_e( 'Product', 'kramar' ); ?></th>
							<th style="text-align:center;padding:8px 0;border-bottom:1px solid #e0e0e0;"><?php esc_html_e( 'Qty', 'kramar' ); ?></th>
							<th style="text-align:right;padding:8px 0;border-bottom:1px solid #e0e0e0;"><?php esc_html_e( 'Total', 'kramar' ); ?></th>
						</tr>
					</thead>
					<tbody>
						<?php foreach ( $order->get_items() as $item ) : ?>
							<tr>
								<td style="padding:8px 0;"><?php echo esc_html( $item->get_name() ); ?></td>
								<td style="text-align:center;padding:8px 0;"><?php echo esc_html( (string) $item->get_quantity() ); ?></td>
								<td style="text-align:right;padding:8px 0;"><?php echo wp_kses_post( wc_price( $item->get_total() ) ); ?></td>
							</tr>
						<?php endforeach; ?>
					</tbody>
					<tfoot>
						<tr>
							<th colspan="2" style="text-align:left;padding:12px 0 4px;border-top:1px solid #e0e0e0;"><?php esc_html_e( 'Total', 'kramar' ); ?></th>
							<td style="text-align:right;padding:12px 0 4px;border-top:1px solid #e0e0e0;"><?php echo wp_kses_post( $order->get_formatted_order_total() ); ?></td>
						</tr>
					</tfoot>
				</table>
			<?php endif; ?>
		</div>
		<?php
		return (string) ob_get_clean();
	}

	/**
	 * Shortcode `[kramar_order_tracking]` — renders inside the editable tracking
	 * page. With a valid signed link it shows the order summary; otherwise it
	 * shows a manual lookup form that emails a one-time link.
	 *
	 * @since 1.0.0
	 *
	 * @return string HTML.
	 */
	public function render_tracking_shortcode(): string {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- token acts as HMAC authorization for the read-only view.
		$order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0;
		$token    = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['token'] ) ) : '';
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		// Signed link from the email → show the order (never via the form).
		if ( $order_id > 0 && '' !== $token ) {
			$order = wc_get_order( $order_id );
			if ( $order instanceof \WC_Order && self::verify_tracking_token( $order_id, $order->get_order_key(), $token ) ) {
				return $this->tracking_summary_html( $order );
			}
			return $this->tracking_summary_html( null );
		}

		// Otherwise → manual lookup form (emails a one-time link, no data shown).
		return $this->render_tracking_form();
	}

	/**
	 * Render (and process) the manual order-lookup form. On a matching
	 * order#+email it emails a one-time tracking link; it never renders order
	 * data and always returns the same generic message (anti-enumeration).
	 *
	 * @since 1.0.0
	 *
	 * @return string HTML.
	 */
	private function render_tracking_form(): string {
		$message = '';

		if ( isset( $_POST['kramar_track_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['kramar_track_nonce'] ) ), 'kramar_track_lookup' ) ) {
			$message = $this->process_tracking_lookup();
		}

		ob_start();
		echo '<div class="kramar-track-form-wrap" style="max-width:480px;">';
		if ( '' !== $message ) {
			echo '<p class="kramar-track-message" style="padding:12px 14px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:4px;">' . esc_html( $message ) . '</p>';
		}
		?>
		<form method="post" class="kramar-track-form">
			<?php wp_nonce_field( 'kramar_track_lookup', 'kramar_track_nonce' ); ?>
			<p>
				<label><?php esc_html_e( 'Order number', 'kramar' ); ?><br>
					<input type="text" name="kramar_order_number" class="input-text form-control" required>
				</label>
			</p>
			<p>
				<label><?php esc_html_e( 'Billing email', 'kramar' ); ?><br>
					<input type="email" name="kramar_track_email" class="input-text form-control" required>
				</label>
			</p>
			<p>
				<button type="submit" class="button btn btn-primary"><?php esc_html_e( 'Email me a tracking link', 'kramar' ); ?></button>
			</p>
			<p class="description" style="font-size:.85em;color:#666;"><?php esc_html_e( 'For your security we email a private link to the billing address on the order, rather than showing details here.', 'kramar' ); ?></p>
		</form>
		<?php
		echo '</div>';
		return (string) ob_get_clean();
	}

	/**
	 * Validate a manual lookup, rate-limit it, and email a one-time tracking
	 * link when the order#+billing-email match. Returns a generic message.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	/**
	 * Build From / Reply-To headers for the guest tracking email.
	 *
	 * This one is sent outside WC_Email, so none of WooCommerce's sender filters
	 * apply. Without headers it goes out as WordPress <wordpress@domain>, which
	 * on a host with SPF/DMARC is the message most likely to be rejected —
	 * and it is one the customer explicitly asked for.
	 *
	 * @since 1.0.0
	 *
	 * @return array<int, string> Mail headers.
	 */
	private function sender_headers(): array {
		$from_name  = '';
		$from_email = '';
		$reply_to   = '';

		foreach ( $this->plugin->get_modules() as $m ) {
			if ( $m instanceof \Kramar\Modules\Email\Email_Manager ) {
				$settings   = $m->get_global_settings();
				$from_name  = (string) ( $settings['from_name'] ?? '' );
				$from_email = (string) ( $settings['from_email'] ?? '' );
				$reply_to   = (string) ( $settings['reply_to'] ?? '' );
				break;
			}
		}

		if ( '' === $from_name ) {
			$from_name = (string) get_bloginfo( 'name' );
		}
		if ( ! is_email( $from_email ) ) {
			$from_email = (string) get_option( 'admin_email' );
		}

		$headers = [ 'From: ' . $from_name . ' <' . $from_email . '>' ];
		if ( is_email( $reply_to ) ) {
			$headers[] = 'Reply-To: ' . $reply_to;
		}

		return $headers;
	}

	private function process_tracking_lookup(): string {
		$generic = __( 'If that order exists, we have emailed a tracking link to the billing address on file.', 'kramar' );

		// Rate-limit per IP (5 attempts / 15 min).
		$ip  = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
		$key = 'kramar_track_rl_' . md5( $ip );
		$cnt = (int) get_transient( $key );
		if ( $cnt >= 5 ) {
			return __( 'Too many attempts. Please try again in a little while.', 'kramar' );
		}
		set_transient( $key, $cnt + 1, 15 * MINUTE_IN_SECONDS );

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Public guest lookup: no session/nonce; rate-limited per IP and only emails a one-time link to the billing address already on the order.
		$number = isset( $_POST['kramar_order_number'] ) ? sanitize_text_field( wp_unslash( $_POST['kramar_order_number'] ) ) : '';
		$email  = isset( $_POST['kramar_track_email'] ) ? sanitize_email( wp_unslash( $_POST['kramar_track_email'] ) ) : '';
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		if ( '' === $number || ! is_email( $email ) ) {
			return $generic;
		}

		$order = $this->find_order_by_number( $number );
		if ( $order instanceof \WC_Order && strtolower( $order->get_billing_email() ) === strtolower( $email ) ) {
			$url = self::get_tracking_url( $order );
			wp_mail(
				$email,
				__( 'Your order tracking link', 'kramar' ),
				sprintf(
					/* translators: 1: order number, 2: tracking URL */
					__( "Here is the secure tracking link for your order #%1\$s:\n\n%2\$s\n\nIf you did not request this, you can ignore this email.", 'kramar' ),
					$order->get_order_number(),
					$url
				),
				$this->sender_headers()
			);
		}

		return $generic;
	}

	/**
	 * Find an order by its raw ID or its formatted (sequential) number.
	 *
	 * @since 1.0.0
	 *
	 * @param string $number Order number as entered.
	 * @return \WC_Order|null
	 */
	private function find_order_by_number( string $number ): ?\WC_Order {
		// Strip a leading "#" the way the withdrawal form's own lookup already
		// does. This module shows the order as "Order #LB01125", WooCommerce
		// writes it that way everywhere, and the reply to a failed lookup is
		// deliberately vague to prevent order-number enumeration — so a customer
		// who typed what they were shown got no email and no way to tell why.
		$number = ltrim( trim( $number ), '#' );

		$order = wc_get_order( absint( $number ) );
		if ( $order instanceof \WC_Order ) {
			return $order;
		}

		$ids = wc_get_orders(
			[
				'limit'      => 1,
				'return'     => 'ids',
				'meta_key'   => '_order_number_formatted', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
				'meta_value' => $number,                   // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
			]
		);
		if ( ! empty( $ids ) ) {
			$order = wc_get_order( $ids[0] );
			return $order instanceof \WC_Order ? $order : null;
		}

		return null;
	}

	/**
	 * Create the editable "Track your order" page on first admin load.
	 *
	 * @since 1.0.0
	 */
	/**
	 * Dedicated home for the tracking page id, outside the shared settings blob.
	 *
	 * @since 1.0.2
	 */
	private const TRACKING_PAGE_OPTION = 'kramar_tracking_page_id';

	/**
	 * Short-lived marker so two concurrent admin requests can't both create the
	 * page. add_option() fails when the row already exists, which makes it an
	 * atomic claim.
	 *
	 * @since 1.0.2
	 */
	private const TRACKING_PAGE_LOCK = 'kramar_tracking_page_lock';

	public function maybe_create_tracking_page(): void {
		if ( ! is_admin() || wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) {
			return;
		}

		$page_id = (int) $this->setting( 'tracking_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_tracking_page( $page_id );
			return;
		}

		// The id used to live only inside the big shared kramar_settings option.
		// Several modules do a read-modify-write on that whole option, so one of
		// them writing a copy it read before this page was created silently reset
		// the id to 0 — and this method runs on EVERY admin page load, so the
		// next one created another page. That produced 18 published "Track your
		// order" pages on one site over two weeks. The id now also lives in its
		// own option, which nothing else rewrites.
		$known = (int) get_option( self::TRACKING_PAGE_OPTION, 0 );
		if ( $known > 0 && self::page_is_usable( (int) $known ) ) {
			$this->remember_tracking_page( $known );
			return;
		}

		// Belt and braces: adopt an existing page rather than making another one.
		$existing = get_posts(
			[
				'post_type'      => 'page',
				'post_status'    => [ 'publish', 'draft', 'pending', 'private' ],
				'posts_per_page' => 1,
				'fields'         => 'ids',
				's'              => '[kramar_order_tracking]',
			]
		);
		if ( ! empty( $existing[0] ) ) {
			$this->remember_tracking_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::TRACKING_PAGE_LOCK, false );
		if ( false !== $lock && ( time() - (int) $lock ) > MINUTE_IN_SECONDS ) {
			delete_option( self::TRACKING_PAGE_LOCK );
			$lock = false;
		}

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

		$page_id = wp_insert_post(
			[
				'post_title'   => __( 'Track your order', 'kramar' ),
				'post_content' => '<!-- wp:shortcode -->[kramar_order_tracking]<!-- /wp:shortcode -->',
				'post_status'  => 'publish',
				'post_type'    => 'page',
			]
		);

		delete_option( self::TRACKING_PAGE_LOCK );

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

	/**
	 * Record the tracking page id in both places it is read from.
	 *
	 * Its own option is authoritative — it is never rewritten wholesale by
	 * another module — while the settings copy keeps the Modules screen and
	 * anything reading the settings tree in step.
	 *
	 * @since 1.0.2
	 */
	private function remember_tracking_page( int $page_id ): void {
		if ( $page_id <= 0 ) {
			return;
		}

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

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

		if ( (int) ( $settings['account']['account-flow']['tracking_page_id'] ?? 0 ) === $page_id ) {
			return;
		}

		$settings['account']['account-flow']['tracking_page_id'] = $page_id;
		update_option( \Kramar\Settings::OPTION_NAME, $settings );
	}

	/**
	 * Keep the order-tracking page out of search indexes (it shows order data).
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, bool> $robots Robots directives.
	 * @return array<string, bool>
	 */
	public function tracking_page_robots( array $robots ): array {
		if ( function_exists( 'get_query_var' ) && '' !== (string) get_query_var( self::TRACK_QUERY_VAR ) ) {
			return wp_robots_no_robots( $robots );
		}

		// Any page carrying the tracking shortcode, not just the one currently
		// configured. Sites that hit the duplicate-page bug before it was fixed
		// still have the extra pages — one had eighteen — and every one of them
		// is a working order-lookup form showing order data. Only the configured
		// id was kept out of search results; the orphans were indexable.
		if ( ! is_page() ) {
			return $robots;
		}

		$post = get_post();
		if ( $post instanceof \WP_Post && has_shortcode( (string) $post->post_content, 'kramar_order_tracking' ) ) {
			return wp_robots_no_robots( $robots );
		}

		return $robots;
	}

	// -------------------------------------------------------------------------
	// Feature 1 — Post-checkout account creation CTA
	// -------------------------------------------------------------------------

	/**
	 * Show an opt-in CTA for account creation on the thank-you page.
	 *
	 * Only shown for guest orders when WC registration is enabled. Account
	 * creation is always an explicit, un-pre-ticked opt-in — never automatic.
	 *
	 * @since 1.0.0
	 *
	 * @param int $order_id Order ID passed by `woocommerce_thankyou`.
	 */
	public function maybe_show_account_cta( int $order_id ): void {
		// Already logged in — no need.
		if ( is_user_logged_in() ) {
			return;
		}

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

		// Only for guest orders.
		if ( $order->get_customer_id() > 0 ) {
			return;
		}

		// WC registration must be enabled. The mismatch is surfaced inline on the
		// module's own settings panel (render_extra_settings) — never broadcast as
		// a global admin notice.
		if ( 'yes' !== get_option( 'woocommerce_enable_myaccount_registration' ) ) {
			return;
		}

		$email = $order->get_billing_email();

		// Email already has an account — don't offer to create another.
		if ( $email && email_exists( $email ) ) {
			return;
		}

		// Opt-in CTA (GDPR: NOT pre-ticked, not auto-submitted).
		$nonce = wp_create_nonce( 'kramar_create_account_' . $order_id );
		?>
		<div class="kramar-account-cta" id="kramar-account-cta" style="margin-top:24px;padding:20px 24px;background:#f8f9fa;border:1px solid #e4e4e7;border-radius:6px;">
			<h3 style="margin:0 0 8px;font-size:16px;"><?php esc_html_e( 'Maak een account aan', 'kramar' ); ?></h3>
			<p style="margin:0 0 16px;color:#50575e;font-size:14px;">
				<?php esc_html_e( 'Sla je bestellinggegevens op voor een snellere afrekening in de toekomst.', 'kramar' ); ?>
			</p>
			<form id="kramar-create-account-form">
				<?php /* GDPR: checkbox is NOT pre-ticked — user must actively opt in. */ ?>
				<label style="display:flex;align-items:flex-start;gap:10px;cursor:pointer;margin-bottom:16px;">
					<input
						type="checkbox"
						id="kramar-account-optin"
						name="kramar_account_optin"
						value="1"
						style="margin-top:3px;flex-shrink:0;"
					/>
					<span style="font-size:14px;color:#1d2327;">
						<?php esc_html_e( 'Ja, ik wil een account aanmaken met het e-mailadres uit mijn bestelling.', 'kramar' ); ?>
					</span>
				</label>
				<div id="kramar-account-cta-result" style="margin-bottom:12px;display:none;font-size:14px;"></div>
				<button
					type="submit"
					id="kramar-account-cta-btn"
					style="display:none;"
					data-order-id="<?php echo esc_attr( (string) $order_id ); ?>"
					data-nonce="<?php echo esc_attr( $nonce ); ?>"
				>
					<?php esc_html_e( 'Account aanmaken', 'kramar' ); ?>
				</button>
			</form>
		</div>
		<?php
	}

	/**
	 * AJAX: create account from opt-in checkbox on thank-you page.
	 *
	 * Accepts: order_id, nonce (kramar_create_account_{order_id}).
	 * Returns JSON success/error.
	 *
	 * @since 1.0.0
	 */
	public function ajax_create_account(): void {
		$order_id = absint( $_POST['order_id'] ?? 0 );
		$nonce    = sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) );

		if ( ! wp_verify_nonce( $nonce, 'kramar_create_account_' . $order_id ) ) {
			wp_send_json_error( [ 'message' => __( 'Security check failed.', 'kramar' ) ] );
		}

		if ( 'yes' !== get_option( 'woocommerce_enable_myaccount_registration' ) ) {
			wp_send_json_error( [ 'message' => __( 'Account registration is currently disabled.', 'kramar' ) ] );
		}

		$order = wc_get_order( $order_id );
		if ( ! $order instanceof \WC_Order || $order->get_customer_id() > 0 ) {
			wp_send_json_error( [ 'message' => __( 'This order already has an account.', 'kramar' ) ] );
		}

		$email = $order->get_billing_email();
		if ( ! $email || email_exists( $email ) ) {
			wp_send_json_error( [ 'message' => __( 'An account with this email already exists.', 'kramar' ) ] );
		}

		$first_name  = $order->get_billing_first_name();
		$last_name   = $order->get_billing_last_name();
		$username    = wc_create_new_customer_username( $email );
		$customer_id = wc_create_new_customer( $email, $username, '', [
			'first_name' => $first_name,
			'last_name'  => $last_name,
		] );

		if ( is_wp_error( $customer_id ) ) {
			wp_send_json_error( [ 'message' => $customer_id->get_error_message() ] );
		}

		$order->set_customer_id( $customer_id );
		$order->save();
		wc_update_new_customer_past_orders( $customer_id );

		// Auto-login if configured.
		if ( $this->setting( 'auto_login' ) ) {
			wc_set_customer_auth_cookie( $customer_id );
		}

		wp_send_json_success( [
			'message' => __( 'Je account is aangemaakt! Controleer je e-mail voor het instellen van je wachtwoord.', 'kramar' ),
		] );
	}

	// -------------------------------------------------------------------------
	// Feature 3 — Auto-login after registration
	// -------------------------------------------------------------------------

	/**
	 * Auto-log in a newly created customer.
	 *
	 * Fires on `woocommerce_created_customer` — works in both Classic and Blocks
	 * checkout because WC calls `wc_create_new_customer()` internally for both.
	 *
	 * @since 1.0.0
	 *
	 * @param int   $customer_id      The new customer ID.
	 * @param array $new_customer_data Customer data array.
	 * @param bool  $password_generated Whether a password was auto-generated.
	 */
	public function auto_login_after_registration( int $customer_id, array $new_customer_data, bool $password_generated ): void {
		// Only log in when a password was auto-generated; if the customer chose
		// their own password they will already be logged in by WC's flow.
		if ( ! $password_generated ) {
			return;
		}

		// Don't double-login if already authenticated.
		if ( is_user_logged_in() ) {
			return;
		}

		wc_set_customer_auth_cookie( $customer_id );
	}

	// -------------------------------------------------------------------------
	// Feature 4 — Login redirect
	// -------------------------------------------------------------------------

	/**
	 * Control where a customer lands after logging in.
	 *
	 * - Admins always go to My Account (prevents accidental wp-admin redirect).
	 * - Normal users: referer, My Account, Shop, or a custom URL.
	 *
	 * @since 1.0.0
	 *
	 * @param string   $redirect Default redirect URL.
	 * @param \WP_User $user     The logged-in user.
	 * @return string Modified redirect URL.
	 */
	public function filter_login_redirect( string $redirect, \WP_User $user ): string {
		// Admins → My Account (not wp-admin, which is handled separately by WP).
		if ( user_can( $user, 'manage_woocommerce' ) ) {
			return wc_get_page_permalink( 'myaccount' );
		}

		$target = (string) $this->setting( 'login_redirect' );

		switch ( $target ) {
			case 'myaccount':
				return wc_get_page_permalink( 'myaccount' );

			case 'shop':
				return wc_get_page_permalink( 'shop' );

			case 'custom':
				$custom = (string) $this->setting( 'login_redirect_url' );
				return $custom ?: wc_get_page_permalink( 'myaccount' );

			case 'referer':
			default:
				$referer = wp_get_referer();
				if ( $referer && ! str_contains( $referer, 'wp-login' ) && ! str_contains( $referer, 'wp-admin' ) ) {
					return $referer;
				}
				return wc_get_page_permalink( 'myaccount' );
		}
	}

	// -------------------------------------------------------------------------
	// Feature 5 — Enhanced My Account dashboard
	// -------------------------------------------------------------------------

	/**
	 * Render the enhanced dashboard section: last orders, reorder, completeness.
	 *
	 * @since 1.0.0
	 */
	public function render_enhanced_dashboard(): void {
		$customer_id = get_current_user_id();
		if ( ! $customer_id ) {
			return;
		}

		$orders = wc_get_orders( [
			'customer_id' => $customer_id,
			'limit'       => 3,
			'orderby'     => 'date',
			'order'       => 'DESC',
			'status'      => array_keys( wc_get_order_statuses() ),
		] );

		// Last active and last order timestamps.
		$last_active = (int) get_user_meta( $customer_id, 'wc_last_active', true );
		$last_order  = ! empty( $orders ) ? $orders[0] : null;

		$reorder_nonce = wp_create_nonce( 'kramar_reorder' );
		?>
		<div class="kramar-account-dashboard">
			<?php // ── Recent orders ──────────────────────────────────────────── ?>
			<?php if ( ! empty( $orders ) ) : ?>
				<div class="kramar-account-section">
					<h3 class="kramar-account-section__title"><?php esc_html_e( 'Recent orders', 'kramar' ); ?></h3>
					<div class="kramar-account-orders">
						<?php foreach ( $orders as $order ) : ?>
							<?php
							/** @var \WC_Order $order */
							$status      = $order->get_status();
							$status_name = wc_get_order_status_name( $status );
							?>
							<div class="kramar-account-order">
								<div class="kramar-account-order__meta">
									<span class="kramar-account-order__number">
										<a href="<?php echo esc_url( $order->get_view_order_url() ); ?>">
											#<?php echo esc_html( $order->get_order_number() ); ?>
										</a>
									</span>
									<span class="kramar-status-badge kramar-status-badge--<?php echo esc_attr( $status ); ?>">
										<?php echo esc_html( $status_name ); ?>
									</span>
									<span class="kramar-account-order__total"><?php echo wp_kses_post( $order->get_formatted_order_total() ); ?></span>
									<?php
									$order_date = $order->get_date_created();
									if ( $order_date ) :
										?>
										<span class="kramar-account-order__date">
											<?php echo esc_html( wp_date( get_option( 'date_format' ), $order_date->getTimestamp() ) ); ?>
										</span>
									<?php endif; ?>
								</div>
								<div class="kramar-account-order__actions">
									<a href="<?php echo esc_url( $order->get_view_order_url() ); ?>" class="kramar-btn kramar-btn--sm">
										<?php esc_html_e( 'View', 'kramar' ); ?>
									</a>
									<button
										type="button"
										class="kramar-btn kramar-btn--sm kramar-reorder-btn"
										data-order-id="<?php echo esc_attr( (string) $order->get_id() ); ?>"
										data-nonce="<?php echo esc_attr( $reorder_nonce ); ?>"
									>
										<?php esc_html_e( 'Reorder', 'kramar' ); ?>
									</button>
								</div>
							</div>
						<?php endforeach; ?>
					</div>
				</div>
			<?php endif; ?>

			<?php // ── Last activity + order ────────────────────────────────── ?>
			<div class="kramar-account-section kramar-account-meta">
				<?php if ( $last_active ) : ?>
					<p class="kramar-account-meta__item">
						<span class="kramar-account-meta__label"><?php esc_html_e( 'Last active:', 'kramar' ); ?></span>
						<span><?php echo esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $last_active ) ); ?></span>
					</p>
				<?php endif; ?>
				<?php if ( $last_order ) : ?>
					<p class="kramar-account-meta__item">
						<span class="kramar-account-meta__label"><?php esc_html_e( 'Last order:', 'kramar' ); ?></span>
						<span>
							<a href="<?php echo esc_url( $last_order->get_view_order_url() ); ?>">
								#<?php echo esc_html( $last_order->get_order_number() ); ?>
							</a>
						</span>
					</p>
				<?php endif; ?>
			</div>

			<?php // ── Account completeness nudges ─────────────────────────── ?>
			<?php $this->render_account_nudges( $customer_id ); ?>
		</div>
		<?php
	}

	/**
	 * Render account-completeness nudges (phone, password).
	 *
	 * @since 1.0.0
	 *
	 * @param int $customer_id WordPress user ID.
	 */
	private function render_account_nudges( int $customer_id ): void {
		$user = get_userdata( $customer_id );
		if ( ! $user ) {
			return;
		}

		$nudges = [];

		if ( empty( get_user_meta( $customer_id, 'billing_phone', true ) ) ) {
			$nudges[] = [
				'text' => __( 'Add a phone number for faster delivery updates.', 'kramar' ),
				'url'  => wc_get_endpoint_url( 'edit-address', 'billing', wc_get_page_permalink( 'myaccount' ) ),
				'link' => __( 'Add phone', 'kramar' ),
			];
		}

		// Suggest password change if the account is freshly auto-generated
		// (WC sets user_pass to a random string but also marks it via user meta).
		if ( get_user_meta( $customer_id, 'default_password_nag', true ) ) {
			$nudges[] = [
				'text' => __( 'You are using a temporary password. Set a personal password now.', 'kramar' ),
				'url'  => wc_get_endpoint_url( 'edit-account', '', wc_get_page_permalink( 'myaccount' ) ),
				'link' => __( 'Set password', 'kramar' ),
			];
		}

		if ( empty( $nudges ) ) {
			return;
		}

		?>
		<div class="kramar-account-section kramar-account-nudges">
			<h3 class="kramar-account-section__title"><?php esc_html_e( 'Complete your account', 'kramar' ); ?></h3>
			<ul class="kramar-nudge-list">
				<?php foreach ( $nudges as $nudge ) : ?>
					<li class="kramar-nudge">
						<span><?php echo esc_html( $nudge['text'] ); ?></span>
						<a href="<?php echo esc_url( $nudge['url'] ); ?>" class="kramar-nudge__link">
							<?php echo esc_html( $nudge['link'] ); ?>
						</a>
					</li>
				<?php endforeach; ?>
			</ul>
		</div>
		<?php
	}

	/**
	 * Add a "Reorder" button to the My Account orders list table.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, array{url: string, name: string}> $actions Existing action links.
	 * @param \WC_Order                                        $order   The order.
	 * @return array<string, array{url: string, name: string}> Modified actions.
	 */
	public function add_reorder_button( array $actions, \WC_Order $order ): array {
		$nonce = wp_create_nonce( 'kramar_reorder' );
		// Use a URL-based reorder for non-JS fallback, but the JS handler intercepts it.
		$actions['kramar-reorder'] = [
			'url'  => add_query_arg(
				[
					'kramar_reorder' => $order->get_id(),
					'nonce'        => $nonce,
				],
				wc_get_page_permalink( 'cart' )
			),
			'name' => __( 'Reorder', 'kramar' ),
		];

		return $actions;
	}

	/**
	 * AJAX: add all items from a past order to the cart.
	 *
	 * @since 1.0.0
	 */
	public function ajax_reorder(): void {
		check_ajax_referer( 'kramar_reorder', 'nonce' );

		if ( ! is_user_logged_in() ) {
			wp_send_json_error( [ 'message' => __( 'Please log in to reorder.', 'kramar' ) ] );
		}

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

		if ( ! $order instanceof \WC_Order ) {
			wp_send_json_error( [ 'message' => __( 'Order not found.', 'kramar' ) ] );
		}

		// Verify the order belongs to the current user.
		if ( (int) $order->get_customer_id() !== get_current_user_id() ) {
			wp_send_json_error( [ 'message' => __( 'You do not have permission to reorder this order.', 'kramar' ) ] );
		}

		$result  = $this->add_order_items_to_cart( $order );
		$added   = $result['added'];
		$skipped = $result['skipped'];

		$cart_url = wc_get_cart_url();

		if ( 0 === $added ) {
			wp_send_json_error( [
				'message' => __( 'None of the products in this order could be added to your cart (they may be out of stock or unavailable).', 'kramar' ),
			] );
		}

		wp_send_json_success( [
			'added'    => $added,
			'skipped'  => $skipped,
			'cart_url' => $cart_url,
			/* translators: %d: number of products added to cart */
			'message'  => sprintf( _n( '%d product added to your cart.', '%d products added to your cart.', $added, 'kramar' ), $added ),
		] );
	}

	/**
	 * Non-JS fallback handler for the My Account orders-list "Reorder" link.
	 *
	 * The orders-list action renders as a plain `<a>` (no data attributes for the
	 * AJAX handler to read), so it points at `?kramar_reorder=ID&nonce=` on the
	 * cart page. This handler processes that request server-side: it adds the
	 * order's items to the cart, then redirects to the cart with a notice.
	 *
	 * @since 1.0.0
	 */
	public function maybe_handle_reorder_url(): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified below.
		if ( empty( $_GET['kramar_reorder'] ) ) {
			return;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$order_id = absint( $_GET['kramar_reorder'] );
		$nonce    = isset( $_GET['nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['nonce'] ) ) : '';
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		$cart_url = wc_get_cart_url();

		if ( ! wp_verify_nonce( $nonce, 'kramar_reorder' ) || ! is_user_logged_in() ) {
			wc_add_notice( __( 'Your reorder link has expired. Please try again from your orders.', 'kramar' ), 'error' );
			wp_safe_redirect( $cart_url );
			exit;
		}

		$order = wc_get_order( $order_id );
		if ( ! $order instanceof \WC_Order || (int) $order->get_customer_id() !== get_current_user_id() ) {
			wc_add_notice( __( 'You do not have permission to reorder this order.', 'kramar' ), 'error' );
			wp_safe_redirect( $cart_url );
			exit;
		}

		$result = $this->add_order_items_to_cart( $order );

		if ( 0 === $result['added'] ) {
			wc_add_notice( __( 'None of the products in this order could be added to your cart (they may be out of stock or unavailable).', 'kramar' ), 'error' );
		} else {
			wc_add_notice(
				sprintf(
					/* translators: %d: number of products added to cart */
					_n( '%d product added to your cart.', '%d products added to your cart.', $result['added'], 'kramar' ),
					$result['added']
				),
				'success'
			);
		}

		wp_safe_redirect( $cart_url );
		exit;
	}

	/**
	 * Add every purchasable item from an order to the current cart.
	 *
	 * Shared by the AJAX reorder (dashboard button) and the non-JS URL fallback
	 * (orders-list link).
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order Order to reorder.
	 * @return array{added:int, skipped:int} Counts of added and skipped items.
	 */
	private function add_order_items_to_cart( \WC_Order $order ): array {
		$added   = 0;
		$skipped = 0;

		foreach ( $order->get_items() as $item ) {
			/** @var \WC_Order_Item_Product $item */
			$product_id   = $item->get_product_id();
			$variation_id = $item->get_variation_id();
			$quantity     = $item->get_quantity();
			$product      = wc_get_product( $variation_id ?: $product_id );

			if ( ! $product || ! $product->is_purchasable() || ! $product->is_in_stock() ) {
				$skipped++;
				continue;
			}

			$cart_result = WC()->cart->add_to_cart( $product_id, $quantity, $variation_id );
			if ( $cart_result ) {
				$added++;
			} else {
				$skipped++;
			}
		}

		return [
			'added'   => $added,
			'skipped' => $skipped,
		];
	}

	// -------------------------------------------------------------------------
	// Feature 6 — Email-first login (Phase 2 / classic only)
	// -------------------------------------------------------------------------

	/**
	 * Inject the email-first field before the classic login form.
	 *
	 * Note: `woocommerce_login_form_start` does NOT fire in WC Blocks checkout.
	 * This feature is therefore classic checkout only. Document this in the UI.
	 *
	 * @since 1.0.0
	 */
	public function render_email_first_field(): void {
		$nonce = wp_create_nonce( self::AJAX_EMAIL_CHECK );
		?>
		<div class="kramar-email-first" id="kramar-email-first">
			<p class="form-row form-row-wide">
				<label for="kramar-login-email">
					<?php esc_html_e( 'Email address', 'kramar' ); ?>
					<span class="required">*</span>
				</label>
				<input
					type="email"
					class="input-text"
					id="kramar-login-email"
					name="kramar_login_email"
					autocomplete="email"
					data-nonce="<?php echo esc_attr( $nonce ); ?>"
				/>
			</p>
			<div id="kramar-email-first-status" style="margin-bottom:12px;font-size:13px;display:none;"></div>
		</div>
		<?php
	}

	/**
	 * AJAX: check whether an email address has an account.
	 *
	 * Response: { has_account: bool }
	 * The JS uses this to decide whether to show the password field (existing
	 * account) or a register prompt (new customer).
	 *
	 * @since 1.0.0
	 */
	public function ajax_email_check(): void {
		check_ajax_referer( self::AJAX_EMAIL_CHECK, 'nonce' );

		// Rate-limit per IP to blunt scripted account enumeration (the public
		// nonce is fixed for uid 0 and is no barrier). Generous enough for any
		// legitimate checkout usage. See L7.
		$ip  = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
		$key = 'kramar_emailchk_rl_' . md5( $ip );
		$cnt = (int) get_transient( $key );
		if ( $cnt >= 30 ) {
			wp_send_json_error( [ 'message' => __( 'Too many attempts. Please try again in a little while.', 'kramar' ) ] );
		}
		set_transient( $key, $cnt + 1, 10 * MINUTE_IN_SECONDS );

		$email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );
		if ( ! is_email( $email ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid email address.', 'kramar' ) ] );
		}

		wp_send_json_success( [ 'has_account' => (bool) email_exists( $email ) ] );
	}

	// -------------------------------------------------------------------------
	// Asset enqueue
	// -------------------------------------------------------------------------

	/**
	 * Enqueue frontend CSS and JS on relevant pages.
	 *
	 * Assets are loaded only on:
	 *   - My Account pages (dashboard + orders list)
	 *   - Order received / thank-you page
	 *   - Guest tracking page
	 *   - Checkout (email-first login form)
	 *
	 * No inline `<script>` or `<style>` tags — all config via wp_localize_script
	 * and wp_add_data.
	 *
	 * @since 1.0.0
	 */
	public function enqueue_frontend_assets(): void {
		$is_account   = is_account_page();
		$is_thankyou  = is_wc_endpoint_url( 'order-received' );
		$is_tracking  = (bool) get_query_var( self::TRACK_QUERY_VAR );
		$is_checkout  = is_checkout();

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$is_tracking  = $is_tracking || ( ! empty( $_GET[ self::TRACK_QUERY_VAR ] ) );

		if ( ! $is_account && ! $is_thankyou && ! $is_tracking && ! $is_checkout ) {
			return;
		}

		wp_enqueue_style(
			'kramar-account-flow',
			KRAMAR_URL . 'assets/css/account-flow.css',
			[],
			KRAMAR_VERSION
		);

		wp_enqueue_script(
			'kramar-account-flow',
			KRAMAR_URL . 'assets/js/account-flow.js',
			[],
			KRAMAR_VERSION,
			true
		);

		wp_localize_script(
			'kramar-account-flow',
			'kramarAccountFlow',
			[
				'ajaxUrl'           => admin_url( 'admin-ajax.php' ),
				'actionCreate'      => self::AJAX_CREATE_ACCOUNT,
				'actionEmailCheck'  => self::AJAX_EMAIL_CHECK,
				'actionReorder'     => self::AJAX_REORDER,
				'emailFirstEnabled' => $this->setting( 'email_first_login' ) ? '1' : '',
				'i18n'              => [
					'creating'    => __( 'Account aanmaken…', 'kramar' ),
					'success'     => __( 'Gelukt!', 'kramar' ),
					'error'       => __( 'Er is iets misgegaan. Probeer het opnieuw.', 'kramar' ),
					'reordering'  => __( 'Adding to cart…', 'kramar' ),
					'checkingEmail' => __( 'Checking…', 'kramar' ),
					'hasAccount'  => __( 'Welcome back! Enter your password below.', 'kramar' ),
					'noAccount'   => __( 'No account found. You can register below.', 'kramar' ),
				],
			]
		);
	}

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

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

		$field = static function ( string $key ) use ( $option_name, $cat, $slug ): string {
			return sprintf( '%s[%s][%s][%s]', $option_name, $cat, $slug, $key );
		};

		$s = fn( string $k ) => $this->setting( $k );

		$redirect         = (string) $s( 'login_redirect' );
		$redirect_url     = (string) $s( 'login_redirect_url' );
		$wc_reg_enabled   = 'yes' === get_option( 'woocommerce_enable_myaccount_registration' );
		?>
		<div class="kramar-module__extra">
			<div class="kramar-settings-stack">

				<?php
				// Preserve the auto-created tracking page id across saves. Without this
				// hidden field sanitize_settings() would reset it to 0 on every save,
				// causing maybe_create_tracking_page() to spawn a fresh duplicate page.
				?>
				<input type="hidden" name="<?php echo esc_attr( $field( 'tracking_page_id' ) ); ?>" value="<?php echo esc_attr( (string) $s( 'tracking_page_id' ) ); ?>" />

				<?php if ( ! $wc_reg_enabled ) : ?>
					<p style="color:#c0392b;font-size:13px;">
						<?php esc_html_e( 'Account creation sub-features are hidden because WooCommerce customer registration is disabled (WooCommerce → Settings → Accounts & Privacy).', 'kramar' ); ?>
					</p>
				<?php endif; ?>

				<?php // ── Account creation ─────────────────────────────────── ?>
				<fieldset style="border:1px solid #e4e4e7;border-radius:4px;padding:12px 16px;margin:0;max-width:600px;">
					<legend style="font-weight:600;padding:0 6px;"><?php esc_html_e( 'Account creation', 'kramar' ); ?></legend>
					<label style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
						<input type="checkbox" name="<?php echo esc_attr( $field( 'guest_account_cta' ) ); ?>" value="1" <?php checked( (bool) $s( 'guest_account_cta' ) ); ?> <?php disabled( ! $wc_reg_enabled ); ?> />
						<?php esc_html_e( 'Show opt-in CTA on thank-you page (GDPR: not pre-ticked)', 'kramar' ); ?>
					</label>
					<label style="display:flex;align-items:center;gap:8px;">
						<input type="checkbox" name="<?php echo esc_attr( $field( 'auto_login' ) ); ?>" value="1" <?php checked( (bool) $s( 'auto_login' ) ); ?> />
						<?php esc_html_e( 'Auto-login after account creation', 'kramar' ); ?>
					</label>
				</fieldset>

				<?php // ── Guest tracking ────────────────────────────────────── ?>
				<fieldset style="border:1px solid #e4e4e7;border-radius:4px;padding:12px 16px;margin:0;max-width:600px;">
					<legend style="font-weight:600;padding:0 6px;"><?php esc_html_e( 'Guest order tracking', 'kramar' ); ?></legend>
					<label style="display:flex;align-items:center;gap:8px;">
						<input type="checkbox" name="<?php echo esc_attr( $field( 'guest_tracking' ) ); ?>" value="1" <?php checked( (bool) $s( 'guest_tracking' ) ); ?> />
						<?php esc_html_e( 'Add signed tracking link to customer emails (no login required)', 'kramar' ); ?>
					</label>
				</fieldset>

				<?php // ── Login redirect ────────────────────────────────────── ?>
				<fieldset style="border:1px solid #e4e4e7;border-radius:4px;padding:12px 16px;margin:0;max-width:600px;">
					<legend style="font-weight:600;padding:0 6px;"><?php esc_html_e( 'After-login redirect', 'kramar' ); ?></legend>
					<p style="margin:0 0 10px;font-size:13px;color:#50575e;">
						<?php esc_html_e( 'Admins always redirect to My Account regardless of this setting.', 'kramar' ); ?>
					</p>
					<div style="display:grid;gap:6px;">
						<?php
						$options = [
							'referer'   => __( 'Previous page (default)', 'kramar' ),
							'myaccount' => __( 'My Account', 'kramar' ),
							'shop'      => __( 'Shop', 'kramar' ),
							'custom'    => __( 'Custom URL', 'kramar' ),
						];
						foreach ( $options as $val => $label ) :
							?>
							<label style="display:flex;align-items:center;gap:8px;">
								<input type="radio" name="<?php echo esc_attr( $field( 'login_redirect' ) ); ?>" value="<?php echo esc_attr( $val ); ?>" <?php checked( $redirect, $val ); ?> />
								<?php echo esc_html( $label ); ?>
							</label>
						<?php endforeach; ?>
					</div>
					<div style="margin-top:10px;" id="kramar-account-flow-custom-url" <?php echo 'custom' !== $redirect ? 'style="display:none;"' : ''; ?>>
						<label for="kramar-redirect-url"><?php esc_html_e( 'Custom URL', 'kramar' ); ?></label><br />
						<input type="url" id="kramar-redirect-url" name="<?php echo esc_attr( $field( 'login_redirect_url' ) ); ?>" value="<?php echo esc_attr( $redirect_url ); ?>" class="regular-text" style="width:100%;" placeholder="https://" />
					</div>
				</fieldset>

				<?php // ── Dashboard + email-first ──────────────────────────── ?>
				<fieldset style="border:1px solid #e4e4e7;border-radius:4px;padding:12px 16px;margin:0;max-width:600px;">
					<legend style="font-weight:600;padding:0 6px;"><?php esc_html_e( 'Additional features', 'kramar' ); ?></legend>
					<label style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
						<input type="checkbox" name="<?php echo esc_attr( $field( 'enhanced_dashboard' ) ); ?>" value="1" <?php checked( (bool) $s( 'enhanced_dashboard' ) ); ?> />
						<?php esc_html_e( 'Enhanced My Account dashboard (last orders, reorder, completeness nudges)', 'kramar' ); ?>
					</label>
					<label style="display:flex;align-items:flex-start;gap:8px;">
						<input type="checkbox" name="<?php echo esc_attr( $field( 'email_first_login' ) ); ?>" value="1" <?php checked( (bool) $s( 'email_first_login' ) ); ?> style="margin-top:3px;" />
						<span>
							<?php esc_html_e( 'Email-first login form (classic checkout only — not compatible with WC Blocks checkout)', 'kramar' ); ?>
						</span>
					</label>
				</fieldset>

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