<?php

namespace AIPT\Api\AutomationJobs;

use AIPT\Api\AutomationJobs\Interfaces\JobExecutorInterface;
use AIPT\Api\AutomationJobs\Interfaces\JobLoggerInterface;
use AIPT\Api\AutomationJobs\Models\AutomationJob;
use AIPT\Api\BulkGenerator\Desc\ContentGenerator;
use AIPT\Api\BulkGenerator\Desc\ProductResult;
use AIPT\Api\BulkGenerator\FAQ\FAQGeneratorService;
use AIPT\Api\BulkGenerator\ImageAlt\Service as ImageAltService;
use AIPT\Api\BulkGenerator\SeoMeta\SeoMetaGeneratorService;
use AIPT\Api\BulkGenerator\Standard\StandardGenerator;
use AIPT\Api\BulkGenerator\Title\TitleGeneratorService;
use AIPT\Api\Constants\AutomationConstants;

use AIPT\Api\AutomationJobs\Services\OptionsTranslator;
use AIPT\Api\AutomationJobs\Services\ResultProcessor;

/**
 * Job Executor
 *
 * Executes automation jobs by integrating with existing AI services
 * Following Single Responsibility and Adapter patterns
 *
 * @since 2.3.0
 */
class JobExecutor implements JobExecutorInterface {

    /**
     * Job manager instance
     * @var AutomationJobManager
     */
    private $job_manager;

    /**
     * Logger instance
     * @var JobLoggerInterface
     */
    private $logger;

    /**
     * WordPress database instance
     * @var \wpdb
     */
    private $wpdb;

    /**
     * Job products table name
     * @var string
     */
    private $job_products_table;

    /**
     * Claim-based queue helper
     * @var \AIPT\Api\AutomationJobs\ClaimBasedQueue
     */
    private $queue;

    /**
     * Incremental progress tracker
     * @var \AIPT\Api\AutomationJobs\IncrementalProgressTracker
     */
    private $progress_tracker;

    /**
     * Draft content manager instance
     * @var DraftContentManager
     */
    private $draft_manager;

    /**
     * Options translator service
     * @var OptionsTranslator
     */
    private $options_translator;

    /**
     * Result processor service
     * @var ResultProcessor
     */
    private $result_processor;
    /**
     * In-memory lock tokens per job to ensure we only release our own locks
     * @var array<int,string>
     */
    private $lock_tokens = [];

    const MIN_WINDOW_SIZE         = AutomationConstants::MIN_WINDOW_SIZE;
    const MAX_WINDOW_SIZE         = AutomationConstants::MAX_WINDOW_SIZE;
    const PROGRESS_FLUSH_EVERY    = AutomationConstants::PROGRESS_FLUSH_EVERY;

    /**
     * Constructor
     *
     * @param AutomationJobManager $job_manager Job manager instance
     * @param JobLoggerInterface $logger Logger instance
     */
    public function __construct(AutomationJobManager $job_manager, JobLoggerInterface $logger) {
        $this->job_manager = $job_manager;
        $this->logger = $logger;
        $this->draft_manager = new DraftContentManager();

        global $wpdb;
        $this->wpdb = $wpdb;
        $this->job_products_table = $wpdb->prefix . 'aipt_job_products';
        $this->queue = new \AIPT\Api\AutomationJobs\ClaimBasedQueue();
        $this->progress_tracker = new \AIPT\Api\AutomationJobs\IncrementalProgressTracker();
        $this->options_translator = new OptionsTranslator();
        $this->result_processor = new ResultProcessor($logger);

    }

    /**
     * Execute a job
     *
     * @param int $job_id Job ID to execute
     * @return bool Success status
     * @throws \Exception On execution failure
     */
    public function executeJob($job_id) {
        $job = $this->job_manager->getJob($job_id);
        if (!$job) {

            throw new \Exception("Job not found: {$job_id}");
        }

        if ($job->status === 'paused') {
            $this->logger->info($job_id, 'Job is paused, skipping execution');
            return true;
        }

        $validation_errors = $this->validateJobForExecution($job);
        if (!empty($validation_errors)) {
            $this->logger->error($job_id, 'Job validation failed', [
                'errors' => $validation_errors,
                'type' => 'VALIDATION'
            ]);
            throw new \Exception('Job validation failed: ' . wp_json_encode($validation_errors));
        }

        try {
            $this->logger->info($job_id, 'Starting job execution (time-sliced)', [
                'tool_type' => $job->tool_type,
                'job_name' => $job->name,
                'timestamp' => current_time('mysql', true)
            ]);

            if (!$this->acquireLock((int) $job_id)) {
                $this->logger->info($job_id, 'Another executor is already running for this job (lock held)');
                return false;
            }

            try {

                $released = $this->queue->forceReleaseAllProcessing((int) $job_id);
                $releaseContext = ['released_count' => (int) $released];
                if ($released > 0 && $this->shouldLogDebugSnapshot((int) $job_id, 'released_processing', $releaseContext)) {
                    $this->logger->debug($job_id, 'Released processing items before tick', $releaseContext);
                }

                $tickStartTs = microtime(true);
                $startState = $this->describeJobProductState((int) $job_id);
                $startContext = array_merge($startState, [
                    'job_status' => $job->status,
                    'progress_percent' => (int) $job->progress,
                    'processed_products' => (int) $job->processed_products,
                    'failed_products' => (int) $job->failed_products,
                    'total_products' => (int) $job->total_products,
                ]);
                if ($this->shouldLogDebugSnapshot((int) $job_id, 'tick_start', $startContext)) {
                    $this->logger->debug($job_id, 'Tick starting', $startContext);
                }

                if ($job->status === 'preparing') {
                    try {
                        $pool_builder = new \AIPT\Api\AutomationJobs\PoolBuilder();
                        $filter = is_array($job->product_filter) ? $job->product_filter : [];
                        $pool_result = $pool_builder->buildPool((int)$job_id, $filter, AutomationConstants::POOL_BUILD_CHUNK_SIZE, AutomationConstants::MAX_POOL_BUILD_CHUNKS);

                        $target_total = isset($pool_result['total']) ? (int)$pool_result['total'] : (int)$job->total_products;

                        $current_total = (int) $this->wpdb->get_var(
                            $this->wpdb->prepare(
                                "SELECT COUNT(*) FROM {$this->job_products_table} WHERE job_id = %d",
                                (int)$job_id
                            )
                        );

                        $jobs_table = $this->wpdb->prefix . 'aipt_automation_jobs';
                        if ($target_total !== (int)$job->total_products) {
                            $this->wpdb->update(
                                $jobs_table,
                                ['total_products' => $target_total, 'updated_at' => current_time('mysql')],
                                ['id' => (int)$job_id],
                                ['%d','%s'],
                                ['%d']
                            );
                        } else {
                            $this->wpdb->update(
                                $jobs_table,
                                ['updated_at' => current_time('mysql')],
                                ['id' => (int)$job_id],
                                ['%s'],
                                ['%d']
                            );
                        }

                        if ($target_total > 0 && $current_total >= $target_total) {
                            $this->job_manager->updateJobStatus((int)$job_id, 'ready');
                            $this->logger->info($job_id, 'Product pool fully built; job set to ready', ['total' => $target_total]);
                        } else {
                            $this->job_manager->updateJobStatus((int)$job_id, 'preparing');
                            $this->logger->info($job->id, 'Continuing pool build in background', [
                                'inserted_so_far' => $current_total,
                                'target_total' => $target_total
                            ]);
                        }

                        \AIPT\Api\AutomationJobs\JobExecutionHandler::register_job_hook((int)$job_id);
                        $hook = 'aipt_execute_job_' . (int)$job_id;
                        $scheduleTimestamp = time() + AutomationConstants::rescheduleDelay();
                        $scheduled = \wp_schedule_single_event($scheduleTimestamp, $hook, [(int)$job_id]);
                        $nextScheduled = \wp_next_scheduled($hook, [(int)$job_id]);

                        $poolContext = array_merge($this->describeJobProductState((int)$job_id), [
                            'remaining_items' => max(0, (int)$target_total - $current_total),
                            'next_tick_due_at' => $nextScheduled ? gmdate('c', $nextScheduled) : null,
                            'schedule_result' => $scheduled === false ? 'failed' : 'scheduled',
                            'pool_target' => $target_total,
                            'pool_current' => $current_total,
                        ]);
                        if ($this->shouldLogDebugSnapshot((int)$job_id, 'pool_rescheduled', $poolContext)) {
                            $this->logger->debug($job_id, 'Pool build tick rescheduled', $poolContext);
                        }

                        if ($scheduled === false) {
                            $this->logger->warning($job_id, 'Failed to schedule next pool build tick', [
                                'hook' => $hook,
                                'pool_target' => $target_total,
                                'pool_current' => $current_total,
                            ]);
                        }

                        return true;
                    } catch (\Throwable $t) {
                        $this->logger->error($job_id, 'Pool build step failed', ['error' => $t->getMessage(), 'type' => 'DB']);

                        return false;
                    }
                }

                $processedThisTick = 0;
                $failedThisTick = 0;
                $claimAttempts = 0;
                $exitReason = 'loop_completed';

                if ($job->status !== 'running') {
                    $this->job_manager->updateJobStatus($job_id, 'running');
                    $this->logger->info($job_id, 'Job status set to running');
                }

                $this->logger->info($job_id, 'Initializing AI service', ['tool_type' => $job->tool_type]);
                $service = $this->getAIService($job->tool_type);
                $this->logger->info($job_id, 'AI service initialized', ['service_class' => get_class($service)]);
                $options = $this->options_translator->translate($job->settings, $job->tool_type);

                $budgetEndsAt = time() + AutomationConstants::tickTimeBudget();
                $processed_inc = 0;
                $failed_inc = 0;
                $last_flush = time();
                $itemCounter = 0;

                $memorySoftLimit = $this->getSoftMemoryLimitBytes();
                $exitReason = 'tick_completed';
                while (time() < $budgetEndsAt) {

                    $freshJob = $this->job_manager->getJob($job_id);
                    if ($freshJob && $freshJob->status === 'paused') {
                        $exitReason = 'paused_mid_tick';
                        $this->logger->info($job_id, 'Job paused mid-execution, stopping tick');
                        break;
                    }

                    $claimAttempts++;
                    $adaptiveWindowSize = $this->getAdaptiveWindowSize((int) $job_id);
                    $claim = $this->queue->claimBatch((int) $job_id, $adaptiveWindowSize);
                    $currentClaimToken = $claim['token'] ?? null;
                    if (empty($claim['product_ids'])) {

                        if ($currentClaimToken) { $this->queue->releaseClaimByToken((int)$job->id, (string)$currentClaimToken); }
                        $exitReason = 'no_claimable_items';
                        break;
                    }

                    foreach ($claim['product_ids'] as $productIndex => $product_id) {

                        if (time() >= $budgetEndsAt) {

                            if ($currentClaimToken) { $this->queue->releaseClaimByToken((int)$job->id, (string)$currentClaimToken); $currentClaimToken = null; }
                            $exitReason = 'time_budget_exhausted';
                            break 1;
                        }
                        $freshJob = $this->job_manager->getJob($job_id);
                        if ($freshJob && $freshJob->status === 'paused') {
                            $exitReason = 'paused_mid_tick';
                            $this->logger->info($job_id, 'Job paused, stopping item loop');
                            if ($currentClaimToken) { $this->queue->releaseClaimByToken((int)$job->id, (string)$currentClaimToken); $currentClaimToken = null; }
                            break 1;
                        }

                        try {

                            $shouldDeductCredits = false;
                            if ($job->tool_type === 'advanced' && $service instanceof ContentGenerator) {
                                $fs_manager = \AIPT\Core\FsManager::getInstance();
                                $plan_type = $fs_manager->getPlanType();

                                if ($plan_type !== 'business') {
                                    $credit = \AIPT\Core\CreditSystem\CreditManager::getInstance();

                                    $creditCheck = $credit->check('description', 1);
                                    if (!$creditCheck->allowed) {
                                        throw new \Exception('Insufficient credits');
                                    }
                                    $shouldDeductCredits = true;
                                }
                            }

                            if ($service instanceof StandardGenerator) {
                                $results = $service->bulkGenerate([(int)$product_id], $options);
                            } elseif ($service instanceof TitleGeneratorService || $service instanceof FAQGeneratorService || $service instanceof SeoMetaGeneratorService || $service instanceof ImageAltService) {
                                $results = $service->bulk_generate([(int)$product_id], $options);
                            } elseif ($service instanceof ContentGenerator) {

                                $contentTypes = $this->extractContentTypes($options);
                                $productResult = $service->generateForProduct((int)$product_id, $contentTypes, $options);
                                $results = $this->transformContentGeneratorResult($productResult, (int)$product_id);
                            } else {
                                throw new \Exception('Unknown service type: ' . get_class($service));
                            }

                            if ($shouldDeductCredits) {
                                $credit = \AIPT\Core\CreditSystem\CreditManager::getInstance();
                                $credit->use('description', 1, [
                                    'context' => 'automation_job',
                                    'job_id' => (int)$job->id,
                                    'tool_type' => $job->tool_type,
                                    'provider' => $options['provider'] ?? null,
                                    'model' => $options['model'] ?? null,
                                    'batch_size' => 1,
                                ]);
                            }

						$counts = $this->processResults($job->id, $results);
						$completedCount = (int) ($counts['completed'] ?? 0);
						$failedCount = (int) ($counts['failed'] ?? 0);
						$failureMessages = isset($counts['failure_messages']) && is_array($counts['failure_messages']) ? $counts['failure_messages'] : [];
						$hasCompleted = $completedCount > 0;
						$hasFailed = $failedCount > 0;

						if ($hasCompleted && !$hasFailed) {

							$processed_inc += 1;
							$processedThisTick += 1;
							$this->queue->markCompleted((int)$job->id, (int)$product_id, (string)$claim['token']);
						} elseif ($hasFailed) {

							$failed_inc += 1;
							$failedThisTick += 1;
							$queueError = isset($failureMessages[(int)$product_id]) ? (string)$failureMessages[(int)$product_id] : 'Content generation failed';
							$this->queue->markFailed((int)$job->id, (int)$product_id, (string)$claim['token'], $queueError);
						} else {

							$failed_inc += 1;
							$failedThisTick += 1;
							$this->queue->markFailed((int)$job->id, (int)$product_id, (string)$claim['token'], 'No products were processed');
						}
                        } catch (\Throwable $t) {

                            $providerName = ucfirst((string)($options['provider'] ?? 'API'));
                            $formattedErr = \AIPT\Core\ErrorHandler::format_connection_error($t->getMessage(), $providerName);

                            $this->result_processor->recordFailure((int)$job->id, (int)$product_id, $formattedErr);
                            $this->queue->markFailed((int)$job->id, (int)$product_id, (string)$claim['token'], $formattedErr);
                            $failed_inc += 1;
                            $failedThisTick += 1;

                            $this->recordErrorForThrottling((int)$job->id);

                            if (stripos($t->getMessage(), 'insufficient credits') !== false || stripos($t->getMessage(), 'insufficient optimization requests') !== false) {
                                $exitReason = 'paused_due_to_insufficient_credits';
                                $this->job_manager->updateJobStatus((int)$job->id, 'paused', 'Insufficient optimization requests for Advanced Generator. Job paused.');
                                $this->logger->warning((int)$job->id, 'Job paused due to insufficient optimization requests', ['type' => 'CREDITS']);
                                if ($currentClaimToken) { $this->queue->releaseClaimByToken((int)$job->id, (string)$currentClaimToken); }
                                break 2;
                            }
                        }

                            $itemCounter++;
                            unset($results);
                            if (($itemCounter % AutomationConstants::GC_COLLECT_CYCLES_INTERVAL) === 0 && function_exists('gc_collect_cycles')) {
                                gc_collect_cycles();
                            }
                            if (function_exists('memory_get_usage')) {
                                $mem = memory_get_usage(true);
                                if ($mem > $memorySoftLimit) {
                                    $exitReason = 'memory_soft_limit';
                                    $this->logger->warning((int)$job->id, 'Soft memory limit reached, stopping tick early', [
                                        'type' => 'SYSTEM',
                                        'memory_usage_bytes' => (int) $mem,
                                        'soft_limit_bytes' => (int) $memorySoftLimit,
                                    ]);
                                    break 1;
                                }
                            }

                        if (($processed_inc + $failed_inc) >= self::PROGRESS_FLUSH_EVERY || (time() - $last_flush) >= 5) {
                            $this->progress_tracker->increment((int)$job->id, (int)$processed_inc, (int)$failed_inc);
                            $processed_inc = 0;
                            $failed_inc = 0;
                            $last_flush = time();
                        }
                    }
                }
                if ($exitReason === 'tick_completed') {
                    $exitReason = ($processedThisTick > 0 || $failedThisTick > 0) ? 'work_done' : 'tick_idle';
                }

                if ($processed_inc > 0 || $failed_inc > 0) {
                    $this->progress_tracker->increment((int)$job->id, (int)$processed_inc, (int)$failed_inc);
                }

                if ($processedThisTick > 0 && $failedThisTick === 0) {
                    $this->resetErrorThrottling((int) $job->id);
                }

                $endState = $this->describeJobProductState((int) $job->id);
                $tickDurationMs = (int) round((microtime(true) - $tickStartTs) * 1000);
                $logContext = array_merge($endState, [
                    'exit_reason' => $exitReason,
                    'processed_this_tick' => $processedThisTick,
                    'failed_this_tick' => $failedThisTick,
                    'claim_batches' => $claimAttempts,
                    'tick_duration_ms' => $tickDurationMs,
                ]);
                if ($this->shouldLogDebugSnapshot((int) $job->id, 'tick_finished', $logContext)) {
                    $this->logger->debug($job_id, 'Tick finished', $logContext);
                }

                $freshJob = $this->job_manager->getJob($job_id);
                if ($freshJob && $freshJob->status === 'paused') {
                    return true;
                }

                $jobs_table = $this->wpdb->prefix . 'aipt_automation_jobs';
                $now = current_time('mysql');
                

                $completed = $this->wpdb->query(
                    $this->wpdb->prepare(
                        "UPDATE {$jobs_table} 
                         SET status = 'completed', completed_at = %s, updated_at = %s, progress = 100
                         WHERE id = %d 
                         AND status = 'running'
                         AND NOT EXISTS (
                             SELECT 1 FROM {$this->job_products_table} 
                             WHERE job_id = %d AND status IN ('pending', 'processing')
                         )",
                        $now,
                        $now,
                        (int)$job->id,
                        (int)$job->id
                    )
                );

                if ($completed > 0) {

                    $this->logger->info($job_id, 'Job completed successfully', $logContext);
                } else {

                    $remaining = (int) $this->wpdb->get_var(
                        $this->wpdb->prepare(
                            "SELECT COUNT(*) FROM {$this->job_products_table} WHERE job_id = %d AND status IN ('pending','processing')",
                            (int)$job->id
                        )
                    );

                    if ($remaining > 0) {
                        \AIPT\Api\AutomationJobs\JobExecutionHandler::register_job_hook((int)$job->id);
                        $hook = 'aipt_execute_job_' . (int)$job->id;
                        $scheduleTimestamp = time() + AutomationConstants::rescheduleDelay();
                        $scheduled = \wp_schedule_single_event($scheduleTimestamp, $hook, [(int)$job->id]);
                        $nextScheduled = \wp_next_scheduled($hook, [(int)$job->id]);

                        $scheduleContext = array_merge($logContext, [
                            'remaining_items' => $remaining,
                            'next_tick_due_at' => $nextScheduled ? gmdate('c', $nextScheduled) : null,
                            'schedule_result' => $scheduled === false ? 'failed' : 'scheduled',
                        ]);

                        if ($this->shouldLogDebugSnapshot((int)$job->id, 'tick_rescheduled', $scheduleContext)) {
                            $this->logger->debug($job_id, 'Tick rescheduled', $scheduleContext);
                        }

                        if ($scheduled === false) {
                            $this->logger->warning($job_id, 'Failed to schedule next tick', array_merge($scheduleContext, [
                                'hook' => $hook,
                            ]));
                        }

                        $this->dispatchAsyncRunner();
                    }
                }

                return true;
            } finally {
                $this->releaseLock((int) $job_id);
            }
        } catch (\Exception $e) {
            $this->logger->error($job_id, 'Job execution failed', [
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
                'type' => 'SYSTEM'
            ]);

            $this->job_manager->updateJobStatus($job_id, 'failed', $e->getMessage());
            throw $e;
        }
    }

    /**
     * Pause a running job
     *
     * @param int $job_id Job ID to pause
     * @return bool Success status
     * @throws \Exception On pause failure
     */
    public function pauseJob($job_id) {
        $job = $this->job_manager->getJob($job_id);
        if (!$job) {

            throw new \Exception("Job not found: {$job_id}");
        }

        if (!$job->canBePaused()) {

            throw new \Exception("Job cannot be paused in current status: {$job->status}");
        }

        $this->job_manager->updateJobStatus($job_id, 'paused');
        $this->logger->info($job_id, 'Job paused by user');

        if (function_exists('delete_transient')) {
            delete_transient('aipt_job_monitor_' . (int) $job_id);
        }

        $hook = 'aipt_execute_job_' . (int) $job_id;
        if (function_exists('wp_clear_scheduled_hook')) {
            wp_clear_scheduled_hook($hook, [(int) $job_id]);
        }

        return true;
    }

    /**
     * Resume a paused job
     *
     * @param int $job_id Job ID to resume
     * @return bool Success status
     * @throws \Exception On resume failure
     */
    public function resumeJob($job_id) {
        $job = $this->job_manager->getJob($job_id);
        if (!$job) {

            throw new \Exception("Job not found: {$job_id}");
        }

        if (!$job->canBeResumed()) {

            throw new \Exception("Job cannot be resumed in current status: {$job->status}");
        }

        $this->job_manager->updateJobStatus($job_id, 'running');
        $this->logger->info($job_id, 'Job resumed by user');

        if (function_exists('delete_transient')) {
            delete_transient('aipt_job_monitor_' . (int) $job_id);
        }

        \AIPT\Api\AutomationJobs\JobExecutionHandler::register_job_hook($job_id);

        $hook = 'aipt_execute_job_' . $job_id;
        $scheduled = \wp_schedule_single_event(time() + AutomationConstants::IMMEDIATE_EXECUTION_DELAY, $hook, [$job_id]);

        if ($scheduled === false) {
            $this->logger->error($job_id, 'Failed to schedule job resume');
        } else {
            $this->logger->info($job_id, 'Job resume scheduled successfully');
        }

        return true;
    }

    /**
     * Cancel a job
     *
     * @param int $job_id Job ID to cancel
     * @return bool Success status
     * @throws \Exception On cancel failure
     */
    public function cancelJob($job_id) {
        $job = $this->job_manager->getJob($job_id);
        if (!$job) {

            throw new \Exception("Job not found: {$job_id}");
        }

        if ($job->isCompleted()) {
            throw new \Exception("Cannot cancel completed job");
        }

        $this->job_manager->updateJobStatus($job_id, 'failed', 'Job cancelled by user');
        $this->logger->info($job_id, 'Job cancelled by user');

        return true;
    }

    /**
     * Get job execution progress
     *
     * @param int $job_id Job ID
     * @return array Progress information
     */
    public function getJobProgress($job_id) {
        $job = $this->job_manager->getJob($job_id);
        if (!$job) {
            return [];
        }

        return [
            'progress' => $job->progress,
            'total_products' => $job->total_products,
            'processed_products' => $job->processed_products,
            'failed_products' => $job->failed_products,
            'status' => $job->status,
            'started_at' => $job->started_at,
            'estimated_completion' => $this->estimateCompletion($job)
        ];
    }

    /**
     * Check if executor can handle specific tool type
     *
     * @param string $tool_type Tool type to check
     * @return bool Whether tool type is supported
     */
    public function supportsToolType($tool_type) {
        return in_array($tool_type, ['advanced', 'standard', 'title', 'faq', 'seo_meta', 'image_alt']);
    }

    /**
     * Validate job before execution
     *
     * @param AutomationJob $job Job to validate
     * @return array Validation errors (empty if valid)
     */
    public function validateJobForExecution(AutomationJob $job) {
        $errors = [];

        if (!$this->supportsToolType($job->tool_type)) {
            $errors[] = "Unsupported tool type: {$job->tool_type}";
        }

        if (!in_array($job->status, ['ready', 'preparing', 'running'])) {
            $errors[] = "Job cannot be executed in current status: {$job->status}";
        }

        if (empty($job->settings['provider'])) {
            $errors[] = "AI provider not specified in job settings";
        }

        if (empty($job->settings['model'])) {
            $errors[] = "AI model not specified in job settings";
        }

        return $errors;
    }

    /**
     * Get AI service for tool type
     *
     * @param string $tool_type Tool type
     * @return object AI service instance
     * @throws \Exception If tool type is not supported
     */
    private function getAIService($tool_type) {
        switch ($tool_type) {
            case 'advanced':
                return new ContentGenerator();
            case 'standard':
                return new StandardGenerator();
            case 'title':
                return new TitleGeneratorService();
            case 'faq':
                return new FAQGeneratorService();
            case 'seo_meta':
                return new SeoMetaGeneratorService();
            case 'image_alt':
                return new ImageAltService();
            default:

                throw new \Exception("Unsupported tool type: {$tool_type}");
        }
    }

    /**
     * Get products to process for a job
     *
     * @param int $job_id Job ID
     * @return array Product IDs
     */
    private function getJobProducts($job_id) {

        $claim = $this->queue->claimBatch((int) $job_id, AutomationConstants::maxBatchSize());
        if (empty($claim['product_ids'])) {
            return [];
        }
        return $claim['product_ids'];
    }

    /**
     * Process products in batches
     *
     * @param AutomationJob $job Job instance
     * @param object $service AI service instance
     * @param array $products Product IDs to process
     */
    private function processBatches(AutomationJob $job, $service, $products) {
        $batch_start_time = time();
        $total_products = count($products);
        $batch_count = 0;
        $max_batch_size = AutomationConstants::maxBatchSize();

        $this->logger->info($job->id, 'Starting batch processing', [
            'total_products' => $total_products,
            'batch_size' => $max_batch_size,
            'estimated_batches' => ceil($total_products / $max_batch_size)
        ]);

        foreach (array_chunk($products, $max_batch_size) as $batch) {
            $batch_count++;

            $this->logger->info($job->id, "Processing batch {$batch_count}", [
                'batch_size' => count($batch),
                'product_ids' => implode(',', $batch)
            ]);

            if ($this->shouldStopExecution($job->id, $batch_start_time)) {
                $this->logger->warning($job->id, 'Stopping execution due to timeout or pause');
                break;
            }

            $this->processBatch($job, $service, $batch);

            $this->logger->info($job->id, "Batch {$batch_count} completed");
        }

        $this->logger->info($job->id, 'Batch processing completed', [
            'total_batches_processed' => $batch_count,
            'execution_time' => time() - $batch_start_time . ' seconds'
        ]);
    }

    /**
     * Process a single batch of products
     *
     * @param AutomationJob $job Job instance
     * @param object $service AI service instance
     * @param array $batch Product IDs batch
     */
    private function processBatch(AutomationJob $job, $service, $batch) {
        try {
            $this->logger->info($job->id, 'Starting batch processing', [
                'batch_size' => count($batch),
                'product_ids' => implode(',', $batch)
            ]);

            $this->logger->info($job->id, 'Translating job settings to service options');
            $options = $this->options_translator->translate($job->settings, $job->tool_type);
            $this->logger->info($job->id, 'Service options prepared', [
                'options' => $options,
                'tool_type' => $job->tool_type
            ]);

            if ($job->tool_type === 'advanced' && $service instanceof ContentGenerator) {
                $fs_manager = \AIPT\Core\FsManager::getInstance();
                $plan_type = $fs_manager->getPlanType();

                if ($plan_type !== 'business') {
                    try {
                        $quantity = count($batch); 
                        $details = [
                            'context' => 'automation_job',
                            'job_id' => (int)$job->id,
                            'tool_type' => $job->tool_type,
                            'provider' => $options['provider'] ?? null,
                            'model' => $options['model'] ?? null,
                            'batch_size' => $quantity,
                        ];
                        $credit = \AIPT\Core\CreditSystem\CreditManager::getInstance();
                        $success = $credit->use('description', $quantity, $details);
                        if (!$success) {
                            $this->logger->error($job->id, 'Insufficient optimization requests for advanced generator batch', [
                                'quantity' => $quantity,
                            ]);
                            throw new \Exception('Insufficient credits');
                        }
                        $this->logger->info($job->id, 'Optimization requests reserved for advanced generator batch', [
                            'quantity' => $quantity
                        ]);
                    } catch (\Throwable $t) {

                        throw $t;
                    }
                }
            }

            $this->logger->info($job->id, 'Calling AI service for content generation', [
                'service_class' => get_class($service)
            ]);

            $results = $this->callAIServiceWithRetry($service, $batch, $options, $job);

            $this->logger->info($job->id, 'AI service call completed', [
                'results_count' => is_array($results) ? count($results) : 'not_array',
                'results_type' => gettype($results)
            ]);

            $this->logger->info($job->id, 'Processing AI service results');
            $counts = $this->processResults($job->id, $results);
            $this->progress_tracker->increment($job->id, $counts['completed'] ?? 0, $counts['failed'] ?? 0);

            $this->logger->info($job->id, 'Batch processed successfully', [
                'batch_size' => count($batch),
                'results_count' => is_array($results) ? count($results) : 'not_array'
            ]);

        } catch (\Exception $e) {
            $providerName = ucfirst((string)($options['provider'] ?? 'API'));
            $formattedErr = \AIPT\Core\ErrorHandler::format_connection_error($e->getMessage(), $providerName);

            $this->result_processor->recordFailuresBulk($job->id, $batch, $formattedErr);
            $this->progress_tracker->increment($job->id, 0, count($batch));

            $classification = $this->classifyException($e);
            $this->logger->error($job->id, 'Batch processing failed', [
                'batch_size' => count($batch),
                'error' => $formattedErr,
                'raw_error' => $e->getMessage(),
                'type' => $classification['type'],
                'retryable' => $classification['retryable'],
                'code' => $classification['code'],
                'provider' => $providerName
            ]);

            if (stripos($e->getMessage(), 'insufficient credits') !== false || stripos($e->getMessage(), 'insufficient optimization requests') !== false) {
                $this->job_manager->updateJobStatus((int) $job->id, 'paused', 'Insufficient optimization requests for Advanced Generator. Job paused.');
                $this->logger->warning($job->id, 'Job paused due to insufficient optimization requests', ['type' => 'CREDITS']);
            }
        }
    }

    /**
     * Translate job settings to service options
     *
     * @param array $job_settings Job settings
     * @param string $tool_type Tool type
     * @return array Service options
     */
    private function translateJobSettings($job_settings, $tool_type) {
        $options = [
            'provider' => $job_settings['provider'] ?? 'openai',
            'model' => $job_settings['model'] ?? 'gpt-4.1-mini',
            'language' => $job_settings['language'] ?? 'English',
            'writing_style' => $job_settings['writing_style'] ?? 'Professional'
        ];

        if (in_array($tool_type, ['advanced', 'standard'])) {
            $options['generateDescription'] = $job_settings['generateDescription'] ?? true;
            $options['generateShortDescription'] = $job_settings['generateShortDescription'] ?? false;
            $options['generateTags'] = $job_settings['generateTags'] ?? false;
        }

        if ($tool_type === 'title') {
            $options['maxLength'] = $job_settings['maxLength'] ?? AutomationConstants::DEFAULT_TITLE_MAX_LENGTH;
        }

        return $options;
    }

    /**
     * Check if execution should stop
     *
     * @param int $job_id Job ID
     * @param int $start_time Batch start time
     * @return bool Whether to stop execution
     */
    private function shouldStopExecution($job_id, $start_time) {

        if (time() - $start_time > AutomationConstants::maxBatchExecutionTime()) {
            $this->logger->warning($job_id, 'Batch execution timeout reached');
            return true;
        }

        $job = $this->job_manager->getJob($job_id);
        if ($job && $job->status === 'paused') {
            $this->logger->info($job_id, 'Job execution stopped - job was paused');
            return true;
        }

        return false;
    }

    /**
     * Update products status
     *
     * @param int $job_id Job ID
     * @param array $product_ids Product IDs
     * @param string $status New status
     * @param string|null $error_message Error message
     */
    private function updateProductsStatus($job_id, $product_ids, $status, $error_message = null) {
        $job_id = (int) $job_id;
        if (empty($product_ids)) {
            return;
        }
        $ids = array_map('intval', (array) $product_ids);
        if (empty($ids)) {
            return;
        }
        $placeholders = implode(',', array_fill(0, count($ids), '%d'));

        $set_parts = ['status = %s'];
        $params = [$status];

        if ($status === 'completed') {
            $set_parts[] = 'processed_at = %s';
            $params[] = current_time('mysql');
        }

        if ($error_message !== null && $error_message !== '') {
            $set_parts[] = 'error_message = %s';
            $params[] = $error_message;
        } else {
            $set_parts[] = 'error_message = NULL';
        }

        $params_with_ids = $params;
        $params_with_ids[] = $job_id;
        $params_with_ids = array_merge($params_with_ids, $ids);

        $sql = $this->wpdb->prepare(
            "UPDATE {$this->job_products_table} SET " . implode(', ', $set_parts) . " WHERE job_id = %d AND product_id IN ($placeholders)",
            $params_with_ids
        );

        $this->wpdb->query($sql);

    }

    /**
     * Process generation results
     *
     * @param int $job_id Job ID
     * @param array $results Generation results
     */
    private function processResults($job_id, $results) {

        $job = $this->job_manager->getJob($job_id);
        if (!$job) {
            $this->logger->error($job_id, 'Job not found when processing results');
            return ['completed' => 0, 'failed' => 0, 'failure_messages' => []];
        }

        $content_settings = [];
        if (!empty($job->settings['contentSettings'])) {
            $content_settings = $job->settings['contentSettings'];
        } elseif (!empty($job->settings['content_settings'])) {
            $content_settings = $job->settings['content_settings'];
        }

        $this->logger->info($job_id, 'Raw job settings for content parsing', [
            'all_settings' => $job->settings,
            'contentSettings_key' => $job->settings['contentSettings'] ?? 'not_found',
            'content_settings_key' => $job->settings['content_settings'] ?? 'not_found'
        ]);

        $content_status = $content_settings['status'] ?? 'draft'; 

        $normalized = strtolower(is_string($content_status) ? $content_status : 'draft');
        $content_settings['status'] = ($normalized === 'draft') ? 'draft' : 'published';
        if (isset($job->settings['writeMode'])) {
            $content_settings['writeMode'] = $job->settings['writeMode'];
        }
        $content_settings['created_by'] = (int) $job->created_by;

        $this->logger->info($job_id, 'Processing results with content status: ' . $content_settings['status'], [
            'results_count' => count($results),
            'content_settings' => $content_settings,
            'raw_results_sample' => array_slice($results, 0, 2) 
        ]);

        $content_items = [];
        $error_items = []; 

        if (is_array($results)) {
            foreach ($results as $rawItem) {
                if (!is_array($rawItem)) { continue; }

                if (!empty($rawItem['api_error'])) {
                    $providerName = isset($rawItem['provider']) ? (string)$rawItem['provider'] : 'API';
                    $productId = isset($rawItem['product_id']) ? (int)$rawItem['product_id'] : null;
                    $formatted = \AIPT\Core\ErrorHandler::format_api_error_message($providerName, (string)$rawItem['api_error']);
                    $truncated = $this->truncateErrorMessage($formatted, AutomationConstants::MAX_ERROR_MESSAGE_LENGTH);
                    
                    if ($productId) {
                        $this->logger->error($job_id, $formatted, [
                            'type' => 'AI_PROVIDER',
                            'product_id' => $productId,
                            'provider' => $providerName,
                        ]);

                        if (!isset($error_items[$productId])) {
                            $error_items[$productId] = $truncated;
                        }
                    } else {

                        $this->logger->warning($job_id, 'API error without product context: ' . $formatted, [
                            'type' => 'AI_PROVIDER',
                            'provider' => $providerName,
                            'api_error' => (string)$rawItem['api_error'],
                        ]);
                    }
                    continue;
                }

                $isAttachmentLevelImageError = !empty($rawItem['error'])
                    && isset($rawItem['attachment_id'], $rawItem['status'])
                    && (int) $rawItem['attachment_id'] > 0
                    && in_array(sanitize_text_field((string)$rawItem['status']), ['error', 'rejected'], true);

                if (!empty($rawItem['error']) && empty($rawItem['type']) && !$isAttachmentLevelImageError) {
                    $productId = isset($rawItem['product_id']) ? (int)$rawItem['product_id'] : null;
                    if ($productId && !isset($error_items[$productId])) {
                        $error_items[$productId] = $this->truncateErrorMessage((string)$rawItem['error'], AutomationConstants::MAX_ERROR_MESSAGE_LENGTH);
                    }
                    continue;
                }

                $content_items[] = $rawItem;
            }
        }

        $unified_results = [];
        
        if (!empty($content_items)) {
            $unified_results = $this->result_processor->transformToUnifiedFormat($job_id, $content_items);
        }

        $completed_count = 0;
        $failed_count = 0;
        $failure_messages = [];
        $processed_product_ids = []; 

        foreach ($error_items as $productId => $errorMsg) {
            if (!isset($processed_product_ids[$productId])) {
                $this->result_processor->recordFailure($job_id, $productId, $errorMsg);
                $processed_product_ids[$productId] = true;
                $failed_count++;
            }
        }

        foreach ($unified_results as $result) {
            if (!isset($result['product_id']) || !is_numeric($result['product_id'])) {
                $this->logger->warning($job_id, 'Skipping result without valid product_id', [
                    'product_id' => $result['product_id'] ?? null,
                    'fields' => array_keys($result),
                ]);
                continue;
            }

            $productId = (int)$result['product_id'];
            $result['created_by'] = (int) $job->created_by;
            

            if (isset($processed_product_ids[$productId])) {
                continue;
            }

            if (!$this->result_processor->hasApplicableContent($result)) {
                $this->logger->warning($job_id, ResultProcessor::NO_APPLICABLE_CONTENT_MESSAGE, [
                    'product_id' => $productId,
                    'type' => 'AI_RESPONSE',
                ]);
                $this->result_processor->recordFailure($job_id, $productId, ResultProcessor::NO_APPLICABLE_CONTENT_MESSAGE);
                $processed_product_ids[$productId] = true;
                $failure_messages[$productId] = ResultProcessor::NO_APPLICABLE_CONTENT_MESSAGE;
                $failed_count++;
                continue;
            }

            try {
                $this->result_processor->applyResult($result, $content_settings, $this->draft_manager);
                $this->result_processor->recordSuccess($job_id, $productId, $result);
                $processed_product_ids[$productId] = true;
                $completed_count++;
            } catch (\Exception $e) {
                $this->logger->error($job_id, 'Failed to save result', [
                    'product_id' => $productId,
                    'error' => $e->getMessage()
                ]);
                $this->result_processor->recordFailure($job_id, $productId, 'Save failed: ' . $e->getMessage());
                $processed_product_ids[$productId] = true;
                $failed_count++;
            }
        }

        return ['completed' => $completed_count, 'failed' => $failed_count, 'failure_messages' => $failure_messages];

    }

    /**
     * Truncate error message to specified length with ellipsis.
     *
     * @param string $message Message to truncate
     * @param int $maxLength Maximum length
     * @return string Truncated message
     */
    private function truncateErrorMessage(string $message, int $maxLength): string {

        if ($maxLength <= 3) {
            return $maxLength > 0 ? str_repeat('.', $maxLength) : '';
        }

        $len = function_exists('mb_strlen') ? mb_strlen($message) : strlen($message);
        if ($len > $maxLength) {
            $truncateAt = $maxLength - 3; 
            return (function_exists('mb_substr') ? mb_substr($message, 0, $truncateAt) : substr($message, 0, $truncateAt)) . '...';
        }
        return $message;
    }

    /**
     * Transform AI Generator results to expected format
     *
     * All AI generators (Standard, Advanced, Title) return results like:
     * [
     *   ['product_id' => 123, 'type' => 'short', 'text' => 'content'],
     *   ['product_id' => 123, 'type' => 'full', 'text' => 'content'],
     *   ['product_id' => 123, 'type' => 'title', 'text' => 'content']
     * ]
     *
     * But saveToDraftSystem expects:
     * [
     *   ['product_id' => 123, 'short_description' => 'content', 'description' => 'content', 'title' => 'content']
     * ]
     *
     * @param int $job_id Job ID
     * @param array $results Raw results from generator
     * @return array Transformed results
     */
    private function transformStandardGeneratorResults($job_id, $results) {

        $is_ai_generator_format = false;
        if (!empty($results) && is_array($results)) {
            $first_result = reset($results);
            if (isset($first_result['type']) && isset($first_result['text']) && isset($first_result['product_id'])) {
                $is_ai_generator_format = true;
            }
        }

        if (!$is_ai_generator_format) {
            $this->logger->info($job_id, 'Results are not in AI Generator format, returning as-is');
            return $results;
        }

        $this->logger->info($job_id, 'Detected AI Generator format, transforming results', [
            'original_count' => count($results)
        ]);

        $grouped_results = [];
        foreach ($results as $result) {
            if (!isset($result['product_id']) || !isset($result['type']) || !isset($result['text'])) {
                continue;
            }

            $product_id = $result['product_id'];
            if (!isset($grouped_results[$product_id])) {
                $grouped_results[$product_id] = ['product_id' => $product_id];
            }

            switch ($result['type']) {
                case 'short':
                    $grouped_results[$product_id]['short_description'] = $result['text'];
                    break;
                case 'full':
                    $grouped_results[$product_id]['description'] = $result['text'];
                    break;
                case 'tags':
                    $grouped_results[$product_id]['tags'] = $result['text'];
                    break;
                case 'title':
                    $grouped_results[$product_id]['title'] = $result['text'];
                    break;
                default:
                    $this->logger->warning($job_id, 'Unknown AI Generator type: ' . $result['type']);
                    break;
            }
        }

        $transformed_results = array_values($grouped_results);

        $this->logger->info($job_id, 'AI Generator results transformed successfully', [
            'original_count' => count($results),
            'transformed_count' => count($transformed_results),
            'sample_transformed' => array_slice($transformed_results, 0, 1)
        ]);

        return $transformed_results;
    }

    /**
     * Save result to draft system
     *
     * @param int $job_id Job ID
     * @param array $result Generation result
     * @param array $content_settings Content settings
     */
    private function saveToDraftSystem($job_id, $result, $content_settings) {
        $product_id = $result['product_id'];
        $created_by = isset($content_settings['created_by']) ? (int) $content_settings['created_by'] : null;

        $this->logger->info($job_id, 'Saving result to draft system', [
            'product_id' => $product_id,
            'result_keys' => array_keys($result)
        ]);

        if (isset($result['title'])) {
            $this->draft_manager->saveDraftContent($job_id, $product_id, 'title', $result['title'], null, $created_by);
        }

        if (isset($result['description'])) {
            $this->draft_manager->saveDraftContent($job_id, $product_id, 'description', $result['description'], null, $created_by);
        }

        if (isset($result['short_description'])) {
            $this->draft_manager->saveDraftContent($job_id, $product_id, 'short_description', $result['short_description'], null, $created_by);
        }

        if (isset($result['tags'])) {
            $tags_content = is_array($result['tags']) ? implode(', ', $result['tags']) : $result['tags'];
            $this->draft_manager->saveDraftContent($job_id, $product_id, 'tags', $tags_content, null, $created_by);
        }

        $this->logger->info($job_id, 'Result saved to draft system successfully', ['product_id' => $product_id]);
    }

    /**
     * Apply result directly to product
     *
     * @param int $job_id Job ID
     * @param array $result Generation result
     */
    private function applyToProduct($job_id, $result) {
        $product_id = $result['product_id'];

        $this->logger->info($job_id, 'Applying result directly to product', [
            'product_id' => $product_id,
            'result_keys' => array_keys($result)
        ]);

        $product = \wc_get_product($product_id);
        if (!$product) {

            throw new \Exception('Product not found: ' . $product_id);
        }

        if (isset($result['title'])) {
            $product->set_name($result['title']);
        }

        if (isset($result['description'])) {
            $product->set_description($result['description']);
        }

        if (isset($result['short_description'])) {
            $product->set_short_description($result['short_description']);
        }

        if (isset($result['tags'])) {
            $tag_names = is_array($result['tags']) ? $result['tags'] : explode(',', $result['tags']);
            $tag_ids = [];

            foreach ($tag_names as $tag_name) {
                $tag_name = trim($tag_name);
                if (!empty($tag_name)) {
                    $term = \get_term_by('name', $tag_name, 'product_tag');
                    if (!$term) {
                        $term = \wp_insert_term($tag_name, 'product_tag');
                        if (!is_wp_error($term)) {
                            $tag_ids[] = $term['term_id'];
                        }
                    } else {
                        $tag_ids[] = $term->term_id;
                    }
                }
            }

            if (!empty($tag_ids)) {
                \wp_set_post_terms($product_id, $tag_ids, 'product_tag');
            }
        }

        $product->save();

        $this->logger->info($job_id, 'Result applied to product successfully', ['product_id' => $product_id]);
    }

    /**
     * Update job progress
     *
     * @param int $job_id Job ID
     */
    private function updateJobProgress($job_id) {

        $stats = $this->wpdb->get_row(
            $this->wpdb->prepare(
                "SELECT
                    COUNT(*) as total,
                    SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
                    SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed
                FROM {$this->job_products_table}
                WHERE job_id = %d",
                $job_id
            ),
            ARRAY_A
        );

        if ($stats) {
            $this->job_manager->updateJobProgress(
                $job_id,
                (int) $stats['completed'],
                (int) $stats['failed']
            );
        }
    }

    /**
     * Check if job is completed
     *
     * @param int $job_id Job ID
     */
    private function checkJobCompletion($job_id) {

        $pending_count = $this->wpdb->get_var(
            $this->wpdb->prepare(
                "SELECT COUNT(*) FROM {$this->job_products_table}
                WHERE job_id = %d AND status IN ('pending', 'processing')",
                $job_id
            )
        );

        if ($pending_count == 0) {
            $this->job_manager->updateJobStatus($job_id, 'completed');
            $this->logger->info($job_id, 'Job completed successfully');
        }
    }

    /**
     * Describe current job product queue state for logging/diagnostics.
     *
     * @param int $job_id
     * @return array<string,mixed>
     */
    private function describeJobProductState(int $job_id): array {

        $row = $this->wpdb->get_row(
            $this->wpdb->prepare(
                "SELECT
                    SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending,
                    SUM(CASE WHEN status = 'processing' THEN 1 ELSE 0 END) AS processing,
                    SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
                    SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
                    MIN(CASE WHEN status = 'processing' THEN processing_started_at END) AS oldest_processing_started_at,
                    MAX(CASE WHEN status = 'processing' THEN processing_started_at END) AS newest_processing_started_at,
                    MIN(CASE WHEN status = 'pending' THEN created_at END) AS oldest_pending_created_at
                FROM {$this->job_products_table}
                WHERE job_id = %d",
                $job_id
            ),
            ARRAY_A
        );

        $state = [
            'pending' => 0,
            'processing' => 0,
            'completed' => 0,
            'failed' => 0,
        ];

        if (is_array($row)) {
            $state['pending'] = (int) ($row['pending'] ?? 0);
            $state['processing'] = (int) ($row['processing'] ?? 0);
            $state['completed'] = (int) ($row['completed'] ?? 0);
            $state['failed'] = (int) ($row['failed'] ?? 0);

            $totalRows = array_sum([$state['pending'], $state['processing'], $state['completed'], $state['failed']]);
            $state['total_rows'] = $totalRows;

            $now = time();
            if (!empty($row['oldest_processing_started_at'])) {
                $ts = strtotime($row['oldest_processing_started_at']);
                if ($ts !== false) {
                    $state['oldest_processing_started_at'] = $row['oldest_processing_started_at'];
                    $state['oldest_processing_age_sec'] = max(0, $now - $ts);
                }
            }

            if (!empty($row['newest_processing_started_at'])) {
                $ts = strtotime($row['newest_processing_started_at']);
                if ($ts !== false) {
                    $state['newest_processing_started_at'] = $row['newest_processing_started_at'];
                    $state['newest_processing_age_sec'] = max(0, $now - $ts);
                }
            }

            if (!empty($row['oldest_pending_created_at'])) {
                $ts = strtotime($row['oldest_pending_created_at']);
                if ($ts !== false) {
                    $state['oldest_pending_created_at'] = $row['oldest_pending_created_at'];
                    $state['oldest_pending_age_sec'] = max(0, $now - $ts);
                }
            }
        } else {
            $state['total_rows'] = 0;
        }

        return $state;
    }

    /**
     * Determine soft memory ceiling for ticks based on environment limits.
     *
     * @return int
     */
    private function getSoftMemoryLimitBytes(): int {
        $default = AutomationConstants::DEFAULT_MEMORY_LIMIT_MB * 1024 * 1024;
        $limits = [];

        if (\function_exists('wp_convert_hr_to_bytes')) {
            $iniLimit = \ini_get('memory_limit');
            if ($iniLimit && $iniLimit !== '-1') {
                $bytes = \wp_convert_hr_to_bytes($iniLimit);
                if ($bytes > 0) {
                    $limits[] = (int) floor($bytes * 0.7); 
                }
            }

            if (\defined('WP_MEMORY_LIMIT') && \WP_MEMORY_LIMIT !== '-1') {
                $wpLimit = \wp_convert_hr_to_bytes(\WP_MEMORY_LIMIT);
                if ($wpLimit > 0) {
                    $limits[] = (int) floor($wpLimit * 0.7);
                }
            }
        }

        if (empty($limits)) {
            return $default;
        }
        $computed = min($limits);
        return (int) min($computed, $default);
    }

    /**
     * Get adaptive window size based on server capacity and recent performance
     *
     * Adjusts batch size dynamically:
     * - Reduces if memory pressure is high
     * - Reduces if recent errors detected
     * - Increases if system is healthy and fast
     *
     * @param int $job_id Job ID for error rate lookup
     * @return int Optimal window size for current conditions
     */
    private function getAdaptiveWindowSize(int $job_id): int {
        $base = AutomationConstants::windowSize();

        if (function_exists('memory_get_usage')) {
            $currentMem = memory_get_usage(true);
            $softLimit = $this->getSoftMemoryLimitBytes();

            $memRatio = $softLimit > 0 ? ($currentMem / $softLimit) : 0;

            if ($memRatio > 0.8) {

                $base = (int) floor($base * 0.5);
            } elseif ($memRatio > 0.6) {

                $base = (int) floor($base * 0.75);
            }
        }

        $errorRateKey = 'aipt_job_error_rate_' . $job_id;
        $recentErrors = 0;
        if (function_exists('get_transient')) {
            $recentErrors = (int) get_transient($errorRateKey);
        }

        if ($recentErrors >= AutomationConstants::ERROR_THROTTLE_THRESHOLD) {
            $base = (int) floor($base * 0.5);
        } elseif ($recentErrors >= 2) {

            $base = (int) floor($base * 0.75);
        }

        $maxExecTime = (int) ini_get('max_execution_time');
        if ($maxExecTime > 0 && $maxExecTime < 30) {

            $base = (int) floor($base * 0.6);
        }

        return max(self::MIN_WINDOW_SIZE, min(self::MAX_WINDOW_SIZE, $base));
    }

    /**
     * Record error for adaptive throttling
     *
     * @param int $job_id Job ID
     */
    private function recordErrorForThrottling(int $job_id): void {
        if (!function_exists('get_transient') || !function_exists('set_transient')) {
            return;
        }

        $key = 'aipt_job_error_rate_' . $job_id;
        $current = (int) get_transient($key);
        set_transient($key, $current + 1, AutomationConstants::ERROR_THROTTLE_WINDOW_SECONDS);
    }

    /**
     * Reset error counter on successful processing
     *
     * @param int $job_id Job ID
     */
    private function resetErrorThrottling(int $job_id): void {
        if (function_exists('delete_transient')) {
            delete_transient('aipt_job_error_rate_' . $job_id);
        }
    }

    /**
     * Prevent noisy debug logs by caching snapshot hashes per job/slug.
     */
    private function shouldLogDebugSnapshot(int $job_id, string $slug, array $context, int $ttl = AutomationConstants::DEBUG_SNAPSHOT_TTL): bool {
        if (!function_exists('get_transient') || !function_exists('set_transient')) {
            return true;
        }

        try {
            ksort($context);
        } catch (\Throwable $t) {

        }

        $hash = md5(wp_json_encode($context));
        $key = 'aipt_jobdbg_' . $slug . '_' . $job_id;
        $stored = get_transient($key);
        if ($stored === $hash) {
            return false;
        }

        set_transient($key, $hash, $ttl);
        return true;
    }
    /**
     * Acquire per-job execution lock using atomic add_option()
     *
     * Uses add_option() for atomic check-and-set to prevent TOCTOU race conditions.
     * add_option() returns false if option already exists, providing atomicity.
     */
    private function acquireLock(int $job_id): bool {
        $key = 'aipt_job_lock_' . $job_id;
        $token = wp_generate_password(AutomationConstants::LOCK_TOKEN_LENGTH, false, false);
        $lock_data = wp_json_encode([
            'token'   => $token,
            'expires' => time() + AutomationConstants::lockTtl(),
        ]);

        $acquired = add_option($key, $lock_data, '', 'no');

        if ($acquired) {
            $this->lock_tokens[$job_id] = $token;
            return true;
        }

        $existing = get_option($key);
        if ($existing) {
            $data = json_decode($existing, true);
            if (is_array($data) && isset($data['expires']) && time() > (int) $data['expires']) {

                wp_cache_delete($key, 'options');
                $fresh_existing = get_option($key);
                $fresh_data = json_decode($fresh_existing, true);

                if (is_array($fresh_data) && isset($fresh_data['expires']) && time() > (int) $fresh_data['expires']) {

                    delete_option($key);

                    $acquired = add_option($key, $lock_data, '', 'no');
                } else {

                    $acquired = false;
                }
                if ($acquired) {
                    $this->lock_tokens[$job_id] = $token;
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Release per-job execution lock (only if we own it)
     *
     * Only releases the lock if our stored token matches the one in the option,
     * preventing accidental release of another process's lock.
     */
    private function releaseLock(int $job_id): void {
        $key = 'aipt_job_lock_' . $job_id;
        $myToken = $this->lock_tokens[$job_id] ?? null;

        if ($myToken) {
            $existing = get_option($key);
            if ($existing) {
                $data = json_decode($existing, true);
                if (is_array($data) && isset($data['token']) && $data['token'] === $myToken) {
                    delete_option($key);
                }
            }
        }

        unset($this->lock_tokens[$job_id]);
    }

    /**
     * Estimate job completion time using actual processing times
     *
     * Uses moving average of real processing durations from job_products table
     * to provide accurate ETA that excludes cron idle time between ticks.
     *
     * @param AutomationJob $job Job instance
     * @return string|null Estimated completion time in Y-m-d H:i:s format
     */
    private function estimateCompletion(AutomationJob $job) {
        if ($job->status !== 'running' || $job->processed_products == 0) {
            return null;
        }

		$remaining_products = max(0, $job->total_products - ($job->processed_products + $job->failed_products));
        if ($remaining_products <= 0) {
            return null;
        }

        $sample_size = min(AutomationConstants::MAX_SAMPLE_SIZE, max(AutomationConstants::MIN_SAMPLE_SIZE, $job->processed_products));
        $avg_seconds = $this->wpdb->get_var(
            $this->wpdb->prepare(
                "SELECT AVG(TIMESTAMPDIFF(SECOND, processing_started_at, processed_at))
                 FROM (
                     SELECT processing_started_at, processed_at
                     FROM {$this->job_products_table}
                     WHERE job_id = %d
                       AND status = 'completed'
                       AND processing_started_at IS NOT NULL
                       AND processed_at IS NOT NULL
                       AND processed_at > processing_started_at
                     ORDER BY processed_at DESC
                     LIMIT %d
                 ) AS recent_products",
                (int) $job->id,
                $sample_size
            )
        );

        if (!$avg_seconds || $avg_seconds <= 0) {
            $elapsed_time = time() - strtotime($job->started_at);
            if ($elapsed_time > 0 && $job->processed_products > 0) {
                $avg_seconds = $elapsed_time / $job->processed_products;
            } else {
                return null;
            }
        }

        $cron_overhead_per_item = AutomationConstants::rescheduleDelay() / AutomationConstants::windowSize();
        $effective_avg = (float) $avg_seconds + $cron_overhead_per_item;

        $estimated_seconds = $remaining_products * $effective_avg;

        $max_seconds = AutomationConstants::MAX_COMPLETION_ESTIMATE_DAYS * DAY_IN_SECONDS;
        if ($estimated_seconds > $max_seconds) {
            $estimated_seconds = $max_seconds;
        }

        return gmdate('Y-m-d H:i:s', time() + (int) $estimated_seconds);
    }

    /**
     * Call AI service with lightweight retry for transient errors
     *
     * @param object $service
     * @param array $batch
     * @param array $options
     * @param AutomationJob $job
     * @return array
     * @throws \Exception
     */
    private function callAIServiceWithRetry($service, array $batch, array $options, AutomationJob $job) {
        $maxAttempts = 3;
        $attempt = 0;
        $lastException = null;

        while ($attempt < $maxAttempts) {
            $attempt++;
            try {
                if ($service instanceof StandardGenerator) {
                    $this->logger->info($job->id, 'Using StandardGenerator::bulkGenerate()', ['attempt' => $attempt]);
                    return $service->bulkGenerate($batch, $options);
                } elseif ($service instanceof TitleGeneratorService || $service instanceof FAQGeneratorService || $service instanceof SeoMetaGeneratorService || $service instanceof ImageAltService) {
                    $this->logger->info($job->id, 'Using ' . get_class($service) . '::bulk_generate()', ['attempt' => $attempt]);
                    return $service->bulk_generate($batch, $options);
                } elseif ($service instanceof ContentGenerator) {
                    $this->logger->info($job->id, 'Using ContentGenerator::generate()', ['attempt' => $attempt]);

                    $contentTypes = $this->extractContentTypes($options);
                    $allResults = [];
                    foreach ($batch as $productId) {
                        $productResult = $service->generateForProduct((int)$productId, $contentTypes, $options);
                        $transformed = $this->transformContentGeneratorResult($productResult, (int)$productId);
                        $allResults = array_merge($allResults, $transformed);
                    }
                    return $allResults;
                }

                $this->logger->error($job->id, 'Unknown service type', ['service_class' => get_class($service), 'type' => 'SYSTEM']);
                throw new \Exception('Unknown service type: ' . get_class($service));
            } catch (\Throwable $e) {
                $lastException = $e instanceof \Exception ? $e : new \Exception($e->getMessage(), (int) $e->getCode(), $e);
                $classification = $this->classifyException($lastException);

                $formattedErr = \AIPT\Core\ErrorHandler::format_connection_error($lastException->getMessage(), ucfirst((string)($options['provider'] ?? 'API')));
                $this->logger->warning($job->id, 'AI service call failed', [
                    'attempt' => $attempt,
                    'max_attempts' => $maxAttempts,
                    'error' => $formattedErr,
                    'raw_error' => $lastException->getMessage(),
                    'type' => $classification['type'],
                    'retryable' => $classification['retryable'],
                    'code' => $classification['code'],
                    'provider' => $options['provider'] ?? null,
                    'model' => $options['model'] ?? null,
                ]);

                if (!$classification['retryable'] || $attempt >= $maxAttempts) {
                    throw $lastException;
                }

                usleep(250000 * $attempt);
            }
        }

        if ($lastException) {
            throw $lastException;
        }
        throw new \Exception('Unknown error during AI service call.');
    }

    /**
     * Classify exception into a logical error category and retryability.
     *
     * @param \Throwable $e
     * @return array{type:string,retryable:bool,code:int|string|null}
     */
    private function classifyException($e): array {
        $msg = strtolower((string) $e->getMessage());
        $code = method_exists($e, 'getCode') ? $e->getCode() : null;

        if (stripos($msg, 'rate limit') !== false || stripos($msg, 'too many requests') !== false || (string)$code === '429') {
            return ['type' => 'RATE_LIMIT', 'retryable' => true, 'code' => $code];
        }

        if (stripos($msg, 'api key') !== false || stripos($msg, 'unauthorized') !== false || stripos($msg, 'invalid api key') !== false || (string)$code === '401') {
            return ['type' => 'AUTH', 'retryable' => false, 'code' => $code];
        }

        if (stripos($msg, 'timed out') !== false || stripos($msg, 'timeout') !== false || stripos($msg, 'connection refused') !== false || stripos($msg, 'curl error') !== false || stripos($msg, 'network') !== false) {
            return ['type' => 'NETWORK', 'retryable' => true, 'code' => $code];
        }

        if (stripos($msg, 'woocommerce') !== false || stripos($msg, 'product save') !== false || stripos($msg, 'product not found') !== false) {
            return ['type' => 'WC_UPDATE', 'retryable' => false, 'code' => $code];
        }

        if (stripos($msg, 'sqlstate') !== false || stripos($msg, 'mysql') !== false || stripos($msg, 'database') !== false) {
            return ['type' => 'DB', 'retryable' => false, 'code' => $code];
        }

        if (stripos($msg, 'validation') !== false || stripos($msg, 'invalid') !== false) {
            return ['type' => 'VALIDATION', 'retryable' => false, 'code' => $code];
        }

        if (stripos($msg, 'insufficient credits') !== false || stripos($msg, 'credit') !== false || stripos($msg, 'insufficient optimization requests') !== false || stripos($msg, 'optimization request') !== false) {
            return ['type' => 'CREDITS', 'retryable' => false, 'code' => $code];
        }

        return ['type' => 'AI_PROVIDER', 'retryable' => false, 'code' => $code];
    }

    /**
     * Extract content types from options for ContentGenerator
     *
     * Converts boolean flags (generateDescription, generateShortDescription, generateTags)
     * to an array of content type strings that ContentGenerator expects.
     *
     * @param array $options Job options containing boolean content type flags
     * @return array Array of content type strings (e.g., ['description', 'short_description', 'tags'])
     */
    private function extractContentTypes(array $options): array {
        $types = [];

        if (!empty($options['generateDescription'])) {
            $types[] = 'description';
        }
        if (!empty($options['generateShortDescription'])) {
            $types[] = 'short_description';
        }
        if (!empty($options['generateTags'])) {
            $types[] = 'tags';
        }

        if (empty($types)) {
            $types[] = 'description';
        }

        return $types;
    }

    /**
     * Transform ContentGenerator ProductResult to expected array format
     *
     * ContentGenerator returns ProductResult objects with a different structure.
     * This method transforms them to the array format expected by the automation pipeline.
     *
     * @param ProductResult $result The ProductResult from ContentGenerator
     * @param int $productId The product ID
     * @return array Array of result items compatible with automation pipeline
     */
    private function transformContentGeneratorResult($result, int $productId): array {
        $output = [];

        if (is_object($result)) {
            $content = $result->content ?? [];
            $success = $result->success ?? false;

            if (!$success && !empty($result->errors)) {

                return [
                    [
                        'product_id' => $productId,
                        'error' => implode(', ', $result->errors),
                    ]
                ];
            }

            foreach ($content as $type => $text) {
                if (empty($text)) {
                    continue;
                }

                $typeMap = [
                    'short_description' => 'short',
                    'description' => 'full',
                    'tags' => 'tags',
                ];
                $dbType = $typeMap[$type] ?? $type;

                $output[] = [
                    'product_id' => $productId,
                    'type' => $dbType,
                    'text' => $text,
                ];
            }

            if (empty($output) && !$success) {
                $errorMsg = !empty($result->errors) ? implode(', ', $result->errors) : 'AI provider returned no content';
                $output[] = [
                    'product_id' => $productId,
                    'error' => $errorMsg,
                ];
            }
        } elseif (is_array($result)) {

            foreach ($result as $item) {
                if (is_array($item)) {
                    $item['product_id'] = $item['product_id'] ?? $productId;
                    $output[] = $item;
                }
            }
        }

        return $output;
    }

    /**
     * Dispatch async runner to trigger immediate cron execution.
     *
     * Attempts multiple methods to trigger cron processing immediately
     * instead of waiting for external page hits. This significantly
     * improves job processing speed on low-traffic sites.
     */
    private function dispatchAsyncRunner(): void {
        if (class_exists('ActionScheduler_AsyncRequest_QueueRunner')) {
            try {
                $async_request = new \ActionScheduler_AsyncRequest_QueueRunner(
                    \ActionScheduler::store()
                );
                $async_request->maybe_dispatch();
            } catch (\Exception $e) {

            }
        }

        if (!\defined('DISABLE_WP_CRON') || !\DISABLE_WP_CRON) {
            if (\function_exists('spawn_cron')) {
                \spawn_cron();
            }
        }
    }

}
