<?php

namespace Fotogenic_AI;

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

final class Job_Manager {
    private static ?Job_Manager $instance = null;
    private const ACTIVE_TTL = 1800;
    private const WORKFLOW_CLAIM_TTL = 600;
    private const ACTIVE_MAP_LOCK_TTL = 30;
    private const ACTIVE_MAP_LOCK_ATTEMPTS = 80;
    private const WORKFLOWS = [ 'product_images', 'campaign_set', 'increase_resolution' ];

    private function __construct() {}

    public static function instance(): Job_Manager {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    public function set_active_job( int $user_id, string $job_id, array $context = [], string $key = 'default' ): ?array {
        $payload = array_merge(
            [
                'job_id'     => $job_id,
                'started_at' => time(),
            ],
            $context
        );
        if ( 'campaign_set' === (string) ( $payload['workflow'] ?? '' ) ) {
            $payload['user_id'] = $user_id;
        }
        $result = $this->mutate_active_jobs(
            $user_id,
            static function ( array $jobs ) use ( $key, $payload ): array {
                $jobs[ $key ] = $payload;
                return [
                    'jobs'   => $jobs,
                    'result' => $payload,
                ];
            }
        );

        return is_array( $result ) ? $result : null;
    }

    public function get_active_job( int $user_id ): ?array {
        $jobs = $this->get_active_jobs( $user_id );
        if ( empty( $jobs ) ) {
            return null;
        }
        if ( isset( $jobs['default'] ) && $this->is_active_job_accessible_to_user( $jobs['default'], $user_id ) ) {
            return $jobs['default'];
        }
        foreach ( $jobs as $job ) {
            if ( is_array( $job ) && $this->is_active_job_accessible_to_user( $job, $user_id ) ) {
                return $job;
            }
        }
        return null;
    }

    public function get_active_job_by_key( int $user_id, string $key ): ?array {
        if ( '' === $key ) {
            return null;
        }
        $jobs = $this->get_active_jobs( $user_id );
        $job = $jobs[ $key ] ?? null;
        return is_array( $job ) && $this->is_active_job_accessible_to_user( $job, $user_id ) ? $job : null;
    }

    public function get_active_job_by_run( int $user_id, string $run_id ): ?array {
        $run_id = trim( $run_id );
        if ( '' === $run_id ) {
            return null;
        }

        $matched = null;
        foreach ( $this->get_active_jobs( $user_id ) as $job ) {
            if (
                ! is_array( $job )
                || ! $this->is_active_job_accessible_to_user( $job, $user_id )
                || ! hash_equals( $run_id, (string) ( $job['run_id'] ?? '' ) )
            ) {
                continue;
            }
            if ( null === $matched || (int) ( $job['started_at'] ?? 0 ) > (int) ( $matched['started_at'] ?? 0 ) ) {
                $matched = $job;
            }
        }

        return $matched;
    }

    public function get_subject_job_key( string $run_target, int $subject_id ): string {
        $run_target = sanitize_key( $run_target );
        if ( ! in_array( $run_target, [ 'product', 'media', 'post', 'page' ], true ) ) {
            return '';
        }
        $subject_id = (int) $subject_id;
        if ( $subject_id <= 0 ) {
            return '';
        }
        return 'subject:' . $run_target . ':' . $subject_id;
    }

    public function get_workflow_job_key( string $workflow, string $run_target, int $subject_id, int $user_id = 0, string $request_id = '' ): string {
        $workflow = sanitize_key( $workflow );
        $run_target = sanitize_key( $run_target );
        $allowed_targets = 'product_images' === $workflow ? [ 'product', 'media', 'post', 'page' ] : [ 'product', 'media' ];
        if ( ! in_array( $workflow, self::WORKFLOWS, true ) || ! in_array( $run_target, $allowed_targets, true ) || $subject_id <= 0 ) {
            return '';
        }
        if ( 'campaign_set' === $workflow ) {
            $request_id = strtolower( trim( $request_id ) );
            if ( $user_id <= 0 || ! $this->is_valid_request_id( $request_id ) ) {
                return '';
            }
            return 'workflow:' . $workflow . ':user:' . $user_id . ':request:' . $request_id . ':' . $run_target . ':' . $subject_id;
        }
        return 'workflow:' . $workflow . ':' . $run_target . ':' . $subject_id;
    }

    public function get_active_workflow_job( int $user_id, string $workflow, string $run_target, int $subject_id, string $request_id = '' ): ?array {
        $workflow = sanitize_key( $workflow );
        $run_target = sanitize_key( $run_target );
        $request_id = strtolower( trim( $request_id ) );
        if ( ! in_array( $workflow, self::WORKFLOWS, true ) || $subject_id <= 0 ) {
            return null;
        }

        $matched = null;
        foreach ( $this->get_active_jobs( $user_id ) as $job ) {
            if ( ! is_array( $job ) || ! $this->is_active_job_accessible_to_user( $job, $user_id ) ) {
                continue;
            }
            if (
                $workflow !== (string) ( $job['workflow'] ?? '' )
                || $run_target !== (string) ( $job['run_target'] ?? '' )
                || $subject_id !== (int) ( $job['subject_id'] ?? 0 )
            ) {
                continue;
            }
            if ( '' !== $request_id && ! hash_equals( (string) ( $job['request_id'] ?? '' ), $request_id ) ) {
                continue;
            }
            if ( null === $matched || (int) ( $job['started_at'] ?? 0 ) > (int) ( $matched['started_at'] ?? 0 ) ) {
                $matched = $job;
            }
        }
        return $matched;
    }

    /**
     * Atomically claim a billable workflow start for one user/target/subject/workflow.
     *
     * @return array|\WP_Error Claim data, including an internal token used for safe release.
     */
    public function claim_generation_start( int $user_id, string $run_target, int $subject_id, string $workflow, string $request_id, string $run_item_uuid = '' ) {
        $run_target = sanitize_key( $run_target );
        $workflow = sanitize_key( $workflow );
        $request_id = strtolower( trim( $request_id ) );
        $run_item_uuid = strtolower( trim( $run_item_uuid ) );
        $allowed_targets = 'product_images' === $workflow ? [ 'product', 'media', 'post', 'page' ] : [ 'product', 'media' ];
        if (
            $user_id <= 0
            || $subject_id <= 0
            || ! in_array( $run_target, $allowed_targets, true )
            || ! in_array( $workflow, self::WORKFLOWS, true )
            || ! $this->is_valid_request_id( $request_id )
            || ( '' !== $run_item_uuid && ! wp_is_uuid( $run_item_uuid ) )
        ) {
            return new \WP_Error(
                'fotogenic_ai_invalid_workflow_claim',
                __( 'Invalid workflow start request.', 'fotogenic-ai' ),
                [ 'status' => 400 ]
            );
        }

        $option_key = $this->get_generation_claim_key( $user_id, $run_target, $subject_id, $workflow, $run_item_uuid );
        $now = time();
        $ttl = $this->get_generation_claim_ttl();
        $claim = [
            'option_key' => $option_key,
            'token'      => wp_generate_uuid4(),
            'request_id' => $request_id,
            'user_id'    => $user_id,
            'run_target' => $run_target,
            'subject_id' => $subject_id,
            'workflow'   => $workflow,
            'run_item_uuid' => $run_item_uuid,
            'state'      => 'claimed',
            'claimed_at' => $now,
            'updated_at' => $now,
            'expires_at' => $now + $ttl,
        ];

        for ( $attempt = 0; $attempt < 3; $attempt++ ) {
            if ( $this->insert_option_direct( $option_key, $claim ) ) {
                return $claim;
            }

            $existing = $this->read_option_direct( $option_key );
            if ( is_array( $existing ) && (int) ( $existing['expires_at'] ?? 0 ) > time() ) {
                return new \WP_Error(
                    'fotogenic_ai_workflow_start_in_progress',
                    __( 'This workflow is already starting or processing for the selected item.', 'fotogenic-ai' ),
                    [
                        'status'    => 409,
                        'requestId' => (string) ( $existing['request_id'] ?? '' ),
                        'run_id'    => (string) ( $existing['run_id'] ?? '' ),
                        'state'     => (string) ( $existing['state'] ?? 'starting' ),
                    ]
                );
            }

            if ( ! $this->compare_and_delete_option( $option_key, $existing ) ) {
                continue;
            }
        }

        return new \WP_Error(
            'fotogenic_ai_workflow_start_in_progress',
            __( 'This workflow is already starting or processing for the selected item.', 'fotogenic-ai' ),
            [ 'status' => 409 ]
        );
    }

    /** @return array|\WP_Error */
    public function update_generation_start_claim( array $claim, array $updates ) {
        $option_key = (string) ( $claim['option_key'] ?? '' );
        $token = (string) ( $claim['token'] ?? '' );
        if ( '' === $option_key || '' === $token ) {
            return new \WP_Error( 'fotogenic_ai_invalid_workflow_claim', __( 'Invalid workflow start request.', 'fotogenic-ai' ), [ 'status' => 400 ] );
        }

        $current = $this->read_option_direct( $option_key );
        if ( ! is_array( $current ) || ! hash_equals( (string) ( $current['token'] ?? '' ), $token ) ) {
            return new \WP_Error( 'fotogenic_ai_workflow_claim_lost', __( 'The workflow start claim expired. Please try again.', 'fotogenic-ai' ), [ 'status' => 409 ] );
        }

        $allowed_updates = [];
        foreach ( [ 'state', 'run_id', 'job_ids', 'uncertain' ] as $key ) {
            if ( array_key_exists( $key, $updates ) ) {
                $allowed_updates[ $key ] = $updates[ $key ];
            }
        }
        if ( isset( $allowed_updates['state'] ) ) {
            $state = sanitize_key( (string) $allowed_updates['state'] );
            $allowed_updates['state'] = in_array( $state, [ 'claimed', 'starting', 'processing', 'uncertain' ], true ) ? $state : (string) $current['state'];
        }
        if ( isset( $allowed_updates['run_id'] ) ) {
            $allowed_updates['run_id'] = substr( sanitize_text_field( (string) $allowed_updates['run_id'] ), 0, 64 );
        }
        if ( isset( $allowed_updates['job_ids'] ) ) {
            $allowed_updates['job_ids'] = $this->normalize_job_ids( $allowed_updates['job_ids'] );
        }
        if ( isset( $allowed_updates['uncertain'] ) ) {
            $allowed_updates['uncertain'] = (bool) $allowed_updates['uncertain'];
        }
        $allowed_updates['updated_at'] = time();
        $allowed_updates['expires_at'] = time() + $this->get_generation_claim_ttl();
        $updated = array_merge( $current, $allowed_updates );
        if ( ! $this->compare_and_update_option( $option_key, $current, $updated ) ) {
            return new \WP_Error( 'fotogenic_ai_workflow_claim_lost', __( 'The workflow start claim expired. Please try again.', 'fotogenic-ai' ), [ 'status' => 409 ] );
        }

        return $updated;
    }

    public function release_generation_start_claim( array $claim ): bool {
        $option_key = (string) ( $claim['option_key'] ?? '' );
        $token = (string) ( $claim['token'] ?? '' );
        if ( '' === $option_key || '' === $token ) {
            return false;
        }
        $current = $this->read_option_direct( $option_key );
        if ( ! is_array( $current ) || ! hash_equals( (string) ( $current['token'] ?? '' ), $token ) ) {
            return false;
        }
        return $this->compare_and_delete_option( $option_key, $current );
    }

	/**
	 * Release a recovery claim only when the durable run identity still matches
	 * every original claim dimension. This is intentionally not a broad cleanup.
	 */
	public function release_generation_start_claim_for_recovery(
		int $user_id,
		string $run_target,
		int $subject_id,
		string $workflow,
		string $request_id,
		string $run_id,
		string $run_item_uuid = ''
	): bool {
		$run_target = sanitize_key( $run_target );
		$workflow = sanitize_key( $workflow );
		$request_id = strtolower( trim( $request_id ) );
		$run_id = trim( $run_id );
		$run_item_uuid = strtolower( trim( $run_item_uuid ) );
		if (
			$user_id <= 0
			|| $subject_id <= 0
			|| '' === $run_id
			|| ! in_array( $workflow, self::WORKFLOWS, true )
			|| ! $this->is_valid_request_id( $request_id )
			|| ( '' !== $run_item_uuid && ! wp_is_uuid( $run_item_uuid ) )
		) {
			return false;
		}

		$option_key = $this->get_generation_claim_key( $user_id, $run_target, $subject_id, $workflow, $run_item_uuid );
		$current = $this->read_option_direct( $option_key );
		if (
			! is_array( $current )
			|| $user_id !== (int) ( $current['user_id'] ?? 0 )
			|| $subject_id !== (int) ( $current['subject_id'] ?? 0 )
			|| ! hash_equals( $run_target, (string) ( $current['run_target'] ?? '' ) )
			|| ! hash_equals( $workflow, (string) ( $current['workflow'] ?? '' ) )
			|| ! hash_equals( $request_id, (string) ( $current['request_id'] ?? '' ) )
			|| ! hash_equals( $run_id, (string) ( $current['run_id'] ?? '' ) )
			|| ! hash_equals( $run_item_uuid, (string) ( $current['run_item_uuid'] ?? '' ) )
		) {
			return false;
		}

		return $this->compare_and_delete_option( $option_key, $current );
	}

    public function get_active_job_by_job_ids( int $user_id, array $job_ids ): ?array {
        $job_ids = $this->normalize_job_ids( $job_ids );
        if ( empty( $job_ids ) ) {
            return null;
        }
        $jobs = $this->get_active_jobs( $user_id );
        foreach ( $jobs as $job ) {
            if ( ! is_array( $job ) || ! $this->is_active_job_accessible_to_user( $job, $user_id ) ) {
                continue;
            }
            $active_job_ids = $this->normalize_job_ids( $job['job_ids'] ?? [ (string) ( $job['job_id'] ?? '' ) ] );
            if ( empty( $active_job_ids ) ) {
                continue;
            }
            $match = true;
            foreach ( $job_ids as $job_id ) {
                if ( ! in_array( $job_id, $active_job_ids, true ) ) {
                    $match = false;
                    break;
                }
            }
            if ( $match ) {
                return $job;
            }
        }
        return null;
    }

    public function update_active_job( int $user_id, array $updates, string $key = '', array $job_ids = [] ): ?array {
        $normalized_job_ids = $this->normalize_job_ids( $job_ids );
        $result = $this->mutate_active_jobs(
            $user_id,
            function ( array $jobs ) use ( $user_id, $updates, $key, $normalized_job_ids ): array {
                if ( empty( $jobs ) ) {
                    return [ 'jobs' => $jobs, 'result' => null ];
                }

                $target_key = '' !== $key ? $key : '';
                if ( '' === $target_key && ! empty( $normalized_job_ids ) ) {
                    foreach ( $jobs as $candidate_key => $job ) {
                        if ( ! is_array( $job ) || ! $this->is_active_job_accessible_to_user( $job, $user_id ) ) {
                            continue;
                        }
                        $active_job_ids = $this->normalize_job_ids( $job['job_ids'] ?? [ (string) ( $job['job_id'] ?? '' ) ] );
                        if ( ! empty( $active_job_ids ) && empty( array_diff( $normalized_job_ids, $active_job_ids ) ) ) {
                            $target_key = (string) $candidate_key;
                            break;
                        }
                    }
                }
                if ( '' === $target_key ) {
                    $target_key = isset( $jobs['default'] ) && $this->is_active_job_accessible_to_user( $jobs['default'], $user_id ) ? 'default' : '';
                }
                if (
                    '' === $target_key
                    || empty( $jobs[ $target_key ] )
                    || ! is_array( $jobs[ $target_key ] )
                    || ! $this->is_active_job_accessible_to_user( $jobs[ $target_key ], $user_id )
                ) {
                    return [ 'jobs' => $jobs, 'result' => null ];
                }

                $jobs[ $target_key ] = array_merge( $jobs[ $target_key ], $updates );
                if ( 'campaign_set' === (string) ( $jobs[ $target_key ]['workflow'] ?? '' ) ) {
                    $jobs[ $target_key ]['user_id'] = $user_id;
                }
                return [
                    'jobs'   => $jobs,
                    'result' => $jobs[ $target_key ],
                ];
            }
        );

        return is_array( $result ) ? $result : null;
    }

    public function clear_active_job( int $user_id, string $key = '' ): void {
        $this->mutate_active_jobs(
            $user_id,
            function ( array $jobs ) use ( $user_id, $key ): array {
                if ( '' === $key ) {
                    foreach ( $jobs as $candidate_key => $job ) {
                        if ( is_array( $job ) && $this->is_active_job_accessible_to_user( $job, $user_id ) ) {
                            unset( $jobs[ $candidate_key ] );
                        }
                    }
                } elseif ( isset( $jobs[ $key ] ) && $this->is_active_job_accessible_to_user( $jobs[ $key ], $user_id ) ) {
                    unset( $jobs[ $key ] );
                }

                return [ 'jobs' => $jobs, 'result' => null ];
            }
        );
    }

    public function clear_active_jobs_by_run( int $user_id, string $run_id ): void {
        if ( '' === $run_id ) {
            return;
        }
        $this->mutate_active_jobs(
            $user_id,
            function ( array $jobs ) use ( $user_id, $run_id ): array {
                foreach ( $jobs as $key => $job ) {
                    if ( ! is_array( $job ) || ! $this->is_active_job_accessible_to_user( $job, $user_id ) ) {
                        continue;
                    }
                    if ( $run_id === (string) ( $job['run_id'] ?? '' ) ) {
                        unset( $jobs[ $key ] );
                    }
                }
                return [ 'jobs' => $jobs, 'result' => null ];
            }
        );
    }

    public function clear_active_job_by_job_ids( int $user_id, array $job_ids ): void {
        $job_ids = $this->normalize_job_ids( $job_ids );
        if ( empty( $job_ids ) ) {
            return;
        }
        $this->mutate_active_jobs(
            $user_id,
            function ( array $jobs ) use ( $user_id, $job_ids ): array {
                foreach ( $jobs as $key => $job ) {
                    if ( ! is_array( $job ) || ! $this->is_active_job_accessible_to_user( $job, $user_id ) ) {
                        continue;
                    }
                    $active_job_ids = $this->normalize_job_ids( $job['job_ids'] ?? [ (string) ( $job['job_id'] ?? '' ) ] );
                    if ( ! empty( $active_job_ids ) && empty( array_diff( $job_ids, $active_job_ids ) ) ) {
                        unset( $jobs[ $key ] );
                    }
                }
                return [ 'jobs' => $jobs, 'result' => null ];
            }
        );
    }

    private function get_active_key( int $user_id ): string {
        return 'fotogenic_ai_active_job_' . $this->normalize_scope_user_id( $user_id );
    }

    private function get_generation_claim_key( int $user_id, string $run_target, int $subject_id, string $workflow, string $run_item_uuid = '' ): string {
        $scope_user_id = 'campaign_set' === $workflow ? max( 0, $user_id ) : $this->normalize_scope_user_id( $user_id );
        $scope_parts = [
            (string) get_current_blog_id(),
            (string) $scope_user_id,
            $run_target,
            (string) $subject_id,
            $workflow,
        ];
        if ( '' !== $run_item_uuid ) {
            $scope_parts[] = $run_item_uuid;
        }
        $scope = implode( '|', $scope_parts );
        return 'fotogenic_ai_generation_claim_' . hash( 'sha256', $scope );
    }

    private function get_generation_claim_ttl(): int {
        $ttl = (int) apply_filters( 'fotogenic_ai_generation_start_claim_ttl', self::WORKFLOW_CLAIM_TTL );
        return max( 60, min( HOUR_IN_SECONDS, $ttl ) );
    }

    private function compare_and_delete_option( string $option_key, $expected_value ): bool {
        global $wpdb;
        $serialized = maybe_serialize( $expected_value );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic compare-and-delete prevents a stale claimant from deleting a newer claim.
        $deleted = $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$wpdb->options} WHERE option_name = %s AND BINARY option_value = BINARY %s",
                $option_key,
                $serialized
            )
        );
        $this->sync_option_cache( $option_key );
        return 1 === (int) $deleted;
    }

    private function compare_and_update_option( string $option_key, $expected_value, $updated_value ): bool {
        global $wpdb;
        $expected = maybe_serialize( $expected_value );
        $updated = maybe_serialize( $updated_value );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Token-bound compare-and-update prevents an expired claimant from overwriting a replacement claim.
        $changed = $wpdb->query(
            $wpdb->prepare(
                "UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND BINARY option_value = BINARY %s",
                $updated,
                $option_key,
                $expected
            )
        );
        $this->sync_option_cache( $option_key );
        return 1 === (int) $changed;
    }

    private function get_active_jobs( int $user_id ): array {
        $option_key = $this->get_active_key( $user_id );
        $stored = $this->read_option_direct( $option_key );
        if ( is_array( $stored ) && array_key_exists( 'jobs', $stored ) ) {
            if ( (int) ( $stored['expires_at'] ?? 0 ) <= time() ) {
                $this->compare_and_delete_option( $option_key, $stored );
                return [];
            }
            return $this->normalize_active_jobs( $stored['jobs'] ?? [] );
        }
        if ( is_array( $stored ) ) {
            return $this->normalize_active_jobs( $stored );
        }

        return [];
    }

    /**
     * Serialize every read-modify-write of the shared active-job map.
     *
     * Administrator-capable users intentionally share scope 0, so per-run locks
     * alone cannot protect this map when separate runs update concurrently.
     *
     * @param int      $user_id  Current user ID.
     * @param callable $mutation Receives the latest jobs and returns jobs/result.
     * @return mixed Mutation result, or null when persistence could not be acquired.
     */
    private function mutate_active_jobs( int $user_id, callable $mutation ) {
        $lock = Background_Lock::instance();
        $lock_key = 'active-job-map:' . get_current_blog_id() . ':' . $this->normalize_scope_user_id( $user_id );
        $token = false;

        for ( $attempt = 0; $attempt < self::ACTIVE_MAP_LOCK_ATTEMPTS; $attempt++ ) {
            $token = $lock->acquire( $lock_key, self::ACTIVE_MAP_LOCK_TTL );
            if ( false !== $token ) {
                break;
            }
            usleep( 25000 );
        }

        if ( false === $token ) {
            Logger::instance()->error( 'active_job_map_lock_failed', [
                'user_id' => $user_id,
            ] );
            return null;
        }

        try {
            $outcome = $mutation( $this->get_active_jobs( $user_id ) );
            if ( ! is_array( $outcome ) || ! isset( $outcome['jobs'] ) || ! is_array( $outcome['jobs'] ) ) {
                Logger::instance()->error( 'active_job_map_mutation_invalid', [
                    'user_id' => $user_id,
                ] );
                return null;
            }
            if ( ! $lock->renew( $lock_key, $token, self::ACTIVE_MAP_LOCK_TTL ) ) {
                Logger::instance()->error( 'active_job_map_lock_lost', [
                    'user_id' => $user_id,
                ] );
                return null;
            }
            if ( ! $this->persist_active_jobs( $user_id, $outcome['jobs'] ) ) {
                Logger::instance()->error( 'active_job_map_persist_failed', [
                    'user_id' => $user_id,
                ] );
                return null;
            }
            return $outcome['result'] ?? null;
        } finally {
            $lock->release( $lock_key, $token );
        }
    }

    private function persist_active_jobs( int $user_id, array $jobs ): bool {
        $option_key = $this->get_active_key( $user_id );
        if ( empty( $jobs ) ) {
            $existing = $this->read_option_direct( $option_key );
            if ( null !== $existing ) {
                $this->compare_and_delete_option( $option_key, $existing );
            }
            return null === $this->read_option_direct( $option_key );
        }

        $stored = [
            'expires_at' => time() + self::ACTIVE_TTL,
            'jobs'       => $this->normalize_active_jobs( $jobs ),
        ];
        if ( ! $this->upsert_option_direct( $option_key, $stored ) ) {
            return false;
        }
        $persisted = $this->read_option_direct( $option_key );
        return is_array( $persisted )
            && (int) ( $persisted['expires_at'] ?? 0 ) > time()
            && $this->normalize_active_jobs( $persisted['jobs'] ?? [] ) === $stored['jobs'];
    }

    private function insert_option_direct( string $option_key, $value ): bool {
        global $wpdb;

        $autoload = function_exists( 'wp_autoload_values_to_autoload' ) ? 'off' : 'no';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- INSERT IGNORE and the unique option_name index provide an atomic workflow-start claim.
        $inserted = $wpdb->query(
            $wpdb->prepare(
                "INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, %s)",
                $option_key,
                maybe_serialize( $value ),
                $autoload
            )
        );
        $this->sync_option_cache( $option_key );
        return 1 === (int) $inserted;
    }

    private function upsert_option_direct( string $option_key, $value ): bool {
        global $wpdb;

        $autoload = function_exists( 'wp_autoload_values_to_autoload' ) ? 'off' : 'no';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- The token-owned map lock serializes this durable state write.
        $written = $wpdb->query(
            $wpdb->prepare(
                "INSERT INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE option_value = VALUES(option_value), autoload = VALUES(autoload)",
                $option_key,
                maybe_serialize( $value ),
                $autoload
            )
        );
        $this->sync_option_cache( $option_key );
        return false !== $written;
    }

    private function read_option_direct( string $option_key ) {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Shared claims and maps must bypass stale request-local and persistent option caches.
        $serialized = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
                $option_key
            )
        );
        return null === $serialized ? null : maybe_unserialize( $serialized );
    }

    private function sync_option_cache( string $option_key ): void {
        wp_cache_delete( $option_key, 'options' );
        wp_cache_delete( 'notoptions', 'options' );
    }

    private function is_active_job_accessible_to_user( array $job, int $user_id ): bool {
        if ( 'campaign_set' !== (string) ( $job['workflow'] ?? '' ) ) {
            return true;
        }
        return $user_id > 0 && (int) ( $job['user_id'] ?? 0 ) === $user_id;
    }

    private function is_valid_request_id( string $request_id ): bool {
        return 1 === preg_match( '/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/', $request_id );
    }

    private function normalize_active_jobs( $value ): array {
        if ( ! is_array( $value ) ) {
            return [];
        }
        if ( array_key_exists( 'job_id', $value ) || array_key_exists( 'job_ids', $value ) ) {
            return [ 'default' => $value ];
        }
        $normalized = [];
        foreach ( $value as $key => $job ) {
            if ( ! is_array( $job ) ) {
                continue;
            }
            if ( empty( $job['job_id'] ) && empty( $job['job_ids'] ) ) {
                continue;
            }
            $normalized[ (string) $key ] = $job;
        }
        return $normalized;
    }

    private function normalize_job_ids( $job_ids ): array {
        if ( is_string( $job_ids ) ) {
            $trimmed = trim( $job_ids );
            if ( '' !== $trimmed && ( fotogenic_ai_str_starts_with( $trimmed, '[' ) || fotogenic_ai_str_starts_with( $trimmed, '{' ) ) ) {
                $decoded = json_decode( $trimmed, true );
                if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
                    $job_ids = $decoded;
                } else {
                    $job_ids = [ $job_ids ];
                }
            } else {
                $job_ids = [ $job_ids ];
            }
        }
        if ( ! is_array( $job_ids ) ) {
            return [];
        }
		$normalized = [];
		foreach ( $job_ids as $job_id ) {
			$job_id = Upstream_Identifier::normalize( $job_id );
			if ( '' !== $job_id ) {
                $normalized[] = $job_id;
            }
        }
        return array_values( array_unique( $normalized ) );
    }

    private function normalize_scope_user_id( int $user_id ): int {
        $user_id = max( 0, $user_id );
        if ( $user_id <= 0 ) {
            return 0;
        }
        if ( user_can( $user_id, fotogenic_ai_minimum_cap() ) ) {
            return 0;
        }
        return $user_id;
    }
}
