<?php

namespace Fotogenic_AI;

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

/**
 * Validate and dispatch one canonical Single Studio start.
 *
 * Workflow services own durable idempotency, multipart construction, upstream
 * dispatch, and uncertain-start recovery behind this canonical boundary.
 */
final class Universal_Start_Service {
	private const ENVELOPE_FIELDS = [
		'requestId',
		'runItemId',
		'campaignGroupId',
		'itemOrdinal',
		'sourceFingerprint',
		'authority',
		'workflow',
		'sourceType',
		'scope',
		'subject',
		'sourcePolicy',
		'recipe',
		'resultPolicy',
	];

	private Workflow_Draft_Normalizer $normalizer;
	private Workflow_Source_Preflight $source_preflight;
	private Workflow_Preflight $preflight;
	private \Closure $request_state;
	private \Closure $dispatcher;

	public function __construct(
		?Workflow_Draft_Normalizer $normalizer = null,
		?Workflow_Source_Preflight $source_preflight = null,
		?Workflow_Preflight $preflight = null,
		?callable $request_exists = null,
		?callable $dispatcher = null
	) {
		$this->normalizer       = $normalizer ?? new Workflow_Draft_Normalizer();
		$this->source_preflight = $source_preflight ?? new Workflow_Source_Preflight();
		$this->preflight        = $preflight ?? new Workflow_Preflight();
		$this->request_state    = \Closure::fromCallable( $request_exists ?? [ $this, 'durable_request_state' ] );
		$this->dispatcher       = \Closure::fromCallable( $dispatcher ?? [ $this, 'dispatch_to_workflow_service' ] );
	}

	/**
	 * @return array|\WP_Error Browser-safe workflow start response.
	 */
	public function start( \stdClass $envelope, array $trusted_bulk_context = [] ) {
		$migration = Universal_Data_Migration::instance();
		if ( ! $migration->is_ready() ) {
			return $migration->readiness_error();
		}
		$allowed = Workflow_Contract_Guard::reject_forbidden( $envelope );
		if ( is_wp_error( $allowed ) ) {
			return $allowed;
		}

		$members = get_object_vars( $envelope );
		if ( count( self::ENVELOPE_FIELDS ) !== count( $members )
			|| ! empty( array_diff( self::ENVELOPE_FIELDS, array_keys( $members ) ) )
			|| ! empty( array_diff( array_keys( $members ), self::ENVELOPE_FIELDS ) )
		) {
			return $this->invalid_envelope();
		}
		foreach ( [ 'requestId', 'runItemId', 'sourceFingerprint', 'workflow', 'sourceType', 'scope' ] as $field ) {
			if ( ! is_string( $members[ $field ] ?? null ) || '' === $members[ $field ] ) {
				return $this->invalid_envelope();
			}
		}
		$request_id = strtolower( trim( $members['requestId'] ) );
		$run_item_id = strtolower( trim( $members['runItemId'] ) );
		$campaign_group_id = $members['campaignGroupId'] ?? null;
		if ( null !== $campaign_group_id && ! is_string( $campaign_group_id ) ) {
			return $this->invalid_envelope();
		}
		$campaign_group_id = is_string( $campaign_group_id ) ? strtolower( trim( $campaign_group_id ) ) : null;
		$item_ordinal = $members['itemOrdinal'] ?? null;
		$source_fingerprint = strtolower( trim( (string) $members['sourceFingerprint'] ) );
		$trusted_bulk_start = [] !== $trusted_bulk_context;
		$accepted_authority = null;
		if ( null !== ( $members['authority'] ?? null ) ) {
			$accepted_authority = $this->normalize_authority_shape( $members['authority'] );
			if ( is_wp_error( $accepted_authority ) ) {
				return $accepted_authority;
			}
		} elseif ( ! $trusted_bulk_start ) {
			return $this->invalid_envelope();
		}
		if (
			! wp_is_uuid( $request_id )
			|| ! wp_is_uuid( $run_item_id )
			|| ( null !== $campaign_group_id && ! wp_is_uuid( $campaign_group_id ) )
			|| ! is_int( $item_ordinal )
			|| $item_ordinal <= 0
			|| ( ! $trusted_bulk_start && 1 !== $item_ordinal )
			|| 1 !== preg_match( '/^[a-f0-9]{64}$/', $source_fingerprint )
			|| ( 'campaign_set' === $members['workflow'] ? null === $campaign_group_id : null !== $campaign_group_id )
			|| 'single' !== $members['scope']
		) {
			return $this->invalid_envelope();
		}

		$subject = $members['subject'] ?? null;
		if ( ! ( $subject instanceof \stdClass ) ) {
			return $this->invalid_envelope();
		}
		$subject_members = get_object_vars( $subject );
		if (
			[ 'id', 'targetType', 'targetId', 'parentProductId', 'variationId' ] !== array_keys( $subject_members )
			|| ! is_int( $subject_members['id'] )
			|| $subject_members['id'] <= 0
			|| ! is_string( $subject_members['targetType'] )
			|| ! is_int( $subject_members['targetId'] )
			|| ! is_int( $subject_members['parentProductId'] )
			|| ! is_int( $subject_members['variationId'] )
			|| $subject_members['id'] !== $subject_members['targetId']
		) {
			return $this->invalid_envelope();
		}
		foreach ( [ 'sourcePolicy', 'recipe', 'resultPolicy' ] as $field ) {
			if ( ! ( ( $members[ $field ] ?? null ) instanceof \stdClass ) ) {
				return $this->invalid_envelope();
			}
		}

		$draft = $this->draft_from_envelope( $members, $subject_members['id'] );
		$normalized = $this->normalizer->normalize( $draft );
		if ( is_wp_error( $normalized ) ) {
			return $normalized;
		}
		$item_identity = [
			'id'         => $run_item_id,
			'groupId'    => $campaign_group_id,
			'ordinal'    => $item_ordinal,
			'targetHint' => [
				'targetType'      => sanitize_key( $subject_members['targetType'] ),
				'targetId'        => $subject_members['targetId'],
				'parentProductId' => $subject_members['parentProductId'],
				'variationId'     => $subject_members['variationId'],
			],
		];
		$fingerprint_identity = $item_identity;
		$fingerprint_identity['sourceFingerprint'] = $source_fingerprint;
		$fingerprint_payload = [ 'draft' => $normalized, 'runItem' => $fingerprint_identity ];
		$fingerprint = $this->normalizer->fingerprint( $fingerprint_payload );
		if ( is_wp_error( $fingerprint ) ) {
			return $fingerprint;
		}

		$user_id = get_current_user_id();
		if ( $user_id <= 0 ) {
			return Messages::error( 'forbidden', 403 );
		}
		$bulk_context = $this->validate_bulk_context( $trusted_bulk_context, $user_id, $normalized, $request_id, $item_identity );
		if ( is_wp_error( $bulk_context ) ) {
			return $bulk_context;
		}
		$request_state = ( $this->request_state )( $user_id, $normalized['workflow'], $request_id, $fingerprint );
		if ( is_wp_error( $request_state ) ) {
			return $request_state;
		}
		if ( is_array( $request_state ) ) {
			return $request_state;
		}
		if ( ! is_bool( $request_state ) && null !== $request_state ) {
			return Messages::error( 'server_error', 500 );
		}
		$request_exists = true === $request_state;

		$snapshot = $this->source_preflight->build_explicit_snapshot( $normalized );
		if ( is_wp_error( $snapshot ) ) {
			return $snapshot;
		}
		if (
			1 !== count( $snapshot )
			|| $normalized['sourceType'] !== ( $snapshot[0]['subjectType'] ?? '' )
			|| $subject_members['id'] !== ( $snapshot[0]['subjectId'] ?? 0 )
		) {
			return new \WP_Error(
				'fotogenic_ai_invalid_start_snapshot',
				__( 'The Studio start source could not be verified.', 'fotogenic-ai' ),
				[ 'status' => 400 ]
			);
		}
		$current_source_fingerprint = $this->source_preflight->fingerprint_snapshot( $snapshot );
		if ( is_wp_error( $current_source_fingerprint ) ) {
			return $current_source_fingerprint;
		}
		if ( ! hash_equals( $source_fingerprint, $current_source_fingerprint ) ) {
			return new \WP_Error(
				'fotogenic_ai_source_snapshot_changed',
				__( 'The selected source images changed after Review. Review the current images before starting.', 'fotogenic-ai' ),
				[ 'status' => 409 ]
			);
		}
		if ( ! Workflow_Item_Identity::hint_matches( $snapshot[0]['target'] ?? [], $item_identity['targetHint'] ) ) {
			return new \WP_Error(
				'fotogenic_ai_target_identity_changed',
				__( 'The selected target changed before the workflow could start.', 'fotogenic-ai' ),
				[ 'status' => 409 ]
			);
		}
		if (
			'campaign_set' === $normalized['workflow']
			&& (
				! isset( $snapshot[0]['campaignGroupId'], $snapshot[0]['itemOrdinal'] )
				|| ! hash_equals( (string) $campaign_group_id, (string) $snapshot[0]['campaignGroupId'] )
				|| 1 !== (int) $snapshot[0]['itemOrdinal']
			)
		) {
			return new \WP_Error(
				'fotogenic_ai_campaign_group_identity_changed',
				__( 'The selected Campaign group changed before the workflow could start.', 'fotogenic-ai' ),
				[ 'status' => 409 ]
			);
		}
		$item_identity['target']      = $snapshot[0]['target'];
		$item_identity['sourceRoles'] = $snapshot[0]['sourceRoles'];
		$campaign_group = 'campaign_set' === $normalized['workflow']
			? ( $normalized['sourcePolicy']['groups'][0] ?? null )
			: null;
		$item_identity['destination'] = is_array( $campaign_group )
			? (array) ( $campaign_group['resultPolicy'] ?? [] )
			: (array) $normalized['resultPolicy'];

		$verified_pricing = [];
		if ( ! $request_exists ) {
			$preflight = $trusted_bulk_start
				? $this->preflight->evaluate( $draft )
				: $this->preflight->evaluate( $draft, $accepted_authority );
			if ( is_wp_error( $preflight ) ) {
				return $preflight;
			}
			if ( (float) ( $preflight['balanceAfter'] ?? -1 ) < 0 ) {
				return new \WP_Error(
					'fotogenic_ai_insufficient_credits',
					__( 'There are not enough credits to start this workflow.', 'fotogenic-ai' ),
					[
						'status'      => 402,
						'balance'     => (float) ( $preflight['balance'] ?? 0 ),
						'totalCredits'=> (float) ( $preflight['totalCredits'] ?? 0 ),
					]
				);
			}
			if ( $trusted_bulk_start && ! $this->bulk_authority_matches( $bulk_context, $preflight ) ) {
				return new \WP_Error(
					'fotogenic_ai_bulk_authority_changed',
					__( 'The connected account, API environment, pricing, or configuration no longer matches the accepted Bulk review.', 'fotogenic-ai' ),
					[ 'status' => 409, 'state' => 'review_changed' ]
				);
			}
			$verified_pricing = is_array( $preflight['pricing'] ?? null ) ? $preflight['pricing'] : [];
		}

		$result = ( $this->dispatcher )( $user_id, $request_id, $normalized, $snapshot[0], $fingerprint, $bulk_context, $item_identity, $verified_pricing );
		if ( is_wp_error( $result ) ) {
			return $result;
		}
		if ( $result instanceof \WP_REST_Response ) {
			$result = $result->get_data();
		}
		if ( ! is_array( $result ) ) {
			return Messages::error( 'server_error', 500 );
		}

		return $result;
	}

	private function draft_from_envelope( array $members, int $subject_id ): \stdClass {
		return (object) [
			'schemaVersion' => Workflow_Registry::SCHEMA_VERSION,
			'workflow'      => $members['workflow'],
			'sourceType'    => $members['sourceType'],
			'scope'         => $members['scope'],
			'selection'     => (object) [
				'strategy'    => 'explicit',
				'selectedIds' => [ $subject_id ],
				'excludedIds' => [],
				'query'       => new \stdClass(),
			],
			'sourcePolicy'  => $members['sourcePolicy'],
			'recipe'        => $members['recipe'],
			'resultPolicy'  => $members['resultPolicy'],
		];
	}

	/** @return array|\WP_Error|null */
	private function durable_request_state( int $user_id, string $workflow, string $request_id, string $fingerprint ) {
		return ( new Workflow_Start_Coordinator() )->replay_universal_request( $user_id, $workflow, $request_id, $fingerprint );
	}

	/**
	 * Map the generic command to typed workflow services. No REST loopback
	 * or upstream endpoint metadata is exposed to the browser.
	 *
	 * @return array|\WP_REST_Response|\WP_Error
	 */
	private function dispatch_to_workflow_service( int $user_id, string $request_id, array $draft, array $source, string $fingerprint, array $bulk_context = [], array $item_identity = [], array $trusted_pricing = [] ) {
		$workflow      = (string) $draft['workflow'];
		$source_type   = (string) $draft['sourceType'];
		$subject_id    = (int) $source['subjectId'];
		$attachment_ids = array_values( array_map( 'absint', (array) ( $source['attachmentIds'] ?? [] ) ) );
		$recipe        = (array) $draft['recipe'];
		if ( 'campaign_set' === $workflow ) {
			$campaign_group = $draft['sourcePolicy']['groups'][0] ?? null;
			if ( ! is_array( $campaign_group ) || ! is_array( $campaign_group['recipe'] ?? null ) ) {
				return Messages::error( 'invalid_request', 400 );
			}
			$recipe = (array) $campaign_group['recipe'];
		}
		$trusted_item_context = [
			'run_item_uuid'       => (string) ( $item_identity['id'] ?? '' ),
			'campaign_group_uuid' => (string) ( $item_identity['groupId'] ?? '' ),
			'item_ordinal'        => (int) ( $item_identity['ordinal'] ?? 0 ),
			'target_snapshot'     => (array) ( $item_identity['target'] ?? [] ),
			'source_roles_snapshot'=> (array) ( $item_identity['sourceRoles'] ?? [] ),
			'destination_snapshot'=> (array) ( $item_identity['destination'] ?? [] ),
			'parallel_item_identity'=> ! empty( $bulk_context ),
		];

		if ( 'campaign_set' === $workflow ) {
			return ( new Service_Campaign_Set_Start() )->start_command(
				$user_id,
				[
					'target'               => $source_type,
					'subjectId'            => $subject_id,
					'requestId'            => $request_id,
					'productAttachmentIds' => $attachment_ids,
					'recipe'               => $recipe,
				],
				[
					'refresh_pricing'      => true,
					'universal_fingerprint'=> $fingerprint,
					'bulk_run_uuid'        => (string) ( $bulk_context['bulk_run_uuid'] ?? '' ),
					'bulk_item_id'         => (int) ( $bulk_context['bulk_item_id'] ?? 0 ),
					'item_identity'        => $trusted_item_context,
					'trusted_pricing'      => $trusted_pricing,
				]
			);
		}

		if ( 'increase_resolution' === $workflow ) {
			return ( new Service_Increase_Resolution_Start() )->start_command(
				$user_id,
				[
					'target'             => $source_type,
					'subjectId'          => $subject_id,
					'requestId'          => $request_id,
					'sourceAttachmentId' => (int) ( $attachment_ids[0] ?? 0 ),
				],
				[
					'universal_fingerprint' => $fingerprint,
					'bulk_context'          => $bulk_context,
					'item_identity'         => $trusted_item_context,
					'trusted_pricing'       => $trusted_pricing,
				]
			);
		}

		if ( 'product_images' !== $workflow ) {
			return Messages::error( 'invalid_request', 400 );
		}

		$params = [
			'requestId'               => $request_id,
			'target'                  => $source_type,
			'subjectId'               => $subject_id,
			'customInstructions'      => (string) ( $recipe['customInstructions'] ?? '' ),
			'imagesCount'             => (int) ( $recipe['count'] ?? 0 ),
			'quality'                 => (string) ( $recipe['quality'] ?? '' ),
			'style'                   => (string) ( $recipe['style'] ?? '' ),
			'applyGlobalInstructions' => (bool) ( $recipe['applyGlobalInstructions'] ?? true ),
			'autoConfigUsed'          => (bool) ( $recipe['autoConfigUsed'] ?? false ),
			'autoConfigDescription'   => (string) ( $recipe['autoConfigDescription'] ?? '' ),
			'selections'              => (array) ( $recipe['selections'] ?? [] ),
			'subjectImageId'          => (int) ( $recipe['subjectImageId'] ?? 0 ),
			'inspirationImageId'      => (int) ( $recipe['inspirationImageId'] ?? 0 ),
			'environmentImageId'      => (int) ( $recipe['environmentImageId'] ?? 0 ),
		];
		foreach ( [ 'personPresence', 'includeFace', 'includeBody', 'lookAtCamera' ] as $field ) {
			if ( array_key_exists( $field, $recipe ) ) {
				$params[ $field ] = (bool) $recipe[ $field ];
			}
		}
		if ( 'product' === $source_type ) {
			$featured_id = (int) get_post_thumbnail_id( $subject_id );
			$params['galleryImageIds'] = array_values( array_diff( $attachment_ids, [ $featured_id ] ) );
			$params['excludeFeatured'] = $featured_id > 0 && ! in_array( $featured_id, $attachment_ids, true );
		}

		return ( new Service_Generate_Start() )->start_command(
			$user_id,
			$params,
			[
				'universal_fingerprint' => $fingerprint,
				'bulk_run_uuid'          => (string) ( $bulk_context['bulk_run_uuid'] ?? '' ),
				'bulk_item_id'           => (int) ( $bulk_context['bulk_item_id'] ?? 0 ),
				'item_identity'          => $trusted_item_context,
				'trusted_pricing'        => $trusted_pricing,
			]
		);
	}

	/**
	 * @param mixed $authority Accepted authority payload.
	 * @return array{version:int,fingerprint:string,expiresAt:int}|\WP_Error
	 */
	private function normalize_authority_shape( $authority ) {
		if ( ! ( $authority instanceof \stdClass ) ) {
			return $this->invalid_envelope();
		}
		$members = get_object_vars( $authority );
		if (
			[ 'version', 'fingerprint', 'expiresAt' ] !== array_keys( $members )
			|| Workflow_Accepted_Authority::VERSION !== ( $members['version'] ?? null )
			|| ! is_string( $members['fingerprint'] ?? null )
			|| 1 !== preg_match( '/^[a-f0-9]{64}$/', $members['fingerprint'] )
			|| ! is_int( $members['expiresAt'] ?? null )
		) {
			return $this->invalid_envelope();
		}

		return $members;
	}

	private function bulk_authority_matches( array $bulk_context, array $preflight ): bool {
		$accepted = $bulk_context['accepted_authority'] ?? null;
		$current  = $preflight['authorityEvidence'] ?? null;
		return is_array( $accepted )
			&& is_array( $current )
			&& 1 === (int) ( $accepted['version'] ?? 0 )
			&& 1 === preg_match( '/^[a-f0-9]{64}$/', (string) ( $accepted['contextFingerprint'] ?? '' ) )
			&& 1 === preg_match( '/^[a-f0-9]{64}$/', (string) ( $accepted['configurationFingerprint'] ?? '' ) )
			&& 1 === preg_match( '/^[a-f0-9]{64}$/', (string) ( $accepted['pricingFingerprint'] ?? '' ) )
			&& hash_equals( (string) $accepted['contextFingerprint'], (string) ( $current['contextFingerprint'] ?? '' ) )
			&& hash_equals( (string) $accepted['configurationFingerprint'], (string) ( $current['configurationFingerprint'] ?? '' ) )
			&& hash_equals( (string) $accepted['pricingFingerprint'], (string) ( $current['pricingFingerprint'] ?? '' ) );
	}

	/** @return array{bulk_run_uuid:string,bulk_item_id:int,accepted_authority:array}|array{}|\WP_Error */
	private function validate_bulk_context( array $context, int $user_id, array $draft, string $request_id, array $item_identity ) {
		if ( empty( $context ) ) {
			return [];
		}
		if ( [ 'bulkRunUuid', 'bulkItemId' ] !== array_keys( $context ) ) {
			return $this->invalid_bulk_context();
		}
		$run_uuid = is_string( $context['bulkRunUuid'] ) ? strtolower( trim( $context['bulkRunUuid'] ) ) : '';
		$item_id  = is_int( $context['bulkItemId'] ) ? $context['bulkItemId'] : 0;
		if ( ! wp_is_uuid( $run_uuid ) || $item_id <= 0 ) {
			return $this->invalid_bulk_context();
		}

		$repo = Bulk_Repo::instance();
		$run  = $repo->get_run_by_uuid( $run_uuid, $user_id );
		$item = $repo->get_item_by_id( $item_id, $user_id );
		if (
			! is_array( $run )
			|| ! is_array( $item )
			|| (int) ( $item['run_id'] ?? 0 ) !== (int) ( $run['id'] ?? 0 )
			|| (string) ( $run['workflow'] ?? '' ) !== (string) ( $draft['workflow'] ?? '' )
			|| (string) ( $item['subject_type'] ?? '' ) !== (string) ( $draft['sourceType'] ?? '' )
			|| (int) ( $item['subject_id'] ?? 0 ) !== (int) ( $draft['selection']['selectedIds'][0] ?? 0 )
			|| ! hash_equals( $request_id, strtolower( (string) ( $item['request_id'] ?? '' ) ) )
			|| ! hash_equals( (string) ( $item_identity['id'] ?? '' ), (string) ( $item['run_item_uuid'] ?? '' ) )
			|| (string) ( $item_identity['groupId'] ?? '' ) !== (string) ( $item['campaign_group_uuid'] ?? '' )
			|| (int) ( $item_identity['ordinal'] ?? 0 ) !== (int) ( $item['item_ordinal'] ?? 0 )
		) {
			return $this->invalid_bulk_context();
		}

		$snapshot = json_decode( (string) ( $run['filters_snapshot'] ?? '' ), true );
		$accepted_authority = is_array( $snapshot ) && is_array( $snapshot['acceptedAuthority'] ?? null )
			? $snapshot['acceptedAuthority']
			: [];

		return [
			'bulk_run_uuid' => $run_uuid,
			'bulk_item_id'  => $item_id,
			'accepted_authority' => $accepted_authority,
		];
	}

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

	private function invalid_envelope(): \WP_Error {
		return new \WP_Error(
			'fotogenic_ai_invalid_start_envelope',
			__( 'The Studio start request does not match the canonical Single contract.', 'fotogenic-ai' ),
			[ 'status' => 400 ]
		);
	}
}
