<?php
/**
 * WBAG Scheduler - WP-Cron Scheduling
 *
 * Handles WP-Cron scheduling for automated blog post generation
 * and topic scouting based on configured intervals.
 *
 * @since 1.0.0
 * @package WP_Blog_Agent
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class WBAG_Scheduler
 *
 * Manages WP-Cron scheduling for the WP Blog Agent plugin.
 *
 * @since 1.0.0
 */
class WBAG_Scheduler {

	/**
	 * Custom cron hook name.
	 *
	 * @since 1.0.0
	 * @const string
	 */
	const WBAG_CRON_HOOK = 'wbag_scheduled_run';

	/**
	 * Initialize the scheduler.
	 *
	 * Registers the cron hook and custom cron schedules.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public static function init() {
		// Register the cron hook.
		add_action( self::WBAG_CRON_HOOK, array( __CLASS__, 'execute_scheduled_run' ) );

		// Register custom cron schedules.
		add_filter( 'cron_schedules', array( __CLASS__, 'register_cron_schedules' ) );
	}

	/**
	 * Registers custom cron intervals.
	 *
	 * @since 1.0.0
	 *
	 * @param array $schedules Array of existing cron schedules.
	 *
	 * @return array Modified schedules array.
	 */
	public static function register_cron_schedules( $schedules ) {
		if ( ! isset( $schedules['wbag_twice_weekly'] ) ) {
			$schedules['wbag_twice_weekly'] = array(
				'interval' => 3.5 * DAY_IN_SECONDS,
				'display'  => esc_html__( 'Twice Weekly', 'wp-blog-agent' ),
			);
		}

		return $schedules;
	}

	/**
	 * Sets up or updates the cron schedule based on settings.
	 *
	 * Clears any existing scheduled events and sets up new ones
	 * based on the provided settings.
	 *
	 * @since 1.0.0
	 *
	 * @param array $settings The plugin settings array containing:
	 *                        - 'schedule_enabled' (bool)
	 *                        - 'schedule_days' (array)
	 *                        - 'schedule_time' (string HH:MM).
	 *
	 * @return bool True on success, false on failure.
	 */
	public static function schedule( $settings ) {
		// Clear any existing events first.
		self::unschedule();

		// Pro feature gate: the scheduler is a paid feature.
		if ( function_exists( 'wbag_is_pro' ) && ! wbag_is_pro() ) {
			return false;
		}

		// Check if scheduling is enabled.
		if ( ! isset( $settings['schedule_enabled'] ) || ! $settings['schedule_enabled'] ) {
			return false;
		}

		// Validate required settings.
		if ( ! isset( $settings['schedule_days'] ) || ! isset( $settings['schedule_time'] ) ) {
			return false;
		}

		$days = $settings['schedule_days'];
		$time = $settings['schedule_time'];

		// Ensure days is an array.
		if ( ! is_array( $days ) ) {
			$days = array( $days );
		}

		// Calculate next occurrence.
		$next_run = self::calculate_next_occurrence( $days, $time );

		if ( ! $next_run ) {
			return false;
		}

		// Schedule the first event as a single event.
		// No args — the handler reads settings from the database.
		// Passing args would create a key mismatch with unschedule() and get_next_run().
		wp_schedule_single_event( $next_run, self::WBAG_CRON_HOOK );

		return true;
	}

	/**
	 * Removes all scheduled cron events.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public static function unschedule() {
		// wp_unschedule_hook removes ALL events for this hook regardless of args.
		// This is critical because older versions accidentally passed args, creating
		// orphaned events that wp_clear_scheduled_hook (arg-sensitive) could not remove.
		wp_unschedule_hook( self::WBAG_CRON_HOOK );
	}

	/**
	 * Executes the scheduled run.
	 *
	 * This method is called when the WP-Cron event fires. It checks
	 * if today is a scheduled day, runs the necessary tasks, and
	 * reschedules the next occurrence.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public static function execute_scheduled_run() {
		// Pro feature gate: if the license lapsed, abort and clean up any pending events.
		if ( function_exists( 'wbag_is_pro' ) && ! wbag_is_pro() ) {
			self::unschedule();
			self::add_to_log(
				array(
					'timestamp' => current_time( 'mysql' ),
					'event'     => 'skipped_pro_required',
				)
			);
			return;
		}

		// Load settings.
		$settings = get_option( 'wbag_settings', array() );

		if ( ! $settings ) {
			return;
		}

		$days = isset( $settings['schedule_days'] ) ? $settings['schedule_days'] : array();
		$time = isset( $settings['schedule_time'] ) ? $settings['schedule_time'] : '';

		// Ensure days is an array.
		if ( ! is_array( $days ) ) {
			$days = array( $days );
		}

		// Get current day of week (lowercase).
		$timezone = wp_timezone();
		$now      = new DateTime( 'now', $timezone );
		$today    = strtolower( $now->format( 'l' ) );

		// Log entry.
		$log_entry = array(
			'timestamp' => current_time( 'mysql' ),
			'event'     => 'started',
		);

		// Check if today is a scheduled day.
		if ( in_array( $today, array_map( 'strtolower', $days ), true ) ) {
			$log_entry['event'] = 'executed';

			// Run the autopilot: find/generate a topic and create a draft post.
			// WBAG_Admin holds all the logic (prompt building, Claude API, SEO, images).
			if ( class_exists( 'WBAG_Admin' ) ) {
				$admin  = new WBAG_Admin();
				$result = $admin->create_post_autopilot();

				if ( is_wp_error( $result ) ) {
					$log_entry['error'] = $result->get_error_message();
				} else {
					$log_entry['post_id']    = $result['post_id'];
					$log_entry['post_title'] = $result['title'];
				}
			} else {
				$log_entry['error'] = 'WBAG_Admin class not found';
			}
		} else {
			$log_entry['event'] = 'skipped_not_scheduled_day';
		}

		// Schedule the next run.
		if ( ! empty( $days ) && ! empty( $time ) ) {
			$next_run = self::calculate_next_occurrence( $days, $time );
			if ( $next_run ) {
				wp_schedule_single_event( $next_run, self::WBAG_CRON_HOOK );
				$log_entry['next_run'] = gmdate( 'Y-m-d H:i:s', $next_run );
			}
		}

		// Update cron log.
		self::add_to_log( $log_entry );
	}

	/**
	 * Gets the next scheduled run timestamp.
	 *
	 * @since 1.0.0
	 *
	 * @return int|bool The Unix timestamp of the next run, or false if not scheduled.
	 */
	public static function get_next_run() {
		$next = wp_next_scheduled( self::WBAG_CRON_HOOK );
		return $next ? $next : false;
	}

	/**
	 * Retrieves the cron log.
	 *
	 * @since 1.0.0
	 *
	 * @return array Array of log entries.
	 */
	public static function get_log() {
		$log = get_option( 'wbag_cron_log', array() );
		return is_array( $log ) ? $log : array();
	}

	/**
	 * Adds an entry to the cron log.
	 *
	 * Keeps the most recent 20 entries.
	 *
	 * @since 1.0.0
	 *
	 * @param array $entry The log entry to add.
	 *
	 * @return void
	 */
	private static function add_to_log( $entry ) {
		$log = self::get_log();

		// Add new entry.
		$log[] = $entry;

		// Keep only the last 20 entries.
		if ( count( $log ) > 20 ) {
			$log = array_slice( $log, -20 );
		}

		update_option( 'wbag_cron_log', $log );
	}

	/**
	 * Calculates the next occurrence of scheduled days and time.
	 *
	 * Finds the next future datetime that matches one of the specified
	 * days of the week at the given time using WordPress timezone.
	 *
	 * @since 1.0.0
	 *
	 * @param array  $days Array of day names (e.g., 'monday', 'tuesday').
	 * @param string $time Time in HH:MM format.
	 *
	 * @return int|bool Unix timestamp of next occurrence, or false on error.
	 */
	private static function calculate_next_occurrence( $days, $time ) {
		// Validate inputs.
		if ( ! is_array( $days ) || empty( $days ) ) {
			return false;
		}

		if ( ! preg_match( '/^\d{2}:\d{2}$/', $time ) ) {
			return false;
		}

		// Parse time.
		$time_parts = explode( ':', $time );
		$hour       = intval( $time_parts[0] );
		$minute     = intval( $time_parts[1] );

		// Validate hour and minute.
		if ( $hour < 0 || $hour > 23 || $minute < 0 || $minute > 59 ) {
			return false;
		}

		// Get WordPress timezone.
		$timezone = wp_timezone();
		$now      = new DateTime( 'now', $timezone );

		// Create a datetime for today at the specified time.
		$next = new DateTime( 'now', $timezone );
		$next->setTime( $hour, $minute, 0 );

		// Map day names to numbers (0 = Sunday).
		$day_map = array(
			'sunday'    => 0,
			'monday'    => 1,
			'tuesday'   => 2,
			'wednesday' => 3,
			'thursday'  => 4,
			'friday'    => 5,
			'saturday'  => 6,
		);

		// Normalize day names to lowercase and get their numeric values.
		$target_days = array();
		foreach ( $days as $day ) {
			$day_lower = strtolower( $day );
			if ( isset( $day_map[ $day_lower ] ) ) {
				$target_days[] = $day_map[ $day_lower ];
			}
		}

		// If no valid days, return false.
		if ( empty( $target_days ) ) {
			return false;
		}

		// If today at the specified time is in the future, and today is a scheduled day, use it.
		$current_day = (int) $now->format( 'w' );
		if ( in_array( $current_day, $target_days, true ) && $next > $now ) {
			return $next->getTimestamp();
		}

		// Find the next scheduled day.
		$days_to_check = 7;
		for ( $i = 1; $i <= $days_to_check; $i++ ) {
			$next->modify( '+1 day' );
			$next->setTime( $hour, $minute, 0 );

			$next_day = (int) $next->format( 'w' );
			if ( in_array( $next_day, $target_days, true ) ) {
				return $next->getTimestamp();
			}
		}

		return false;
	}
}
