<?php

namespace Fotogenic_AI;

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

/**
 * Server-authoritative matrix and browser-safe metadata for Studio workflows.
 *
	 * The registry deliberately owns no endpoint, multipart, pricing, persistence,
	 * retry, or finalization behavior. Those protected boundaries remain in their
	 * owning services.
 */
final class Workflow_Registry {
	public const SCHEMA_VERSION = 1;

	private function __construct() {}

	/**
	 * Return all built-in immutable definitions keyed by workflow identifier.
	 *
	 * @return array<string, Workflow_Definition_Interface>
	 */
	public static function all(): array {
		static $definitions = null;

		if ( null === $definitions ) {
			$definitions = self::build_definitions();
		}

		return $definitions;
	}

	public static function get( string $workflow ): ?Workflow_Definition_Interface {
		$workflow = strtolower( trim( $workflow ) );

		return self::all()[ $workflow ] ?? null;
	}

	public static function supports( string $workflow, string $source_type, string $scope ): bool {
		$definition = self::get( $workflow );

		return null !== $definition && $definition->supports( $source_type, $scope );
	}

	/**
	 * Determine whether a source/scope pair may be admitted as a Studio run.
	 */
	public static function supports_new_run( string $workflow, string $source_type, string $scope ): bool {
		$workflow = strtolower( trim( $workflow ) );
		$scope    = strtolower( trim( $scope ) );

		return self::supports( $workflow, $source_type, $scope );
	}

	/**
	 * Return the complete JSON-safe projection for a later Studio manifest route.
	 */
	public static function get_browser_projection(): array {
		$projection = [
			'schemaVersion'     => self::SCHEMA_VERSION,
			'operationalLimits' => [
				'maxRunItems'       => Workflow_Operational_Limits::MAX_RUN_ITEMS,
				'maxOutputsPerItem' => Workflow_Operational_Limits::MAX_OUTPUTS_PER_ITEM,
				'maxRunOutputs'     => Workflow_Operational_Limits::MAX_RUN_OUTPUTS,
			],
			'workflows'         => array_values(
				array_map(
					static fn( Workflow_Definition_Interface $definition ): array => $definition->get_browser_projection(),
					self::all()
				)
			),
		];
		if ( function_exists( 'apply_filters' ) ) {
			/**
			 * Filter browser-safe Studio presentation copy.
			 *
			 * Only label, description, help, and placeholder strings are accepted
			 * from the filtered value. The workflow/source matrix, recipe contract,
			 * pricing, endpoints, ownership, and cardinality remain immutable.
			 *
			 * @param array $projection Built-in browser projection.
			 */
			$filtered   = apply_filters( 'fotogenic_ai_studio_workflow_definitions', $projection );
			$projection = self::merge_safe_presentation_copy( $projection, $filtered );
		}

		return self::normalize_json_object_fields( $projection );
	}

	/**
	 * Preserve strict object schemas when PHP serializes an optionless recipe.
	 */
	private static function normalize_json_object_fields( array $projection ): array {
		foreach ( $projection['workflows'] as &$workflow ) {
			if ( [] === ( $workflow['recipe']['fields'] ?? null ) ) {
				$workflow['recipe']['fields'] = (object) [];
			}
		}
		unset( $workflow );

		return $projection;
	}

	/**
	 * Copy bounded presentation strings onto the immutable built-in projection.
	 * @param mixed $filtered Filtered value.
	 */
	private static function merge_safe_presentation_copy( array $core, $filtered ): array {
		if ( ! is_array( $filtered ) || ! isset( $filtered['workflows'] ) || ! is_array( $filtered['workflows'] ) ) {
			return $core;
		}

		$filtered_workflows = [];
		foreach ( $filtered['workflows'] as $workflow ) {
			if ( is_array( $workflow ) && is_string( $workflow['id'] ?? null ) ) {
				$filtered_workflows[ $workflow['id'] ] = $workflow;
			}
		}

		foreach ( $core['workflows'] as &$workflow ) {
			$filtered_workflow = $filtered_workflows[ $workflow['id'] ] ?? null;
			if ( ! is_array( $filtered_workflow ) ) {
				continue;
			}
			self::copy_presentation_strings( $workflow, $filtered_workflow );

			$filtered_sources = [];
			foreach ( $filtered_workflow['sources'] ?? [] as $source ) {
				if ( is_array( $source ) && is_string( $source['id'] ?? null ) ) {
					$filtered_sources[ $source['id'] ] = $source;
				}
			}
			foreach ( $workflow['sources'] as &$source ) {
				$filtered_source = $filtered_sources[ $source['id'] ] ?? null;
				if ( is_array( $filtered_source ) ) {
					self::copy_presentation_strings( $source, $filtered_source );
				}
			}
			unset( $source );

			self::merge_recipe_presentation_copy(
				$workflow['recipe']['fields'],
				$filtered_workflow['recipe']['fields'] ?? []
			);
		}
		unset( $workflow );

		return $core;
	}

	/**
	 * Recursively copy presentation strings for existing recipe fields only.
	 * @param mixed $filtered_fields Filtered field definitions.
	 */
	private static function merge_recipe_presentation_copy( array &$core_fields, $filtered_fields ): void {
		if ( ! is_array( $filtered_fields ) ) {
			return;
		}
		foreach ( $core_fields as $field_name => &$field ) {
			$filtered_field = $filtered_fields[ $field_name ] ?? null;
			if ( ! is_array( $filtered_field ) ) {
				continue;
			}
			self::copy_presentation_strings( $field, $filtered_field );
			if ( isset( $field['fields'] ) && is_array( $field['fields'] ) ) {
				self::merge_recipe_presentation_copy( $field['fields'], $filtered_field['fields'] ?? [] );
			}
		}
		unset( $field );
	}

	/**
	 * Copy only documented, bounded UI text values.
	 */
	private static function copy_presentation_strings( array &$target, array $source ): void {
		$limits = [
			'label'       => 120,
			'description' => 320,
			'help'        => 320,
			'placeholder' => 240,
		];
		foreach ( $limits as $key => $maximum ) {
			if ( ! isset( $source[ $key ] ) || ! is_string( $source[ $key ] ) ) {
				continue;
			}
			$value = trim( wp_strip_all_tags( $source[ $key ] ) );
			if ( '' !== $value ) {
				$target[ $key ] = function_exists( 'mb_substr' )
					? mb_substr( $value, 0, $maximum )
					: substr( $value, 0, $maximum );
			}
		}
	}

	/**
	 * @return array<string, Workflow_Definition_Interface>
	 */
	private static function build_definitions(): array {
		return [
			'product_images'      => new Workflow_Definition(
				'product_images',
				[
					'product' => self::source( true, 10, 10, 13, 13, self::product_bulk_source_modes() ),
					'media'   => self::source( false, 1, 1, 4, 4 ),
					'post'    => self::source( false, 1, 1, 4, 4 ),
					'page'    => self::source( false, 1, 1, 4, 4 ),
				],
				self::generate_images_recipe(),
				[
					'minimum'      => 1,
					'maximum'      => 6,
					'controlledBy' => 'count',
				]
			),
			'campaign_set'       => new Workflow_Definition(
				'campaign_set',
				[
					'product' => self::source( true, 10, 10, null, null, self::product_bulk_source_modes() ),
					'media'   => self::source( false, 10, 10 ),
				],
				self::campaign_set_recipe(),
				[
					'minimum'      => 2,
					'maximum'      => 6,
					'controlledBy' => 'count',
				]
			),
			'increase_resolution' => new Workflow_Definition(
				'increase_resolution',
				[
					'product' => self::source( true, 1, 1, 1, 1, [ 'featured' ] ),
					'media'   => self::source( false, 1, 1, 1, 1 ),
				],
				[
					'type'                 => 'object',
					'additionalProperties' => false,
					'fields'               => [],
				],
				[
					'minimum' => 1,
					'maximum' => 1,
				]
			),
		];
	}

	/**
	 * Build one source pair shared by Single and Bulk.
	 */
	private static function source(
		bool $requires_woocommerce,
		int $single_primary_maximum,
		int $bulk_primary_maximum,
		?int $single_total_maximum = null,
		?int $bulk_total_maximum = null,
		array $bulk_source_modes = [],
		bool $supports_bulk = true
	): array {
		$single_cardinality = [
			'primary' => [
				'minimum' => 1,
				'maximum' => $single_primary_maximum,
			],
		];
		if ( null !== $single_total_maximum ) {
			$single_cardinality['total'] = [
				'minimum' => 1,
				'maximum' => $single_total_maximum,
			];
		}
		$bulk_cardinality = [
			'primary' => [
				'minimum' => 1,
				'maximum' => $bulk_primary_maximum,
			],
		];
		if ( null !== $bulk_total_maximum ) {
			$bulk_cardinality['total'] = [
				'minimum' => 1,
				'maximum' => $bulk_total_maximum,
			];
		}

		$source = [
			'scopes'              => $supports_bulk ? [ 'single', 'bulk' ] : [ 'single' ],
			'requiresWooCommerce' => $requires_woocommerce,
			'sourcePolicy'        => [
				'bulkModes' => array_values( $bulk_source_modes ),
			],
			'selectionCardinality' => [
				'single' => [
					'minimum' => 1,
					'maximum' => 1,
				],
				'bulk'   => [
					'minimum' => 1,
					'maximum' => Workflow_Operational_Limits::MAX_RUN_ITEMS,
				],
			],
			'inputCardinality'    => [
				'single'   => $single_cardinality,
				'bulkItem' => $bulk_cardinality,
			],
		];
		if ( ! $supports_bulk ) {
			$source['sourcePolicy']['bulkModes'] = [];
			unset( $source['selectionCardinality']['bulk'], $source['inputCardinality']['bulkItem'] );
		}

		return $source;
	}

	/**
	 * Internal Product image-selection modes. These values never cross the
	 * upstream request boundary; the source resolver converts them to IDs.
	 *
	 * @return string[]
	 */
	private static function product_bulk_source_modes(): array {
		return [ 'featured', 'featured_gallery', 'gallery_only' ];
	}

	private static function generate_images_recipe(): array {
		$selection_fields = [];
		foreach (
			[
				'emotion',
				'styling',
				'clothing',
				'action',
				'expression',
				'grip',
				'framing',
				'setting',
				'focus',
				'bgElements',
				'lighting',
				'atmosphere',
				'angle',
				'shotSize',
				'aspectRatio',
			] as $selection_key
		) {
			$selection_fields[ $selection_key ] = [
				'type'                 => 'object',
				'required'             => false,
				'additionalProperties' => false,
				'fields'               => [
					'id' => [
						'type'     => 'string',
						'required' => true,
						'omission' => 'empty',
					],
				],
				'optionSource'         => 'configuration',
				'omission'             => 'empty',
			];
		}
		$selection_fields['aspectRatio']['omission'] = 'missing-empty-or-auto';

		return [
			'type'                 => 'object',
			'additionalProperties' => false,
			'fields'               => [
				'count' => [
					'type'     => 'integer',
					'required' => true,
					'minimum'  => 1,
					'maximum'  => 6,
					'omission' => 'never',
				],
				'quality' => [
					'type'         => 'string',
					'required'     => true,
					'default'      => '2K',
					'optionSource' => 'image_generation_qualities',
					'omission'     => 'never',
				],
				'style' => [
					'type'         => 'string',
					'required'     => false,
					'maxLength'    => 120,
					'optionSource' => 'styles',
					'omission'     => 'empty',
				],
				'customInstructions' => [
					'type'      => 'string',
					'required'  => false,
					'maxLength' => 5000,
					'omission'  => 'empty',
				],
				'applyGlobalInstructions' => [
					'type'     => 'boolean',
					'required' => false,
					'default'  => false,
					'omission' => 'missing',
				],
				'autoConfigUsed' => [
					'type'     => 'boolean',
					'required' => false,
					'default'  => false,
					'omission' => 'missing',
				],
				'autoConfigDescription' => [
					'type'       => 'string',
					'required'   => false,
					'requires'   => [ 'autoConfigUsed' => true ],
					'omission'   => 'empty',
				],
				'personPresence' => self::optional_boolean(),
				'includeFace'    => self::optional_model_flag(),
				'includeBody'    => self::optional_model_flag(),
				'lookAtCamera'   => self::optional_model_flag(),
				'selections' => [
					'type'                 => 'object',
					'required'             => false,
					'additionalProperties' => false,
					'fields'               => $selection_fields,
					'omission'             => 'empty',
				],
				'subjectImageId'     => self::optional_attachment_id(),
				'inspirationImageId' => self::optional_attachment_id(),
				'environmentImageId' => self::optional_attachment_id(),
			],
		];
	}

	private static function campaign_set_recipe(): array {
		return [
			'type'                 => 'object',
			'additionalProperties' => false,
			'fields'               => [
				'version' => [
					'type'     => 'integer',
					'required' => true,
					'enum'     => [ 1 ],
					'omission' => 'never',
				],
				'count' => [
					'type'     => 'integer',
					'required' => true,
					'minimum'  => 2,
					'maximum'  => 6,
					'omission' => 'never',
				],
				'quality' => [
					'type'         => 'string',
					'required'     => true,
					'default'      => '4K',
					'optionSource' => 'campaign_set_qualities',
					'omission'     => 'never',
				],
				'customInstructions' => [
					'type'      => 'string',
					'required'  => false,
					'maxLength' => 5000,
					'omission'  => 'empty',
				],
				'style' => [
					'type'         => 'string',
					'required'     => false,
					'maxLength'    => 120,
					'optionSource' => 'styles',
					'omission'     => 'empty',
				],
				'aspectRatio' => [
					'type'         => 'string',
					'required'     => false,
					'maxLength'    => 32,
					'optionSource' => 'configuration',
					'omission'     => 'missing-empty-or-auto',
				],
				'subjectAttachmentId'     => self::optional_attachment_id(),
				'environmentAttachmentId' => self::optional_attachment_id(),
			],
		];
	}

	private static function optional_model_flag(): array {
		return array_merge(
			self::optional_boolean(),
			[
				'requires' => [ 'subjectImageId' => 'positive-integer' ],
			]
		);
	}

	private static function optional_boolean(): array {
		return [
			'type'     => 'boolean',
			'required' => false,
			'omission' => 'missing-or-auto',
		];
	}

	private static function optional_attachment_id(): array {
		return [
			'type'     => 'integer',
			'required' => false,
			'minimum'  => 1,
			'omission' => 'missing-or-zero',
		];
	}
}
