<?php
/**
 * AJAX handler that streams a PDF receipt to the donor's browser.
 *
 * @since   1.0.0
 * @package DonationRouter
 */

declare( strict_types=1 );
namespace DonationRouter;

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

/**
 * Registers WordPress AJAX hooks so donors (logged-in or not) can download
 * a PDF copy of their donation receipt.
 *
 * Access control:
 *   - Nonce must be valid for the specific payment ID.
 *   - The requester's email must match the donor email stored on the donation,
 *     OR the requester must be an administrator.
 *
 * @since 1.0.0
 */
class Receipt_PDF_Handler {

	/**
	 * PDF generator dependency.
	 *
	 * @since 1.0.0
	 *
	 * @var Receipt_PDF_Generator
	 */
	private Receipt_PDF_Generator $generator;

	/**
	 * Initialise with a configured generator.
	 *
	 * @since 1.0.0
	 *
	 * @param Receipt_PDF_Generator $generator  Pre-configured PDF generator.
	 */
	public function __construct( Receipt_PDF_Generator $generator ) {
		$this->generator = $generator;
	}

	/**
	 * Register AJAX hooks for authenticated and anonymous donors.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function register_hooks(): void {
		add_action( 'wp_ajax_' . Receipt_PDF_Generator::AJAX_ACTION, array( $this, 'handle' ) );
		add_action( 'wp_ajax_nopriv_' . Receipt_PDF_Generator::AJAX_ACTION, array( $this, 'handle' ) );
	}

	/**
	 * Handle the AJAX download request.
	 *
	 * Verifies the nonce and access rights, generates the PDF, and streams it
	 * as an attachment download. Terminates the request on success or failure.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function handle(): void {
		$payment_id = absint( $_GET['payment_id'] ?? 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$nonce      = sanitize_key( (string) ( $_GET['nonce'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		if ( $payment_id <= 0 ) {
			wp_die( esc_html__( 'Invalid payment ID.', 'alamin-donation-router' ), 400 );
		}

		if ( ! wp_verify_nonce( $nonce, Receipt_PDF_Generator::NONCE_ACTION_PREFIX . $payment_id ) ) {
			wp_die( esc_html__( 'Invalid or expired link. Please return to the receipt page and try again.', 'alamin-donation-router' ), 403 );
		}

		if ( ! $this->can_access( $payment_id ) ) {
			wp_die( esc_html__( 'You do not have permission to download this receipt.', 'alamin-donation-router' ), 403 );
		}

		try {
			$pdf      = $this->generator->generate( $payment_id );
			$filename = 'receipt-' . $payment_id . '.pdf';

			header( 'Content-Type: application/pdf' );
			header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
			header( 'Content-Length: ' . strlen( $pdf ) );
			header( 'Cache-Control: private, no-cache' );
			header( 'Pragma: no-cache' );

			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- raw binary PDF
			echo $pdf;
			$this->terminate();
			return;
		} catch ( \Throwable $e ) {
			wp_die( esc_html__( 'Receipt PDF could not be generated.', 'alamin-donation-router' ), 500 );
		}
	}

	// ── Access control ────────────────────────────────────────────────────────

	/**
	 * End the request after the PDF has been streamed.
	 *
	 * Extracted so the authorised path can be tested. A bare `exit` here ends
	 * the PHP process, which under PHPUnit kills the runner mid-suite — so
	 * before this seam existed the success path could not be covered at all,
	 * and every test in ReceiptPDFHandlerAjaxTest asserted a *rejection*. A
	 * handler that refused everyone therefore passed the entire suite, which is
	 * exactly what shipped: can_access() read a GiveWP meta key that 4.x no
	 * longer writes, so PDF receipts failed for every non-administrator.
	 *
	 * Tests override this to return instead of exiting.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	protected function terminate(): void {
		exit;
	}

	/**
	 * Determine whether the current request is authorised to download the receipt.
	 *
	 * Admins are always allowed. Anonymous or authenticated donors must supply
	 * an email matching the donation record via the `email` query parameter.
	 *
	 * @since 1.0.0
	 *
	 * @param int $payment_id  Donation post ID.
	 *
	 * @return bool
	 */
	private function can_access( int $payment_id ): bool {
		if ( current_user_can( 'manage_options' ) ) {
			return true;
		}

		$donor_email = strtolower( Receipt_PDF_Generator::donor_email( $payment_id ) );
		if ( '' === $donor_email ) {
			return false;
		}

		// Allow logged-in donors whose account email matches.
		if ( is_user_logged_in() ) {
			$current = strtolower( (string) wp_get_current_user()->user_email );
			if ( $current === $donor_email ) {
				return true;
			}
		}

		// Allow anonymous donors who supply the correct email in the URL.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- nonce verified above; email addresses contain no slashable characters
		$supplied = strtolower( sanitize_email( (string) ( $_GET['email'] ?? '' ) ) );
		return '' !== $supplied && $supplied === $donor_email;
	}
}
