<?php

namespace SugarCalendarBookings\Payments;

use Exception;
use SugarCalendarBookings\Appointments\AppointmentModel;
use SugarCalendarBookings\Appointments\AppointmentService;
use SugarCalendarBookings\Integrations\MeetingManager;
use SugarCalendarBookings\Notifications\PaymentNotifier;
use SugarCalendarBookings\BookingForm\BookingFormService;
use SugarCalendarBookings\Payments\Gateways\PaymentGatewayRegistry;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Support\Ajax\AbstractController;

/**
 * Class PaymentsController.
 *
 * @since 1.0.0
 */
class PaymentsController extends AbstractController {

	/**
	 * Authorize payment.
	 *
	 * @since 1.0.0
	 */
	public function authorize() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$rules = [
			'payment_method' => 'required|string',
			'item_id'        => 'required|id',
		];

		$data               = $this->get_input_data( $rules );
		$payment_gateway_id = $data['payment_method'];
		$appointment        = AppointmentModel::get( $data['item_id'] );

		if ( ! $appointment ) {
			wp_send_json_error( esc_html__( 'Appointment not found.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		// Global gateway gate: refuse anything not in the configured set.
		// Done as a manual check (not the validator's `in:` rule) so we can
		// return a user-friendly message instead of leaking the list of
		// enabled gateways back to the caller.
		$configured_gateways = array_keys( Plugin::get( PaymentGatewayRegistry::class )->get_configured() );

		if ( ! in_array( $payment_gateway_id, $configured_gateways, true ) ) {
			wp_send_json_error( esc_html__( 'Selected payment method is not available.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		// Per-service allowlist: when the service restricts which gateways
		// it accepts, refuse a gateway-swap to anything off that list. The
		// globally-configured filter above is necessary but not sufficient —
		// a service may be set to "Stripe only" while On-Site is enabled
		// site-wide, and the booking-form UI honors the per-service list
		// when rendering the dropdown. The request validator has to mirror
		// that, otherwise the per-service restriction is purely cosmetic.
		//
		// Only enforce the per-service list when the override toggle is ON.
		// `payment_gateways` meta is not cleared when override is toggled
		// OFF, so a stale list would otherwise reject globally-allowed
		// gateways the booking form correctly offers under override-OFF.
		$override_enabled = Plugin::instance()->is_pro() && ( $appointment->service->override_payment_methods ?? false );
		$service_gateways = $appointment->service->payment_gateways ?? [];

		if ( $override_enabled && ! empty( $service_gateways ) && ! in_array( $payment_gateway_id, $service_gateways, true ) ) {
			wp_send_json_error( esc_html__( 'Selected payment method is not available for this service.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		$payment_dto = new CreatePaymentDTO(
			[
				'customer_id'     => $appointment->customer->id,
				'item_id'         => $appointment->id,
				'total_amount'    => $appointment->service->price ?? 0,
				'subtotal_amount' => $appointment->service->price ?? 0,
				'currency'        => PaymentsHelpers::get_currency(),
				'gateway'         => $payment_gateway_id,
			]
		);

		try {
			$payment = Plugin::get( PaymentService::class )->create( $payment_gateway_id, $payment_dto );
		} catch ( Exception $e ) {
			$appointment->delete();

			wp_send_json_error( $e->getMessage() );
		}

		try {
			$processor = Plugin::get( PaymentGatewayRegistry::class )
				->get( $payment_gateway_id )
				->get_processor();

			$payment_data = [];
			$rules        = $processor->get_authorization_data_processing_rules();

			if ( ! empty( $rules ) ) {
				$rules        = $this->wrap_processor_rules( $rules, 'payment_data' );
				$payment_data = $this->get_input_data( $rules )['payment_data'];
			}

			$authorization_data = $processor->authorize_payment( $payment, $payment_data );

			// Check if meeting creation failed during finalization.
			$meeting_error = Plugin::get( MeetingManager::class )->get_last_error();

			if ( $meeting_error ) {
				$appointment = $payment->appointments[0] ?? null;

				if ( $appointment ) {
					Plugin::get( AppointmentService::class )->delete( $appointment );
				}

				$payment->delete();

				wp_send_json_error(
					esc_html__( 'Failed to create online meeting. Please check your integration connection and try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' )
				);
			}

			wp_send_json_success(
				[
					'payment_id'         => $payment->id,
					'session_token'      => self::generate_session_token( $payment->id ),
					'authorization_data' => $authorization_data,
				]
			);
		} catch ( Exception $e ) {
			$payment->delete();

			wp_send_json_error( $e->getMessage() );
		}
	}

	/**
	 * Generate the per-payment session token returned at authorize time and
	 * required at capture time. Binds the public capture endpoint to the
	 * caller who originated the payment, closing the IDOR through which any
	 * unauthenticated visitor could capture / fetch any payment by id.
	 *
	 * Only computable by the server (wp_salt() is the secret), so a leaked
	 * payment_id cannot be replayed without the matching session token.
	 *
	 * @since 1.4.1
	 *
	 * @param int $payment_id Payment ID.
	 *
	 * @return string
	 */
	public static function generate_session_token( $payment_id ) {

		return hash_hmac( 'sha256', 'payment_session|' . (int) $payment_id, wp_salt() );
	}

	/**
	 * Verify the per-payment session token against the payment_id.
	 *
	 * @since 1.4.1
	 *
	 * @param int    $payment_id Payment ID.
	 * @param string $token      Token to verify.
	 *
	 * @return bool
	 */
	public static function verify_session_token( $payment_id, $token ) {

		if ( ! is_string( $token ) || $token === '' ) {
			return false;
		}

		return hash_equals( self::generate_session_token( $payment_id ), $token );
	}

	/**
	 * Capture payment.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function capture() {

		$rules = [
			'payment_id'    => 'required|id',
			'session_token' => 'required|string',
		];

		$data = $this->get_input_data( $rules );

		// Bind the public capture endpoint to the caller that originated the
		// payment. Without this gate, payments_capture is a public IDOR — any
		// visitor iterating sequential payment_id values can fetch the
		// appointment / customer record attached to every payment.
		if ( ! self::verify_session_token( $data['payment_id'], $data['session_token'] ) ) {
			wp_send_json_error( esc_html__( 'Invalid payment session.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		$payment = PaymentModel::get( $data['payment_id'] );

		if ( ! $payment ) {
			wp_send_json_error( esc_html__( 'Payment not found.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		try {
			$processor = Plugin::get( PaymentGatewayRegistry::class )
				->get( $payment->gateway )
				->get_processor();

			$authorization_data = [];
			$rules              = $processor->get_capture_data_processing_rules();

			if ( ! empty( $rules ) ) {
				$rules              = $this->wrap_processor_rules( $rules, 'authorization_data' );
				$authorization_data = $this->get_input_data( $rules )['authorization_data'];
			}

			$capture_data = $processor->capture_payment( $payment, $authorization_data );

			$response = [
				'payment'      => Plugin::get( PaymentService::class )->format_payment_data( $payment ),
				'capture_data' => $capture_data,
			];

			// Re-fetch appointment to include data set after capture (e.g., Zoom join_url).
			$appointment = $payment->appointments[0] ?? null;

			if ( $appointment ) {
				$appointment = AppointmentModel::get( $appointment->id );

				// Empty customer override — capture response must not echo
				// DB customer fields. If the booking flow auto-merged the
				// caller's phone-only input against an existing customer
				// record, returning the matched email here would turn the
				// capture endpoint into a phone→email PII oracle.
				$response['appointment'] = Plugin::get( BookingFormService::class )
					->get_appointment_form_data(
						$appointment,
						[
							'customer' => [
								'name'  => '',
								'email' => '',
								'phone' => '',
							],
						]
					);
			}

			wp_send_json_success( $response );
		} catch ( Exception $e ) {
			wp_send_json_error( $e->getMessage() );
		}
	}

	/**
	 * Refund payment.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function refund() {

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( esc_html__( 'You are not allowed to refund payments.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		$rules = [
			'payment_id' => 'required|id',
		];

		$data    = $this->get_input_data( $rules );
		$payment = PaymentModel::query()
			->with( [ 'appointments' ] )
			->where( 'id', '=', $data['payment_id'], '%d' )
			->first();

		if ( ! $payment ) {
			wp_send_json_error( esc_html__( 'Payment not found.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		// Check if payment is refundable (must be completed).
		if ( $payment->status !== PaymentModel::STATUS_COMPLETED ) {
			wp_send_json_error( esc_html__( 'Only completed payments can be refunded.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		try {
			$processor = Plugin::get( PaymentGatewayRegistry::class )
				->get( $payment->gateway )
				->get_processor();

			// Process refund through payment gateway.
			$refund_data = $processor->refund_payment( $payment );

			// Store previous status for notification.
			$previous_status = $payment->status;

			// Update payment status to refunded.
			$payment->status = PaymentModel::STATUS_REFUNDED;

			$payment->save();

			// Send refund notification email.
			Plugin::get( PaymentNotifier::class )->notify( $payment, $previous_status );

			wp_send_json_success(
				[
					'message'     => esc_html__( 'Payment refunded successfully.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					'refund_data' => $refund_data,
				]
			);
		} catch ( Exception $e ) {
			wp_send_json_error( $e->getMessage() );
		}
	}

	/**
	 * Wrap processor rules.
	 *
	 * @since 1.1.0
	 *
	 * @param array  $processor_rules Processor rules.
	 * @param string $prefix          Rule prefix.
	 *
	 * @return string[]
	 */
	private function wrap_processor_rules( $processor_rules, $prefix ) {

		return array_reduce(
			array_keys( $processor_rules ),
			fn( $rules, $key ) => array_merge(
				$rules,
				[
					"$prefix.$key" => $processor_rules[ $key ],
				]
			),
			[
				$prefix => 'array',
			]
		);
	}
}
