<?php
/**
 * AI-powered form generation for Basic Forms.
 *
 * @package BasicForms
 */

namespace BasicForms;

use WP_PowerSuite\AI\ConnectionSettings;
use WP_PowerSuite\AI\OpenAiCompatibleClient;
use WP_PowerSuite\AI\ProviderRegistry;

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

/**
 * Generates form schemas from natural-language prompts via site-wide AI connections.
 */
final class AiFormGenerator {

	private const MAX_PROMPT_LENGTH = 8000;

	private const MAX_HISTORY_MESSAGES = 20;

	private const MAX_HISTORY_CONTENT = 2000;

	/** @var list<string> */
	private const ALLOWED_TYPES = [
		'text',
		'textarea',
		'email',
		'phone',
		'number',
		'select',
		'radio',
		'checkbox',
		'file',
		'date',
		'time',
		'datetime',
		'url',
		'hidden',
		'password',
		'html',
		'separator',
		'group',
	];

	/** @var list<string> */
	private const INTENTS = [ 'create', 'modify', 'add_fields', 'replace', 'design', 'auto' ];

	/**
	 * Infer how to apply an edit-mode prompt. Edits now use a single "auto" intent
	 * so the model can change fields, settings, and design together — only a clear
	 * "rebuild from scratch" request short-circuits to a full replace.
	 *
	 * @param list<array{role: string, content: string}> $history
	 */
	public static function inferIntentFromPrompt( string $prompt, bool $has_context = true, array $history = [] ): string {
		if ( ! $has_context ) {
			return 'create';
		}

		$lower = self::combinePromptWithHistory( $prompt, $history );

		if ( preg_match( '/\b(replace all|replace the entire|rebuild|start over|from scratch|entire form|whole form|regenerate|redo the (whole )?form|new form instead|scrap (it|this|the form))\b/u', $lower ) ) {
			return 'replace';
		}

		return 'auto';
	}

	/**
	 * Detect an explicit "reset the design / revert styling to default" request.
	 * Kept conservative so ordinary design edits are not treated as a full reset.
	 */
	public static function isDesignResetRequest( string $prompt ): bool {
		$p = mb_strtolower( trim( $prompt ) );
		if ( $p === '' ) {
			return false;
		}

		$reset_verb   = (bool) preg_match( '/\b(reset|revert|restore)\b/u', $p );
		$clear_verb   = (bool) preg_match( '/\bclear\b/u', $p );
		$design_noun  = (bool) preg_match( '/\b(design|styling|styles?|theme|appearance|formatting)\b/u', $p );
		$default_word = (bool) preg_match( '/\bdefaults?\b/u', $p );

		if ( $reset_verb && ( $design_noun || $default_word ) ) {
			return true; // "reset design", "revert to default", "restore default styling".
		}
		if ( $clear_verb && $design_noun ) {
			return true; // "clear the styling/design".
		}
		if ( $default_word && $design_noun ) {
			return true; // "default design", "use the default theme".
		}

		return false;
	}

	/**
	 * @param list<array{role: string, content: string}> $history
	 */
	private static function combinePromptWithHistory( string $prompt, array $history ): string {
		$parts = [ trim( $prompt ) ];

		foreach ( array_reverse( $history ) as $message ) {
			if ( ( $message['role'] ?? '' ) !== 'user' ) {
				continue;
			}

			$content = trim( (string) ( $message['content'] ?? '' ) );
			if ( $content === '' ) {
				continue;
			}

			$parts[] = $content;
			if ( count( $parts ) >= 3 ) {
				break;
			}
		}

		return mb_strtolower( implode( ' ', $parts ) );
	}

	/**
	 * @param array<string, mixed> $options {
	 *     @type string               $intent  create|modify|add_fields|replace|design
	 *     @type array<string, mixed> $context Optional current form snapshot.
	 *     @type string               $model   Optional model override.
	 * }
	 * @return array{title: string, fields: array<int, array<string, mixed>>, settings: array<string, string>, design: array<string, string>, form_id: int}|\WP_Error
	 */
	public function generate( string $prompt, ?string $provider = null, array $options = [] ): array|\WP_Error {
		$prompt = trim( $prompt );
		if ( $prompt === '' ) {
			return new \WP_Error( 'empty_prompt', __( 'Describe what you want the form to do.', 'wp-powersuite' ) );
		}

		if ( mb_strlen( $prompt ) > self::MAX_PROMPT_LENGTH ) {
			return new \WP_Error(
				'prompt_too_long',
				sprintf(
					/* translators: %d: maximum character count */
					__( 'Prompt must be %d characters or fewer.', 'wp-powersuite' ),
					self::MAX_PROMPT_LENGTH
				)
			);
		}

		$intent  = $this->sanitizeIntent( (string) ( $options['intent'] ?? 'create' ) );
		$context = $this->normalizeContext( is_array( $options['context'] ?? null ) ? $options['context'] : [] );

		// Deterministic "reset the design to default" — applied without calling the
		// model so it always works on an explicit request.
		if ( 'auto' === $intent && self::isDesignResetRequest( $prompt ) ) {
			// Only the design changes; fields/title/settings are left untouched
			// (the frontend applies a part only when it is listed in "changed").
			return [
				'title'      => $context['title'],
				'fields'     => [],
				'settings'   => [],
				'design'     => DesignDefaults::get(),
				'changed'    => [ 'design' ],
				'ai_message' => __( 'Reset the form design to the default theme. Open Form Design to review the result.', 'wp-powersuite' ),
			];
		}

		$module_ai = AiSettings::get();
		$provider  = ConnectionSettings::sanitizeProvider( $provider ?? (string) ( $module_ai['provider'] ?? 'openai' ) );
		$model     = FormAiModels::sanitize( (string) ( $options['model'] ?? $module_ai['model'] ?? '' ), $provider );
		$conn      = ConnectionSettings::get();
		$api_key   = ConnectionSettings::getApiKey( $conn, $provider );

		if ( $api_key === '' ) {
			return new \WP_Error(
				'missing_api_key',
				__( 'No AI API key is configured for the selected provider. Add one in WP PowerSuite Settings → AI Connections and choose a provider in Basic Forms settings.', 'wp-powersuite' )
			);
		}

		$system_prompt = $this->buildSystemPrompt( $intent );
		$user_prompt   = $this->buildUserPrompt( $prompt, $intent, $context );
		$history       = self::normalizeHistory( is_array( $options['history'] ?? null ) ? $options['history'] : [] );

		/**
		 * Filter AI form builder prompts before sending to the provider.
		 *
		 * @param array{system: string, user: string} $prompts
		 * @param string                              $provider
		 * @param string                              $intent
		 * @param array<string, mixed>                $context
		 */
		$prompts = apply_filters(
			'wpps_basic_forms_ai_generate_prompt',
			[
				'system' => $system_prompt,
				'user'   => $user_prompt,
			],
			$provider,
			$intent,
			$context
		);

		$raw = $this->callProvider(
			$provider,
			$model,
			$api_key,
			(string) ( $prompts['system'] ?? $system_prompt ),
			(string) ( $prompts['user'] ?? $user_prompt ),
			$history
		);
		if ( is_wp_error( $raw ) ) {
			return $raw;
		}

		$payload = $this->parsePayload( $raw );
		if ( is_wp_error( $payload ) ) {
			$payload = $this->retryInvalidJson(
				$provider,
				$model,
				$api_key,
				(string) ( $prompts['system'] ?? $system_prompt ),
				$raw,
				$history
			);
		}
		if ( is_wp_error( $payload ) ) {
			return $payload;
		}

		$normalized = $this->normalizePayload( $payload, $intent, $context );

		/**
		 * Filter parsed AI form payload before sanitization.
		 *
		 * @param array<string, mixed> $normalized
		 * @param string               $prompt
		 * @param string               $provider
		 * @param string               $intent
		 */
		$normalized = apply_filters( 'wpps_basic_forms_ai_generate_result', $normalized, $prompt, $provider, $intent );

		return $normalized;
	}

	/**
	 * @param list<array<string, mixed>> $history
	 * @return list<array{role: string, content: string}>
	 */
	public static function normalizeHistory( array $history ): array {
		$normalized = [];

		foreach ( array_slice( $history, -self::MAX_HISTORY_MESSAGES ) as $message ) {
			if ( ! is_array( $message ) ) {
				continue;
			}

			$role = isset( $message['role'] ) ? sanitize_key( (string) $message['role'] ) : '';
			if ( ! in_array( $role, [ 'user', 'assistant' ], true ) ) {
				continue;
			}

			$content = isset( $message['content'] ) ? trim( sanitize_textarea_field( (string) $message['content'] ) ) : '';
			if ( $content === '' ) {
				continue;
			}

			if ( mb_strlen( $content ) > self::MAX_HISTORY_CONTENT ) {
				$content = mb_substr( $content, 0, self::MAX_HISTORY_CONTENT );
			}

			$normalized[] = [
				'role'    => $role,
				'content' => $content,
			];
		}

		while ( [] !== $normalized && 'assistant' === $normalized[0]['role'] ) {
			array_shift( $normalized );
		}

		return self::ensureAlternatingRoles( $normalized );
	}

	/**
	 * Drop consecutive duplicate roles so provider APIs receive valid turns.
	 *
	 * @param list<array{role: string, content: string}> $history
	 * @return list<array{role: string, content: string}>
	 */
	private static function ensureAlternatingRoles( array $history ): array {
		$clean = [];

		foreach ( $history as $message ) {
			if ( [] !== $clean && $clean[ array_key_last( $clean ) ]['role'] === $message['role'] ) {
				$clean[ array_key_last( $clean ) ]['content'] .= "\n" . $message['content'];
				continue;
			}

			$clean[] = $message;
		}

		return $clean;
	}

	/**
	 * Human-readable chat summary for a successful AI response.
	 *
	 * @param array<string, mixed> $result
	 */
	public static function buildAssistantSummary( array $result, string $intent ): string {
		if ( 'auto' === $intent ) {
			$message = isset( $result['ai_message'] ) ? trim( (string) $result['ai_message'] ) : '';
			if ( $message !== '' ) {
				return $message;
			}

			$changed = isset( $result['changed'] ) && is_array( $result['changed'] ) ? $result['changed'] : [];
			if ( [] === $changed ) {
				return __( 'I could not detect a change to apply. Try rephrasing your request.', 'wp-powersuite' );
			}

			$parts = [];
			if ( in_array( 'fields', $changed, true ) ) {
				$count = count( self::fieldLabelsFromResult( $result ) );
				$parts[] = sprintf(
					/* translators: %d: number of fields */
					_n( 'the %d form field', 'the %d form fields', $count, 'wp-powersuite' ),
					$count
				);
			}
			if ( in_array( 'design', $changed, true ) ) {
				$parts[] = __( 'the design', 'wp-powersuite' );
			}
			if ( in_array( 'settings', $changed, true ) ) {
				$parts[] = __( 'form settings', 'wp-powersuite' );
			}
			if ( in_array( 'title', $changed, true ) ) {
				$parts[] = __( 'the form name', 'wp-powersuite' );
			}

			return sprintf(
				/* translators: %s: list of changed parts, e.g. "the design and form settings" */
				__( 'Updated %s. Review and save when ready.', 'wp-powersuite' ),
				self::joinList( $parts )
			);
		}

		if ( 'design' === $intent ) {
			return __( 'Updated the form design based on your request. Open Form Design to review, then save when ready.', 'wp-powersuite' );
		}

		if ( 'add_fields' === $intent ) {
			$labels = self::fieldLabelsFromResult( $result );
			if ( [] !== $labels ) {
				return sprintf(
					/* translators: 1: number of fields, 2: comma-separated field labels */
					__( 'Added %1$d field(s): %2$s. Review and save your form when ready.', 'wp-powersuite' ),
					count( $labels ),
					implode( ', ', array_slice( $labels, 0, 8 ) )
				);
			}

			return __( 'Added new fields to your form. Review and save when ready.', 'wp-powersuite' );
		}

		$title  = isset( $result['title'] ) ? sanitize_text_field( (string) $result['title'] ) : '';
		$labels = self::fieldLabelsFromResult( $result );

		if ( in_array( $intent, [ 'create', 'replace' ], true ) ) {
			$summary = $title !== ''
				? sprintf(
					/* translators: 1: form title, 2: field count */
					__( 'Created form "%1$s" with %2$d field(s)', 'wp-powersuite' ),
					$title,
					count( $labels )
				)
				: sprintf(
					/* translators: %d: field count */
					__( 'Created a form with %d field(s)', 'wp-powersuite' ),
					count( $labels )
				);
		} else {
			$summary = $title !== ''
				? sprintf(
					/* translators: %s: form title */
					__( 'Updated form "%s"', 'wp-powersuite' ),
					$title
				)
				: __( 'Updated your form based on your request', 'wp-powersuite' );
		}

		if ( [] !== $labels ) {
			$shown = array_slice( $labels, 0, 8 );
			$suffix = count( $labels ) > 8 ? '…' : '';
			$summary .= ': ' . implode( ', ', $shown ) . $suffix;
		}

		return $summary . '. ' . __( 'Review the form and save when ready.', 'wp-powersuite' );
	}

	/**
	 * @param array<string, mixed> $result
	 * @return list<string>
	 */
	private static function fieldLabelsFromResult( array $result ): array {
		$fields = isset( $result['fields'] ) && is_array( $result['fields'] ) ? $result['fields'] : [];
		$labels = [];

		foreach ( $fields as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}

			$type = sanitize_key( (string) ( $field['type'] ?? 'text' ) );
			if ( in_array( $type, [ 'separator', 'group', 'html' ], true ) ) {
				continue;
			}

			$label = isset( $field['label'] ) ? sanitize_text_field( (string) $field['label'] ) : '';
			if ( $label !== '' ) {
				$labels[] = $label;
			}
		}

		return $labels;
	}

	public static function intentLabel( string $intent ): string {
		return match ( self::sanitizeIntentStatic( $intent ) ) {
			'design'     => __( 'Design update', 'wp-powersuite' ),
			'add_fields' => __( 'Fields added', 'wp-powersuite' ),
			'replace'    => __( 'Form replaced', 'wp-powersuite' ),
			'create'     => __( 'Form created', 'wp-powersuite' ),
			default      => __( 'Done', 'wp-powersuite' ),
		};
	}

	/**
	 * @param list<string> $items
	 */
	private static function joinList( array $items ): string {
		$items = array_values( array_filter( $items ) );
		$count = count( $items );
		if ( 0 === $count ) {
			return '';
		}
		if ( 1 === $count ) {
			return $items[0];
		}
		$last = array_pop( $items );
		return implode( ', ', $items ) . ' ' . __( 'and', 'wp-powersuite' ) . ' ' . $last;
	}

	private static function sanitizeIntentStatic( string $intent ): string {
		return in_array( $intent, self::INTENTS, true ) ? $intent : 'modify';
	}

	private function sanitizeIntent( string $intent ): string {
		return in_array( $intent, self::INTENTS, true ) ? $intent : 'create';
	}

	/**
	 * @param array<string, mixed> $context
	 * @return array{title: string, fields: array<int, array<string, mixed>>, settings: array<string, string>, design: array<string, string>, form_id: int}
	 */
	private function normalizeContext( array $context ): array {
		$title = isset( $context['title'] ) ? sanitize_text_field( (string) $context['title'] ) : '';
		$form_id = isset( $context['form_id'] ) ? max( 0, (int) $context['form_id'] ) : 0;

		$fields = [];
		if ( isset( $context['fields'] ) && is_array( $context['fields'] ) ) {
			foreach ( $context['fields'] as $field ) {
				if ( ! is_array( $field ) ) {
					continue;
				}
				$fields[] = $this->summarizeFieldForContext( $field );
			}
		}

		$settings = [];
		if ( isset( $context['settings'] ) && is_array( $context['settings'] ) ) {
			foreach ( [ 'subject', 'success_message', 'submit_button_text', 'redirect_url', 'enable_autoreply', 'autoreply_subject', 'autoreply_message' ] as $key ) {
				if ( isset( $context['settings'][ $key ] ) ) {
					$settings[ $key ] = in_array( $key, [ 'success_message', 'autoreply_message' ], true )
						? sanitize_textarea_field( (string) $context['settings'][ $key ] )
						: sanitize_text_field( (string) $context['settings'][ $key ] );
				}
			}
		}

		$design = [];
		if ( isset( $context['design'] ) && is_array( $context['design'] ) ) {
			$design = $this->summarizeDesignForContext( $context['design'] );
		}

		return [
			'title'    => $title,
			'fields'   => $fields,
			'settings' => $settings,
			'design'   => $design,
			'form_id'  => $form_id,
		];
	}

	/**
	 * Keep design context compact: non-empty values only, excluding long typography keys when empty.
	 *
	 * @param array<string, mixed> $design
	 * @return array<string, string>
	 */
	private function summarizeDesignForContext( array $design ): array {
		$allowed = array_keys( DesignDefaults::get() );
		$summary = [];

		foreach ( $design as $key => $value ) {
			if ( ! is_string( $key ) || ! in_array( $key, $allowed, true ) ) {
				continue;
			}
			if ( ! is_scalar( $value ) ) {
				continue;
			}
			$str = (string) $value;
			if ( $str === '' && 'custom_css' !== $key ) {
				continue;
			}
			if ( 'custom_css' === $key ) {
				$summary[ $key ] = mb_substr( $str, 0, 2000 );
				continue;
			}
			$summary[ $key ] = sanitize_text_field( $str );
		}

		return $summary;
	}

	/**
	 * @param array<string, mixed> $field
	 * @return array<string, mixed>
	 */
	private function summarizeFieldForContext( array $field ): array {
		$summary = [
			'type'        => isset( $field['type'] ) ? sanitize_key( (string) $field['type'] ) : 'text',
			'label'       => isset( $field['label'] ) ? sanitize_text_field( (string) $field['label'] ) : '',
			'placeholder' => isset( $field['placeholder'] ) ? sanitize_text_field( (string) $field['placeholder'] ) : '',
			'required'    => ! empty( $field['required'] ),
		];

		if ( ! empty( $field['options'] ) ) {
			$summary['options'] = sanitize_textarea_field( (string) $field['options'] );
		}
		if ( ! empty( $field['width'] ) ) {
			$summary['width'] = sanitize_key( (string) $field['width'] );
		}
		if ( ! empty( $field['description'] ) ) {
			$summary['description'] = sanitize_textarea_field( (string) $field['description'] );
		}
		if ( ! empty( $field['field_id'] ) ) {
			$summary['field_id'] = sanitize_key( (string) $field['field_id'] );
		}

		// Carry the remaining configurable properties so the model preserves them across edits.
		foreach ( [ 'help', 'empty_label', 'checkbox_label', 'min', 'max', 'step', 'default_value', 'maxlength', 'rows', 'minlength', 'default_country', 'file_types', 'max_size' ] as $key ) {
			if ( isset( $field[ $key ] ) && '' !== (string) $field[ $key ] ) {
				$summary[ $key ] = sanitize_text_field( (string) $field[ $key ] );
			}
		}
		if ( ! empty( $field['inline'] ) ) {
			$summary['inline'] = true;
			$summary['cols']   = isset( $field['cols'] ) && '' !== (string) $field['cols'] ? sanitize_text_field( (string) $field['cols'] ) : '3';
		}
		if ( ! empty( $field['multiple'] ) ) {
			$summary['multiple'] = true;
		}
		if ( 'html' === $summary['type'] && ! empty( $field['content'] ) ) {
			$summary['content'] = wp_kses_post( (string) $field['content'] );
		}
		if ( ! empty( $field['show_if'] ) && is_array( $field['show_if'] ) ) {
			$normalized = ConditionalFields::normalizeShowIf( $field['show_if'] );
			if ( $normalized !== null ) {
				$summary['show_if'] = $normalized;
			}
		}

		return $summary;
	}

	private function buildSystemPrompt( string $intent ): string {
		$types = implode( ', ', self::ALLOWED_TYPES );
		$design_help = $this->designPromptFragment();

		if ( 'auto' === $intent ) {
			return 'You are the AI assistant for the WP PowerSuite "Basic Forms" builder. You edit an existing form. Return ONLY valid JSON, no markdown fences, no commentary.'
				. ' Output object may contain any of these top-level keys, and MUST contain ONLY the parts you are changing: '
				. '"title" (string), "fields" (array), "settings" (object), "design" (object), "design_reset" (boolean), "message" (a short 1-sentence summary of what you changed for the user).'
				. ' DESIGN RESET: if the user asks to reset/clear/revert the design or styling to default (e.g. "reset the design", "remove all styling", "use the default theme"), return {"design_reset":true,"message":"..."} and do NOT include a "design" object — the builder restores all defaults itself.'
				. ' CRITICAL: If the request is purely about appearance (colors, theme, background, buttons, borders, fonts, spacing, shadows), return ONLY "design" (and "message"). Do NOT return "fields".'
				. ' If the request is about field content/structure (add, remove, rename, reorder, required/optional, options, field width), return the COMPLETE "fields" array with every field (changed and unchanged), preserving field_id values.'
				. ' Field schema: {"id":0,"type":"text","label":"","placeholder":"","description":"","options":"","required":false,"width":"","field_id":"name_0","maxlength":"","show_if":{"field_id":"x_0","operator":"equals","value":"Other"}}.'
				. ' Allowed field types: ' . $types . '. Use sequential numeric ids from 0. For select/radio/checkbox put options one per line in "options".'
				. $this->fieldPropertiesFragment()
				. ' FIELD WIDTH: the form container is narrow, so default to full width by setting width to "" (empty). Allowed widths: ""(full), "half", "third", "quarter", "40", "60". "Make all fields full width" means set every field width to "".'
				. ' Use type "group" for section headings (its "label" is the heading, "description" optional) and "separator" for divider lines.'
				. ' settings keys: subject (admin email subject), success_message, submit_button_text, redirect_url, enable_autoreply ("1"/""), autoreply_subject, autoreply_message.'
				. $design_help
				. ' GROUP/SECTION HEADING color is NOT a design key — change it with scoped custom_css targeting .wpps-bf-group-title.'
				. ' DARK THEME RULE: when the background is dark, you MUST also set readable light text: label_color, field_text_color, help_text_color to light values, and add custom_css so .wpps-bf-group-title and option/legend text are light too. Never leave dark text on a dark background.'
				. ' IMPORTANT: If you cannot do what was asked, the request is outside what this form builder supports, or nothing actually needs to change, return ONLY {"message":"..."} with a clear short explanation and NO other keys. Never invent changes or claim success when you did not change anything.'
				. ' Keep everything the user did not ask to change.';
		}

		if ( 'design' === $intent ) {
			return 'You are a WordPress form design assistant. Return ONLY valid JSON with no markdown fences or commentary.'
				. ' Schema: {"design":{"primary_color":"#2271b1","custom_css":".wpps-bf-wrapper.wpps-bf-123 .wpps-bf-form {}"}}.'
				. ' Return ONLY keys inside design that you are changing. Do NOT return title, fields, or settings.'
				. $design_help;
		}

		if ( 'add_fields' === $intent ) {
			return 'You are a WordPress form builder assistant. Return ONLY valid JSON with no markdown fences or commentary.'
				. ' Schema: {"fields":[{"type":"text","label":"Resume","required":true,"width":"half","field_id":"resume_0","show_if":{"field_id":"case_type_0","operator":"equals","value":"Other"}}]}.'
				. ' Return ONLY new fields to append — do not repeat existing fields.'
				. ' Allowed field types: ' . $types . '.'
				. ' Use field_id slugs like label_0. Omit numeric id — the app assigns ids.'
				. $this->fieldPropertiesFragment();
		}

		$base = 'You are a WordPress form builder assistant. Return ONLY valid JSON with no markdown fences or commentary.'
			. ' Schema: {"title":"string","fields":[{"id":0,"type":"text","label":"Name","placeholder":"","description":"","options":"","required":false,"width":"half","field_id":"name_0","maxlength":"","show_if":{"field_id":"case_type_0","operator":"equals","value":"Other"}}],"settings":{"subject":"string","success_message":"string","submit_button_text":"string","enable_autoreply":"1","autoreply_subject":"string","autoreply_message":"string"},"design":{"primary_color":"#2271b1"}}.'
			. ' Allowed field types: ' . $types . '.'
			. ' Use sequential numeric ids starting at 0.'
			. ' For select, radio, and checkbox fields, put options one per line in the options string.'
			. ' Use group for section headings and separator for visual breaks when helpful.'
			. $this->fieldPropertiesFragment()
			. ' settings.subject is the admin notification email subject; success_message is shown after submit; submit_button_text should be action-oriented.'
			. ' When the user wants a confirmation email, set enable_autoreply to "1" and include autoreply_subject and autoreply_message.'
			. ' Fields may use show_if for conditional visibility: {"field_id":"controller_field_id","operator":"equals|not_equals|contains|not_empty|empty","value":"Other"}. Use show_if only when it improves UX (e.g. show explain field when Other is selected).'
			. ' Respect explicit field count limits from the user (e.g. max 10 fields). Include maxlength on text/textarea when limits are requested.'
			. ' Include design only when the user asks for styling, colors, layout, or appearance changes.'
			. $design_help;

		if ( 'modify' === $intent ) {
			return $base . ' You are modifying an existing form. Keep fields the user did not ask to change. Apply only the requested edits while preserving overall structure when sensible.';
		}

		if ( 'replace' === $intent ) {
			return $base . ' Replace the entire form with a fresh schema based on the user request.';
		}

		return $base . ' Prefer practical labels, placeholders, and required flags. Keep forms focused unless the prompt clearly needs more fields.';
	}

	/**
	 * Full reference of supported field properties so the model can use every builder feature.
	 */
	private function fieldPropertiesFragment(): string {
		return ' FIELD PROPERTIES (set only those relevant to the field type):'
			. ' label, placeholder, description, help (small text under the field), required (bool), width (""=full, half, third, quarter, 40, 60), field_id (slug, keep stable when editing).'
			. ' select/radio/checkbox: options (one choice per line).'
			. ' radio & checkbox groups: inline (bool, render choices side by side) and cols ("2"-"5", columns when inline). To "inline" or "put in columns" set inline:true (and cols).'
			. ' single checkbox without options: checkbox_label (the consent text).'
			. ' select: empty_label (first/placeholder option text).'
			. ' number/date/time/datetime: min, max. number also: step, default_value.'
			. ' text/email/url: maxlength, default_value. textarea: rows (2-20), maxlength.'
			. ' password: minlength. phone: default_country (numeric calling code, e.g. "1").'
			. ' file: file_types (e.g. "image/*,.pdf,.docx"), max_size (MB), multiple (bool).'
			. ' html: content (HTML markup). Layout-only types: group (section heading via its label) and separator (divider line).'
			. ' CONDITIONAL VISIBILITY via show_if — single rule {"field_id":"controller_id","operator":"equals|not_equals|contains|not_empty|empty","value":"Other"},'
			. ' or multiple rules {"relation":"and"|"or","rules":[{rule},{rule}]}. equals/not_equals/contains match case-insensitively.';
	}

	private function designPromptFragment(): string {
		return ' Design keys (include only what you change): layout (default|full|compact|narrow|custom), max_width, align (left|center|right), field_spacing (tight|normal|relaxed|spacious), field_gap, bg_color, padding, border_width, border_color, border_radius, label_position (above|left), label_color, label_font_weight, primary_color, button_hover_color, button_style (solid|outline|flat), button_size (small|medium|large), button_border_radius, button_full_width (0|1), button_shadow (none|sm|md|lg), input_border_color, input_bg, input_border_radius, input_padding, placeholder_color, field_text_color, help_text_color, success_msg_bg, success_msg_color, error_msg_bg, error_msg_color, custom_css.'
			. ' Use hex colors. For effects not covered by these options (gradients, animations, advanced layout), add or update custom_css.'
			. ' Scope every custom_css selector to .wpps-bf-wrapper.wpps-bf-{form_id} using the form_id from context when provided.';
	}

	/**
	 * @param array{title: string, fields: array<int, array<string, mixed>>, settings: array<string, string>, design: array<string, string>, form_id: int} $context
	 */
	private function buildUserPrompt( string $prompt, string $intent, array $context ): string {
		if ( 'create' === $intent ) {
			return 'Create a form for: ' . $prompt;
		}

		if ( 'auto' === $intent ) {
			$context_json = wp_json_encode( $context, JSON_UNESCAPED_UNICODE );
			return "Here is the current form as JSON (fields, settings, design, form_id):\n"
				. $context_json
				. "\n\nApply this change, returning only the keys you change:\n" . $prompt;
		}

		if ( 'design' === $intent ) {
			$context_json = wp_json_encode(
				[
					'form_id' => $context['form_id'],
					'design'  => $context['design'],
				],
				JSON_UNESCAPED_UNICODE
			);
			return "Update the form design based on the request.\n\nCurrent design:\n"
				. $context_json
				. "\n\nUser request: " . $prompt;
		}

		if ( 'replace' === $intent ) {
			$context_json = wp_json_encode( $context, JSON_UNESCAPED_UNICODE );
			return "Replace this form entirely based on the request.\n\nCurrent form (for reference):\n"
				. $context_json
				. "\n\nUser request: " . $prompt;
		}

		if ( 'add_fields' === $intent ) {
			$fields_json = wp_json_encode( $context['fields'] ?? [], JSON_UNESCAPED_UNICODE );
			return "Existing fields (do not duplicate):\n" . $fields_json . "\n\nAdd new fields for: " . $prompt;
		}

		$context_json = wp_json_encode( $context, JSON_UNESCAPED_UNICODE );
		return "Modify this existing form based on the user request.\n\nCurrent form:\n"
			. $context_json
			. "\n\nUser request: " . $prompt;
	}

	private function callProvider( string $provider, string $model, string $api_key, string $system_prompt, string $user_prompt, array $history = [] ): string|\WP_Error {
		return match ( ProviderRegistry::apiFamily( $provider ) ) {
			'gemini'            => $this->callGemini( $model, $api_key, $system_prompt, $user_prompt, $history ),
			'claude'            => $this->callClaude( $model, $api_key, $system_prompt, $user_prompt, $history ),
			'openai_compatible' => $this->callOpenAiCompatible( $provider, $model, $api_key, $system_prompt, $user_prompt, $history ),
			default             => $this->callOpenAi( $model, $api_key, $system_prompt, $user_prompt, $history ),
		};
	}

	/**
	 * @param list<array{role: string, content: string}> $history
	 * @return list<array{role: string, content: string}>
	 */
	private function buildApiMessages( string $system_prompt, array $history, string $user_prompt ): array {
		$messages = [
			[
				'role'    => 'system',
				'content' => $system_prompt,
			],
		];

		foreach ( $history as $message ) {
			$messages[] = $message;
		}

		$messages[] = [
			'role'    => 'user',
			'content' => $user_prompt,
		];

		return $messages;
	}

	/**
	 * @param list<array{role: string, content: string}> $history
	 */
	private function callOpenAi( string $model, string $api_key, string $system_prompt, string $user_prompt, array $history = [] ): string|\WP_Error {
		return $this->callOpenAiCompatible( 'openai', $model, $api_key, $system_prompt, $user_prompt, $history );
	}

	/**
	 * @param list<array{role: string, content: string}> $history
	 */
	private function callOpenAiCompatible( string $provider, string $model, string $api_key, string $system_prompt, string $user_prompt, array $history = [] ): string|\WP_Error {
		$messages     = $this->buildApiMessages( $system_prompt, $history, $user_prompt );
		$extra_headers = $provider === 'openrouter' ? OpenAiCompatibleClient::openRouterHeaders() : [];

		return OpenAiCompatibleClient::chatCompletions(
			ProviderRegistry::baseUrl( $provider ),
			$api_key,
			$model,
			$messages,
			[
				'json_mode'     => true,
				'temperature'   => 0.4,
				'max_tokens'    => 4096,
				'extra_headers' => $extra_headers,
			]
		);
	}

	private function callGemini( string $model, string $api_key, string $system_prompt, string $user_prompt, array $history = [] ): string|\WP_Error {
		$url = sprintf(
			'https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent',
			rawurlencode( $model )
		);

		$contents = [];
		foreach ( $history as $message ) {
			$contents[] = [
				'role'  => 'assistant' === $message['role'] ? 'model' : 'user',
				'parts' => [
					[ 'text' => $message['content'] ],
				],
			];
		}
		$contents[] = [
			'role'  => 'user',
			'parts' => [
				[ 'text' => $user_prompt ],
			],
		];

		$response = wp_remote_post(
			$url,
			[
				'timeout' => 90,
				'headers' => [
					'Content-Type'   => 'application/json',
					'x-goog-api-key' => $api_key,
				],
				'body'    => wp_json_encode(
					[
						'system_instruction' => [
							'parts' => [
								[ 'text' => $system_prompt ],
							],
						],
						'contents'           => $contents,
						'generationConfig'   => [
							'temperature'      => 0.4,
							'maxOutputTokens'  => 4096,
							'responseMimeType' => 'application/json',
						],
					]
				),
			]
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = wp_remote_retrieve_response_code( $response );
		$body = json_decode( (string) wp_remote_retrieve_body( $response ), true );
		if ( $code !== 200 || ! is_array( $body ) ) {
			return new \WP_Error( 'api_error', __( 'Gemini request failed.', 'wp-powersuite' ) );
		}

		$text = $body['candidates'][0]['content']['parts'][0]['text'] ?? '';
		return is_string( $text ) && $text !== '' ? $text : new \WP_Error( 'empty_response', __( 'AI returned an empty response.', 'wp-powersuite' ) );
	}

	private function callClaude( string $model, string $api_key, string $system_prompt, string $user_prompt, array $history = [] ): string|\WP_Error {
		$messages = [];
		foreach ( $history as $message ) {
			$messages[] = $message;
		}
		$messages[] = [
			'role'    => 'user',
			'content' => $user_prompt,
		];

		$response = wp_remote_post(
			'https://api.anthropic.com/v1/messages',
			[
				'timeout' => 90,
				'headers' => [
					'x-api-key'         => $api_key,
					'anthropic-version' => '2023-06-01',
					'Content-Type'      => 'application/json',
				],
				'body'    => wp_json_encode(
					[
						'model'       => $model,
						'max_tokens'  => 4096,
						'system'      => $system_prompt,
						'temperature' => 0.4,
						'messages'    => $messages,
					]
				),
			]
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = wp_remote_retrieve_response_code( $response );
		$body = json_decode( (string) wp_remote_retrieve_body( $response ), true );
		if ( $code !== 200 || ! is_array( $body ) ) {
			$message = is_array( $body ) ? (string) ( $body['error']['message'] ?? __( 'Claude request failed.', 'wp-powersuite' ) ) : __( 'Claude request failed.', 'wp-powersuite' );
			return new \WP_Error( 'api_error', $message );
		}

		$blocks = $body['content'] ?? [];
		if ( ! is_array( $blocks ) ) {
			return new \WP_Error( 'empty_response', __( 'AI returned an empty response.', 'wp-powersuite' ) );
		}

		$text = '';
		foreach ( $blocks as $block ) {
			if ( is_array( $block ) && ( $block['type'] ?? '' ) === 'text' && isset( $block['text'] ) ) {
				$text .= (string) $block['text'];
			}
		}

		return $text !== '' ? $text : new \WP_Error( 'empty_response', __( 'AI returned an empty response.', 'wp-powersuite' ) );
	}

	/**
	 * @return array<string, mixed>|\WP_Error
	 */
	private function parsePayload( string $raw ): array|\WP_Error {
		$raw = trim( $raw );
		if ( preg_match( '/```(?:json)?\s*(.*?)\s*```/s', $raw, $matches ) ) {
			$raw = trim( (string) $matches[1] );
		}

		$decoded = json_decode( $raw, true );
		if ( ! is_array( $decoded ) ) {
			return new \WP_Error( 'invalid_json', __( 'AI returned an invalid form structure. Try again with a clearer prompt.', 'wp-powersuite' ) );
		}

		return $decoded;
	}

	/**
	 * Attempt one repair pass when the model returns malformed JSON.
	 *
	 * @param list<array{role: string, content: string}> $history
	 * @return array<string, mixed>|\WP_Error
	 */
	private function retryInvalidJson(
		string $provider,
		string $model,
		string $api_key,
		string $system_prompt,
		string $raw,
		array $history
	): array|\WP_Error {
		$snippet = mb_substr( trim( $raw ), 0, 6000 );
		$repair_prompt = "The previous response was invalid JSON. Return ONLY corrected valid JSON with the same intent and data.\n\nInvalid response:\n"
			. $snippet;

		$repaired = $this->callProvider( $provider, $model, $api_key, $system_prompt, $repair_prompt, $history );
		if ( is_wp_error( $repaired ) ) {
			return new \WP_Error( 'invalid_json', __( 'AI returned an invalid form structure. Try again with a clearer prompt.', 'wp-powersuite' ) );
		}

		$payload = $this->parsePayload( $repaired );
		if ( is_wp_error( $payload ) ) {
			return new \WP_Error( 'invalid_json', __( 'AI returned an invalid form structure. Try again with a clearer prompt.', 'wp-powersuite' ) );
		}

		return $payload;
	}

	/**
	 * @param array<string, mixed> $payload
	 * @param array{title: string, fields: array<int, array<string, mixed>>, settings: array<string, string>, design: array<string, string>, form_id: int} $context
	 * @return array{title: string, fields: array<int, array<string, mixed>>, settings: array<string, string>, design?: array<string, string>}
	 */
	private function normalizePayload( array $payload, string $intent, array $context ): array {
		if ( 'auto' === $intent ) {
			$changed = [];

			$has_fields = isset( $payload['fields'] ) && is_array( $payload['fields'] ) && [] !== $payload['fields'];
			if ( $has_fields ) {
				$fields = $this->normalizeFieldsList( $payload['fields'], 0 );
				$changed[] = 'fields';
			} else {
				$fields = $this->normalizeFieldsList( $context['fields'], 0 );
			}

			$title = isset( $payload['title'] ) ? sanitize_text_field( (string) $payload['title'] ) : '';
			if ( $title !== '' && $title !== $context['title'] ) {
				$changed[] = 'title';
			}
			if ( $title === '' ) {
				$title = $context['title'] !== '' ? $context['title'] : __( 'New Form', 'wp-powersuite' );
			}

			$settings = $context['settings'];
			if ( isset( $payload['settings'] ) && is_array( $payload['settings'] ) && [] !== $payload['settings'] ) {
				$settings = $this->extractSettings( $payload['settings'], $context['settings'] );
				$changed[] = 'settings';
			}

			$result = [
				'title'    => $title,
				'fields'   => $fields,
				'settings' => $settings,
				'changed'  => $changed,
				'ai_message' => isset( $payload['message'] ) ? sanitize_text_field( (string) $payload['message'] ) : '',
			];

			$design_reset = ! empty( $payload['design_reset'] )
				&& ! in_array( $payload['design_reset'], [ '0', 'false', 'no' ], true );
			if ( $design_reset ) {
				$result['design']    = DesignDefaults::get();
				$result['changed'][] = 'design';
				if ( '' === $result['ai_message'] ) {
					$result['ai_message'] = __( 'Reset the form design to the default theme.', 'wp-powersuite' );
				}
			} elseif ( isset( $payload['design'] ) && is_array( $payload['design'] ) && [] !== $payload['design'] ) {
				$normalized_design = $this->normalizeDesign( $payload['design'], $context['design'] );
				if ( [] !== $normalized_design ) {
					$result['design'] = $normalized_design;
					$result['changed'][] = 'design';
				}
			}

			return $result;
		}

		if ( 'design' === $intent ) {
			$raw_design = isset( $payload['design'] ) && is_array( $payload['design'] ) ? $payload['design'] : $payload;

			return [
				'title'    => $context['title'] !== '' ? $context['title'] : __( 'New Form', 'wp-powersuite' ),
				'fields'   => [],
				'settings' => $context['settings'],
				'design'   => $this->normalizeDesign( is_array( $raw_design ) ? $raw_design : [], $context['design'] ),
			];
		}

		if ( 'add_fields' === $intent ) {
			$fields = [];
			if ( isset( $payload['fields'] ) && is_array( $payload['fields'] ) ) {
				$fields = $payload['fields'];
			} elseif ( isset( $payload['type'] ) && is_array( $payload ) ) {
				$fields = [ $payload ];
			}

			$result = [
				'title'    => $context['title'] !== '' ? $context['title'] : __( 'New Form', 'wp-powersuite' ),
				'fields'   => $this->normalizeFieldsList( $fields, 0 ),
				'settings' => $context['settings'],
			];
			if ( isset( $payload['design'] ) && is_array( $payload['design'] ) ) {
				$result['design'] = $this->normalizeDesign( $payload['design'], $context['design'] );
			}

			return $result;
		}

		$title  = isset( $payload['title'] ) ? sanitize_text_field( (string) $payload['title'] ) : $context['title'];
		$fields = isset( $payload['fields'] ) && is_array( $payload['fields'] ) ? $payload['fields'] : [];

		$normalized_fields = $this->normalizeFieldsList( $fields, 0 );
		if ( [] === $normalized_fields && [] !== $context['fields'] ) {
			$result = [
				'title'    => $title !== '' ? $title : $context['title'],
				'fields'   => $this->normalizeFieldsList( $context['fields'], 0 ),
				'settings' => $this->mergeSettings( $payload, $context['settings'] ),
			];
			if ( isset( $payload['design'] ) && is_array( $payload['design'] ) ) {
				$result['design'] = $this->normalizeDesign( $payload['design'], $context['design'] );
			}

			return $result;
		}

		$raw_settings = isset( $payload['settings'] ) && is_array( $payload['settings'] ) ? $payload['settings'] : [];

		$result = [
			'title'    => $title !== '' ? $title : ( $context['title'] !== '' ? $context['title'] : __( 'New Form', 'wp-powersuite' ) ),
			'fields'   => $normalized_fields,
			'settings' => $this->extractSettings( $raw_settings, $context['settings'] ),
		];
		if ( isset( $payload['design'] ) && is_array( $payload['design'] ) ) {
			$result['design'] = $this->normalizeDesign( $payload['design'], $context['design'] );
		}

		return $result;
	}

	/**
	 * Merge AI design changes onto existing design context.
	 *
	 * @param array<string, mixed> $raw_design
	 * @param array<string, string> $context_design
	 * @return array<string, string>
	 */
	private function normalizeDesign( array $raw_design, array $context_design ): array {
		$defaults  = DesignDefaults::get();
		$allowed   = array_keys( $defaults );
		$incoming  = [];

		foreach ( $raw_design as $key => $value ) {
			if ( ! is_string( $key ) || ! in_array( $key, $allowed, true ) || ! is_scalar( $value ) ) {
				continue;
			}
			if ( 'custom_css' === $key ) {
				$incoming[ $key ] = $this->sanitizeCustomCss( (string) $value );
				continue;
			}
			$incoming[ $key ] = sanitize_text_field( (string) $value );
		}

		if ( [] === $incoming ) {
			return [];
		}

		$merged = wp_parse_args( $incoming, wp_parse_args( $context_design, $defaults ) );

		return DesignDefaults::normalizeForAdminForm( $merged );
	}

	private function sanitizeCustomCss( string $css ): string {
		if ( $css === '' ) {
			return '';
		}

		$css = preg_replace( '/<script\b[^>]*>(.*?)<\/script>/is', '', $css );
		$css = wp_strip_all_tags( $css, true );
		$css = preg_replace( '/javascript\s*:/i', '', $css );
		$css = preg_replace( '/expression\s*\(/i', '', $css );
		$css = preg_replace( '/@import/i', '', $css );
		$css = preg_replace( '/behavior\s*:/i', '', $css );
		$css = preg_replace( '/<\/?style\b/i', '', $css );
		$css = trim( $css );

		if ( strlen( $css ) > 10000 ) {
			$css = substr( $css, 0, 10000 );
		}

		return $css;
	}

	/**
	 * @param array<int, mixed> $fields
	 * @return array<int, array<string, mixed>>
	 */
	private function normalizeFieldsList( array $fields, int $start_id ): array {
		$next_id           = $start_id;
		$normalized_fields = [];

		foreach ( $fields as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}

			$type = isset( $field['type'] ) ? sanitize_key( (string) $field['type'] ) : 'text';
			if ( ! in_array( $type, self::ALLOWED_TYPES, true ) ) {
				$type = 'text';
			}

			$label = isset( $field['label'] ) ? sanitize_text_field( (string) $field['label'] ) : '';
			if ( $label === '' && ! in_array( $type, [ 'separator', 'html' ], true ) ) {
				$label = ucfirst( $type ) . ' Field';
			}

			$item = [
				'id'          => $next_id,
				'type'        => $type,
				'label'       => $label,
				'placeholder' => isset( $field['placeholder'] ) ? sanitize_text_field( (string) $field['placeholder'] ) : '',
				'description' => isset( $field['description'] ) ? sanitize_textarea_field( (string) $field['description'] ) : '',
				'options'     => isset( $field['options'] ) ? sanitize_textarea_field( (string) $field['options'] ) : '',
				'required'    => ! empty( $field['required'] ),
			];

			if ( ! in_array( $type, [ 'separator', 'group', 'html' ], true ) ) {
				$base_slug = sanitize_key( str_replace( '-', '_', sanitize_title( $label ) ) );
				if ( $base_slug === '' ) {
					$base_slug = 'field';
				}
				$item['field_id'] = ! empty( $field['field_id'] ) ? sanitize_key( (string) $field['field_id'] ) : $base_slug . '_' . $next_id;
			}

			if ( isset( $field['width'] ) ) {
				$width = sanitize_key( (string) $field['width'] );
				if ( in_array( $width, [ 'half', 'third', 'quarter', '40', '60' ], true ) ) {
					$item['width'] = $width;
				}
			}

			if ( ! empty( $field['help'] ) ) {
				$item['help'] = sanitize_text_field( (string) $field['help'] );
			}
			if ( isset( $field['maxlength'] ) && (int) $field['maxlength'] > 0 ) {
				$item['maxlength'] = (string) (int) $field['maxlength'];
			}
			if ( isset( $field['rows'] ) && (int) $field['rows'] > 0 ) {
				$item['rows'] = (string) max( 2, min( 20, (int) $field['rows'] ) );
			}
			if ( ! empty( $field['checkbox_label'] ) ) {
				$item['checkbox_label'] = sanitize_text_field( (string) $field['checkbox_label'] );
			}
			if ( 'select' === $type && isset( $field['empty_label'] ) ) {
				$item['empty_label'] = sanitize_text_field( (string) $field['empty_label'] );
			}

			// Radio / checkbox group layout.
			if ( in_array( $type, [ 'checkbox', 'radio' ], true ) && ! empty( $field['inline'] ) ) {
				$item['inline'] = true;
				$cols = isset( $field['cols'] ) ? (int) $field['cols'] : 3;
				$item['cols'] = (string) max( 2, min( 5, $cols > 0 ? $cols : 3 ) );
			}

			// Numeric / date range and step.
			if ( in_array( $type, [ 'number', 'date', 'time', 'datetime' ], true ) ) {
				if ( isset( $field['min'] ) && '' !== (string) $field['min'] ) {
					$item['min'] = sanitize_text_field( (string) $field['min'] );
				}
				if ( isset( $field['max'] ) && '' !== (string) $field['max'] ) {
					$item['max'] = sanitize_text_field( (string) $field['max'] );
				}
			}
			if ( 'number' === $type && isset( $field['step'] ) && '' !== (string) $field['step'] ) {
				$item['step'] = sanitize_text_field( (string) $field['step'] );
			}
			if ( in_array( $type, [ 'text', 'email', 'url', 'number', 'hidden' ], true ) && isset( $field['default_value'] ) && '' !== (string) $field['default_value'] ) {
				$item['default_value'] = sanitize_text_field( (string) $field['default_value'] );
			}
			if ( 'password' === $type && isset( $field['minlength'] ) && (int) $field['minlength'] > 0 ) {
				$item['minlength'] = (string) max( 1, min( 128, (int) $field['minlength'] ) );
			}
			if ( 'phone' === $type && isset( $field['default_country'] ) && '' !== (string) $field['default_country'] ) {
				$item['default_country'] = preg_replace( '/[^0-9]/', '', (string) $field['default_country'] );
			}

			if ( 'file' === $type ) {
				if ( ! empty( $field['file_types'] ) ) {
					$item['file_types'] = sanitize_text_field( (string) $field['file_types'] );
				}
				if ( isset( $field['max_size'] ) && '' !== (string) $field['max_size'] ) {
					$item['max_size'] = sanitize_text_field( (string) $field['max_size'] );
				}
				if ( ! empty( $field['multiple'] ) ) {
					$item['multiple'] = true;
				}
			} elseif ( ! empty( $field['file_types'] ) ) {
				$item['file_types'] = sanitize_text_field( (string) $field['file_types'] );
			}
			if ( isset( $field['show_if'] ) && is_array( $field['show_if'] ) ) {
				$normalized_show_if = ConditionalFields::normalizeShowIf( $field['show_if'] );
				if ( $normalized_show_if !== null ) {
					$item['show_if'] = $normalized_show_if;
				}
			}

			if ( $type === 'html' && isset( $field['content'] ) ) {
				$item['content'] = wp_kses_post( (string) $field['content'] );
			}

			$normalized_fields[] = $item;
			++$next_id;
		}

		if ( [] === $normalized_fields && 0 === $start_id ) {
			$normalized_fields[] = [
				'id'          => 0,
				'type'        => 'textarea',
				'label'       => __( 'Message', 'wp-powersuite' ),
				'placeholder' => '',
				'description' => '',
				'options'     => '',
				'required'    => true,
				'field_id'    => 'message_0',
			];
		}

		return $normalized_fields;
	}

	/**
	 * @param array<string, mixed> $payload
	 * @param array<string, string>  $fallback
	 * @return array<string, string>
	 */
	private function mergeSettings( array $payload, array $fallback ): array {
		$raw_settings = isset( $payload['settings'] ) && is_array( $payload['settings'] ) ? $payload['settings'] : $payload;
		return $this->extractSettings( is_array( $raw_settings ) ? $raw_settings : [], $fallback );
	}

	/**
	 * @param array<string, mixed> $raw_settings
	 * @param array<string, string> $fallback
	 * @return array<string, string>
	 */
	private function extractSettings( array $raw_settings, array $fallback ): array {
		$settings = $fallback;
		foreach ( [ 'subject', 'success_message', 'submit_button_text', 'redirect_url', 'autoreply_subject' ] as $key ) {
			if ( isset( $raw_settings[ $key ] ) && (string) $raw_settings[ $key ] !== '' ) {
				$settings[ $key ] = in_array( $key, [ 'success_message', 'autoreply_message' ], true )
					? sanitize_textarea_field( (string) $raw_settings[ $key ] )
					: sanitize_text_field( (string) $raw_settings[ $key ] );
			}
		}

		if ( isset( $raw_settings['autoreply_message'] ) && (string) $raw_settings['autoreply_message'] !== '' ) {
			$settings['autoreply_message'] = sanitize_textarea_field( (string) $raw_settings['autoreply_message'] );
		}

		if ( array_key_exists( 'enable_autoreply', $raw_settings ) ) {
			$settings['enable_autoreply'] = ! empty( $raw_settings['enable_autoreply'] ) ? '1' : '';
		}

		return $settings;
	}
}
