<?php

namespace Fotogenic_AI;

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

/**
 * Shared start lifecycle for billable workflows.
 */
final class Workflow_Start_Coordinator {
    private const MAX_ERROR_MESSAGE_LENGTH = 500;
    private const RUN_LIFECYCLE_LOCK_TTL = 600;
    private const DISPATCHED_UNCERTAIN_RUN_ERRORS = [ 'start_projection_pending', 'start_response_uncertain', 'claim_lost_after_start' ];
    private const DISPATCHED_UNCERTAIN_REQUEST_ERROR = 'fotogenic_ai_workflow_start_uncertain';

    private Workflow_Request_Store $request_store;
    private Workflow_Response_Factory $response_factory;
    private Workflow_Upstream_Response_Parser $upstream_response_parser;
    private Workflow_Request_Reconciler $workflow_request_reconciler;

    /** @return bool|\WP_Error */
    public function ensure_connected() {
        if ( ! Auth_Session::instance()->is_connected() ) {
            return Messages::error( 'missing_auth_session', 400 );
        }
        return true;
    }

    /** @return string|\WP_Error */
    public function validate_request_id( string $request_id ) {
        $request_id = strtolower( trim( $request_id ) );
        if ( ! $this->request_store()->is_valid_request_id( $request_id ) ) {
            return new \WP_Error(
                'fotogenic_ai_invalid_request_id',
                __( 'A valid requestId is required.', 'fotogenic-ai' ),
                [ 'status' => 400 ]
            );
        }
        return $request_id;
    }

    /**
     * Capture a safe terminal replay payload before logger retention removes the run row.
     */
    public static function sync_finalized_request( string $run_id, string $final_status ): void {
        $coordinator = new self();
        $coordinator->sync_finalized_request_record( $run_id, $final_status );
    }

    /**
     * Terminalize one stale workflow run under the shared lifecycle lock.
     */
    public static function terminalize_stale_run( string $run_id ): bool {
        $coordinator = new self();
        $run_id = substr( sanitize_text_field( trim( $run_id ) ), 0, 64 );
        if ( '' === $run_id ) {
            return false;
        }

        $lock = Background_Lock::instance();
        $lock_key = $coordinator->get_run_lifecycle_lock_key( $run_id );
        $token = $lock->acquire( $lock_key, self::RUN_LIFECYCLE_LOCK_TTL );
        if ( false === $token ) {
            return false;
        }
        try {
            return $coordinator->terminalize_stale_run_locked( $run_id );
        } finally {
            $lock->release( $lock_key, $token );
        }
    }

    /**
     * Recover one bounded page of stale durable request records.
     *
     * @return int Cursor for the next bounded page, or zero after the final page.
     */
    public static function recover_stale_request_records( int $cursor, int $limit ): int {
        $coordinator = new self();
        return $coordinator->recover_stale_request_record_batch( $cursor, $limit );
    }

    /**
     * Hash a normalized request payload without retaining its potentially private text.
     * @return string|\WP_Error
     */
    public function fingerprint_payload( string $workflow, array $payload ) {
        $workflow = sanitize_key( $workflow );
        if ( ! in_array( $workflow, Workflow_Request_Store::WORKFLOWS, true ) ) {
            return Messages::error( 'invalid_request', 400 );
        }

        $valid = true;
        $canonical = $this->canonicalize_payload_value(
            [
                'schemaVersion' => Workflow_Request_Store::RECORD_VERSION,
                'workflow'      => $workflow,
                'payload'       => $payload,
            ],
            $valid
        );
        if ( ! $valid ) {
            return new \WP_Error(
                'fotogenic_ai_invalid_workflow_payload',
                __( 'The workflow request payload is invalid.', 'fotogenic-ai' ),
                [ 'status' => 400 ]
            );
        }

        $encoded = wp_json_encode( $canonical, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
        if ( ! is_string( $encoded ) || '' === $encoded ) {
            return Messages::error( 'request_failed', 500 );
        }
        return hash( 'sha256', $encoded );
    }

    /**
     * Replay a previously admitted request, or return null when this UUID is unused.
     *
     * @return array|\WP_Error|null
     */
    public function replay_request( int $user_id, string $workflow, string $request_id, string $fingerprint ) {
        $store = $this->request_store();
        $option_key = $store->get_option_key( $user_id, $workflow, $request_id );
        if ( '' === $option_key ) {
            return Messages::error( 'invalid_request', 400 );
        }

        $record = $store->read( $option_key );
        if ( null === $record ) {
            return null;
        }
        if ( ! is_array( $record ) ) {
            return $store->conflict_error( $request_id );
        }

        $matches = $store->matches( $record, $user_id, $workflow, $request_id, $fingerprint, $option_key );
        if ( is_wp_error( $matches ) ) {
            return $matches;
        }
        return $this->replay_request_record( $record );
    }

    /**
     * Replay an immutable universal Studio command without re-reading mutable sources.
     *
     * @return array|\WP_Error|null
     */
    public function replay_universal_request( int $user_id, string $workflow, string $request_id, string $fingerprint ) {
        if ( ! preg_match( '/^[a-f0-9]{64}$/', $fingerprint ) ) {
            return Messages::error( 'invalid_request', 400 );
        }

        $store      = $this->request_store();
        $option_key = $store->get_option_key( $user_id, $workflow, $request_id );
        if ( '' === $option_key ) {
            return Messages::error( 'invalid_request', 400 );
        }
        $record = $store->read( $option_key );
        if ( null === $record ) {
            return null;
        }
        if ( ! is_array( $record ) ) {
            return $store->conflict_error( $request_id );
        }

        $matches = $store->matches_universal( $record, $user_id, $workflow, $request_id, $fingerprint, $option_key );
        if ( is_wp_error( $matches ) ) {
            return $matches;
        }

        return $this->replay_request_record( $record );
    }

    /**
     * Prevent a different request UUID from crossing an active subject/workflow start.
     *
     * The durable request UUID protects exact replays. This subject-scoped guard
     * protects a second tab or bulk worker that presents a different UUID after
     * the short admission claim has expired. A dispatched uncertain run remains
     * active indefinitely because elapsed time cannot prove that it was not billed.
     * @return bool|\WP_Error
     */
    public function guard_subject_admission( int $user_id, string $workflow, string $request_id, array $context ) {
        $workflow = sanitize_key( $workflow );
        $run_target = sanitize_key( (string) ( $context['run_target'] ?? '' ) );
        $subject_id = (int) ( $context['subject_id'] ?? 0 );
        if (
            $user_id <= 0
            || ! in_array( $workflow, Workflow_Request_Store::WORKFLOWS, true )
            || ! $this->request_store()->is_valid_request_id( $request_id )
            || ! in_array( $run_target, $this->allowed_targets_for_workflow( $workflow ), true )
            || $subject_id <= 0
        ) {
            return Messages::error( 'invalid_request', 400 );
        }

        $logger = Logger::instance();
        $run_item_uuid = strtolower( trim( (string) ( $context['run_item_uuid'] ?? '' ) ) );
        $parallel_item = ! empty( $context['parallel_item_identity'] ) && wp_is_uuid( $run_item_uuid );
        $row = $parallel_item
            ? $logger->get_latest_workflow_run_for_item( $user_id, $workflow, $run_item_uuid )
            : $logger->get_latest_workflow_run_for_subject( $user_id, $workflow, $run_target, $subject_id );
        if ( empty( $row ) ) {
            return true;
        }

        $status = $this->normalize_run_status( (string) ( $row['final_status'] ?? '' ) );
		if ( in_array( $status, Workflow_Request_Store::QUARANTINE_STATUSES, true ) ) {
			return new \WP_Error(
				'fotogenic_ai_workflow_reconciliation_unresolved',
				__( 'A previous generation for this item still has an unresolved upstream result. Contact support with the request ID before starting another generation.', 'fotogenic-ai' ),
				[
					'status'         => 409,
					'requestId'      => strtolower( trim( (string) ( $row['request_id'] ?? '' ) ) ),
					'runId'          => substr( sanitize_text_field( (string) ( $row['run_id'] ?? '' ) ), 0, 64 ),
					'jobIds'         => $this->response_factory()->normalize_job_ids( $row['job_ids'] ?? [] ),
					'workflowStatus' => 'unresolved',
					'uncertain'      => true,
				]
			);
		}
        if ( in_array( $status, Workflow_Request_Store::ACTIVE_STATUSES, true ) && $this->run_is_stale( $row, $workflow ) ) {
            self::terminalize_stale_run( (string) ( $row['run_id'] ?? '' ) );
            $row = $parallel_item
                ? $logger->get_latest_workflow_run_for_item( $user_id, $workflow, $run_item_uuid )
                : $logger->get_latest_workflow_run_for_subject( $user_id, $workflow, $run_target, $subject_id );
            $status = $this->normalize_run_status( (string) ( $row['final_status'] ?? '' ) );
        }
        if ( empty( $row ) || ! in_array( $status, Workflow_Request_Store::ACTIVE_STATUSES, true ) ) {
            $has_active = $parallel_item
                ? $logger->has_active_workflow_run_for_item( $user_id, $workflow, $run_item_uuid )
                : $logger->has_active_workflow_run( $user_id, $workflow, $run_target, $subject_id );
            if ( empty( $row ) || ! $has_active ) {
                return true;
            }
            return new \WP_Error(
                'fotogenic_ai_workflow_start_in_progress',
                __( 'A generation start is already being resolved for this item. Do not submit another request yet.', 'fotogenic-ai' ),
                [
                    'status'         => 409,
                    'requestId'      => '',
                    'runId'          => '',
                    'jobIds'         => [],
                    'workflowStatus' => 'starting',
                    'uncertain'      => false,
                ]
            );
        }

        $existing_request_id = strtolower( trim( (string) ( $row['request_id'] ?? '' ) ) );
        $job_ids = $this->response_factory()->normalize_job_ids( $row['job_ids'] ?? [] );
        return new \WP_Error(
            'fotogenic_ai_workflow_start_in_progress',
            __( 'A generation start is already being resolved for this item. Do not submit another request yet.', 'fotogenic-ai' ),
            [
                'status'         => 409,
                'requestId'      => $existing_request_id,
                'runId'          => (string) ( $row['run_id'] ?? '' ),
                'jobIds'         => $job_ids,
                'workflowStatus' => $status,
                'uncertain'      => empty( $job_ids ) && 'start_response_uncertain' === (string) ( $row['error_code'] ?? '' ),
            ]
        );
    }

    /**
     * Atomically admit one owner/workflow/request UUID before a billable call.
     *
     * @return array|\WP_Error Admission wrapper. A replay wrapper contains the stored result.
     */
    public function admit_request(
        int $user_id,
        string $workflow,
        string $request_id,
        string $fingerprint,
        array $context,
        array $metadata,
        string $universal_fingerprint = ''
    ) {
        $store = $this->request_store();
        $option_key = $store->get_option_key( $user_id, $workflow, $request_id );
        if (
            '' === $option_key
            || ! preg_match( '/^[a-f0-9]{64}$/', $fingerprint )
            || ( '' !== $universal_fingerprint && ! preg_match( '/^[a-f0-9]{64}$/', $universal_fingerprint ) )
        ) {
            return Messages::error( 'invalid_request', 400 );
        }
        $store->maybe_cleanup();
		$upstream_idempotency_key = Upstream_Request_Identity::create();
		if ( is_wp_error( $upstream_idempotency_key ) ) {
			return $upstream_idempotency_key;
		}

        $now = time();
		$reviewed_output_count = $metadata['expected_output_count'] ?? null;
		$reviewed_estimated_cost = $metadata['estimated_cost'] ?? null;
		if (
			! is_int( $reviewed_output_count )
			|| $reviewed_output_count < 1
			|| $reviewed_output_count > 6
			|| ( ! is_int( $reviewed_estimated_cost ) && ! is_float( $reviewed_estimated_cost ) )
			|| ! is_finite( (float) $reviewed_estimated_cost )
			|| (float) $reviewed_estimated_cost < 0
		) {
			return Messages::error( 'invalid_request', 400 );
		}
        $record = [
            'version'             => Workflow_Request_Store::RECORD_VERSION,
            'option_key'          => $option_key,
            'token'               => wp_generate_uuid4(),
            'user_id'             => $user_id,
            'workflow'            => $workflow,
            'request_id'          => $request_id,
            'payload_fingerprint' => $fingerprint,
            'universal_fingerprint'=> $universal_fingerprint,
			'upstream'            => [
				'idempotency_key'       => $upstream_idempotency_key,
				'request_id'            => '',
				'generation_request_id' => '',
				'reconcile_state'       => '',
			],
            'state'               => 'starting',
            'run_id'              => '',
            'response'            => $this->response_factory()->build_context_response( $workflow, $request_id, $context, $metadata, 'starting' ),
            'error'               => [],
            'uncertain'           => false,
			'dispatch'            => Workflow_Request_Store::initial_dispatch_intent(),
			'admission'           => [],
			'review'              => [
				'expected_output_count' => $reviewed_output_count,
				'estimated_cost'        => (float) $reviewed_estimated_cost,
			],
			'admission_evidence'  => Workflow_Request_Store::initial_admission_evidence(),
            'created_at'          => $now,
            'updated_at'          => $now,
        ];

        $inserted = $store->insert( $option_key, $record );
        if ( true === $inserted ) {
            return [
                'replayed' => false,
                'record'   => $record,
            ];
        }

        $existing = $store->read( $option_key );
        if ( ! is_array( $existing ) ) {
            if ( is_wp_error( $inserted ) ) {
                return $inserted;
            }
            return $store->conflict_error( $request_id );
        }
        $matches = $store->matches( $existing, $user_id, $workflow, $request_id, $fingerprint, $option_key );
        if ( is_wp_error( $matches ) ) {
            return $matches;
        }
        if ( '' !== $universal_fingerprint ) {
            $universal_matches = $store->matches_universal(
                $existing,
                $user_id,
                $workflow,
                $request_id,
                $universal_fingerprint,
                $option_key
            );
            if ( is_wp_error( $universal_matches ) ) {
                return $universal_matches;
            }
        }

        return [
            'replayed' => true,
            'result'   => $this->replay_request_record( $existing ),
        ];
    }

	/**
	 * Return the immutable upstream key from a newly admitted request.
	 *
	 * @return string|\WP_Error
	 */
	public function get_upstream_idempotency_key( array $admission ) {
		$record = is_array( $admission['record'] ?? null ) ? $admission['record'] : [];
		$key = is_array( $record['upstream'] ?? null )
			? (string) ( $record['upstream']['idempotency_key'] ?? '' )
			: '';
		if ( ! Upstream_Request_Identity::is_valid( $key ) ) {
			return new \WP_Error(
				'fotogenic_ai_invalid_idempotency_key',
				__( 'The generation request identity is invalid.', 'fotogenic-ai' ),
				[
					'status'         => 500,
					'not_dispatched' => true,
				]
			);
		}

		return $key;
	}

	/**
	 * Persist and verify irreversible dispatch intent immediately before a
	 * billable upstream POST. Callers must refuse the POST on any error.
	 *
	 * @return array|\WP_Error
	 */
	public function prepare_upstream_dispatch( array $start ) {
		$record = is_array( $start['request_record'] ?? null ) ? $start['request_record'] : [];
		if ( empty( $record ) ) {
			return $this->not_dispatched_start_error();
		}
		$current = $this->request_store()->read( (string) ( $record['option_key'] ?? '' ) );
		if (
			! is_array( $current )
			|| (string) ( $record['token'] ?? '' ) !== (string) ( $current['token'] ?? '' )
			|| 'not_dispatched' !== $this->dispatch_intent( $current )['state']
		) {
			return $this->not_dispatched_start_error();
		}
		$now = time();
		$reconcile_not_before_at = $now + Workflow_Operational_Limits::billable_transport_envelope_seconds();
		$reconcile_deadline_at = $reconcile_not_before_at + Workflow_Operational_Limits::RECONCILIATION_WINDOW_SECONDS;
		$updated = $this->request_store()->update(
			$current,
			[
				'dispatch' => [
					'state'                   => 'in_flight',
					'reconcile_attempts'      => 0,
					'reconcile_not_before_at' => $reconcile_not_before_at,
					'reconcile_deadline_at'   => $reconcile_deadline_at,
				],
				'uncertain' => true,
			]
		);
		if ( is_wp_error( $updated ) ) {
			return $this->not_dispatched_start_error();
		}
		$verified = $this->request_store()->read( (string) ( $updated['option_key'] ?? '' ) );
		$dispatch = is_array( $verified['dispatch'] ?? null ) ? $verified['dispatch'] : [];
		if (
			! is_array( $verified )
			|| (string) ( $record['token'] ?? '' ) !== (string) ( $verified['token'] ?? '' )
			|| ! hash_equals( (string) ( $record['upstream']['idempotency_key'] ?? '' ), (string) ( $verified['upstream']['idempotency_key'] ?? '' ) )
			|| true !== ( $verified['uncertain'] ?? false )
			|| 'in_flight' !== ( $dispatch['state'] ?? '' )
			|| $reconcile_not_before_at !== (int) ( $dispatch['reconcile_not_before_at'] ?? 0 )
			|| $reconcile_deadline_at !== (int) ( $dispatch['reconcile_deadline_at'] ?? 0 )
		) {
			return $this->not_dispatched_start_error();
		}
		$start['request_record'] = $verified;
		return $start;
	}

	/**
	 * Validate and durably capture a successful upstream start before jobs are
	 * projected into the local run.
	 *
	 * @return array{request_record:array,job_ids:array<int,string>,accepted_output_count:int,credits_debited:float,accepted_outputs:array<int,array{output_id:string,output_index:int,job_id:string,status:string}>}|\WP_Error
	 */
	public function capture_upstream_start_response( string $workflow, array $start, array $response ) {
		$record = is_array( $start['request_record'] ?? null ) ? $start['request_record'] : [];
		if ( empty( $record ) ) {
			return Messages::error( 'invalid_request', 400 );
		}
		$parsed = $this->upstream_response_parser()->parse_start( $workflow, $response );
		if ( is_wp_error( $parsed ) ) {
			$quarantined = $this->quarantine_verified_start_error( $record, $parsed );
			if ( null !== $quarantined ) {
				return $quarantined;
			}
			return $this->upstream_start_capture_uncertain_error( $parsed );
		}
		$upstream = array_merge(
			is_array( $record['upstream'] ?? null ) ? $record['upstream'] : [],
			$parsed['upstream']
		);
		$changes = [
			'upstream'            => $upstream,
			'admission_evidence'  => [
				'job_ids'               => $parsed['job_ids'],
				'accepted_output_count' => (int) $parsed['accepted_output_count'],
				'credits_debited'        => (float) $parsed['credits_debited'],
			],
			'uncertain'           => true,
			'dispatch'            => $this->reconciliation_ready_dispatch_intent( $record ),
		];
		$accepted_outputs = [];
		if ( 'campaign_set' === $workflow ) {
			$accepted_outputs = is_array( $parsed['accepted_outputs'] ?? null ) ? $parsed['accepted_outputs'] : [];
			$changes['admission'] = [
				'accepted_output_count' => (int) $parsed['accepted_output_count'],
				'outputs'               => $accepted_outputs,
			];
		}
		$updated = $this->request_store()->update(
			$record,
			$changes
		);
		if ( is_wp_error( $updated ) ) {
			return $this->upstream_start_capture_uncertain_error( $updated );
		}
		$billing_bound = $this->record_admission_billing(
			$updated,
			(int) $parsed['accepted_output_count'],
			(float) $parsed['credits_debited']
		);
		if ( is_wp_error( $billing_bound ) ) {
			return $this->upstream_start_capture_uncertain_error( $billing_bound );
		}
		if ( ! $this->admission_count_matches_review( $updated ) ) {
			return $this->quarantine_admission_mismatch( $updated );
		}

		return [
			'request_record'         => $updated,
			'job_ids'                => $parsed['job_ids'],
			'accepted_output_count'  => (int) $parsed['accepted_output_count'],
			'credits_debited'         => (float) $parsed['credits_debited'],
			'accepted_outputs'       => $accepted_outputs,
		];
	}

	private function quarantine_verified_start_error( array $record, \WP_Error $cause ): ?\WP_Error {
		$data = $cause->get_error_data();
		$verified = is_array( $data ) && is_array( $data['verified_admission'] ?? null )
			? $data['verified_admission']
			: [];
		$upstream = is_array( $verified['upstream'] ?? null ) ? $verified['upstream'] : [];
		$evidence = is_array( $verified['admission_evidence'] ?? null ) ? $verified['admission_evidence'] : [];
		if (
			[ 'request_id', 'generation_request_id', 'reconcile_state' ] !== array_keys( $upstream )
			|| [ 'job_ids', 'accepted_output_count', 'credits_debited' ] !== array_keys( $evidence )
			|| ! is_array( $evidence['job_ids'] )
			|| $this->response_factory()->normalize_job_ids( $evidence['job_ids'] ) !== $evidence['job_ids']
			|| ( null !== $evidence['accepted_output_count'] && ! is_int( $evidence['accepted_output_count'] ) )
			|| ! is_float( $evidence['credits_debited'] )
		) {
			return null;
		}
		$updated = $this->request_store()->update(
			$record,
			[
				'upstream'           => array_merge( is_array( $record['upstream'] ?? null ) ? $record['upstream'] : [], $upstream ),
				'admission_evidence' => $evidence,
				'uncertain'          => true,
				'dispatch'           => $this->reconciliation_ready_dispatch_intent( $record ),
			]
		);
		if ( is_wp_error( $updated ) ) {
			return $this->upstream_start_capture_uncertain_error( $updated );
		}
		$billing_bound = $this->record_admission_billing(
			$updated,
			$evidence['accepted_output_count'],
			$evidence['credits_debited']
		);
		if ( is_wp_error( $billing_bound ) ) {
			return $this->upstream_start_capture_uncertain_error( $billing_bound );
		}
		if ( null !== $evidence['accepted_output_count'] && ! $this->admission_count_matches_review( $updated ) ) {
			return $this->quarantine_admission_mismatch( $updated );
		}

		return $this->quarantine_admission_integrity(
			$updated,
			'fotogenic_ai_upstream_admission_response_invalid',
			'upstream_admission_response_invalid',
			__( 'The upstream generation admission response could not be safely projected. Contact support with this request ID.', 'fotogenic-ai' )
		);
	}

	/** @return array|\WP_Error */
	private function record_admission_billing( array $record, ?int $accepted_count, ?float $credits_debited ) {
		if ( null === $accepted_count && null === $credits_debited ) {
			return [];
		}
		$run_id = substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 );
		if ( '' === $run_id ) {
			return [];
		}
		$logger = Logger::instance();
		$row = $logger->get_run_row( $run_id );
		if ( empty( $row ) || ! $this->run_matches_request_record( $row, $record ) ) {
			return new \WP_Error(
				'fotogenic_ai_admission_run_unavailable',
				__( 'The upstream admission could not be bound to its local run.', 'fotogenic-ai' ),
				[ 'status' => 502 ]
			);
		}
		return Workflow_Credit_Settlement::record_admission(
			$logger,
			$run_id,
			$row['credit_settlement_state'] ?? null,
			$accepted_count,
			$credits_debited
		);
	}

	private function admission_count_matches_review( array $record ): bool {
		$review = is_array( $record['review'] ?? null ) ? $record['review'] : [];
		$evidence = is_array( $record['admission_evidence'] ?? null ) ? $record['admission_evidence'] : [];
		$expected_count = $review['expected_output_count'] ?? null;
		$accepted_count = $evidence['accepted_output_count'] ?? null;
		return is_int( $expected_count )
			&& is_int( $accepted_count )
			&& $expected_count === $accepted_count;
	}

	/** @return \WP_Error */
	private function quarantine_admission_mismatch( array $record ) {
		return $this->quarantine_admission_integrity(
			$record,
			'fotogenic_ai_upstream_admission_count_mismatch',
			'upstream_admission_count_mismatch',
			__( 'The upstream generation admission did not match the reviewed output count. Contact support with this request ID.', 'fotogenic-ai' )
		);
	}

	/** @return \WP_Error */
	private function quarantine_admission_integrity( array $record, string $public_code, string $run_code, string $message ) {
		$run_id = substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 );
		$evidence = is_array( $record['admission_evidence'] ?? null ) ? $record['admission_evidence'] : [];
		$error = new \WP_Error(
			$public_code,
			$message,
			[
				'status'    => 502,
				'requestId' => (string) ( $record['request_id'] ?? '' ),
				'runId'     => $run_id,
				'jobIds'    => $evidence['job_ids'] ?? [],
			]
		);
		if ( '' !== $run_id ) {
			$logger = Logger::instance();
			$row = $logger->get_run_row( $run_id );
			if (
				empty( $row )
				|| ! $this->run_matches_request_record( $row, $record )
				|| ! $logger->update_run(
					$run_id,
					[
						'final_status'  => 'unresolved',
						'error_code'    => $run_code,
						'error_message' => $error->get_error_message(),
					]
				)
			) {
				return $this->upstream_start_capture_uncertain_error( $error );
			}
			Workflow_Credit_Settlement::schedule_terminal_settlement( $run_id, 'unresolved' );
		}
		$intent = $this->dispatch_intent( $record );
		$intent['state'] = 'unresolved';
		$updated = $this->request_store()->update(
			$record,
			[
				'state'     => 'unresolved',
				'error'     => $this->serialize_error( $error, $record, 'unresolved', false ),
				'uncertain' => true,
				'dispatch'  => $intent,
			]
		);
		return is_wp_error( $updated )
			? $this->upstream_start_capture_uncertain_error( $updated )
			: $this->error_from_record( $updated );
	}

	/**
	 * Re-read an uncertain start using its persisted upstream identity. This
	 * method never dispatches a replacement start request.
	 *
	 * @return array{request_record:array,state:string,job_ids:array<int,string>}|\WP_Error
	 */
	public function reconcile_uncertain_request( array $record ) {
		$reconciled = $this->read_uncertain_reconciliation( $record );
		if ( is_wp_error( $reconciled ) ) {
			return $reconciled;
		}

		return $this->persist_uncertain_reconciliation( $record, $reconciled );
	}

	/**
	 * Perform the authenticated reconciliation GET and strict response parsing
	 * without mutating the durable request projection.
	 *
	 * @return array<string,mixed>|\WP_Error
	 */
	private function read_uncertain_reconciliation( array $record ) {
		$workflow = sanitize_key( (string) ( $record['workflow'] ?? '' ) );
		$upstream = is_array( $record['upstream'] ?? null ) ? $record['upstream'] : [];
		$idempotency_key = (string) ( $upstream['idempotency_key'] ?? '' );
		if ( ! in_array( $workflow, Workflow_Request_Store::WORKFLOWS, true ) || ! Upstream_Request_Identity::is_valid( $idempotency_key ) ) {
			return Messages::error( 'invalid_request', 400 );
		}

		$reconciled = $this->workflow_request_reconciler()->reconcile( $workflow, $idempotency_key );
		if ( is_wp_error( $reconciled ) ) {
			return $reconciled;
		}
		if ( 'not_found' === $reconciled['state'] && ( '' !== (string) ( $upstream['request_id'] ?? '' ) || '' !== (string) ( $upstream['generation_request_id'] ?? '' ) ) ) {
			return new \WP_Error(
				'fotogenic_ai_reconciliation_identity_conflict',
				__( 'The upstream generation status could not be verified.', 'fotogenic-ai' ),
				[ 'status' => 502 ]
			);
		}

		return $reconciled;
	}

	/**
	 * CAS-persist a previously parsed reconciliation result.
	 *
	 * @param array<string,mixed> $reconciled
	 * @return array{request_record:array,state:string,job_ids:array<int,string>}|\WP_Error
	 */
	private function persist_uncertain_reconciliation( array $record, array $reconciled ) {
		$upstream = is_array( $record['upstream'] ?? null ) ? $record['upstream'] : [];
		$updated_upstream = array_merge( $upstream, $reconciled['upstream'] );
		$admission_evidence = $this->reconciled_admission_evidence( $record, $reconciled );
		if ( is_wp_error( $admission_evidence ) ) {
			return $admission_evidence;
		}
		$updated = $this->request_store()->update(
			$record,
			[
				'upstream'           => $updated_upstream,
				'admission_evidence' => $admission_evidence,
			]
		);
		if ( is_wp_error( $updated ) ) {
			return $updated;
		}
		$billing_bound = $this->record_admission_billing(
			$updated,
			$admission_evidence['accepted_output_count'],
			$admission_evidence['credits_debited']
		);
		if ( is_wp_error( $billing_bound ) ) {
			return $billing_bound;
		}
		if ( null !== $admission_evidence['accepted_output_count'] && ! $this->admission_count_matches_review( $updated ) ) {
			return $this->quarantine_admission_mismatch( $updated );
		}

		return [
			'request_record' => $updated,
			'state'          => $reconciled['state'],
			'job_ids'        => $reconciled['job_ids'],
		];
	}

	/** @return array{job_ids:array<int,string>,accepted_output_count:?int,credits_debited:?float}|\WP_Error */
	private function reconciled_admission_evidence( array $record, array $reconciled ) {
		$current = is_array( $record['admission_evidence'] ?? null )
			? $record['admission_evidence']
			: Workflow_Request_Store::initial_admission_evidence();
		$job_ids = $this->response_factory()->normalize_job_ids( $reconciled['job_ids'] ?? [] );
		if ( empty( $job_ids ) ) {
			$job_ids = is_array( $current['job_ids'] ?? null ) ? $current['job_ids'] : [];
		}
		$accepted_count = $current['accepted_output_count'] ?? null;
		$workflow = sanitize_key( (string) ( $record['workflow'] ?? '' ) );
		if ( null === $accepted_count && ! empty( $job_ids ) ) {
			if ( 'campaign_set' === $workflow ) {
				$campaign_admission = Generate_Campaign_Admission::normalize( $record['admission'] ?? null );
				$accepted_count = empty( $campaign_admission ) ? null : $campaign_admission['accepted_output_count'];
			} elseif ( in_array( $workflow, [ 'product_images', 'increase_resolution' ], true ) ) {
				$accepted_count = count( $job_ids );
			}
		}
		$credits_debited = $reconciled['credits_debited'] ?? null;
		if ( null === $credits_debited ) {
			$credits_debited = $current['credits_debited'] ?? null;
		}

		return [
			'job_ids'               => $job_ids,
			'accepted_output_count' => is_int( $accepted_count ) ? $accepted_count : null,
			'credits_debited'        => null === $credits_debited ? null : (float) $credits_debited,
		];
	}

	private function current_projection_request_record( array $record, int $user_id, string $workflow, string $request_id ): array {
		$option_key = (string) ( $record['option_key'] ?? '' );
		$fingerprint = (string) ( $record['payload_fingerprint'] ?? '' );
		if ( '' === $option_key || '' === $fingerprint ) {
			return [];
		}
		$current = $this->request_store()->read( $option_key );
		if ( ! is_array( $current ) || 'starting' !== ( $current['state'] ?? null ) ) {
			return [];
		}
		$matched = $this->request_store()->matches( $current, $user_id, $workflow, $request_id, $fingerprint, $option_key );
		return true === $matched ? $current : [];
	}

	private function settlement_matches_admission_evidence( array $run_row, array $evidence ): bool {
		$accepted_count = $evidence['accepted_output_count'] ?? null;
		$credits_debited = $evidence['credits_debited'] ?? null;
		if ( ! is_int( $accepted_count ) || ! is_float( $credits_debited ) ) {
			return false;
		}
		$settlement = Workflow_Credit_Settlement::normalize_state( $run_row['credit_settlement_state'] ?? null );
		return $accepted_count === ( $settlement['acceptedImagesCount'] ?? null )
			&& isset( $settlement['creditsDebited'] )
			&& abs( $credits_debited - (float) $settlement['creditsDebited'] ) <= 0.000001;
	}

	/**
	 * Validate the exact Product or Media target identity and ownership.
	 *
	 * @return array|\WP_Error
	 */
	public function resolve_target_context( string $target, int $subject_id ) {
        $target = sanitize_key( $target );
        if ( ! in_array( $target, [ 'product', 'media' ], true ) || $subject_id <= 0 ) {
            return new \WP_Error( 'fotogenic_ai_invalid_target', __( 'Select a valid Product or Media target.', 'fotogenic-ai' ), [ 'status' => 400 ] );
        }

        if ( 'product' === $target ) {
            $target_snapshot = Workflow_Item_Identity::resolve_target( 'product', $subject_id );
            if ( is_wp_error( $target_snapshot ) ) {
                return $target_snapshot;
            }
            return [
                'run_target'           => 'product',
                'subject_id'           => $subject_id,
                'product_title'        => (string) $target_snapshot['label'],
                'source_attachment_id' => 0,
                'target_type'          => (string) $target_snapshot['targetType'],
                'parent_product_id'    => (int) $target_snapshot['parentProductId'],
                'variation_id'         => (int) $target_snapshot['variationId'],
                'target_snapshot'      => $target_snapshot,
            ];
        }

        $attachment = $this->validate_editable_image_attachment( $subject_id );
        if ( is_wp_error( $attachment ) ) {
            return $attachment;
        }
        $target_snapshot = Workflow_Item_Identity::resolve_target( 'media', $subject_id );
        if ( is_wp_error( $target_snapshot ) ) {
            return $target_snapshot;
        }
        return [
            'run_target'           => 'media',
            'subject_id'           => $subject_id,
            'product_title'        => (string) $target_snapshot['label'],
            'source_attachment_id' => $subject_id,
            'target_type'          => 'media',
            'parent_product_id'    => 0,
            'variation_id'         => 0,
            'target_snapshot'      => $target_snapshot,
        ];
    }

    /**
     * Bind a server-validated Universal item identity to the workflow context.
     *
     * @return array|\WP_Error
     */
    public function attach_item_identity_context( array $context, array $identity ) {
        if ( empty( $identity ) ) {
            return $context;
        }
        if (
            [ 'run_item_uuid', 'campaign_group_uuid', 'item_ordinal', 'target_snapshot', 'source_roles_snapshot', 'destination_snapshot', 'parallel_item_identity' ] !== array_keys( $identity )
            || ! wp_is_uuid( (string) $identity['run_item_uuid'] )
            || ( '' !== (string) $identity['campaign_group_uuid'] && ! wp_is_uuid( (string) $identity['campaign_group_uuid'] ) )
            || (int) $identity['item_ordinal'] <= 0
            || ! is_array( $identity['source_roles_snapshot'] )
            || ! is_array( $identity['destination_snapshot'] )
            || ! is_bool( $identity['parallel_item_identity'] )
        ) {
            return Messages::error( 'invalid_request', 400 );
        }
        $target = Workflow_Item_Identity::normalize_target_snapshot( $identity['target_snapshot'] );
        if (
            is_wp_error( $target )
            || (string) ( $context['run_target'] ?? '' ) !== (string) $target['sourceType']
            || (int) ( $context['subject_id'] ?? 0 ) !== (int) $target['targetId']
            || ! Workflow_Item_Identity::hint_matches( (array) ( $context['target_snapshot'] ?? [] ), Workflow_Item_Identity::target_hint( $target ) )
        ) {
            return new \WP_Error(
                'fotogenic_ai_target_identity_changed',
                __( 'The selected target changed before the workflow could start.', 'fotogenic-ai' ),
                [ 'status' => 409 ]
            );
        }

        $context['run_item_uuid'] = strtolower( (string) $identity['run_item_uuid'] );
        $context['campaign_group_uuid'] = strtolower( (string) $identity['campaign_group_uuid'] );
        $context['item_ordinal'] = (int) $identity['item_ordinal'];
        $context['target_type'] = (string) $target['targetType'];
        $context['parent_product_id'] = (int) $target['parentProductId'];
        $context['variation_id'] = (int) $target['variationId'];
        $context['target_snapshot'] = $target;
        $context['source_roles_snapshot'] = $identity['source_roles_snapshot'];
        $context['destination_snapshot'] = $identity['destination_snapshot'];
        $context['parallel_item_identity'] = $identity['parallel_item_identity'];
        return $context;
    }

    /**
     * Project identity fields into run persistence without upstream serialization.
     */
    public function item_identity_metadata( array $context ): array {
        $fields = [];
        foreach ( [ 'run_item_uuid', 'campaign_group_uuid', 'item_ordinal', 'target_type', 'parent_product_id', 'variation_id', 'target_snapshot', 'source_roles_snapshot', 'destination_snapshot' ] as $field ) {
            if ( array_key_exists( $field, $context ) ) {
                $fields[ $field ] = $context[ $field ];
            }
        }
        return $fields;
    }

    /** @return bool|\WP_Error */
    public function validate_editable_image_attachment( int $attachment_id ) {
        if ( $attachment_id <= 0 || ! current_user_can( 'upload_files' ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
            return Messages::error( 'forbidden_image', 403 );
        }
        $attachment = get_post( $attachment_id );
        if ( ! ( $attachment instanceof \WP_Post ) || 'attachment' !== $attachment->post_type || ! wp_attachment_is_image( $attachment_id ) ) {
            return new \WP_Error( 'fotogenic_ai_invalid_attachment', __( 'Select a valid image attachment.', 'fotogenic-ai' ), [ 'status' => 400 ] );
        }
        return true;
    }

    /**
     * Persist a starting run and bind it to the durable request before the upstream request.
     *
     * @return array|\WP_Error Array containing claim, request record, and run_id.
     */
    public function begin_start(
        int $user_id,
        string $workflow,
        string $request_id,
        array $context,
        array $metadata,
        array $admission
    ) {
        $record = isset( $admission['record'] ) && is_array( $admission['record'] ) ? $admission['record'] : [];
        if ( empty( $record ) || ! empty( $admission['replayed'] ) ) {
            return Messages::error( 'invalid_request', 400 );
        }

        $job_manager = Job_Manager::instance();
        $claim = $job_manager->claim_generation_start(
            $user_id,
            (string) $context['run_target'],
            (int) $context['subject_id'],
            $workflow,
            $request_id,
            ! empty( $context['parallel_item_identity'] ) ? (string) ( $context['run_item_uuid'] ?? '' ) : ''
        );
        if ( is_wp_error( $claim ) ) {
            $this->persist_request_error( $record, $claim, 'failed', false );
            return $claim;
        }

        $logger = Logger::instance();
        $run_id = $logger->generate_run_id();
        $run_metadata = array_merge(
            $context,
            $metadata,
            [
				'workflow'                => $workflow,
				'request_id'              => $request_id,
				'user_id'                 => $user_id,
				'final_status'            => 'starting',
				'credit_settlement_state' => Workflow_Credit_Settlement::pending_state( $user_id, $metadata ),
            ]
        );
		if ( ! $logger->create_run( $run_id, $run_metadata ) ) {
			$job_manager->release_generation_start_claim( $claim );
			$error = new \WP_Error(
				'fotogenic_ai_workflow_run_persist_failed',
				__( 'The workflow run could not be prepared. Reload Studio and try again.', 'fotogenic-ai' ),
				[ 'status' => 503, 'not_dispatched' => true ]
			);
			$this->persist_request_error( $record, $error, 'failed', false );
			return $error;
		}
		$created_row = $logger->get_run_row( $run_id );
		$settlement_state = Workflow_Credit_Settlement::normalize_state( $created_row['credit_settlement_state'] ?? null );
		if ( empty( $created_row ) || 'pending' !== ( $settlement_state['status'] ?? '' ) ) {
            $job_manager->release_generation_start_claim( $claim );
			if ( ! empty( $created_row ) ) {
				$logger->finalize_run( $run_id, 'failed', [ 'error_code' => 'credit_settlement_unavailable' ] );
			}
			$error = new \WP_Error(
				'fotogenic_ai_credit_settlement_unavailable',
				__( 'Generation did not start because credit settlement could not be prepared. Reload Studio and try again. No credits were used.', 'fotogenic-ai' ),
				[ 'status' => 503, 'uncertain' => false, 'not_dispatched' => true ]
			);
            $this->persist_request_error( $record, $error, 'failed', false );
            return $error;
        }

        $response = $this->response_factory()->build_context_response( $workflow, $request_id, $context, $metadata, 'starting' );
        $response['runId'] = $run_id;
        $bound_record = $this->request_store()->update(
            $record,
            [
                'run_id'  => $run_id,
                'state'   => 'starting',
                'response'=> $response,
            ]
        );
        if ( is_wp_error( $bound_record ) || ! hash_equals( $run_id, (string) ( $bound_record['run_id'] ?? '' ) ) ) {
			Workflow_Credit_Settlement::cancel_before_dispatch( $logger, $run_id, $settlement_state );
            $logger->finalize_run( $run_id, 'failed', [ 'error_code' => 'request_record_bind_failed' ] );
            $job_manager->release_generation_start_claim( $claim );
            return new \WP_Error(
                'fotogenic_ai_workflow_request_persist_failed',
                __( 'The workflow request identity could not be persisted.', 'fotogenic-ai' ),
				[ 'status' => 503, 'uncertain' => false, 'not_dispatched' => true ]
            );
        }

        $updated_claim = $job_manager->update_generation_start_claim(
            $claim,
            [
                'state'  => 'starting',
                'run_id' => $run_id,
            ]
        );
        if ( is_wp_error( $updated_claim ) ) {
			Workflow_Credit_Settlement::cancel_before_dispatch( $logger, $run_id, $settlement_state );
            $logger->finalize_run( $run_id, 'failed', [ 'error_code' => 'claim_lost' ] );
            $job_manager->release_generation_start_claim( $claim );
            $this->persist_request_error( $bound_record, $updated_claim, 'failed', false );
            return $updated_claim;
        }
        if ( $logger->enabled() ) {
            $logger->info(
                'workflow_start_claimed',
                [
                    'run_id'     => $run_id,
                    'request_id' => $request_id,
                    'workflow'   => $workflow,
                    'run_target' => (string) $context['run_target'],
                    'subject_id' => (int) $context['subject_id'],
                    'user_id'    => $user_id,
                ]
            );
        }
        return [
            'claim'          => $updated_claim,
            'request_record' => $bound_record,
            'run_id'         => $run_id,
        ];
    }

    /**
     * Persist jobs and active context before releasing the short start claim.
     *
     * @return array|\WP_Error Browser response payload.
     */
    public function complete_start( int $user_id, string $workflow, string $request_id, array $context, array $start, array $job_ids, array $metadata ) {
        $run_id = trim( (string) ( $start['run_id'] ?? '' ) );
        $normalized_job_ids = $this->response_factory()->normalize_job_ids( $job_ids );
        if ( '' === $run_id || empty( $normalized_job_ids ) ) {
            return $this->complete_start_under_lock( $user_id, $workflow, $request_id, $context, $start, $job_ids, $metadata );
        }

        $lock = Background_Lock::instance();
        $lock_key = $this->get_run_lifecycle_lock_key( $run_id );
        $token = $lock->acquire( $lock_key, self::RUN_LIFECYCLE_LOCK_TTL );
        if ( false === $token ) {
            $record = isset( $start['request_record'] ) && is_array( $start['request_record'] ) ? $start['request_record'] : [];
            $error = $this->uncertain_start_error( $request_id, $run_id, $normalized_job_ids );
            $this->persist_request_error( $record, $error, 'starting', true );
            return $error;
        }
        try {
            return $this->complete_start_under_lock( $user_id, $workflow, $request_id, $context, $start, $normalized_job_ids, $metadata );
        } finally {
            $lock->release( $lock_key, $token );
        }
    }

    /** @return array|\WP_Error */
    private function complete_start_under_lock( int $user_id, string $workflow, string $request_id, array $context, array $start, array $job_ids, array $metadata, bool $recovery = false ) {
        $job_ids = $this->response_factory()->normalize_job_ids( $job_ids );
        $run_id = (string) ( $start['run_id'] ?? '' );
        $claim = isset( $start['claim'] ) && is_array( $start['claim'] ) ? $start['claim'] : [];
        $record = isset( $start['request_record'] ) && is_array( $start['request_record'] ) ? $start['request_record'] : [];
        if ( '' === $run_id || empty( $job_ids ) ) {
            return $this->handle_upstream_error(
                new \WP_Error(
                    'fotogenic_ai_missing_job',
                    __( 'The workflow start was accepted but no job identifiers were returned.', 'fotogenic-ai' ),
                    [ 'status' => 503, 'uncertain' => true ]
                ),
                $workflow,
                $request_id,
                $context,
                $start
            );
        }
		$current_record = $this->current_projection_request_record( $record, $user_id, $workflow, $request_id );
		if ( empty( $current_record ) ) {
			$error = $this->uncertain_start_error( $request_id, $run_id, $job_ids );
			$this->persist_request_error( $record, $error, 'starting', true );
			return $error;
		}
		$record = $current_record;
		if ( ! $this->admission_count_matches_review( $record ) ) {
			return $this->quarantine_admission_mismatch( $record );
		}
		$evidence = is_array( $record['admission_evidence'] ?? null ) ? $record['admission_evidence'] : [];
		if ( $job_ids !== ( $evidence['job_ids'] ?? null ) || ! $this->has_recoverable_campaign_admission( $record ) ) {
			return $this->quarantine_admission_integrity(
				$record,
				'fotogenic_ai_upstream_admission_evidence_conflict',
				'upstream_admission_evidence_conflict',
				__( 'The upstream generation admission could not be matched to the reviewed request. Contact support with this request ID.', 'fotogenic-ai' )
			);
		}
		$metadata['expected_output_count'] = (int) $record['review']['expected_output_count'];
		$metadata['estimated_cost'] = (float) $record['review']['estimated_cost'];

        $logger = Logger::instance();
        $run_row = $logger->get_run_row( $run_id );
        $run_status = $this->normalize_run_status( (string) ( $run_row['final_status'] ?? '' ) );
        if (
            ! $this->run_matches_start( $run_row, $run_id, $user_id, $workflow, $request_id, $context )
            || ! in_array( $run_status, Workflow_Request_Store::ACTIVE_STATUSES, true )
        ) {
            $error = $this->uncertain_start_error( $request_id, $run_id, $job_ids );
            $this->persist_request_error( $record, $error, 'starting', true );
            return $error;
        }
		if ( ! $this->settlement_matches_admission_evidence( $run_row, $evidence ) ) {
			return $this->quarantine_admission_integrity(
				$record,
				'fotogenic_ai_upstream_admission_billing_conflict',
				'upstream_admission_billing_conflict',
				__( 'The upstream generation billing could not be matched to the admitted request. Contact support with this request ID.', 'fotogenic-ai' )
			);
		}

        $job_manager = Job_Manager::instance();
        $job_key = $this->resolve_active_job_key( $job_manager, $workflow, $request_id, $user_id, $context, $metadata );
        if ( '' === $job_key ) {
            $error = $this->uncertain_start_error( $request_id, $run_id, $job_ids );
            $this->persist_request_error( $record, $error, 'starting', true );
            return $error;
        }

		$projection_authorized = $recovery;
        $release_claim = $claim;
		if ( ! $recovery && $this->claim_matches_start( $claim, $user_id, $workflow, $request_id, $context ) ) {
            $updated_claim = $job_manager->update_generation_start_claim(
                $claim,
                [
                    'state'   => 'processing',
                    'run_id'  => $run_id,
                    'job_ids' => $job_ids,
                ]
            );
            if ( ! is_wp_error( $updated_claim ) ) {
				$projection_authorized = true;
                $release_claim = $updated_claim;
            }
        }

        $active = array_merge(
            $context,
            $metadata,
            [
                'run_id'            => $run_id,
                'job_id'            => (string) $job_ids[0],
                'job_ids'           => $job_ids,
                'workflow'          => $workflow,
                'request_id'        => $request_id,
                'user_id'           => $user_id,
                'scene_description' => (string) ( $metadata['scene_description'] ?? '' ),
                'completed_job_ids' => [],
                'attachment_ids'    => [],
                'attachments_map'   => [],
                'failed_job_ids'    => [],
                'final_status'      => 'processing',
            ]
        );
		$active_record = $job_manager->set_active_job( $user_id, (string) $job_ids[0], $active, $job_key );
		if ( ! is_array( $active_record ) ) {
			$error = $this->uncertain_start_error( $request_id, $run_id, $job_ids );
			if ( ! $recovery ) {
				$job_manager->update_generation_start_claim(
					$release_claim,
					[ 'state' => 'uncertain', 'run_id' => $run_id, 'job_ids' => $job_ids, 'uncertain' => true ]
				);
			}
			$this->persist_request_error( $record, $error, 'starting', true );
			return $error;
		}

		$run_update = [
                'job_id'         => (string) $job_ids[0],
                'job_ids'        => wp_json_encode( $job_ids ),
                'attachments_map'=> wp_json_encode( [] ),
                'final_status'   => 'processing',
				'error_code'     => $projection_authorized ? 'start_projection_pending' : 'claim_lost_after_start',
				'error_message'  => $projection_authorized ? __( 'The upstream jobs are awaiting durable local projection.', 'fotogenic-ai' ) : __( 'The workflow started, but its short start claim could not be confirmed.', 'fotogenic-ai' ),
		];
		$campaign_admission_meta = $this->campaign_admission_finalize_meta( $record, $run_row );
		if ( null !== $campaign_admission_meta ) {
			$run_update['finalize_meta'] = $campaign_admission_meta;
		}
		$logger_updated = $logger->update_run( $run_id, $run_update );
		$projected_run = $logger->get_run_row( $run_id );
		$projected_ids = $this->response_factory()->normalize_job_ids( $projected_run['job_ids'] ?? [] );
		if (
			! $logger_updated
			|| ! $this->run_matches_start( $projected_run, $run_id, $user_id, $workflow, $request_id, $context )
			|| $projected_ids !== $job_ids
			|| 'processing' !== $this->normalize_run_status( (string) ( $projected_run['final_status'] ?? '' ) )
		) {
			$error = $this->uncertain_start_error( $request_id, $run_id, $job_ids );
			if ( ! $recovery ) {
				$job_manager->update_generation_start_claim(
					$release_claim,
					[ 'state' => 'uncertain', 'run_id' => $run_id, 'job_ids' => $job_ids, 'uncertain' => true ]
				);
			}
			$this->persist_request_error( $record, $error, 'starting', true );
			return $error;
		}

        $result = $this->response_factory()->build_context_response( $workflow, $request_id, $context, $metadata, 'processing' );
        $result['runId'] = $run_id;
        $result['jobIds'] = $job_ids;

        $stored_record = $this->request_store()->update(
            $record,
            [
                'run_id'   => $run_id,
                'state'    => 'processing',
                'response' => $result,
                'error'    => [],
				'uncertain'=> ! $projection_authorized,
				'dispatch' => $projection_authorized ? $this->resolved_dispatch_intent( $record ) : $this->dispatch_intent( $record ),
            ]
        );

		if ( is_wp_error( $stored_record ) ) {
			$logger->update_run(
				$run_id,
				[
					'error_code'    => 'start_response_uncertain',
					'error_message' => __( 'The upstream jobs could not be durably projected.', 'fotogenic-ai' ),
				]
			);
			if ( ! $recovery ) {
				$job_manager->update_generation_start_claim(
					$release_claim,
					[ 'state' => 'uncertain', 'run_id' => $run_id, 'job_ids' => $job_ids, 'uncertain' => true ]
				);
			}
			Background_Scheduler::instance()->schedule_finalize( $run_id, $user_id, 10 );
			return $this->uncertain_start_error( $request_id, $run_id, $job_ids );
		}

		$marker_cleared = $logger->update_run(
			$run_id,
			[
				'error_code'    => $projection_authorized ? '' : 'claim_lost_after_start',
				'error_message' => $projection_authorized ? '' : __( 'The workflow started, but its short start claim could not be confirmed.', 'fotogenic-ai' ),
			]
		);
		if ( ! $marker_cleared ) {
			$this->request_store()->update(
				$stored_record,
				[
					'state'     => 'processing',
					'error'     => $this->serialize_error( $this->uncertain_start_error( $request_id, $run_id, $job_ids ), $stored_record, 'processing', true ),
					'uncertain' => true,
				]
			);
			Background_Scheduler::instance()->schedule_finalize( $run_id, $user_id, 10 );
			return $this->uncertain_start_error( $request_id, $run_id, $job_ids );
		}
		Background_Scheduler::instance()->schedule_finalize( $run_id, $user_id, 10 );
		if ( ! $recovery ) {
			$job_manager->release_generation_start_claim( $release_claim );
		}

        if ( $logger->enabled() ) {
            $logger->info(
                'workflow_job_started',
                [
                    'run_id'         => $run_id,
                    'request_id'     => $request_id,
                    'workflow'       => $workflow,
                    'run_target'     => (string) $context['run_target'],
                    'subject_id'     => (int) $context['subject_id'],
                    'job_ids'        => $job_ids,
                    'jobs_count'     => count( $job_ids ),
                    'expected_count' => (int) ( $metadata['expected_output_count'] ?? count( $job_ids ) ),
                    'user_id'        => $user_id,
                ]
            );
        }

		if ( ! $projection_authorized ) {
            $error = $this->uncertain_start_error( $request_id, $run_id, $job_ids );
            if ( ! is_wp_error( $stored_record ) ) {
                $this->persist_request_error( $stored_record, $error, 'processing', true );
            }
            return $error;
        }

        if ( function_exists( 'do_action' ) ) {
            /**
             * Fires after a workflow start and its job identifiers are durable.
             *
             * The payload intentionally excludes prompts, file paths, URLs,
             * credentials, nonces, prices, and multipart data.
             *
             * @param array $event Safe immutable workflow-start metadata.
             */
            do_action(
                'fotogenic_ai_workflow_started',
                [
                    'workflow'           => $workflow,
                    'runId'              => $run_id,
                    'requestId'          => $request_id,
                    'jobIds'             => $job_ids,
                    'sourceType'         => sanitize_key( (string) ( $context['run_target'] ?? '' ) ),
                    'subjectId'          => absint( $context['subject_id'] ?? 0 ),
                    'expectedOutputCount' => absint( $metadata['expected_output_count'] ?? count( $job_ids ) ),
                ]
            );
        }
        return $result;
    }

    public function handle_upstream_error( \WP_Error $error, string $workflow, string $request_id, array $context, array $start ) {
        $run_id = (string) ( $start['run_id'] ?? '' );
        $claim = isset( $start['claim'] ) && is_array( $start['claim'] ) ? $start['claim'] : [];
        $record = isset( $start['request_record'] ) && is_array( $start['request_record'] ) ? $start['request_record'] : [];
        $data = $error->get_error_data();
        $data = is_array( $data ) ? $data : [];
        $uncertain = ! empty( $data['uncertain'] );
		$not_dispatched = true === ( $data['not_dispatched'] ?? false );
        $logger = Logger::instance();
        $job_manager = Job_Manager::instance();
		$current_record = $this->request_store()->read( (string) ( $record['option_key'] ?? '' ) );
		if (
			is_array( $current_record )
			&& in_array(
				$this->request_store()->normalize_state( (string) ( $current_record['state'] ?? '' ) ),
				Workflow_Request_Store::QUARANTINE_STATUSES,
				true
			)
		) {
			return $this->error_from_record( $current_record );
		}
		$run_quarantine = $this->admission_quarantine_from_run( $run_id, $current_record );
		if ( null !== $run_quarantine ) {
			Workflow_Credit_Settlement::schedule_terminal_settlement( $run_id, 'unresolved' );
			if ( is_array( $current_record ) ) {
				$intent = $this->dispatch_intent( $current_record );
				$intent['state'] = 'unresolved';
				$repaired = $this->request_store()->update(
					$current_record,
					[
						'state'     => 'unresolved',
						'error'     => $this->serialize_error( $run_quarantine, $current_record, 'unresolved', false ),
						'uncertain' => true,
						'dispatch'  => $intent,
					]
				);
				if ( is_array( $repaired ) ) {
					return $this->error_from_record( $repaired );
				}
			}
			return $run_quarantine;
		}

        if ( $uncertain ) {
            if ( '' !== $run_id ) {
                $logger->update_run(
                    $run_id,
                    [
                        'error_code'          => 'start_response_uncertain',
                        'error_message'       => __( 'The upstream start response could not be confirmed.', 'fotogenic-ai' ),
                        'last_api_request_at' => gmdate( 'Y-m-d H:i:s' ),
                    ]
                );
            }
            $job_manager->update_generation_start_claim(
                $claim,
                [
                    'state'     => 'uncertain',
                    'run_id'    => $run_id,
                    'uncertain' => true,
                ]
            );
            if ( $logger->enabled() ) {
                $logger->warn(
                    'workflow_start_uncertain',
                    [
                        'run_id'     => $run_id,
                        'request_id' => $request_id,
                        'workflow'   => $workflow,
                        'run_target' => (string) $context['run_target'],
                        'subject_id' => (int) $context['subject_id'],
                    ]
                );
            }
            $result = $this->uncertain_start_error( $request_id, $run_id, [] );
            $this->persist_request_error( $record, $result, 'starting', true );
            return $result;
        }

        if ( '' !== $run_id ) {
			if ( $not_dispatched ) {
				$row = $logger->get_run_row( $run_id );
				Workflow_Credit_Settlement::cancel_before_dispatch(
					$logger,
					$run_id,
					$row['credit_settlement_state'] ?? null
				);
			}
            $logger->finalize_run(
                $run_id,
                'failed',
                [
                    'error_code'    => sanitize_key( (string) $error->get_error_code() ),
                    'error_message' => $error->get_error_message(),
                ]
            );
        }
        $job_manager->release_generation_start_claim( $claim );
        $status = (int) ( $data['status'] ?? 500 );
        $result = 402 === $status ? Messages::error( 'insufficient_credits', 402 ) : $error;
		$this->persist_request_error( $record, $result, 'failed', false );
		return $result;
	}

	private function admission_quarantine_from_run( string $run_id, $record ): ?\WP_Error {
		if ( '' === $run_id || ! is_array( $record ) ) {
			return null;
		}
		$row = Logger::instance()->get_run_row( $run_id );
		if ( ! $this->run_matches_request_record( $row, $record ) || 'unresolved' !== $this->normalize_run_status( (string) ( $row['final_status'] ?? '' ) ) ) {
			return null;
		}
		$run_code = sanitize_key( (string) ( $row['error_code'] ?? '' ) );
		$definitions = [
			'upstream_admission_count_mismatch' => [
				'fotogenic_ai_upstream_admission_count_mismatch',
				__( 'The upstream generation admission did not match the reviewed output count. Contact support with this request ID.', 'fotogenic-ai' ),
			],
			'upstream_admission_evidence_conflict' => [
				'fotogenic_ai_upstream_admission_evidence_conflict',
				__( 'The upstream generation admission could not be matched to the reviewed request. Contact support with this request ID.', 'fotogenic-ai' ),
			],
			'upstream_admission_billing_conflict' => [
				'fotogenic_ai_upstream_admission_billing_conflict',
				__( 'The upstream generation billing could not be matched to the admitted request. Contact support with this request ID.', 'fotogenic-ai' ),
			],
			'upstream_admission_response_invalid' => [
				'fotogenic_ai_upstream_admission_response_invalid',
				__( 'The upstream generation admission response could not be safely projected. Contact support with this request ID.', 'fotogenic-ai' ),
			],
		];
		if ( ! isset( $definitions[ $run_code ] ) ) {
			return null;
		}
		$evidence = is_array( $record['admission_evidence'] ?? null ) ? $record['admission_evidence'] : [];

		return new \WP_Error(
			$definitions[ $run_code ][0],
			$definitions[ $run_code ][1],
			[
				'status'    => 502,
				'requestId' => (string) ( $record['request_id'] ?? '' ),
				'runId'     => $run_id,
				'jobIds'    => $evidence['job_ids'] ?? [],
			]
		);
	}

	/** @return array|\WP_Error */
    private function replay_request_record( array $record ) {
        $record_state = $this->request_store()->normalize_state( (string) ( $record['state'] ?? 'starting' ) );
		if ( in_array( $record_state, Workflow_Request_Store::QUARANTINE_STATUSES, true ) ) {
			return $this->error_from_record( $record );
		}
        if ( in_array( $record_state, Workflow_Request_Store::FAILURE_STATUSES, true ) ) {
            return $this->error_from_record( $record );
        }
        if ( in_array( $record_state, Workflow_Request_Store::SUCCESS_STATUSES, true ) ) {
            return $this->response_factory()->sanitize_stored_response( $record, $record_state );
        }

        $run_id = substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 );
        if ( '' !== $run_id ) {
            $row = Logger::instance()->get_run_row( $run_id );
            if ( ! empty( $row ) ) {
                if ( ! $this->run_matches_request_record( $row, $record ) ) {
                    return $this->request_store()->conflict_error( (string) $record['request_id'] );
                }
                $status = $this->normalize_run_status( (string) ( $row['final_status'] ?? '' ) );
                if ( in_array( $status, Workflow_Request_Store::ACTIVE_STATUSES, true ) && $this->run_is_stale( $row, (string) $record['workflow'] ) ) {
                    if ( self::terminalize_stale_run( $run_id ) ) {
                        $row = Logger::instance()->get_run_row( $run_id );
                        $status = $this->normalize_run_status( (string) ( $row['final_status'] ?? $status ) );
                    }
                }

                $response = $this->response_factory()->build_run_response( $row, $record, $status );
                if ( in_array( $status, Workflow_Request_Store::FAILURE_STATUSES, true ) ) {
                    $error = $this->error_from_run( $row, $status );
                    $updated = $this->request_store()->update(
                        $record,
						[
							'state'    => $status,
							'response' => $response,
							'error'    => $this->serialize_error( $error, $record, $status, false ),
							'uncertain'=> false,
							'dispatch' => $this->terminal_dispatch_intent( $record ),
						]
                    );
                    if ( ! is_wp_error( $updated ) && $status !== $this->request_store()->normalize_state( (string) ( $updated['state'] ?? '' ) ) ) {
                        return $this->replay_request_record( $updated );
                    }
        return $error;
    }

                $preserve_uncertain = in_array( $status, Workflow_Request_Store::ACTIVE_STATUSES, true )
                    && ( $this->record_has_dispatched_uncertainty( $record ) || $this->run_has_dispatched_uncertainty( $row ) )
                    && empty( $response['jobIds'] );
				$changes = [
					'state'    => $status,
					'response' => $response,
				];
				if ( in_array( $status, Workflow_Request_Store::SUCCESS_STATUSES, true ) ) {
					$changes['dispatch'] = $this->terminal_dispatch_intent( $record );
				}
                if ( $preserve_uncertain && ! $this->record_has_dispatched_uncertainty( $record ) ) {
                    $uncertain_error = $this->uncertain_start_error(
                        (string) ( $record['request_id'] ?? '' ),
                        $run_id,
                        []
                    );
                    $changes['error'] = $this->serialize_error( $uncertain_error, $record, $status, true );
                    $changes['uncertain'] = true;
                } elseif ( ! $preserve_uncertain ) {
                    $changes['error'] = [];
                    $changes['uncertain'] = false;
                }
                $updated = $this->request_store()->update( $record, $changes );
                if ( ! is_wp_error( $updated ) ) {
                    $record = $updated;
                    if ( $status !== $this->request_store()->normalize_state( (string) ( $record['state'] ?? '' ) ) ) {
                        return $this->replay_request_record( $record );
                    }
                }
                if (
                    $preserve_uncertain
                ) {
                    return $this->error_from_record( $record );
                }
                return $response;
            }
        }

        $state = $this->request_store()->normalize_state( (string) ( $record['state'] ?? 'starting' ) );
        if ( in_array( $state, Workflow_Request_Store::ACTIVE_STATUSES, true ) && $this->record_is_stale( $record ) ) {
            $error = $this->stale_request_error( $record, $run_id );
            $updated = $this->terminalize_stale_request_record( $record );
            if ( ! is_wp_error( $updated ) && 'stale' !== $this->request_store()->normalize_state( (string) ( $updated['state'] ?? '' ) ) ) {
                return $this->replay_request_record( $updated );
            }
            return $error;
        }
        if ( in_array( $state, Workflow_Request_Store::FAILURE_STATUSES, true ) ) {
            return $this->error_from_record( $record );
        }
        if ( ! empty( $record['uncertain'] ) ) {
            return $this->error_from_record( $record );
        }

        return $this->response_factory()->sanitize_stored_response( $record, $state );
    }

    private function terminalize_stale_run_locked( string $run_id ): bool {
        $run_id = substr( sanitize_text_field( trim( $run_id ) ), 0, 64 );
        if ( '' === $run_id ) {
            return false;
        }

        $logger = Logger::instance();
        $row = $logger->get_run_row( $run_id );
        $workflow = sanitize_key( (string) ( $row['workflow'] ?? '' ) );
        $status = $this->normalize_run_status( (string) ( $row['final_status'] ?? '' ) );
        if (
            empty( $row )
            || ! in_array( $workflow, Workflow_Request_Store::WORKFLOWS, true )
        ) {
            return false;
        }
        if ( 'stale' === $status ) {
            $this->sync_finalized_request_record( $run_id, 'stale' );
            return true;
        }
        if ( ! in_array( $status, Workflow_Request_Store::ACTIVE_STATUSES, true ) || ! $this->run_is_stale( $row, $workflow ) ) {
            return false;
        }

        $logger->finalize_run(
            $run_id,
            'stale',
            [
                'error_code'    => 'stale_start_claim',
                'error_message' => __( 'The workflow request expired before completion.', 'fotogenic-ai' ),
            ]
        );
        $finalized = $logger->get_run_row( $run_id );
        if ( 'stale' !== $this->normalize_run_status( (string) ( $finalized['final_status'] ?? '' ) ) ) {
            return false;
        }

        Job_Manager::instance()->clear_active_jobs_by_run( (int) ( $finalized['user_id'] ?? 0 ), $run_id );
        $this->sync_finalized_request_record( $run_id, 'stale' );
        return true;
    }

    private function get_run_lifecycle_lock_key( string $run_id ): string {
        return 'run-finalization:' . $run_id;
    }

    private function recover_stale_request_record_batch( int $cursor, int $limit ): int {
        $page = $this->request_store()->get_recovery_page( $cursor, $limit );
        $rows = $page['rows'];
        if ( empty( $rows ) ) {
            return 0;
        }

        foreach ( $rows as $row ) {
            $option_name = (string) ( $row['option_name'] ?? '' );
            $record = maybe_unserialize( $row['option_value'] ?? null );
            if ( ! is_array( $record ) || ! $this->request_record_can_be_recovered( $option_name, $record ) ) {
                continue;
            }

			if ( $this->request_record_has_recoverable_uncertainty( $record ) ) {
				$this->recover_uncertain_request_record( $record );
				continue;
			}

            $run_id = substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 );
            if ( '' !== $run_id ) {
                $run_row = Logger::instance()->get_run_row( $run_id );
                if ( ! empty( $run_row ) ) {
                    if ( ! $this->run_matches_request_record( $run_row, $record ) ) {
                        continue;
                    }
                    $run_status = $this->normalize_run_status( (string) ( $run_row['final_status'] ?? '' ) );
                    if ( in_array( $run_status, array_merge( Workflow_Request_Store::SUCCESS_STATUSES, Workflow_Request_Store::FAILURE_STATUSES ), true ) ) {
                        $this->sync_finalized_request_record( $run_id, $run_status );
                        continue;
                    }
                    if ( self::terminalize_stale_run( $run_id ) ) {
                        continue;
                    }

                    // Keep the durable request active when its bound run still exists but could not be terminalized.
                    continue;
                }
            }

            $this->terminalize_stale_request_record( $record );
        }

        return $page['next_cursor'];
    }

	/**
	 * Reconcile only a record for which a billable start may already have been
	 * admitted. This path never dispatches a replacement start request.
	 */
	private function recover_uncertain_request_record( array $record ): void {
		$option_key = (string) ( $record['option_key'] ?? '' );
		if ( '' === $option_key ) {
			return;
		}

		$lock = Background_Lock::instance();
		$lock_key = 'workflow-request-reconcile:' . hash( 'sha256', $option_key );
		$token = $lock->acquire( $lock_key, Workflow_Operational_Limits::reconciliation_transport_envelope_seconds() );
		if ( false === $token ) {
			return;
		}

		try {
			$current = $this->request_store()->read( $option_key );
			if ( ! is_array( $current ) || ! $this->request_record_has_recoverable_uncertainty( $current ) ) {
				return;
			}

			$run_id = substr( sanitize_text_field( (string) ( $current['run_id'] ?? '' ) ), 0, 64 );
			$run_row = '' !== $run_id ? Logger::instance()->get_run_row( $run_id ) : [];
			$run_status = $this->normalize_run_status( (string) ( $run_row['final_status'] ?? '' ) );
			if ( $this->run_matches_request_record( $run_row, $current ) && $this->run_status_is_closed( $run_status ) ) {
				$this->synchronize_closed_reconciliation_run( $current );
				return;
			}
			$run_job_ids = $this->run_matches_request_record( $run_row, $current )
				? $this->response_factory()->normalize_job_ids( $run_row['job_ids'] ?? [] )
				: [];
			if ( ! empty( $run_job_ids ) ) {
				$this->hydrate_reconciled_start( $current, $run_job_ids );
				return;
			}

			$intent = $this->dispatch_intent( $current );
			if ( 'in_flight' === $intent['state'] ) {
				if ( time() < $intent['reconcile_not_before_at'] ) {
					return;
				}
				$intent['state'] = 'reconcile_ready';
				$current = $this->request_store()->update( $current, [ 'dispatch' => $intent ] );
				if ( is_wp_error( $current ) ) {
					return;
				}
				$intent = $this->dispatch_intent( $current );
			}
			if ( 'reconcile_ready' !== $intent['state'] || time() < $intent['reconcile_not_before_at'] ) {
				return;
			}

			$reconciled = $this->read_uncertain_reconciliation( $current );
			if ( ! $lock->renew( $lock_key, $token, Workflow_Operational_Limits::RECONCILIATION_LOCK_SAFETY_SECONDS ) ) {
				return;
			}
			$current = $this->current_reconciliation_record( $current );
			if ( empty( $current ) ) {
				return;
			}
			if ( is_wp_error( $reconciled ) ) {
				$this->retain_or_terminalize_reconciliation_wait( $current, 'error' );
				return;
			}
			$reconciled = $this->persist_uncertain_reconciliation( $current, $reconciled );
			if ( is_wp_error( $reconciled ) ) {
				return;
			}

			$state = (string) ( $reconciled['state'] ?? '' );
			$updated_record = is_array( $reconciled['request_record'] ?? null ) ? $reconciled['request_record'] : [];
			if ( empty( $updated_record ) ) {
				return;
			}
			if ( in_array( $state, [ 'accepted', 'unknown', 'not_found' ], true ) ) {
				$this->retain_or_terminalize_reconciliation_wait( $updated_record, $state );
				return;
			}
			if ( in_array( $state, [ 'processing', 'completed' ], true ) ) {
				if ( ! $this->has_recoverable_campaign_admission( $updated_record ) ) {
					$this->retain_or_terminalize_reconciliation_wait( $updated_record, $state );
					return;
				}
				$this->hydrate_reconciled_start( $updated_record, (array) ( $reconciled['job_ids'] ?? [] ) );
				return;
			}
			if ( 'failed' === $state ) {
				$this->terminalize_reconciled_request( $updated_record, 'upstream_reconciled_failed', false );
				return;
			}
		} finally {
			$lock->release( $lock_key, $token );
		}
	}

	/**
	 * Preserve non-terminal reconciliation states through the full recovery
	 * window. No replacement start or local refund inference is permitted.
	 */
	private function retain_or_terminalize_reconciliation_wait( array $record, string $reconcile_state ): void {
		$intent = $this->dispatch_intent( $record );
		if ( 'reconcile_ready' !== $intent['state'] ) {
			return;
		}
		if ( $intent['reconcile_deadline_at'] <= 0 ) {
			$intent['reconcile_deadline_at'] = max( time(), $intent['reconcile_not_before_at'] ) + Workflow_Operational_Limits::RECONCILIATION_WINDOW_SECONDS;
		}
		$intent['reconcile_attempts']++;
		if ( time() < $intent['reconcile_deadline_at'] ) {
			$this->request_store()->update(
				$record,
				[
					'dispatch'  => $intent,
					'uncertain' => true,
				]
			);
			return;
		}

		if ( 'not_found' === $reconcile_state ) {
			$this->terminalize_reconciled_request( $record, 'upstream_reconcile_not_found', true );
			return;
		}

		$this->quarantine_reconciliation_unresolved( $record, $intent );
	}

	private function quarantine_reconciliation_unresolved( array $record, array $intent ): void {
		$run_id = substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 );
		if ( '' === $run_id ) {
			$current = $this->current_reconciliation_record( $record );
			if ( ! empty( $current ) ) {
				$this->quarantine_reconciliation_unresolved_locked( $current, $intent );
			}
			return;
		}

		$lock = Background_Lock::instance();
		$lock_key = $this->get_run_lifecycle_lock_key( $run_id );
		$token = $lock->acquire( $lock_key, self::RUN_LIFECYCLE_LOCK_TTL );
		if ( false === $token ) {
			return;
		}
		try {
			$current = $this->current_uncertain_request_record( $record );
			$row = Logger::instance()->get_run_row( $run_id );
			if (
				empty( $current )
				|| ! $this->run_matches_request_record( $row, $current )
			) {
				return;
			}
			$run_status = $this->normalize_run_status( (string) ( $row['final_status'] ?? '' ) );
			if ( $this->run_status_is_closed( $run_status ) ) {
				$this->sync_closed_run_request_record( $row, $current, $run_status );
				return;
			}
			if (
				! in_array( $run_status, Workflow_Request_Store::ACTIVE_STATUSES, true )
				|| 'reconcile_ready' !== $this->dispatch_intent( $current )['state']
				|| ! empty( $this->response_factory()->normalize_job_ids( $row['job_ids'] ?? [] ) )
			) {
				return;
			}
			$this->quarantine_reconciliation_unresolved_locked( $current, $intent );
		} finally {
			$lock->release( $lock_key, $token );
		}
	}

	private function quarantine_reconciliation_unresolved_locked( array $record, array $intent ): void {
		$run_id = substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 );
		$logger = Logger::instance();
		if (
			'' !== $run_id
			&& ! $logger->update_run(
				$run_id,
				[
					'final_status'  => 'unresolved',
					'error_code'    => 'upstream_reconciliation_unresolved',
					'error_message' => __( 'The upstream generation could not be confirmed within the recovery window.', 'fotogenic-ai' ),
				]
			)
		) {
			return;
		}
		$current_intent = $this->dispatch_intent( $record );
		$intent['state'] = 'unresolved';
		$intent['reconcile_attempts'] = max( $intent['reconcile_attempts'], $current_intent['reconcile_attempts'] );
		$intent['reconcile_not_before_at'] = max( $intent['reconcile_not_before_at'], $current_intent['reconcile_not_before_at'] );
		$intent['reconcile_deadline_at'] = max( $intent['reconcile_deadline_at'], $current_intent['reconcile_deadline_at'] );
		$error = new \WP_Error(
			'fotogenic_ai_workflow_reconciliation_unresolved',
			__( 'The upstream generation could not be confirmed within the recovery window. Contact support with this request ID.', 'fotogenic-ai' ),
			[
				'status'    => 502,
				'requestId' => (string) ( $record['request_id'] ?? '' ),
				'runId'     => $run_id,
			]
		);
		$this->request_store()->update(
			$record,
			[
				'state'     => 'unresolved',
				'error'     => $this->serialize_error( $error, $record, 'unresolved', false ),
				'uncertain' => true,
				'dispatch'  => $intent,
			]
		);
	}

	/**
	 * Rebuild the local processing projection from confirmed upstream job IDs.
	 * A run that already carries the same IDs is only re-scheduled, never
	 * re-hydrated.
	 */
	private function hydrate_reconciled_start( array $record, array $job_ids ): void {
		$job_ids = $this->response_factory()->normalize_job_ids( $job_ids );
		$run_id = substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 );
		if ( '' === $run_id || empty( $job_ids ) ) {
			return;
		}

		$lock = Background_Lock::instance();
		$lock_key = $this->get_run_lifecycle_lock_key( $run_id );
		$token = $lock->acquire( $lock_key, self::RUN_LIFECYCLE_LOCK_TTL );
		if ( false === $token ) {
			return;
		}

		try {
			$current = $this->current_recoverable_request_record( $record );
			$row = Logger::instance()->get_run_row( $run_id );
			if ( empty( $current ) || ! $this->run_matches_request_record( $row, $current ) ) {
				return;
			}

			$run_status = $this->normalize_run_status( (string) ( $row['final_status'] ?? '' ) );
			if ( $this->run_status_is_closed( $run_status ) ) {
				$this->sync_closed_run_request_record( $row, $current, $run_status );
				return;
			}
			if ( ! in_array( $run_status, Workflow_Request_Store::ACTIVE_STATUSES, true ) || ! $this->has_recoverable_campaign_admission( $current ) ) {
				return;
			}

			$current_ids = $this->response_factory()->normalize_job_ids( $row['job_ids'] ?? [] );
			if ( ! empty( $current_ids ) ) {
				if ( $current_ids === $job_ids ) {
					$this->repair_reconciled_request_projection_locked( $current, $row, $job_ids );
				}
				return;
			}

			$stored = is_array( $current['response'] ?? null ) ? $current['response'] : [];
			$context = [
				'run_target' => sanitize_key( (string) ( $row['run_target'] ?? $stored['sourceType'] ?? '' ) ),
				'subject_id' => (int) ( $row['subject_id'] ?? $stored['subjectId'] ?? 0 ),
			];
			$metadata = [
				'source_attachment_id'  => (int) ( $row['source_attachment_id'] ?? $stored['sourceAttachmentId'] ?? 0 ),
				'expected_output_count' => max( 0, (int) ( $row['expected_output_count'] ?? $stored['expectedOutputCount'] ?? count( $job_ids ) ) ),
				'estimated_cost'        => max( 0, (float) ( $row['estimated_cost'] ?? $stored['estimatedCost'] ?? 0 ) ),
				'images_count'          => max( 0, (int) ( $row['images_count'] ?? count( $job_ids ) ) ),
				'quality'               => (string) ( $row['quality'] ?? '' ),
				'bulk_run_uuid'         => (string) ( $row['bulk_run_uuid'] ?? '' ),
				'bulk_item_id'          => (int) ( $row['bulk_item_id'] ?? 0 ),
			];
			if ( ! in_array( $context['run_target'], $this->allowed_targets_for_workflow( (string) $current['workflow'] ), true ) || $context['subject_id'] <= 0 ) {
				return;
			}

			$completed = $this->complete_start_under_lock(
				(int) $current['user_id'],
				(string) $current['workflow'],
				(string) $current['request_id'],
				$context,
				[ 'run_id' => $run_id, 'request_record' => $current ],
				$job_ids,
				$metadata,
				true
			);
			$hydrated = Logger::instance()->get_run_row( $run_id );
			if ( ! is_wp_error( $completed ) && $this->response_factory()->normalize_job_ids( $hydrated['job_ids'] ?? [] ) === $job_ids ) {
				$this->release_recovery_claim( $hydrated, $current );
			}
		} finally {
			$lock->release( $lock_key, $token );
		}
	}

	private function has_recoverable_campaign_admission( array $record ): bool {
		return 'campaign_set' !== ( $record['workflow'] ?? null )
			|| ! empty( Generate_Campaign_Admission::normalize( $record['admission'] ?? null ) );
	}

	/**
	 * Repair only the durable request projection after a crash or CAS failure
	 * that occurred after the run accepted its job IDs. It intentionally never
	 * rewrites active-job or run attachment state.
	 */
	private function repair_reconciled_request_projection_locked( array $record, array $row, array $job_ids ): void {
		$workflow = (string) ( $record['workflow'] ?? '' );
		if (
			! in_array( $this->normalize_run_status( (string) ( $row['final_status'] ?? '' ) ), Workflow_Request_Store::ACTIVE_STATUSES, true )
			|| ! in_array( sanitize_key( (string) ( $row['run_target'] ?? '' ) ), $this->allowed_targets_for_workflow( $workflow ), true )
			|| (int) ( $row['subject_id'] ?? 0 ) <= 0
		) {
			return;
		}
		$run_changes = [
			'error_code'    => '',
			'error_message' => '',
		];
		if ( 'campaign_set' === $workflow ) {
			$campaign_finalize_meta = $this->campaign_admission_finalize_meta( $record, $row );
			if ( null === $campaign_finalize_meta ) {
				return;
			}
			$run_changes['finalize_meta'] = $campaign_finalize_meta;
		}
		$response = $this->response_factory()->build_run_response( $row, $record, 'processing' );
		$updated = $this->request_store()->update(
			$record,
			[
				'run_id'    => (string) $record['run_id'],
				'state'     => 'processing',
				'response'  => $response,
				'error'     => [],
				'uncertain' => false,
				'dispatch'  => $this->resolved_dispatch_intent( $record ),
			]
		);
		if ( is_wp_error( $updated ) ) {
			return;
		}
		$marker_cleared = Logger::instance()->update_run( (string) $record['run_id'], $run_changes );
		if ( ! $marker_cleared ) {
			$this->request_store()->update(
				$updated,
				[
					'state'     => 'processing',
					'error'     => $this->serialize_error( $this->uncertain_start_error( (string) $record['request_id'], (string) $record['run_id'], $job_ids ), $updated, 'processing', true ),
					'uncertain' => true,
				]
			);
			return;
		}
		Background_Scheduler::instance()->schedule_finalize( (string) $record['run_id'], (int) $record['user_id'], 5 );
		$this->release_recovery_claim( $row, $updated );
	}

	/**
	 * End a confirmed reconciliation outcome without inventing a refund. Only
	 * confirmed not_found cancels the local pending pre-dispatch settlement.
	 */
	private function terminalize_reconciled_request( array $record, string $reason, bool $confirmed_not_found ): void {
		$run_id = substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 );
		if ( '' === $run_id ) {
			$current = $this->current_reconciliation_record( $record );
			if ( ! empty( $current ) ) {
				$this->terminalize_reconciled_request_locked( $current, $reason, $confirmed_not_found, [] );
			}
			return;
		}

		$lock = Background_Lock::instance();
		$lock_key = $this->get_run_lifecycle_lock_key( $run_id );
		$token = $lock->acquire( $lock_key, self::RUN_LIFECYCLE_LOCK_TTL );
		if ( false === $token ) {
			return;
		}
		try {
			$current = $this->current_uncertain_request_record( $record );
			$row = Logger::instance()->get_run_row( $run_id );
			if (
				empty( $current )
				|| ! $this->run_matches_request_record( $row, $current )
			) {
				return;
			}
			$run_status = $this->normalize_run_status( (string) ( $row['final_status'] ?? '' ) );
			if ( $this->run_status_is_closed( $run_status ) ) {
				$this->sync_closed_run_request_record( $row, $current, $run_status );
				return;
			}
			if (
				! in_array( $run_status, Workflow_Request_Store::ACTIVE_STATUSES, true )
				|| 'reconcile_ready' !== $this->dispatch_intent( $current )['state']
				|| ! empty( $this->response_factory()->normalize_job_ids( $row['job_ids'] ?? [] ) )
			) {
				return;
			}
			$this->terminalize_reconciled_request_locked( $current, $reason, $confirmed_not_found, $row );
		} finally {
			$lock->release( $lock_key, $token );
		}
	}

	private function terminalize_reconciled_request_locked( array $record, string $reason, bool $confirmed_not_found, array $row ): void {
		$run_id = substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 );
		$user_id = (int) ( $record['user_id'] ?? 0 );
		$logger = Logger::instance();
		if ( '' !== $run_id && ! empty( $row ) ) {
			if ( ! $logger->finalize_run( $run_id, 'failed', [ 'error_code' => $reason ] ) ) {
				return;
			}
			$finalized = $logger->get_run_row( $run_id );
			if (
				empty( $finalized )
				|| ! hash_equals( $run_id, (string) ( $finalized['run_id'] ?? '' ) )
				|| ! $this->run_matches_request_record( $finalized, $record )
				|| 'failed' !== $this->normalize_run_status( (string) ( $finalized['final_status'] ?? '' ) )
				|| ! hash_equals( sanitize_key( $reason ), sanitize_key( (string) ( $finalized['error_code'] ?? '' ) ) )
			) {
				return;
			}
			if ( $confirmed_not_found ) {
				Workflow_Credit_Settlement::cancel_before_dispatch( $logger, $run_id, $finalized['credit_settlement_state'] ?? null );
			}
			Job_Manager::instance()->clear_active_jobs_by_run( $user_id, $run_id );
			$this->release_recovery_claim( $finalized, $record );
		}

		$error = new \WP_Error(
			'fotogenic_ai_' . $reason,
			__( 'The upstream generation request did not complete.', 'fotogenic-ai' ),
			[ 'status' => 502, 'requestId' => (string) ( $record['request_id'] ?? '' ), 'runId' => $run_id ]
		);
		$this->request_store()->update(
			$record,
			[
				'state'     => 'failed',
				'error'     => $this->serialize_error( $error, $record, 'failed', false ),
				'uncertain' => false,
				'dispatch'  => $this->resolved_dispatch_intent( $record ),
			]
		);
	}

	private function synchronize_closed_reconciliation_run( array $record ): void {
		$run_id = substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 );
		if ( '' === $run_id ) {
			return;
		}

		$lock = Background_Lock::instance();
		$lock_key = $this->get_run_lifecycle_lock_key( $run_id );
		$token = $lock->acquire( $lock_key, self::RUN_LIFECYCLE_LOCK_TTL );
		if ( false === $token ) {
			return;
		}
		try {
			$current = $this->current_uncertain_request_record( $record );
			$row = Logger::instance()->get_run_row( $run_id );
			$status = $this->normalize_run_status( (string) ( $row['final_status'] ?? '' ) );
			if ( ! empty( $current ) && $this->run_matches_request_record( $row, $current ) && $this->run_status_is_closed( $status ) ) {
				$this->sync_closed_run_request_record( $row, $current, $status );
			}
		} finally {
			$lock->release( $lock_key, $token );
		}
	}

	private function current_reconciliation_record( array $record ): array {
		$current = $this->current_uncertain_request_record( $record );
		if ( empty( $current ) || 'reconcile_ready' !== $this->dispatch_intent( $current )['state'] ) {
			return [];
		}

		return $current;
	}

	private function current_uncertain_request_record( array $record ): array {
		$current = $this->current_active_request_record( $record );
		if ( empty( $current ) || ! $this->record_has_dispatched_uncertainty( $current ) ) {
			return [];
		}

		return $current;
	}

	private function current_recoverable_request_record( array $record ): array {
		$current = $this->current_active_request_record( $record );
		if ( empty( $current ) || ! $this->request_record_has_recoverable_uncertainty( $current ) ) {
			return [];
		}

		return $current;
	}

	private function current_active_request_record( array $record ): array {
		$option_key = (string) ( $record['option_key'] ?? '' );
		$current = '' !== $option_key ? $this->request_store()->read( $option_key ) : null;
		if (
			! is_array( $current )
			|| ! hash_equals( (string) ( $record['token'] ?? '' ), (string) ( $current['token'] ?? '' ) )
			|| ! in_array( $this->request_store()->normalize_state( (string) ( $current['state'] ?? '' ) ), Workflow_Request_Store::ACTIVE_STATUSES, true )
		) {
			return [];
		}

		return $current;
	}

	private function release_recovery_claim( array $row, array $record ): void {
		$run_id = substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 );
		if ( '' === $run_id ) {
			return;
		}
		Job_Manager::instance()->release_generation_start_claim_for_recovery(
			(int) ( $record['user_id'] ?? 0 ),
			(string) ( $row['run_target'] ?? '' ),
			(int) ( $row['subject_id'] ?? 0 ),
			(string) ( $record['workflow'] ?? '' ),
			(string) ( $record['request_id'] ?? '' ),
			$run_id,
			(string) ( $row['run_item_uuid'] ?? '' )
		);
	}

    private function request_record_can_be_recovered( string $option_name, array $record ): bool {
        $user_id = (int) ( $record['user_id'] ?? 0 );
        $workflow = sanitize_key( (string) ( $record['workflow'] ?? '' ) );
        $request_id = strtolower( trim( (string) ( $record['request_id'] ?? '' ) ) );
        $fingerprint = (string) ( $record['payload_fingerprint'] ?? '' );
        if ( $option_name !== $this->request_store()->get_option_key( $user_id, $workflow, $request_id ) ) {
            return false;
        }
        $matches = $this->request_store()->matches( $record, $user_id, $workflow, $request_id, $fingerprint, $option_name );
        $state = $this->request_store()->normalize_state( (string) ( $record['state'] ?? 'starting' ) );
        return ! is_wp_error( $matches )
            && in_array( $state, Workflow_Request_Store::ACTIVE_STATUSES, true )
            && ( $this->request_record_has_recoverable_uncertainty( $record ) || $this->record_is_stale( $record ) );
    }

    /**
     * @return array|\WP_Error
     */
    private function terminalize_stale_request_record( array $record ) {
        $state = $this->request_store()->normalize_state( (string) ( $record['state'] ?? 'starting' ) );
        if ( ! in_array( $state, Workflow_Request_Store::ACTIVE_STATUSES, true ) || ! $this->record_is_stale( $record ) ) {
            return $record;
        }

        $run_id = substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 );
        $error = $this->stale_request_error( $record, $run_id );
		return $this->request_store()->update(
			$record,
			[
				'state'     => 'stale',
				'error'     => $this->serialize_error( $error, $record, 'stale', false ),
				'uncertain' => false,
				'dispatch'  => $this->terminal_dispatch_intent( $record ),
			]
		);
    }

    private function stale_request_error( array $record, string $run_id ): \WP_Error {
        return new \WP_Error(
            'fotogenic_ai_workflow_request_stale',
            __( 'The workflow request expired before completion.', 'fotogenic-ai' ),
            [
                'status'         => 409,
                'requestId'      => (string) ( $record['request_id'] ?? '' ),
                'runId'          => $run_id,
                'workflowStatus' => 'stale',
            ]
        );
    }

    private function sync_finalized_request_record( string $run_id, string $final_status ): void {
        $run_id = substr( sanitize_text_field( trim( $run_id ) ), 0, 64 );
        if ( '' === $run_id ) {
            return;
        }

        $row = Logger::instance()->get_run_row( $run_id );
        $workflow = sanitize_key( (string) ( $row['workflow'] ?? '' ) );
        $request_id = strtolower( trim( (string) ( $row['request_id'] ?? '' ) ) );
        $user_id = (int) ( $row['user_id'] ?? 0 );
        if ( empty( $row ) || ! in_array( $workflow, Workflow_Request_Store::WORKFLOWS, true ) || ! $this->request_store()->is_valid_request_id( $request_id ) ) {
            return;
        }

        $status = $this->normalize_run_status( (string) ( $row['final_status'] ?? $final_status ) );
		if ( ! $this->run_status_is_closed( $status ) ) {
            return;
        }

        $option_key = $this->request_store()->get_option_key( $user_id, $workflow, $request_id );
        $record = '' !== $option_key ? $this->request_store()->read( $option_key ) : null;
        if ( ! is_array( $record ) ) {
            return;
        }
        $fingerprint = (string) ( $record['payload_fingerprint'] ?? '' );
        $matches = $this->request_store()->matches( $record, $user_id, $workflow, $request_id, $fingerprint, $option_key );
        if ( is_wp_error( $matches ) || ! $this->run_matches_request_record( $row, $record ) ) {
            return;
        }
		$this->sync_closed_run_request_record( $row, $record, $status );
	}

	private function sync_closed_run_request_record( array $row, array $record, string $status ): void {
		$status = $this->normalize_run_status( $status );
		if ( ! $this->run_status_is_closed( $status ) || ! $this->run_matches_request_record( $row, $record ) ) {
			return;
		}

		$run_id = substr( sanitize_text_field( (string) ( $row['run_id'] ?? $record['run_id'] ?? '' ) ), 0, 64 );
		if ( '' === $run_id ) {
			return;
		}
		$record['run_id'] = $run_id;
        $response = $this->response_factory()->build_run_response( $row, $record, $status );
		$is_quarantined = in_array( $status, Workflow_Request_Store::QUARANTINE_STATUSES, true );
		$dispatch = $this->dispatch_intent( $record );
		if ( $is_quarantined && in_array( $dispatch['state'], [ 'in_flight', 'reconcile_ready' ], true ) ) {
			$dispatch['state'] = 'unresolved';
		} else {
			$dispatch = $this->terminal_dispatch_intent( $record );
		}
		$changes = [
			'run_id'   => $run_id,
			'state'    => $status,
			'response' => $response,
			'error'    => [],
			'uncertain'=> $is_quarantined,
			'dispatch' => $dispatch,
		];
		if ( in_array( $status, array_merge( Workflow_Request_Store::FAILURE_STATUSES, Workflow_Request_Store::QUARANTINE_STATUSES ), true ) ) {
            $error = $this->error_from_run( $row, $status );
			$changes['error'] = $this->serialize_error( $error, $record, $status, $is_quarantined );
        }
		$this->request_store()->update( $record, $changes );
    }

	private function run_status_is_closed( string $status ): bool {
		return in_array(
			$this->normalize_run_status( $status ),
			array_merge( Workflow_Request_Store::SUCCESS_STATUSES, Workflow_Request_Store::FAILURE_STATUSES, Workflow_Request_Store::QUARANTINE_STATUSES ),
			true
		);
	}

    private function persist_request_error( array $record, \WP_Error $error, string $state, bool $uncertain ): void {
        if ( empty( $record ) ) {
            return;
        }
		$changes = [
			'state'     => $state,
			'error'     => $this->serialize_error( $error, $record, $state, $uncertain ),
			'uncertain' => $uncertain,
		];
		$dispatch = $this->dispatch_intent( $record );
		if ( $uncertain && 'in_flight' === $dispatch['state'] ) {
			$changes['dispatch'] = $this->reconciliation_ready_dispatch_intent( $record );
		} elseif ( ! $uncertain && in_array( $dispatch['state'], [ 'in_flight', 'reconcile_ready' ], true ) ) {
			$changes['dispatch'] = $this->resolved_dispatch_intent( $record );
		}
        $this->request_store()->update(
            $record,
			$changes
        );
    }

    private function serialize_error( \WP_Error $error, array $record, string $state, bool $uncertain ): array {
        $data = $error->get_error_data();
        $data = is_array( $data ) ? $data : [];
        $status = (int) ( $data['status'] ?? 500 );
        if ( $status < 400 || $status > 599 ) {
            $status = 500;
        }
        return [
            'code'           => substr( sanitize_key( (string) $error->get_error_code() ), 0, 64 ),
            'message'        => $this->response_factory()->truncate_text( sanitize_text_field( $error->get_error_message() ), self::MAX_ERROR_MESSAGE_LENGTH ),
            'status'         => $status,
            'requestId'      => (string) ( $record['request_id'] ?? '' ),
            'runId'          => substr( sanitize_text_field( (string) ( $record['run_id'] ?? $data['runId'] ?? '' ) ), 0, 64 ),
            'jobIds'         => $this->response_factory()->normalize_job_ids( $data['jobIds'] ?? [] ),
            'workflowStatus' => $this->request_store()->normalize_state( $state ),
            'uncertain'      => $uncertain,
        ];
    }

    private function error_from_record( array $record ): \WP_Error {
        $stored = isset( $record['error'] ) && is_array( $record['error'] ) ? $record['error'] : [];
        $state = $this->request_store()->normalize_state( (string) ( $record['state'] ?? 'failed' ) );
        $code = substr( sanitize_key( (string) ( $stored['code'] ?? '' ) ), 0, 64 );
        if ( '' === $code ) {
            $code = 'fotogenic_ai_workflow_' . $state;
        }
        $message = $this->response_factory()->truncate_text( sanitize_text_field( (string) ( $stored['message'] ?? '' ) ), self::MAX_ERROR_MESSAGE_LENGTH );
        if ( '' === $message ) {
            $message = in_array( $state, [ 'stale', 'timeout' ], true )
                ? __( 'The workflow request expired before completion.', 'fotogenic-ai' )
                : __( 'The workflow request failed.', 'fotogenic-ai' );
        }
        $status = (int) ( $stored['status'] ?? 500 );
        if ( $status < 400 || $status > 599 ) {
            $status = 500;
        }
        return new \WP_Error(
            $code,
            $message,
            [
                'status'         => $status,
                'requestId'      => (string) $record['request_id'],
                'runId'          => substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 ),
                'jobIds'         => $this->response_factory()->normalize_job_ids( $stored['jobIds'] ?? [] ),
                'workflowStatus' => $state,
                'uncertain'      => ! empty( $record['uncertain'] ),
            ]
        );
    }

    private function error_from_run( array $row, string $status ): \WP_Error {
        $run_code = sanitize_key( (string) ( $row['error_code'] ?? '' ) );
        $code = fotogenic_ai_str_starts_with( $run_code, 'fotogenic_ai_' ) ? $run_code : 'fotogenic_ai_workflow_' . ( '' !== $run_code ? $run_code : $status );
        $message = $this->response_factory()->truncate_text( sanitize_text_field( (string) ( $row['error_message'] ?? '' ) ), self::MAX_ERROR_MESSAGE_LENGTH );
        if ( '' === $message ) {
            $message = in_array( $status, [ 'stale', 'timeout' ], true )
                ? __( 'The workflow request expired before completion.', 'fotogenic-ai' )
                : __( 'The workflow request failed.', 'fotogenic-ai' );
        }
        return new \WP_Error(
            $code,
            $message,
            [
                'status'         => in_array( $status, [ 'stale', 'timeout' ], true ) ? 409 : 500,
                'requestId'      => (string) ( $row['request_id'] ?? '' ),
                'runId'          => (string) ( $row['run_id'] ?? '' ),
                'jobIds'         => $this->response_factory()->normalize_job_ids( $row['job_ids'] ?? [] ),
                'workflowStatus' => $status,
                'uncertain'      => false,
            ]
        );
    }

    private function uncertain_start_error( string $request_id, string $run_id, array $job_ids ): \WP_Error {
        return new \WP_Error(
            'fotogenic_ai_workflow_start_uncertain',
            __( 'The start response could not be confirmed. Do not retry yet; refresh this workflow shortly.', 'fotogenic-ai' ),
            [
                'status'    => 503,
                'requestId' => $request_id,
                'runId'     => $run_id,
                'jobIds'    => $this->response_factory()->normalize_job_ids( $job_ids ),
                'uncertain' => true,
            ]
        );
    }

    private function resolve_active_job_key(
        Job_Manager $job_manager,
        string $workflow,
        string $request_id,
        int $user_id,
        array $context,
        array $metadata
    ): string {
        $run_target = (string) ( $context['run_target'] ?? '' );
        $subject_id = (int) ( $context['subject_id'] ?? 0 );
        $bulk_run_uuid = trim( (string) ( $metadata['bulk_run_uuid'] ?? '' ) );
        $bulk_item_id = (int) ( $metadata['bulk_item_id'] ?? 0 );
        if ( '' !== $bulk_run_uuid && $bulk_item_id > 0 ) {
            return 'bulk:' . $bulk_run_uuid . ':' . $bulk_item_id;
        }
        if ( 'product_images' === $workflow ) {
            return $job_manager->get_subject_job_key( $run_target, $subject_id );
        }
        return $job_manager->get_workflow_job_key(
            $workflow,
            $run_target,
            $subject_id,
            $user_id,
            $request_id
        );
    }

    private function allowed_targets_for_workflow( string $workflow ): array {
        return 'product_images' === $workflow
            ? [ 'product', 'media', 'post', 'page' ]
            : [ 'product', 'media' ];
    }

    private function get_stale_start_seconds( string $workflow ): int {
        $seconds = (int) apply_filters( 'fotogenic_ai_generation_start_claim_ttl', 600 );
        $seconds = max( 60, min( HOUR_IN_SECONDS, $seconds ) );
        return max( $seconds, Logger::instance()->get_generation_timeout_seconds( $workflow ) );
    }

    private function record_is_stale( array $record ): bool {
        if ( $this->record_has_dispatched_uncertainty( $record ) ) {
            return false;
        }
        $created_at = (int) ( $record['created_at'] ?? 0 );
        return $created_at > 0
            && time() - $created_at > $this->get_stale_start_seconds( (string) ( $record['workflow'] ?? '' ) );
    }

    private function run_is_stale( array $row, string $workflow ): bool {
        if ( $this->run_has_dispatched_uncertainty( $row ) ) {
            return false;
        }
        $started_at = strtotime( (string) ( $row['started_at'] ?? '' ) );
        return false !== $started_at
            && time() - $started_at > $this->get_stale_start_seconds( $workflow );
    }

    /**
     * Uncertainty is written only after a billable dispatch could have reached
     * upstream. It must never be converted to a retryable state by elapsed time.
     */
    private function record_has_dispatched_uncertainty( array $record ): bool {
        $error = isset( $record['error'] ) && is_array( $record['error'] ) ? $record['error'] : [];
        return ! empty( $record['uncertain'] )
            || ! empty( $error['uncertain'] )
            || self::DISPATCHED_UNCERTAIN_REQUEST_ERROR === sanitize_key( (string) ( $error['code'] ?? '' ) );
    }

	private function request_record_has_recoverable_uncertainty( array $record ): bool {
		if ( $this->record_has_dispatched_uncertainty( $record ) ) {
			return true;
		}
		$run_id = substr( sanitize_text_field( (string) ( $record['run_id'] ?? '' ) ), 0, 64 );
		if ( '' === $run_id ) {
			return false;
		}
		$row = Logger::instance()->get_run_row( $run_id );
		return $this->run_matches_request_record( $row, $record )
			&& $this->run_has_dispatched_uncertainty( $row );
	}

	/**
	 * Treat either durable marker as proof that a billable dispatch is uncertain.
	 */
    private function run_has_dispatched_uncertainty( array $row ): bool {
        $error_code = sanitize_key( (string) ( $row['error_code'] ?? '' ) );
        if ( in_array( $error_code, self::DISPATCHED_UNCERTAIN_RUN_ERRORS, true ) ) {
            return true;
        }

        $user_id = (int) ( $row['user_id'] ?? 0 );
        $workflow = sanitize_key( (string) ( $row['workflow'] ?? '' ) );
        $request_id = strtolower( trim( (string) ( $row['request_id'] ?? '' ) ) );
        $option_key = $this->request_store()->get_option_key( $user_id, $workflow, $request_id );
        if ( '' === $option_key ) {
            return false;
        }

        $record = $this->request_store()->read( $option_key );
        return is_array( $record )
            && $this->run_matches_request_record( $row, $record )
            && $this->record_has_dispatched_uncertainty( $record );
    }

    private function claim_matches_start( array $claim, int $user_id, string $workflow, string $request_id, array $context ): bool {
        $parallel_item = ! empty( $context['parallel_item_identity'] );
        return $user_id > 0
            && (int) ( $claim['user_id'] ?? 0 ) === $user_id
            && $workflow === (string) ( $claim['workflow'] ?? '' )
            && hash_equals( $request_id, (string) ( $claim['request_id'] ?? '' ) )
            && (string) $context['run_target'] === (string) ( $claim['run_target'] ?? '' )
            && (int) $context['subject_id'] === (int) ( $claim['subject_id'] ?? 0 )
            && ( ! $parallel_item || hash_equals( (string) ( $context['run_item_uuid'] ?? '' ), (string) ( $claim['run_item_uuid'] ?? '' ) ) )
            && '' !== (string) ( $claim['token'] ?? '' );
    }

    private function run_matches_start( array $row, string $run_id, int $user_id, string $workflow, string $request_id, array $context ): bool {
        $parallel_item = ! empty( $context['parallel_item_identity'] );
        return ! empty( $row )
            && hash_equals( $run_id, (string) ( $row['run_id'] ?? '' ) )
            && (int) ( $row['user_id'] ?? 0 ) === $user_id
            && $workflow === (string) ( $row['workflow'] ?? '' )
            && hash_equals( $request_id, (string) ( $row['request_id'] ?? '' ) )
            && (string) $context['run_target'] === (string) ( $row['run_target'] ?? '' )
            && (int) $context['subject_id'] === (int) ( $row['subject_id'] ?? 0 )
            && ( ! $parallel_item || hash_equals( (string) ( $context['run_item_uuid'] ?? '' ), (string) ( $row['run_item_uuid'] ?? '' ) ) );
    }

    private function run_matches_request_record( array $row, array $record ): bool {
        return ! empty( $row )
            && (int) ( $row['user_id'] ?? 0 ) === (int) ( $record['user_id'] ?? 0 )
            && (string) ( $row['workflow'] ?? '' ) === (string) ( $record['workflow'] ?? '' )
            && hash_equals( (string) ( $record['request_id'] ?? '' ), (string) ( $row['request_id'] ?? '' ) );
    }

    private function normalize_run_status( string $status ): string {
        $status = sanitize_key( $status );
        if ( in_array( $status, [ 'started', '' ], true ) ) {
            return 'starting';
        }
        return $this->request_store()->normalize_state( $status );
    }

    private function request_store(): Workflow_Request_Store {
        if ( ! isset( $this->request_store ) ) {
            $this->request_store = new Workflow_Request_Store( $this->response_factory() );
        }
        return $this->request_store;
    }

    private function response_factory(): Workflow_Response_Factory {
        if ( ! isset( $this->response_factory ) ) {
            $this->response_factory = new Workflow_Response_Factory();
        }
        return $this->response_factory;
    }

	private function upstream_response_parser(): Workflow_Upstream_Response_Parser {
		if ( ! isset( $this->upstream_response_parser ) ) {
			$this->upstream_response_parser = new Workflow_Upstream_Response_Parser();
		}
		return $this->upstream_response_parser;
	}

	private function workflow_request_reconciler(): Workflow_Request_Reconciler {
		if ( ! isset( $this->workflow_request_reconciler ) ) {
			$this->workflow_request_reconciler = new Workflow_Request_Reconciler( $this->upstream_response_parser() );
		}
		return $this->workflow_request_reconciler;
	}

	/**
	 * A nominal upstream success was already observed, so any subsequent local
	 * parser or persistence failure must never be treated as undispatched.
	 */
	private function upstream_start_capture_uncertain_error( \WP_Error $cause ): \WP_Error {
		$cause_code = sanitize_key( (string) $cause->get_error_code() );
		return new \WP_Error(
			'' !== $cause_code ? $cause_code : self::DISPATCHED_UNCERTAIN_REQUEST_ERROR,
			__( 'The upstream start response could not be confirmed.', 'fotogenic-ai' ),
			[
				'status'    => 503,
				'uncertain' => true,
				'cause'     => $cause_code,
			]
		);
	}

	private function not_dispatched_start_error(): \WP_Error {
		return new \WP_Error(
			'fotogenic_ai_workflow_dispatch_not_durable',
			__( 'The generation request could not be prepared for dispatch. Reload Studio and try again.', 'fotogenic-ai' ),
			[
				'status'         => 503,
				'not_dispatched' => true,
			]
		);
	}

	/** @return array{state:string,reconcile_attempts:int,reconcile_not_before_at:int,reconcile_deadline_at:int} */
	private function dispatch_intent( array $record ): array {
		$intent = $record['dispatch'] ?? null;
		if (
			! is_array( $intent )
			|| [ 'state', 'reconcile_attempts', 'reconcile_not_before_at', 'reconcile_deadline_at' ] !== array_keys( $intent )
			|| ! in_array( $intent['state'], Workflow_Request_Store::DISPATCH_INTENT_STATES, true )
			|| ! is_int( $intent['reconcile_attempts'] )
			|| ! is_int( $intent['reconcile_not_before_at'] )
			|| ! is_int( $intent['reconcile_deadline_at'] )
		) {
			return Workflow_Request_Store::initial_dispatch_intent();
		}
		return $intent;
	}

	/** @return array{state:string,reconcile_attempts:int,reconcile_not_before_at:int,reconcile_deadline_at:int} */
	private function reconciliation_ready_dispatch_intent( array $record ): array {
		$intent = $this->dispatch_intent( $record );
		$intent['state'] = 'reconcile_ready';
		$intent['reconcile_not_before_at'] = min( time(), $intent['reconcile_not_before_at'] );
		return $intent;
	}

	/** @return array{state:string,reconcile_attempts:int,reconcile_not_before_at:int,reconcile_deadline_at:int} */
	private function resolved_dispatch_intent( array $record ): array {
		$intent = $this->dispatch_intent( $record );
		$intent['state'] = 'resolved';
		return $intent;
	}

	/** @return array{state:string,reconcile_attempts:int,reconcile_not_before_at:int,reconcile_deadline_at:int} */
	private function terminal_dispatch_intent( array $record ): array {
		$intent = $this->dispatch_intent( $record );
		if ( in_array( $intent['state'], [ 'in_flight', 'reconcile_ready', 'resolved' ], true ) ) {
			$intent['state'] = 'resolved';
		}
		return $intent;
	}

	private function campaign_admission_finalize_meta( array $record, array $run_row ): ?string {
		if ( 'campaign_set' !== (string) ( $record['workflow'] ?? '' ) ) {
			return null;
		}
		$admission = Generate_Campaign_Admission::normalize( $record['admission'] ?? null );
		if ( empty( $admission ) ) {
			return null;
		}
		$existing = $run_row['finalize_meta'] ?? [];
		if ( is_string( $existing ) ) {
			$decoded = json_decode( $existing, true );
			$existing = is_array( $decoded ) ? $decoded : [];
		}
		if ( ! is_array( $existing ) ) {
			$existing = [];
		}
		$existing[ Generate_Campaign_Admission::FINALIZE_META_KEY ] = $admission;
		return wp_json_encode( $existing, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	}
    private function canonicalize_payload_value( $value, bool &$valid ) {
        if ( is_array( $value ) ) {
            if ( fotogenic_ai_array_is_list( $value ) ) {
                $result = [];
                foreach ( $value as $item ) {
                    $result[] = $this->canonicalize_payload_value( $item, $valid );
                }
                return $result;
            }
            foreach ( array_keys( $value ) as $key ) {
                if ( ! is_string( $key ) ) {
                    $valid = false;
                    return [];
                }
            }
            ksort( $value, SORT_STRING );
            $result = [];
            foreach ( $value as $key => $item ) {
                $result[ $key ] = $this->canonicalize_payload_value( $item, $valid );
            }
            return $result;
        }
        if ( null === $value || is_string( $value ) || is_int( $value ) || is_bool( $value ) ) {
            return $value;
        }
        if ( is_float( $value ) && is_finite( $value ) ) {
            return $value;
        }
        $valid = false;
        return null;
    }

}

if ( function_exists( 'add_action' ) ) {
    add_action( 'fotogenic_ai_run_finalized', [ Workflow_Start_Coordinator::class, 'sync_finalized_request' ], 20, 2 );
}
