<?php
/**
 * REST API  YOCO PAYMENTS controller
 *
 * Handles requests to the yoco/webhook endpoint.
 *
 * @package Kkart\RestApi
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Order Notes controller class.
 *
 * @package Kkart\RestApi
 */

class KKART_REST_Yoco_Webhook_Controller  extends KKART_REST_Controller {
	
	protected $namespace = 'kkart/v3';
	protected $rest_base = 'webhook';
	
	public function register_routes(){
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			'methods'=> 'GET, POST',
			'callback'=> array( $this, 'yoco_webhook_handler' ),
			'permission_callback' => array( $this, 'permit' ),
		) );
	}

	public function yoco_webhook_handler($request){
		
		$body = $request->get_params();
		$eventType = isset( $body['type'] ) && ! empty( $body['type'] ) ? $body['type'] : '';
		$checkout_id = isset( $body['payload']['metadata']['checkoutId'] ) ? $body['payload']['metadata']['checkoutId'] : '';
		$payment_id = isset( $body['payload']['id']  ) ? $body['payload']['id'] : '';
		
		if($eventType == 'payment.succeeded'){
			$this->update_payment_status($checkout_id, $payment_id);
		}
		
		if($eventType === 'refund.failed'){
			$this->update_refund_failed($checkout_id, $payment_id);
		}
		
		if($eventType === 'refund.succeeded'){
			$this->update_refund_succeeded($checkout_id, $payment_id);
		}

	}
	
	public function update_payment_status($checkout_id, $payment_id){
		
		$args = array(
			'meta_key'   => 'kkart_yoco_order_checkout_id',
			'meta_value' => $checkout_id,
			'meta_compare' => "=",
		);
		
		$orders = kkart_get_orders($args);	
		
		if( empty( $orders ) ){
			return null;
		}
		
		$order = array_shift( $orders );
		$order_status = is_a( $order, KKART_Order::class ) ? $order : null;

		if( null === $order_status ){
			return new WP_REST_Response(
				array(
					'description' => sprintf( 'No order found for CheckoutId %s.', $checkout_id ),
				),
				404,
			);
		}

		if( true === $order_status->update_status( 'processing' ) ){
			$order->update_meta_data( 'kkart_yoco_order_payment_id', $payment_id );
			$order->save_meta_data();
			return new WP_REST_Response();
		}else{
			return new WP_REST_Response(
				array(
					'description' => sprintf( 'Failed to complete payment of order #%s.', $order->get_id() ),
				),
				500,
			);
		}
	}
	
	public function update_refund_succeeded($checkout_id, $payment_id){
		
		$args = array(
			'meta_key'   => 'kkart_yoco_order_checkout_id',
			'meta_value' => $checkout_id,
			'meta_compare' => "=",
		);
		
		$orders = kkart_get_orders($args);	
		
		if( empty( $orders ) ){
			return null;
		}
		
		$order = array_shift( $orders );
		$order_status = is_a( $order, KKART_Order::class ) ? $order : null;

		if( null ===  $order_status ){
			return new WP_REST_Response(
				array(
					'description' => sprintf( 'Could not find the order for checkout id %s.', $checkout_id ),
				),
				403,
			);
		}
		
		if( 'refunded' === $order_status->get_status() ){
			return new WP_REST_Response(
				array(
					'description' => sprintf( 'Order for checkout id %s is already refunded.', $checkout_id ),
				),
				403,
			);
		}
		
		try{
			$refund = $this->refund_amc($order_status);
			
			if( null === $refund ){
				return new WP_REST_Response();
			}
			
			if( 'completed' === $refund->get_status() ){
				$order_status->update_meta_data( 'kkart_yoco_order_payment_id', $payment_id );
				$order_status->save_meta_data();
				return new WP_REST_Response();
			}
			
			return new WP_REST_Response(
				array(
					'description' => sprintf( 'Failed to complete refund of order #%s - wrong order status.', $order_status->get_id() ),
				),
				403,
			);
			
		}catch (\Throwable $th){
			return new WP_REST_Response(
				array(
					'description' => sprintf('Could not find the order for checkout id %s.', $checkout_id),
				),
				403
			);
		}

	}
	
	public function update_refund_failed($checkout_id, $payment_id){
		
		$args = array(
			'meta_key'   => 'kkart_yoco_order_checkout_id',
			'meta_value' => $checkout_id,
			'meta_compare' => "=",
		);
		
		$orders = kkart_get_orders($args);	
		
		if( empty( $orders ) ){
			return null;
		}
		
		$order = array_shift( $orders );
		$order_status = is_a( $order, KKART_Order::class ) ? $order : null;

		if( null ===  $order_status ){
			return new WP_REST_Response(
				array(
					'description' => sprintf( 'Could not find the order for checkout id %s.', $checkout_id ),
				),
				403,
			);
		}
		
		if ( 'refunded' === $order_status->get_status() ) {
			return new WP_REST_Response(
				array(
					'description' => sprintf( 'Order for checkout id %s is already refunded.', $checkout_id ),
				),
				403,
			);
		}
		
		return new WP_REST_Response();
		
	}

	public function permit($request) {
		$headers = array(
			'webhook_id' => $request->get_header('webhook_id'),
			'webhook_timestamp' => $request->get_header('webhook_timestamp'),
			'webhook_signature' => $request->get_header('webhook_signature'),
		);

		return $this->validate($request->get_body(), $headers);
	}

	public function validate(string $payload, array $webhookHeaders){   
		try{
			$this->verify($payload, $webhookHeaders);

			return true;
		}catch(\Throwable $th){
			return false;
		}
	}

	public function verify($payload, $headers){
		
		if(
			!isset($headers['webhook_id']) ||
			!isset($headers['webhook_timestamp']) ||
			!isset($headers['webhook_signature'])
		){
			throw new Exception('Webhook Signature Validator is missing required headers');
		}
		
		$msgId = $headers['webhook_id'];
		$msgTimestamp = $headers['webhook_timestamp'];
		$msgSignature = $headers['webhook_signature'];
		$timestamp = $this->verifyTimestamp($msgTimestamp);
		$signature = $this->sign($msgId, $timestamp, $payload);
		$expectedSignature = explode(',', $signature, 2)[1];
		$passedSignatures = explode(' ', $msgSignature);
		
		foreach($passedSignatures as $versionedSignature){
			
			$sigParts = explode(',', $versionedSignature, 2);
			$version = $sigParts[0];
			$passedSignature = $sigParts[1];

			if(0 !== strcmp($version, 'v1')){
				continue;
			}

			if(hash_equals($expectedSignature, $passedSignature)){
				return json_decode($payload, true);
			}
		}
		
		throw new Exception('Webhook no matching signature found');
	}

	public function sign(string $msgId, int $timestamp, string $payload): string {
		
		if (!is_int($timestamp)) {
			throw new Exception('Invalid timestamp format');
		}
		
		$toSign = "{$msgId}.{$timestamp}.{$payload}";
		$secret = $this->secret();
		$hex_hash = hash_hmac('sha256', $toSign, $secret);
		$signature = base64_encode(pack('H*', $hex_hash)); 

		return "v1,{$signature}";
	}
	
	public function secret(){
		
		$settings = get_option( 'kkart_yoco_settings', null );  
		
		if( ! isset( $settings['mode'] ) ){
			return '';
		}
		
		$key = 'live' === $settings['mode'] ? 'yoco_payment_gateway_live_webhook_secret' : 'yoco_payment_gateway_test_webhook_secret';
		$SECRET_PREFIX = 'whsec_';
		$secret = get_option( $key, '' );
		
		if(substr($secret, 0, strlen($SECRET_PREFIX)) === $SECRET_PREFIX){ 
			$secret = substr($secret, strlen($SECRET_PREFIX));
			return base64_decode($secret);
		}
	}

	public function verifyTimestamp($timestampHeader): int{
		
		$now = time();
		$timestamp = intval($timestampHeader, 10);

		if ($timestamp < ($now - 5 * 60)) {
			throw new Exception('Webhook timestamp is too old');
		}

		if ($timestamp > ($now + 5 * 60)) {
			throw new Exception('Webhook timestamp is too new');
		}

		return $timestamp;
	}
	
	public function refund_amc($order){
		
		if( ! empty( $refunds = $order->get_refunds() ) ){
			return array_shift( $refunds );
		}
		
		$args = array(
			'amount' => $order->get_total(),
			'reason' => __( 'Refund requested via webhook.', 'kkart' ),
			'order_id' => $order->get_id(),
			'refund_payment_method' => 'yoco',
			'line_items' => $order->get_items(),
		);
		
		$refund = kkart_create_refund( apply_filters( 'yoco_payment_gateway/request/refund/args', $args ) );

		if( is_wp_error( $refund ) ){
			throw new Error( $refund->get_error_message(), (int) $refund->get_error_code() );
		}

		return $refund;
	}

}
?>
