<?php
/**
 * Masteriyo Mollie setup.
 *
 * @package Masteriyo\Addons\Mollie
 *
 * @since 1.16.0 [Free]
 */
namespace Masteriyo\Addons\Mollie;

use Mollie\Api\MollieApiClient;

use Exception;
use Masteriyo\Addons\CourseBundle\Models\OrderItemCourseBundle;
use Masteriyo\Enums\OrderItemType;
use Masteriyo\Enums\OrderStatus;
use Masteriyo\Pro\Enums\SubscriptionStatus;
use Masteriyo\Pro\Models\Subscription;
use Mollie\Api\Exceptions\ApiException;

defined( 'ABSPATH' ) || exit;
/**
 * Main Masteriyo Mollie class.
 *
 * @class Masteriyo\Addons\Mollie
 */
class MollieAddon {
	/**
	 * Instance
	 *
	 * @since 1.16.0 [Free]
	 *
	 * @var \Masteriyo\Addons\Mollie\MollieAddon
	 */
	protected static $instance = null;

	/**
	 * Constructor.
	 *
	 * @since 1.16.0 [Free]
	 */
	private function __construct() {
	}

	/**
	 * Return the instance.
	 *
	 * @since 1.16.0 [Free]
	 *
	 * @return \Masteriyo\Addons\Mollie\MollieAddon
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Initialize module.
	 *
	 * @since 1.16.0 [Free]
	 */
	public function init() {
		$this->init_hooks();
	}

	/**
	 * Initialize hooks.
	 *
	 * @since 1.16.0 [Free]
	 */
	public function init_hooks() {
		add_filter( 'masteriyo_rest_response_setting_data', array( $this, 'append_setting_in_response' ), 10, 4 );
		add_action( 'masteriyo_new_setting', array( $this, 'save_mollie_settings' ), 10, 1 );

		add_filter( 'masteriyo_payment_gateways', array( $this, 'add_payment_gateway' ), 11, 1 );
		add_action( 'wp_ajax_masteriyo_mollie_webhook', array( $this, 'handle_webhook' ) );
		add_action( 'wp_ajax_nopriv_masteriyo_mollie_webhook', array( $this, 'handle_webhook' ) );

	}

	/**
	 * Handle the webhook request from Mollie.
	 *
	 *  @since 1.16.0 [Free]
	 *
	 * This method processes incoming webhook notifications from Mollie.
	 * It retrieves payment information and updates the order status accordingly.
	 */
	public function handle_webhook() {
		try {
			masteriyo_get_logger()->info( 'Mollie webhook processing started', array( 'source' => 'payment-mollie' ) );
			$payload = @file_get_contents( 'php://input' );

			if ( empty( $payload ) ) {
				masteriyo_get_logger()->error( 'Empty payload received', array( 'source' => 'payment-mollie' ) );
				throw new Exception( 'Payload is empty.', 400 );
			}

			parse_str( $payload, $data );

			if ( empty( $data ) || ! is_array( $data ) ) {
				throw new Exception( 'Invalid payload format.', 400 );
			}

			if ( ! isset( $data['id'] ) ) {
				masteriyo_get_logger()->error( 'Missing payment ID in payload', array( 'source' => 'payment-mollie' ) );
				http_response_code( 400 );
				exit;
			}

			$secret = masteriyo_mollie_get_api_key();
			if ( empty( $secret ) ) {
				throw new Exception( 'Mollie API key is not configured.', 500 );
			}
			$mollie = new MollieApiClient();

			try {
				$mollie->setApiKey( $secret );
				$payment = $mollie->payments->get( $data['id'] );
			} catch ( ApiException $e ) {
					masteriyo_get_logger()->error( 'Mollie API error: ' . $e->getMessage(), array( 'source' => 'payment-mollie' ) );
					throw new Exception( 'Failed to retrieve payment information.', 502 );
			}

			if ( ! $payment || ! isset( $payment->status ) ) {
				throw new Exception( 'Invalid payment data received from Mollie.', 400 );
			}

			if ( ! isset( $payment->metadata ) ) {
				throw new Exception( __( 'Metadata is missing.', 'learning-management-system' ) );
			}

			$order_id = isset( $payment->metadata->order_id ) ? $payment->metadata->order_id : null;

			if ( ! $order_id || $order_id <= 0 ) {
				throw new Exception( __( 'Invalid order ID in payment metadata.', 'learning-management-system' ) );
			}

			$order = masteriyo_get_order( $order_id );

			if ( ! $order ) {
				throw new Exception( __( 'Order not found.', 'learning-management-system' ) );
			}

			$payment_type = $payment->metadata->payment_type ?? 'one-time';

			if ( 'one-time' === $payment_type ) {
				$this->process_one_time_payment( $order, $payment );
			}

			if ( 'recurring' === $payment_type ) {
				$this->process_subscription_payment( $order, $payment, $mollie );
			}

			masteriyo_get_logger()->info( 'Mollie webhook processing completed', array( 'source' => 'payment-mollie' ) );
			http_response_code( 200 );
		} catch ( Exception $e ) {
			masteriyo_get_logger()->error( $e->getMessage(), array( 'source' => 'payment-mollie' ) );
			wp_send_json_error( array( 'message' => $e->getMessage() ), $e->getCode() );
		}
	}

	/**
	 * Process one-time payment and update the order status.
	 *
	 * Checks the payment status and updates the order to completed, refunded, or other statuses.
	 *
	 * @since 1.16.0 [Free]
	 *
	 * @param Order   $order   Order object.
	 * @param Payment $payment Mollie payment object.
	 */
	private function process_one_time_payment( $order, $payment ) {
		if ( $payment->isPaid() ) {
			if ( $payment->amountRefunded && $payment->amount->value === $payment->amountRefunded->value ) {
					$order->set_status( OrderStatus::REFUNDED );
			} else {
					$order->set_status( OrderStatus::COMPLETED );
			}
		} elseif ( 'expired' === $payment->status ) {
			$order->set_status( OrderStatus::FAILED );
		} elseif ( 'failed' === $payment->status ) {
			$order->set_status( OrderStatus::FAILED );
		} elseif ( 'canceled' === $payment->status ) {
			$order->set_status( OrderStatus::CANCELLED );
		}
		$order->save();
	}

	/**
	 * Process subscription payment and update the order and subscription status.
	 *
	 * Handles the creation of new subscriptions, updates status on payment events,
	 * and manages cancellation or refund for subscription payments.
	 *
	 * @since 1.16.0
	 *
	 * @param Order   $order   Order object.
	 * @param Payment $payment Mollie payment object.
	 * @param MollieApiClient $mollie Mollie API client.
	 */
	private function process_subscription_payment( $order, $payment, $mollie ) {
		$customer_id = $payment->customerId ?? null;
		if ( ! $customer_id ) {
			throw new Exception( 'Customer ID missing in payment data.' );
		}

		$subscription_id = $order->get_meta( 'mollie_subscription_id', true );

		if ( ! $subscription_id && 'paid' === $payment->status ) {
			$customer          = $mollie->customers->get( $customer_id );
			$subscription_data = $this->get_subscription_data( $order, $payment );
			try {
				$mollie_subscription = $customer->createSubscription( $subscription_data );
			} catch ( ApiException $e ) {
				masteriyo_get_logger()->error( 'Failed to create subscription: ' . $e->getMessage() );
			}
			$subscription = $this->create_subscription( $order, $mollie_subscription );
			$order->update_meta_data( 'mollie_subscription_id', $mollie_subscription->id );
			$order->update_meta_data( 'subscription_id', $subscription->get_id() );
			$order->save_meta_data();

			masteriyo_get_logger()->info( 'New subscription created: ' . $mollie_subscription->id, array( 'source' => 'payment-mollie' ) );
		}

		if ( $payment->isPaid() ) {
			if ( $payment->amountRefunded && $payment->amountRefunded->value > 0 ) {
				$order->set_status( OrderStatus::REFUNDED );
				$order_subscription_id = $order->get_meta( 'subscription_id', true );
				if ( $subscription_id ) {
					$this->change_subscription_status( SubscriptionStatus::CANCELLED, $order_subscription_id );
				}
			} else {
				$order->set_status( OrderStatus::COMPLETED );
				masteriyo_get_logger()->info( 'Recurring payment successful for order: ' . $order->get_id(), array( 'source' => 'payment-mollie' ) );
			}
		} elseif ( 'expired' === $payment->status ) {
			$order->set_status( OrderStatus::CANCELLED );
			$order_subscription_id = $order->get_meta( 'subscription_id', true );
			if ( $subscription_id ) {
				$this->change_subscription_status( SubscriptionStatus::EXPIRED, $order_subscription_id );
			}
		} elseif ( 'failed' === $payment->status ) {
			$order->set_status( OrderStatus::FAILED );
			$order_subscription_id = $order->get_meta( 'subscription_id', true );
			if ( $subscription_id ) {
				$this->change_subscription_status( SubscriptionStatus::CANCELLED, $order_subscription_id );
			}
		} elseif ( 'canceled' === $payment->status ) {
			$order->set_status( OrderStatus::CANCELLED );
			$order_subscription_id = $order->get_meta( 'subscription_id', true );
			if ( $subscription_id ) {
				$this->change_subscription_status( SubscriptionStatus::FAILED, $order_subscription_id );
			}
		}
		$order->save();
	}

	/**
	 * Change subscription status of subscription of order.
	 *
	 * @since 2.17.0
	 *
	 * @param string $status
	 * @param string $order_subscription_id
	 * @return void
	 */
	public function change_subscription_status( $status, $order_subscription_id ) {
		/** @var \Masteriyo\Pro\Models\Subscription */
		$existing_subscription = masteriyo( 'subscription' );
		$existing_subscription->set_id( $order_subscription_id );
		/** @var \Masteriyo\Pro\Repository\SubscriptionRepository */
		$subscription_repo = masteriyo( 'subscription.store' );
		$subscription_repo->read( $existing_subscription );

		$existing_subscription->set_status( $status );
		$existing_subscription->save();
	}


	/**
	 * Get the subscription data for creating or updating a subscription.
	 *
	 * Constructs and returns the subscription data array with metadata and billing information.
	 *
	 * @since 1.16.0
	 *
	 * @param Order   $order   Order object.
	 * @param Payment $payment Mollie payment object.
	 * @return array Subscription data array.
	 */
	private function get_subscription_data( $order, $payment ) {
		$order_items = $order->get_items();
		if ( ! empty( $order_items[0] ) && $order_items[0] instanceof OrderItemCourseBundle ) {
			$courses          = $order_items;
			$course_bundle_id = $order_items[0]->get_course_bundle_id();
			$first_course     = masteriyo_get_course_bundle( $course_bundle_id );
		} else {
			$courses      = array_map(
				function( $order_item ) {
					return $order_item->get_course();
				},
				$order_items
			);
			$first_course = current( $courses );
		}

		$receipt_id = get_bloginfo( 'admin_email' );

		$billing_expire_after = $first_course->get_billing_expire_after();
		$billing_interval     = $first_course->get_billing_interval();
		$billing_period       = $first_course->get_billing_period();

		$times = null;
		switch ( $billing_period ) {
			case 'day':
					$times = $billing_expire_after > 0 ? ceil( ( $billing_expire_after * 30 ) / $billing_interval ) : null;
				break;
			case 'week':
					$times = $billing_expire_after > 0 ? ceil( ( $billing_expire_after * 4 ) / $billing_interval ) : null;
				break;
			case 'month':
					$times = $billing_expire_after > 0 ? ceil( $billing_expire_after / $billing_interval ) : null;
				break;
			case 'year':
					$times = $billing_expire_after > 0 ? ceil( $billing_expire_after / ( $billing_interval * 12 ) ) : null;
				break;
		}

		return array(
			'amount'      => array(
				'currency' => $order->get_currency(),
				'value'    => number_format( $order->get_total(), 2, '.', '' ),
			),
			'interval'    => "{$billing_interval} {$billing_period}",
			/* translators: %s: order id */
			'description' => sprintf( __( 'Subscription for Order #%s', 'learning-management-system' ), $order->get_id() ),
			'times'       => $times,
			'webhookUrl'  => home_url( 'wp-admin/admin-ajax.php?action=masteriyo_mollie_webhook' ),
			'metadata'    => array(
				'order_id'     => $order->get_id(),
				'payment_type' => 'recurring',
				'course_id'    => $first_course->get_id(),
				'receipt'      => $receipt_id,
			),
		);
	}

	/**
	 * Get the return url (thank you page).
	 *
	 * @since 1.16.0 [Free]
	 *
	 * @param Order|null $order Order object.
	 * @return string
	 */
	public function get_return_url( $order = null ) {
		if ( $order ) {
			$return_url = $order->get_checkout_order_received_url();
		} else {
			$return_url = masteriyo_get_endpoint_url( 'order-received', '', masteriyo_get_checkout_url() );
		}

		/**
		 * Filters return URL for a payment gateway.
		 *
		 * @since 1.16.0 [Free]
		 *
		 * @param string $return_url The return URL.
		 * @param Masteriyo\Models\Order\Order|null $order The order object.
		 */
		return apply_filters( 'masteriyo_get_return_url', $return_url, $order );
	}

	/**
	 * Create subscription model.
	 *
	 * @since 1.16.0
	 * @param \Masteriyo\Models\Order\Order $order
	 * @param \Mollie\Subscription $mollie_subscription
	 */
	public function create_subscription( $order, $mollie_subscription ) {
		masteriyo_get_logger()->info( 'Mollie create_subscription: Start', array( 'source' => 'payment-mollie' ) );
		$order_data = $order->get_data();
		unset( $order_data['id'] );

		$subscription = Subscription::instance();
		$subscription->set_props( $order_data );

		$order_item = current( $order->get_items() );
		if ( $order_item instanceof OrderItemCourseBundle ) {
			$course = masteriyo_get_course_bundle( $order_item->get_course_bundle_id() );
		} else {
			$course = masteriyo_get_course( $order_item->get_course_id() );
		}

		$subscription->set_props(
			array(
				'billing_period'          => $course->get_billing_period(),
				'billing_interval'        => $course->get_billing_interval(),
				'billing_expire_after'    => $course->get_billing_expire_after(),
				'requires_manual_renewal' => false,
				'status'                  => SubscriptionStatus::ACTIVE,
				'parent_id'               => $order->get_id(),
				'subscription_id'         => $mollie_subscription->id,
				'recurring_amount'        => $order->get_total(),
			)
		);

		foreach ( $order->get_items( OrderItemType::all() ) as $order_item ) {
			$subscription->add_item( $order_item );
		}

		$subscription->save();

		masteriyo_get_logger()->info( 'Mollie create_subscription: Success', array( 'source' => 'payment-mollie' ) );

		return $subscription;
	}

	/**
	 * Append setting to response.
	 *
	 * @since 1.16.0 [Free]
	 *
	 * @param array $data Setting data.
	 * @param \Masteriyo\Models\Setting $setting Setting object.
	 * @param string $context What the value is for. Valid values are view and edit.
	 * @param \Masteriyo\RestApi\Controllers\Version1\SettingsController $controller REST settings controller object.
	 *
	 * @return array
	 */
	public function append_setting_in_response( $data, $setting, $context, $controller ) {
		$data['payments']['mollie'] = Setting::all();

		return $data;
	}

	/**
	 * Save global Mollie settings.
	 *
	 * @since 1.16.0 [Free]
	 *
	 * @param \Masteriyo\Models\Setting $setting Setting object.
	 */
	public function save_mollie_settings( $setting ) {
		$request = masteriyo_current_http_request();

		if ( ! masteriyo_is_rest_api_request() || ! isset( $request['payments']['mollie'] ) ) {
			return;
		}

		$current_settings = Setting::all();
		$new_settings     = masteriyo_array_only( $request['payments']['mollie'], array_keys( $current_settings ) );
		$new_settings     = masteriyo_parse_args( $new_settings, $current_settings );

		$new_settings['enable']       = masteriyo_string_to_bool( $new_settings['enable'] );
		$new_settings['title']        = sanitize_text_field( $new_settings['title'] );
		$new_settings['sandbox']      = masteriyo_string_to_bool( $new_settings['sandbox'] );
		$new_settings['description']  = sanitize_textarea_field( $new_settings['description'] );
		$new_settings['test_api_key'] = sanitize_text_field( $new_settings['test_api_key'] );
		$new_settings['live_api_key'] = sanitize_text_field( $new_settings['live_api_key'] );

		if ( $this->api_key_changed( $current_settings, $new_settings ) ) {
			$new_settings['error_message'] = $this->validate_mollie_keys( $new_settings );
		} else {
			$new_settings['error_message'] = $current_settings['error_message'] ?? '';
		}

		Setting::set_props( $new_settings );

		Setting::save();
	}

	/**
	 * Add Mollie payment gateway to available payment gateways.
	 *
	 * @since 1.16.0 [Free]
	 *
	 * @param Masteriyo\Abstracts\PaymentGateway[]
	 *
	 * @return Masteriyo\Abstracts\PaymentGateway[]
	 */
	public function add_payment_gateway( $gateways ) {
		$gateways[] = Mollie::class;

		return $gateways;
	}

	/**
	 * Validate Mollie API keys based on settings.
	 *
	 * @since 1.16.0 [Free]
	 *
	 * @param array $settings Mollie settings array.
	 * @return string Error message if validation fails, or an empty string if successful.
	 */
	private function validate_mollie_keys( $settings ) {
		$mollie = new MollieApiClient();

		try {
			if ( $settings['enable'] ) {
					$api_key = $settings['sandbox'] ? $settings['test_api_key'] : $settings['live_api_key'];
					$mollie->setApiKey( $api_key );
					$mollie->profiles->getCurrent();

					return '';
			}
		} catch ( \Mollie\Api\Exceptions\ApiException $e ) {
			return $settings['sandbox']
					? 'Invalid Test API key. Please verify that your Mollie API credentials are correct and save the settings again.'
					: 'Invalid Live API key. Please verify that your Mollie API credentials are correct and save the settings again.';
		}

		return '';
	}

	/**
	 * Determine if relevant Mollie API key settings have changed.
	 *
	 * @since 1.16.0 [Free]
	 *
	 * @param array $current_settings The current stored settings.
	 * @param array $new_settings The new settings from the request.
	 * @return bool True if any API key or sandbox mode has changed, false otherwise.
	 */
	private function api_key_changed( $current_settings, $new_settings ) {
		return $current_settings['sandbox'] !== $new_settings['sandbox'] ||
		$current_settings['test_api_key'] !== $new_settings['test_api_key'] ||
		$current_settings['live_api_key'] !== $new_settings['live_api_key'];
	}

}
