<?php

declare(strict_types=1);

namespace Australcode\Rewards\Admin;

use Australcode\Rewards\Domain\Wallet\Balance;
use Australcode\Rewards\Domain\Wallet\BalanceDirectory;
use Australcode\Rewards\Domain\Wallet\CustomerBlock;
use Australcode\Rewards\Domain\Wallet\Transaction;
use Australcode\Rewards\Domain\Wallet\TransactionType;
use Australcode\Rewards\Service\Licensing\LicenseGate;
use Australcode\Rewards\Service\WalletService;

defined( 'ABSPATH' ) || exit;

/**
 * Customers management view. Two modes:
 *
 *  - List: every user with a points balance, highest first, paginated (read
 *    model over BalanceDirectory).
 *  - Detail (`?user=<id>`): one customer's balance + lifetime totals, a
 *    paginated ledger of their movements, and a manual points-adjustment form
 *    (admin-post, nonce + capability) that writes through the WalletService so
 *    the balance and ledger stay in sync.
 */
final class CustomersPage {

	use RendersPageHeader;

	public const ADJUST_ACTION = 'acrwd_adjust_points';
	public const NONCE         = 'acrwd_adjust_points';
	public const EXPORT_ACTION = 'acrwd_export_customers';
	public const EXPORT_NONCE  = 'acrwd_export_customers';

	/** Shared notice transient prefix — the Pro tools (CustomerAdminPro) flash here too. */
	public const FLASH_PREFIX = 'acrwd_customers_notice_';

	private const CAPABILITY   = 'manage_woocommerce';
	private const PAGE_SLUG    = 'acrwd-customers';
	private const PER_PAGE     = 20;
	private const EXPORT_CHUNK = 500;

	public function __construct(
		private readonly BalanceDirectory $directory,
		private readonly WalletService $wallet,
		private readonly LicenseGate $gate,
		private readonly CustomerBlock $blocks,
	) {
	}

	public function register(): void {
		add_action( 'admin_post_' . self::ADJUST_ACTION, array( $this, 'handle_adjust' ) );
		add_action( 'admin_post_' . self::EXPORT_ACTION, array( $this, 'handle_export' ) );
	}

	public function render(): void {
		$user_id = $this->requested_user_id();
		if ( $user_id > 0 ) {
			$this->render_detail( $user_id );
			return;
		}
		$this->render_list();
	}

	/*
	 * --------------------------------------------------------------------------
	 * Manual adjustment (admin-post)
	 * --------------------------------------------------------------------------
	 */

	public function handle_adjust(): void {
		$this->guard();
		check_admin_referer( self::NONCE );

		$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
		$points  = isset( $_POST['points'] ) ? (int) $_POST['points'] : 0;
		$reason  = sanitize_text_field( (string) wp_unslash( $_POST['reason'] ?? '' ) );

		if ( $user_id <= 0 || false === get_userdata( $user_id ) ) {
			$this->flash( 'error', __( 'Unknown customer.', 'australcode-points-rewards' ) );
			$this->redirect_to_list();
		}

		if ( 0 === $points ) {
			$this->flash( 'error', __( 'Enter a non-zero amount to adjust.', 'australcode-points-rewards' ) );
			$this->redirect_to_detail( $user_id );
		}

		if ( '' === $reason ) {
			$reason = __( 'Manual adjustment', 'australcode-points-rewards' );
		}

		$this->wallet->credit( $user_id, $points, TransactionType::Adjust, $reason );

		/**
		 * Fires after a single manual points adjustment from the Customers screen.
		 * The Pro "points updated" email listens here; bulk and CSV-import adjustments
		 * deliberately do NOT fire it (no per-customer email on batch ops). No listener
		 * in the free build.
		 *
		 * @param int $user_id Customer id.
		 * @param int $points  Signed points adjusted (positive added, negative removed).
		 */
		do_action( 'acrwd_points_adjusted', $user_id, $points );

		$this->flash(
			'success',
			sprintf(
				/* translators: %s: signed points amount, e.g. +100 or -50. */
				__( 'Adjusted balance by %s points.', 'australcode-points-rewards' ),
				$this->signed( $points )
			)
		);
		$this->redirect_to_detail( $user_id );
	}

	/*
	 * --------------------------------------------------------------------------
	 * Export (admin-post) — FREE. Streams every balance as CSV.
	 * --------------------------------------------------------------------------
	 */

	public function handle_export(): void {
		$this->guard();
		check_admin_referer( self::EXPORT_NONCE );

		$is_pro = $this->gate->is_pro_active();

		nocache_headers();
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename=' . $this->export_filename() );

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- streaming a download to the response body, not writing to the filesystem.
		$out = fopen( 'php://output', 'w' );
		if ( false === $out ) {
			exit;
		}

		// UTF-8 BOM so spreadsheet apps read accented names correctly.
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- writing to the open output stream.
		fwrite( $out, "\xEF\xBB\xBF" );

		$headers = array(
			__( 'User ID', 'australcode-points-rewards' ),
			__( 'Name', 'australcode-points-rewards' ),
			__( 'Email', 'australcode-points-rewards' ),
			__( 'Balance', 'australcode-points-rewards' ),
			__( 'Lifetime earned', 'australcode-points-rewards' ),
			__( 'Lifetime redeemed', 'australcode-points-rewards' ),
		);
		if ( $is_pro ) {
			$headers[] = __( 'Tier', 'australcode-points-rewards' );
			$headers[] = __( 'Blocked', 'australcode-points-rewards' );
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv -- writing CSV to the open output stream.
		fputcsv( $out, $headers );

		$offset = 0;
		do {
			$rows    = $this->directory->paginate( self::EXPORT_CHUNK, $offset );
			$count   = count( $rows );
			$blocked = $is_pro
				? array_flip( $this->blocks->blocked_among( $this->row_user_ids( $rows ) ) )
				: array();

			foreach ( $rows as $balance ) {
				$user = get_userdata( $balance->user_id );
				$line = array(
					(string) $balance->user_id,
					false !== $user ? $user->display_name : '',
					false !== $user ? $user->user_email : '',
					(string) $balance->points_balance,
					(string) $balance->lifetime_earned,
					(string) $balance->lifetime_redeemed,
				);
				if ( $is_pro ) {
					$line[] = $this->tier_label( $balance->tier_id );
					$line[] = isset( $blocked[ $balance->user_id ] ) ? '1' : '0';
				}
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv -- writing CSV to the open output stream.
				fputcsv( $out, $line );
			}

			$offset += self::EXPORT_CHUNK;
		} while ( self::EXPORT_CHUNK === $count );

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- closing the output stream.
		fclose( $out );
		exit;
	}

	private function export_filename(): string {
		return 'australcode-rewards-customers-' . gmdate( 'Y-m-d' ) . '.csv';
	}

	/**
	 * Human label for a customer's tier id, resolved by the Pro tier service via
	 * the `acrwd_tier_label` filter. Empty string in the free build (no listener)
	 * or when the customer has no tier.
	 */
	private function tier_label( string $tier_id ): string {
		if ( '' === $tier_id ) {
			return '';
		}
		/** @var string $label */
		$label = apply_filters( 'acrwd_tier_label', '', $tier_id );
		return $label;
	}

	/**
	 * @param Balance[] $rows
	 * @return int[]
	 */
	private function row_user_ids( array $rows ): array {
		return array_map( static fn ( Balance $balance ): int => $balance->user_id, $rows );
	}

	/*
	 * --------------------------------------------------------------------------
	 * Detail view
	 * --------------------------------------------------------------------------
	 */

	private function render_detail( int $user_id ): void {
		$user    = get_userdata( $user_id );
		$balance = $this->wallet->get_balance( $user_id );

		echo '<div class="wrap acds-root acrwd-page"><div class="acds-wrap">';

		$name = ( false !== $user && '' !== $user->display_name )
			? $user->display_name
			/* translators: %d: WordPress user id. */
			: sprintf( __( 'User #%d', 'australcode-points-rewards' ), $user_id );

		$this->render_page_header(
			$name,
			false !== $user ? $user->user_email : '',
			array( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) => __( '← Back to customers', 'australcode-points-rewards' ) )
		);
		$this->render_flash();

		$this->render_balance_card( $balance );
		$this->render_adjust_form( $user_id );
		$this->render_ledger( $user_id );

		echo '</div></div>';
	}

	private function render_balance_card( Balance $balance ): void {
		echo '<div class="acrwd-kpi-grid">';
		$this->kpi( __( 'Balance', 'australcode-points-rewards' ), $balance->points_balance );
		$this->kpi( __( 'Lifetime earned', 'australcode-points-rewards' ), $balance->lifetime_earned, $this->lifetime_tooltip() );
		$this->kpi( __( 'Lifetime redeemed', 'australcode-points-rewards' ), $balance->lifetime_redeemed, $this->lifetime_tooltip() );
		echo '</div>';
	}

	private function kpi( string $label, int $value, ?string $tooltip = null ): void {
		echo '<div class="acds-kpi"><span class="acds-kpi__value">'
			. esc_html( number_format_i18n( $value ) )
			. '</span><span class="acds-kpi__label">' . esc_html( $label );
		if ( null !== $tooltip ) {
			printf(
				' <span class="acrwd-kpi-info dashicons dashicons-info-outline" tabindex="0" role="img" title="%1$s" aria-label="%1$s"></span>',
				esc_attr( $tooltip )
			);
		}
		echo '</span></div>';
	}

	/**
	 * Why this customer's lifetime totals do not reconcile with their balance:
	 * gross running totals (never reduced by refunds) seeded from any migrated
	 * program. Mirrors AdminMenu::lifetime_tooltip().
	 */
	private function lifetime_tooltip(): string {
		return __( 'Gross running total — never reduced by refunds, and includes points migrated from a previous loyalty program. It will exceed the live balance.', 'australcode-points-rewards' );
	}

	private function render_adjust_form( int $user_id ): void {
		echo '<div class="acds-surface"><div class="acds-surface__head"><h2 class="acds-surface__title">'
			. esc_html__( 'Adjust points', 'australcode-points-rewards' ) . '</h2></div><div class="acds-surface__body">';
		echo '<p class="acds-muted">' . esc_html__( 'Add or remove points manually. Use a negative amount to deduct. The change is recorded in the ledger.', 'australcode-points-rewards' ) . '</p>';

		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
		wp_nonce_field( self::NONCE );
		echo '<input type="hidden" name="action" value="' . esc_attr( self::ADJUST_ACTION ) . '">';
		echo '<input type="hidden" name="user_id" value="' . esc_attr( (string) $user_id ) . '">';

		echo '<p><label for="acrwd-points">' . esc_html__( 'Points (use a negative number to deduct)', 'australcode-points-rewards' ) . '</label><br>';
		echo '<input type="number" step="1" id="acrwd-points" name="points" class="small-text" value="0" required></p>';

		echo '<p><label for="acrwd-reason">' . esc_html__( 'Reason (optional)', 'australcode-points-rewards' ) . '</label><br>';
		echo '<input type="text" id="acrwd-reason" name="reason" class="regular-text" maxlength="180" placeholder="'
			. esc_attr__( 'e.g. Goodwill gesture', 'australcode-points-rewards' ) . '"></p>';

		echo '<p><button type="submit" class="button button-primary">' . esc_html__( 'Apply adjustment', 'australcode-points-rewards' ) . '</button></p>';
		echo '</form></div></div>';
	}

	private function render_ledger( int $user_id ): void {
		$total = $this->wallet->count_transactions( $user_id );

		echo '<div class="acds-surface"><div class="acds-surface__head"><h2 class="acds-surface__title">'
			. esc_html__( 'History', 'australcode-points-rewards' ) . '</h2></div>';

		if ( 0 === $total ) {
			echo '<div class="acds-surface__body"><p class="acds-muted">' . esc_html__( 'No points activity yet.', 'australcode-points-rewards' ) . '</p></div></div>';
			return;
		}

		$pages   = (int) max( 1, (int) ceil( $total / self::PER_PAGE ) );
		$paged   = min( $this->current_page(), $pages );
		$offset  = ( $paged - 1 ) * self::PER_PAGE;
		$entries = $this->wallet->get_transactions( $user_id, self::PER_PAGE, $offset );

		echo '<table class="wp-list-table widefat striped acrwd-table-cards"><thead><tr>';
		echo '<th>' . esc_html__( 'Date', 'australcode-points-rewards' ) . '</th>';
		echo '<th>' . esc_html__( 'Type', 'australcode-points-rewards' ) . '</th>';
		echo '<th>' . esc_html__( 'Points', 'australcode-points-rewards' ) . '</th>';
		echo '<th>' . esc_html__( 'Balance', 'australcode-points-rewards' ) . '</th>';
		echo '<th>' . esc_html__( 'Reason', 'australcode-points-rewards' ) . '</th>';
		echo '</tr></thead><tbody>';

		foreach ( $entries as $entry ) {
			$this->render_ledger_row( $entry );
		}

		echo '</tbody></table>';
		$this->render_pagination( $user_id, $paged, $pages );
		echo '</div>';
	}

	private function render_ledger_row( Transaction $entry ): void {
		$when = null !== $entry->created_at
			? (string) mysql2date( (string) get_option( 'date_format', 'Y-m-d' ), $entry->created_at )
			: '—';

		$reason = '' !== $entry->reason
			? $entry->reason
			: ( null !== $entry->order_id
				/* translators: %d: order number. */
				? sprintf( __( 'Order #%d', 'australcode-points-rewards' ), $entry->order_id )
				: '—' );

		echo '<tr>';
		echo '<td data-label="' . esc_attr__( 'Date', 'australcode-points-rewards' ) . '">' . esc_html( $when ) . '</td>';
		echo '<td data-label="' . esc_attr__( 'Type', 'australcode-points-rewards' ) . '">' . esc_html( self::type_label( $entry->type ) ) . '</td>';
		echo '<td class="acrwd-cell-key" data-label="' . esc_attr__( 'Points', 'australcode-points-rewards' ) . '">' . esc_html( $this->signed( $entry->points_delta ) ) . '</td>';
		echo '<td data-label="' . esc_attr__( 'Balance', 'australcode-points-rewards' ) . '">' . esc_html( number_format_i18n( $entry->balance_after ) ) . '</td>';
		echo '<td data-label="' . esc_attr__( 'Reason', 'australcode-points-rewards' ) . '">' . esc_html( $reason ) . '</td>';
		echo '</tr>';
	}

	/*
	 * --------------------------------------------------------------------------
	 * List view
	 * --------------------------------------------------------------------------
	 */

	private function render_list(): void {
		$search  = $this->search_term();
		$has_any = $this->directory->count() > 0;

		echo '<div class="wrap acds-root acrwd-page"><div class="acds-wrap">';
		$this->render_page_header(
			__( 'Customers', 'australcode-points-rewards' ),
			__( 'Customers with a points balance and their accumulated activity.', 'australcode-points-rewards' ),
			$this->dashboard_back_action()
		);
		$this->render_flash();

		if ( ! $has_any && '' === $search ) {
			echo '<div class="acds-surface"><div class="acds-empty">';
			echo '<p class="acds-empty__title">' . esc_html__( 'No customers have earned points yet', 'australcode-points-rewards' ) . '</p>';
			echo '<p class="acds-empty__body">' . esc_html__( 'Once customers complete an order, their points balance appears here.', 'australcode-points-rewards' ) . '</p>';
			echo '<div class="acds-empty__cta"><a class="acds-btn acds-btn--primary" href="' . esc_url( admin_url( 'admin.php?page=acrwd-settings' ) ) . '">'
				. esc_html__( 'Review earning settings', 'australcode-points-rewards' ) . '</a></div>';
			echo '</div></div></div></div>';
			return;
		}

		$this->render_search_form( $search );

		$total = '' !== $search ? $this->directory->count_search( $search ) : $this->directory->count();

		if ( 0 === $total ) {
			echo '<div class="acds-surface"><div class="acds-empty">';
			echo '<p class="acds-empty__title">' . esc_html__( 'No customers match your search', 'australcode-points-rewards' ) . '</p>';
			echo '<p class="acds-empty__body">' . esc_html__( 'Try a different name, email or user ID.', 'australcode-points-rewards' ) . '</p>';
			echo '</div></div></div></div>';
			return;
		}

		$pages  = (int) max( 1, (int) ceil( $total / self::PER_PAGE ) );
		$paged  = min( $this->current_page(), $pages );
		$offset = ( $paged - 1 ) * self::PER_PAGE;
		$rows   = '' !== $search
			? $this->directory->search( $search, self::PER_PAGE, $offset )
			: $this->directory->paginate( self::PER_PAGE, $offset );

		echo '<p class="acds-muted">' . esc_html(
			'' !== $search
				? sprintf(
					/* translators: %s: number of matching customers. */
					_n( '%s customer matches your search.', '%s customers match your search.', $total, 'australcode-points-rewards' ),
					number_format_i18n( $total )
				)
				: sprintf(
					/* translators: %s: number of customers. */
					_n( '%s customer with points.', '%s customers with points.', $total, 'australcode-points-rewards' ),
					number_format_i18n( $total )
				)
		) . '</p>';

		$is_pro      = $this->gate->is_pro_active();
		$blocked_ids = $is_pro
			? array_flip( $this->blocks->blocked_among( $this->row_user_ids( $rows ) ) )
			: array();

		$this->render_export_button();

		// Pro tools panel (CSV import) above the table. Rendered by CustomerAdminPro;
		// no listener in the free build.
		if ( $is_pro ) {
			do_action( 'acrwd_customers_tools' );
		}

		echo '<div class="acds-surface">';

		// The Pro bulk tools wrap the table in a POST form so per-row checkboxes and
		// the toolbar's action submit together. No listener in the free build, so the
		// form/toolbar never render and the list stays a plain read-only table.
		if ( $is_pro ) {
			echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="acrwd-customers-bulk-form">';
			do_action( 'acrwd_customers_bulk_toolbar' );
		}

		echo '<table class="wp-list-table widefat fixed striped acrwd-table-cards"><thead><tr>';
		if ( $is_pro ) {
			do_action( 'acrwd_customers_head_check' );
		}
		echo '<th>' . esc_html__( 'Customer', 'australcode-points-rewards' ) . '</th>';
		echo '<th>' . esc_html__( 'Email', 'australcode-points-rewards' ) . '</th>';
		echo '<th>' . esc_html__( 'Balance', 'australcode-points-rewards' ) . '</th>';
		echo '<th>' . esc_html__( 'Lifetime earned', 'australcode-points-rewards' ) . '</th>';
		echo '<th>' . esc_html__( 'Lifetime redeemed', 'australcode-points-rewards' ) . '</th>';
		if ( $is_pro ) {
			echo '<th>' . esc_html__( 'Tier', 'australcode-points-rewards' ) . '</th>';
			do_action( 'acrwd_customers_head_actions' );
		}
		echo '</tr></thead><tbody>';

		foreach ( $rows as $balance ) {
			$this->render_row( $balance, $is_pro, isset( $blocked_ids[ $balance->user_id ] ) );
		}

		echo '</tbody></table>';
		$this->render_list_pagination( $paged, $pages );
		if ( $is_pro ) {
			echo '</form>';
		}
		echo '</div></div></div>';
	}

	/**
	 * Export-to-CSV button (FREE). A tiny POST form so the download carries a nonce.
	 */
	private function render_export_button(): void {
		echo '<div class="acrwd-list-actions">';
		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="acrwd-export-form">';
		echo '<input type="hidden" name="action" value="' . esc_attr( self::EXPORT_ACTION ) . '" />';
		wp_nonce_field( self::EXPORT_NONCE );
		echo '<button type="submit" class="acds-btn acds-btn--secondary acds-btn--sm">'
			. esc_html__( 'Export CSV', 'australcode-points-rewards' ) . '</button>';
		echo '</form></div>';
	}

	private function render_row( Balance $balance, bool $is_pro, bool $is_blocked ): void {
		$user = get_userdata( $balance->user_id );

		$name = ( false !== $user && '' !== $user->display_name )
			? $user->display_name
			/* translators: %d: WordPress user id. */
			: sprintf( __( 'User #%d', 'australcode-points-rewards' ), $balance->user_id );
		$email = false !== $user ? $user->user_email : '—';

		// data-label feeds the mobile card-collapse (≤600px): each cell shows its
		// column name via CSS ::before, with Balance promoted as the key figure.
		echo '<tr>';
		if ( $is_pro ) {
			do_action( 'acrwd_customers_row_check', $balance->user_id );
		}
		echo '<td data-label="' . esc_attr__( 'Customer', 'australcode-points-rewards' ) . '"><a href="' . esc_url( $this->detail_url( $balance->user_id ) ) . '">' . esc_html( $name ) . '</a>';
		if ( $is_pro && $is_blocked ) {
			echo ' <span class="acds-badge acds-badge--error">' . esc_html__( 'Blocked', 'australcode-points-rewards' ) . '</span>';
		}
		echo '</td>';
		echo '<td data-label="' . esc_attr__( 'Email', 'australcode-points-rewards' ) . '">' . esc_html( $email ) . '</td>';
		echo '<td class="acrwd-cell-key" data-label="' . esc_attr__( 'Balance', 'australcode-points-rewards' ) . '">' . esc_html( number_format_i18n( $balance->points_balance ) ) . '</td>';
		echo '<td data-label="' . esc_attr__( 'Lifetime earned', 'australcode-points-rewards' ) . '">' . esc_html( number_format_i18n( $balance->lifetime_earned ) ) . '</td>';
		echo '<td data-label="' . esc_attr__( 'Lifetime redeemed', 'australcode-points-rewards' ) . '">' . esc_html( number_format_i18n( $balance->lifetime_redeemed ) ) . '</td>';
		if ( $is_pro ) {
			$tier = $this->tier_label( $balance->tier_id );
			echo '<td data-label="' . esc_attr__( 'Tier', 'australcode-points-rewards' ) . '">' . esc_html( '' !== $tier ? $tier : '—' ) . '</td>';
			do_action( 'acrwd_customers_row_actions', $balance->user_id, $is_blocked );
		}
		echo '</tr>';
	}

	private function render_search_form( string $search ): void {
		echo '<form method="get" class="acrwd-search-form">';
		echo '<input type="hidden" name="page" value="' . esc_attr( self::PAGE_SLUG ) . '" />';
		echo '<p class="search-box">';
		echo '<label class="screen-reader-text" for="acrwd-customer-search">'
			. esc_html__( 'Search customers', 'australcode-points-rewards' ) . '</label>';
		echo '<input type="search" id="acrwd-customer-search" name="s" value="' . esc_attr( $search )
			. '" placeholder="' . esc_attr__( 'Name, email or user ID', 'australcode-points-rewards' ) . '" />';
		echo ' <button type="submit" class="button">' . esc_html__( 'Search customers', 'australcode-points-rewards' ) . '</button>';
		if ( '' !== $search ) {
			echo ' <a class="button-link" href="' . esc_url( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) ) . '">'
				. esc_html__( 'Clear', 'australcode-points-rewards' ) . '</a>';
		}
		echo '</p></form>';
	}

	private function search_term(): string {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only search param, no state change.
		return isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['s'] ) ) : '';
	}

	/*
	 * --------------------------------------------------------------------------
	 * Shared helpers
	 * --------------------------------------------------------------------------
	 */

	/**
	 * Human label for a ledger entry type.
	 */
	public static function type_label( TransactionType $type ): string {
		return match ( $type ) {
			TransactionType::Earn   => __( 'Earned', 'australcode-points-rewards' ),
			TransactionType::Redeem => __( 'Redeemed', 'australcode-points-rewards' ),
			TransactionType::Expire => __( 'Expired', 'australcode-points-rewards' ),
			TransactionType::Adjust => __( 'Adjustment', 'australcode-points-rewards' ),
			TransactionType::Refund => __( 'Refund', 'australcode-points-rewards' ),
		};
	}

	private function signed( int $points ): string {
		$formatted = number_format_i18n( abs( $points ) );
		return ( $points < 0 ? '−' : '+' ) . $formatted;
	}

	private function render_list_pagination( int $paged, int $pages ): void {
		$this->render_pagination_links( $paged, $pages, fn ( int $p ): string => $this->list_page_url( $p ) );
	}

	private function render_pagination( int $user_id, int $paged, int $pages ): void {
		$this->render_pagination_links(
			$paged,
			$pages,
			fn ( int $p ): string => $this->detail_url( $user_id, $p )
		);
	}

	/**
	 * @param callable(int):string $url_for
	 */
	private function render_pagination_links( int $paged, int $pages, callable $url_for ): void {
		if ( $pages <= 1 ) {
			return;
		}

		echo '<div class="acds-pagination">';
		echo '<span class="acds-pagination__pages">' . sprintf(
			/* translators: 1: current page number, 2: total pages. */
			esc_html__( 'Page %1$s of %2$s', 'australcode-points-rewards' ),
			esc_html( number_format_i18n( $paged ) ),
			esc_html( number_format_i18n( $pages ) )
		) . '</span>';
		echo '<span class="acds-button-row">';
		if ( $paged > 1 ) {
			echo '<a class="acds-btn acds-btn--secondary acds-btn--sm" href="' . esc_url( $url_for( $paged - 1 ) ) . '">'
				. esc_html__( '← Previous', 'australcode-points-rewards' ) . '</a>';
		}
		if ( $paged < $pages ) {
			echo '<a class="acds-btn acds-btn--secondary acds-btn--sm" href="' . esc_url( $url_for( $paged + 1 ) ) . '">'
				. esc_html__( 'Next →', 'australcode-points-rewards' ) . '</a>';
		}
		echo '</span></div>';
	}

	private function list_page_url( int $paged ): string {
		$args   = array(
			'page'  => self::PAGE_SLUG,
			'paged' => $paged,
		);
		$search = $this->search_term();
		if ( '' !== $search ) {
			$args['s'] = $search;
		}
		return add_query_arg( $args, admin_url( 'admin.php' ) );
	}

	private function detail_url( int $user_id, int $paged = 0 ): string {
		$args = array(
			'page' => self::PAGE_SLUG,
			'user' => $user_id,
		);
		if ( $paged > 0 ) {
			$args['paged'] = $paged;
		}
		return add_query_arg( $args, admin_url( 'admin.php' ) );
	}

	private function requested_user_id(): int {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only navigation param, no state change.
		return isset( $_GET['user'] ) ? max( 0, (int) $_GET['user'] ) : 0;
	}

	private function current_page(): int {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only pagination param, no state change.
		$paged = isset( $_GET['paged'] ) ? (int) $_GET['paged'] : 1;
		return max( 1, $paged );
	}

	private function guard(): void {
		if ( ! current_user_can( self::CAPABILITY ) ) {
			wp_die( esc_html__( 'You are not allowed to manage Australcode Rewards customers.', 'australcode-points-rewards' ) );
		}
	}

	private function flash( string $type, string $message ): void {
		self::set_flash( $type, $message );
	}

	/**
	 * Sets a one-shot admin notice shown on the next Customers page render. Public +
	 * static so the Pro tools (CustomerAdminPro), which redirect back to this page,
	 * can surface their own results through the same notice channel.
	 */
	public static function set_flash( string $type, string $message ): void {
		set_transient(
			self::FLASH_PREFIX . get_current_user_id(),
			array(
				'type'    => $type,
				'message' => $message,
			),
			MINUTE_IN_SECONDS
		);
	}

	private function render_flash(): void {
		$transient_key = self::FLASH_PREFIX . get_current_user_id();
		$flash         = get_transient( $transient_key );
		if ( ! is_array( $flash ) ) {
			return;
		}
		delete_transient( $transient_key );

		$class = 'success' === ( $flash['type'] ?? '' ) ? 'notice-success' : 'notice-error';
		echo '<div class="notice ' . esc_attr( $class ) . ' is-dismissible"><p>'
			. esc_html( (string) ( $flash['message'] ?? '' ) )
			. '</p></div>';
	}

	private function redirect_to_detail( int $user_id ): void {
		wp_safe_redirect( $this->detail_url( $user_id ) );
		exit;
	}

	private function redirect_to_list(): void {
		wp_safe_redirect( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) );
		exit;
	}
}
