<?php
/**
 * PayPal partner onboarding flow and account admin-post action handler.
 *
 * @since   1.0.0
 * @package DonationRouter
 */

declare( strict_types=1 );
namespace DonationRouter;

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

/**
 * Handles the PayPal partner onboarding OAuth flow and all admin-post
 * actions related to account management (save, deactivate, bulk-deactivate).
 *
 * State tokens are stored as WordPress transients with a 10-minute TTL and
 * are single-use — they are deleted immediately after successful verification.
 * Credentials, OAuth tokens, and webhook secrets are never logged.
 *
 * @since 1.0.0
 */
class Onboarding_Handler {

	/**
	 * Admin-post action: save an existing account's metadata.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public const ACTION_SAVE = 'ador_save_account';

	/**
	 * Admin-post action: deactivate a single account.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public const ACTION_DEACTIVATE = 'ador_deactivate_account';

	/**
	 * Admin-post action: activate a single account.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public const ACTION_ACTIVATE = 'ador_activate_account';

	/**
	 * Admin-post action: permanently delete a single account.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public const ACTION_DELETE = 'ador_delete_account';

	/**
	 * Admin-post action: bulk-deactivate multiple accounts.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public const ACTION_BULK_DEACTIVATE = 'ador_bulk_deactivate_accounts';

	/**
	 * Admin-post action: import GiveWP's already-connected PayPal account.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public const ACTION_IMPORT = 'ador_import_givewp_account';

	/**
	 * Admin-post action: re-sync an imported account's credentials from GiveWP.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public const ACTION_RESYNC = 'ador_resync_givewp_account';

	/**
	 * Admin-post action: register a PayPal webhook for an account.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public const ACTION_REGISTER_WEBHOOK = 'ador_register_webhook';

	/**
	 * Account data source.
	 *
	 * @since 1.0.0
	 *
	 * @var Account_Repository_Interface
	 */
	private Account_Repository_Interface $account_repo;

	/**
	 * Read-only view of GiveWP's connected PayPal account (import source).
	 *
	 * @since 1.1.0
	 *
	 * @var GiveWP_Merchant_Reader
	 */
	private GiveWP_Merchant_Reader $merchant_reader;

	/**
	 * Per-account PayPal webhook registrar.
	 *
	 * @since 1.1.0
	 *
	 * @var Webhook_Registrar
	 */
	private Webhook_Registrar $webhook_registrar;

	/**
	 * Initialise the handler with the account repository.
	 *
	 * @since 1.0.0
	 *
	 * @param Account_Repository_Interface $account_repo      Account data source.
	 * @param GiveWP_Merchant_Reader|null  $merchant_reader   Import source; defaults to a real reader.
	 * @param Webhook_Registrar|null       $webhook_registrar Webhook registrar; defaults to a real one.
	 */
	public function __construct(
		Account_Repository_Interface $account_repo,
		?GiveWP_Merchant_Reader $merchant_reader = null,
		?Webhook_Registrar $webhook_registrar = null
	) {
		$this->account_repo      = $account_repo;
		$this->merchant_reader   = $merchant_reader ?? new GiveWP_Merchant_Reader();
		$this->webhook_registrar = $webhook_registrar ?? new Webhook_Registrar( $account_repo );
	}

	/**
	 * Register WordPress admin-post hooks for all handled actions.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function register_hooks(): void {
		add_action( 'admin_post_' . self::ACTION_SAVE, array( $this, 'handle_save_account' ) );
		add_action( 'admin_post_' . self::ACTION_DEACTIVATE, array( $this, 'handle_deactivate_account' ) );
		add_action( 'admin_post_' . self::ACTION_ACTIVATE, array( $this, 'handle_activate_account' ) );
		add_action( 'admin_post_' . self::ACTION_DELETE, array( $this, 'handle_delete_account' ) );
		add_action( 'admin_post_' . self::ACTION_BULK_DEACTIVATE, array( $this, 'handle_bulk_deactivate' ) );
		add_action( 'admin_post_' . self::ACTION_IMPORT, array( $this, 'handle_import_givewp_account' ) );
		add_action( 'admin_post_' . self::ACTION_RESYNC, array( $this, 'handle_resync_account' ) );
		add_action( 'admin_post_' . self::ACTION_REGISTER_WEBHOOK, array( $this, 'handle_register_webhook' ) );
	}

	/**
	 * Adopt GiveWP's already-connected PayPal account into the plugin's managed
	 * accounts, reusing the credentials GiveWP already stores (no second OAuth).
	 *
	 * The operator supplies the legal-entity fields GiveWP does not hold; the
	 * merchant ID, country, and credentials are read from GiveWP's connected
	 * MerchantDetail. Idempotent: a merchant already in ador_accounts is not
	 * re-imported. Ends in wp_safe_redirect()+exit() on every path.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function handle_import_givewp_account(): void {
		if ( ! current_user_can( Admin_Settings::CAPABILITY ) ) {
			wp_die( esc_html__( 'You do not have permission to perform this action.', 'alamin-donation-router' ) );
		}

		check_admin_referer( self::ACTION_IMPORT, 'ador_nonce' );

		if ( ! $this->merchant_reader->is_connected() ) {
			$this->set_notice( 'error', __( 'No connected GiveWP PayPal account was found to import.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_add_account_url() );
			exit;
		}

		$merchant_id = $this->merchant_reader->merchant_id();
		if ( '' === $merchant_id ) {
			$this->set_notice( 'error', __( 'GiveWP did not report a merchant ID for the connected account.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_add_account_url() );
			exit;
		}

		$mode = $this->merchant_reader->current_mode();

		// Idempotent per mode: the same merchant may be imported once for live
		// and once for sandbox, but never twice for the same environment.
		$already_managed = true;
		try {
			$this->account_repo->find_by_merchant_id_and_mode( $merchant_id, $mode );
		} catch ( Account_Not_Found_Exception $e ) {
			$already_managed = false;
		}

		if ( $already_managed ) {
			$this->set_notice(
				'error',
				sprintf(
					/* translators: %s: PayPal environment (live/sandbox). */
					__( 'This PayPal account is already managed by the plugin for %s mode.', 'alamin-donation-router' ),
					$mode
				)
			);
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
		$fields = $this->sanitize_account_fields( $_POST );

		if ( '' === $fields['account_label'] ) {
			$fields['account_label'] = __( 'PayPal (imported)', 'alamin-donation-router' );
		}

		// Prefer GiveWP's reported country; fall back to the submitted value.
		$country = $this->merchant_reader->account_country();
		if ( '' !== $country ) {
			$fields['account_country'] = $country;
		}

		$validation_error = $this->validate_account_fields( $fields );
		if ( null !== $validation_error ) {
			$this->set_notice( 'error', $validation_error );
			wp_safe_redirect( $this->get_add_account_url() );
			exit;
		}

		$credentials = $this->merchant_reader->to_credentials();
		if ( empty( $credentials['clientId'] ) || empty( $credentials['clientSecret'] ) ) {
			$this->set_notice( 'error', __( 'The connected GiveWP account has no stored credentials to import.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_add_account_url() );
			exit;
		}

		try {
			$now     = new \DateTimeImmutable();
			$account = new PayPal_Account(
				id: null,
				account_label: (string) $fields['account_label'],
				merchant_id: $merchant_id,
				account_country: (string) $fields['account_country'],
				legal_entity_name: (string) ( $fields['legal_entity_name'] ?? '' ),
				legal_entity_tax_id: (string) ( $fields['legal_entity_tax_id'] ?? '' ),
				legal_entity_address: (string) ( $fields['legal_entity_address'] ?? '' ),
				credentials: $credentials,
				webhook_id: null,
				is_active: true,
				created_at: $now,
				updated_at: $now,
				mode: $mode,
			);

			$this->account_repo->save( $account );
			$this->set_notice( 'success', __( 'Your connected GiveWP PayPal account was imported.', 'alamin-donation-router' ) );
		} catch ( \InvalidArgumentException | Persistence_Exception $e ) {
			$this->set_notice( 'error', $e->getMessage() );
		}

		wp_safe_redirect( $this->get_accounts_list_url() );
		exit;
	}

	/**
	 * Refresh an imported account's credentials from GiveWP's current connection.
	 *
	 * Only overwrites credentials — label, country, and legal-entity fields are
	 * preserved. Refuses to run unless GiveWP is connected to the SAME merchant
	 * and is in the SAME mode as the stored account, so credentials are never
	 * crossed between merchants or environments. Ends in wp_safe_redirect()+exit().
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function handle_resync_account(): void {
		if ( ! current_user_can( Admin_Settings::CAPABILITY ) ) {
			wp_die( esc_html__( 'You do not have permission to perform this action.', 'alamin-donation-router' ) );
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$account_id = isset( $_GET['account_id'] ) ? absint( $_GET['account_id'] ) : 0;

		check_admin_referer( self::ACTION_RESYNC . '_' . $account_id );

		if ( $account_id <= 0 ) {
			$this->set_notice( 'error', __( 'Invalid account ID.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		try {
			$existing = $this->account_repo->find_by_id( $account_id );
		} catch ( Account_Not_Found_Exception $e ) {
			$this->set_notice( 'error', __( 'Account not found.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		if ( ! $this->merchant_reader->is_connected() ) {
			$this->set_notice( 'error', __( 'GiveWP has no connected PayPal account to re-sync from.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		if ( $this->merchant_reader->merchant_id() !== $existing->merchant_id ) {
			$this->set_notice( 'error', __( 'The account connected in GiveWP is a different merchant than this record.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		if ( $this->merchant_reader->current_mode() !== $existing->mode ) {
			$this->set_notice(
				'error',
				sprintf(
					/* translators: %s: PayPal environment (live/sandbox). */
					__( 'Switch GiveWP to %s mode to re-sync this account.', 'alamin-donation-router' ),
					$existing->mode
				)
			);
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		$credentials = $this->merchant_reader->to_credentials();
		if ( empty( $credentials['clientId'] ) || empty( $credentials['clientSecret'] ) ) {
			$this->set_notice( 'error', __( 'The connected GiveWP account has no stored credentials to re-sync.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		try {
			$updated = new PayPal_Account(
				id: $existing->id,
				account_label: $existing->account_label,
				merchant_id: $existing->merchant_id,
				account_country: $existing->account_country,
				legal_entity_name: $existing->legal_entity_name,
				legal_entity_tax_id: $existing->legal_entity_tax_id,
				legal_entity_address: $existing->legal_entity_address,
				credentials: $credentials,
				webhook_id: $existing->webhook_id,
				is_active: $existing->is_active,
				created_at: $existing->created_at,
				updated_at: new \DateTimeImmutable(),
				mode: $existing->mode,
			);

			$this->account_repo->save( $updated );
			$this->set_notice( 'success', __( 'Credentials re-synced from GiveWP.', 'alamin-donation-router' ) );
		} catch ( \InvalidArgumentException | Persistence_Exception $e ) {
			$this->set_notice( 'error', $e->getMessage() );
		}

		wp_safe_redirect( $this->get_accounts_list_url() );
		exit;
	}

	/**
	 * Register a PayPal webhook for an account, pointing PayPal at the plugin's
	 * per-account endpoint, and store the returned webhook ID.
	 *
	 * Requires the account to already hold PayPal credentials (i.e. an imported
	 * account). Ends in wp_safe_redirect()+exit() on every path.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function handle_register_webhook(): void {
		if ( ! current_user_can( Admin_Settings::CAPABILITY ) ) {
			wp_die( esc_html__( 'You do not have permission to perform this action.', 'alamin-donation-router' ) );
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$account_id = isset( $_GET['account_id'] ) ? absint( $_GET['account_id'] ) : 0;

		check_admin_referer( self::ACTION_REGISTER_WEBHOOK . '_' . $account_id );

		try {
			$account = $this->account_repo->find_by_id( $account_id );
		} catch ( Account_Not_Found_Exception $e ) {
			$this->set_notice( 'error', __( 'Account not found.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		try {
			$webhook_id = $this->webhook_registrar->register( $account );
			$this->set_notice(
				'success',
				sprintf(
					/* translators: %s: PayPal webhook ID. */
					__( 'PayPal webhook registered (ID %s).', 'alamin-donation-router' ),
					$webhook_id
				)
			);
		} catch ( \Throwable $e ) {
			$this->set_notice( 'error', $e->getMessage() );
		}

		wp_safe_redirect( $this->get_accounts_list_url() );
		exit;
	}

	// -------------------------------------------------------------------------
	// Action handlers
	// -------------------------------------------------------------------------

	/**
	 * Save updated metadata for an existing account.
	 *
	 * Does not modify the PayPal credentials — only label, country, and
	 * legal entity fields are updated.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function handle_save_account(): void {
		if ( ! current_user_can( Admin_Settings::CAPABILITY ) ) {
			wp_die( esc_html__( 'You do not have permission to perform this action.', 'alamin-donation-router' ) );
		}

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$account_id = isset( $_POST['account_id'] ) ? absint( $_POST['account_id'] ) : 0;

		check_admin_referer( 'ador_save_account_' . $account_id, 'ador_nonce' );

		if ( $account_id <= 0 ) {
			$this->set_notice( 'error', __( 'Invalid account ID.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		try {
			$existing = $this->account_repo->find_by_id( $account_id );
		} catch ( Account_Not_Found_Exception $e ) {
			$this->set_notice( 'error', __( 'Account not found.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
		$fields = $this->sanitize_account_fields( $_POST );

		$validation_error = $this->validate_account_fields( $fields );
		if ( null !== $validation_error ) {
			$this->set_notice( 'error', $validation_error );
			wp_safe_redirect( $this->get_edit_account_url( $account_id ) );
			exit;
		}

		try {
			$updated = new PayPal_Account(
				id: $existing->id,
				account_label: (string) ( $fields['account_label'] ?? '' ),
				merchant_id: $existing->merchant_id,
				account_country: (string) ( $fields['account_country'] ?? 'US' ),
				legal_entity_name: (string) ( $fields['legal_entity_name'] ?? '' ),
				legal_entity_tax_id: (string) ( $fields['legal_entity_tax_id'] ?? '' ),
				legal_entity_address: (string) ( $fields['legal_entity_address'] ?? '' ),
				credentials: $existing->get_credentials(),
				webhook_id: $existing->webhook_id,
				is_active: $existing->is_active,
				created_at: $existing->created_at,
				updated_at: new \DateTimeImmutable(),
				mode: $existing->mode,
			);

			$this->account_repo->save( $updated );

			// Save optional receipt template (sanitised with wp_kses_post).
			$this->save_receipt_template( $account_id );

			$this->set_notice( 'success', __( 'Account updated successfully.', 'alamin-donation-router' ) );
		} catch ( \InvalidArgumentException | Persistence_Exception $e ) {
			$this->set_notice( 'error', $e->getMessage() );
		}

		wp_safe_redirect( $this->get_accounts_list_url() );
		exit;
	}

	/**
	 * Deactivate a single account by ID.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function handle_deactivate_account(): void {
		if ( ! current_user_can( Admin_Settings::CAPABILITY ) ) {
			wp_die( esc_html__( 'You do not have permission to perform this action.', 'alamin-donation-router' ) );
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$account_id = isset( $_GET['account_id'] ) ? absint( $_GET['account_id'] ) : 0;

		check_admin_referer( 'ador_deactivate_' . $account_id );

		if ( $account_id <= 0 ) {
			$this->set_notice( 'error', __( 'Invalid account ID.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		$result = $this->account_repo->deactivate( $account_id );

		if ( $result ) {
			$this->set_notice( 'success', __( 'Account deactivated successfully.', 'alamin-donation-router' ) );
		} else {
			$this->set_notice( 'error', __( 'Account could not be deactivated. It may already be inactive.', 'alamin-donation-router' ) );
		}

		wp_safe_redirect( $this->get_accounts_list_url() );
		exit;
	}

	/**
	 * Activate a single account (the inverse of handle_deactivate_account).
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function handle_activate_account(): void {
		if ( ! current_user_can( Admin_Settings::CAPABILITY ) ) {
			wp_die( esc_html__( 'You do not have permission to perform this action.', 'alamin-donation-router' ) );
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$account_id = isset( $_GET['account_id'] ) ? absint( $_GET['account_id'] ) : 0;

		check_admin_referer( 'ador_activate_' . $account_id );

		if ( $account_id <= 0 ) {
			$this->set_notice( 'error', __( 'Invalid account ID.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		$result = $this->account_repo->activate( $account_id );

		if ( $result ) {
			$this->set_notice( 'success', __( 'Account activated successfully.', 'alamin-donation-router' ) );
		} else {
			$this->set_notice( 'error', __( 'Account could not be activated. It may already be active.', 'alamin-donation-router' ) );
		}

		wp_safe_redirect( $this->get_accounts_list_url() );
		exit;
	}

	/**
	 * Permanently delete a single account, with best-effort webhook cleanup.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function handle_delete_account(): void {
		if ( ! current_user_can( Admin_Settings::CAPABILITY ) ) {
			wp_die( esc_html__( 'You do not have permission to perform this action.', 'alamin-donation-router' ) );
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$account_id = isset( $_GET['account_id'] ) ? absint( $_GET['account_id'] ) : 0;

		check_admin_referer( 'ador_delete_' . $account_id );

		if ( $account_id <= 0 ) {
			$this->set_notice( 'error', __( 'Invalid account ID.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		// Best-effort: remove the account's PayPal webhook before deleting the row
		// so PayPal stops delivering to an endpoint we can no longer authenticate.
		// A failure here must not block the deletion.
		try {
			$account = $this->account_repo->find_by_id( $account_id );
			if ( null !== $account->webhook_id && '' !== $account->webhook_id ) {
				$this->webhook_registrar->deregister( $account );
			}
		} catch ( \Throwable $e ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			error_log( '[ADOR] Webhook deregistration during account delete failed: ' . $e->getMessage() );
		}

		$result = $this->account_repo->delete( $account_id );

		if ( $result ) {
			$this->set_notice( 'success', __( 'Account deleted.', 'alamin-donation-router' ) );
		} else {
			$this->set_notice( 'error', __( 'Account could not be deleted. It may have already been removed.', 'alamin-donation-router' ) );
		}

		wp_safe_redirect( $this->get_accounts_list_url() );
		exit;
	}

	/**
	 * Bulk-deactivate multiple accounts.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function handle_bulk_deactivate(): void {
		if ( ! current_user_can( Admin_Settings::CAPABILITY ) ) {
			wp_die( esc_html__( 'You do not have permission to perform this action.', 'alamin-donation-router' ) );
		}

		check_admin_referer( 'ador_bulk_deactivate', 'ador_bulk_nonce' );

		$raw_ids     = isset( $_POST['account_ids'] ) && is_array( $_POST['account_ids'] )
			? array_map( 'sanitize_text_field', wp_unslash( $_POST['account_ids'] ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			: array();
		$account_ids = array_map( 'absint', $raw_ids );
		$account_ids = array_filter( $account_ids );

		if ( empty( $account_ids ) ) {
			$this->set_notice( 'error', __( 'No accounts selected.', 'alamin-donation-router' ) );
			wp_safe_redirect( $this->get_accounts_list_url() );
			exit;
		}

		$count = 0;
		foreach ( $account_ids as $id ) {
			if ( $this->account_repo->deactivate( $id ) ) {
				++$count;
			}
		}

		$this->set_notice(
			'success',
			sprintf(
				/* translators: %d: number of accounts deactivated */
				_n( '%d account deactivated.', '%d accounts deactivated.', $count, 'alamin-donation-router' ),
				$count
			)
		);

		wp_safe_redirect( $this->get_accounts_list_url() );
		exit;
	}

	/**
	 * Persist the receipt template for an account if submitted.
	 *
	 * Reads `ador_receipt_template` from the current POST and stores it in
	 * wp_options, sanitised with wp_kses_post.  Deletes the option when the
	 * field is left blank (falls back to country default at render time).
	 *
	 * Must be called after nonce verification has already been performed.
	 *
	 * @since 1.0.0
	 *
	 * @param int $account_id  Account whose template to update.
	 *
	 * @return void
	 */
	private function save_receipt_template( int $account_id ): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified by caller; kses is the sanitiser
		$sanitized  = wp_kses_post( wp_unslash( (string) ( $_POST['ador_receipt_template'] ?? '' ) ) );
		$option_key = Receipt_Customizer::OPTION_TEMPLATE_PREFIX . $account_id;

		if ( '' === $sanitized ) {
			delete_option( $option_key );
		} else {
			update_option( $option_key, $sanitized, false );
		}
	}

	/**
	 * Sanitize raw POST values for the account fields.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string,mixed> $post  Raw POST data (pre-unslashed values).
	 *
	 * @return array<string,string>  Sanitized field map.
	 */
	private function sanitize_account_fields( array $post ): array {
		return array(
			'account_label'        => sanitize_text_field( wp_unslash( (string) ( $post['account_label'] ?? '' ) ) ),
			'account_country'      => strtoupper( sanitize_text_field( wp_unslash( (string) ( $post['account_country'] ?? '' ) ) ) ),
			'legal_entity_name'    => sanitize_text_field( wp_unslash( (string) ( $post['legal_entity_name'] ?? '' ) ) ),
			'legal_entity_tax_id'  => sanitize_text_field( wp_unslash( (string) ( $post['legal_entity_tax_id'] ?? '' ) ) ),
			'legal_entity_address' => sanitize_textarea_field( wp_unslash( (string) ( $post['legal_entity_address'] ?? '' ) ) ),
		);
	}

	/**
	 * Validate sanitized account fields and return a human-readable error string
	 * when validation fails, or null when all fields are valid.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string,mixed> $fields  Sanitized field values.
	 *
	 * @return string|null  Error message, or null when valid.
	 */
	private function validate_account_fields( array $fields ): ?string {
		if ( '' === ( $fields['account_label'] ?? '' ) ) {
			return __( 'Account Label is required.', 'alamin-donation-router' );
		}

		if ( strlen( $fields['account_label'] ?? '' ) > 191 ) {
			return __( 'Account Label must not exceed 191 characters.', 'alamin-donation-router' );
		}

		$country = $fields['account_country'] ?? '';
		if ( 1 !== preg_match( '/^[A-Z]{2}$/', $country ) ) {
			return __( 'Please select a valid account country.', 'alamin-donation-router' );
		}

		if ( ! array_key_exists( $country, Account_Form::get_paypal_countries() ) ) {
			return __( 'The selected country is not supported by PayPal.', 'alamin-donation-router' );
		}

		if ( '' === ( $fields['legal_entity_name'] ?? '' ) ) {
			return __( 'Legal Entity Name is required.', 'alamin-donation-router' );
		}

		if ( '' === ( $fields['legal_entity_tax_id'] ?? '' ) ) {
			return __( 'Tax ID / EIN is required.', 'alamin-donation-router' );
		}

		if ( '' === ( $fields['legal_entity_address'] ?? '' ) ) {
			return __( 'Legal Entity Address is required.', 'alamin-donation-router' );
		}

		return null;
	}

	/**
	 * Store an admin notice in a per-user transient for display on next page load.
	 *
	 * @since 1.0.0
	 *
	 * @param string $type     Notice type: success, error, warning, or info.
	 * @param string $message  Notice text (plain text, will be escaped on output).
	 *
	 * @return void
	 */
	private function set_notice( string $type, string $message ): void {
		set_transient(
			'ador_admin_notice_' . get_current_user_id(),
			array(
				'type'    => $type,
				'message' => $message,
			),
			60
		);
	}

	/**
	 * Return the admin URL for the accounts list view.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	private function get_accounts_list_url(): string {
		return add_query_arg(
			array(
				'post_type' => 'give_forms',
				'page'      => Admin_Settings::MENU_SLUG,
				'tab'       => Admin_Settings::TAB_ACCOUNTS,
			),
			admin_url( 'edit.php' )
		);
	}

	/**
	 * Return the admin URL for the add-account form view.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	private function get_add_account_url(): string {
		return add_query_arg(
			array(
				'post_type' => 'give_forms',
				'page'      => Admin_Settings::MENU_SLUG,
				'tab'       => Admin_Settings::TAB_ACCOUNTS,
				'view'      => 'add',
			),
			admin_url( 'edit.php' )
		);
	}

	/**
	 * Return the admin URL for the edit-account form view.
	 *
	 * @since 1.0.0
	 *
	 * @param int $account_id  Account database row ID.
	 *
	 * @return string
	 */
	private function get_edit_account_url( int $account_id ): string {
		return add_query_arg(
			array(
				'post_type'  => 'give_forms',
				'page'       => Admin_Settings::MENU_SLUG,
				'tab'        => Admin_Settings::TAB_ACCOUNTS,
				'view'       => 'edit',
				'account_id' => $account_id,
			),
			admin_url( 'edit.php' )
		);
	}
}
