<?php
/**
 * REST webhook endpoints for payment gateways.
 *
 * Exposes:
 *   POST /wp-json/lucrsure/v1/webhooks/stripe
 *   POST /wp-json/lucrsure/v1/webhooks/sslcommerz
 *   POST /wp-json/lucrsure/v1/webhooks/razorpay
 *   POST /wp-json/lucrsure/v1/webhooks/paypal
 *
 * The endpoints verify authenticity and mark the matching invoice/subscription
 * as paid so the billing loop is fully automated.
 *
 * @package Lucrativeit_Subscription_Recurring_Billing
 */

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

/**
 * Handles incoming gateway webhooks.
 */
class LUCRSURE_Webhooks {

	/**
	 * Singleton.
	 *
	 * @var LUCRSURE_Webhooks|null
	 */
	private static $instance = null;

	/**
	 * REST namespace.
	 */
	const NAMESPACE = 'lucrsure/v1';

	/**
	 * Get the singleton.
	 *
	 * @return LUCRSURE_Webhooks
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Register hooks.
	 */
	public function init() {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	/**
	 * Build the public webhook URL for a gateway.
	 *
	 * @param string $gateway Gateway id.
	 * @return string
	 */
	public static function url( $gateway ) {
		return esc_url_raw( rest_url( self::NAMESPACE . '/webhooks/' . $gateway ) );
	}

	/**
	 * Register the REST routes.
	 */
	public function register_routes() {
		$args = array(
			'methods'             => 'POST',
			'permission_callback' => '__return_true',
		);

		register_rest_route( self::NAMESPACE, '/webhooks/stripe', array_merge( $args, array( 'callback' => array( $this, 'handle_stripe' ) ) ) );
		register_rest_route( self::NAMESPACE, '/webhooks/sslcommerz', array_merge( $args, array( 'callback' => array( $this, 'handle_sslcommerz' ) ) ) );
		register_rest_route( self::NAMESPACE, '/webhooks/razorpay', array_merge( $args, array( 'callback' => array( $this, 'handle_razorpay' ) ) ) );
		register_rest_route( self::NAMESPACE, '/webhooks/paypal', array_merge( $args, array( 'callback' => array( $this, 'handle_paypal' ) ) ) );
	}

	/**
	 * Read a gateway config block from settings.
	 *
	 * @param string $gateway Gateway id.
	 * @return array
	 */
	protected function gateway_config( $gateway ) {
		$settings = get_option( 'lucrsure_settings', array() );
		$gateways = isset( $settings['gateways'] ) ? $settings['gateways'] : array();
		return isset( $gateways[ $gateway ] ) ? $gateways[ $gateway ] : array();
	}

	/* ----------------------------------------------------------------- *
	 * Stripe
	 * ----------------------------------------------------------------- */

	/**
	 * Handle Stripe webhook events.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function handle_stripe( WP_REST_Request $request ) {
		$payload = $request->get_body();
		$config  = $this->gateway_config( 'stripe' );
		$secret  = isset( $config['webhook_secret'] ) ? $config['webhook_secret'] : '';

		// Always require a configured signing secret — never process unsigned events.
		if ( empty( $secret ) ) {
			return new WP_REST_Response( array( 'error' => 'webhook_not_configured' ), 503 );
		}

		$sig = $request->get_header( 'stripe_signature' );
		if ( ! $this->verify_stripe_signature( $payload, $sig, $secret ) ) {
			return new WP_REST_Response( array( 'error' => 'invalid_signature' ), 400 );
		}

		$event = json_decode( $payload, true );
		if ( empty( $event['type'] ) ) {
			return new WP_REST_Response( array( 'error' => 'invalid_payload' ), 400 );
		}

		$object   = isset( $event['data']['object'] ) ? $event['data']['object'] : array();
		$engine   = LUCRSURE_Billing_Engine::instance();
		$invoices = new LUCRSURE_Invoice_Repository();
		$subs     = new LUCRSURE_Subscription_Repository();

		switch ( $event['type'] ) {
			case 'checkout.session.completed':
				$mode = isset( $object['mode'] ) ? $object['mode'] : 'payment';

				if ( 'subscription' === $mode ) {
					$local_sub_id = isset( $object['client_reference_id'] ) ? (int) $object['client_reference_id'] : 0;
					if ( ! $local_sub_id && ! empty( $object['metadata']['lucrsure_subscription_id'] ) ) {
						$local_sub_id = (int) $object['metadata']['lucrsure_subscription_id'];
					}
					$stripe_sub = isset( $object['subscription'] ) ? $object['subscription'] : '';

					if ( $local_sub_id && $stripe_sub ) {
						$existing   = $subs->find( $local_sub_id );
						$was_active = $existing && 'active' === $existing['status'];

						$subs->update(
							$local_sub_id,
							array(
								'status'                  => 'active',
								'gateway_subscription_id' => $stripe_sub,
							)
						);

						if ( ! $was_active ) {
							$activated = $subs->find( $local_sub_id );
							if ( $activated ) {
								$engine->notify_subscription_active( $activated );
							}
						}
					}

					// Mark the first recorded invoice as paid (session id, then metadata/number, then latest for sub).
					$invoice = $invoices->find_by_gateway_payment( isset( $object['id'] ) ? $object['id'] : '' );
					if ( ! $invoice && ! empty( $object['metadata']['lucrsure_invoice_number'] ) ) {
						$invoice = $invoices->find_by_number( (string) $object['metadata']['lucrsure_invoice_number'] );
					}
					if ( ! $invoice && $local_sub_id ) {
						$invoice = $invoices->find_latest_for_subscription( $local_sub_id );
					}
					if ( $invoice ) {
						if ( empty( $invoice['gateway_payment_id'] ) && ! empty( $object['id'] ) ) {
							$invoices->update( (int) $invoice['id'], array( 'gateway_payment_id' => $object['id'] ) );
						}
						$engine->mark_invoice_paid( (int) $invoice['id'] );
					}
				} else {
					$number  = isset( $object['client_reference_id'] ) ? $object['client_reference_id'] : ( $object['metadata']['lucrsure_invoice_number'] ?? '' );
					$invoice = $invoices->find_by_number( $number );
					if ( ! $invoice && ! empty( $object['id'] ) ) {
						$invoice = $invoices->find_by_gateway_payment( $object['id'] );
					}
					if ( $invoice ) {
						$engine->mark_invoice_paid( (int) $invoice['id'] );
					}
				}
				break;

			case 'invoice.payment_succeeded':
				// Only act on recurring cycle renewals; the first invoice is
				// already handled by checkout.session.completed.
				$reason = isset( $object['billing_reason'] ) ? $object['billing_reason'] : '';
				if ( 'subscription_cycle' === $reason || 'subscription_update' === $reason ) {
					$stripe_sub = isset( $object['subscription'] ) ? $object['subscription'] : '';
					$local      = $subs->find_by_gateway_subscription( $stripe_sub );
					if ( $local ) {
						$amount   = isset( $object['amount_paid'] ) ? ( (float) $object['amount_paid'] / 100 ) : 0;
						$currency = isset( $object['currency'] ) ? strtoupper( $object['currency'] ) : 'USD';
						$pay_id   = isset( $object['payment_intent'] ) ? $object['payment_intent'] : ( $object['id'] ?? '' );
						$engine->record_recurring_payment( $local, $amount, $currency, $pay_id );
					}
				}
				break;

			case 'invoice.payment_failed':
				$stripe_sub = isset( $object['subscription'] ) ? $object['subscription'] : '';
				$local      = $subs->find_by_gateway_subscription( $stripe_sub );
				if ( $local ) {
					$subs->update( (int) $local['id'], array( 'status' => 'past_due' ) );

					/**
					 * Fires when a subscription payment fails.
					 *
					 * Add-ons (e.g. the Pro plugin) can hook this to send a
					 * payment-failed alert to the customer/admin.
					 *
					 * @param array $local The local subscription row.
					 */
					do_action( 'lucrsure_subscription_payment_failed', $local );
				}
				break;

			case 'customer.subscription.deleted':
				$stripe_sub = isset( $object['id'] ) ? $object['id'] : '';
				$local      = $subs->find_by_gateway_subscription( $stripe_sub );
				if ( $local ) {
					$subs->update(
						(int) $local['id'],
						array(
							'status'      => 'canceled',
							'canceled_at' => current_time( 'mysql' ),
						)
					);
				}
				break;
		}

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

	/**
	 * Verify a Stripe webhook signature (Stripe-Signature header).
	 *
	 * @param string $payload Raw body.
	 * @param string $header  Signature header value.
	 * @param string $secret  Webhook signing secret.
	 * @return bool
	 */
	protected function verify_stripe_signature( $payload, $header, $secret ) {
		if ( empty( $header ) ) {
			return false;
		}

		$timestamp = '';
		$signature = '';
		foreach ( explode( ',', $header ) as $part ) {
			$pair = explode( '=', trim( $part ), 2 );
			if ( 2 !== count( $pair ) ) {
				continue;
			}
			if ( 't' === $pair[0] ) {
				$timestamp = $pair[1];
			} elseif ( 'v1' === $pair[0] ) {
				$signature = $pair[1];
			}
		}

		if ( '' === $timestamp || '' === $signature ) {
			return false;
		}

		// Reject events older than 5 minutes (replay protection).
		if ( abs( time() - (int) $timestamp ) > 300 ) {
			return false;
		}

		$expected = hash_hmac( 'sha256', $timestamp . '.' . $payload, $secret );
		return hash_equals( $expected, $signature );
	}

	/* ----------------------------------------------------------------- *
	 * SSLCommerz (IPN)
	 * ----------------------------------------------------------------- */

	/**
	 * Handle SSLCommerz IPN.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function handle_sslcommerz( WP_REST_Request $request ) {
		$params  = $request->get_params();
		$tran_id = isset( $params['tran_id'] ) ? sanitize_text_field( wp_unslash( $params['tran_id'] ) ) : '';
		$val_id  = isset( $params['val_id'] ) ? sanitize_text_field( wp_unslash( $params['val_id'] ) ) : '';

		if ( '' === $tran_id || '' === $val_id ) {
			return new WP_REST_Response( array( 'error' => 'missing_tran_id' ), 400 );
		}

		$gateway = new LUCRSURE_SSLCommerz_Gateway();
		if ( ! $gateway->complete_return( $tran_id, $val_id ) ) {
			return new WP_REST_Response( array( 'error' => 'validation_failed' ), 400 );
		}

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

	/**
	 * Validate an SSLCommerz transaction via their validator API.
	 *
	 * @deprecated Use LUCRSURE_SSLCommerz_Gateway::validate_transaction().
	 * @param string $val_id Validation id from SSLCommerz.
	 * @return bool
	 */
	protected function validate_sslcommerz( $val_id ) {
		$gateway = new LUCRSURE_SSLCommerz_Gateway();
		return $gateway->validate_transaction( $val_id );
	}

	/* ----------------------------------------------------------------- *
	 * Razorpay
	 * ----------------------------------------------------------------- */

	/**
	 * Handle Razorpay webhook events.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function handle_razorpay( WP_REST_Request $request ) {
		$payload = $request->get_body();
		$config  = $this->gateway_config( 'razorpay' );
		$secret  = isset( $config['webhook_secret'] ) ? (string) $config['webhook_secret'] : '';

		if ( '' === $secret ) {
			return new WP_REST_Response( array( 'error' => 'webhook_not_configured' ), 503 );
		}

		$sig      = sanitize_text_field( (string) $request->get_header( 'x_razorpay_signature' ) );
		$expected = hash_hmac( 'sha256', $payload, $secret );
		if ( '' === $sig || ! hash_equals( $expected, $sig ) ) {
			return new WP_REST_Response( array( 'error' => 'invalid_signature' ), 400 );
		}

		$event = json_decode( $payload, true );
		if ( empty( $event['event'] ) || ! is_array( $event ) ) {
			return new WP_REST_Response( array( 'error' => 'invalid_payload' ), 400 );
		}

		$event_name = sanitize_text_field( (string) $event['event'] );
		$reference  = '';
		$pay_id     = '';

		if ( isset( $event['payload']['payment_link']['entity'] ) && is_array( $event['payload']['payment_link']['entity'] ) ) {
			$entity    = $event['payload']['payment_link']['entity'];
			$reference = isset( $entity['reference_id'] ) ? sanitize_text_field( (string) $entity['reference_id'] ) : '';
		}
		if ( isset( $event['payload']['payment']['entity'] ) && is_array( $event['payload']['payment']['entity'] ) ) {
			$payment = $event['payload']['payment']['entity'];
			$pay_id  = isset( $payment['id'] ) ? sanitize_text_field( (string) $payment['id'] ) : '';
			if ( '' === $reference && isset( $payment['notes']['lucrsure_invoice_number'] ) ) {
				$reference = sanitize_text_field( (string) $payment['notes']['lucrsure_invoice_number'] );
			}
		}

		$paid_events = array( 'payment_link.paid', 'payment.captured', 'order.paid' );
		if ( in_array( $event_name, $paid_events, true ) && '' !== $reference ) {
			$invoices = new LUCRSURE_Invoice_Repository();
			$invoice  = $invoices->find_by_number( $reference );
			if ( $invoice ) {
				if ( $pay_id ) {
					$invoices->update( (int) $invoice['id'], array( 'gateway_payment_id' => $pay_id ) );
				}
				LUCRSURE_Billing_Engine::instance()->mark_invoice_paid( (int) $invoice['id'] );
			}
		}

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

	/* ----------------------------------------------------------------- *
	 * PayPal
	 * ----------------------------------------------------------------- */

	/**
	 * Handle PayPal webhook events.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function handle_paypal( WP_REST_Request $request ) {
		$payload = $request->get_body();
		$event   = json_decode( $payload, true );

		if ( empty( $event['event_type'] ) || ! is_array( $event ) ) {
			return new WP_REST_Response( array( 'error' => 'invalid_payload' ), 400 );
		}

		$config = $this->gateway_config( 'paypal' );
		if ( empty( $config['webhook_id'] ) ) {
			return new WP_REST_Response( array( 'error' => 'webhook_not_configured' ), 503 );
		}

		if ( ! $this->verify_paypal_webhook( $request, $payload, sanitize_text_field( (string) $config['webhook_id'] ) ) ) {
			return new WP_REST_Response( array( 'error' => 'invalid_signature' ), 400 );
		}

		$event_type = sanitize_text_field( (string) $event['event_type'] );

		$paid_events = array(
			'PAYMENT.CAPTURE.COMPLETED',
			'CHECKOUT.ORDER.APPROVED',
			'CHECKOUT.ORDER.COMPLETED',
		);

		if ( ! in_array( $event_type, $paid_events, true ) ) {
			return new WP_REST_Response(
				array(
					'received' => true,
					'ignored'  => true,
				),
				200
			);
		}

		$resource       = isset( $event['resource'] ) && is_array( $event['resource'] ) ? $event['resource'] : array();
		$invoice_number = '';
		$payment_id     = isset( $resource['id'] ) ? sanitize_text_field( (string) $resource['id'] ) : '';

		if ( ! empty( $resource['custom_id'] ) ) {
			$invoice_number = sanitize_text_field( (string) $resource['custom_id'] );
		} elseif ( ! empty( $resource['purchase_units'][0]['custom_id'] ) ) {
			$invoice_number = sanitize_text_field( (string) $resource['purchase_units'][0]['custom_id'] );
		} elseif ( ! empty( $resource['purchase_units'][0]['reference_id'] ) ) {
			$invoice_number = sanitize_text_field( (string) $resource['purchase_units'][0]['reference_id'] );
		} elseif ( ! empty( $resource['supplementary_data']['related_ids']['order_id'] ) ) {
			$order_id = sanitize_text_field( (string) $resource['supplementary_data']['related_ids']['order_id'] );
			$paypal   = new LUCRSURE_PayPal_Gateway();
			$order    = $paypal->get_order( $order_id );
			if ( $order ) {
				if ( ! empty( $order['purchase_units'][0]['custom_id'] ) ) {
					$invoice_number = sanitize_text_field( (string) $order['purchase_units'][0]['custom_id'] );
				} elseif ( ! empty( $order['purchase_units'][0]['reference_id'] ) ) {
					$invoice_number = sanitize_text_field( (string) $order['purchase_units'][0]['reference_id'] );
				}
			}
		}

		if ( 'CHECKOUT.ORDER.APPROVED' === $event_type && '' !== $payment_id ) {
			$paypal = new LUCRSURE_PayPal_Gateway();
			$paypal->capture_order( $payment_id );
			return new WP_REST_Response( array( 'received' => true ), 200 );
		}

		if ( '' === $invoice_number ) {
			return new WP_REST_Response(
				array(
					'received' => true,
					'error'    => 'missing_invoice',
				),
				200
			);
		}

		$invoices = new LUCRSURE_Invoice_Repository();
		$invoice  = $invoices->find_by_number( $invoice_number );
		if ( $invoice ) {
			if ( $payment_id ) {
				$invoices->update( (int) $invoice['id'], array( 'gateway_payment_id' => $payment_id ) );
			}
			LUCRSURE_Billing_Engine::instance()->mark_invoice_paid( (int) $invoice['id'] );
		}

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

	/**
	 * Verify a PayPal webhook signature via the Verify Webhook Signature API.
	 *
	 * @param WP_REST_Request $request    Request.
	 * @param string          $payload    Raw body.
	 * @param string          $webhook_id PayPal webhook id.
	 * @return bool
	 */
	protected function verify_paypal_webhook( WP_REST_Request $request, $payload, $webhook_id ) {
		$paypal = new LUCRSURE_PayPal_Gateway();
		$token  = $paypal->get_access_token();
		if ( '' === $token ) {
			return false;
		}

		$transmission_id   = sanitize_text_field( (string) $request->get_header( 'paypal-transmission-id' ) );
		$transmission_time = sanitize_text_field( (string) $request->get_header( 'paypal-transmission-time' ) );
		$cert_url          = sanitize_text_field( (string) $request->get_header( 'paypal-cert-url' ) );
		$auth_algo         = sanitize_text_field( (string) $request->get_header( 'paypal-auth-algo' ) );
		$transmission_sig  = sanitize_text_field( (string) $request->get_header( 'paypal-transmission-sig' ) );

		if ( '' === $transmission_id || '' === $transmission_sig ) {
			return false;
		}

		$event = json_decode( $payload );
		if ( null === $event ) {
			return false;
		}

		$response = wp_remote_post(
			$paypal->api_base() . '/v1/notifications/verify-webhook-signature',
			array(
				'timeout' => 30,
				'headers' => array(
					'Authorization' => 'Bearer ' . $token,
					'Content-Type'  => 'application/json',
				),
				'body'    => wp_json_encode(
					array(
						'transmission_id'   => $transmission_id,
						'transmission_time' => $transmission_time,
						'cert_url'          => $cert_url,
						'auth_algo'         => $auth_algo,
						'transmission_sig'  => $transmission_sig,
						'webhook_id'        => $webhook_id,
						'webhook_event'     => $event,
					)
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			return false;
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		return isset( $body['verification_status'] ) && 'SUCCESS' === $body['verification_status'];
	}

}
