<?php

namespace Fotogenic_AI;

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

/**
 * Durable request identities for billable workflow starts.
 */
final class Workflow_Request_Store {
    public const ACTIVE_STATUSES = [ 'starting', 'processing' ];
    public const SUCCESS_STATUSES = [ 'completed', 'partial' ];
    public const FAILURE_STATUSES = [ 'failed', 'timeout', 'stale' ];
    public const QUARANTINE_STATUSES = [ 'unresolved' ];
    public const WORKFLOWS = [ 'product_images', 'campaign_set', 'increase_resolution' ];
    public const RECORD_VERSION = 4;
	public const DISPATCH_INTENT_STATES = [ 'not_dispatched', 'in_flight', 'reconcile_ready', 'resolved', 'unresolved' ];

    private const OPTION_PREFIX = 'fotogenic_ai_workflow_request_';
    private const CLEANUP_OPTION = 'fotogenic_ai_workflow_cleanup_state';
    private const RECORD_RETENTION_SECONDS = 30 * 24 * HOUR_IN_SECONDS;
    private const CLEANUP_INTERVAL_SECONDS = 6 * HOUR_IN_SECONDS;
    private const CLEANUP_BATCH_SIZE = 500;
    private const MAX_RECOVERY_BATCH_SIZE = 100;
    private const MAX_UPDATE_ATTEMPTS = 5;

    private Workflow_Response_Factory $response_factory;

    public function __construct( Workflow_Response_Factory $response_factory ) {
        $this->response_factory = $response_factory;
    }

    public function get_option_key( int $user_id, string $workflow, string $request_id ): string {
        $workflow = sanitize_key( $workflow );
        $request_id = strtolower( trim( $request_id ) );
        if ( $user_id <= 0 || ! in_array( $workflow, self::WORKFLOWS, true ) || ! $this->is_valid_request_id( $request_id ) ) {
            return '';
        }
        return self::OPTION_PREFIX . hash( 'sha256', $user_id . '|' . $workflow . '|' . $request_id );
    }

    public function read( string $option_key ) {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Request coordination must bypass stale persistent option caches before CAS.
        $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 );
    }

    /** @return bool|\WP_Error */
    public function insert( string $option_key, array $record ) {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- The unique option_name index is the admission boundary for a billable request.
        $inserted = $wpdb->query(
            $wpdb->prepare(
                "INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, %s)",
                $option_key,
                maybe_serialize( $record ),
                'no'
            )
        );
        $this->clear_option_cache( $option_key );

        if ( false === $inserted ) {
            return $this->persist_error();
        }
        return 1 === (int) $inserted;
    }

    /** @return array|\WP_Error */
    public function update( array $record, array $changes ) {
        $option_key = (string) ( $record['option_key'] ?? '' );
        $token = (string) ( $record['token'] ?? '' );
        if ( '' === $option_key || '' === $token ) {
            return $this->conflict_error( (string) ( $record['request_id'] ?? '' ) );
        }

        for ( $attempt = 0; $attempt < self::MAX_UPDATE_ATTEMPTS; $attempt++ ) {
            $current = $this->read( $option_key );
            if ( ! is_array( $current ) || ! $this->identity_matches( $current, $record ) ) {
                return $this->conflict_error( (string) ( $record['request_id'] ?? '' ) );
            }

            $current_state = $this->normalize_state( (string) ( $current['state'] ?? 'starting' ) );
            $requested_state = array_key_exists( 'state', $changes )
                ? $this->normalize_state( (string) $changes['state'] )
                : $current_state;

            if ( $this->transition_is_stale( $current_state, $requested_state ) ) {
                return $current;
            }

            $updated = $current;
            $updated['state'] = $requested_state;
            if ( array_key_exists( 'run_id', $changes ) ) {
                $requested_run_id = substr( sanitize_text_field( (string) $changes['run_id'] ), 0, 64 );
                $current_run_id = (string) ( $current['run_id'] ?? '' );
                if ( '' !== $current_run_id && ! hash_equals( $current_run_id, $requested_run_id ) ) {
                    return $this->conflict_error( (string) ( $record['request_id'] ?? '' ) );
                }
                $updated['run_id'] = $requested_run_id;
            }
            if ( array_key_exists( 'response', $changes ) ) {
                $candidate = is_array( $changes['response'] ) ? $changes['response'] : [];
                $response_record = $updated;
                $response_record['response'] = $candidate;
                $updated['response'] = $this->response_factory->sanitize_stored_response( $response_record, $requested_state );
            }
            if ( array_key_exists( 'error', $changes ) ) {
                $updated['error'] = is_array( $changes['error'] ) ? $changes['error'] : [];
            }
            if ( array_key_exists( 'uncertain', $changes ) ) {
                $updated['uncertain'] = (bool) $changes['uncertain'];
            }
			if ( array_key_exists( 'upstream', $changes ) ) {
				$upstream = $this->normalize_upstream_update( $current['upstream'] ?? null, $changes['upstream'] );
				if ( is_wp_error( $upstream ) ) {
					return $upstream;
				}
				$updated['upstream'] = $upstream;
			}
			if ( array_key_exists( 'dispatch', $changes ) ) {
				$dispatch = $this->normalize_dispatch_update( $current['dispatch'] ?? null, $changes['dispatch'] );
				if ( is_wp_error( $dispatch ) ) {
					return $dispatch;
				}
				$updated['dispatch'] = $dispatch;
			}
			if ( array_key_exists( 'admission', $changes ) ) {
				$admission = $this->normalize_admission_update( $current['admission'] ?? [], $changes['admission'] );
				if ( is_wp_error( $admission ) ) {
					return $admission;
				}
				$updated['admission'] = $admission;
			}
			if ( array_key_exists( 'admission_evidence', $changes ) ) {
				$admission_evidence = $this->normalize_admission_evidence_update(
					$current['admission_evidence'] ?? self::initial_admission_evidence(),
					$changes['admission_evidence']
				);
				if ( is_wp_error( $admission_evidence ) ) {
					return $admission_evidence;
				}
				$updated['admission_evidence'] = $admission_evidence;
			}

            if ( $updated === $current ) {
                return $current;
            }
            $updated['updated_at'] = max( time(), (int) ( $current['updated_at'] ?? 0 ) + 1 );

            $changed = $this->compare_and_update( $option_key, $current, $updated );
            if ( is_wp_error( $changed ) ) {
                return $changed;
            }
            if ( ! $changed ) {
                continue;
            }

            $stored = $this->read( $option_key );
            if ( is_array( $stored ) && $this->identity_matches( $stored, $record ) ) {
                return $stored;
            }
            return $this->persist_error();
        }

        $latest = $this->read( $option_key );
        if ( is_array( $latest ) && $this->identity_matches( $latest, $record ) ) {
            $latest_state = $this->normalize_state( (string) ( $latest['state'] ?? 'starting' ) );
            $requested_state = array_key_exists( 'state', $changes )
                ? $this->normalize_state( (string) $changes['state'] )
                : $latest_state;
            if ( $this->transition_is_stale( $latest_state, $requested_state ) ) {
                return $latest;
            }
        }

        return $this->persist_error();
    }

    /** @return bool|\WP_Error */
    public function matches(
        array $record,
        int $user_id,
        string $workflow,
        string $request_id,
        string $fingerprint,
        string $option_key
    ) {
        if (
            self::RECORD_VERSION !== (int) ( $record['version'] ?? 0 )
            || $user_id !== (int) ( $record['user_id'] ?? 0 )
            || $workflow !== (string) ( $record['workflow'] ?? '' )
            || ! hash_equals( $request_id, (string) ( $record['request_id'] ?? '' ) )
            || ! hash_equals( $option_key, (string) ( $record['option_key'] ?? '' ) )
            || '' === (string) ( $record['token'] ?? '' )
            || ! preg_match( '/^[a-f0-9]{64}$/', (string) ( $record['payload_fingerprint'] ?? '' ) )
			|| ! $this->has_valid_upstream_identity( $record )
			|| ! $this->has_valid_review( $record['review'] ?? null )
			|| ! $this->has_valid_admission_evidence( $record['admission_evidence'] ?? null )
        ) {
            return $this->conflict_error( $request_id );
        }
        if ( ! hash_equals( $fingerprint, (string) ( $record['payload_fingerprint'] ?? '' ) ) ) {
            return new \WP_Error(
                'fotogenic_ai_workflow_request_payload_mismatch',
                __( 'This requestId was already used with different workflow inputs.', 'fotogenic-ai' ),
                [
                    'status'    => 409,
                    'requestId' => $request_id,
                ]
            );
        }
        return true;
    }

    /**
     * Match the immutable universal Studio command before any mutable source lookup.
     * @return bool|\WP_Error
     */
    public function matches_universal(
        array $record,
        int $user_id,
        string $workflow,
        string $request_id,
        string $fingerprint,
        string $option_key
    ) {
        if (
            self::RECORD_VERSION !== (int) ( $record['version'] ?? 0 )
            || $user_id !== (int) ( $record['user_id'] ?? 0 )
            || $workflow !== (string) ( $record['workflow'] ?? '' )
            || ! hash_equals( $request_id, (string) ( $record['request_id'] ?? '' ) )
            || ! hash_equals( $option_key, (string) ( $record['option_key'] ?? '' ) )
            || '' === (string) ( $record['token'] ?? '' )
			|| ! $this->has_valid_upstream_identity( $record )
        ) {
            return $this->conflict_error( $request_id );
        }

        $stored = (string) ( $record['universal_fingerprint'] ?? '' );
        if ( ! preg_match( '/^[a-f0-9]{64}$/', $stored ) || ! hash_equals( $fingerprint, $stored ) ) {
            return new \WP_Error(
                'fotogenic_ai_workflow_request_payload_mismatch',
                __( 'This requestId was already used with different workflow inputs.', 'fotogenic-ai' ),
                [
                    'status'    => 409,
                    'requestId' => $request_id,
                ]
            );
        }

        return true;
    }

    public function conflict_error( string $request_id ): \WP_Error {
        return new \WP_Error(
            'fotogenic_ai_workflow_request_conflict',
            __( 'This workflow request identity is already reserved and cannot be started again.', 'fotogenic-ai' ),
            [
                'status'    => 409,
                'requestId' => $request_id,
            ]
        );
    }

    public function normalize_state( string $state ): string {
        $state = sanitize_key( $state );
        return in_array( $state, array_merge( self::ACTIVE_STATUSES, self::SUCCESS_STATUSES, self::FAILURE_STATUSES, self::QUARANTINE_STATUSES ), true )
            ? $state
            : 'starting';
    }

    public 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 );
    }

    /**
     * Read one bounded page of durable request records for stale recovery.
     *
     * @return array{rows: array, next_cursor: int}
     */
    public function get_recovery_page( int $cursor, int $limit ): array {
        global $wpdb;

        $cursor = max( 0, $cursor );
        $limit = max( 1, min( self::MAX_RECOVERY_BATCH_SIZE, $limit ) );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Scheduled housekeeping performs a bounded prefix scan of durable workflow identities.
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT option_id, option_name, option_value FROM {$wpdb->options}
                WHERE option_name LIKE %s AND option_id > %d
                ORDER BY option_id ASC LIMIT %d",
                $wpdb->esc_like( self::OPTION_PREFIX ) . '%',
                $cursor,
                $limit
            ),
            ARRAY_A
        );
        if ( ! is_array( $rows ) || empty( $rows ) ) {
            return [
                'rows'        => [],
                'next_cursor' => 0,
            ];
        }

        $last_option_id = $cursor;
        foreach ( $rows as $row ) {
            $last_option_id = max( $last_option_id, (int) ( $row['option_id'] ?? 0 ) );
        }
        return [
            'rows'        => $rows,
            'next_cursor' => count( $rows ) < $limit ? 0 : $last_option_id,
        ];
    }

    /**
     * Lazily remove expired terminal request records without scanning the full
     * options table on a normal workflow request.
     */
    public function maybe_cleanup(): void {
        $now = time();
        $state = get_option( self::CLEANUP_OPTION, [] );
        $state = is_array( $state ) ? $state : [];
        if ( (int) ( $state['next_at'] ?? 0 ) > $now ) {
            return;
        }

        $cursor = max( 0, (int) ( $state['cursor'] ?? 0 ) );
        update_option(
            self::CLEANUP_OPTION,
            [
                'next_at' => $now + self::CLEANUP_INTERVAL_SECONDS,
                'cursor'  => $cursor,
            ],
            false
        );

        $next_cursor = $this->cleanup_expired_records( $now, $cursor );
        update_option(
            self::CLEANUP_OPTION,
            [
                'next_at' => $now + self::CLEANUP_INTERVAL_SECONDS,
                'cursor'  => $next_cursor,
            ],
            false
        );
    }

	private function identity_matches( array $current, array $record ): bool {
        return self::RECORD_VERSION === (int) ( $current['version'] ?? 0 )
            && hash_equals( (string) ( $record['option_key'] ?? '' ), (string) ( $current['option_key'] ?? '' ) )
            && hash_equals( (string) ( $record['token'] ?? '' ), (string) ( $current['token'] ?? '' ) )
            && (int) ( $record['user_id'] ?? 0 ) === (int) ( $current['user_id'] ?? 0 )
            && hash_equals( (string) ( $record['workflow'] ?? '' ), (string) ( $current['workflow'] ?? '' ) )
            && hash_equals( (string) ( $record['request_id'] ?? '' ), (string) ( $current['request_id'] ?? '' ) )
            && hash_equals( (string) ( $record['payload_fingerprint'] ?? '' ), (string) ( $current['payload_fingerprint'] ?? '' ) )
			&& hash_equals( (string) ( $record['universal_fingerprint'] ?? '' ), (string) ( $current['universal_fingerprint'] ?? '' ) )
			&& $this->has_valid_review( $record['review'] ?? null )
			&& $this->has_valid_review( $current['review'] ?? null )
			&& hash_equals( maybe_serialize( $record['review'] ), maybe_serialize( $current['review'] ) )
			&& $this->has_valid_admission_evidence( $record['admission_evidence'] ?? null )
			&& $this->has_valid_admission_evidence( $current['admission_evidence'] ?? null )
			&& $this->has_valid_upstream_identity( $record )
			&& $this->has_valid_upstream_identity( $current )
			&& hash_equals(
				(string) $record['upstream']['idempotency_key'],
				(string) $current['upstream']['idempotency_key']
			);
    }

	private function has_valid_upstream_identity( array $record ): bool {
		$upstream = $record['upstream'] ?? null;
		return is_array( $upstream )
			&& [ 'idempotency_key', 'request_id', 'generation_request_id', 'reconcile_state' ] === array_keys( $upstream )
			&& Upstream_Request_Identity::is_valid( (string) $upstream['idempotency_key'] )
			&& is_string( $upstream['request_id'] )
			&& is_string( $upstream['generation_request_id'] )
			&& is_string( $upstream['reconcile_state'] );
	}

	/** @return array{job_ids:array<int,string>,accepted_output_count:?int,credits_debited:?float} */
	public static function initial_admission_evidence(): array {
		return [
			'job_ids'               => [],
			'accepted_output_count' => null,
			'credits_debited'        => null,
		];
	}

	private function has_valid_review( $review ): bool {
		return is_array( $review )
			&& [ 'expected_output_count', 'estimated_cost' ] === array_keys( $review )
			&& is_int( $review['expected_output_count'] )
			&& $review['expected_output_count'] >= 1
			&& $review['expected_output_count'] <= 6
			&& ( is_int( $review['estimated_cost'] ) || is_float( $review['estimated_cost'] ) )
			&& is_finite( (float) $review['estimated_cost'] )
			&& (float) $review['estimated_cost'] >= 0;
	}

	private function has_valid_admission_evidence( $evidence ): bool {
		if (
			! is_array( $evidence )
			|| [ 'job_ids', 'accepted_output_count', 'credits_debited' ] !== array_keys( $evidence )
			|| ! is_array( $evidence['job_ids'] )
			|| ! fotogenic_ai_array_is_list( $evidence['job_ids'] )
		) {
			return false;
		}
		$seen = [];
		foreach ( $evidence['job_ids'] as $job_id ) {
			$job_id = Upstream_Identifier::normalize( $job_id );
			if ( '' === $job_id || isset( $seen[ $job_id ] ) ) {
				return false;
			}
			$seen[ $job_id ] = true;
		}
		$accepted_count = $evidence['accepted_output_count'];
		$credits_debited = $evidence['credits_debited'];
		return ( null === $accepted_count || ( is_int( $accepted_count ) && $accepted_count >= 1 && $accepted_count <= 6 ) )
			&& ( null === $credits_debited || ( ( is_int( $credits_debited ) || is_float( $credits_debited ) ) && is_finite( (float) $credits_debited ) && (float) $credits_debited >= 0 ) );
	}

	/** @return array{job_ids:array<int,string>,accepted_output_count:?int,credits_debited:?float}|\WP_Error */
	private function normalize_admission_evidence_update( $current, $candidate ) {
		if ( ! $this->has_valid_admission_evidence( $current ) || ! $this->has_valid_admission_evidence( $candidate ) ) {
			return $this->conflict_error( '' );
		}
		$normalized = [
			'job_ids' => array_map( static fn( $job_id ): string => Upstream_Identifier::normalize( $job_id ), $candidate['job_ids'] ),
			'accepted_output_count' => $candidate['accepted_output_count'],
			'credits_debited' => null === $candidate['credits_debited'] ? null : (float) $candidate['credits_debited'],
		];
		foreach ( [ 'job_ids', 'accepted_output_count', 'credits_debited' ] as $field ) {
			$current_value = $current[ $field ];
			$candidate_value = $normalized[ $field ];
			$current_empty = 'job_ids' === $field ? [] === $current_value : null === $current_value;
			$candidate_empty = 'job_ids' === $field ? [] === $candidate_value : null === $candidate_value;
			if ( ! $current_empty && ( $candidate_empty || ! hash_equals( maybe_serialize( $current_value ), maybe_serialize( $candidate_value ) ) ) ) {
				return $this->conflict_error( '' );
			}
		}
		return $normalized;
	}

	/** @return array{state:string,reconcile_attempts:int,reconcile_not_before_at:int,reconcile_deadline_at:int} */
	public static function initial_dispatch_intent(): array {
		return [
			'state'                   => 'not_dispatched',
			'reconcile_attempts'      => 0,
			'reconcile_not_before_at' => 0,
			'reconcile_deadline_at'   => 0,
		];
	}

	/** @return array{state:string,reconcile_attempts:int,reconcile_not_before_at:int,reconcile_deadline_at:int}|\WP_Error */
	private function normalize_dispatch_update( $current, $candidate ) {
		$current = is_array( $current ) ? $current : self::initial_dispatch_intent();
		if (
			! is_array( $candidate )
			|| [ 'state', 'reconcile_attempts', 'reconcile_not_before_at', 'reconcile_deadline_at' ] !== array_keys( $current )
			|| [ 'state', 'reconcile_attempts', 'reconcile_not_before_at', 'reconcile_deadline_at' ] !== array_keys( $candidate )
			|| ! in_array( $current['state'], self::DISPATCH_INTENT_STATES, true )
			|| ! in_array( $candidate['state'], self::DISPATCH_INTENT_STATES, true )
			|| ! is_int( $candidate['reconcile_attempts'] )
			|| ! is_int( $candidate['reconcile_not_before_at'] )
			|| ! is_int( $candidate['reconcile_deadline_at'] )
			|| $candidate['reconcile_attempts'] < 0
			|| $candidate['reconcile_not_before_at'] < 0
			|| $candidate['reconcile_deadline_at'] < 0
		) {
			return $this->conflict_error( '' );
		}

		$allowed_transitions = [
			'not_dispatched' => [ 'not_dispatched', 'in_flight' ],
			'in_flight'      => [ 'in_flight', 'reconcile_ready', 'resolved', 'unresolved' ],
			'reconcile_ready' => [ 'reconcile_ready', 'resolved', 'unresolved' ],
			'resolved'       => [ 'resolved' ],
			'unresolved'     => [ 'unresolved' ],
		];
		if ( ! in_array( $candidate['state'], $allowed_transitions[ $current['state'] ], true ) ) {
			return $this->conflict_error( '' );
		}
		if ( $candidate['reconcile_attempts'] < (int) $current['reconcile_attempts'] ) {
			return $this->conflict_error( '' );
		}

		$current_state = (string) $current['state'];
		$candidate_state = (string) $candidate['state'];
		$candidate_has_timing = $candidate['reconcile_not_before_at'] > 0 || $candidate['reconcile_deadline_at'] > 0;
		if (
			$candidate_has_timing
			&& (
				$candidate['reconcile_not_before_at'] <= 0
				|| $candidate['reconcile_deadline_at'] < $candidate['reconcile_not_before_at']
			)
		) {
			return $this->conflict_error( '' );
		}
		if (
			'not_dispatched' === $candidate_state
			&& (
				0 !== $candidate['reconcile_attempts']
				|| 0 !== $candidate['reconcile_not_before_at']
				|| 0 !== $candidate['reconcile_deadline_at']
			)
		) {
			return $this->conflict_error( '' );
		}
		if (
			'in_flight' === $candidate_state
			&& (
				0 !== $candidate['reconcile_attempts']
				|| ! $candidate_has_timing
			)
		) {
			return $this->conflict_error( '' );
		}

		$entering_ready = 'in_flight' === $current_state && 'reconcile_ready' === $candidate_state;
		if (
			! $entering_ready
			&& (
				$candidate['reconcile_not_before_at'] < (int) $current['reconcile_not_before_at']
				|| $candidate['reconcile_deadline_at'] < (int) $current['reconcile_deadline_at']
			)
		) {
			return $this->conflict_error( '' );
		}
		if ( $entering_ready && $candidate['reconcile_deadline_at'] < (int) $current['reconcile_deadline_at'] ) {
			return $this->conflict_error( '' );
		}

		return $candidate;
	}

	/** @return array{accepted_output_count:int,outputs:array<int,array{output_id:string,output_index:int,job_id:string,status:string}>}|array{}|\WP_Error */
	private function normalize_admission_update( $current, $candidate ) {
		$current = is_array( $current ) ? $current : [];
		if ( [] === $candidate && [] === $current ) {
			return [];
		}
		$normalized = Generate_Campaign_Admission::normalize( $candidate );
		if ( empty( $normalized ) ) {
			return $this->conflict_error( '' );
		}
		if ( [] !== $current && ! hash_equals( maybe_serialize( $current ), maybe_serialize( $normalized ) ) ) {
			return $this->conflict_error( '' );
		}
		return $normalized;
	}

	/** @return array<string,string>|\WP_Error */
	private function normalize_upstream_update( $current, $candidate ) {
		if ( ! is_array( $current ) || ! is_array( $candidate ) || [ 'idempotency_key', 'request_id', 'generation_request_id', 'reconcile_state' ] !== array_keys( $candidate ) ) {
			return $this->conflict_error( '' );
		}
		$idempotency_key = (string) ( $candidate['idempotency_key'] ?? '' );
		$request_id = $candidate['request_id'] ?? null;
		$generation_request_id = $candidate['generation_request_id'] ?? null;
		$reconcile_state = $candidate['reconcile_state'] ?? null;
		if (
			! Upstream_Request_Identity::is_valid( $idempotency_key )
			|| ! is_string( $request_id )
			|| ! is_string( $generation_request_id )
			|| ! is_string( $reconcile_state )
			|| strlen( $request_id ) > 255
			|| strlen( $generation_request_id ) > 255
			|| ! in_array( $reconcile_state, [ '', 'accepted', 'processing', 'completed', 'failed', 'unknown', 'not_found' ], true )
			|| ! hash_equals( (string) ( $current['idempotency_key'] ?? '' ), $idempotency_key )
			|| ( '' !== (string) ( $current['request_id'] ?? '' ) && ! hash_equals( (string) $current['request_id'], $request_id ) )
			|| ( '' !== (string) ( $current['generation_request_id'] ?? '' ) && ! hash_equals( (string) $current['generation_request_id'], $generation_request_id ) )
		) {
			return $this->conflict_error( '' );
		}

		return [
			'idempotency_key'       => $idempotency_key,
			'request_id'            => $request_id,
			'generation_request_id' => $generation_request_id,
			'reconcile_state'       => $reconcile_state,
		];
	}

    private function transition_is_stale( string $current_state, string $requested_state ): bool {
        if ( in_array( $current_state, array_merge( self::SUCCESS_STATUSES, self::FAILURE_STATUSES, self::QUARANTINE_STATUSES ), true ) ) {
            return true;
        }
        return 'processing' === $current_state && 'starting' === $requested_state;
    }

    /** @return bool|\WP_Error */
    private function compare_and_update( string $option_key, array $current, array $updated ) {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Byte-exact CAS prevents stale request writers from overwriting newer state.
        $changed = $wpdb->query(
            $wpdb->prepare(
                "UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND BINARY option_value = BINARY %s",
                maybe_serialize( $updated ),
                $option_key,
                maybe_serialize( $current )
            )
        );
        $this->clear_option_cache( $option_key );

        if ( false === $changed ) {
            return $this->persist_error();
        }
        return 1 === (int) $changed;
    }

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

    /**
     * Process one bounded batch and return the cursor for the next cleanup.
     */
    private function cleanup_expired_records( int $now, int $cursor ): int {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- A bounded prefix scan is required to expire per-request coordination options.
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT option_id, option_name, option_value FROM {$wpdb->options}
                WHERE option_name LIKE %s AND option_id > %d
                ORDER BY option_id ASC LIMIT %d",
                $wpdb->esc_like( self::OPTION_PREFIX ) . '%',
                max( 0, $cursor ),
                self::CLEANUP_BATCH_SIZE
            ),
            ARRAY_A
        );
        if ( ! is_array( $rows ) || empty( $rows ) ) {
            return 0;
        }

        $last_option_id = 0;
        foreach ( $rows as $row ) {
            $last_option_id = max( $last_option_id, (int) ( $row['option_id'] ?? 0 ) );
            $option_name = (string) ( $row['option_name'] ?? '' );
            if ( ! fotogenic_ai_str_starts_with( $option_name, self::OPTION_PREFIX ) ) {
                continue;
            }
            $record = maybe_unserialize( $row['option_value'] ?? null );
            if ( ! is_array( $record ) || ! $this->is_expired( $record, $now ) ) {
                continue;
            }
            delete_option( $option_name );
        }

        return count( $rows ) < self::CLEANUP_BATCH_SIZE ? 0 : $last_option_id;
    }

    private function is_expired( array $record, int $now ): bool {
        $state = $this->normalize_state( (string) ( $record['state'] ?? '' ) );
        if ( ! in_array( $state, array_merge( self::SUCCESS_STATUSES, self::FAILURE_STATUSES ), true ) ) {
            return false;
        }
        $updated_at = (int) ( $record['updated_at'] ?? $record['created_at'] ?? 0 );
        return $updated_at > 0 && $now - $updated_at > self::RECORD_RETENTION_SECONDS;
    }

    private function persist_error(): \WP_Error {
        return new \WP_Error(
            'fotogenic_ai_workflow_request_persist_failed',
            __( 'The workflow request identity could not be persisted.', 'fotogenic-ai' ),
            [ 'status' => 503 ]
        );
    }
}
