<?php

namespace Limb_Chatbot\Includes\Services\Jobs;

use Limb_Chatbot\Includes\Data_Objects\Job;
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\Repositories\Job_Repository;
use Limb_Chatbot\Includes\Repositories\Job_Task_Repository;

/**
 * Job Service
 *
 * Main service for managing job lifecycle including creation,
 * task generation, and batch processing.
 *
 * Follows SOLID principles with clear separation of concerns.
 *
 * @since 1.1.0
 */
class Job_Service {

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

	/**
	 * Job stats service instance.
	 *
	 * @var Job_Stats_Service
	 * @since 1.1.0
	 */
	private Job_Stats_Service $stats_service;

	/**
	 * 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 processor instance.
	 *
	 * @var Job_Processor
	 * @since 1.1.0
	 */
	private Job_Processor $processor;

	/**
	 * Task generator instance.
	 *
	 * @var Job_Tasks_Generator
	 * @since 1.1.0
	 */
	private Job_Tasks_Generator $task_generator;

	/**
	 * Constructor.
	 *
	 * @param  Job_Handler_Factory|null  $factory  Handler factory.
	 * @param  Job_Stats_Service|null  $stats_service  Stats service.
	 * @param  Job_Repository|null  $job_repository  Job repository.
	 * @param  Job_Task_Repository|null  $task_repository  Task repository.
	 * @param  Job_Processor|null  $processor  Job processor.
	 * @param  Job_Tasks_Generator|null  $task_generator  Task generator.
	 *
	 * @since 1.1.0
	 */
	public function __construct(
		?Job_Handler_Factory $factory = null,
		?Job_Stats_Service $stats_service = null,
		?Job_Repository $job_repository = null,
		?Job_Task_Repository $task_repository = null,
		?Job_Processor $processor = null,
		?Job_Tasks_Generator $task_generator = null
	) {
		$this->factory         = $factory ?? new Job_Handler_Factory();
		$this->stats_service   = $stats_service ?? new Job_Stats_Service();
		$this->job_repository  = $job_repository ?? new Job_Repository();
		$this->task_repository = $task_repository ?? new Job_Task_Repository();
		$this->processor       = $processor ?? new Job_Processor( $this->job_repository, $this->task_repository, $this->factory, );
		$this->task_generator  = $task_generator ?? new Job_Tasks_Generator( $this->factory, $this->task_repository, $this->job_repository, $this->stats_service );
	}

	/**
	 * Create a new job.
	 *
	 * Creates job with 'generating_tasks' status. Task generation happens
	 * in chunks during polling to prevent timeouts with large datasets.
	 *
	 * @param  string  $type  Job type.
	 * @param  array  $config  Job configuration.
	 *
	 * @return Job Created job instance.
	 * @throws Exception If validation fails or job creation fails.
	 * @since 1.1.0
	 */
	public function create_job( string $type, array $config, ?string $chatbot_uuid = null, $sub_type = null ): Job {
		// Get handler for this job type
		$handler = $this->factory->make( $type );

		// Validate configuration only (no total calculation or stats initialization)
		$handler->validate( $config, $chatbot_uuid );

		// Create job with empty stats and no started_at
		// Stats and started_at will be initialized when processing starts
		$job = $this->job_repository->create( [
			'type'             => $type,
			'status'           => Job::STATUS_PENDING,
			'progress_percent' => 0,
			'chatbot_uuid'     => $chatbot_uuid,
			'config'           => $config,
			'sub_type'         => $sub_type,
			'stats'            => null, // Empty stats - will be initialized during first processing
			'errors'           => [],
			'started_at'       => null, // Will be set when processing starts
		] );

		if ( ! $job ) {
			throw new Exception(
				Error_Codes::TECHNICAL_ERROR,
				__( 'Failed to create job', 'limb-chatbot' )
			);
		}

		return $job;
	}

	/**
	 * Generate a batch of tasks for a job.
	 *
	 * Called repeatedly during polling to generate tasks in chunks.
	 * This prevents timeouts with large datasets.
	 *
	 * @param  Job  $job  Job instance.
	 * @param  int|null  $batch_size  Number of tasks to generate in this batch.
	 *
	 * @return Job Updated job instance.
	 * @throws Exception If generation fails.
	 * @since 1.1.0
	 */
	public function generate_job_batch( Job $job, ?int $batch_size = null ): Job {
		return $this->task_generator->generate_batch( $job, $batch_size );
	}

	/**
	 * Process a batch of tasks for a job.
	 *
	 * This is the main polling endpoint. It:
	 * 1. Fetches pending tasks
	 * 2. Processes them using the appropriate handler
	 * 3. Updates job stats and progress
	 * 4. Handles errors gracefully
	 *
	 * Uses locking to prevent concurrent processing of the same job.
	 *
	 * @param  Job  $job  Job to process.
	 * @param  int  $batch_size  Number of tasks to process in this batch.
	 *
	 * @return Job Updated job instance.
	 * @throws Exception If processing fails.
	 * @since 1.1.0
	 */
	public function process_batch( Job $job, int $batch_size = 0 ) {
		// Process batch using processor
		// If batch_size is 0, the processor will check the handler's batch_size property
		$this->processor->process_batch( $job, $batch_size );

		// Reload job to get latest state
		$updated_job = $this->job_repository->find( $job->get_id() );
		
		// If job was deleted (completed), return null to indicate completion
		if ( ! $updated_job ) {
			return null;
		}

		return $updated_job;
	}

	/**
	 * Pause a job.
	 *
	 * @param  int  $job_id  Job ID.
	 *
	 * @return Job Updated job instance.
	 * @throws Exception If job not found or cannot be paused.
	 * @since 1.1.0
	 */
	public function pause_job( int $job_id ): Job {
		$job = $this->job_repository->find( $job_id );

		if ( ! $job ) {
			throw new Exception(
				Error_Codes::NOT_FOUND,
				__( 'Job not found', 'limb-chatbot' )
			);
		}

		if ( ! $job->can_process() ) {
			throw new Exception(
				Error_Codes::VALIDATION_INVALID_VALUE,
				sprintf( __( 'Job cannot be paused (status: %s)', 'limb-chatbot' ), $job->get_status() )
			);
		}

		$job->set_status( Job::STATUS_PAUSED );
		$this->job_repository->update( $job, [
			'status' => $job->get_status(),
		] );

		return $job;
	}

	/**
	 * Resume a job.
	 *
	 * @param  int  $job_id  Job ID.
	 *
	 * @return Job Updated job instance.
	 * @throws Exception If job not found or cannot be resumed.
	 * @since 1.1.0
	 */
	public function resume_job( int $job_id ): Job {
		$job = $this->job_repository->find( $job_id );

		if ( ! $job ) {
			throw new Exception(
				Error_Codes::NOT_FOUND,
				__( 'Job not found', 'limb-chatbot' )
			);
		}
		$job->clear_errors();

		if ( ! $job->is_paused() ) {
			throw new Exception(
				Error_Codes::VALIDATION_INVALID_VALUE,
				sprintf( __( 'Job cannot be resumed (status: %s)', 'limb-chatbot' ), $job->get_status() )
			);
		}

		// Resume to processing if it was processing, otherwise keep original status
		if ( $job->get_started_at() ) {
			$job->set_status( Job::STATUS_PROCESSING );
		} else {
			$job->set_status( Job::STATUS_PENDING );
		}

		$this->job_repository->update( $job, [
			'status' => $job->get_status(),
		] );

		return $job;
	}

	/**
	 * Cancel a job and delete all associated data.
	 *
	 * This permanently removes the job and all its tasks from the system.
	 * Use this when you want to completely remove a job.
	 *
	 * @param  int  $job_id  Job ID.
	 *
	 * @return bool True if cancelled and deleted successfully.
	 * @throws Exception If job not found.
	 * @since 1.1.0
	 */
	public function cancel_job( int $job_id ): bool {
		$job = $this->job_repository->find( $job_id );

		if ( ! $job ) {
			throw new Exception(
				Error_Codes::NOT_FOUND,
				__( 'Job not found', 'limb-chatbot' )
			);
		}

		// Delete associated tasks first
		$this->task_repository->delete_by_job_id( $job_id );

		// Delete job
		return $this->job_repository->delete( $job );
	}
}
