<?php

namespace Limb_Chatbot\Includes\Services\Jobs;

use Limb_Chatbot\Includes\Data_Objects\Job;
use Limb_Chatbot\Includes\Data_Objects\Task;
use Limb_Chatbot\Includes\Exceptions\Error_Codes;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Factories\Job_Handler_Factory;
use Limb_Chatbot\Includes\Factories\Task_Processor_Factory;
use Limb_Chatbot\Includes\Interfaces\Task_Processor_Interface;
use Limb_Chatbot\Includes\Repositories\Job_Repository;
use Limb_Chatbot\Includes\Repositories\Job_Task_Repository;
use Limb_Chatbot\Includes\Services\Jobs\Managers\Job_Error_Handler;
use Limb_Chatbot\Includes\Services\Jobs\Managers\Job_Lock_Manager;
use Limb_Chatbot\Includes\Services\Jobs\Managers\Job_Processing_Limits;
use Limb_Chatbot\Includes\Services\Jobs\Managers\Job_State_Manager;
use Limb_Chatbot\Includes\Services\Jobs\Processors\Child_Task_Generation_Processor;
use Limb_Chatbot\Includes\Services\Jobs\Processors\Child_Task_Processor;

/**
 * Job Processor Service
 *
 * Orchestrates batch processing of job tasks.
 * Delegates responsibilities to specialized managers and processors.
 *
 * @since 1.1.0
 */
class Job_Processor {

	/**
	 * Job repository instance.
	 *
	 * @var Job_Repository
	 * @since 1.1.0
	 */
	private Job_Repository $job_repository;

	/**
	 * Task repository instance.
	 *
	 * @var Job_Task_Repository
	 * @since 1.1.0
	 */
	private Job_Task_Repository $task_repository;

	/**
	 * Job handler factory instance.
	 *
	 * @var Job_Handler_Factory
	 * @since 1.1.0
	 */
	private Job_Handler_Factory $handler_factory;

	/**
	 * Lock manager instance.
	 *
	 * @var Job_Lock_Manager
	 * @since 1.1.0
	 */
	private Job_Lock_Manager $lock_manager;

	/**
	 * Processing limits manager instance.
	 *
	 * @var Job_Processing_Limits
	 * @since 1.1.0
	 */
	private Job_Processing_Limits $limits_manager;

	/**
	 * State manager instance.
	 *
	 * @var Job_State_Manager
	 * @since 1.1.0
	 */
	private Job_State_Manager $state_manager;

	/**
	 * Error handler instance.
	 *
	 * @var Job_Error_Handler
	 * @since 1.1.0
	 */
	private Job_Error_Handler $error_handler;

	/**
	 * Constructor.
	 *
	 * @param  Job_Repository|null  $job_repository  Job repository.
	 * @param  Job_Task_Repository|null  $task_repository  Task repository.
	 * @param  Job_Handler_Factory|null  $handler_factory  Handler factory.
	 *
	 * @since 1.1.0
	 */
	public function __construct(
		?Job_Repository $job_repository = null,
		?Job_Task_Repository $task_repository = null,
		?Job_Handler_Factory $handler_factory = null
	) {
		$this->job_repository  = $job_repository ?? new Job_Repository();
		$this->task_repository = $task_repository ?? new Job_Task_Repository();
		$this->handler_factory = $handler_factory ?? new Job_Handler_Factory();

		// Initialize managers
		$this->lock_manager   = new Job_Lock_Manager();
		$this->limits_manager = new Job_Processing_Limits();
		$this->state_manager  = new Job_State_Manager( $this->task_repository );
		$this->error_handler  = new Job_Error_Handler();
	}

	/**
	 * Process a batch of tasks for a job.
	 *
	 * This method orchestrates the batch processing:
	 * 1. Validates the job can be processed
	 * 2. Acquires a processing lock
	 * 3. Fetches pending tasks
	 * 4. Processes each task with limit checking
	 * 5. Updates stats periodically
	 * 6. Releases the lock
	 *
	 * @param  Job  $job  Job to process.
	 * @param  int  $batch_size  Number of tasks to process in this batch.
	 *
	 * @return int Number of tasks successfully processed.
	 * @throws Exception If processing fails or job cannot be processed.
	 * @since 1.1.0
	 */
	public function process_batch( Job $job, int $batch_size = 0 ): int {
		// Validate job can be processed
		if ( ! $job->can_process() ) {
			throw new Exception(
				Error_Codes::VALIDATION_INVALID_VALUE,
				sprintf( __( 'Job cannot be processed (status: %s)', 'limb-chatbot' ), $job->get_status() )
			);
		}

		// Acquire processing lock
		if ( ! $this->lock_manager->acquire( $job->get_id() ) ) {
			return 0; // Another process is already handling this job
		}

		// Start tracking limits
		$this->limits_manager->start();

		try {
			return $this->process_tasks( $job, $batch_size );
		} finally {
			// Always release lock, even on error
			$this->lock_manager->release( $job->get_id() );
		}
	}

	/**
	 * Process tasks for a job.
	 *
	 * @param  Job  $job  Job to process.
	 * @param  int  $batch_size  Batch size.
	 *
	 * @return int Number of tasks processed.
	 * @throws Exception
	 * @since 1.1.0
	 */
	private function process_tasks( Job $job, int $batch_size = 0 ): int {
		// Get handler and prepare job
		$handler = $this->handler_factory->make( $job->get_type() );

		// Determine batch size from handler if not provided
		if ( property_exists( $handler, 'batch_size' ) ) {
			$batch_size = $handler->batch_size;
		}
		// If still 0, use a default fallback
		if ( $batch_size === 0 ) {
			$batch_size = 5; // Default fallback
		}

		$job = $this->prepare_job_for_processing( $job );

		if ( ! $job ) {
			return 0;
		}

		// Process tasks
		return $this->process_task_batch( $job, $handler, $batch_size );
	}

	/**
	 * Prepare job for processing.
	 *
	 * @param  Job  $job  Job instance.
	 *
	 * @return Job|null Prepared job or null if cannot process.
	 * @throws \Exception
	 * @since 1.1.0
	 */
	private function prepare_job_for_processing( Job $job ): ?Job {
		// Reload job to get latest state
		$job = $this->job_repository->find( $job->get_id() );
		if ( ! $job || ! $job->can_process() ) {
			return null;
		}

		// Still generating tasks, nothing to process yet
		if ( $job->is_generating_tasks() ) {
			return null;
		}

		// Start processing if needed
		$this->state_manager->start_processing( $job );

		return $job;
	}

	/**
	 * Process a batch of tasks.
	 *
	 * @param  Job  $job  Job instance.
	 * @param  mixed  $handler  Job handler.
	 * @param  int  $batch_size  Batch size.
	 *
	 * @return int Number of tasks processed.
	 * @throws \Exception
	 * @since 1.1.0
	 */
	private function process_task_batch( Job $job, $handler, int $batch_size ): int {
		$tasks = $this->task_repository->get_pending_tasks( $job->get_id(), $batch_size );
		if ( $tasks->is_empty() ) {
			$this->finalize_processing( $job );

			return 0;
		}
		$task_processor_factory = new Task_Processor_Factory();
		$processed_count        = 0;

		foreach ( $tasks as $task ) {
			if ( $this->should_stop_processing( $job ) ) {
				break;
			}
			if ( ! $task instanceof Task ) {
				continue;
			}

			$processor = $task_processor_factory->make( $task, $job, $handler, $this->error_handler );

			// Check if this is a child task (for multitask handlers)
			$is_child_task = ! empty( $task->get_parent_task_id() );

			try {
				// For parent tasks (non-child), update progress BEFORE marking as processing
				// The repository logic handles counting PENDING parents as 50% if all previous parents are done
				// This ensures progress shows correctly (25% before first parent, 75% before second parent, etc.)
				if ( ! $is_child_task ) {
					// For Child_Task_Generation_Processor, use Child_Task_Processor to update stats
					// because Child_Task_Generation_Processor's update_stats is empty
					if ( $processor instanceof Child_Task_Generation_Processor ) {
						$child_task_processor = new Child_Task_Processor();
						$this->update_job_stats( $job, $child_task_processor );
					} else {
						$this->update_job_stats( $job, $processor );
					}
				}

				// Mark task as processing
				$this->mark_processing( $task, $job );

				// Process the task
				$processed = $processor->process( $task, $handler, $job );
				if ( ! $processed ) {
					throw new Exception( Error_Codes::TECHNICAL_ERROR, __( 'Failed to process.', 'limb-chatbot' ) );
				}

				// Mark task as completed
				$this->mark_completed( $task, $job );
				$processed_count ++;

				// For child task generation, update stats using child task processor and return early
				if ( $processor instanceof Child_Task_Generation_Processor ) {
					return $processed_count;
				}
			} catch ( \Exception $e ) {
				// Check if this is a warning (non-critical) exception
				// Warnings should complete the task, not fail it
				if ( $e instanceof Exception ) {
					$is_critical = $handler->is_critical_error( $e );
					
					if ( ! $is_critical ) {
						// For warnings, log the error but mark task as completed
						// This prevents retries for non-critical errors like "No content found"
						$error = $this->error_handler->create_error( $e );
						$job->add_error( $error );
						$job->save();
						
						// Mark task as completed (warnings don't fail the task)
						$this->mark_completed( $task, $job );
						$processed_count ++;
						
						// Update stats after completion for child tasks
						if ( $is_child_task ) {
							$this->update_job_stats( $job, $processor );
						}
					} else {
						// Critical errors should fail the task
						$this->handle_task_failure( $task, $e, $job, $handler );

						// Update stats after failure for child tasks
						if ( $is_child_task ) {
							$this->update_job_stats( $job, $processor );
						}
					}
				} else {
					// Non-custom exceptions are treated as failures
					$this->handle_task_failure( $task, $e, $job, $handler );

					// Update stats after failure for child tasks
					if ( $is_child_task ) {
						$this->update_job_stats( $job, $processor );
					}
				}
			}
			// Update stats periodically for regular tasks (not after every task to reduce DB calls)
			$this->update_job_stats( $job, $processor );
		}

		return $processed_count;
	}

	/**
	 * Finalize processing - update stats and check completion.
	 *
	 * @param  Job  $job  Job instance.
	 *
	 * @return void
	 * @throws \Exception
	 * @since 1.1.0
	 */
	private function finalize_processing( Job $job ): void {
		// Reload job to get latest state before checking completion
		$fresh_job = $this->job_repository->find( $job->get_id() );
		if ( ! $fresh_job ) {
			return;
		}

		$completed_job = $this->state_manager->check_and_complete( $fresh_job );

		// If job is completed, clean up
		if ( $completed_job ) {
			// Delete the job and all associated tasks after completion
			$this->task_repository->delete_by_job_id( $completed_job->get_id() );
			$this->job_repository->delete( $completed_job );
		}
	}

	/**
	 * Check if processing should stop.
	 *
	 * @param  Job  $job  Job instance.
	 *
	 * @return bool True if should stop.
	 * @since 1.1.0
	 */
	private function should_stop_processing( Job $job ): bool {
		// Check limits
		if ( $this->limits_manager->exceeded() ) {
			return true;
		}

		// Check if job can still be processed (use cached job instance to avoid DB call)
		if ( ! $job->can_process() ) {
			return true;
		}

		return false;
	}

	/**
	 * Update job statistics using the processor.
	 *
	 * @param  Job  $job  Job instance.
	 * @param  mixed  $processor  Task processor.
	 *
	 * @return void
	 * @since 1.1.0
	 */
	private function update_job_stats( Job $job, Task_Processor_Interface $processor ): void {
		if ( method_exists( $processor, 'update_stats' ) ) {
			$processor->update_stats( $job, $this->job_repository, $this->task_repository );
		}
	}

	/**
	 * Mark task as processing.
	 *
	 * Updates progress by X/2 before task processing starts.
	 *
	 * @param  Task  $task  Task to mark.
	 * @param  Job  $job  Job instance.
	 *
	 * @return void
	 * @throws \Exception
	 * @since 1.1.0
	 */
	private function mark_processing( Task $task, Job $job ): void {
		$task->mark_processing();
		$task->increment_attempts();
		$task->save();
	}

	/**
	 * Mark task as completed.
	 *
	 * Updates progress by X/2 after task completion.
	 *
	 * @param  Task  $task  Task to mark.
	 * @param  Job  $job  Job instance.
	 *
	 * @return void
	 * @throws \Exception
	 * @since 1.1.0
	 */
	private function mark_completed( Task $task, Job $job ): void {
		$task->mark_completed();
		$task->save();
	}

	/**
	 * Handle task failure with proper error handling.
	 *
	 * Uses the error handler's comprehensive error handling logic which includes:
	 * - Determining if error is critical
	 * - Creating structured error
	 * - Handling task retry logic
	 * - Pausing job if critical
	 *
	 * @param  Task  $task  Task that failed.
	 * @param  \Exception  $e  Exception that occurred.
	 * @param  Job  $job  Job instance.
	 * @param  mixed  $handler  Job handler.
	 *
	 * @return void
	 * @throws \Exception
	 * @since 1.1.0
	 */
	private function handle_task_failure( Task $task, \Exception $e, Job $job, $handler ): void {
		// Convert to our Exception type if needed for error handler
		if ( $e instanceof Exception ) {
			// Use error handler's comprehensive error handling
			$this->error_handler->handle_task_error( $task, $e, $handler, $job );
		} else {
			// For non-custom exceptions, use basic error handling
			$error = $this->error_handler->create_error( $e );
			$task->mark_failed( $error );
			$task->save();
			$job->add_error( $error );
			$job->save();
		}
	}

	/**
	 * Mark task as failed.
	 *
	 * @param  Task  $task  Task to mark.
	 * @param  \Exception  $e  Exception that occurred.
	 *
	 * @return void
	 * @throws \Exception
	 * @since 1.1.0
	 */
	private function mark_failed( Task $task, \Exception $e ): void {
		$error = $this->error_handler->create_error( $e );
		$task->mark_failed( $error );
		$task->save();
	}
}