<?php

/**
 * Renewal Processor
 *
 * Main service for processing subscription renewals.
 *
 * @package ArraySubs\Features\RecurringBilling\Services
 * @since 1.0.0
 */

namespace ArraySubs\Features\RecurringBilling\Services;

defined('ABSPATH') || exit;

use ArraySubs\Features\RecurringBilling\Services\OrderCreation;
use ArraySubs\Features\RecurringBilling\Services\PaymentProcessor;
use ArraySubs\Features\RecurringBilling\Services\ExpirationChecker;
use ArraySubs\Features\RecurringBilling\Services\RenewalScheduler;
use ArraySubs\Supports\ActionScheduler;

/**
 * Renewal processor service
 */
class RenewalProcessor
{
	/**
	 * Create a renewal invoice (pending order) for a subscription
	 * This is called 6 hours before the subscription is due
	 *
	 * @param int $subscription_id Subscription ID
	 * @return bool True on success, false on failure
	 */
	public function createRenewalInvoice(int $subscription_id): bool
	{
		// Get subscription
		$subscription = arraysubs_get_subscription($subscription_id);

		if (!$subscription) {
			return false;
		}

		// Lifetime subscriptions never generate renewal invoices
		if (arraysubs_is_lifetime_subscription($subscription_id)) {
			return false;
		}

		// Validate subscription is active or trial
		$status = get_post_status($subscription_id);
		if (!in_array($status, ['arraysubs-active', 'arraysubs-trial'], true)) {
			return false;
		}

		// Pending-cancel guard. The bulk renewal cron and the per-sub
		// invoice scheduler both gate on the
		// arraysubs_should_generate_renewal_invoice filter, but
		// createRenewalInvoice() is also called directly by callers
		// outside the renewal pipeline (admin tools, plan-switch flow,
		// process() below as a fallback when no pending order exists).
		// Refusing here blocks every entry point uniformly.
		if (arraysubs_is_waiting_cancellation($subscription_id)) {
			return false;
		}

		// CRITICAL: Check if a pending renewal order already exists to prevent duplicate orders
		if ($this->hasPendingRenewalOrder($subscription_id)) {
			return false;
		}

		// Create renewal order
		$order_creator = new OrderCreation();
		$order = $order_creator->createRenewalOrder($subscription_id);

		if (!$order) {
			return false;
		}

		// Add order to subscription history
		arraysubs_add_subscription_order($subscription_id, $order->get_id());

		// IMPORTANT: Do NOT change subscription status here!
		// The subscription should remain ACTIVE during the grace period.
		// Status changes are handled by checkOverdueRenewals based on grace period constants:
		// - GRACE_DAYS_BEFORE_ON_HOLD: days after due date that subscription stays active
		// - GRACE_DAYS_BEFORE_CANCEL: days after on-hold before cancellation
		// This allows customers time to pay without immediately losing access.

		// Store the pending renewal order ID
		update_post_meta($subscription_id, '_pending_renewal_order_id', $order->get_id());

		// Trigger action for email notifications
		do_action('arraysubs_renewal_invoice_created', $subscription_id, $order->get_id());

		return true;
	}

	/**
	 * Cancel subscription due to overdue unpaid invoice
	 *
	 * @param int $subscription_id Subscription ID
	 * @return bool True on success
	 */
	public function cancelOverdueSubscription(int $subscription_id): bool
	{
		// Update subscription status to cancelled
		wp_update_post([
			'ID' => $subscription_id,
			'post_status' => 'arraysubs-cancelled',
		]);

		$subscription = arraysubs_get_subscription($subscription_id);
		if (!$subscription instanceof \WP_Post) {
			return false;
		}

		// Auto-downgrade may immediately reactivate the subscription while the
		// status transition is being processed. Avoid clearing the new schedule.
		if ($subscription->post_status !== 'arraysubs-cancelled') {
			return true;
		}

		// Set end date
		update_post_meta($subscription_id, '_end_date', current_time('mysql', true));
		update_post_meta($subscription_id, '_cancelled_date', current_time('mysql', true));
		update_post_meta($subscription_id, '_cancelled_by', 'system');
		update_post_meta($subscription_id, '_cancellation_reason', 'overdue_payment');

		// Cancel any pending renewal orders
		$this->cancelPendingRenewalOrders($subscription_id);

		// Unschedule any future renewals
		RenewalScheduler::unschedule($subscription_id);

		// Trigger action for email notifications
		do_action('arraysubs_data_cancelled_overdue', $subscription_id);

		return true;
	}

	/**
	 * Cancel pending renewal orders for a subscription
	 *
	 * @param int $subscription_id Subscription ID
	 * @return void
	 */
	private function cancelPendingRenewalOrders(int $subscription_id): void
	{
		// For HPOS compatibility
		if (
			class_exists('Automattic\WooCommerce\Utilities\OrderUtil') &&
			\Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled()
		) {
			$orders = wc_get_orders([
				'limit'      => -1,
				'status'     => ['pending', 'on-hold'],
				'meta_query' => [
					'relation' => 'AND',
					[
						'key'   => '_is_renewal_order',
						'value' => 'yes',
					],
					[
						'key'   => '_subscription_id',
						'value' => $subscription_id,
					],
				],
			]);
		} else {
			$order_posts = get_posts([
				'post_type'      => 'shop_order',
				'post_status'    => ['wc-pending', 'wc-on-hold'],
				'posts_per_page' => -1,
				'meta_query'     => [
					'relation' => 'AND',
					[
						'key'   => '_is_renewal_order',
						'value' => 'yes',
					],
					[
						'key'   => '_subscription_id',
						'value' => $subscription_id,
					],
				],
			]);
			$orders = array_map('wc_get_order', wp_list_pluck($order_posts, 'ID'));
		}

		foreach ($orders as $order) {
			if ($order) {
				$order->update_status('cancelled', __('Subscription cancelled due to non-payment.', 'arraysubs'));
			}
		}
	}

	/**
	 * Process a subscription renewal at the scheduled due time.
	 *
	 * @param int $subscription_id Subscription ID
	 * @return bool True on success, false on failure
	 */
	public function process(int $subscription_id): bool
	{
		// Get subscription
		$subscription = arraysubs_get_subscription($subscription_id);

		if (!$subscription) {
			return false;
		}

		// Validate subscription is active, trial, or on-hold (retry)
		$status = get_post_status($subscription_id);
		if (!in_array($status, ['arraysubs-active', 'arraysubs-trial', 'arraysubs-on-hold'], true)) {
			return false;
		}

		// Pending-cancel guard. arraysubs_schedule_cancellation()
		// unschedules the queued HOOK_PROCESS_RENEWAL job, but a job
		// already firing when the cancel is scheduled, or one queued by
		// external code, could still reach this method. Refusing to
		// charge protects every gateway uniformly — this is the last
		// gate before PaymentProcessor calls into Stripe/PayPal/Paddle.
		if (arraysubs_is_waiting_cancellation($subscription_id)) {
			return false;
		}

		// Check if renewal is due
		if (!$this->isRenewalDue($subscription)) {
			return false;
		}

		$order = $this->getPendingRenewalOrder($subscription_id);
		$is_new_order = false;

		if (!$order) {
			$order_creator = new OrderCreation();
			$order = $order_creator->createRenewalOrder($subscription_id);
			$is_new_order = (bool) $order;
		}

		if (!$order) {
			return $this->handleFailure($subscription_id, __('Failed to create renewal order', 'arraysubs'));
		}

		if ($is_new_order) {
			arraysubs_add_subscription_order($subscription_id, $order->get_id());
			update_post_meta($subscription_id, '_pending_renewal_order_id', $order->get_id());
			do_action('arraysubs_renewal_invoice_created', $subscription_id, $order->get_id());
		}

		// Pre-retry charge verification: if this is a retry attempt, confirm
		// with the gateway that the customer was not already charged by a
		// missed webhook. The filter delegates to the gateway adapter; default
		// is a no-op for gateways that don't implement it.
		$retry_attempts = arraysubs_get_payment_retry_attempts($subscription_id);
		if ($retry_attempts > 0) {
			$verification = apply_filters(
				'arraysubs_pre_retry_charge_verification',
				['found' => false, 'charged' => false, 'transaction_id' => '', 'message' => '', 'raw' => []],
				$subscription_id,
				$order
			);

			if (! empty($verification['charged'])) {
				$transaction_id = (string) ($verification['transaction_id'] ?? '');
				$order->add_order_note(sprintf(
					/* translators: %s: transaction id */
					__('Renewal payment was already charged at the gateway (transaction %s); a webhook was missed. Marking order as paid without re-charging.', 'arraysubs'),
					$transaction_id
				));
				$order->payment_complete($transaction_id);
				arraysubs_add_subscription_note(
					$subscription_id,
					sprintf(
						/* translators: 1: order id, 2: transaction id */
						__('Pre-retry verification found order #%1$d was already charged at the gateway (%2$s). Reconciled without re-charging.', 'arraysubs'),
						$order->get_id(),
						$transaction_id
					),
					'private',
					0,
					[],
					'subscription'
				);
				arraysubs_reset_payment_retry_attempts($subscription_id);
				return true;
			}
		}

		// Process payment (automatic when available, manual invoice otherwise)
		$payment_processor = new PaymentProcessor();
		$result = $payment_processor->processPayment($subscription_id, $order);

		if ($order->is_paid() || 'paid' === ($result['status'] ?? '')) {
			arraysubs_reset_payment_retry_attempts($subscription_id);
			return true;
		}

		if (in_array(($result['status'] ?? ''), ['manual_required', 'pending', 'requires_action'], true)) {
			return $this->handleManualPaymentPending($subscription_id, $order, $result);
		}

		return $this->handlePaymentAttemptFailure(
			$subscription_id,
			$order,
			$result['message'] ?? __('Renewal payment failed.', 'arraysubs'),
			(string) ($result['error_code'] ?? '')
		);
	}

	/**
	 * Manually retry a failed renewal, initiated by an admin or the customer
	 * via REST. Wraps {@see self::process()} with extra preflight checks and
	 * returns a structured result for the caller.
	 *
	 * @param int    $subscription_id Subscription ID.
	 * @param string $initiated_by    'admin' or 'customer'.
	 * @return array{success: bool, status: string, message: string, order_id: int}
	 */
	public function manualRetry(int $subscription_id, string $initiated_by = 'admin'): array
	{
		$subscription = arraysubs_get_subscription($subscription_id);
		if (! $subscription instanceof \WP_Post) {
			return [
				'success'  => false,
				'status'   => 'invalid_subscription',
				'message'  => __('Subscription not found.', 'arraysubs'),
				'order_id' => 0,
			];
		}

		if (arraysubs_is_waiting_cancellation($subscription_id)) {
			return [
				'success'  => false,
				'status'   => 'waiting_cancellation',
				'message'  => __('Cannot retry: subscription is pending cancellation.', 'arraysubs'),
				'order_id' => 0,
			];
		}

		$existing_order = $this->getPendingRenewalOrder($subscription_id);
		if (! $existing_order instanceof \WC_Order) {
			return [
				'success'  => false,
				'status'   => 'no_failed_renewal',
				'message'  => __('No failed renewal order to retry.', 'arraysubs'),
				'order_id' => 0,
			];
		}

		// Force the retry counter to at least 1 so the pre-retry verification
		// always runs for manual retries, even when the gateway has not yet
		// scheduled an automatic retry.
		$attempts = arraysubs_get_payment_retry_attempts($subscription_id);
		if ($attempts < 1) {
			update_post_meta($subscription_id, '_payment_retry_attempts', 1);
		}

		arraysubs_add_subscription_note(
			$subscription_id,
			sprintf(
				/* translators: 1: actor (admin or customer), 2: order id */
				__('Manual renewal retry initiated by %1$s for order #%2$d. Pre-retry charge verification will run before any new charge.', 'arraysubs'),
				$initiated_by,
				$existing_order->get_id()
			),
			'private',
			0,
			[],
			'subscription',
			'renewal_payment_succeeded'
		);

		$succeeded = $this->process($subscription_id);
		$order     = $this->getPendingRenewalOrder($subscription_id) ?: $existing_order;

		if ($succeeded || ($order instanceof \WC_Order && $order->is_paid())) {
			return [
				'success'  => true,
				'status'   => 'paid',
				'message'  => __('Renewal payment successful.', 'arraysubs'),
				'order_id' => $order instanceof \WC_Order ? $order->get_id() : 0,
			];
		}

		$last_failure_reason = (string) get_post_meta($subscription_id, '_last_payment_failure_reason', true);

		return [
			'success'  => false,
			'status'   => 'failed',
			'message'  => '' !== $last_failure_reason
				? sprintf(
					/* translators: %s: failure reason */
					__('Renewal payment failed: %s', 'arraysubs'),
					$last_failure_reason
				)
				: __('Renewal payment failed.', 'arraysubs'),
			'order_id' => $order instanceof \WC_Order ? $order->get_id() : 0,
		];
	}

	/**
	 * Check if renewal is due
	 *
	 * @param \WP_Post $subscription Subscription post object
	 * @return bool True if renewal is due
	 */
	private function isRenewalDue(\WP_Post $subscription): bool
	{
		$next_payment = get_post_meta($subscription->ID, '_next_payment_date', true);

		if (empty($next_payment)) {
			return false;
		}

		$next_payment_time = strtotime($next_payment);
		$now = time();

		return $next_payment_time <= $now;
	}

	/**
	 * Handle manual payment pending for renewal
	 *
	 * @param int       $subscription_id Subscription ID
	 * @param \WC_Order $order Renewal order
	 * @param array     $result Payment result
	 * @return bool True on success
	 */
	private function handleManualPaymentPending(int $subscription_id, $order, array $result): bool
	{
		// Add order to subscription history
		arraysubs_add_subscription_order($subscription_id, $order->get_id());

		// Store the pending renewal order ID
		update_post_meta($subscription_id, '_pending_renewal_order_id', $order->get_id());

		if ('requires_action' === ($result['status'] ?? '')) {
			$order->add_order_note(
				$result['message'] ?? __('Renewal payment requires customer authentication.', 'arraysubs')
			);
		}

		return true;
	}

	/**
	 * Handle a failed automatic payment attempt without ending the grace period early.
	 *
	 * @param int       $subscription_id Subscription ID.
	 * @param \WC_Order $order           Renewal order.
	 * @param string    $error_message   Error message.
	 * @param string    $error_code      Gateway error code.
	 * @return bool False to indicate failure.
	 */
	private function handlePaymentAttemptFailure(int $subscription_id, $order, string $error_message, string $error_code = ''): bool
	{
		update_post_meta($subscription_id, '_last_payment_failure', current_time('mysql', true));
		update_post_meta($subscription_id, '_last_payment_failure_reason', $error_message);

		// Classify the failure so notes, emails, and dunning logic can speak
		// in human language ("card declined (insufficient funds)") instead
		// of bouncing a raw gateway message at the customer.
		$category = function_exists('arraysubs_classify_payment_failure_reason')
			? arraysubs_classify_payment_failure_reason($error_code, $error_message)
			: 'unknown';
		update_post_meta($subscription_id, '_last_payment_failure_category', $category);

		if ($order instanceof \WC_Order && !$order->has_status(['failed', 'cancelled'])) {
			/* translators: %s: Gateway payment failure message. */
			$order->update_status('failed', sprintf(__('Renewal payment failed: %s', 'arraysubs'), $error_message));
		}

		// Add a customer-readable note explaining why the payment failed,
		// derived from the classified category.
		if (function_exists('arraysubs_describe_payment_failure_category') && 'unknown' !== $category) {
			arraysubs_add_subscription_note(
				$subscription_id,
				sprintf(
					/* translators: 1: human-readable failure reason, 2: raw gateway message */
					__('Renewal payment failed: %1$s. Gateway message: "%2$s".', 'arraysubs'),
					arraysubs_describe_payment_failure_category($category),
					wp_strip_all_tags($error_message)
				),
				'private',
				0,
				[],
				'subscription',
				'renewal_payment_failed'
			);
		}

		$this->scheduleNextRetry($subscription_id);

		do_action('arraysubs_renewal_payment_failed', $subscription_id, $order instanceof \WC_Order ? $order->get_id() : 0, $error_message);

		$gateway_slug = get_post_meta($subscription_id, '_payment_gateway', true);

		if (!empty($gateway_slug)) {
			do_action(
				'arraysubs_gateway_payment_failed',
				$subscription_id,
				$order instanceof \WC_Order ? $order->get_id() : 0,
				$error_message,
				$gateway_slug
			);
		}

		return false;
	}

	/**
	 * Schedule (or skip) the next automatic retry based on the gateway's
	 * retry config and the current attempt counter.
	 *
	 * @param int $subscription_id Subscription ID.
	 * @return void
	 */
	private function scheduleNextRetry(int $subscription_id): void
	{
		$config   = arraysubs_get_payment_retry_config($subscription_id);
		$attempts = arraysubs_get_payment_retry_attempts($subscription_id);

		if (! $config['enabled']) {
			arraysubs_add_subscription_note(
				$subscription_id,
				__('Automatic retries are disabled in the gateway settings. The next attempt requires a manual retry.', 'arraysubs'),
				'private',
				0,
				[],
				'subscription'
			);
			return;
		}

		if ($attempts >= $config['max_attempts']) {
			arraysubs_add_subscription_note(
				$subscription_id,
				sprintf(
					/* translators: %d: total attempts made */
					__('Reached the configured retry limit (%d). No further automatic retries will be scheduled. The subscription will continue through the standard grace period; an admin or customer can also trigger a manual retry.', 'arraysubs'),
					$config['max_attempts']
				),
				'private',
				0,
				[],
				'subscription'
			);
			return;
		}

		$next_attempt    = $attempts + 1;
		$next_attempt_at = time() + $config['interval_seconds'];

		ActionScheduler::scheduleSingle(
			ActionScheduler::HOOK_PROCESS_RENEWAL,
			$next_attempt_at,
			[$subscription_id],
			ActionScheduler::GROUP_RENEWALS
		);

		update_post_meta($subscription_id, '_payment_retry_attempts', $next_attempt);
		update_post_meta($subscription_id, '_payment_retry_last_attempt_at', current_time('mysql', true));
		update_post_meta($subscription_id, '_payment_retry_next_attempt_at', gmdate('Y-m-d H:i:s', $next_attempt_at));

		arraysubs_add_subscription_note(
			$subscription_id,
			sprintf(
				/* translators: 1: attempt number, 2: max attempts, 3: scheduled local time */
				__('Scheduled retry %1$d of %2$d at %3$s. Plugin verifies with the gateway before re-charging to avoid double charges.', 'arraysubs'),
				$next_attempt,
				$config['max_attempts'],
				arraysubs_format_timestamp_local($next_attempt_at, get_option('date_format') . ' ' . get_option('time_format'))
			),
			'private',
			0,
			[],
			'subscription',
			'retry_scheduled'
		);
	}

	/**
	 * Handle failed renewal payment
	 *
	 * @param int            $subscription_id Subscription ID
	 * @param string         $error_message Error message
	 * @param \WC_Order|null $order Renewal order if created
	 * @return bool False to indicate failure
	 */
	private function handleFailure(int $subscription_id, string $error_message, $order = null): bool
	{
		if ($order) {
			$order->update_status('failed', $error_message);
			$order->add_order_note(
				/* translators: %s: error message */
				sprintf(__('Renewal payment failed: %s', 'arraysubs'), $error_message)
			);
		}

		// Change subscription to on-hold
		wp_update_post([
			'ID' => $subscription_id,
			'post_status' => 'arraysubs-on-hold',
		]);

		// Log the failure
		update_post_meta($subscription_id, '_last_payment_failure', current_time('mysql', true));
		update_post_meta($subscription_id, '_last_payment_failure_reason', $error_message);

		// Schedule next retry per gateway config
		$this->scheduleNextRetry($subscription_id);

		$gateway_slug = \get_post_meta($subscription_id, '_payment_gateway', true);

		// Trigger action for email notifications
		\do_action('arraysubs_renewal_payment_failed', $subscription_id, $order ? $order->get_id() : 0, $error_message);

		/**
		 * Fires after a payment failure on a subscription with a gateway attached.
		 * Pro uses this for gateway-specific failure logging.
		 *
		 * @param int    $subscription_id Subscription ID.
		 * @param int    $order_id        Order ID (0 if no order).
		 * @param string $error_code      Error code or message.
		 * @param string $gateway_slug    Payment gateway slug.
		 */
		if (!empty($gateway_slug)) {
			\do_action(
				'arraysubs_gateway_payment_failed',
				$subscription_id,
				$order ? $order->get_id() : 0,
				$error_message,
				$gateway_slug
			);
		}

		return false;
	}

	/**
	 * Calculate next payment date using standard interval arithmetic.
	 *
	 * @param int $subscription_id Subscription ID
	 * @return string Next payment date in MySQL format (UTC)
	 */
	private function calculateNextPaymentDate(int $subscription_id): string
	{
		$current_next = get_post_meta($subscription_id, '_next_payment_date', true);
		$interval = (int) get_post_meta($subscription_id, '_billing_interval', true);
		$period = get_post_meta($subscription_id, '_billing_period', true);

		// Start from current next payment date
		$next_date = !empty($current_next) ? strtotime($current_next) : time();

		// Add the billing interval
		switch ($period) {
			case 'day':
				$next_date = strtotime("+{$interval} days", $next_date);
				break;
			case 'week':
				$next_date = strtotime("+{$interval} weeks", $next_date);
				break;
			case 'month':
				$next_date = strtotime("+{$interval} months", $next_date);
				break;
			case 'year':
				$next_date = strtotime("+{$interval} years", $next_date);
				break;
			default:
				$next_date = strtotime('+1 month', $next_date);
		}

		return gmdate('Y-m-d H:i:s', $next_date);
	}

	/**
	 * Check if subscription has a pending renewal order
	 *
	 * @param int $subscription_id Subscription ID
	 * @return bool True if pending renewal order exists
	 */
	private function hasPendingRenewalOrder(int $subscription_id): bool
	{
		return $this->getPendingRenewalOrder($subscription_id) instanceof \WC_Order;
	}

	/**
	 * Get the current unpaid renewal order for a subscription.
	 *
	 * @param int $subscription_id Subscription ID.
	 * @return \WC_Order|null Renewal order or null.
	 */
	private function getPendingRenewalOrder(int $subscription_id): ?\WC_Order
	{
		// For HPOS compatibility
		if (
			class_exists('Automattic\WooCommerce\Utilities\OrderUtil') &&
			\Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled()
		) {
			$orders = wc_get_orders([
				'limit'      => 1,
				'status'     => ['pending', 'on-hold', 'failed'],
				'orderby'    => 'date',
				'order'      => 'DESC',
				'meta_query' => [
					'relation' => 'AND',
					[
						'key'   => '_is_renewal_order',
						'value' => 'yes',
					],
					[
						'key'   => '_subscription_id',
						'value' => $subscription_id,
					],
				],
			]);
			$order = !empty($orders) ? reset($orders) : null;
			return $order instanceof \WC_Order ? $order : null;
		}

		$args = [
			'post_type'      => 'shop_order',
			'post_status'    => ['wc-pending', 'wc-on-hold', 'wc-failed'],
			'posts_per_page' => 1,
			'orderby'        => 'date',
			'order'          => 'DESC',
			'meta_query'     => [
				'relation' => 'AND',
				[
					'key'   => '_is_renewal_order',
					'value' => 'yes',
				],
				[
					'key'   => '_subscription_id',
					'value' => $subscription_id,
				],
			],
		];

		$orders = get_posts($args);
		$order_id = !empty($orders) ? (int) $orders[0]->ID : 0;
		$order = $order_id > 0 ? wc_get_order($order_id) : null;

		return $order instanceof \WC_Order ? $order : null;
	}
}
