<?php
if (!defined('ABSPATH')) exit;

if (!class_exists('VMO_Media_Replacement_Jobs')) {
    class VMO_Media_Replacement_Jobs {
        const JOBS_OPTION = 'vmo_media_replacement_jobs';
        const PROCESS_HOOK = 'vmo_process_media_replacement_job_event';
        const LOCK_PREFIX = 'vmo_media_replacement_job_lock_';

        public function __construct() {
            add_action('wp_ajax_vmo_estimate_media_replacement_job', array($this, 'ajax_estimate_job'));
            add_action('wp_ajax_vmo_create_media_replacement_job', array($this, 'ajax_create_job'));
            add_action('wp_ajax_vmo_get_media_replacement_job', array($this, 'ajax_get_job'));
            add_action('wp_ajax_vmo_process_media_replacement_job', array($this, 'ajax_process_job'));
            add_action(self::PROCESS_HOOK, array($this, 'process_job_event'));
        }

        public function ajax_estimate_job() {
            check_ajax_referer('vmo_builder_nonce', 'security');

            if (!is_user_logged_in()) {
                wp_send_json_error(array('message' => 'Not logged in.'), 403);
            }

            $request = $this->sanitize_job_request($_POST);
            wp_send_json_success($this->estimate_for_request(get_current_user_id(), $request));
        }

        public function ajax_create_job() {
            check_ajax_referer('vmo_builder_nonce', 'security');

            if (!is_user_logged_in()) {
                wp_send_json_error(array('message' => 'Not logged in.'), 403);
            }

            $user_id = get_current_user_id();
            $request = $this->sanitize_job_request($_POST);
            $estimate = $this->estimate_for_request($user_id, $request);
            if (in_array($request['replacement_type'], array('library', 'upload'), true) && empty($request['url'])) {
                wp_send_json_error(array('message' => 'Select or upload media before creating a replacement job.'), 400);
            }

            if (!in_array($request['replacement_type'], array('library', 'upload'), true) && trim($request['prompt']) === '') {
                wp_send_json_error(array('message' => 'Describe the media to generate before creating a replacement job.'), 400);
            }

            if (empty($estimate['can_start'])) {
                wp_send_json_error(array(
                    'message' => 'Not enough credits for this media generation.',
                    'code' => 'insufficient_tokens',
                    'required_tokens' => intval($estimate['required_tokens']),
                    'current_balance' => intval($estimate['current_balance']),
                    'lower_cost_options' => $estimate['lower_cost_options'],
                    'estimate' => $estimate,
                ), 402);
            }

            $job_id = 'vmo_media_job_' . wp_generate_password(18, false, false);
            $now = current_time('mysql');
            $is_immediate = in_array($request['replacement_type'], array('library', 'upload'), true);

            $job = array(
                'id' => $job_id,
                'user_id' => $user_id,
                'project_id' => intval($request['project_id']),
                'slot_context' => $request['slot_context'],
                'media_type' => $request['media_type'],
                'replacement_type' => $request['replacement_type'],
                'prompt' => $request['prompt'],
                'model' => $request['model'],
                'seconds' => $request['seconds'],
                'size' => $request['size'],
                'voice' => $request['voice'],
                'source_url' => $request['url'],
                'url' => $is_immediate ? $request['url'] : '',
                'attachment_id' => $is_immediate ? intval($request['attachment_id']) : 0,
                'status' => $is_immediate ? 'complete' : 'queued',
                'stage' => $is_immediate ? 'complete' : 'queued',
                'stage_label' => $is_immediate ? 'Ready to insert' : 'Job created and queued',
                'progress' => $is_immediate ? 100 : 2,
                'estimated_seconds_remaining' => $is_immediate ? 0 : intval($estimate['estimated_seconds']),
                'token_cost' => intval($estimate['required_tokens']),
                'token_balance_at_request' => intval($estimate['current_balance']),
                'tokens_charged' => false,
                'tokens_refunded' => false,
                'error' => '',
                'plain_language_error' => '',
                'retry_available' => true,
                'recoveries' => $estimate['recovery_options'],
                'lower_cost_options' => $estimate['lower_cost_options'],
                'created_at' => $now,
                'updated_at' => $now,
                'completed_at' => $is_immediate ? $now : '',
                'process_token' => wp_generate_password(40, false, false),
                'workflow' => $this->workflow_stages(),
            );

            if ($is_immediate) {
                $job['stage_label'] = $request['replacement_type'] === 'upload' ? 'Uploaded media is ready' : 'Library media is ready';
                $this->attach_slot_context_to_attachment($job);
            }

            $this->save_job($job);

            if (!$is_immediate) {
                $this->dispatch_async($job_id);
                $this->schedule_fallback($job_id);
            }

            wp_send_json_success(array('job' => $this->public_job($job), 'estimate' => $estimate));
        }

        public function ajax_get_job() {
            check_ajax_referer('vmo_builder_nonce', 'security');

            if (!is_user_logged_in()) {
                wp_send_json_error(array('message' => 'Not logged in.'), 403);
            }

            $job_id = sanitize_text_field(wp_unslash($_POST['job_id'] ?? ''));
            $job = $this->get_job($job_id);
            if (!$job || intval($job['user_id'] ?? 0) !== get_current_user_id()) {
                wp_send_json_error(array('message' => 'Media job not found.'), 404);
            }

            if ($this->job_has_pending_work($job)) {
                $this->dispatch_async($job_id);
                $this->schedule_fallback($job_id);
            }

            wp_send_json_success(array('job' => $this->public_job($job)));
        }

        public function ajax_process_job() {
            $this->prepare_long_running_runtime();

            $job_id = sanitize_text_field(wp_unslash($_REQUEST['job_id'] ?? ''));
            $token = sanitize_text_field(wp_unslash($_REQUEST['token'] ?? ''));
            $job = $this->get_job($job_id);

            if (!$job || !$this->authorize_process_request($job, $token)) {
                wp_send_json_error(array('message' => 'Unauthorized media job request.'), 403);
            }

            $job = $this->process_job($job_id);
            if (!$job) {
                wp_send_json_error(array('message' => 'Media job not found.'), 404);
            }

            wp_send_json_success(array('job' => $this->public_job($job)));
        }

        public function process_job_event($job_id) {
            $this->prepare_long_running_runtime();
            $this->process_job(sanitize_text_field($job_id));
        }

        protected function sanitize_job_request($source) {
            $slot_context = $this->decode_slot_context($source['slot_context'] ?? ($source['context'] ?? array()));
            $media_type = $this->normalize_media_type($source['media_type'] ?? ($source['type'] ?? ($slot_context['media_type'] ?? 'image')));
            $replacement_type = $this->normalize_replacement_type($source['replacement_type'] ?? ($source['job_type'] ?? 'ai_image'), $media_type);
            $model = sanitize_text_field(wp_unslash($source['model'] ?? ''));
            $seconds = sanitize_text_field(wp_unslash($source['seconds'] ?? ''));

            if ($replacement_type === 'sora_pro' || $replacement_type === 'ai_video_pro') {
                $model = 'sora-2-pro';
                $replacement_type = 'ai_video';
            }

            if ($replacement_type === 'sora_standard' || $replacement_type === 'ai_video_standard') {
                $model = 'sora-2';
                $replacement_type = 'ai_video';
            }

            if ($replacement_type === 'ai_video' && $model === '') {
                $model = $media_type === 'video' ? 'sora-2' : 'sora-2';
            }

            if ($replacement_type === 'ai_image' && $model === '') {
                $model = 'openai-image-generation-2';
            }

            if ($replacement_type === 'ai_audio' && $model === '') {
                $model = 'tts-1-hd';
            }

            if (!in_array($seconds, array('4', '8', '12'), true)) {
                $seconds = '4';
            }

            $project_id = intval($source['project_id'] ?? $source['post_id'] ?? 0);
            if (!$project_id && !empty($slot_context['project_id'])) {
                $project_id = intval($slot_context['project_id']);
            }

            $slot_context['media_type'] = $media_type;
            if (empty($slot_context['slot_id'])) {
                $slot_context['slot_id'] = sanitize_text_field($slot_context['media_id'] ?? $slot_context['mediaId'] ?? '');
            }

            return array(
                'project_id' => $project_id,
                'slot_context' => $slot_context,
                'media_type' => $media_type,
                'replacement_type' => $replacement_type,
                'prompt' => sanitize_textarea_field(wp_unslash($source['prompt'] ?? $source['desc'] ?? '')),
                'model' => $model,
                'seconds' => $seconds,
                'size' => sanitize_text_field(wp_unslash($source['size'] ?? '1280x720')),
                'voice' => sanitize_text_field(wp_unslash($source['voice'] ?? 'alloy')),
                'url' => esc_url_raw(wp_unslash($source['url'] ?? $source['source_url'] ?? '')),
                'attachment_id' => intval($source['attachment_id'] ?? 0),
            );
        }

        protected function decode_slot_context($raw_context) {
            if (is_string($raw_context)) {
                $decoded = json_decode(wp_unslash($raw_context), true);
                $raw_context = is_array($decoded) ? $decoded : array();
            }

            if (!is_array($raw_context)) {
                return array();
            }

            $allowed = array(
                'slot_id', 'slotId', 'media_id', 'mediaId', 'page_id', 'pageId', 'section_id', 'sectionId',
                'media_type', 'mediaType', 'product_id', 'productId', 'placement_role', 'placementRole',
                'current_attachment_id', 'currentAttachmentId', 'selector', 'target_selector', 'allowed_replacement_types',
                'allowedReplacementTypes', 'block_id', 'blockId', 'current_src', 'currentSrc', 'target_tag', 'targetTag',
                'project_id', 'post_id', 'label', 'alt_text', 'source', 'schema'
            );
            $context = array();

            foreach ($allowed as $key) {
                if (!array_key_exists($key, $raw_context)) {
                    continue;
                }

                $snake_key = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $key));
                $value = $raw_context[$key];
                if (is_array($value)) {
                    $context[$snake_key] = array_values(array_filter(array_map('sanitize_text_field', array_map('strval', $value))));
                } else {
                    $context[$snake_key] = sanitize_text_field(wp_unslash((string) $value));
                }
            }

            if (empty($context['media_id']) && !empty($context['mediaid'])) {
                $context['media_id'] = $context['mediaid'];
            }
            if (empty($context['slot_id']) && !empty($context['slotid'])) {
                $context['slot_id'] = $context['slotid'];
            }
            if (empty($context['page_id']) && !empty($context['pageid'])) {
                $context['page_id'] = $context['pageid'];
            }
            if (empty($context['section_id']) && !empty($context['sectionid'])) {
                $context['section_id'] = $context['sectionid'];
            }
            if (empty($context['product_id']) && !empty($context['productid'])) {
                $context['product_id'] = $context['productid'];
            }
            if (empty($context['placement_role']) && !empty($context['placementrole'])) {
                $context['placement_role'] = $context['placementrole'];
            }
            if (empty($context['current_attachment_id']) && !empty($context['currentattachment_id'])) {
                $context['current_attachment_id'] = $context['currentattachment_id'];
            }
            if (empty($context['current_src']) && !empty($context['currentsrc'])) {
                $context['current_src'] = $context['currentsrc'];
            }
            if (empty($context['target_tag']) && !empty($context['targettag'])) {
                $context['target_tag'] = $context['targettag'];
            }
            if (empty($context['allowed_replacement_types']) && !empty($context['allowedreplacement_types'])) {
                $context['allowed_replacement_types'] = $context['allowedreplacement_types'];
            }

            return $context;
        }

        protected function normalize_media_type($type) {
            $type = strtolower(sanitize_key((string) $type));
            if (in_array($type, array('video', 'audio', 'logo'), true)) {
                return $type;
            }
            return 'image';
        }

        protected function normalize_replacement_type($type, $media_type) {
            $type = strtolower(str_replace('-', '_', sanitize_key((string) $type)));
            if (in_array($type, array('library', 'upload', 'ai_image', 'ai_video', 'ai_audio', 'sora_pro', 'sora_standard', 'ai_video_pro', 'ai_video_standard'), true)) {
                return $type;
            }
            if ($media_type === 'video') {
                return 'ai_video';
            }
            if ($media_type === 'audio') {
                return 'ai_audio';
            }
            return 'ai_image';
        }

        protected function estimate_for_request($user_id, array $request) {
            $cost = $this->token_cost_for_request($request);
            $balance = function_exists('vmo_get_token_balance') ? vmo_get_token_balance($user_id) : 0;
            $has_bypass = function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id);
            $seconds = $this->estimated_seconds_for_request($request);

            return array(
                'required_tokens' => $cost,
                'current_balance' => $balance,
                'can_start' => $has_bypass || $balance >= $cost,
                'estimated_seconds' => $seconds,
                'estimated_remaining_text' => $this->format_eta($seconds),
                'media_type' => $request['media_type'],
                'replacement_type' => $request['replacement_type'],
                'model' => $request['model'],
                'seconds' => $request['seconds'],
                'lower_cost_options' => $this->lower_cost_options($request),
                'recovery_options' => $this->recovery_options($request),
            );
        }

        protected function token_cost_for_request(array $request) {
            if (in_array($request['replacement_type'], array('library', 'upload'), true)) {
                return 0;
            }

            if ($request['replacement_type'] === 'ai_video') {
                if (class_exists('VMO_Sora_Video_Generator')) {
                    return (int) VMO_Sora_Video_Generator::get_token_cost($request['model'] ?: 'sora-2', $request['seconds'] ?: '4');
                }
                $operation = ($request['model'] === 'sora-2-pro' ? 'sora_video_' : 'sora_std_') . ($request['seconds'] ?: '4') . 's';
                return function_exists('vmo_get_token_cost') ? (int) vmo_get_token_cost($operation) : 12600;
            }

            if ($request['replacement_type'] === 'ai_audio') {
                $base = function_exists('vmo_get_token_cost') ? (int) vmo_get_token_cost('tts') : 81;
                $per_1k = function_exists('vmo_get_token_cost') ? (int) vmo_get_token_cost('tts_per_1k') : 405;
                $units = max(1, (int) ceil(strlen($request['prompt']) / 1000));
                return max($base, $units * $per_1k);
            }

            return function_exists('vmo_get_token_cost') ? (int) vmo_get_token_cost('dalle_image') : 1080;
        }

        protected function estimated_seconds_for_request(array $request) {
            if (in_array($request['replacement_type'], array('library', 'upload'), true)) {
                return 0;
            }
            if ($request['replacement_type'] === 'ai_video') {
                return max(90, intval($request['seconds']) * 35);
            }
            if ($request['replacement_type'] === 'ai_audio') {
                return max(20, (int) ceil(strlen($request['prompt']) / 120));
            }
            return 75;
        }

        protected function lower_cost_options(array $request) {
            $options = array();
            if ($request['replacement_type'] === 'ai_video') {
                $options[] = array('label' => 'Use Sora 2 Standard, 4 seconds', 'replacement_type' => 'ai_video', 'model' => 'sora-2', 'seconds' => '4', 'token_cost' => class_exists('VMO_Sora_Video_Generator') ? VMO_Sora_Video_Generator::get_token_cost('sora-2', '4') : (function_exists('vmo_get_token_cost') ? vmo_get_token_cost('sora_std_4s') : 12600));
                $options[] = array('label' => 'Upload a video instead', 'replacement_type' => 'upload', 'token_cost' => 0);
                $options[] = array('label' => 'Pick an existing Media Library video', 'replacement_type' => 'library', 'token_cost' => 0);
                return $options;
            }
            if ($request['replacement_type'] === 'ai_image') {
                $options[] = array('label' => 'Upload an image instead', 'replacement_type' => 'upload', 'token_cost' => 0);
                $options[] = array('label' => 'Pick an existing Media Library image', 'replacement_type' => 'library', 'token_cost' => 0);
                return $options;
            }
            if ($request['replacement_type'] === 'ai_audio') {
                $options[] = array('label' => 'Upload audio instead', 'replacement_type' => 'upload', 'token_cost' => 0);
                $options[] = array('label' => 'Pick existing audio from Media Library', 'replacement_type' => 'library', 'token_cost' => 0);
            }
            return $options;
        }

        protected function recovery_options(array $request) {
            $options = array(
                array('action' => 'retry', 'label' => 'Retry this job'),
                array('action' => 'upload', 'label' => 'Upload media instead'),
                array('action' => 'library', 'label' => 'Choose from Media Library'),
            );
            if ($request['replacement_type'] === 'ai_video') {
                $options[] = array('action' => 'lower_cost', 'label' => 'Try shorter Sora 2 Standard video');
            }
            return $options;
        }

        protected function process_job($job_id) {
            $job = $this->get_job($job_id);
            if (!$job) {
                return null;
            }

            if (!$this->job_has_pending_work($job)) {
                return $job;
            }

            if (!$this->acquire_lock($job_id)) {
                return $job;
            }

            try {
                switch ($job['replacement_type']) {
                    case 'ai_video':
                        $job = $this->process_video_job($job);
                        break;
                    case 'ai_audio':
                        $job = $this->process_audio_job($job);
                        break;
                    case 'ai_image':
                    default:
                        $job = $this->process_image_job($job);
                        break;
                }

                $this->save_job($job);
                if ($this->job_has_pending_work($job)) {
                    $this->dispatch_async($job_id);
                    $this->schedule_fallback($job_id);
                }
                return $job;
            } finally {
                $this->release_lock($job_id);
            }
        }

        protected function process_image_job(array $job) {
            $this->mark_stage($job, 'token_checked', 10, 'Checking credits');
            if (!$this->ensure_tokens_charged($job, 'queued image replacement generation')) {
                return $this->fail_job($job, 'Insufficient credits for image generation.', 'Choose an existing image, upload a new image, or add credits before retrying.');
            }

            $this->mark_stage($job, 'generating', 35, 'Generating image');
            $this->save_job($job);

            if (!function_exists('vmo_generate_image_asset')) {
                $this->refund_tokens($job, 'Image helper unavailable - refund');
                return $this->fail_job($job, 'Image generation is unavailable right now.', 'Upload an image or try again after the generator is configured.');
            }

            $context = is_array($job['slot_context'] ?? null) ? $job['slot_context'] : array();
            $result = vmo_generate_image_asset(array(
                'user_id' => intval($job['user_id']),
                'desc' => $job['prompt'],
                'position' => sanitize_text_field($context['placement_role'] ?? 'replacement'),
                'size' => '1024x1024',
                'section' => sanitize_text_field($context['section_id'] ?? 'builder-media-replacement'),
                'project_id' => intval($job['project_id']),
                'charge_tokens' => false,
                'require_consultation' => false,
                'model' => $job['model'],
            ));

            if (!empty($result['error'])) {
                $this->refund_tokens($job, 'Image replacement generation failed - refund');
                return $this->fail_job($job, $result['error'], 'Try a different prompt, upload media, or choose an existing Media Library item.');
            }

            $this->mark_stage($job, 'optimizing', 85, 'Optimizing generated image');
            $job['url'] = esc_url_raw($result['url'] ?? '');
            $job['attachment_id'] = intval($result['attachment_id'] ?? 0);
            $this->attach_slot_context_to_attachment($job);
            $this->mark_stage($job, 'inserting', 95, 'Ready to insert into the exact slot');
            return $this->complete_job($job);
        }

        protected function process_video_job(array $job) {
            $generator = $this->get_video_generator();
            if (!$generator) {
                return $this->fail_job($job, 'Video generation is unavailable right now.', 'Upload a video or choose an existing Media Library video.');
            }

            if (empty($job['remote_job_id'])) {
                $this->mark_stage($job, 'token_checked', 10, 'Checking credits');
                if (!$this->ensure_tokens_charged($job, 'queued video replacement generation')) {
                    return $this->fail_job($job, 'Insufficient credits for video generation.', 'Use Sora 2 Standard at 4 seconds, upload a video, or add credits before retrying.');
                }

                $this->mark_stage($job, 'generating', 22, 'Submitting video job to Sora');
                $this->save_job($job);

                $video_options = array(
                    'model' => $job['model'] ?: 'sora-2',
                    'seconds' => $job['seconds'] ?: '4',
                    'size' => $job['size'] ?: '1280x720',
                );

                if (!empty($job['source_url'])) {
                    $video_options['input_reference'] = esc_url_raw($job['source_url']);
                }

                $result = $generator->generate_video($job['prompt'], $video_options);

                if (!empty($result['error'])) {
                    $this->refund_tokens($job, 'Video replacement generation failed - refund');
                    return $this->fail_job($job, $result['error'], 'Try a lower-cost Sora option, upload a video, or choose an existing clip.');
                }

                $job['remote_job_id'] = sanitize_text_field($result['video_id'] ?? '');
                $this->mark_stage($job, 'generating', 35, 'Sora is generating the video');
                return $job;
            }

            $status = $generator->get_video_status($job['remote_job_id']);
            if (!empty($status['error'])) {
                $this->mark_stage($job, 'generating', max(35, intval($job['progress'] ?? 35)), 'Waiting for Sora status');
                return $job;
            }

            $remote_status = strtolower((string) ($status['status'] ?? ''));
            $remote_progress = intval($status['progress'] ?? 0);
            $progress = max(35, min(82, 35 + (int) round($remote_progress * 0.47)));
            $this->mark_stage($job, 'generating', $progress, 'Sora is generating the video');
            $job['remote_status'] = $remote_status;

            if ($remote_status === 'failed' || $remote_status === 'error') {
                $this->refund_tokens($job, 'Video replacement generation failed - refund');
                $message = is_array($status['error'] ?? null) ? sanitize_text_field($status['error']['message'] ?? 'Video generation failed.') : sanitize_text_field($status['error'] ?? 'Video generation failed.');
                return $this->fail_job($job, $message, 'Retry, use a shorter standard video, upload media, or select from the Media Library.');
            }

            if ($remote_status !== 'completed' && $remote_status !== 'succeeded') {
                return $job;
            }

            $this->mark_stage($job, 'optimizing', 88, 'Downloading and saving the video');
            $this->save_job($job);

            $download = $generator->download_and_save_video(
                $job['remote_job_id'],
                intval($job['user_id']),
                'VMO Replacement Video',
                array(
                    'video_type' => 'builder_replacement',
                    'prompt' => $job['prompt'],
                    'project_id' => intval($job['project_id']),
                    'slot_context' => $job['slot_context'],
                )
            );

            if (!empty($download['error'])) {
                return $this->fail_job($job, $download['error'], 'The Sora job finished, but saving it failed. Retry download or upload a video.');
            }

            $job['url'] = esc_url_raw($download['url'] ?? '');
            $job['attachment_id'] = intval($download['attachment_id'] ?? 0);
            $this->attach_slot_context_to_attachment($job);
            $this->mark_stage($job, 'inserting', 96, 'Ready to insert into the exact slot');
            return $this->complete_job($job);
        }

        protected function process_audio_job(array $job) {
            $this->mark_stage($job, 'token_checked', 10, 'Checking credits');
            if (!$this->ensure_tokens_charged($job, 'queued audio replacement generation')) {
                return $this->fail_job($job, 'Insufficient credits for audio generation.', 'Upload audio or choose an existing Media Library audio file.');
            }

            if (trim($job['prompt']) === '') {
                $this->refund_tokens($job, 'Audio replacement had empty text - refund');
                return $this->fail_job($job, 'Audio generation needs script text.', 'Add the narration or audio script, then retry.');
            }

            $api_key = get_option('vmo_openai_api_key', '');
            if (!$api_key) {
                $this->refund_tokens($job, 'Audio replacement missing API key - refund');
                return $this->fail_job($job, 'AI API key is missing.', 'Configure the AI API key, upload audio, or choose an existing audio file.');
            }

            $this->mark_stage($job, 'generating', 35, 'Generating audio');
            $this->save_job($job);

            $response = wp_remote_post('https://api.openai.com/v1/audio/speech', array(
                'headers' => array(
                    'Authorization' => 'Bearer ' . $api_key,
                    'Content-Type' => 'application/json',
                ),
                'body' => wp_json_encode(array(
                    'model' => $job['model'] ?: 'tts-1-hd',
                    'input' => $job['prompt'],
                    'voice' => $job['voice'] ?: 'alloy',
                    'response_format' => 'mp3',
                    'speed' => 1.0,
                )),
                'timeout' => 120,
            ));

            if (is_wp_error($response)) {
                $this->refund_tokens($job, 'Audio replacement generation request failed - refund');
                return $this->fail_job($job, $response->get_error_message(), 'Retry, upload audio, or choose an existing audio file.');
            }

            $code = wp_remote_retrieve_response_code($response);
            $body = wp_remote_retrieve_body($response);
            if ($code < 200 || $code >= 300 || $body === '') {
                $this->refund_tokens($job, 'Audio replacement generation failed - refund');
                return $this->fail_job($job, 'Audio generation failed.', 'Retry with shorter text, upload audio, or choose an existing file.');
            }

            $this->mark_stage($job, 'optimizing', 80, 'Saving audio to the Media Library');
            $tmp = wp_tempnam('vmo-generated-audio.mp3');
            if (!$tmp || file_put_contents($tmp, $body) === false) {
                $this->refund_tokens($job, 'Audio replacement temp file failed - refund');
                return $this->fail_job($job, 'Could not prepare generated audio for saving.', 'Retry or upload an audio file.');
            }

            if (!function_exists('media_handle_sideload')) {
                require_once ABSPATH . 'wp-admin/includes/image.php';
                require_once ABSPATH . 'wp-admin/includes/file.php';
                require_once ABSPATH . 'wp-admin/includes/media.php';
            }

            $file_array = array(
                'name' => 'vmo-generated-audio-' . time() . '.mp3',
                'tmp_name' => $tmp,
            );
            $attachment_id = media_handle_sideload($file_array, 0);
            if (is_wp_error($attachment_id)) {
                @unlink($tmp);
                $this->refund_tokens($job, 'Audio replacement media upload failed - refund');
                return $this->fail_job($job, 'Could not save generated audio.', 'Retry or upload an audio file.');
            }

            wp_update_post(array('ID' => $attachment_id, 'post_author' => intval($job['user_id'])));
            update_post_meta($attachment_id, '_vmo_audio_prompt', $job['prompt']);
            update_post_meta($attachment_id, '_vmo_project_id', intval($job['project_id']));

            $job['url'] = esc_url_raw(wp_get_attachment_url($attachment_id));
            $job['attachment_id'] = intval($attachment_id);
            $this->attach_slot_context_to_attachment($job);
            $this->mark_stage($job, 'inserting', 95, 'Ready to insert into the exact slot');
            return $this->complete_job($job);
        }

        protected function ensure_tokens_charged(array &$job, $description) {
            if (!empty($job['tokens_charged'])) {
                return true;
            }

            $cost = intval($job['token_cost'] ?? 0);
            if ($cost <= 0) {
                $job['tokens_charged'] = true;
                return true;
            }

            $user_id = intval($job['user_id']);
            if (function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id)) {
                $job['tokens_charged'] = true;
                return true;
            }

            if (!function_exists('vmo_deduct_tokens') || !vmo_deduct_tokens($user_id, $cost, 'media_replacement', $description)) {
                return false;
            }

            $job['tokens_charged'] = true;
            $job['tokens_charged_at'] = current_time('mysql');
            return true;
        }

        protected function refund_tokens(array &$job, $description) {
            $cost = intval($job['token_cost'] ?? 0);
            if (empty($job['tokens_charged']) || !empty($job['tokens_refunded']) || $cost <= 0) {
                return;
            }
            if (function_exists('vmo_add_tokens')) {
                vmo_add_tokens(intval($job['user_id']), $cost, 'media_replacement_refund', $description);
            } else {
                $current_balance = function_exists('vmo_get_token_balance') ? vmo_get_token_balance(intval($job['user_id'])) : (int) get_user_meta(intval($job['user_id']), 'vmo_token_balance', true);
                update_user_meta(intval($job['user_id']), 'vmo_token_balance', $current_balance + $cost);
            }
            $job['tokens_refunded'] = true;
            $job['tokens_refunded_at'] = current_time('mysql');
        }

        protected function complete_job(array $job) {
            $job['status'] = 'complete';
            $job['stage'] = 'complete';
            $job['stage_label'] = 'Complete and ready to insert';
            $job['progress'] = 100;
            $job['estimated_seconds_remaining'] = 0;
            $job['completed_at'] = current_time('mysql');
            $job['updated_at'] = current_time('mysql');
            $job['plain_language_error'] = '';
            $job['error'] = '';
            return $job;
        }

        protected function fail_job(array $job, $error, $plain_language_error = '') {
            $job['status'] = 'failed';
            $job['stage'] = 'failed';
            $job['stage_label'] = 'Media job needs attention';
            $job['progress'] = max(1, intval($job['progress'] ?? 1));
            $job['estimated_seconds_remaining'] = 0;
            $job['error'] = sanitize_text_field((string) $error);
            $job['plain_language_error'] = sanitize_text_field($plain_language_error ?: (string) $error);
            $job['failed_at'] = current_time('mysql');
            $job['updated_at'] = current_time('mysql');
            $job['retry_available'] = true;
            return $job;
        }

        protected function mark_stage(array &$job, $stage, $progress, $label) {
            $job['status'] = in_array($stage, array('queued', 'complete', 'failed'), true) ? $stage : 'processing';
            $job['stage'] = $stage;
            $job['stage_label'] = sanitize_text_field($label);
            $job['progress'] = max(0, min(100, intval($progress)));
            $job['updated_at'] = current_time('mysql');
            $elapsed = max(1, time() - strtotime($job['created_at'] ?? current_time('mysql')));
            $estimated_total = max($elapsed + 15, intval($job['estimated_seconds_remaining'] ?? 0) + $elapsed);
            if ($job['progress'] > 0 && $job['progress'] < 100) {
                $estimated_total = max($estimated_total, (int) round($elapsed / ($job['progress'] / 100)));
            }
            $job['estimated_seconds_remaining'] = max(5, $estimated_total - $elapsed);
        }

        protected function attach_slot_context_to_attachment(array $job) {
            $attachment_id = intval($job['attachment_id'] ?? 0);
            if (!$attachment_id) {
                return;
            }

            update_post_meta($attachment_id, '_vmo_media_asset_context', wp_json_encode(array(
                'schema' => 'vmo_media_asset_v1',
                'source' => 'media_replacement_job',
                'job_id' => $job['id'],
                'project_id' => intval($job['project_id']),
                'attachment_id' => $attachment_id,
                'media_type' => $job['media_type'],
                'slot_context' => $job['slot_context'],
                'slot_id' => $job['slot_context']['slot_id'] ?? '',
                'page_id' => $job['slot_context']['page_id'] ?? '',
                'section_id' => $job['slot_context']['section_id'] ?? '',
                'product_id' => $job['slot_context']['product_id'] ?? '',
                'placement_role' => $job['slot_context']['placement_role'] ?? '',
                'quality_status' => 'ready',
                'ai_classification_status' => 'assigned',
                'created_at' => current_time('mysql'),
            )));
        }

        protected function get_video_generator() {
            if (!empty($GLOBALS['vmo_sora_video_generator']) && $GLOBALS['vmo_sora_video_generator'] instanceof VMO_Sora_Video_Generator) {
                return $GLOBALS['vmo_sora_video_generator'];
            }
            return null;
        }

        protected function job_has_pending_work(array $job) {
            return !in_array($job['status'] ?? '', array('complete', 'failed'), true);
        }

        protected function public_job(array $job) {
            unset($job['process_token']);
            $job['estimated_remaining_text'] = $this->format_eta(intval($job['estimated_seconds_remaining'] ?? 0));
            return $job;
        }

        protected function workflow_stages() {
            return array(
                array('key' => 'queued', 'label' => 'Job created'),
                array('key' => 'token_checked', 'label' => 'Credit precheck'),
                array('key' => 'processing', 'label' => 'Processing'),
                array('key' => 'generating', 'label' => 'Generating'),
                array('key' => 'optimizing', 'label' => 'Optimizing'),
                array('key' => 'inserting', 'label' => 'Inserting'),
                array('key' => 'complete', 'label' => 'Complete'),
            );
        }

        protected function format_eta($seconds) {
            $seconds = intval($seconds);
            if ($seconds <= 0) {
                return 'Ready now';
            }
            if ($seconds < 60) {
                return $seconds . ' seconds';
            }
            $minutes = (int) ceil($seconds / 60);
            return $minutes . ' minute' . ($minutes === 1 ? '' : 's');
        }

        protected function get_jobs() {
            $jobs = get_option(self::JOBS_OPTION, array());
            return is_array($jobs) ? $jobs : array();
        }

        protected function get_job($job_id) {
            if ($job_id === '') {
                return null;
            }
            $jobs = $this->get_jobs();
            return is_array($jobs[$job_id] ?? null) ? $jobs[$job_id] : null;
        }

        protected function save_job(array $job) {
            $jobs = $this->get_jobs();
            $jobs[$job['id']] = $job;

            if (count($jobs) > 120) {
                uasort($jobs, function($a, $b) {
                    return strtotime($a['created_at'] ?? 'now') <=> strtotime($b['created_at'] ?? 'now');
                });
                $jobs = array_slice($jobs, -100, null, true);
            }

            update_option(self::JOBS_OPTION, $jobs, false);
        }

        protected function dispatch_async($job_id) {
            $job = $this->get_job($job_id);
            if (!$job || empty($job['process_token'])) {
                return;
            }

            wp_remote_post(admin_url('admin-ajax.php'), array(
                'timeout' => 1,
                'blocking' => false,
                'body' => array(
                    'action' => 'vmo_process_media_replacement_job',
                    'job_id' => $job_id,
                    'token' => $job['process_token'],
                ),
            ));
        }

        protected function schedule_fallback($job_id) {
            if (!wp_next_scheduled(self::PROCESS_HOOK, array($job_id))) {
                wp_schedule_single_event(time() + 45, self::PROCESS_HOOK, array($job_id));
            }
        }

        protected function authorize_process_request(array $job, $token) {
            return !empty($job['process_token']) && $token && hash_equals((string) $job['process_token'], (string) $token);
        }

        protected function acquire_lock($job_id) {
            $key = self::LOCK_PREFIX . sanitize_key($job_id);
            if (get_transient($key)) {
                return false;
            }
            set_transient($key, 1, 5 * MINUTE_IN_SECONDS);
            return true;
        }

        protected function release_lock($job_id) {
            delete_transient(self::LOCK_PREFIX . sanitize_key($job_id));
        }

        protected function prepare_long_running_runtime() {
            if (function_exists('set_time_limit')) {
                @set_time_limit(0);
            }
            if (function_exists('ignore_user_abort')) {
                @ignore_user_abort(true);
            }
            if (function_exists('ini_set')) {
                @ini_set('memory_limit', '512M');
            }
        }
    }
}

if (!function_exists('vmo_get_media_replacement_jobs_service')) {
    function vmo_get_media_replacement_jobs_service() {
        static $service = null;
        if (!$service && class_exists('VMO_Media_Replacement_Jobs')) {
            $service = new VMO_Media_Replacement_Jobs();
        }
        return $service;
    }
}

if (class_exists('VMO_Media_Replacement_Jobs')) {
    vmo_get_media_replacement_jobs_service();
}
