<?php

namespace AIPT\Api\AutomationJobs;

/**
 * ClaimBasedQueue
 *
 * Atomic batch claiming with processing_token to avoid double-processing
 *
 * @since 2.3.0
 */
class ClaimBasedQueue {
    /** @var \wpdb */
    private $wpdb;
    /** @var string */
    private $job_products_table;

    public function __construct() {
        global $wpdb;
        $this->wpdb = $wpdb;
        $this->job_products_table = $wpdb->prefix . 'aipt_job_products';
    }

    /**
     * Claim a batch of pending items for a job atomically.
     *
     * Steps:
     * 1) UPDATE ... SET processing_token=?, processing_started_at=NOW(), status='processing'
     *    WHERE job_id=? AND status='pending' AND processing_token IS NULL ORDER BY id ASC LIMIT N
     * 2) SELECT product_id WHERE processing_token=? to return claimed items
     *
     * @param int $job_id
     * @param int $limit
     * @return array{token:string,product_ids:int[]} token and claimed product IDs
     */
    public function claimBatch($job_id, $limit = 10) {
        $job_id = (int) $job_id;
        $limit = max(1, (int) $limit);

        $token = wp_generate_uuid4();


        $sql = $this->wpdb->prepare(
            "UPDATE {$this->job_products_table}
             SET processing_token = %s,
                 processing_started_at = %s,
                 status = 'processing'
             WHERE job_id = %d AND status = 'pending' AND processing_token IS NULL
             ORDER BY id ASC
             LIMIT %d",
            $token,
            current_time('mysql'),
            $job_id,
            $limit
        );


        $attempt = 0;
        $updated = false;
        do {
            $updated = $this->wpdb->query($sql);
            if ($updated === false) {
                $err = strtolower((string) $this->wpdb->last_error);
                if (strpos($err, 'deadlock') !== false || strpos($err, 'lock wait timeout') !== false) {
                    usleep((int) (pow(2, $attempt) * 100000)); 
                    $attempt++;
                    continue;
                }
                return ['token' => $token, 'product_ids' => []];
            }
            break;
        } while ($attempt < 3);

        if ((int) $updated === 0) {
            return ['token' => $token, 'product_ids' => []];
        }


        $rows = $this->wpdb->get_results(
            $this->wpdb->prepare(
                "SELECT product_id FROM {$this->job_products_table} WHERE job_id = %d AND processing_token = %s ORDER BY id ASC",
                $job_id,
                $token
            ),
            ARRAY_A
        );


        $ids = array_map('intval', array_column($rows, 'product_id'));
        return ['token' => $token, 'product_ids' => $ids];
    }

    /**
     * Release stale claims older than given minutes back to pending status.
     *
     * @param int $minutes
     * @param int|null $job_id Optional specific job
     * @return int number of rows reset
     */
    public function releaseStaleClaims($minutes = 15, $job_id = null) {
        $minutes = max(1, (int) $minutes);


        $threshold_ts = time() - ($minutes * MINUTE_IN_SECONDS);
        $threshold = function_exists('wp_date')
            ? wp_date('Y-m-d H:i:s', $threshold_ts)
            : gmdate('Y-m-d H:i:s', $threshold_ts);

        $where_job = '';
        $params = [$threshold];
        if ($job_id !== null) {
            $where_job = ' AND job_id = %d';
            $params[] = (int) $job_id;
        }


        $sql = $this->wpdb->prepare(
            "UPDATE {$this->job_products_table}
             SET status = 'pending', processing_token = NULL, processing_started_at = NULL
             WHERE status = 'processing' AND processing_started_at IS NOT NULL AND processing_started_at < %s" . $where_job,
            $params
        );

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

        return (int) ($res === false ? 0 : $res);
    }
    /**
     * Mark a claimed item as completed.
     *
     * @param int $job_id
     * @param int $product_id
     * @param string $token
     * @return bool
     */
    public function markCompleted($job_id, $product_id, $token) {
        $job_id = (int) $job_id;
        $product_id = (int) $product_id;
        $token = (string) $token;


        $attempt = 0; $res = false;
        do {
            $res = $this->wpdb->update(
                $this->job_products_table,
                [
                    'status' => 'completed',
                    'processed_at' => current_time('mysql'),
                    'error_message' => null,
                    'processing_token' => null,
                    'processing_started_at' => null,
                ],
                [
                    'job_id' => $job_id,
                    'product_id' => $product_id,
                    'processing_token' => $token,
                ],
                ['%s','%s','%s','%s','%s'],
                ['%d','%d','%s']
            );
            if ($res === false) {
                $err = strtolower((string) $this->wpdb->last_error);
                if (strpos($err, 'deadlock') !== false || strpos($err, 'lock wait timeout') !== false) {
                    usleep((int) (pow(2, $attempt) * 100000));
                    $attempt++;
                    continue;
                }
            }
            break;
        } while ($attempt < 3);

        return $res !== false;
    }

    /**
     * Mark a claimed item as failed with an error message.
     *
     * @param int $job_id
     * @param int $product_id
     * @param string $token
     * @param string|null $error_message
     * @return bool
     */
    public function markFailed($job_id, $product_id, $token, $error_message = null) {
        $job_id = (int) $job_id;
        $product_id = (int) $product_id;
        $token = (string) $token;
        $error_message = $error_message !== null ? (string) $error_message : null;


        $attempt = 0; $res = false;
        do {
            $res = $this->wpdb->update(
                $this->job_products_table,
                [
                    'status' => 'failed',
                    'processed_at' => current_time('mysql'),
                    'error_message' => $error_message,
                    'processing_token' => null,
                    'processing_started_at' => null,
                ],
                [
                    'job_id' => $job_id,
                    'product_id' => $product_id,
                    'processing_token' => $token,
                ],
                ['%s','%s','%s','%s','%s'],
                ['%d','%d','%s']
            );
            if ($res === false) {
                $err = strtolower((string) $this->wpdb->last_error);
                if (strpos($err, 'deadlock') !== false || strpos($err, 'lock wait timeout') !== false) {
                    usleep((int) (pow(2, $attempt) * 100000));
                    $attempt++;
                    continue;
                }
            }
            break;
        } while ($attempt < 3);

        return $res !== false;
    }

    /**
     * Check if there are pending items for a job.
     *
     * @param int $job_id
     * @return bool
     */
    public function hasPending($job_id) {
        $job_id = (int) $job_id;

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

        return ((int) $count) > 0;
    }
    /**
     * Release all processing items of a specific claim token back to pending.
     * Safe to call even if all items were processed (no-op then).
     *
     * @param int $job_id
     * @param string $token
     * @return int number of rows reset
     */
    public function releaseClaimByToken($job_id, $token) {
        $job_id = (int) $job_id;
        $token = (string) $token;


        $sql = $this->wpdb->prepare(
            "UPDATE {$this->job_products_table}
             SET status = 'pending', processing_token = NULL, processing_started_at = NULL
             WHERE job_id = %d AND processing_token = %s AND status = 'processing'",
            $job_id,
            $token
        );

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

        return (int) ($res === false ? 0 : $res);
    }

    /**
     * Force-reset all processing items for a job back to pending status.
     * Useful immediately after hard failures/timeouts to ensure retry.
     *
     * @param int $job_id
     * @return int number of rows reset
     */
    public function forceReleaseAllProcessing($job_id) {
        $job_id = (int) $job_id;


        $sql = $this->wpdb->prepare(
            "UPDATE {$this->job_products_table}
             SET status = 'pending', processing_token = NULL, processing_started_at = NULL
             WHERE job_id = %d AND status = 'processing'",
            $job_id
        );

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

        return (int) ($res === false ? 0 : $res);
    }


}
