<?php
/**
 * Stripe webhook handler — REST API endpoint.
 *
 * @package Donatico\Stripe
 */

declare(strict_types=1);

namespace Donatico\Stripe;

use Donatico\Database\DonationRepository;
use Donatico\Models\Donation;

/**
 * Registers /wp-json/donatico/v1/webhook and processes inbound Stripe events.
 *
 * Stripe signature verification is the only authentication required here;
 * the permission_callback is intentionally __return_true.
 *
 * Supported events:
 *   checkout.session.completed     → STATUS_COMPLETED
 *   checkout.session.expired       → STATUS_FAILED
 *   payment_intent.payment_failed  → STATUS_FAILED
 *   charge.refunded                → STATUS_REFUNDED
 */
class WebhookHandler {

	/** REST API namespace. */
	const NAMESPACE = 'donatico/v1';

	/** REST API route. */
	const ROUTE = '/webhook';

	/** @var DonationRepository */
	private DonationRepository $repository;

	/**
	 * @param DonationRepository $repository
	 */
	public function __construct( DonationRepository $repository ) {
		$this->repository = $repository;
	}

	/**
	 * Register the REST route.
	 * Hooked to rest_api_init.
	 *
	 * @return void
	 */
	public function registerRoute(): void {
		register_rest_route(
			self::NAMESPACE,
			self::ROUTE,
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'handleWebhook' ),
				'permission_callback' => '__return_true', // Stripe signature is the auth.
			)
		);
	}

	/**
	 * Handle an incoming Stripe webhook POST.
	 *
	 * @param \WP_REST_Request $request
	 * @return \WP_REST_Response
	 */
	public function handleWebhook( \WP_REST_Request $request ): \WP_REST_Response {
		$payload   = $request->get_body();
		$sig       = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput

		$webhook_secret = $this->getWebhookSecret();

		if ( empty( $webhook_secret ) ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
				error_log( '[Donatico] Webhook received but no webhook secret is configured.' );
			}
			return new \WP_REST_Response( array( 'error' => 'Webhook secret not configured.' ), 500 );
		}

		// Verify the signature before trusting the payload.
		try {
			$event = \Stripe\Webhook::constructEvent( $payload, $sig, $webhook_secret );
		} catch ( \UnexpectedValueException $e ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
				error_log( '[Donatico] Webhook invalid payload: ' . $e->getMessage() );
			}
			return new \WP_REST_Response( array( 'error' => 'Invalid payload.' ), 400 );
		} catch ( \Stripe\Exception\SignatureVerificationException $e ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
				error_log( '[Donatico] Webhook signature verification failed: ' . $e->getMessage() );
			}
			return new \WP_REST_Response( array( 'error' => 'Invalid signature.' ), 400 );
		}

		// Dispatch to the appropriate handler.
		switch ( $event->type ) {
			case 'checkout.session.completed':
				$this->onCheckoutCompleted( $event->data->object );
				break;

			case 'checkout.session.expired':
				$this->onSessionExpired( $event->data->object );
				break;

			case 'payment_intent.payment_failed':
				$this->onPaymentFailed( $event->data->object );
				break;

			case 'charge.refunded':
				$this->onChargeRefunded( $event->data->object );
				break;

			default:
				// Unknown events are acknowledged but not processed.
				break;
		}

		return new \WP_REST_Response( array( 'received' => true ), 200 );
	}

	// -------------------------------------------------------------------------
	// Event handlers
	// -------------------------------------------------------------------------

	/**
	 * Payment succeeded — mark donation completed and fire action for emails.
	 *
	 * @param \Stripe\Checkout\Session $session
	 * @return void
	 */
	private function onCheckoutCompleted( $session ): void {
		$donation = $this->repository->findByStripeSessionId( (string) $session->id );

		if ( ! $donation ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
				error_log( '[Donatico] checkout.session.completed — no donation found for session: ' . $session->id );
			}
			return;
		}

		$payment_intent_id = isset( $session->payment_intent ) ? (string) $session->payment_intent : null;

		$this->repository->update(
			$donation->getId(),
			array(
				'status'                   => Donation::STATUS_COMPLETED,
				'stripe_payment_intent_id' => $payment_intent_id,
			)
		);

		$donation->setStatus( Donation::STATUS_COMPLETED );
		if ( $payment_intent_id ) {
			$donation->setStripePaymentIntentId( $payment_intent_id );
		}

		/**
		 * Fires when a donation is successfully completed.
		 *
		 * @param Donation $donation The completed donation.
		 */
		do_action( 'donatico_donation_completed', $donation );
	}

	/**
	 * Stripe session timed out (user abandoned checkout).
	 *
	 * @param \Stripe\Checkout\Session $session
	 * @return void
	 */
	private function onSessionExpired( $session ): void {
		$donation = $this->repository->findByStripeSessionId( (string) $session->id );

		if ( ! $donation ) {
			return;
		}

		$this->repository->update( $donation->getId(), array( 'status' => Donation::STATUS_FAILED ) );

		/** @fires donatico_donation_expired */
		do_action( 'donatico_donation_expired', $donation );
	}

	/**
	 * Payment attempt failed (card declined, etc.).
	 *
	 * @param \Stripe\PaymentIntent $payment_intent
	 * @return void
	 */
	private function onPaymentFailed( $payment_intent ): void {
		$donation = $this->repository->findByStripePaymentIntentId( (string) $payment_intent->id );

		if ( ! $donation ) {
			// The intent may not be linked to a donation yet (first failed attempt).
			return;
		}

		$this->repository->update( $donation->getId(), array( 'status' => Donation::STATUS_FAILED ) );

		/** @fires donatico_donation_failed */
		do_action( 'donatico_donation_failed', $donation );
	}

	/**
	 * Charge fully or partially refunded.
	 *
	 * @param \Stripe\Charge $charge
	 * @return void
	 */
	private function onChargeRefunded( $charge ): void {
		$payment_intent_id = isset( $charge->payment_intent ) ? (string) $charge->payment_intent : null;

		if ( ! $payment_intent_id ) {
			return;
		}

		$donation = $this->repository->findByStripePaymentIntentId( $payment_intent_id );

		if ( ! $donation ) {
			return;
		}

		$this->repository->update( $donation->getId(), array( 'status' => Donation::STATUS_REFUNDED ) );

		/** @fires donatico_donation_refunded */
		do_action( 'donatico_donation_refunded', $donation );
	}

	// -------------------------------------------------------------------------
	// Helpers
	// -------------------------------------------------------------------------

	/**
	 * Read the webhook secret from options.
	 * Returns the single stored secret — admins configure one per active mode.
	 *
	 * @return string
	 */
	private function getWebhookSecret(): string {
		return (string) get_option( 'donatico_stripe_webhook_secret', '' );
	}
}
