<?php
/**
 * PDF receipt generation via dompdf.
 *
 * @since   1.0.0
 * @package DonationRouter
 */

declare( strict_types=1 );
namespace DonationRouter;

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

use DonationRouter\Template_Loader;
use Dompdf\Dompdf;
use Dompdf\Options;

/**
 * Generates a PDF donation receipt for a given payment, using the legal entity
 * details of the PayPal account that processed it.
 *
 * Requires dompdf (`composer require dompdf/dompdf`). The constructor throws
 * \RuntimeException when dompdf is not available so callers can gate the
 * feature cleanly.
 *
 * @since 1.0.0
 */
class Receipt_PDF_Generator {

	use Template_Loader;

	/**
	 * Download nonce action prefix.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public const NONCE_ACTION_PREFIX = 'ador_pdf_receipt_';

	/**
	 * WordPress AJAX action name.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public const AJAX_ACTION = 'ador_download_pdf_receipt';

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

	/**
	 * Initialise and verify dompdf is available.
	 *
	 * @since 1.0.0
	 *
	 * @param Account_Repository_Interface $account_repo  Account data source.
	 *
	 * @throws \RuntimeException  When dompdf is not installed.
	 */
	public function __construct( Account_Repository_Interface $account_repo ) {
		if ( ! class_exists( Dompdf::class ) ) {
			throw new \RuntimeException( 'dompdf is not installed. Run: composer require dompdf/dompdf' );
		}
		$this->account_repo = $account_repo;
	}

	/**
	 * Generate a PDF receipt and return the raw bytes.
	 *
	 * @since 1.0.0
	 *
	 * @param int $payment_id  GiveWP donation post ID.
	 *
	 * @return string  Raw PDF bytes.
	 *
	 * @throws \InvalidArgumentException  When the payment does not exist or is not routed.
	 */
	public function generate( int $payment_id ): string {
		$account_id = (int) get_post_meta(
			$payment_id,
			PayPal_Credential_Router::META_PROCESSING_ACCOUNT_ID,
			true
		);

		if ( $account_id <= 0 ) {
			throw new \InvalidArgumentException(
				'Payment ' . $payment_id . ' was not routed through this plugin.' // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			);
		}

		$account        = $this->account_repo->find_by_id( $account_id );
		$receipt_number = (string) get_post_meta( $payment_id, Receipt_Customizer::META_RECEIPT_NUMBER, true );
		$donation       = get_post( $payment_id );

		if ( ! $donation instanceof \WP_Post ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- exception message, not echoed to browser
			throw new \InvalidArgumentException( 'Payment post ' . $payment_id . ' not found.' );
		}

		$html = $this->build_html( $payment_id, $account, $receipt_number, $donation );

		$options = new Options();
		$options->set( 'isHtml5ParserEnabled', true );
		$options->set( 'isRemoteEnabled', false );

		$dompdf = new Dompdf( $options );
		$dompdf->loadHtml( $html );
		$dompdf->setPaper( 'A4', 'portrait' );
		$dompdf->render();

		return (string) $dompdf->output();
	}

	/**
	 * The donation's donor email.
	 *
	 * GiveWP renamed this meta key: 4.x writes `_give_payment_donor_email`,
	 * while `_give_payment_user_email` is the legacy key from earlier
	 * versions. Reading only the legacy key returns an empty string on every
	 * modern donation — which silently disabled PDF receipts for everyone
	 * except administrators, because Receipt_PDF_Handler::can_access() treats
	 * an empty donor email as "cannot authorise".
	 *
	 * Both keys are read, current first, so donations recorded by an older
	 * GiveWP still resolve.
	 *
	 * @since 1.2.0
	 *
	 * @param int $payment_id GiveWP donation post ID.
	 *
	 * @return string The donor's email, or '' when the donation has none.
	 */
	public static function donor_email( int $payment_id ): string {
		$email = (string) get_post_meta( $payment_id, '_give_payment_donor_email', true );

		if ( '' === $email ) {
			$email = (string) get_post_meta( $payment_id, '_give_payment_user_email', true );
		}

		return $email;
	}

	/**
	 * Return a signed URL that, when visited, streams the PDF for this payment.
	 *
	 * The URL is valid for the lifetime of the nonce (~12 hours).
	 *
	 * Static because it needs no instance state — only the donation id and the
	 * donor's email. Templates call it directly, and constructing the generator
	 * there would drag in the account repository and its dompdf availability
	 * check, which can throw.
	 *
	 * @since 1.0.0
	 *
	 * @param int $payment_id  GiveWP donation post ID.
	 *
	 * @return string  Absolute URL.
	 */
	public static function get_download_url( int $payment_id ): string {
		$args = array(
			'action'     => self::AJAX_ACTION,
			'payment_id' => $payment_id,
			'nonce'      => wp_create_nonce( self::NONCE_ACTION_PREFIX . $payment_id ),
		);

		/*
		 * The donor's email is part of the link, not decoration.
		 *
		 * Receipt_PDF_Handler::can_access() authorises three ways: an
		 * administrator, a logged-in user whose account email matches the
		 * donation's, or a request supplying the donation's email. A guest
		 * donor — which is most donors — is none of the first two, so a link
		 * without `email` fails authorisation and returns 403. Until this was
		 * added, the Download PDF Receipt button on the success page did not
		 * work for anonymous donors at all.
		 *
		 * This does not weaken the check: the address is already known to
		 * whoever is being shown the receipt, and the nonce still scopes the
		 * link to this one donation.
		 */
		$donor_email = self::donor_email( $payment_id );

		if ( '' !== $donor_email ) {
			$args['email'] = $donor_email;
		}

		return add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
	}

	// ── HTML template ─────────────────────────────────────────────────────────

	/**
	 * Build the HTML string that dompdf will render to PDF.
	 *
	 * @since 1.0.0
	 *
	 * @param int            $payment_id      GiveWP donation post ID.
	 * @param PayPal_Account $account         Receiving PayPal account.
	 * @param string         $receipt_number  Pre-formatted receipt number or empty string.
	 * @param \WP_Post       $donation        Donation post object.
	 *
	 * @return string  Complete HTML document.
	 */
	private function build_html(
		int $payment_id,
		PayPal_Account $account,
		string $receipt_number,
		\WP_Post $donation
	): string {
		$donor_name    = $this->get_donor_name( $payment_id );
		$donor_email   = (string) get_post_meta( $payment_id, '_give_payment_user_email', true );
		$amount        = (string) get_post_meta( $payment_id, '_give_payment_total', true );
		$currency      = (string) get_post_meta( $payment_id, '_give_payment_currency', true );
		$form_title    = (string) get_post_meta( $payment_id, '_give_payment_form_title', true );
		$donation_date = (string) get_the_date( 'F j, Y', $donation );

		$tax_statement  = $this->build_tax_statement( $account );
		$amount_display = $currency ? $currency . ' ' . number_format( (float) $amount, 2 ) : number_format( (float) $amount, 2 );

		ob_start();
		$this->render_template(
			'receipts/receipt-pdf.php',
			compact( 'account', 'receipt_number', 'donor_name', 'donor_email', 'amount_display', 'form_title', 'donation_date', 'tax_statement' )
		);
		return (string) ob_get_clean();
	}

	/**
	 * Retrieve the donor's full name from payment meta.
	 *
	 * @since 1.0.0
	 *
	 * @param int $payment_id  Donation post ID.
	 *
	 * @return string
	 */
	private function get_donor_name( int $payment_id ): string {
		$first = (string) get_post_meta( $payment_id, '_give_donor_billing_first_name', true );
		$last  = (string) get_post_meta( $payment_id, '_give_donor_billing_last_name', true );
		$full  = trim( $first . ' ' . $last );
		return '' !== $full ? $full : (string) get_post_meta( $payment_id, '_give_payment_user_email', true );
	}

	/**
	 * Build the tax-deductibility statement for the account's country.
	 *
	 * Checks for a custom option override first, then falls back to the
	 * country-specific template built into Receipt_Customizer.
	 *
	 * @since 1.0.0
	 *
	 * @param PayPal_Account $account  Receiving account.
	 *
	 * @return string  Safe HTML string (may be empty).
	 */
	private function build_tax_statement( PayPal_Account $account ): string {
		$custom = (string) get_option( Receipt_Customizer::OPTION_TEMPLATE_PREFIX . $account->id, '' );
		if ( '' !== $custom ) {
			return wp_kses_post( $custom );
		}

		// Instantiate a temporary customizer just for the static default.
		$tmp = new Receipt_Customizer( $this->account_repo, new Receipt_Numbering() );
		return $tmp->default_tax_statement( $account );
	}
}
