<?php
/**
 * Background job orchestration using Action Scheduler and OpenAI Responses API.
 *
 * Job Lifecycle:
 * 1. queued → agenticwp_subagent_start scheduled
 * 2. starting → OpenAI background response created
 * 3. polling → agenticwp_subagent_poll runs with exponential backoff (5s → 10s → 20s)
 * 4. complete/failed/cancelled → final status
 *
 * Active Hooks:
 * - agenticwp_subagent_start: Initiates background response creation
 * - agenticwp_subagent_poll: Polls OpenAI API for response status and executes function calls
 *
 * Note: Only these 2 hooks are needed for the background job system.
 * Any other scheduled actions (e.g., agenticwp_background_tasks) are orphaned.
 *
 * @package AgenticWP
 */

namespace Agentic_WP;

defined( 'ABSPATH' ) || exit;

use Agentic_WP\Error_Handler;

/**
 * Background job orchestration class.
 */
final class Background_Jobs {
	private const GROUP         = 'agenticwp_jobs';
	private const OPTION_JOBS   = 'agenticwp_jobs';
	private const MAX_POLLS     = 60;
	private const INITIAL_DELAY = 5;
	private const MAX_DELAY     = 20;

	/**
	 * Register background job hooks with Action Scheduler.
	 *
	 * @return void
	 */
	public static function register(): void {
		add_action( 'action_scheduler_init', array( __CLASS__, 'init_hooks' ) );
	}

	/**
	 * Initialize action hooks for background job processing.
	 *
	 * @return void
	 */
	public static function init_hooks(): void {
		add_action( 'agenticwp_subagent_start', array( __CLASS__, 'handle_start' ), 10, 3 );
		add_action( 'agenticwp_subagent_poll', array( __CLASS__, 'handle_poll' ), 10, 1 );
	}


	/**
	 * Schedule a new sub-agent background job.
	 *
	 * @param string $type Job type: 'post_create', 'post_edit', 'post_interlinking', or 'meta_description_generate'.
	 * @param array  $params Job parameters including 'prompt'.
	 * @return array|\WP_Error ['job_id' => string, 'action_id' => int] or WP_Error on failure.
	 */
	public static function schedule_sub_agent_job( string $type, array $params ) {
		$valid_types = array( 'post_create', 'post_edit', 'post_interlinking', 'meta_description_generate' );

		if ( ! in_array( $type, $valid_types, true ) ) {
			Error_Handler::log_error(
				'background_job',
				'Invalid job type provided',
				array( 'type' => $type )
			);
			return new \WP_Error(
				'invalid_job_type',
				__( 'Invalid job type.', 'agenticwp' )
			);
		}

		if ( ! function_exists( 'as_schedule_single_action' ) ) {
			Error_Handler::log_error(
				'background_job',
				'Action Scheduler not available'
			);
			return new \WP_Error(
				'scheduler_unavailable',
				__( 'Background job scheduler not available.', 'agenticwp' )
			);
		}

		$job_id  = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : wp_unique_id( 'job_' );
		$payload = array(
			'job_id' => $job_id,
			'type'   => $type,
			'params' => $params,
		);

		$action_id = (int) as_schedule_single_action( time(), 'agenticwp_subagent_start', $payload, self::GROUP );

		self::save_job(
			$job_id,
			array(
				'status'     => 'queued',
				'type'       => $type,
				'prompt'     => $params['prompt'] ?? '',
				'created_at' => time(),
				'action_id'  => $action_id,
			)
		);

		// Set pending jobs flag for health monitor.
		if ( class_exists( 'Agentic_WP\Job_Health_Monitor' ) ) {
			Job_Health_Monitor::set_pending_jobs_flag();
		}

		Error_Handler::debug_log(
			'Background job scheduled',
			array(
				'job_id'    => $job_id,
				'type'      => $type,
				'action_id' => $action_id,
			)
		);

		return array(
			'job_id'    => $job_id,
			'action_id' => $action_id,
		);
	}

	/**
	 * Get job state and details.
	 *
	 * @param string $job_id Job identifier.
	 * @return array|null Job data or null if not found.
	 */
	public static function get_job( string $job_id ): ?array {
		$all = get_option( self::OPTION_JOBS, array() );
		return is_array( $all ) && isset( $all[ $job_id ] ) && is_array( $all[ $job_id ] ) ? $all[ $job_id ] : null;
	}

	/**
	 * Cancel a background job (optional implementation).
	 *
	 * @param string $job_id Job identifier.
	 * @return bool Success status.
	 */
	public static function cancel_job( string $job_id ): bool {
		$job = self::get_job( $job_id );
		if ( ! $job ) {
			return false;
		}

		// Cancel current OpenAI response if in progress.
		if ( ! empty( $job['response_id'] ) ) {
			$client = new OpenAI_Client();
			$client->cancel_response( $job['response_id'] );
		}

		// Unschedule future polls.
		if ( function_exists( 'as_unschedule_all_actions' ) ) {
			as_unschedule_all_actions( 'agenticwp_subagent_poll', array( $job_id ), self::GROUP );
		}

		self::patch_job(
			$job_id,
			array(
				'status'      => 'cancelled',
				'finished_at' => time(),
			)
		);

		return true;
	}

	/**
	 * Save complete job data.
	 *
	 * @param string $job_id Job identifier.
	 * @param array  $data Job data to save.
	 */
	private static function save_job( string $job_id, array $data ): void {
		$all = get_option( self::OPTION_JOBS, array() );
		if ( ! is_array( $all ) ) {
			$all = array();
		}

		$all[ $job_id ] = array_merge( $data, array( 'updated_at' => time() ) );

		if ( 100 < count( $all ) ) {
			$all = array_slice( $all, -100, null, true );
		}

		update_option( self::OPTION_JOBS, $all, false );
	}

	/**
	 * Update job with partial data.
	 *
	 * @param string $job_id Job identifier.
	 * @param array  $patch Partial job data to merge.
	 */
	private static function patch_job( string $job_id, array $patch ): void {
		$current = self::get_job( $job_id );
		if ( ! $current ) {
			$current = array();
		}
		self::save_job( $job_id, array_merge( $current, $patch ) );
	}

	/**
	 * Handle initial background response creation.
	 *
	 * @param string $job_id Job identifier.
	 * @param string $type Job type.
	 * @param array  $params Job parameters.
	 *
	 * @throws \RuntimeException If job initialization fails.
	 */
	public static function handle_start( string $job_id, string $type, array $params ): void {
		if ( '' === $job_id || '' === $type ) {
			Error_Handler::log_error(
				'background_job',
				'Invalid start parameters',
				array(
					'job_id' => $job_id,
					'type'   => $type,
				)
			);
			return;
		}

		self::patch_job( $job_id, array( 'status' => 'starting' ) );
		Error_Handler::debug_log(
			'Background job starting',
			array(
				'job_id' => $job_id,
				'type'   => $type,
			)
		);

		try {
			$client = new OpenAI_Client();
			$prompt = trim( (string) ( $params['prompt'] ?? '' ) );

			if ( '' === $prompt ) {
				throw new \RuntimeException( 'Empty prompt provided' );
			}

			switch ( $type ) {
				case 'post_create':
					$tools        = Agents::get_post_creator_tools();
					$instructions = Agents::get_post_creator_instructions();
					$job_name     = 'Post Create';
					break;
				case 'post_edit':
					$tools        = Tools::get_editor_tools();
					$instructions = Agents::get_editor_instructions();
					$job_name     = 'Post Edit';
					break;
				case 'post_interlinking':
					$tools        = Tools::get_interlinking_tools();
					$instructions = Agents::get_interlinking_instructions();
					$job_name     = 'Post Interlinking';
					break;
				case 'meta_description_generate':
					$tools        = Tools::get_meta_description_tools();
					$instructions = Agents::get_meta_description_instructions();
					$job_name     = 'Meta Description Generate';
					break;
				default:
					$tools        = Agents::get_post_creator_tools();
					$instructions = Agents::get_post_creator_instructions();
					$job_name     = 'Post Create';
					break;
			}

			$bg_params = array(
				'input'        => array(
					array(
						'role'    => 'user',
						'content' => $prompt,
					),
				),
				'instructions' => $instructions,
				'tools'        => $tools,
			);

			if ( 'post_create' === $type ) {
				$bg_params['text'] = array( 'format' => Agents::get_post_layout_schema() );
			}

			$response = $client->create_response_background( $bg_params );

			if ( is_wp_error( $response ) ) {
				throw new \RuntimeException( $response->get_error_message() );
			}

			$response_id = (string) ( $response['id'] ?? '' );
			if ( '' === $response_id ) {
				throw new \RuntimeException( 'No response ID returned from OpenAI' );
			}

			self::patch_job(
				$job_id,
				array(
					'status'       => 'polling',
					'response_id'  => $response_id,
					'steps'        => 0,
					'prompt'       => $prompt,
					'instructions' => $instructions,
					'tools'        => $tools,
				)
			);

			if ( function_exists( 'as_schedule_single_action' ) ) {
				as_schedule_single_action( time() + self::INITIAL_DELAY, 'agenticwp_subagent_poll', array( $job_id ), self::GROUP );
				Error_Handler::debug_log(
					'First poll scheduled',
					array(
						'job_id'      => $job_id,
						'response_id' => $response_id,
					)
				);
			}
		} catch ( \Throwable $e ) {
			Error_Handler::log_error(
				'background_job',
				"Job start failed: {$e->getMessage()}",
				array(
					'job_id' => $job_id,
					'type'   => $type,
					'trace'  => $e->getTraceAsString(),
				)
			);
			self::patch_job(
				$job_id,
				array(
					'status'      => 'failed',
					'error'       => $e->getMessage(),
					'finished_at' => time(),
				)
			);
		}
	}

	/**
	 * Handle polling of background responses and function call execution.
	 *
	 * @param string $job_id Job identifier.
	 *
	 * @throws \RuntimeException If polling fails.
	 */
	public static function handle_poll( string $job_id ): void {
		if ( ! function_exists( 'as_schedule_single_action' ) ) {
			Error_Handler::log_error(
				'background_job',
				'Action Scheduler not available for polling'
			);
			return;
		}

		if ( '' === $job_id ) {
			Error_Handler::log_error(
				'background_job',
				'Poll called with empty job_id'
			);
			return;
		}

		$job = self::get_job( $job_id );
		if ( ! is_array( $job ) || empty( $job['response_id'] ) ) {
			Error_Handler::log_error(
				'background_job',
				'Job not found or missing response_id',
				array( 'job_id' => $job_id )
			);
			return;
		}

		try {
			$client   = new OpenAI_Client();
			$response = $client->get_response( (string) $job['response_id'] );

			if ( is_wp_error( $response ) ) {
				throw new \RuntimeException( $response->get_error_message() );
			}

			$status = (string) ( $response['status'] ?? '' );
			$steps  = (int) ( $job['steps'] ?? 0 );

			if ( 'queued' === $status || 'in_progress' === $status ) {
				if ( $steps >= self::MAX_POLLS ) {
					throw new \RuntimeException( 'Maximum poll attempts exceeded' );
				}

				$delay = min( self::INITIAL_DELAY * pow( 2, min( $steps, 3 ) ), self::MAX_DELAY );
				self::patch_job( $job_id, array( 'steps' => $steps + 1 ) );

				as_schedule_single_action( time() + $delay, 'agenticwp_subagent_poll', array( $job_id ), self::GROUP );
				Error_Handler::debug_log(
					'Job still processing, poll rescheduled',
					array(
						'job_id' => $job_id,
						'delay'  => $delay,
						'step'   => $steps,
					)
				);
				return;
			}

			$calls = $client->extract_function_calls_from_output( (array) ( $response['output'] ?? array() ) );

			if ( ! empty( $calls ) ) {
				Error_Handler::debug_log(
					'Processing function calls',
					array(
						'job_id' => $job_id,
						'count'  => count( $calls ),
					)
				);

				$original_user = get_current_user_id();
				$admin_user    = get_users(
					array(
						'role'   => 'administrator',
						'number' => 1,
					)
				);
				if ( ! empty( $admin_user ) ) {
					wp_set_current_user( $admin_user[0]->ID );
				}

				$tool_outputs = array();
				foreach ( $calls as $call ) {
					$call_name = (string) ( $call['name'] ?? '' );
					$call_id   = (string) ( $call['call_id'] ?? '' );
					$call_args = (array) ( $call['arguments'] ?? array() );

					$output         = Tools::dispatch( $call_name, $call_args );
					$tool_outputs[] = array(
						'type'    => 'function_call_output',
						'call_id' => $call_id,
						'output'  => $output,
					);
				}

				wp_set_current_user( $original_user );

				$follow_opts = array_filter(
					array(
						'instructions' => $job['instructions'] ?? null,
						'tools'        => $job['tools'] ?? null,
					)
				);

				$followup = $client->followup_response_background(
					(string) $response['id'],
					$tool_outputs,
					$follow_opts
				);

				if ( is_wp_error( $followup ) ) {
					throw new \RuntimeException( $followup->get_error_message() );
				}

				$new_response_id = (string) ( $followup['id'] ?? '' );
				self::patch_job(
					$job_id,
					array(
						'response_id' => $new_response_id,
						'steps'       => $steps + 1,
					)
				);

				as_schedule_single_action( time() + self::INITIAL_DELAY, 'agenticwp_subagent_poll', array( $job_id ), self::GROUP );
				Error_Handler::debug_log(
					'Follow-up posted, next poll scheduled',
					array(
						'job_id'      => $job_id,
						'response_id' => $new_response_id,
					)
				);
				return;
			}

			$output_text = (string) ( $response['output_text'] ?? '' );
			self::patch_job(
				$job_id,
				array(
					'status'            => 'complete',
					'finished_at'       => time(),
					'last_output'       => $output_text,
					'final_response_id' => (string) ( $response['id'] ?? '' ),
				)
			);

			// Clear pending jobs flag if no more pending jobs exist.
			self::maybe_clear_pending_jobs_flag();

			Error_Handler::debug_log(
				'Job completed successfully',
				array( 'job_id' => $job_id )
			);

		} catch ( \Throwable $e ) {
			Error_Handler::log_error(
				'background_job',
				"Job poll failed: {$e->getMessage()}",
				array(
					'job_id' => $job_id,
					'trace'  => $e->getTraceAsString(),
				)
			);
			self::patch_job(
				$job_id,
				array(
					'status'      => 'failed',
					'error'       => $e->getMessage(),
					'finished_at' => time(),
				)
			);

			// Clear pending jobs flag if no more pending jobs exist.
			self::maybe_clear_pending_jobs_flag();
		}
	}

	/**
	 * Clear the pending jobs flag if no more pending jobs exist.
	 *
	 * Called after job completion or failure to update the frontend beacon trigger state.
	 */
	private static function maybe_clear_pending_jobs_flag(): void {
		if ( ! class_exists( 'Agentic_WP\Job_Health_Monitor' ) ) {
			return;
		}

		// Check if there are still pending jobs.
		if ( ! Job_Health_Monitor::has_pending_jobs() ) {
			Job_Health_Monitor::clear_pending_jobs_flag();
		}
	}
}
