<?php
/**
 * Index Job
 *
 * Manages indexing job creation, status tracking, and progress updates.
 *
 * @package BoostBot
 * @since 1.0.0
 */

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Class BoostBot_Index_Job
 *
 * Handles indexing job lifecycle and status tracking.
 */
class BoostBot_Index_Job
{
    /**
     * Option name for storing active job.
     *
     * @var string
     */
    private const OPTION_ACTIVE_JOB = 'boostbot_active_index_job';

    /**
     * Option name for storing job history.
     *
     * @var string
     */
    private const OPTION_JOB_HISTORY = 'boostbot_index_job_history';

    /**
     * Job statuses.
     */
    public const STATUS_PENDING = 'pending';
    public const STATUS_RUNNING = 'running';
    public const STATUS_COMPLETED = 'completed';
    public const STATUS_FAILED = 'failed';
    public const STATUS_CANCELLED = 'cancelled';

    /**
     * Creates a new indexing job.
     *
     * @since 1.0.0
     * @param array $options Job options (content_types, selected_ids).
     * @return string The job ID.
     */
    public static function create(array $options = []): string
    {
        $job_id = wp_generate_uuid4();

        $job = [
            'id'            => $job_id,
            'status'        => self::STATUS_PENDING,
            'content_types' => $options['content_types'] ?? ['post', 'page', 'product'],
            'selected_ids'  => $options['selected_ids'] ?? null,
            'index_mode'    => $options['index_mode'] ?? 'basic',
            'total'         => 0,
            'processed'     => 0,
            'errors'        => [],
            'created_at'    => current_time('mysql'),
            'started_at'    => null,
            'completed_at'  => null,
        ];

        update_option(self::OPTION_ACTIVE_JOB, $job);

        BoostBot_Logger::info('Index job created', ['job_id' => $job_id]);

        return $job_id;
    }

    /**
     * Gets the current active job.
     *
     * @since 1.0.0
     * @param string|null $job_id Optional specific job ID.
     * @return object|null The job object or null.
     */
    public static function get(?string $job_id = null): ?object
    {
        $job = get_option(self::OPTION_ACTIVE_JOB, null);

        if (!$job) {
            return null;
        }

        // If specific job_id requested, verify it matches
        if ($job_id && $job['id'] !== $job_id) {
            return null;
        }

        return (object) $job;
    }

    /**
     * Updates job status.
     *
     * @since 1.0.0
     * @param string $job_id The job ID.
     * @param string $status The new status.
     * @return bool True on success.
     */
    public static function update_status(string $job_id, string $status): bool
    {
        $job = get_option(self::OPTION_ACTIVE_JOB, null);

        if (!$job || $job['id'] !== $job_id) {
            return false;
        }

        $job['status'] = $status;

        if ($status === self::STATUS_RUNNING && !$job['started_at']) {
            $job['started_at'] = current_time('mysql');
        }

        if (in_array($status, [self::STATUS_COMPLETED, self::STATUS_FAILED, self::STATUS_CANCELLED], true)) {
            $job['completed_at'] = current_time('mysql');
            self::archive_job($job);
            // archive_job deletes the option, so don't update it again
            return true;
        }

        update_option(self::OPTION_ACTIVE_JOB, $job);

        BoostBot_Logger::debug('Index job status updated', [
            'job_id' => $job_id,
            'status' => $status,
        ]);

        return true;
    }

    /**
     * Updates job progress.
     *
     * @since 1.0.0
     * @param string $job_id   The job ID.
     * @param int    $processed Number of items processed.
     * @param int    $total     Total number of items.
     * @return bool True on success.
     */
    public static function update_progress(string $job_id, int $processed, int $total): bool
    {
        $job = get_option(self::OPTION_ACTIVE_JOB, null);

        if (!$job || $job['id'] !== $job_id) {
            return false;
        }

        $job['processed'] = $processed;
        $job['total']     = $total;

        update_option(self::OPTION_ACTIVE_JOB, $job);

        return true;
    }

    /**
     * Adds an error to the job.
     *
     * @since 1.0.0
     * @param string $job_id The job ID.
     * @param array  $error  Error details.
     * @return bool True on success.
     */
    public static function add_error(string $job_id, array $error): bool
    {
        $job = get_option(self::OPTION_ACTIVE_JOB, null);

        if (!$job || $job['id'] !== $job_id) {
            return false;
        }

        $job['errors'][] = array_merge($error, [
            'time' => current_time('mysql'),
        ]);

        update_option(self::OPTION_ACTIVE_JOB, $job);

        return true;
    }

    /**
     * Calculates job progress percentage.
     *
     * @since 1.0.0
     * @param string $job_id The job ID.
     * @return int Progress percentage (0-100).
     */
    public static function get_progress(string $job_id): int
    {
        $job = self::get($job_id);

        if (!$job || $job->total === 0) {
            return 0;
        }

        return (int) round(($job->processed / $job->total) * 100);
    }

    /**
     * Week 30: Get aggregated queue stats for the active job.
     *
     * @since 1.13.0
     * @return array|null
     */
    public static function get_queue_stats(): ?array {
        $job = self::get();
        if ( ! $job ) {
            return null;
        }
        if ( ! class_exists( 'BoostBot_Index_Queue' ) ) {
            return null;
        }
        $queue = new BoostBot_Index_Queue();
        return $queue->get_stats();
    }

    /**
     * Week 30: Get numeric error count (UI-friendly).
     *
     * @since 1.13.0
     * @param object|null $job
     * @return int
     */
    public static function get_error_count( ?object $job ): int {
        if ( ! $job ) {
            return 0;
        }
        // Backward compatible: errors stored as array of entries.
        if ( isset( $job->errors ) && is_array( $job->errors ) ) {
            return count( $job->errors );
        }
        // Newer/other shapes: errors may be an int.
        if ( isset( $job->errors ) && is_numeric( $job->errors ) ) {
            return (int) $job->errors;
        }
        return 0;
    }

    /**
     * Cancels the active job.
     *
     * @since 1.0.0
     * @param string $job_id The job ID.
     * @return bool True on success.
     */
    public static function cancel(string $job_id): bool
    {
        return self::update_status($job_id, self::STATUS_CANCELLED);
    }

    /**
     * Checks if there's an active job running.
     *
     * @since 1.0.0
     * @return bool True if a job is running.
     */
    public static function is_running(): bool
    {
        $job = self::get();

        if (!$job) {
            return false;
        }

        return $job->status === self::STATUS_RUNNING;
    }

    /**
     * Archives a completed job to history.
     *
     * @since 1.0.0
     * @param array $job The job data.
     */
    private static function archive_job(array $job): void
    {
        $history = get_option(self::OPTION_JOB_HISTORY, []);

        // Keep only last 10 jobs
        array_unshift($history, $job);
        $history = array_slice($history, 0, 10);

        update_option(self::OPTION_JOB_HISTORY, $history);

        // Clear active job if completed
        if (in_array($job['status'], [self::STATUS_COMPLETED, self::STATUS_FAILED, self::STATUS_CANCELLED], true)) {
            delete_option(self::OPTION_ACTIVE_JOB);
        }
    }

    /**
     * Gets job history.
     *
     * @since 1.0.0
     * @param int $limit Maximum number of jobs to return.
     * @return array Array of job objects.
     */
    public static function get_history(int $limit = 10): array
    {
        $history = get_option(self::OPTION_JOB_HISTORY, []);

        return array_slice($history, 0, $limit);
    }

    /**
     * Clears job history.
     *
     * @since 1.0.0
     * @return bool True on success.
     */
    public static function clear_history(): bool
    {
        return delete_option(self::OPTION_JOB_HISTORY);
    }
}

