<?php

namespace Fotogenic_AI;

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

/**
 * Authorize and validate explicit WordPress sources without building files.
 */
final class Workflow_Source_Preflight {
	/**
	 * Validate every explicit subject, primary attachment, and reference.
	 *
	 * @return array|\WP_Error Internal counts only; no paths or URLs.
	 */
	public function validate( array $draft ) {
		$snapshot = $this->build_explicit_snapshot( $draft );
		if ( is_wp_error( $snapshot ) ) {
			return $snapshot;
		}
		$fingerprint = $this->fingerprint_snapshot( $snapshot );
		if ( is_wp_error( $fingerprint ) ) {
			return $fingerprint;
		}

		return [
			'totalItems'        => count( $snapshot ),
			'sourceFingerprint' => $fingerprint,
			'primaryCounts' => array_map(
				static fn( array $item ): int => count( $item['attachmentIds'] ),
				$snapshot
			),
		];
	}

	/**
	 * Bind Review to the ordered billable attachment roles and exact target IDs.
	 * Presentation metadata such as Product labels and SKUs is intentionally
	 * excluded so harmless edits do not invalidate an otherwise exact Review.
	 *
	 * @param array<int, array> $snapshot Validated explicit source snapshot.
	 * @return string|\WP_Error SHA-256 fingerprint or a bounded server error.
	 */
	public function fingerprint_snapshot( array $snapshot ) {
		$projection = [];
		foreach ( $snapshot as $item ) {
			$source_roles = is_array( $item['sourceRoles'] ?? null ) ? $item['sourceRoles'] : [];
			$row = [
				'subjectType'  => (string) ( $item['subjectType'] ?? '' ),
				'subjectId'    => (int) ( $item['subjectId'] ?? 0 ),
				'attachmentIds' => array_values( array_map( 'absint', (array) ( $item['attachmentIds'] ?? [] ) ) ),
				'target'       => Workflow_Item_Identity::target_hint( (array) ( $item['target'] ?? [] ) ),
				'sourceRoles'  => [
					'primaryAttachmentId'     => absint( $source_roles['primaryAttachmentId'] ?? 0 ),
					'supportingAttachmentIds' => array_values( array_map( 'absint', (array) ( $source_roles['supportingAttachmentIds'] ?? [] ) ) ),
				],
			];
			if ( isset( $item['campaignGroupId'], $item['itemOrdinal'] ) ) {
				$row['campaignGroupId'] = strtolower( trim( (string) $item['campaignGroupId'] ) );
				$row['itemOrdinal']     = (int) $item['itemOrdinal'];
			}
			$projection[] = $row;
		}
		$encoded = wp_json_encode( $projection, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
		if ( false === $encoded ) {
			return new \WP_Error(
				'fotogenic_ai_source_fingerprint_failed',
				__( 'The selected source snapshot could not be verified.', 'fotogenic-ai' ),
				[ 'status' => 500 ]
			);
		}

		return hash( 'sha256', $encoded );
	}

	/**
	 * Build the immutable, ordered source snapshot used by a later Bulk owner.
	 *
	 * Query selection must first be expanded server-side and normalized as an
	 * explicit selection. The returned rows contain WordPress IDs only.
	 *
	 * @return array<int, array{subjectType:string, subjectId:int, attachmentIds:int[], target:array, sourceRoles:array}>|\WP_Error
	 */
	public function build_explicit_snapshot( array $draft ) {
		$workflow = (string) ( $draft['workflow'] ?? '' );
		$source   = (string) ( $draft['sourceType'] ?? '' );
		$scope    = (string) ( $draft['scope'] ?? '' );
		$selection = isset( $draft['selection'] ) && is_array( $draft['selection'] ) ? $draft['selection'] : [];
		if ( 'explicit' !== ( $selection['strategy'] ?? '' ) ) {
			return $this->invalid( __( 'This preflight requires an immutable explicit selection.', 'fotogenic-ai' ) );
		}

		$subject_ids = isset( $selection['selectedIds'] ) && is_array( $selection['selectedIds'] )
			? $selection['selectedIds']
			: [];
		$policy = isset( $draft['sourcePolicy'] ) && is_array( $draft['sourcePolicy'] ) ? $draft['sourcePolicy'] : [];
		$groups = 'campaign_set' === $workflow && is_array( $policy['groups'] ?? null )
			? $policy['groups']
			: [];
		$total_items  = ! empty( $groups ) ? count( $groups ) : count( $subject_ids );
		$definition   = Workflow_Registry::get( $workflow );
		$sources      = null !== $definition ? $definition->get_sources() : [];
		$selection_limits = $sources[ $source ]['selectionCardinality'][ $scope ] ?? [];
		$minimum      = (int) ( $selection_limits['minimum'] ?? 0 );
		$maximum      = (int) ( $selection_limits['maximum'] ?? 0 );
		if ( $total_items < $minimum || $total_items > $maximum ) {
			return $this->invalid( __( 'The selected subject count is outside the workflow limits.', 'fotogenic-ai' ) );
		}

		$policy_attachment_ids = isset( $policy['attachmentIds'] ) && is_array( $policy['attachmentIds'] )
			? $policy['attachmentIds']
			: [];
		$source_mode = isset( $policy['sourceMode'] ) && is_string( $policy['sourceMode'] )
			? $policy['sourceMode']
			: '';
		if ( 'bulk' === $scope && ! empty( $policy_attachment_ids ) ) {
			return $this->invalid( __( 'Bulk source files must come from each immutable item snapshot.', 'fotogenic-ai' ) );
		}

		$reference_ids = $this->get_reference_attachment_ids( $workflow, (array) ( $draft['recipe'] ?? [] ) );
		foreach ( $reference_ids as $attachment_id ) {
			$valid = $this->validate_image_attachment( $attachment_id );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}

		$source_rows = ! empty( $groups )
			? array_map( static fn( array $group ): array => [ 'subjectId' => (int) $group['subjectId'], 'group' => $group ], $groups )
			: array_map( static fn( int $subject_id ): array => [ 'subjectId' => $subject_id, 'group' => null ], $subject_ids );
		if ( 'media' === $source && ! empty( $groups ) ) {
			$selected_primaries = array_values( array_unique( array_map( 'absint', $subject_ids ) ) );
			foreach ( $groups as $group ) {
				if ( ! empty( array_intersect( $selected_primaries, (array) ( $group['supportingAttachmentIds'] ?? [] ) ) ) ) {
					return $this->invalid( __( 'A selected Media primary cannot be reused as another Campaign group supporting image.', 'fotogenic-ai' ) );
				}
			}
		}
		$snapshot = [];
		foreach ( $source_rows as $source_row ) {
			$subject_id = (int) $source_row['subjectId'];
			$group = is_array( $source_row['group'] ) ? $source_row['group'] : null;
			$target = Workflow_Item_Identity::resolve_target( $source, (int) $subject_id );
			if ( is_wp_error( $target ) ) {
				return $target;
			}
			if ( false === ( $target['eligible'] ?? false ) ) {
				return new \WP_Error(
					'fotogenic_ai_source_ineligible',
					__( 'The selected exact target is no longer eligible for this workflow.', 'fotogenic-ai' ),
					[ 'status' => 422 ]
				);
			}
			if ( null !== $group && ! Workflow_Item_Identity::hint_matches( $target, (array) $group['target'] ) ) {
				return new \WP_Error(
					'fotogenic_ai_target_identity_changed',
					__( 'A selected Campaign target changed before preflight.', 'fotogenic-ai' ),
					[ 'status' => 409 ]
				);
			}
			$primary_ids = null !== $group
				? $this->validate_explicit_campaign_group_attachments( $source, $subject_id, $group )
				: $this->resolve_primary_attachment_ids(
					$workflow,
					$source,
					$scope,
					(int) $subject_id,
					$policy_attachment_ids,
					$source_mode,
					count( $reference_ids )
				);
			if ( is_wp_error( $primary_ids ) ) {
				return $primary_ids;
			}

			$cardinality = $this->validate_cardinality( $workflow, $source, $scope, count( $primary_ids ), count( $reference_ids ) );
			if ( is_wp_error( $cardinality ) ) {
				return $cardinality;
			}
			$eligible = $this->apply_source_eligibility_filter(
				[
					'workflow'      => $workflow,
					'sourceType'    => $source,
					'scope'         => $scope,
					'subjectId'     => (int) $subject_id,
					'attachmentIds' => $primary_ids,
				]
			);
			if ( is_wp_error( $eligible ) ) {
				return $eligible;
			}
			$snapshot_row = [
				'subjectType'  => $source,
				'subjectId'    => (int) $subject_id,
				'attachmentIds' => $primary_ids,
				'target'        => $target,
				'sourceRoles'   => [
					'primaryAttachmentId'    => (int) ( $primary_ids[0] ?? 0 ),
					'supportingAttachmentIds'=> array_values( array_slice( $primary_ids, 1 ) ),
				],
			];
			if ( null !== $group ) {
				$snapshot_row['campaignGroupId'] = (string) $group['id'];
				$snapshot_row['itemOrdinal'] = (int) $group['ordinal'];
			}
			$snapshot[] = $snapshot_row;
		}

		return $snapshot;
	}

	/** @return int[]|\WP_Error */
	private function validate_explicit_campaign_group_attachments( string $source, int $subject_id, array $group ) {
		$attachment_ids = array_merge(
			[ (int) ( $group['primaryAttachmentId'] ?? 0 ) ],
			array_values( (array) ( $group['supportingAttachmentIds'] ?? [] ) )
		);
		if ( empty( $attachment_ids ) || in_array( 0, $attachment_ids, true ) || count( $attachment_ids ) !== count( array_unique( $attachment_ids ) ) ) {
			return $this->invalid( __( 'A Campaign group contains invalid source roles.', 'fotogenic-ai' ) );
		}
		foreach ( $attachment_ids as $attachment_id ) {
			$valid = $this->validate_image_attachment( (int) $attachment_id );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
		if ( 'media' === $source ) {
			return $subject_id === (int) $attachment_ids[0]
				? $attachment_ids
				: $this->invalid( __( 'A Media Campaign group must keep its selected source as primary.', 'fotogenic-ai' ) );
		}
		if ( 'product' !== $source || ! fotogenic_ai_woocommerce_available() ) {
			return new \WP_Error( 'woocommerce_not_active', __( 'WooCommerce is required for Product sources.', 'fotogenic-ai' ), [ 'status' => 503 ] );
		}
		$product = wc_get_product( $subject_id );
		if ( ! $product instanceof \WC_Product ) {
			return $this->invalid( __( 'The selected Product target is unavailable.', 'fotogenic-ai' ) );
		}
		$is_variation = $product instanceof \WC_Product_Variation || $product->is_type( 'variation' );
		$available = array_values(
			array_unique(
				array_filter(
					array_merge(
						[ (int) $product->get_image_id( 'edit' ) ],
						$is_variation ? [] : array_map( 'absint', (array) $product->get_gallery_image_ids( 'edit' ) )
					)
				)
			)
		);
		return empty( array_diff( $attachment_ids, $available ) )
			? $attachment_ids
			: $this->invalid( __( 'Campaign group source images must belong to the exact Product target.', 'fotogenic-ai' ) );
	}

	/**
	 * Let extensions reject an already core-validated source without allowing
	 * them to replace its identity, files, ownership, or cardinality.
	 *
	 * @param array{workflow:string,sourceType:string,scope:string,subjectId:int,attachmentIds:int[]} $context Validated source context.
	 * @return true|\WP_Error
	 */
	private function apply_source_eligibility_filter( array $context ) {
		if ( ! function_exists( 'apply_filters' ) ) {
			return true;
		}

		/**
		 * Filter whether an already validated Studio source remains eligible.
		 *
		 * Return strict boolean true to retain eligibility. Any other result
		 * denies the source with a bounded public error. The context is read-only
		 * metadata and contains no paths, URLs, prompts, credentials, or payloads.
		 *
		 * @param bool  $eligible Whether the source remains eligible.
		 * @param array $context  Validated workflow/source/attachment identifiers.
		 */
		$eligible = apply_filters( 'fotogenic_ai_studio_source_eligibility', true, $context );
		if ( true !== $eligible ) {
			return new \WP_Error(
				'fotogenic_ai_source_ineligible',
				__( 'The selected source is not eligible for this workflow.', 'fotogenic-ai' ),
				[ 'status' => 422 ]
			);
		}

		return true;
	}

	/**
	 * @return int[]
	 */
	private function get_reference_attachment_ids( string $workflow, array $recipe ): array {
		$keys = [];
		if ( 'product_images' === $workflow ) {
			$keys = [ 'subjectImageId', 'inspirationImageId', 'environmentImageId' ];
		} elseif ( 'campaign_set' === $workflow ) {
			$keys = [ 'subjectAttachmentId', 'environmentAttachmentId' ];
		}

		$ids = [];
		foreach ( $keys as $key ) {
			if ( isset( $recipe[ $key ] ) && is_int( $recipe[ $key ] ) && $recipe[ $key ] > 0 ) {
				$ids[] = $recipe[ $key ];
			}
		}

		// Count file roles, not unique attachment IDs: one attachment selected for
		// two roles still creates two multipart file parts at the start boundary.
		return $ids;
	}

	/**
	 * @param int[] $policy_attachment_ids Explicit Single source attachments.
	 * @return int[]|\WP_Error
	 */
	private function resolve_primary_attachment_ids(
		string $workflow,
		string $source,
		string $scope,
		int $subject_id,
		array $policy_attachment_ids,
		string $source_mode,
		int $reference_count
	) {
		if ( 'product' === $source ) {
			return $this->resolve_product_attachments( $workflow, $scope, $subject_id, $policy_attachment_ids, $source_mode, $reference_count );
		}
		if ( 'media' === $source ) {
			return $this->resolve_media_attachments( $workflow, $scope, $subject_id, $policy_attachment_ids );
		}
		if ( in_array( $source, [ 'post', 'page' ], true ) ) {
			return $this->resolve_content_attachment( $source, $scope, $subject_id, $policy_attachment_ids );
		}

		return $this->invalid( __( 'The selected source type is not supported.', 'fotogenic-ai' ) );
	}

	/**
	 * @param int[] $policy_attachment_ids Explicit Single source attachments.
	 * @return int[]|\WP_Error
	 */
	private function resolve_product_attachments(
		string $workflow,
		string $scope,
		int $product_id,
		array $policy_attachment_ids,
		string $source_mode,
		int $reference_count
	) {
		if (
			! function_exists( __NAMESPACE__ . '\\fotogenic_ai_woocommerce_available' )
			|| ! fotogenic_ai_woocommerce_available()
		) {
			return new \WP_Error(
				'woocommerce_not_active',
				__( 'WooCommerce is required for Product sources.', 'fotogenic-ai' ),
				[ 'status' => 503 ]
			);
		}
		// phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce maps this meta capability for exact Products and variations.
		if ( $product_id <= 0 || ! current_user_can( 'edit_product', $product_id ) ) {
			return $this->forbidden();
		}

		$product = wc_get_product( $product_id );
		if ( ! is_object( $product ) || ! method_exists( $product, 'get_image_id' ) || ! method_exists( $product, 'get_gallery_image_ids' ) ) {
			return new \WP_Error(
				'fotogenic_ai_invalid_product',
				__( 'The selected product is unavailable.', 'fotogenic-ai' ),
				[ 'status' => 404 ]
			);
		}

		$is_variation = $product instanceof \WC_Product_Variation || $product->is_type( 'variation' );
		$featured_id = (int) $product->get_image_id( 'edit' );
		$gallery_ids = $is_variation
			? []
			: array_values( array_unique( array_filter( array_map( 'absint', (array) $product->get_gallery_image_ids( 'edit' ) ) ) ) );
		$available = array_values(
			array_unique(
				array_filter(
					array_merge(
						[ $featured_id ],
						$gallery_ids
					)
				)
			)
		);
		if ( empty( $available ) ) {
			return $this->no_source_image();
		}

		$uses_generate_source_mode = 'product_images' === $workflow && '' !== $source_mode;
		if ( 'single' === $scope && ! $uses_generate_source_mode ) {
			if ( empty( $policy_attachment_ids ) ) {
				return $this->invalid( __( 'Choose at least one Product source image.', 'fotogenic-ai' ) );
			}
			if ( ! empty( array_diff( $policy_attachment_ids, $available ) ) ) {
				return $this->invalid( __( 'Selected Product source attachments do not belong to that product.', 'fotogenic-ai' ) );
			}
			$primary_ids = $policy_attachment_ids;
		} else {
			if ( 'featured' === $source_mode ) {
				$primary_ids = $featured_id > 0 ? [ $featured_id ] : [];
			} elseif ( 'gallery_only' === $source_mode ) {
				$primary_ids = $gallery_ids;
			} elseif ( 'featured_gallery' === $source_mode ) {
				$primary_ids = array_values( array_unique( array_filter( array_merge( [ $featured_id ], $gallery_ids ) ) ) );
			} else {
				return $this->invalid( __( 'Select a supported Product image source mode.', 'fotogenic-ai' ) );
			}

			$definition = Workflow_Registry::get( $workflow );
			$sources    = null !== $definition ? $definition->get_sources() : [];
			$scope_key  = 'single' === $scope ? 'single' : 'bulkItem';
			$maximum    = (int) ( $sources['product']['inputCardinality'][ $scope_key ]['primary']['maximum'] ?? 0 );
			if ( 'product_images' === $workflow ) {
				$total_maximum = (int) ( $sources['product']['inputCardinality'][ $scope_key ]['total']['maximum'] ?? 0 );
				$maximum       = min( $maximum, max( 0, $total_maximum - $reference_count ) );
			}
			$primary_ids = $maximum > 0 ? array_slice( $primary_ids, 0, $maximum ) : [];
			if ( empty( $primary_ids ) ) {
				return $this->no_source_image();
			}
		}

		foreach ( $primary_ids as $attachment_id ) {
			$valid = $this->validate_image_attachment( $attachment_id );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}

		return $primary_ids;
	}

	/**
	 * @param int[] $policy_attachment_ids Explicit Single source attachments.
	 * @return int[]|\WP_Error
	 */
	private function resolve_media_attachments( string $workflow, string $scope, int $subject_id, array $policy_attachment_ids ) {
		$valid = $this->validate_image_attachment( $subject_id );
		if ( is_wp_error( $valid ) ) {
			return $valid;
		}

		if ( 'single' !== $scope || empty( $policy_attachment_ids ) ) {
			return [ $subject_id ];
		}
		if ( $subject_id !== (int) $policy_attachment_ids[0] ) {
			return $this->invalid( __( 'The first Media source attachment must match the selected subject.', 'fotogenic-ai' ) );
		}
		if ( 'campaign_set' !== $workflow && [ $subject_id ] !== $policy_attachment_ids ) {
			return $this->invalid( __( 'This workflow accepts exactly one Media source attachment.', 'fotogenic-ai' ) );
		}

		foreach ( $policy_attachment_ids as $attachment_id ) {
			$valid = $this->validate_image_attachment( $attachment_id );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}

		return $policy_attachment_ids;
	}

	/**
	 * @param int[] $policy_attachment_ids Explicit Single source attachments.
	 * @return int[]|\WP_Error
	 */
	private function resolve_content_attachment( string $source, string $scope, int $subject_id, array $policy_attachment_ids ) {
		$primitive_capability = 'post' === $source ? 'edit_posts' : 'edit_pages';
		if ( ! current_user_can( $primitive_capability ) || ! current_user_can( 'edit_post', $subject_id ) ) {
			return $this->forbidden();
		}

		$post = get_post( $subject_id );
		if ( ! ( $post instanceof \WP_Post ) || $source !== $post->post_type ) {
			return new \WP_Error(
				'fotogenic_ai_invalid_subject',
				__( 'The selected WordPress subject is unavailable.', 'fotogenic-ai' ),
				[ 'status' => 404 ]
			);
		}

		$featured_id = (int) get_post_thumbnail_id( $subject_id );
		if ( $featured_id <= 0 ) {
			return $this->no_source_image();
		}
		if ( 'single' === $scope && ! empty( $policy_attachment_ids ) && [ $featured_id ] !== $policy_attachment_ids ) {
			return $this->invalid( __( 'The selected source must match the subject featured image.', 'fotogenic-ai' ) );
		}
		$valid = $this->validate_image_attachment( $featured_id );
		if ( is_wp_error( $valid ) ) {
			return $valid;
		}

		return [ $featured_id ];
	}

	/** @return bool|\WP_Error */
	private function validate_image_attachment( int $attachment_id ) {
		if ( $attachment_id <= 0 || ! current_user_can( 'upload_files' ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
			return $this->forbidden();
		}
		$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 editable image attachment.', 'fotogenic-ai' ),
				[ 'status' => 400 ]
			);
		}
		$resolved = Image_Source_Resolver::instance()->get_image_file_for_workflow_attachment( $attachment_id );
		if ( is_wp_error( $resolved ) ) {
			return $resolved;
		}

		return true;
	}

	/** @return bool|\WP_Error */
	private function validate_cardinality(
		string $workflow,
		string $source,
		string $scope,
		int $primary_count,
		int $reference_count
	) {
		$definition = Workflow_Registry::get( $workflow );
		$sources    = null !== $definition ? $definition->get_sources() : [];
		$scope_key  = 'single' === $scope ? 'single' : 'bulkItem';
		$limits     = $sources[ $source ]['inputCardinality'][ $scope_key ] ?? [];
		$primary    = isset( $limits['primary'] ) && is_array( $limits['primary'] ) ? $limits['primary'] : [];
		$minimum    = (int) ( $primary['minimum'] ?? 0 );
		$maximum    = (int) ( $primary['maximum'] ?? 0 );
		if ( $primary_count < $minimum || $primary_count > $maximum ) {
			return $this->invalid( __( 'The selected source image count is outside the workflow limits.', 'fotogenic-ai' ) );
		}

		if ( 'product_images' === $workflow ) {
			$total_maximum = (int) ( $limits['total']['maximum'] ?? 0 );
			if ( $primary_count + $reference_count > $total_maximum ) {
				return new \WP_Error(
					'fotogenic_ai_too_many_generate_inputs',
					__( 'The selected Product images and optional references exceed the current Generate Images limits.', 'fotogenic-ai' ),
					[ 'status' => 400 ]
				);
			}
		}

		return true;
	}

	private function forbidden(): \WP_Error {
		return new \WP_Error(
			'fotogenic_ai_forbidden_source',
			__( 'You do not have permission to use one of the selected sources.', 'fotogenic-ai' ),
			[ 'status' => 403 ]
		);
	}

	private function no_source_image(): \WP_Error {
		return new \WP_Error(
			'fotogenic_ai_no_source_image',
			__( 'Every selected subject needs an eligible source image.', 'fotogenic-ai' ),
			[ 'status' => 400 ]
		);
	}

	private function invalid( string $message ): \WP_Error {
		return new \WP_Error( 'fotogenic_ai_invalid_studio_source', $message, [ 'status' => 400 ] );
	}
}
