<?php
/**
 * GGM Commerce Subscription Renewal Engine
 * WP-Cron based renewal processing for subscriptions.
 *
 * @package GGM_Commerce
 */

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Subscription renewal engine class
 */
class GGM_Commerce_Subscription_Renewal {

	/**
	 * Cron hook names
	 */
	const CRON_PROCESS_RENEWALS  = 'ggmc_process_subscription_renewals';
	const CRON_PROCESS_RETRIES   = 'ggmc_process_subscription_retries';
	const CRON_CHECK_EXPIRATIONS = 'ggmc_check_subscription_expirations';
	const CRON_CHECK_TRIALS      = 'ggmc_check_trial_expirations';

	/**
	 * Singleton instance
	 *
	 * @var GGM_Commerce_Subscription_Renewal
	 */
	private static $instance = null;

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

	/**
	 * Constructor — register cron hooks.
	 */
	private function __construct() {
		add_action( self::CRON_PROCESS_RENEWALS, array( $this, 'process_renewals' ) );
		add_action( self::CRON_PROCESS_RETRIES, array( $this, 'process_retries' ) );
		add_action( self::CRON_CHECK_EXPIRATIONS, array( $this, 'check_expirations' ) );
		add_action( self::CRON_CHECK_TRIALS, array( $this, 'check_trial_expirations' ) );
	}

	/**
	 * Schedule all cron events.
	 */
	public function schedule_all() {
		if ( ! wp_next_scheduled( self::CRON_PROCESS_RENEWALS ) ) {
			wp_schedule_event( time(), 'hourly', self::CRON_PROCESS_RENEWALS );
		}

		if ( ! wp_next_scheduled( self::CRON_PROCESS_RETRIES ) ) {
			wp_schedule_event( time(), 'twicedaily', self::CRON_PROCESS_RETRIES );
		}

		if ( ! wp_next_scheduled( self::CRON_CHECK_EXPIRATIONS ) ) {
			wp_schedule_event( time(), 'daily', self::CRON_CHECK_EXPIRATIONS );
		}

		if ( ! wp_next_scheduled( self::CRON_CHECK_TRIALS ) ) {
			wp_schedule_event( time(), 'hourly', self::CRON_CHECK_TRIALS );
		}
	}

	/**
	 * Unschedule all cron events.
	 */
	public function unschedule_all() {
		wp_clear_scheduled_hook( self::CRON_PROCESS_RENEWALS );
		wp_clear_scheduled_hook( self::CRON_PROCESS_RETRIES );
		wp_clear_scheduled_hook( self::CRON_CHECK_EXPIRATIONS );
		wp_clear_scheduled_hook( self::CRON_CHECK_TRIALS );
	}

	/**
	 * Process due subscription renewals (hourly).
	 *
	 * Finds active subscriptions where next_payment_date <= now,
	 * creates renewal orders, and charges the payment gateway.
	 */
	public function process_renewals() {
		$sub_manager = GGM_Commerce_Subscriptions::instance();
		$due         = $sub_manager->get_subscriptions_due_for_renewal();

		if ( empty( $due ) ) {
			return;
		}

		$orders_handler = GGM_Commerce_Orders::get_instance();

		foreach ( $due as $subscription ) {
			// Skip PayPal — PayPal handles its own renewals via webhooks.
			if ( 'paypal' === $subscription->payment_gateway ) {
				continue;
			}

			// Skip if manual renewal is required.
			if ( ! empty( $subscription->requires_manual_renewal ) ) {
				continue;
			}

			// Create a renewal order.
			$order_id = $orders_handler->create_renewal_order( $subscription );

			if ( is_wp_error( $order_id ) ) {
				$sub_manager->log_event(
					$subscription->id,
					'renewal_order_failed',
					'Failed to create renewal order: ' . $order_id->get_error_message(),
					null,
					null,
					'system'
				);
				continue;
			}

			// Charge via Stripe.
			if ( 'stripe' === $subscription->payment_gateway ) {
				$gateway = ggm_commerce_gateways()->get_gateway( 'stripe' );

				if ( ! $gateway || ! $gateway->is_available() ) {
					$sub_manager->log_event(
						$subscription->id,
						'renewal_charge_failed',
						'Stripe gateway not available for renewal.',
						$order_id,
						null,
						'system'
					);
					continue;
				}

				$result = $gateway->create_renewal_payment( $subscription, $order_id );

				if ( ! empty( $result['success'] ) && ! empty( $result['data']['paid'] ) ) {
					// Payment succeeded — update last_payment_date first so
					// calculate_next_payment_date uses it as the base date.
					$sub_manager->update_subscription( $subscription->id, array(
						'last_payment_date' => current_time( 'mysql', true ),
					) );
					$subscription = $sub_manager->get_subscription( $subscription->id );

					$next_payment = $sub_manager->calculate_next_payment_date( $subscription );

					$sub_manager->update_subscription( $subscription->id, array(
						'next_payment_date'  => $next_payment,
						'status'             => 'active',
					) );

					do_action( 'ggmc_subscription_renewal_succeeded', $subscription->id, $result );
				} else {
					// Payment failed — schedule retry.
					$sub_manager->update_subscription( $subscription->id, array(
						'status' => 'failed_payment',
					) );

					$sub_manager->schedule_retry( $subscription->id, 1 );

					do_action( 'ggmc_subscription_renewal_failed', $subscription->id, $result );
				}
			}
		}
	}

	/**
	 * Process scheduled payment retries (twice daily).
	 *
	 * Finds pending retries that are due, charges the gateway,
	 * and on exhausted retries puts subscription on hold.
	 */
	public function process_retries() {
		$sub_manager    = GGM_Commerce_Subscriptions::instance();
		$pending_retries = $sub_manager->get_pending_retries();

		if ( empty( $pending_retries ) ) {
			return;
		}

		$settings    = GGM_Commerce_Subscriptions::get_settings();
		$max_retries = $settings['max_retries'];

		$orders_handler = GGM_Commerce_Orders::get_instance();

		foreach ( $pending_retries as $retry ) {
			// Create a new renewal order for the retry.
			$subscription = $sub_manager->get_subscription( $retry->subscription_id );

			if ( ! $subscription || 'paypal' === $subscription->payment_gateway ) {
				$sub_manager->update_retry( $retry->id, array(
					'attempted_at' => current_time( 'mysql', true ),
					'result'       => 'skipped',
				) );
				continue;
			}

			$order_id = $orders_handler->create_renewal_order( $subscription );

			if ( is_wp_error( $order_id ) ) {
				$sub_manager->update_retry( $retry->id, array(
					'attempted_at' => current_time( 'mysql', true ),
					'result'       => 'failed',
					'error_message' => $order_id->get_error_message(),
				) );
				continue;
			}

			$success = false;

			if ( 'stripe' === $subscription->payment_gateway ) {
				$gateway = ggm_commerce_gateways()->get_gateway( 'stripe' );

				if ( $gateway && $gateway->is_available() ) {
					$result  = $gateway->create_renewal_payment( $subscription, $order_id );
					$success = ! empty( $result['success'] ) && ! empty( $result['data']['paid'] );
				}
			}

			$sub_manager->update_retry( $retry->id, array(
				'attempted_at' => current_time( 'mysql', true ),
				'result'       => $success ? 'success' : 'failed',
				'order_id'     => $order_id,
			) );

			if ( $success ) {
				// Retry succeeded — reactivate subscription.
				$sub_manager->update_subscription( $subscription->id, array(
					'last_payment_date' => current_time( 'mysql', true ),
				) );
				$subscription = $sub_manager->get_subscription( $subscription->id );

				$next_payment = $sub_manager->calculate_next_payment_date( $subscription );

				$sub_manager->update_subscription( $subscription->id, array(
					'next_payment_date'  => $next_payment,
					'status'             => 'active',
				) );

				$sub_manager->log_event(
					$subscription->id,
					'retry_succeeded',
					sprintf( 'Payment retry #%d succeeded.', $retry->retry_number ),
					$order_id,
					$subscription->recurring_total,
					'system'
				);

				do_action( 'ggmc_subscription_renewal_succeeded', $subscription->id );
			} else {
				// Retry failed — check if we should schedule another or put on hold.
				if ( (int) $retry->retry_number >= $max_retries ) {
					$sub_manager->hold_subscription( $subscription->id, 'All payment retries exhausted.' );
				} else {
					$sub_manager->schedule_retry( $subscription->id, (int) $retry->retry_number + 1 );
				}

				do_action( 'ggmc_subscription_renewal_failed', $subscription->id );
			}
		}
	}

	/**
	 * Check for expired subscriptions (daily).
	 *
	 * Finds subscriptions past their end_date or on_hold past grace period,
	 * and transitions them to expired/cancelled.
	 */
	public function check_expirations() {
		global $wpdb;

		$sub_manager = GGM_Commerce_Subscriptions::instance();
		$settings    = GGM_Commerce_Subscriptions::get_settings();
		$grace_days  = $settings['grace_period'];

		// Expire subscriptions past their end date.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$expired = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id FROM {$wpdb->prefix}ggmc_subscriptions
				 WHERE status IN ('active', 'trial')
				 AND end_date IS NOT NULL
				 AND end_date != '0000-00-00 00:00:00'
				 AND end_date <= %s",
				current_time( 'mysql', true )
			)
		);

		foreach ( $expired as $sub ) {
			$sub_manager->expire_subscription( $sub->id );
		}

		// Cancel on_hold subscriptions past the grace period.
		if ( $grace_days > 0 ) {
			$grace_cutoff = gmdate( 'Y-m-d H:i:s', strtotime( "-{$grace_days} days" ) );

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$on_hold_expired = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT id FROM {$wpdb->prefix}ggmc_subscriptions
					 WHERE status = 'on_hold'
					 AND updated_at <= %s",
					$grace_cutoff
				)
			);

			foreach ( $on_hold_expired as $sub ) {
				$sub_manager->cancel_subscription( $sub->id, 'Grace period expired after payment failure.', 'system' );
			}
		}

		// Handle pending_cancel subscriptions that have reached their end date.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$pending_cancel = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id FROM {$wpdb->prefix}ggmc_subscriptions
				 WHERE status = 'pending_cancel'
				 AND next_payment_date IS NOT NULL
				 AND next_payment_date != '0000-00-00 00:00:00'
				 AND next_payment_date <= %s",
				current_time( 'mysql', true )
			)
		);

		foreach ( $pending_cancel as $sub ) {
			$sub_manager->cancel_subscription( $sub->id, 'Scheduled cancellation at end of billing period.', 'system' );
		}
	}

	/**
	 * Check for trial expirations (hourly).
	 *
	 * Transitions trial subscriptions to their first billing cycle
	 * when the trial end date has passed.
	 */
	public function check_trial_expirations() {
		global $wpdb;

		$sub_manager = GGM_Commerce_Subscriptions::instance();

		// Send "trial ending soon" notifications (3 days before expiration).
		$three_days_from_now = gmdate( 'Y-m-d H:i:s', strtotime( '+3 days' ) );
		$now_utc             = current_time( 'mysql', true );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$ending_soon = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, trial_end_date FROM {$wpdb->prefix}ggmc_subscriptions
				 WHERE status = 'trial'
				 AND trial_end_date IS NOT NULL
				 AND trial_end_date != '0000-00-00 00:00:00'
				 AND trial_end_date > %s
				 AND trial_end_date <= %s",
				$now_utc,
				$three_days_from_now
			)
		);

		foreach ( $ending_soon as $sub ) {
			// Check if we already sent a trial-ending email (avoid duplicates).
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$already_notified = $wpdb->get_var( $wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->prefix}ggmc_subscription_log
				 WHERE subscription_id = %d AND event_type = 'trial_ending_notice'",
				$sub->id
			) );

			if ( ! $already_notified ) {
				$days_remaining = max( 1, (int) ceil( ( strtotime( $sub->trial_end_date ) - time() ) / DAY_IN_SECONDS ) );
				do_action( 'ggmc_subscription_trial_ending', $sub->id, $days_remaining );

				$sub_manager->log_event(
					$sub->id,
					'trial_ending_notice',
					sprintf( 'Trial ending notification sent. %d day(s) remaining.', $days_remaining ),
					null,
					null,
					'system'
				);
			}
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$expired_trials = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}ggmc_subscriptions
				 WHERE status = 'trial'
				 AND trial_end_date IS NOT NULL
				 AND trial_end_date != '0000-00-00 00:00:00'
				 AND trial_end_date <= %s",
				current_time( 'mysql', true )
			)
		);

		foreach ( $expired_trials as $subscription ) {
			$sub_manager->log_event(
				$subscription->id,
				'trial_ended',
				'Free trial period ended. First billing cycle begins.',
				null,
				null,
				'system'
			);

			// Calculate next payment date from now.
			$next_payment = $sub_manager->calculate_next_payment_date(
				$subscription->billing_period,
				$subscription->billing_interval
			);

			// For Stripe: charge the first payment immediately.
			if ( 'stripe' === $subscription->payment_gateway && ! empty( $subscription->payment_method_token ) ) {
				$orders_handler = GGM_Commerce_Orders::get_instance();
				$order_id       = $orders_handler->create_renewal_order( $subscription );

				if ( ! is_wp_error( $order_id ) ) {
					$gateway = ggm_commerce_gateways()->get_gateway( 'stripe' );

					if ( $gateway && $gateway->is_available() ) {
						$result = $gateway->create_renewal_payment( $subscription, $order_id );

						if ( ! empty( $result['success'] ) && ! empty( $result['data']['paid'] ) ) {
							$sub_manager->update_subscription( $subscription->id, array(
								'status'             => 'active',
								'last_payment_date'  => current_time( 'mysql', true ),
								'next_payment_date'  => $next_payment,
							) );

							do_action( 'ggmc_subscription_trial_ended', $subscription->id, true );
							continue;
						}
					}
				}

				// First payment failed.
				$sub_manager->update_subscription( $subscription->id, array(
					'status'            => 'failed_payment',
					'next_payment_date' => $next_payment,
				) );

				$sub_manager->schedule_retry( $subscription->id, 1 );
				do_action( 'ggmc_subscription_trial_ended', $subscription->id, false );
			} else {
				// PayPal handles its own billing cycle transitions.
				// Just update status.
				$sub_manager->update_subscription( $subscription->id, array(
					'status'            => 'active',
					'next_payment_date' => $next_payment,
				) );

				do_action( 'ggmc_subscription_trial_ended', $subscription->id, true );
			}
		}
	}
}
