<?php

namespace SugarCalendarBookings\Payments\Gateways\Stripe;

use Exception;
use SugarCalendarBookings\Vendor\Stripe\Exception\ApiErrorException;
use SugarCalendarBookings\Vendor\Stripe\PaymentIntent;
use SugarCalendarBookings\Options;
use SugarCalendarBookings\Appointments\AppointmentService;
use SugarCalendarBookings\Payments\Gateways\AbstractPaymentProcessor;
use SugarCalendarBookings\Payments\Gateways\PaymentGatewayRegistry;
use SugarCalendarBookings\Payments\PaymentModel;
use SugarCalendarBookings\Payments\PaymentsHelpers;
use SugarCalendarBookings\Plugin;

/**
 * Class StripePaymentProcessor.
 *
 * @since 1.0.0
 */
class StripePaymentProcessor extends AbstractPaymentProcessor {

	/**
	 * Payment gateway.
	 *
	 * @since 1.0.0
	 *
	 * @var StripePaymentGateway
	 */
	protected $gateway;

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {

		$this->gateway = Plugin::get( PaymentGatewayRegistry::class )->get( 'stripe' );
	}

	/**
	 * Get processing rules for authorization_data data.
	 *
	 * @since 1.1.0
	 *
	 * @return array
	 */
	public function get_authorization_data_processing_rules() {

		// item_id intentionally absent — the charge amount is derived from
		// $payment->total_amount (which the controller built from the
		// appointment's service price) rather than from a client-supplied
		// service id. Accepting an item_id here was a price-tampering vector
		// since it let the caller swap the charged price independently of
		// the appointment they were booking.
		return [
			'confirmation_token_id' => 'required|string',
			'redirect_url'          => 'required|string',
		];
	}

	/**
	 * Authorize payment without capturing.
	 *
	 * @since 1.0.0
	 *
	 * @param PaymentModel $payment            Payment model instance.
	 * @param array        $authorization_data Payment data.
	 *
	 * @return array
	 *
	 * @throws Exception If payment cannot be authorized.
	 */
	public function authorize_payment( $payment, $authorization_data ) {

		// Bail if not configured.
		if ( ! $this->gateway->is_configured() ) {
			wp_send_json_error( __( 'Gateway not configured.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		// Derive the charge amount from the payment record the controller
		// just built (price comes from the appointment's service), not from
		// the request — see get_authorization_data_processing_rules().
		$amount = StripeHelpers::get_amount( $payment->total_amount );

		if ( $amount <= 0 ) {
			throw new Exception( esc_html__( 'Invalid payment amount.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		$redirect_url = PaymentsHelpers::get_redirect_url( $authorization_data['redirect_url'], $this->gateway::get_id() );

		// Create payment intent.
		$payment_intent = $this->create_confirm_intent(
			$payment,
			$authorization_data['confirmation_token_id'],
			$amount,
			PaymentsHelpers::get_currency(),
			$redirect_url
		);

		// Set payment transaction ID.
		$payment->transaction_id = $payment_intent->id;

		$payment->save();

		return [
			'id'            => $payment_intent->id,
			'client_secret' => $payment_intent->client_secret,
			'status'        => $payment_intent->status,
		];
	}

	/**
	 * Create and confirm a PaymentIntent.
	 *
	 * @since 1.0.0
	 * @since 1.5.0 Added $payment param so customer info can be attached;
	 *                  introduced the sugar_calendar_bookings_stripe_payment_intent_data
	 *                  filter for receipt_email / description / metadata.
	 *
	 * @param PaymentModel $payment               Payment being authorized.
	 * @param string       $confirmation_token_id Confirmation token ID.
	 * @param int          $amount                Payment amount.
	 * @param string       $currency              Payment currency.
	 * @param string       $redirect_url          Redirect URL.
	 *
	 * @return PaymentIntent
	 *
	 * @throws ApiErrorException On API error.
	 */
	private function create_confirm_intent( PaymentModel $payment, $confirmation_token_id, $amount, $currency, $redirect_url ) {

		$core_data = [
			'amount'             => $amount,
			'currency'           => strtolower( $currency ),
			'confirm'            => true,
			'confirmation_token' => $confirmation_token_id,
			'return_url'         => $redirect_url,
		];

		if ( ! sugar_calendar_bookings()->is_pro() ) {
			$core_data['application_fee_amount'] = (int) round( $amount * 0.03, 2 );
		}

		$customer_data = $this->build_customer_data( $payment );

		/**
		 * Filter the customer-info portion of the Stripe PaymentIntent payload.
		 *
		 * Only receipt_email, description, and metadata are exposed here.
		 * Core fields (amount, currency, confirm, confirmation_token,
		 * return_url, application_fee_amount) are stripped from the filter
		 * output before merging, so callbacks cannot tamper with the charge.
		 *
		 * @since 1.5.0
		 *
		 * @param array        $customer_data Receipt/description/metadata payload.
		 * @param PaymentModel $payment       Payment being authorized.
		 */
		$customer_data = (array) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
			'sugar_calendar_bookings_stripe_payment_intent_data',
			$customer_data,
			$payment
		);

		// Defend against a buggy filter that replaces metadata with a non-array
		// — Stripe would reject the whole request; drop it instead so the
		// charge still goes through with a minimal payload.
		if ( isset( $customer_data['metadata'] ) && ! is_array( $customer_data['metadata'] ) ) {
			unset( $customer_data['metadata'] );
		}

		// Declarative security invariant: strip any core key a filter callback
		// claimed before merge, so the protection isn't positional. Adding a
		// new protected field is a one-line edit here.
		$protected_keys = array_flip(
			[ 'amount', 'currency', 'confirm', 'confirmation_token', 'return_url', 'application_fee_amount' ]
		);
		$customer_data  = array_diff_key( $customer_data, $protected_keys );

		return StripeHelpers::get_client()->paymentIntents->create(
			$customer_data + $core_data
		);
	}

	/**
	 * Build the customer-info portion of the PaymentIntent payload.
	 *
	 * Resolves the booking customer deterministically, sanitizes each field,
	 * and returns a partial array. Missing or invalid fields are simply
	 * omitted so the charge still succeeds with a degraded payload.
	 *
	 * @since 1.5.0
	 *
	 * @param PaymentModel $payment Payment being authorized.
	 *
	 * @return array
	 */
	private function build_customer_data( PaymentModel $payment ) {

		$metadata = [
			'payment_id' => (string) $payment->id,
			'site_url'   => esc_url_raw( home_url() ),
		];

		// Stripe rejects metadata values that are empty strings, so only
		// include customer_id when it actually resolves.
		if ( ! empty( $payment->customer_id ) ) {
			$metadata['customer_id'] = (string) (int) $payment->customer_id;
		}

		$data = [ 'metadata' => $metadata ];

		$customer = $payment->customer;

		if ( ! $customer ) {
			return $data;
		}

		$name  = StripeHelpers::truncate_metadata_value( (string) $customer->name );
		$email = sanitize_email( (string) $customer->email );
		$phone = StripeHelpers::truncate_metadata_value( (string) $customer->phone );

		if ( $name !== '' ) {
			$data['metadata']['customer_name'] = $name;
			$data['description']               = sprintf(
				/* translators: %1$d - internal payment ID; %2$s - customer name. */
				esc_html__( 'Booking #%1$d — %2$s', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				$payment->id,
				$name
			);
		} else {
			$data['description'] = sprintf(
				/* translators: %1$d - internal payment ID. */
				esc_html__( 'Booking #%1$d', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				$payment->id
			);
		}

		if ( $email !== '' && is_email( $email ) ) {
			$data['metadata']['customer_email'] = $email;
			$data['receipt_email']              = $email;
		}

		if ( $phone !== '' ) {
			$data['metadata']['customer_phone'] = $phone;
		}

		return $data;
	}

	/**
	 * Get processing rules for capture data.
	 *
	 * Intentionally empty since 1.4.1 — the previous shape accepted a
	 * client-supplied `status` field which was trusted at face value (the
	 * forged-`status: succeeded` payment bypass). The processor now reconciles
	 * against the Stripe API using only $payment->transaction_id, so no
	 * client input is consumed here.
	 *
	 * @since 1.1.0
	 * @since 1.4.1 Returns empty array — status is no longer client-supplied.
	 *
	 * @return array
	 */
	public function get_capture_data_processing_rules() {

		return [];
	}

	/**
	 * Capture previously authorized payment.
	 *
	 * Reconciles state with Stripe — never trusts the client. Verifies both
	 * intent.status and intent.amount_received to close two attacks together:
	 *  - forged `status: succeeded` post-3DS (intent retrieval makes Stripe
	 *    authoritative);
	 *  - price tampering at authorize time (amount_received must cover
	 *    $payment->total_amount, blocking any successful capture for a
	 *    short-charge).
	 *
	 * @since 1.0.0
	 * @since 1.4.1 Reconcile with Stripe via paymentIntents->retrieve.
	 *
	 * @param PaymentModel $payment      Payment model instance.
	 * @param array        $capture_data Unused — kept for processor interface compatibility.
	 *
	 * @return array
	 *
	 * @throws Exception If the Stripe API verification fails or the intent indicates failure.
	 */
	public function capture_payment( $payment, $capture_data ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found

		if ( empty( $payment->transaction_id ) ) {
			throw new Exception( esc_html__( 'Payment is missing a Stripe transaction ID.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		try {
			$intent = StripeHelpers::get_client()->paymentIntents->retrieve( $payment->transaction_id );
		} catch ( ApiErrorException $e ) {
			throw new Exception(
				sprintf(
					/* translators: %s - Stripe API error message. */
					esc_html__( 'Stripe verification failed: %s', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					esc_html( $e->getMessage() )
				)
			);
		}

		$expected_amount = StripeHelpers::get_amount( $payment->total_amount );
		$amount_received = isset( $intent->amount_received ) ? (int) $intent->amount_received : 0;

		$response = [
			'id'              => $intent->id,
			'client_secret'   => $intent->client_secret,
			'status'          => $intent->status,
			'amount_received' => $amount_received,
		];

		if ( $intent->status === 'succeeded' && $amount_received >= $expected_amount ) {
			$payment->status = PaymentModel::STATUS_COMPLETED;
			$payment->save();

			// Finalize appointment after payment processing.
			$appointment = $payment->appointments[0];

			Plugin::get( AppointmentService::class )->finalize_after_payment( $appointment );

			return $response;
		}

		if ( in_array( $intent->status, [ 'requires_payment_method', 'canceled', 'failed' ], true ) ) {
			$payment->status = PaymentModel::STATUS_FAILED;
			$payment->save();

			throw new Exception( esc_html__( 'Payment failed.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		// requires_action / processing / unknown — keep payment PENDING so
		// the client can complete 3DS via stripe.handleNextAction or the
		// Stripe-side redirect flow drives a later capture.
		return $response;
	}

	/**
	 * Refund a completed payment.
	 *
	 * @since 1.1.0
	 *
	 * @param PaymentModel $payment Payment model instance.
	 *
	 * @return array Refund data.
	 *
	 * @throws Exception If payment cannot be refunded.
	 */
	public function refund_payment( $payment ) {

		// Bail if not configured.
		if ( ! $this->gateway->is_configured() ) {
			throw new Exception( esc_html__( 'Payment gateway not configured.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		// Bail if payment doesn't have a transaction ID.
		if ( empty( $payment->transaction_id ) ) {
			throw new Exception( esc_html__( 'Payment does not have a transaction ID.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		try {
			$refund = StripeHelpers::get_client()->refunds->create(
				[
					'payment_intent' => $payment->transaction_id,
					'reason'         => 'requested_by_customer',
					'metadata'       => [
						'payment_id' => $payment->id,
					],
				]
			);

			return [
				'refund_id' => $refund->id,
				'amount'    => $refund->amount,
				'status'    => $refund->status,
			];
		} catch ( ApiErrorException $e ) {
			throw new Exception(
				sprintf(
					/* translators: %1$s - Stripe API error message. */
					esc_html__( 'Stripe refund failed: %1$s', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					esc_html( $e->getMessage() )
				)
			);
		}
	}
}
