<?php

namespace Fotogenic_AI;

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

/**
 * Validate and execute one immutable universal Bulk item.
 *
 * The Bulk runner owns leases and state transitions. This class owns the
 * definition-driven command boundary, durable Single-request reconciliation,
 * and direct calls to the shared Single start/poll services.
 */
final class Universal_Bulk_Item_Dispatcher {
	private Workflow_Draft_Normalizer $normalizer;
	private \Closure $starter;
	private \Closure $request_reader;
	private \Closure $poller;

	public function __construct(
		?Workflow_Draft_Normalizer $normalizer = null,
		?callable $starter = null,
		?callable $request_reader = null,
		?callable $poller = null
	) {
		$this->normalizer     = $normalizer ?? new Workflow_Draft_Normalizer();
		$this->starter        = \Closure::fromCallable( $starter ?? [ $this, 'start_single' ] );
		$this->request_reader = \Closure::fromCallable( $request_reader ?? [ $this, 'read_durable_request' ] );
		$this->poller         = \Closure::fromCallable( $poller ?? [ $this, 'poll_single' ] );
	}

	public function supports( array $item ): bool {
		return '' !== trim( (string) ( $item['command_snapshot'] ?? '' ) );
	}

	/**
	 * Verify the immutable row and produce a canonical Single envelope.
	 *
	 * @return array|\WP_Error
	 */
	public function prepare( array $run, array $item ) {
		$encoded = trim( (string) ( $item['command_snapshot'] ?? '' ) );
		$command = '' !== $encoded ? json_decode( $encoded, true ) : null;
		if ( ! is_array( $command ) || fotogenic_ai_array_is_list( $command ) || JSON_ERROR_NONE !== json_last_error() ) {
			return $this->invalid_command();
		}
		if (
			2 !== (int) ( $command['schemaVersion'] ?? 0 )
			|| [ 'schemaVersion', 'runItem', 'draft', 'sources' ] !== array_keys( $command )
		) {
			return $this->invalid_command();
		}
		$allowed = Workflow_Contract_Guard::reject_forbidden( $command );
		if ( is_wp_error( $allowed ) ) {
			return $allowed;
		}

		$draft_object = is_array( $command['draft'] ?? null )
			? $this->normalizer->rehydrate_normalized( $command['draft'] )
			: $this->invalid_command();
		if ( is_wp_error( $draft_object ) ) {
			return $draft_object;
		}
		$draft = $this->normalizer->normalize( $draft_object );
		if ( is_wp_error( $draft ) || 'bulk' !== (string) ( $draft['scope'] ?? '' ) ) {
			return is_wp_error( $draft ) ? $draft : $this->invalid_command();
		}

		$workflow = sanitize_key( (string) ( $run['workflow'] ?? '' ) );
		$subject_type = sanitize_key( (string) ( $item['subject_type'] ?? '' ) );
		$subject_id = absint( $item['subject_id'] ?? 0 );
		$request_id = strtolower( trim( (string) ( $item['request_id'] ?? '' ) ) );
		$sources = is_array( $command['sources'] ?? null ) ? $command['sources'] : [];
		if (
			[ 'subjectType', 'subjectId', 'attachmentIds' ] !== array_keys( $sources )
			|| $workflow !== (string) ( $draft['workflow'] ?? '' )
			|| $subject_type !== (string) ( $draft['sourceType'] ?? '' )
			|| $subject_type !== (string) ( $sources['subjectType'] ?? '' )
			|| $subject_id <= 0
			|| $subject_id !== (int) ( $sources['subjectId'] ?? 0 )
			|| [ $subject_id ] !== (array) ( $draft['selection']['selectedIds'] ?? [] )
			|| ! Workflow_Registry::supports( $workflow, $subject_type, 'bulk' )
			|| ! wp_is_uuid( $request_id )
		) {
			return $this->invalid_command();
		}

		$attachment_ids = $this->normalize_attachment_ids( $sources['attachmentIds'] ?? null );
		if ( is_wp_error( $attachment_ids ) ) {
			return $attachment_ids;
		}
		$fingerprint = $this->normalizer->fingerprint( $command );
		$stored_fingerprint = strtolower( trim( (string) ( $item['request_fingerprint'] ?? '' ) ) );
		if (
			is_wp_error( $fingerprint )
			|| 64 !== strlen( $stored_fingerprint )
			|| ! hash_equals( $stored_fingerprint, $fingerprint )
		) {
			return is_wp_error( $fingerprint ) ? $fingerprint : $this->invalid_command();
		}

		$run_item = is_array( $command['runItem'] ?? null ) ? $command['runItem'] : [];
		if ( [ 'id', 'groupId', 'ordinal', 'target', 'sourceRoles', 'destination' ] !== array_keys( $run_item ) ) {
			return $this->invalid_command();
		}
		$run_item_id = strtolower( trim( (string) ( $run_item['id'] ?? '' ) ) );
		if ( ! wp_is_uuid( $run_item_id ) ) {
			return $this->invalid_command();
		}
		$campaign_group_id = $run_item['groupId'];
		if ( null !== $campaign_group_id ) {
			$campaign_group_id = strtolower( trim( (string) $campaign_group_id ) );
			if ( ! wp_is_uuid( $campaign_group_id ) ) {
				return $this->invalid_command();
			}
		}
		$item_ordinal = $run_item['ordinal'] ?? null;
		if ( ! is_int( $item_ordinal ) || $item_ordinal <= 0 ) {
			return $this->invalid_command();
		}
		$target = Workflow_Item_Identity::normalize_target_snapshot( $run_item['target'] ?? null );
		if ( is_wp_error( $target ) ) {
			return $target;
		}
		$source_roles = is_array( $run_item['sourceRoles'] ?? null ) ? $run_item['sourceRoles'] : [];
		$destination = is_array( $run_item['destination'] ?? null ) ? $run_item['destination'] : [];
		if (
			[ 'primaryAttachmentId', 'supportingAttachmentIds' ] !== array_keys( $source_roles )
			|| (int) $source_roles['primaryAttachmentId'] !== (int) $attachment_ids[0]
			|| array_values( (array) $source_roles['supportingAttachmentIds'] ) !== array_values( array_slice( $attachment_ids, 1 ) )
			|| $subject_type !== (string) $target['sourceType']
			|| $subject_id !== (int) $target['targetId']
			|| ! $this->current_group_contract_matches( $draft, $campaign_group_id, $subject_id, $target, $source_roles )
			|| ! $this->current_item_identity_matches( $item, $run_item_id, $campaign_group_id, $item_ordinal, $target, $source_roles, $destination )
		) {
			return $this->invalid_command();
		}

		$recipe         = (array) ( $draft['recipe'] ?? [] );
		$expected_count = 'increase_resolution' === $workflow ? 1 : (int) ( $recipe['count'] ?? 0 );
		if ( $expected_count <= 0 ) {
			return $this->invalid_command();
		}
		$single_source_policy = 'campaign_set' === $workflow
			? (array) ( $draft['sourcePolicy'] ?? [] )
			: [ 'attachmentIds' => $attachment_ids ];
		$single_draft         = $this->normalizer->rehydrate_normalized(
			[
				'schemaVersion' => Workflow_Registry::SCHEMA_VERSION,
				'workflow'      => $workflow,
				'sourceType'    => $subject_type,
				'scope'         => 'single',
				'selection'     => [
					'strategy'    => 'explicit',
					'selectedIds' => [ $subject_id ],
					'excludedIds' => [],
					'query'       => [],
				],
				'sourcePolicy'  => $single_source_policy,
				'recipe'        => $recipe,
				'resultPolicy'  => (array) ( $draft['resultPolicy'] ?? [] ),
			]
		);
		if ( is_wp_error( $single_draft ) ) {
			return $single_draft;
		}
		$single_normalized = $this->normalizer->normalize( $single_draft );
		if ( is_wp_error( $single_normalized ) ) {
			return $single_normalized;
		}
		$source_snapshot = [
			'subjectType'   => $subject_type,
			'subjectId'     => $subject_id,
			'attachmentIds' => $attachment_ids,
			'target'        => $target,
			'sourceRoles'   => $source_roles,
		];
		if ( 'campaign_set' === $workflow ) {
			$source_snapshot['campaignGroupId'] = $campaign_group_id;
			$source_snapshot['itemOrdinal']     = 1;
		}
		$source_fingerprint = ( new Workflow_Source_Preflight() )->fingerprint_snapshot( [ $source_snapshot ] );
		if ( is_wp_error( $source_fingerprint ) ) {
			return $source_fingerprint;
		}
		$universal_fingerprint = $this->normalizer->fingerprint(
			[
				'draft'   => $single_normalized,
				'runItem' => [
					'id'                => $run_item_id,
					'groupId'           => $campaign_group_id,
					'ordinal'           => $item_ordinal,
					'targetHint'        => Workflow_Item_Identity::target_hint( $target ),
					'sourceFingerprint' => $source_fingerprint,
				],
			]
		);
		if ( is_wp_error( $universal_fingerprint ) ) {
			return $universal_fingerprint;
		}
		$single_members = get_object_vars( $single_draft );
		$envelope = (object) [
			'requestId'         => $request_id,
			'runItemId'         => $run_item_id,
			'campaignGroupId'   => $campaign_group_id,
			'itemOrdinal'       => $item_ordinal,
			'sourceFingerprint' => $source_fingerprint,
			'authority'         => null,
			'workflow'          => $workflow,
			'sourceType'      => $subject_type,
			'scope'           => 'single',
			'subject'         => (object) array_merge( [ 'id' => $subject_id ], Workflow_Item_Identity::target_hint( $target ) ),
			'sourcePolicy'    => $single_members['sourcePolicy'] ?? null,
			'recipe'          => $single_members['recipe'] ?? null,
			'resultPolicy'    => $single_members['resultPolicy'] ?? null,
		];
		if (
			! ( $envelope->{'sourcePolicy'} instanceof \stdClass )
			|| ! ( $envelope->recipe instanceof \stdClass )
			|| ! ( $envelope->{'resultPolicy'} instanceof \stdClass )
		) {
			return $this->invalid_command();
		}

		return [
			'workflow'            => $workflow,
			'sourceType'          => $subject_type,
			'subjectId'           => $subject_id,
			'requestId'           => $request_id,
			'runItemId'           => $run_item_id,
			'campaignGroupId'     => $campaign_group_id,
			'itemOrdinal'         => $item_ordinal,
			'target'              => $target,
			'sourceRoles'         => $source_roles,
			'destination'         => $destination,
			'attachmentIds'       => $attachment_ids,
			'expectedOutputCount'                  => $expected_count,
			'universalFingerprint'                 => $universal_fingerprint,
			'envelope'                             => $envelope,
		];
	}

	private function current_group_contract_matches( array $draft, ?string $group_id, int $subject_id, array $target, array $source_roles ): bool {
		if ( 'campaign_set' !== (string) ( $draft['workflow'] ?? '' ) ) {
			return null === $group_id;
		}
		$groups = $draft['sourcePolicy']['groups'] ?? null;
		if ( ! is_array( $groups ) || 1 !== count( $groups ) || ! is_array( $groups[0] ) ) {
			return false;
		}
		$group = $groups[0];
		return is_string( $group_id )
			&& hash_equals( $group_id, (string) ( $group['id'] ?? '' ) )
			&& 1 === (int) ( $group['ordinal'] ?? 0 )
			&& $subject_id === (int) ( $group['subjectId'] ?? 0 )
			&& Workflow_Item_Identity::hint_matches( $target, (array) ( $group['target'] ?? [] ) )
			&& (int) $source_roles['primaryAttachmentId'] === (int) ( $group['primaryAttachmentId'] ?? 0 )
			&& array_values( (array) $source_roles['supportingAttachmentIds'] ) === array_values( (array) ( $group['supportingAttachmentIds'] ?? [] ) )
			&& (array) ( $draft['recipe'] ?? [] ) === (array) ( $group['recipe'] ?? [] )
			&& (array) ( $draft['resultPolicy'] ?? [] ) === (array) ( $group['resultPolicy'] ?? [] );
	}

	private function current_item_identity_matches(
		array $item,
		string $run_item_id,
		?string $campaign_group_id,
		int $item_ordinal,
		array $target,
		array $source_roles,
		array $destination
	): bool {
		$stored_target = Workflow_Item_Identity::normalize_target_snapshot( $this->decode_object( $item['target_snapshot'] ?? null ) );
		$stored_roles = $this->decode_object( $item['source_roles_snapshot'] ?? null );
		$stored_destination = $this->decode_object( $item['destination_snapshot'] ?? null );
		return ! is_wp_error( $stored_target )
			&& hash_equals( $run_item_id, (string) ( $item['run_item_uuid'] ?? '' ) )
			&& (string) ( $campaign_group_id ?? '' ) === (string) ( $item['campaign_group_uuid'] ?? '' )
			&& $item_ordinal === (int) ( $item['item_ordinal'] ?? 0 )
			&& $target === $stored_target
			&& $source_roles === $stored_roles
			&& $destination === $stored_destination;
	}

	/** @param mixed $value Candidate value. */
	private function decode_object( $value ): array {
		if ( is_string( $value ) ) {
			$trimmed = trim( $value );
			$decoded = json_decode( $trimmed, true );
			return is_array( $decoded ) && fotogenic_ai_str_starts_with( $trimmed, '{' ) ? $decoded : [];
		}
		return is_array( $value ) && ( empty( $value ) || ! fotogenic_ai_array_is_list( $value ) ) ? $value : [];
	}

	/**
	 * Read the durable Single-request record without dispatching or refreshing price.
	 *
	 * @return array|\WP_Error
	 */
	public function inspect_durable_request( int $user_id, array $prepared ) {
		$expected_fingerprint = strtolower( trim( (string) ( $prepared['universalFingerprint'] ?? '' ) ) );
		if ( 1 !== preg_match( '/^[a-f0-9]{64}$/', $expected_fingerprint ) ) {
			return $this->invalid_durable_request();
		}

		return $this->inspect_durable_record(
			$user_id,
			(string) ( $prepared['workflow'] ?? '' ),
			(string) ( $prepared['requestId'] ?? '' ),
			$expected_fingerprint
		);
	}

	/**
	 * @return array|\WP_Error
	 */
	private function inspect_durable_record(
		int $user_id,
		string $workflow,
		string $request_id,
		string $expected_fingerprint
	) {
		$record = ( $this->request_reader )(
			$user_id,
			$workflow,
			$request_id
		);
		if ( null === $record ) {
			return [ 'exists' => false ];
		}
		if ( ! is_array( $record ) ) {
			return $this->invalid_durable_request();
		}

		$state = sanitize_key( (string) ( $record['state'] ?? '' ) );
		$stored_fingerprint = (string) ( $record['universal_fingerprint'] ?? '' );
		if (
			Workflow_Request_Store::RECORD_VERSION !== (int) ( $record['version'] ?? 0 )
			|| $user_id !== (int) ( $record['user_id'] ?? 0 )
			|| $workflow !== (string) ( $record['workflow'] ?? '' )
			|| ! hash_equals( $request_id, (string) ( $record['request_id'] ?? '' ) )
			|| '' === (string) ( $record['token'] ?? '' )
			|| 1 !== preg_match( '/^[a-f0-9]{64}$/', (string) ( $record['payload_fingerprint'] ?? '' ) )
			|| 1 !== preg_match( '/^[a-f0-9]{64}$/', $stored_fingerprint )
			|| ! hash_equals( $expected_fingerprint, $stored_fingerprint )
			|| ! in_array(
				$state,
				array_merge(
					Workflow_Request_Store::ACTIVE_STATUSES,
					Workflow_Request_Store::SUCCESS_STATUSES,
					Workflow_Request_Store::FAILURE_STATUSES
				),
				true
			)
		) {
			return $this->invalid_durable_request();
		}

		$response = ( new Workflow_Response_Factory() )->sanitize_stored_response( $record, $state );
		return [
			'exists'    => true,
			'state'     => $state,
			'uncertain' => ! empty( $record['uncertain'] ),
			'runId'     => trim( (string) ( $record['run_id'] ?? $response['runId'] ?? '' ) ),
			'jobIds'    => $this->normalize_job_ids( $response['jobIds'] ?? [] ),
			'response'  => $response,
		];
	}

	/** @return array|\WP_Error */
	public function start( array $prepared, array $run, array $item ) {
		$run_uuid = strtolower( trim( (string) ( $run['run_uuid'] ?? '' ) ) );
		$item_id  = (int) ( $item['id'] ?? 0 );
		if ( ! wp_is_uuid( $run_uuid ) || $item_id <= 0 ) {
			return $this->invalid_durable_request();
		}

		$result = ( $this->starter )(
			$prepared['envelope'],
			[
				'bulkRunUuid' => $run_uuid,
				'bulkItemId'  => $item_id,
			]
		);
		return $this->normalize_start_result( $result );
	}

	/** @return array|\WP_Error */
	public function poll( array $run, array $item, array $prepared ) {
		$result = ( $this->poller )( $run, $item, $prepared );
		if ( is_wp_error( $result ) ) {
			return $result;
		}
		$status_code = $result instanceof \WP_REST_Response ? (int) $result->get_status() : 200;
		$data = $result instanceof \WP_REST_Response ? $result->get_data() : $result;
		if ( ! is_array( $data ) ) {
			return new \WP_Error( 'fotogenic_ai_bulk_poll_invalid', __( 'Bulk status could not be read.', 'fotogenic-ai' ), [ 'status' => 500 ] );
		}
		if ( $status_code >= 400 ) {
			return new \WP_Error(
				sanitize_key( (string) ( $data['errorCode'] ?? 'fotogenic_ai_bulk_poll_failed' ) ),
				__( 'Bulk status could not be read.', 'fotogenic-ai' ),
				[ 'status' => $status_code ]
			);
		}

		$status = sanitize_key( (string) ( $data['status'] ?? '' ) );
		if ( ! in_array( $status, [ 'processing', 'completed', 'partial', 'failed', 'timeout' ], true ) ) {
			return new \WP_Error( 'fotogenic_ai_bulk_poll_invalid', __( 'Bulk status could not be read.', 'fotogenic-ai' ), [ 'status' => 500 ] );
		}
		$attachment_ids = [];
		foreach ( (array) ( $data['attachments'] ?? [] ) as $attachment ) {
			if ( is_array( $attachment ) ) {
			$attachment_ids[] = absint( $attachment['attachmentId'] ?? 0 );
			}
		}
		$attachment_ids = array_values( array_unique( array_filter( $attachment_ids ) ) );

		return [
			'status'        => $status,
			'attachmentIds' => $attachment_ids,
			'partial'       => ! empty( $data['partial'] ) || 'partial' === $status,
			'errorCode'     => substr( sanitize_key( (string) ( $data['errorCode'] ?? '' ) ), 0, 64 ),
			'finalizationPending' => ! empty( $data['finalizationPending'] ),
		];
	}

	public function poll_error_is_retryable( \WP_Error $error ): bool {
		$data = $error->get_error_data();
		$status = is_array( $data ) ? (int) ( $data['status'] ?? 0 ) : 0;
		if ( in_array( $status, [ 408, 425, 429, 500, 502, 503, 504 ], true ) ) {
			return true;
		}
		if ( $status >= 400 ) {
			return false;
		}
		$code = strtoupper( str_replace( '-', '_', sanitize_key( (string) $error->get_error_code() ) ) );
		foreach ( [ 'HTTP_REQUEST_FAILED', 'REQUEST_FAILED', 'TIMEOUT', 'NETWORK', 'CONNECTION', 'RATE_LIMIT', 'SERVICE_UNAVAILABLE', 'TEMPORARY' ] as $needle ) {
			if ( fotogenic_ai_str_contains( $code, $needle ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @param mixed $result Operation result.
	 * @return array|\WP_Error
	 */
	private function normalize_start_result( $result ) {
		if ( is_wp_error( $result ) ) {
			return $result;
		}
		$status_code = $result instanceof \WP_REST_Response ? (int) $result->get_status() : 200;
		$data = $result instanceof \WP_REST_Response ? $result->get_data() : $result;
		if ( ! is_array( $data ) ) {
			return new \WP_Error( 'fotogenic_ai_bulk_start_invalid', __( 'Bulk start could not be confirmed.', 'fotogenic-ai' ), [ 'status' => 500 ] );
		}
		if ( $status_code >= 400 ) {
			return new \WP_Error(
				sanitize_key( (string) ( $data['code'] ?? 'fotogenic_ai_bulk_start_failed' ) ),
				__( 'Bulk start could not be confirmed.', 'fotogenic-ai' ),
				is_array( $data['data'] ?? null ) ? $data['data'] : [ 'status' => $status_code ]
			);
		}
		if (
			! is_string( $data['status'] ?? null )
			|| '' === sanitize_key( $data['status'] )
			|| ! is_string( $data['runId'] ?? null )
			|| ! is_array( $data['jobIds'] ?? null )
			|| ! fotogenic_ai_array_is_list( $data['jobIds'] )
			|| ! is_string( $data['requestId'] ?? null )
		) {
			return new \WP_Error( 'fotogenic_ai_bulk_start_invalid', __( 'Bulk start could not be confirmed.', 'fotogenic-ai' ), [ 'status' => 500 ] );
		}
		$job_ids = $this->normalize_job_ids( $data['jobIds'] );
		return [
			'status'    => sanitize_key( $data['status'] ),
			'runId'     => trim( $data['runId'] ),
			'jobIds'    => $job_ids,
			'requestId' => strtolower( trim( $data['requestId'] ) ),
		];
	}

	/**
	 * @param mixed $raw Raw value.
	 * @return int[]|\WP_Error
	 */
	private function normalize_attachment_ids( $raw ) {
		if ( ! is_array( $raw ) || ! fotogenic_ai_array_is_list( $raw ) ) {
			return $this->invalid_command();
		}
		$ids = [];
		foreach ( $raw as $value ) {
			if ( ! is_int( $value ) || $value <= 0 || in_array( $value, $ids, true ) ) {
				return $this->invalid_command();
			}
			$ids[] = $value;
		}
		return ! empty( $ids ) ? $ids : $this->invalid_command();
	}

	/**
	 * @param mixed $raw Raw value.
	 * @return string[]
	 */
	private function normalize_job_ids( $raw ): array {
		if ( is_string( $raw ) || is_int( $raw ) ) {
			$decoded = is_string( $raw ) ? json_decode( $raw, true ) : null;
			$raw = is_array( $decoded ) ? $decoded : [ $raw ];
		}
		if ( ! is_array( $raw ) ) {
			return [];
		}
		$ids = [];
		foreach ( $raw as $value ) {
			if ( ! is_string( $value ) && ! is_int( $value ) ) {
				continue;
			}
			$value = trim( sanitize_text_field( (string) $value ) );
			if ( '' !== $value && strlen( $value ) <= 64 ) {
				$ids[] = $value;
			}
		}
		return array_values( array_unique( $ids ) );
	}

	/** @return array|\WP_REST_Response|\WP_Error */
	private function start_single( \stdClass $envelope, array $bulk_context = [] ) {
		return ( new Universal_Start_Service() )->start( $envelope, $bulk_context );
	}

	/** @return mixed */
	private function read_durable_request( int $user_id, string $workflow, string $request_id ) {
		$store = new Workflow_Request_Store( new Workflow_Response_Factory() );
		$key = $store->get_option_key( $user_id, $workflow, $request_id );
		return '' !== $key ? $store->read( $key ) : null;
	}

	/** @return array|\WP_REST_Response|\WP_Error */
	private function poll_single( array $run, array $item, array $prepared ) {
		return ( new Service_Generate_Poll() )->poll(
			(int) ( $run['user_id'] ?? 0 ),
			[
				'jobIds'     => $this->normalize_job_ids( $item['job_ids'] ?? [] ),
				'runId'      => (string) ( $item['run_id_single'] ?? '' ),
				'requestId'  => (string) $prepared['requestId'],
				'workflow'   => (string) $prepared['workflow'],
				'sourceType' => (string) $prepared['sourceType'],
				'subjectId'  => (int) $prepared['subjectId'],
				'bulkRunUuid'=> (string) ( $run['run_uuid'] ?? '' ),
				'bulkItemId' => (int) ( $item['id'] ?? 0 ),
			]
		);
	}

	private function invalid_command(): \WP_Error {
		return new \WP_Error(
			'fotogenic_ai_bulk_command_invalid',
			__( 'The saved Bulk command is invalid and was not submitted.', 'fotogenic-ai' ),
			[ 'status' => 409 ]
		);
	}

	private function invalid_durable_request(): \WP_Error {
		return new \WP_Error(
			'fotogenic_ai_bulk_request_conflict',
			__( 'The saved Bulk request identity could not be reconciled.', 'fotogenic-ai' ),
			[ 'status' => 409 ]
		);
	}
}
