<?php
/**
 * Warranty Cron Jobs
 *
 * Handles scheduled tasks for warranty management including expiration checking and reminders.
 *
 * @package WarrantyCart
 */

namespace WarrantyCart\Core;

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

/**
 * Warranty Cron Jobs Class
 */
class Cron {

	/**
	 * Whether the expiry reminder is sent.
	 */
	public const REMINDER_OPTION = 'warranty_cart_reminder_enabled';

	/**
	 * Whether the "your cover has ended" notice is sent.
	 */
	public const EXPIRY_NOTICE_OPTION = 'warranty_cart_expiry_notice_enabled';

	/**
	 * Whether a scheduled email is switched on.
	 *
	 * Separate switches because they are separate messages with separate
	 * welcomes. A shop that wants a nudge before cover ends and nothing after
	 * it — or the reverse — previously had to turn off every email the plugin
	 * sends, including the one telling a customer their claim was approved.
	 *
	 * @param string $option One of the constants above.
	 *
	 * @return bool
	 */
	protected static function notice_enabled( string $option ): bool {
		return 'no' !== \get_option( $option, 'yes' );
	}

	/**
	 * Constructor
	 */
	public function __construct() {
		$this->init_hooks();
	}

	/**
	 * Initialize hooks
	 */
	protected function init_hooks(): void {
		/*
		 * Put the schedule back if anything has taken it away.
		 *
		 * Both events were scheduled on activation and nowhere else, so a cron
		 * cleared by a migration, a management plugin or `wp cron event delete`
		 * stayed cleared — and expiry and reminders stopped with no sign that
		 * they had. Two `wp_next_scheduled()` calls per request is a cheap price
		 * for a sweep that is otherwise silently gone.
		 */
		\add_action( 'init', array( $this, 'ensure_scheduled' ) );

		\add_action( 'warranty_cart_check_expirations', array( $this, 'check_expiring_warranties' ) );
		\add_action( 'warranty_cart_send_reminders', array( $this, 'send_expiration_reminders' ) );
	}

	/**
	 * Check for expiring warranties and update status
	 *
	 * @return void
	 */
	/**
	 * Make sure both daily sweeps are on the schedule.
	 *
	 * @return void
	 */
	public function ensure_scheduled(): void {
		foreach ( array( 'warranty_cart_check_expirations', 'warranty_cart_send_reminders' ) as $event ) {
			if ( ! \wp_next_scheduled( $event ) ) {
				\wp_schedule_event( time(), 'daily', $event );
			}
		}
	}

	public function check_expiring_warranties(): void {
		global $wpdb;

		$table_name = $wpdb->prefix . 'warranty_cart_warranties';

		// Get current date
		$current_date = \current_time( 'Y-m-d' );

		try {
			/*
			 * The table name is interpolated, not a placeholder.
			 *
			 * This passed $table_name through %s, which is a *value* placeholder
			 * — $wpdb->prepare() quotes it, producing
			 * `SELECT * FROM 'wp_warranty_cart_warranties'`. MySQL rejects that,
			 * get_results() returned an empty array with the error suppressed,
			 * and the "no expired warranties" early return below treated the
			 * failure as good news. This scheduled task has therefore never
			 * expired a warranty on any installation.
			 *
			 * $table_name is $wpdb->prefix plus a literal, so interpolating it
			 * introduces nothing an attacker controls.
			 */
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$expired_warranties = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT * FROM {$table_name}
					WHERE status = 'active'
					AND expiration_date < %s
					ORDER BY expiration_date ASC",
					$current_date
				)
			);
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

			if ( empty( $expired_warranties ) ) {
				return; // No expired warranties
			}

			$expired_count      = 0;
			$notification_count = 0;
			$failed_count       = 0;

			foreach ( $expired_warranties as $warranty ) {
				try {
					// Update warranty status to expired
					$result = $wpdb->update(
						$table_name,
						array(
							'status'           => 'expired',
							'expired_notified' => 1,
							'updated_at'       => \current_time( 'mysql' ),
						),
						array( 'id' => $warranty->id ),
						array( '%s', '%d', '%s' ),
						array( '%d' )
					);

					if ( false !== $result ) {
						++$expired_count;

						/*
						 * The status change happens either way.
						 *
						 * Only the email is optional: a shop that does not want
						 * to tell customers their cover has ended still needs
						 * the cover to actually end, or a claim would be
						 * accepted against a warranty that expired months ago.
						 */
						$notification_sent = self::notice_enabled( self::EXPIRY_NOTICE_OPTION )
							&& $this->send_expiration_notification( $warranty );

						if ( $notification_sent ) {
							++$notification_count;
						}

						// Trigger action for further processing
						\do_action( 'warranty_cart_warranty_expired', $warranty );
					} else {
						++$failed_count;
						\wc_get_logger()->error(
							\sprintf( 'Failed to update expired status for warranty #%d', $warranty->id ),
							array( 'source' => 'warranty-cart-cron' )
						);
					}
				} catch ( \Exception $e ) {
					++$failed_count;
					\wc_get_logger()->error(
						\sprintf( 'Exception processing expired warranty #%d: %s', $warranty->id, $e->getMessage() ),
						array( 'source' => 'warranty-cart-cron' )
					);
				}
			}

			// Log results
			if ( $expired_count > 0 ) {
				\wc_get_logger()->info(
					\sprintf( 'Marked %d warranties as expired (%d notifications sent, %d failed)', $expired_count, $notification_count, $failed_count ),
					array( 'source' => 'warranty-cart-cron' )
				);
			}

			if ( $failed_count > 0 ) {
				\wc_get_logger()->warning(
					\sprintf( 'Failed to process %d expired warranties', $failed_count ),
					array( 'source' => 'warranty-cart-cron' )
				);
			}
		} catch ( \Exception $e ) {
			\wc_get_logger()->error(
				\sprintf( 'Exception in check_expiring_warranties: %s', $e->getMessage() ),
				array( 'source' => 'warranty-cart-cron' )
			);
		}
	}

	/**
	 * Send expiration reminders for warranties expiring soon
	 *
	 * @return void
	 */
	public function send_expiration_reminders(): void {
		global $wpdb;

		// This sweep exists only to send. Nothing else depends on it, so a shop
		// that does not want the nudge can have the whole thing skipped.
		if ( ! self::notice_enabled( self::REMINDER_OPTION ) ) {
			return;
		}

		$table_name = $wpdb->prefix . 'warranty_cart_warranties';

		// How far ahead to warn, from the settings screen. The filter still
		// wins, so a site that already overrode this keeps its value.
		$reminder_days = (int) \apply_filters(
			'warranty_cart_reminder_days',
			(int) \get_option( 'warranty_cart_reminder_days', 30 )
		);
		$reminder_days = max( 1, $reminder_days ); // Minimum 1 day

		// Calculate reminder date range
		$current_date  = \current_time( 'Y-m-d' );
		$reminder_date = \wp_date( 'Y-m-d', \strtotime( $current_date . " +{$reminder_days} days" ) );

		try {
			// Find warranties expiring within reminder period that haven't been reminded
			// Interpolated table name — see the note in check_expiring_warranties().
			// This query failed the same way, so no expiry reminder has ever been sent.
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$warranties_needing_reminder = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT * FROM {$table_name}
					WHERE status = 'active'
					AND reminder_sent = 0
					AND expiration_date <= %s
					AND expiration_date >= %s
					ORDER BY expiration_date ASC",
					$reminder_date,
					$current_date
				)
			);
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

			if ( empty( $warranties_needing_reminder ) ) {
				return; // No reminders needed
			}

			$reminder_count = 0;
			$failed_count   = 0;

			foreach ( $warranties_needing_reminder as $warranty ) {
				try {
					// Send reminder email
					$email_sent = $this->send_expiration_reminder( $warranty );

					if ( $email_sent ) {
						// Mark as reminded
						$result = $wpdb->update(
							$table_name,
							array(
								'reminder_sent' => 1,
								'updated_at'    => \current_time( 'mysql' ),
							),
							array( 'id' => $warranty->id ),
							array( '%d', '%s' ),
							array( '%d' )
						);

						if ( false !== $result ) {
							++$reminder_count;
							// Trigger action for further processing
							\do_action( 'warranty_cart_warranty_reminder_sent', $warranty );
						} else {
							++$failed_count;
							\wc_get_logger()->error(
								\sprintf( 'Failed to update reminder status for warranty #%d', $warranty->id ),
								array( 'source' => 'warranty-cart-cron' )
							);
						}
					} else {
						++$failed_count;
					}
				} catch ( \Exception $e ) {
					++$failed_count;
					\wc_get_logger()->error(
						\sprintf( 'Exception sending reminder for warranty #%d: %s', $warranty->id, $e->getMessage() ),
						array( 'source' => 'warranty-cart-cron' )
					);
				}
			}

			// Log results
			if ( $reminder_count > 0 ) {
				\wc_get_logger()->info(
					\sprintf( 'Sent expiration reminders for %d warranties (failed: %d)', $reminder_count, $failed_count ),
					array( 'source' => 'warranty-cart-cron' )
				);
			}

			if ( $failed_count > 0 ) {
				\wc_get_logger()->warning(
					\sprintf( 'Failed to send reminders for %d warranties', $failed_count ),
					array( 'source' => 'warranty-cart-cron' )
				);
			}
		} catch ( \Exception $e ) {
			\wc_get_logger()->error(
				\sprintf( 'Exception in send_expiration_reminders: %s', $e->getMessage() ),
				array( 'source' => 'warranty-cart-cron' )
			);
		}
	}

	/**
	 * Send expiration notification email
	 *
	 * @param object $warranty Warranty data object.
	 *
	 * @return bool True if email was sent successfully, false otherwise.
	 */
	protected function send_expiration_notification( object $warranty ): bool {
		try {
			/*
			 * The expiration date, not the whole warranty row.
			 *
			 * WarrantyExpired::trigger() is typed `string $expiration_date`, so
			 * handing it the stdClass raised a TypeError and took the cron run
			 * down with it. Nobody saw that until now, because the query that
			 * feeds this method was itself broken and never returned a row —
			 * fixing the SQL is what made this reachable.
			 */
			$email_class = new \WarrantyCart\Notifications\Emails\WarrantyExpired();
			$email_class->trigger( (int) $warranty->order_id, (string) $warranty->expiration_date );

			/*
			 * No return value to test.
			 *
			 * trigger() is declared `: void`. This assigned it to $result and
			 * branched on it, so the success arm was unreachable and every
			 * delivered notification was logged as "Failed to send" — the log
			 * said the opposite of what happened. Completing without an
			 * exception is the only signal the email API offers here.
			 */
			\wc_get_logger()->info(
				\sprintf( 'Sent expiration notification for warranty #%d', $warranty->id ),
				array( 'source' => 'warranty-cart-cron' )
			);

			return true;
		} catch ( \Throwable $e ) {
			// Throwable, not Exception: the mismatch this method used to hit
			// was a TypeError, which is an Error and slipped straight past a
			// catch on Exception to abort the whole run.
			\wc_get_logger()->error(
				\sprintf( 'Exception sending expiration notification for warranty #%d: %s', $warranty->id, $e->getMessage() ),
				array( 'source' => 'warranty-cart-cron' )
			);

			return false;
		}
	}

	/**
	 * Send expiration reminder email
	 *
	 * @param object $warranty Warranty data object.
	 *
	 * @return bool True if email was sent successfully, false otherwise.
	 */
	protected function send_expiration_reminder( object $warranty ): bool {
		try {
			/*
			 * The same three faults as send_expiration_notification(), in its
			 * twin: the whole row was passed where a date string is declared,
			 * and a `: void` return was assigned and branched on. See the notes
			 * there.
			 *
			 * days_remaining is trigger()'s third parameter and defaults to 30.
			 * Computing it means the reminder can say how long is actually
			 * left rather than always claiming a month.
			 */
			$days_remaining = (int) max(
				0,
				(int) ceil(
					( (int) strtotime( (string) $warranty->expiration_date ) - time() ) / DAY_IN_SECONDS
				)
			);

			$email_class = new \WarrantyCart\Notifications\Emails\WarrantyReminder();
			$email_class->trigger(
				(int) $warranty->order_id,
				(string) $warranty->expiration_date,
				$days_remaining
			);

			\wc_get_logger()->info(
				\sprintf( 'Sent expiration reminder for warranty #%d', $warranty->id ),
				array( 'source' => 'warranty-cart-cron' )
			);

			return true;
		} catch ( \Throwable $e ) {
			\wc_get_logger()->error(
				\sprintf( 'Exception sending expiration reminder for warranty #%d: %s', $warranty->id, $e->getMessage() ),
				array( 'source' => 'warranty-cart-cron' )
			);

			return false;
		}
	}

	/**
	 * Get warranty statistics for admin dashboard
	 *
	 * @return array Warranty statistics.
	 */
	public static function get_warranty_stats(): array {
		global $wpdb;

		$table_name = $wpdb->prefix . 'warranty_cart_warranties';

		/*
		 * Interpolated table name — see check_expiring_warranties(). There is
		 * nothing left to prepare once it is inlined: every other value in this
		 * statement is a literal, and prepare() with no placeholders is an
		 * error in its own right.
		 */
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$stats = $wpdb->get_row(
			"SELECT
				COUNT(*) as total_warranties,
				SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_warranties,
				SUM(CASE WHEN status = 'expired' THEN 1 ELSE 0 END) as expired_warranties,
				SUM(CASE WHEN expiration_date < CURDATE() AND status = 'active' THEN 1 ELSE 0 END) as overdue_warranties,
				SUM(CASE WHEN expiration_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY) AND status = 'active' THEN 1 ELSE 0 END) as expiring_soon
			FROM {$table_name}"
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		return array(
			'total_warranties'   => (int) ( $stats->total_warranties ?? 0 ),
			'active_warranties'  => (int) ( $stats->active_warranties ?? 0 ),
			'expired_warranties' => (int) ( $stats->expired_warranties ?? 0 ),
			'overdue_warranties' => (int) ( $stats->overdue_warranties ?? 0 ),
			'expiring_soon'      => (int) ( $stats->expiring_soon ?? 0 ),
		);
	}
}
