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

/**
 * Handles bulk voice generation via WP-Cron queue.
 */
class Podcastmaker_Bulk_Generator
{
    const OPTION_KEY = 'podcastmaker_bulk_job';
    const CRON_HOOK  = 'podcastmaker_bulk_process_next';

    // An item left in "processing" longer than this (no update) is assumed to have
    // died mid-request (PHP timeout, fatal error) and is reset to "pending".
    const STALE_PROCESSING_SECONDS = 300;

    public function __construct()
    {
        add_action(self::CRON_HOOK, array($this, 'process_next_item'));
        add_action('wp_ajax_podcastmaker_bulk_count',  array($this, 'ajax_count_posts'));
        add_action('wp_ajax_podcastmaker_bulk_search', array($this, 'ajax_search_posts'));
        add_action('wp_ajax_podcastmaker_bulk_start',  array($this, 'ajax_start_job'));
        add_action('wp_ajax_podcastmaker_bulk_status', array($this, 'ajax_get_status'));
        add_action('wp_ajax_podcastmaker_bulk_retry',  array($this, 'ajax_retry_item'));
    }

    // ---------------------------------------------------------------
    // Queue helpers
    // ---------------------------------------------------------------

    private function get_job()
    {
        $job = get_option(self::OPTION_KEY, null);
        return is_array($job) ? $job : null;
    }

    private function save_job($job)
    {
        update_option(self::OPTION_KEY, $job, false);
    }

    private function find_next_pending_index($job)
    {
        foreach ($job['items'] as $index => $item) {
            if ($item['status'] === 'pending') {
                return $index;
            }
        }
        return false;
    }

    private function job_has_active_processing($job)
    {
        foreach ($job['items'] as $item) {
            if ($item['status'] === 'processing') {
                return true;
            }
        }
        return false;
    }

    /**
     * Reset any item stuck in "processing" for longer than STALE_PROCESSING_SECONDS
     * back to "pending" so it gets picked up again. Handles the case where a prior
     * request died (timeout, fatal error) before it could update the item status.
     */
    private function recover_stale_items(&$job)
    {
        $changed = false;
        $now     = time();
        foreach ($job['items'] as &$item) {
            if ($item['status'] !== 'processing') {
                continue;
            }
            $updated_at = isset($item['updated_at']) ? (int) $item['updated_at'] : 0;
            if ($updated_at > 0 && ($now - $updated_at) < self::STALE_PROCESSING_SECONDS) {
                continue;
            }
            $item['status']     = 'pending';
            $item['updated_at'] = $now;
            $changed             = true;
        }
        unset($item);
        if ($changed) {
            $this->save_job($job);
        }
    }

    private function schedule_next($delay_seconds = 0)
    {
        if (!wp_next_scheduled(self::CRON_HOOK)) {
            wp_schedule_single_event(time() + $delay_seconds, self::CRON_HOOK);
        }
    }

    // ---------------------------------------------------------------
    // Post query
    // ---------------------------------------------------------------

    public function count_posts_for_filters($filters)
    {
        return count($this->get_posts_for_filters($filters));
    }

    private function get_posts_for_filters($filters)
    {
        $category_id   = isset($filters['category_id'])   ? absint($filters['category_id'])              : 0;
        $author_id     = isset($filters['author_id'])     ? absint($filters['author_id'])                : 0;
        $date_after    = isset($filters['date_after'])    ? sanitize_text_field($filters['date_after'])  : '';
        $date_before   = isset($filters['date_before'])   ? sanitize_text_field($filters['date_before']) : '';
        $no_audio_only = isset($filters['no_audio_only']) ? (bool) $filters['no_audio_only']             : true;

        $args = array(
            'post_type'      => 'post',
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'fields'         => 'all',
        );
        if ($category_id > 0) {
            $args['cat'] = $category_id;
        }
        if ($author_id > 0) {
            $args['author'] = $author_id;
        }
        if (!empty($date_after) || !empty($date_before)) {
            $args['date_query'] = array(array(
                'after'     => !empty($date_after)  ? $date_after  : '',
                'before'    => !empty($date_before) ? $date_before : '',
                'inclusive' => true,
            ));
        }
        if ($no_audio_only) {
            global $wpdb;
            $post_ids_with_audio = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                "SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_content LIKE '%<!-- wp:audio -->%'"
            );
            if (!empty($post_ids_with_audio)) {
                $args['post__not_in'] = array_map('absint', $post_ids_with_audio);
            }
        }

        return get_posts($args);
    }

    // ---------------------------------------------------------------
    // AJAX handlers
    // ---------------------------------------------------------------

    public function ajax_count_posts()
    {
        check_ajax_referer('podcastmaker_bulk_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Permission denied.'));
        }
        $filters = array(
            'category_id'   => isset($_POST['category_id'])   ? absint($_POST['category_id'])                               : 0,
            'author_id'     => isset($_POST['author_id'])     ? absint($_POST['author_id'])                                 : 0,
            'date_after'    => isset($_POST['date_after'])    ? sanitize_text_field(wp_unslash($_POST['date_after']))        : '',
            'date_before'   => isset($_POST['date_before'])   ? sanitize_text_field(wp_unslash($_POST['date_before']))       : '',
            'no_audio_only' => isset($_POST['no_audio_only']) && $_POST['no_audio_only'] === '1',
        );
        wp_send_json_success(array('count' => $this->count_posts_for_filters($filters)));
    }

    public function ajax_search_posts()
    {
        check_ajax_referer('podcastmaker_bulk_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Permission denied.'));
        }
        $filters = array(
            'category_id'   => isset($_POST['category_id'])   ? absint($_POST['category_id'])                               : 0,
            'author_id'     => isset($_POST['author_id'])     ? absint($_POST['author_id'])                                 : 0,
            'date_after'    => isset($_POST['date_after'])    ? sanitize_text_field(wp_unslash($_POST['date_after']))        : '',
            'date_before'   => isset($_POST['date_before'])   ? sanitize_text_field(wp_unslash($_POST['date_before']))       : '',
            'no_audio_only' => isset($_POST['no_audio_only']) && $_POST['no_audio_only'] === '1',
        );
        $posts = $this->get_posts_for_filters($filters);
        $items = array();
        foreach ($posts as $p) {
            $content_stripped = wp_strip_all_tags(strip_shortcodes($p->post_content));
            $excerpt          = mb_substr($content_stripped, 0, 100);
            $categories       = wp_get_post_categories($p->ID, array('fields' => 'names'));
            $author_name      = get_the_author_meta('display_name', $p->post_author);
            $items[] = array(
                'post_id'    => $p->ID,
                'title'      => $p->post_title,
                'excerpt'    => $excerpt,
                'author'     => $author_name,
                'date'       => get_the_date('Y-m-d', $p),
                'categories' => $categories,
                'url'        => get_permalink($p->ID),
            );
        }
        wp_send_json_success(array(
            'count' => count($items),
            'posts' => $items,
        ));
    }

    public function ajax_start_job()
    {
        check_ajax_referer('podcastmaker_bulk_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Permission denied.'));
        }
        if (!Podcastmaker_License::is_business()) {
            wp_send_json_error(array('message' => 'Bulk voice creation requires a Business plan.'));
        }

        $existing_job = $this->get_job();
        if ($existing_job && $existing_job['status'] === 'processing') {
            wp_send_json_error(array('message' => 'A bulk job is already running.'));
        }

        $filters = array(
            'category_id'   => isset($_POST['category_id'])   ? absint($_POST['category_id'])                               : 0,
            'author_id'     => isset($_POST['author_id'])     ? absint($_POST['author_id'])                                 : 0,
            'date_after'    => isset($_POST['date_after'])    ? sanitize_text_field(wp_unslash($_POST['date_after']))        : '',
            'date_before'   => isset($_POST['date_before'])   ? sanitize_text_field(wp_unslash($_POST['date_before']))       : '',
            'no_audio_only' => isset($_POST['no_audio_only']) && $_POST['no_audio_only'] === '1',
        );
        $settings = array(
            'language'      => isset($_POST['language'])      ? sanitize_text_field(wp_unslash($_POST['language']))          : 'ja',
            'tts_model'     => isset($_POST['tts_model'])     ? sanitize_text_field(wp_unslash($_POST['tts_model']))         : get_option('Podcastmaker_gemini_tts_model', 'gemini-2.5-flash-preview-tts'),
            'script_mode'   => isset($_POST['script_mode'])   ? sanitize_text_field(wp_unslash($_POST['script_mode']))       : 'single',
            'voice_id'      => isset($_POST['voice_id'])      ? sanitize_text_field(wp_unslash($_POST['voice_id']))          : '',
            'voice_b_id'    => isset($_POST['voice_b_id'])    ? sanitize_text_field(wp_unslash($_POST['voice_b_id']))        : '',
            'prompt_id'     => isset($_POST['prompt_id'])     ? sanitize_text_field(wp_unslash($_POST['prompt_id']))         : '',
            'smart_tone_id' => isset($_POST['smart_tone_id']) ? sanitize_text_field(wp_unslash($_POST['smart_tone_id']))     : '',
        );

        $posts = $this->get_posts_for_filters($filters);
        if (empty($posts)) {
            wp_send_json_error(array('message' => 'No posts found matching the filters.'));
        }

        $items = array();
        foreach ($posts as $post) {
            $items[] = array(
                'post_id'    => $post->ID,
                'title'      => $post->post_title,
                'status'     => 'pending',
                'retries'    => 0,
                'error'      => '',
                'duration'   => 0,
                'updated_at' => time(),
            );
        }

        $job = array(
            'job_id'       => 'bulk_' . time(),
            'status'       => 'processing',
            'settings'     => $settings,
            'items'        => $items,
            'started_at'   => current_time('mysql'),
            'completed_at' => null,
        );
        $this->save_job($job);
        $this->schedule_next();

        wp_send_json_success(array(
            'job_id' => $job['job_id'],
            'total'  => count($items),
        ));
    }

    public function ajax_get_status()
    {
        check_ajax_referer('podcastmaker_bulk_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Permission denied.'));
        }

        $job = $this->get_job();
        if (!$job) {
            wp_send_json_success(array('status' => 'idle'));
            return;
        }

        // Drive the queue forward from the admin panel's own polling, rather than
        // relying solely on WP-Cron (which may not fire on low-traffic sites).
        if ($job['status'] === 'processing') {
            $this->process_next_item();
            $job = $this->get_job();
            if (!$job) {
                wp_send_json_success(array('status' => 'idle'));
                return;
            }
        }

        $success    = 0;
        $failed     = 0;
        $pending    = 0;
        $processing = null;

        foreach ($job['items'] as $item) {
            if ($item['status'] === 'success')        { $success++; }
            elseif ($item['status'] === 'failed')     { $failed++; }
            elseif ($item['status'] === 'processing') { $processing = $item['title']; $pending++; }
            else                                      { $pending++; }
        }

        wp_send_json_success(array(
            'status'           => $job['status'],
            'job_id'           => $job['job_id'],
            'total'            => count($job['items']),
            'success'          => $success,
            'failed'           => $failed,
            'pending'          => $pending,
            'processing_title' => $processing,
            'started_at'       => $job['started_at'],
            'completed_at'     => $job['completed_at'],
            'items'            => $job['items'],
        ));
    }

    public function ajax_retry_item()
    {
        check_ajax_referer('podcastmaker_bulk_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Permission denied.'));
        }

        $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0;
        if (!$post_id) {
            wp_send_json_error(array('message' => 'Missing post_id.'));
        }

        $job = $this->get_job();
        if (!$job) {
            wp_send_json_error(array('message' => 'No active job found.'));
        }

        $found = false;
        foreach ($job['items'] as &$item) {
            if ($item['post_id'] === $post_id && $item['status'] === 'failed') {
                $item['status']     = 'pending';
                $item['retries']    = 0;
                $item['error']      = '';
                $item['updated_at'] = time();
                $found              = true;
                break;
            }
        }
        unset($item);

        if (!$found) {
            wp_send_json_error(array('message' => 'Item not found or not failed.'));
        }

        $job['status'] = 'processing';
        $this->save_job($job);
        $this->schedule_next();

        wp_send_json_success(array('message' => 'Retry scheduled.'));
    }

    // ---------------------------------------------------------------
    // Cron processor
    // ---------------------------------------------------------------

    public function process_next_item()
    {
        $job = $this->get_job();
        if (!$job || $job['status'] !== 'processing') {
            return;
        }

        $this->recover_stale_items($job);
        $job = $this->get_job();
        if (!$job || $job['status'] !== 'processing') {
            return;
        }

        $index = $this->find_next_pending_index($job);
        if ($index === false) {
            if (!$this->job_has_active_processing($job)) {
                $job['status']       = 'completed';
                $job['completed_at'] = current_time('mysql');
                $this->save_job($job);
            }
            return;
        }

        $job['items'][$index]['status']     = 'processing';
        $job['items'][$index]['updated_at'] = time();
        $this->save_job($job);

        // Guarantee recovery if this request dies mid-processing (PHP timeout, fatal
        // error) before it can update the item status itself.
        register_shutdown_function(array($this, 'recover_if_still_processing'), $index, $job['items'][$index]['post_id']);

        $item  = $job['items'][$index];
        $start = microtime(true);
        $error = $this->process_single_post($item['post_id'], $job['settings']);

        $job = $this->get_job();
        if (!$job) {
            return;
        }
        $job['items'][$index]['duration']   = (int)(microtime(true) - $start);
        $job['items'][$index]['updated_at'] = time();

        if (is_wp_error($error)) {
            $job['items'][$index]['retries']++;
            $job['items'][$index]['error'] = $error->get_error_message();
            $retry_count = $job['items'][$index]['retries'];

            if ($retry_count >= 3) {
                $job['items'][$index]['status'] = 'failed';
                $this->save_job($job);
                $this->schedule_next();
            } else {
                $job['items'][$index]['status'] = 'pending';
                $this->save_job($job);
                $delays = array(1 => 60, 2 => 300);
                $delay  = isset($delays[$retry_count]) ? $delays[$retry_count] : 60;
                $this->schedule_next($delay);
            }
            return;
        }

        $job['items'][$index]['status'] = 'success';
        $job['items'][$index]['error']  = '';
        $this->save_job($job);
        $this->schedule_next();
    }

    /**
     * Registered via register_shutdown_function() when an item is marked "processing".
     * If the item is still "processing" by the time PHP shuts down, the request died
     * before it could update the status itself — reset it to "pending" for retry.
     */
    public function recover_if_still_processing($index, $post_id)
    {
        $job = $this->get_job();
        if (!$job || !isset($job['items'][$index])) {
            return;
        }
        $item = $job['items'][$index];
        if ($item['status'] !== 'processing' || (int) $item['post_id'] !== (int) $post_id) {
            return;
        }
        $job['items'][$index]['status']     = 'pending';
        $job['items'][$index]['updated_at'] = time();
        $this->save_job($job);
        $this->schedule_next();
    }

    private function process_single_post($source_post_id, $settings)
    {
        if (!class_exists('Podcastmaker_Script_Generator')) {
            return new WP_Error('class_missing', 'Podcastmaker_Script_Generator not found.');
        }
        $generator = new Podcastmaker_Script_Generator();

        $prompt_id = isset($settings['prompt_id']) ? $settings['prompt_id'] : '';
        if (empty($prompt_id)) {
            if (!class_exists('Podcastmaker_Prompt_Sets')) {
                require_once plugin_dir_path(__FILE__) . 'class-podcastmaker-prompt-sets.php';
            }
            $prompt_manager = new Podcastmaker_Prompt_Sets();
            $speaker_type   = isset($settings['script_mode']) && $settings['script_mode'] === 'multi' ? 'multi' : 'single';
            $prompt_id      = $prompt_manager->get_default_prompt_set_id($speaker_type);
            if (empty($prompt_id)) {
                return new WP_Error('no_prompt', 'No default Prompt Set found. Please set a default Prompt Set in settings.');
            }
        }

        $script_content = $generator->generate_script_content_for_post($source_post_id, array(
            'prompt_id'       => $prompt_id,
            'script_language' => isset($settings['language']) ? $settings['language'] : 'ja',
            'script_length'   => '60sec',
        ));
        if (is_wp_error($script_content)) {
            return $script_content;
        }

        $source_post    = get_post($source_post_id);
        $script_post_id = wp_insert_post(array(
            'post_type'    => 'podcastmaker_script',
            'post_title'   => $source_post ? $source_post->post_title : 'Bulk Script',
            'post_content' => $script_content,
            'post_status'  => 'publish',
            'meta_input'   => array(
                '_Podcastmaker_source_post_id'  => $source_post_id,
                '_Podcastmaker_script_mode'     => isset($settings['script_mode']) ? $settings['script_mode'] : 'single',
                '_Podcastmaker_script_language' => isset($settings['language'])    ? $settings['language']    : 'ja',
            ),
        ));
        if (is_wp_error($script_post_id)) {
            return $script_post_id;
        }

        $audio_result = $generator->generate_and_save_audio($script_post_id, $script_content, array(
            'tts_model'   => isset($settings['tts_model'])   ? $settings['tts_model']   : '',
            'script_mode' => isset($settings['script_mode']) ? $settings['script_mode'] : 'single',
            'voice_id'    => isset($settings['voice_id'])    ? $settings['voice_id']    : '',
            'voice_b_id'  => isset($settings['voice_b_id'])  ? $settings['voice_b_id']  : '',
        ));
        if (is_wp_error($audio_result)) {
            return $audio_result;
        }

        $this->embed_audio_in_post($source_post_id, $audio_result['audio_url']);

        return null;
    }

    private function embed_audio_in_post($post_id, $audio_url)
    {
        $post = get_post($post_id);
        if (!$post) {
            return;
        }
        $audio_block     = '<!-- wp:audio -->' . "\n"
            . '<figure class="wp-block-audio"><audio controls src="' . esc_url($audio_url) . '"></audio></figure>' . "\n"
            . '<!-- /wp:audio -->';
        $updated_content = $audio_block . "\n\n" . $post->post_content;
        wp_update_post(array(
            'ID'           => $post_id,
            'post_content' => $updated_content,
        ));
    }
}
