<?php

namespace GriffinForms\Includes\Ai\PromptRegistry\Handlers;

use GriffinForms\Includes\Ai\PatchPlanParser;
use GriffinForms\Includes\Ai\PatchPlanApplier;
use GriffinForms\Includes\Ai\PromptRegistry\StructureSnapshotBuilder;
use GriffinForms\Includes\Ai\PromptRegistry\Handlers\Row\RowPromptHelper;
use GriffinForms\Includes\Ai\PromptRegistry\Handlers\Row\RowCopyResolver;
use GriffinForms\Includes\Ai\PromptRegistry\Handlers\Page\PagePromptHelper;

class GeneratePatchPlanPromptHandler extends BasePromptHandler
{
    protected const MAX_REFLECTION_RETRIES = 1;

    protected PatchPlanParser $parser;
    protected PatchPlanApplier $applier;
    protected RowPromptHelper $row_prompt_helper;
    protected RowCopyResolver $row_copy_resolver;
    protected PagePromptHelper $page_prompt_helper;

    public function __construct()
    {
        parent::__construct();
        $this->parser = new PatchPlanParser();
        $this->applier = new PatchPlanApplier();
        $this->row_prompt_helper = new RowPromptHelper();
        $this->row_copy_resolver = new RowCopyResolver();
        $this->page_prompt_helper = new PagePromptHelper();
    }

    protected function buildAdvisoryFromPlan(array $plan, array $selected_context = []): array
    {
        $required_inputs = (array) ($plan['required_inputs'] ?? []);
        $draft = (array) ($plan['draft_patch'] ?? []);
        $ops = (array) ($draft['operations'] ?? []);

        $choices = [];
        $target_label = '';
        $prop_name = 'label';
        $scope = '';
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));

        if (!empty($ops)) {
            $op = (array) $ops[0];
            $changes = (array) ($op['changes'] ?? []);
            if (!empty($changes)) {
                $prop_name = array_key_first($changes);
            }
            $target_label = (string) ($op['target']['item_label'] ?? '');
            if (!$target_label && !empty($op['target']['semantic_hint'])) {
                $target_label = (string) $op['target']['semantic_hint'];
            }
            $scope = sanitize_key((string) ($op['target']['scope'] ?? ''));
            if ($scope === 'form' && $selected_type === 'field') {
                $scope = 'field';
            }
        }

        $message = (string) ($plan['success_message'] ?? __('I need a clearer request before I can continue safely.', 'griffinforms-form-builder'));

        foreach ($required_inputs as $input) {
            $input = (array) $input;
            $label = (string) ($input['label'] ?? '');
            $value = (string) ($input['value'] ?? $label);

            // For submit_label choices, the label IS the intended value. The model sometimes
            // sets value to the form title or another unrelated string, so normalise here.
            if ($prop_name === 'submit_label') {
                $value = $label;
            }

            if ($label === '') {
                continue;
            }

            // Detection: If the input looks like an instruction (ends in :), promote it to message.
            // Avoid stripping valid long descriptions by removing the arbitrary 40 char limit.
            if (str_ends_with(trim($label), ':')) {
                $message = $label;
                continue;
            }

            $suggested_prompt = (string) ($input['suggested_prompt'] ?? '');
            if (!$suggested_prompt) {
                if ($target_label) {
                    $suggested_prompt = sprintf('Change %s %s to "%s"', $target_label, $prop_name, $value);
                } elseif ($scope === 'field') {
                    $suggested_prompt = sprintf('Update field %s to "%s"', $prop_name, $value);
                } elseif ($scope === 'form') {
                    $suggested_prompt = sprintf('Update form %s to "%s"', $prop_name, $value);
                } else {
                    $suggested_prompt = sprintf('Update %s to "%s"', $prop_name, $value);
                }
            }

            $choices[] = [
                'label' => $label,
                'suggested_prompt' => $suggested_prompt,
                'semantic_intent' => 'builder_mutation',
            ];
        }

        return [
            'response_type' => 'patch_with_user_input',
            'message' => $message,
            'choices' => $choices,
        ];
    }

    protected function canAutoResolveNavigationLabelPlan(array $plan, array $structure, array $selected_context): bool
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $changes = (array) ($operation['changes'] ?? []);
        $property = sanitize_key((string) array_key_first($changes));

        if ($property === 'submit_label') {
            return false;
        }

        if (empty($operations)) {
            return false;
        }

        $target = (array) ($operation['target'] ?? []);
        if (count($changes) !== 1) {
            return false;
        }

        if (!in_array($property, ['next_label', 'previous_label', 'submit_label'], true)) {
            return false;
        }

        if (!empty($target['scope'])) {
            $scope = sanitize_key((string) $target['scope']);
            if (!in_array($scope, ['page', 'form', 'selected'], true)) {
                return false;
            }
        }

        return $this->resolveNavigationLabelPlan($plan, $structure, $selected_context) !== null;
    }

    protected function canCommitSubmitLabelChoice(array $plan, array $selected_context, string $prompt): bool
    {
        return $this->resolveSubmitLabelChoicePlan($plan, $selected_context, $prompt) !== null;
    }

    protected function canAutoResolveRowHeadingPlan(array $plan, array $structure, array $selected_context): bool
    {
        return $this->resolveRowHeadingPlan($plan, $structure, $selected_context) !== null;
    }

    protected function canAutoResolveRowDescriptionPlan(array $plan, array $structure, array $selected_context): bool
    {
        return $this->resolveRowDescriptionPlan($plan, $structure, $selected_context) !== null;
    }

    protected function canAutoResolveFormNamePlan(array $plan, array $structure, array $selected_context): bool
    {
        return $this->resolveFormNamePlan($plan, $structure, $selected_context) !== null;
    }

    protected function canAutoResolveFormDescriptionPlan(array $plan, array $structure, array $selected_context): bool
    {
        return $this->resolveFormDescriptionPlan($plan, $structure, $selected_context) !== null;
    }

    protected function convertFormNamePlanToPatch(array $plan, array $structure, array $selected_context): array
    {
        $resolved = $this->resolveFormNamePlan($plan, $structure, $selected_context);
        if ($resolved === null) {
            return $plan;
        }

        return [
            'mode' => 'patch',
            'success_message' => (string) ($resolved['success_message'] ?? __('Updated the form name.', 'griffinforms-form-builder')),
            'operations' => (array) ($resolved['operations'] ?? []),
        ];
    }

    protected function convertFormDescriptionPlanToPatch(array $plan, array $structure, array $selected_context): array
    {
        $resolved = $this->resolveFormDescriptionPlan($plan, $structure, $selected_context);
        if ($resolved === null) {
            return $plan;
        }

        return [
            'mode' => 'patch',
            'success_message' => (string) ($resolved['success_message'] ?? __('Updated the form description.', 'griffinforms-form-builder')),
            'operations' => (array) ($resolved['operations'] ?? []),
        ];
    }

    protected function attachPlanContext(array $plan, array $selected_context, array $request_context, array $structure): array
    {
        $plan['selected_context'] = $selected_context;
        $plan['request_context'] = $request_context;
        $plan['structure'] = $structure;

        return $plan;
    }

    protected function resolveFormNamePlan(array $plan, array $structure, array $selected_context): ?array
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $changes = (array) ($operation['changes'] ?? []);
        if (empty($changes)) {
            return null;
        }
        
        $property = sanitize_key((string) array_key_first($changes));
        $target = (array) ($operation['target'] ?? []);
        $scope = sanitize_key((string) ($target['scope'] ?? ''));

        if (!in_array($property, ['name', 'heading'], true) || $scope !== 'form') {
            return null;
        }

        $resolved_value = $this->normalizeText((string) ($changes[$property] ?? ''), 120);
        if ($resolved_value === '') {
            return null;
        }

        return [
            'success_message' => $property === 'heading' ? __('Updated the form heading.', 'griffinforms-form-builder') : __('Updated the form name.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'update_form',
                'target' => [
                    'scope' => 'form',
                    'item_id' => absint($selected_context['form_id'] ?? 0),
                ],
                'changes' => [
                    $property => $resolved_value,
                ],
            ]],
        ];
    }

    protected function resolveFormDescriptionPlan(array $plan, array $structure, array $selected_context): ?array
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $changes = (array) ($operation['changes'] ?? []);
        if (empty($changes)) {
            return null;
        }
        
        $property = sanitize_key((string) array_key_first($changes));
        $target = (array) ($operation['target'] ?? []);
        $scope = sanitize_key((string) ($target['scope'] ?? ''));

        if ($property !== 'description' || $scope !== 'form') {
            return null;
        }

        $resolved_value = $this->normalizeText((string) ($changes['description'] ?? ''), 180);
        if ($resolved_value === '') {
            return null;
        }

        return [
            'success_message' => __('Updated the form description.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'update_form',
                'target' => [
                    'scope' => 'form',
                    'item_id' => absint($selected_context['form_id'] ?? 0),
                ],
                'changes' => [
                    'description' => $resolved_value,
                ],
            ]],
        ];
    }

    protected function canAutoResolvePageHeadingPlan(array $plan, array $structure, array $selected_context): bool
    {
        return $this->resolvePageHeadingPlan($plan, $structure, $selected_context) !== null;
    }

    protected function canAutoResolvePageDescriptionPlan(array $plan, array $structure, array $selected_context): bool
    {
        return $this->resolvePageDescriptionPlan($plan, $structure, $selected_context) !== null;
    }

    protected function canAutoResolveFieldLabelPlan(array $plan, array $structure, array $selected_context, string $prompt = ''): bool
    {
        return $this->resolveFieldLabelPlan($plan, $structure, $selected_context, $prompt) !== null;
    }

    protected function canAutoResolveFieldDescriptionPlan(array $plan, array $structure, array $selected_context, string $prompt = ''): bool
    {
        return $this->resolveFieldDescriptionPlan($plan, $structure, $selected_context, $prompt) !== null;
    }

    protected function canAutoResolvePageMovePlan(array $plan, array $structure, array $selected_context): bool
    {
        return $this->resolvePageMovePlan($plan, $structure, $selected_context) !== null;
    }

    protected function canAutoResolveRowDeletePlan(array $plan, array $structure, array $selected_context): bool
    {
        return $this->resolveRowDeletePlan($plan, $structure, $selected_context) !== null;
    }

    protected function convertNavigationLabelPlanToPatch(array $plan, array $structure, array $selected_context): array
    {
        $resolved = $this->resolveNavigationLabelPlan($plan, $structure, $selected_context);
        if ($resolved === null) {
            return $plan;
        }

        return [
            'mode' => 'patch',
            'success_message' => (string) ($resolved['success_message'] ?? __('Updated the button label.', 'griffinforms-form-builder')),
            'operations' => (array) ($resolved['operations'] ?? []),
        ];
    }

    protected function convertSubmitLabelChoicePlanToPatch(array $plan, array $selected_context, string $prompt): array
    {
        $resolved = $this->resolveSubmitLabelChoicePlan($plan, $selected_context, $prompt);
        if ($resolved === null) {
            return $plan;
        }

        return [
            'mode' => 'patch',
            'success_message' => (string) ($resolved['success_message'] ?? __('Updated the submit button label.', 'griffinforms-form-builder')),
            'operations' => (array) ($resolved['operations'] ?? []),
        ];
    }

    protected function maybeBuildDirectSubmitLabelPatch(string $prompt, array $selected_context): ?array
    {
        $normalized_prompt = trim($prompt);
        if ($normalized_prompt === '') {
            return null;
        }

        if (!preg_match('/(?:submit[_\s-]?label|submit button label)\b/i', $normalized_prompt)) {
            return null;
        }

        if (
            !preg_match('/\bto\s+"([^"]+)"(?:\s*$)/i', $normalized_prompt, $matches) &&
            !preg_match('/\bto\s+([A-Za-z][A-Za-z0-9&\s-]{1,40})$/i', $normalized_prompt, $matches)
        ) {
            return null;
        }

        $value = $this->normalizeText((string) ($matches[1] ?? ''), 60);
        if ($value === '') {
            return null;
        }

        return [
            'mode' => 'patch',
            'success_message' => __('Updated the submit button label.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'update_form',
                'target' => [
                    'scope' => 'form',
                    'item_id' => absint($selected_context['form_id'] ?? 0),
                ],
                'changes' => [
                    'submit_label' => $value,
                ],
            ]],
        ];
    }

    protected function convertRowHeadingPlanToPatch(array $plan, array $structure, array $selected_context): array
    {
        $resolved = $this->resolveRowHeadingPlan($plan, $structure, $selected_context);
        if ($resolved === null) {
            return $plan;
        }

        return [
            'mode' => 'patch',
            'success_message' => (string) ($resolved['success_message'] ?? __('Updated the row heading.', 'griffinforms-form-builder')),
            'operations' => (array) ($resolved['operations'] ?? []),
        ];
    }

    protected function convertRowDescriptionPlanToPatch(array $plan, array $structure, array $selected_context): array
    {
        $resolved = $this->resolveRowDescriptionPlan($plan, $structure, $selected_context);
        if ($resolved === null) {
            return $plan;
        }

        return [
            'mode' => 'patch',
            'success_message' => (string) ($resolved['success_message'] ?? __('Updated the row description.', 'griffinforms-form-builder')),
            'operations' => (array) ($resolved['operations'] ?? []),
        ];
    }

    protected function convertPageHeadingPlanToPatch(array $plan, array $structure, array $selected_context): array
    {
        $resolved = $this->resolvePageHeadingPlan($plan, $structure, $selected_context);
        if ($resolved === null) {
            return $plan;
        }

        return [
            'mode' => 'patch',
            'success_message' => (string) ($resolved['success_message'] ?? __('Updated the page heading.', 'griffinforms-form-builder')),
            'operations' => (array) ($resolved['operations'] ?? []),
        ];
    }

    protected function convertPageDescriptionPlanToPatch(array $plan, array $structure, array $selected_context): array
    {
        $resolved = $this->resolvePageDescriptionPlan($plan, $structure, $selected_context);
        if ($resolved === null) {
            return $plan;
        }

        return [
            'mode' => 'patch',
            'success_message' => (string) ($resolved['success_message'] ?? __('Updated the page description.', 'griffinforms-form-builder')),
            'operations' => (array) ($resolved['operations'] ?? []),
        ];
    }

    protected function convertFieldLabelPlanToPatch(array $plan, array $structure, array $selected_context, string $prompt = ''): array
    {
        $resolved = $this->resolveFieldLabelPlan($plan, $structure, $selected_context, $prompt);
        if ($resolved === null) {
            return $plan;
        }

        return [
            'mode' => 'patch',
            'success_message' => (string) ($resolved['success_message'] ?? __('Updated the selected field label.', 'griffinforms-form-builder')),
            'operations' => (array) ($resolved['operations'] ?? []),
        ];
    }

    protected function convertFieldDescriptionPlanToPatch(array $plan, array $structure, array $selected_context, string $prompt = ''): array
    {
        $resolved = $this->resolveFieldDescriptionPlan($plan, $structure, $selected_context, $prompt);
        if ($resolved === null) {
            return $plan;
        }

        return [
            'mode' => 'patch',
            'success_message' => (string) ($resolved['success_message'] ?? __('Updated the selected field description.', 'griffinforms-form-builder')),
            'operations' => (array) ($resolved['operations'] ?? []),
        ];
    }

    protected function canAutoResolveFieldPlaceholderPlan(array $plan, array $structure, array $selected_context, string $prompt = ''): bool
    {
        return $this->resolveFieldPlaceholderPlan($plan, $structure, $selected_context, $prompt) !== null;
    }

    protected function convertFieldPlaceholderPlanToPatch(array $plan, array $structure, array $selected_context, string $prompt = ''): array
    {
        $resolved = $this->resolveFieldPlaceholderPlan($plan, $structure, $selected_context, $prompt);
        if ($resolved === null) {
            return $plan;
        }

        return [
            'mode' => 'patch',
            'success_message' => (string) ($resolved['success_message'] ?? __('Updated the selected field placeholder.', 'griffinforms-form-builder')),
            'operations' => (array) ($resolved['operations'] ?? []),
        ];
    }

    protected function convertSingleFieldGrowthPlanToPatch(array $plan, array $structure, array $selected_context, string $prompt = ''): ?array
    {
        $normalized_prompt = strtolower(trim($prompt));
        if (!$this->isFieldGrowthPrompt($normalized_prompt)) {
            return null;
        }

        // Multi-field requests — the AI plan already contains all operations; don't replace it
        if ($this->isMultiFieldPrompt($normalized_prompt)) {
            return null;
        }

        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        if ($selected_type === 'field' && !empty($selected_context['item_id'])) {
            return null;
        }

        $field = $this->inferSingleFieldFromPrompt($normalized_prompt);
        if (!is_array($field) || empty($field['field_type'])) {
            return null;
        }

        $field_type = sanitize_key((string) ($field['field_type'] ?? ''));
        $label = $this->normalizeText((string) ($field['label'] ?? ''), 120);
        if ($field_type === '' || $label === '') {
            return null;
        }

        // These types require subject-aware generation — let the AI handle them
        if (in_array($field_type, ['inputlikert', 'payment'], true)) {
            return null;
        }

        // File upload with constraints — let the AI apply field_settings
        if ($field_type === 'fileupload' && preg_match('/\b(only|accept|image|photo|pdf|doc|video|type|format|limit|size|restrict)\b/', $normalized_prompt)) {
            return null;
        }

        $name = $this->normalizeText((string) ($field['name'] ?? ''), 120);
        if ($name === '') {
            $name = sanitize_title($label);
        }
        if ($name === '') {
            $name = $field_type;
        }

        $target_form_id = absint($selected_context['form_id'] ?? 0);
        if ($target_form_id <= 0) {
            return null;
        }

        $field_payload = [
            'field_type' => $field_type,
            'name' => $name,
            'label' => $label,
            'description' => $this->normalizeText((string) ($field['description'] ?? ''), 180),
            'placeholder' => $this->normalizeText((string) ($field['placeholder'] ?? ''), 120),
            'required' => !empty($field['required']),
        ];

        if (in_array($field_type, ['selectfield', 'inputcheckbox', 'inputradio'], true)) {
            // Only stay in the fast path when we have a hardcoded label set (meal, drink, yes/no, etc.)
            // that produces correct results without AI. For all other prompts — whether they have a subject
            // or none at all — let the AI generate contextual options so defaults are never generic ("Option 1").
            $subject = $this->extractFieldSubjectFromPrompt($normalized_prompt);
            if (!$this->hasHardcodedChoiceLabels($normalized_prompt)) {
                return null;
            }
            $field_payload['options'] = !empty($field['options']) && is_array($field['options'])
                ? $field['options']
                : $this->buildDefaultChoiceOptions($field_type, $normalized_prompt, $subject, $label);
        }

        return [
            'mode' => 'patch',
            'success_message' => sprintf(__('Added %s field.', 'griffinforms-form-builder'), $label),
            'operations' => [[
                'op' => 'insert_field',
                'target' => [
                    'scope' => 'form',
                    'item_id' => $target_form_id,
                ],
                'position' => 'after',
                'field' => $field_payload,
            ]],
        ];
    }

    protected function isEmptySelectedFieldPlaceholderPlan(array $plan, array $selected_context): bool
    {
        if (sanitize_key((string) ($selected_context['item_type'] ?? '')) !== 'field') {
            return false;
        }

        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $changes = (array) ($operation['changes'] ?? []);
        if (sanitize_key((string) array_key_first($changes)) !== 'placeholder') {
            return false;
        }

        return trim((string) ($changes['placeholder'] ?? '')) === '';
    }

    protected function convertPageMovePlanToPatch(array $plan, array $structure, array $selected_context): array
    {
        $resolved = $this->resolvePageMovePlan($plan, $structure, $selected_context);
        if ($resolved === null) {
            return $plan;
        }

        return [
            'mode' => 'patch',
            'success_message' => (string) ($resolved['success_message'] ?? __('Moved the page.', 'griffinforms-form-builder')),
            'operations' => (array) ($resolved['operations'] ?? []),
        ];
    }

    protected function convertRowDeletePlanToPatch(array $plan, array $structure, array $selected_context): array
    {
        $resolved = $this->resolveRowDeletePlan($plan, $structure, $selected_context);
        if ($resolved === null) {
            return $plan;
        }

        return [
            'mode' => 'patch',
            'success_message' => (string) ($resolved['success_message'] ?? __('Deleted the row.', 'griffinforms-form-builder')),
            'operations' => (array) ($resolved['operations'] ?? []),
        ];
    }

    protected function resolveNavigationLabelPlan(array $plan, array $structure, array $selected_context): ?array
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $changes = (array) ($operation['changes'] ?? []);
        $property = sanitize_key((string) array_key_first($changes));
        $page_count = count((array) ($structure['pages'] ?? []));
        $pages = array_values((array) ($structure['pages'] ?? []));
        $selected_page_id = absint($selected_context['item_id'] ?? 0);
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_page = ($selected_type === 'formpage' && $selected_page_id > 0) ? $selected_page_id : 0;
        $resolved_value = $this->resolveNavigationLabelValue($property, $structure, $selected_context);

        if ($property === '' || $resolved_value === null || $resolved_value === '') {
            return null;
        }

        if ($property === 'submit_label') {
            return null;
        }

        // Self-update guard: if the resolved value matches the current value on the selected page,
        // return null so the AI's advisory/patch_with_user_input passes through instead.
        if ($selected_page > 0) {
            foreach ($pages as $page) {
                if (absint($page['item_id'] ?? 0) === $selected_page) {
                    $current_value = sanitize_text_field((string) ($page[$property] ?? ''));
                    if ($current_value !== '' && strtolower($current_value) === strtolower($resolved_value)) {
                        return null;
                    }
                    break;
                }
            }
        }

        // Position guards: next_label has no effect on the last page; previous_label has no effect on the first page.
        if ($selected_page > 0 && $page_count > 1) {
            $first_page_id = absint($pages[0]['item_id'] ?? 0);
            $last_page_id  = absint($pages[$page_count - 1]['item_id'] ?? 0);
            if ($property === 'next_label' && $selected_page === $last_page_id) {
                return [
                    'success_message' => __('The last page has no Next button. Select a different page and try again.', 'griffinforms-form-builder'),
                    'operations' => [],
                ];
            }
            if ($property === 'previous_label' && $selected_page === $first_page_id) {
                return [
                    'success_message' => __('The first page has no Previous button. Select a different page and try again.', 'griffinforms-form-builder'),
                    'operations' => [],
                ];
            }
        }

        if ($page_count <= 1) {
            return [
                'success_message' => $property === 'previous_label'
                    ? __('There is no previous button on single-page forms.', 'griffinforms-form-builder')
                    : __('There is no next button on single-page forms.', 'griffinforms-form-builder'),
                'operations' => [],
            ];
        }

        if ($page_count === 2) {
            $target_page_id = 0;
            if ($selected_page > 0) {
                $target_page_id = $selected_page;
            } elseif ($property === 'next_label') {
                $target_page_id = absint($pages[0]['item_id'] ?? 0);
            } elseif ($property === 'previous_label') {
                $target_page_id = absint($pages[1]['item_id'] ?? 0);
            }

            if ($target_page_id <= 0) {
                return null;
            }

            return [
                'success_message' => $property === 'previous_label'
                    ? __('Updated the selected page previous button label.', 'griffinforms-form-builder')
                    : __('Updated the selected page next button label.', 'griffinforms-form-builder'),
                'operations' => [[
                    'op' => 'update_page',
                    'target' => [
                        'scope' => 'page',
                        'item_id' => $target_page_id,
                    ],
                    'changes' => [
                        $property => $resolved_value,
                    ],
                ]],
            ];
        }

        if ($selected_page > 0) {
            $page_property = $property === 'previous_label' ? 'previous_label' : 'next_label';
            return [
                'success_message' => $property === 'previous_label'
                    ? __('Updated the selected page previous button label.', 'griffinforms-form-builder')
                    : __('Updated the selected page next button label.', 'griffinforms-form-builder'),
                'operations' => [[
                    'op' => 'update_page',
                    'target' => [
                        'scope' => 'page',
                        'item_id' => $selected_page,
                    ],
                    'changes' => [
                        $page_property => $resolved_value,
                    ],
                ]],
            ];
        }

        $operations_out = [];
        foreach ($pages as $page) {
            $page_id = absint($page['item_id'] ?? 0);
            if ($page_id <= 0) {
                continue;
            }

            if ($property === 'next_label') {
                $operations_out[] = [
                    'op' => 'update_page',
                    'target' => [
                        'scope' => 'page',
                        'item_id' => $page_id,
                    ],
                    'changes' => [
                        'next_label' => __('Continue', 'griffinforms-form-builder'),
                    ],
                ];
                continue;
            }

            $operations_out[] = [
                'op' => 'update_page',
                'target' => [
                    'scope' => 'page',
                    'item_id' => $page_id,
                ],
                'changes' => [
                    'previous_label' => __('Back', 'griffinforms-form-builder'),
                ],
            ];
        }

        return [
            'success_message' => $property === 'previous_label'
                ? __('Updated the previous button label across all pages.', 'griffinforms-form-builder')
                : __('Updated the next button label across all pages.', 'griffinforms-form-builder'),
            'operations' => $operations_out,
        ];
    }

    protected function resolveRowHeadingPlan(array $plan, array $structure, array $selected_context): ?array
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $changes = (array) ($operation['changes'] ?? []);
        $property = sanitize_key((string) array_key_first($changes));
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_row_id = absint($selected_context['item_id'] ?? 0);
        $resolved_value = $this->normalizeText((string) ($changes['heading'] ?? ''), 120);
        if ($resolved_value === '') {
            $resolved_value = $this->resolveRowHeadingValueFromFields(
                (array) ($this->findRowSnapshot($structure, $selected_row_id) ?? []),
                $structure,
                true
            );
        }

        if ($property !== 'heading' || !in_array($selected_type, ['pagerow', 'row'], true) || $selected_row_id <= 0 || $resolved_value === '') {
            return null;
        }

        $row = $this->findRowSnapshot($structure, $selected_row_id);
        if (!is_array($row)) {
            return null;
        }

        return [
            'success_message' => __('Updated the row heading.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'update_row',
                'target' => [
                    'scope' => 'row',
                    'item_id' => $selected_row_id,
                ],
                'changes' => [
                    'heading' => $resolved_value,
                ],
            ]],
        ];
    }

    protected function resolveRowDescriptionPlan(array $plan, array $structure, array $selected_context): ?array
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $changes = (array) ($operation['changes'] ?? []);
        $property = sanitize_key((string) array_key_first($changes));
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_row_id = absint($selected_context['item_id'] ?? 0);
        $resolved_value = $this->normalizeText((string) ($changes['description'] ?? ''), 180);
        if ($resolved_value === '') {
            $resolved_value = $this->resolveRowDescriptionValue(
                (array) ($this->findRowSnapshot($structure, $selected_row_id) ?? []),
                (string) ($changes['description'] ?? '')
            );
        }

        if ($property !== 'description' || !in_array($selected_type, ['pagerow', 'row'], true) || $selected_row_id <= 0 || $resolved_value === '') {
            return null;
        }

        $row = $this->findRowSnapshot($structure, $selected_row_id);
        if (!is_array($row)) {
            return null;
        }

        return [
            'success_message' => __('Updated the row description.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'update_row',
                'target' => [
                    'scope' => 'row',
                    'item_id' => $selected_row_id,
                ],
                'changes' => [
                    'description' => $resolved_value,
                ],
            ]],
        ];
    }

    protected function resolvePageHeadingPlan(array $plan, array $structure, array $selected_context): ?array
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $changes = (array) ($operation['changes'] ?? []);
        $property = sanitize_key((string) array_key_first($changes));
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_page_id = absint($selected_context['item_id'] ?? 0);
        $resolved_value = $this->normalizeText((string) ($changes['heading'] ?? ''), 120);
        if ($resolved_value === '') {
            $resolved_value = $this->resolvePageHeadingValue(
                (array) ($this->findPageSnapshot($structure, $selected_page_id) ?? []),
                true
            );
        }

        if ($property !== 'heading' || $selected_type !== 'formpage' || $selected_page_id <= 0 || $resolved_value === '') {
            return null;
        }

        $page = $this->findPageSnapshot($structure, $selected_page_id);
        if (!is_array($page)) {
            return null;
        }

        return [
            'success_message' => __('Updated the selected page heading.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'update_page',
                'target' => [
                    'scope' => 'page',
                    'item_id' => $selected_page_id,
                ],
                'changes' => [
                    'heading' => $resolved_value,
                ],
            ]],
        ];
    }

    protected function resolvePageDescriptionPlan(array $plan, array $structure, array $selected_context): ?array
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $changes = (array) ($operation['changes'] ?? []);
        $property = sanitize_key((string) array_key_first($changes));
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_page_id = absint($selected_context['item_id'] ?? 0);
        $resolved_value = $this->normalizeText((string) ($changes['description'] ?? ''), 180);
        if ($resolved_value === '') {
            $resolved_value = $this->resolvePageDescriptionValue(
                (array) ($this->findPageSnapshot($structure, $selected_page_id) ?? []),
                (string) ($changes['description'] ?? '')
            );
        }

        if ($property !== 'description' || $selected_type !== 'formpage' || $selected_page_id <= 0 || $resolved_value === '') {
            return null;
        }

        $page = $this->findPageSnapshot($structure, $selected_page_id);
        if (!is_array($page)) {
            return null;
        }

        return [
            'success_message' => __('Updated the selected page description.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'update_page',
                'target' => [
                    'scope' => 'page',
                    'item_id' => $selected_page_id,
                ],
                'changes' => [
                    'description' => $resolved_value,
                ],
            ]],
        ];
    }

    protected function resolveFieldLabelPlan(array $plan, array $structure, array $selected_context, string $prompt = ''): ?array
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $changes = (array) ($operation['changes'] ?? []);
        $property = sanitize_key((string) array_key_first($changes));
        $target = (array) ($operation['target'] ?? []);
        $plan_prompt = $this->normalizeText((string) ($plan['prompt'] ?? ''), 160);
        $prompt_source = (string) ($target['prompt'] ?? '');
        if ($prompt_source === '') {
            $prompt_source = $prompt !== '' ? $prompt : $plan_prompt;
        }
        $prompt = $this->normalizeText($prompt_source, 160);
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_field_id = absint($selected_context['item_id'] ?? 0);
        $field = $this->findFieldSnapshot($structure, $selected_field_id);
        if (!is_array($field)) {
            return null;
        }

        $resolved_value = $this->normalizeText((string) ($changes['label'] ?? ''), 120);
        $prompt_hint = $prompt;
        if ($this->looksLikeFieldIdentifier($resolved_value) || $resolved_value === '') {
            $resolved_value = $this->resolveFieldLabelValue($field, $prompt_hint !== '' ? $prompt_hint : (string) ($changes['label'] ?? ''), $selected_context);
        } else {
            $resolved_value = $this->humanizeFieldLabelCandidate($resolved_value);
        }


        if ($property !== 'label' || $selected_type !== 'field' || $selected_field_id <= 0 || $resolved_value === '') {
            return null;
        }

        return [
            'success_message' => __('Updated the selected field label.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'update_field',
                'target' => [
                    'scope' => 'field',
                    'item_id' => $selected_field_id,
                ],
                'changes' => [
                    'label' => $resolved_value,
                ],
            ]],
        ];
    }

    protected function resolveFieldDescriptionPlan(array $plan, array $structure, array $selected_context, string $prompt = ''): ?array
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $changes = (array) ($operation['changes'] ?? []);
        $property = sanitize_key((string) array_key_first($changes));
        $plan_prompt = $this->normalizeText((string) ($plan['prompt'] ?? ''), 160);
        if ($prompt === '') {
            $prompt = $plan_prompt;
        }
        $prompt = $this->normalizeText($prompt, 160);
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_field_id = absint($selected_context['item_id'] ?? 0);
        $resolved_value = $this->normalizeText((string) ($changes['description'] ?? ''), 180);
        $skip_local_resolver = preg_match('/\bformat\s+hint\b|\bformat_type\b|\bcountry\s+code\b|\binternational\b|\brange\s+hint\b|\badd\s+range\b|\bdate\s+window\s+hint\b|\badd\s+date\s+window\b|\bchar(?:acter)?\s+limit\s+hint\b|\badd\s+char(?:acter)?\s+limit\b|\bdescribe\s+upload\s+requirements?\b|\bupload\s+requirements?\b|\bpolish\s+description\b/i', $prompt);
        if ($resolved_value === '' && !$skip_local_resolver) {
            $resolved_value = $this->resolveFieldDescriptionValue(
                (array) ($this->findFieldSnapshot($structure, $selected_field_id) ?? []),
                $prompt !== '' ? $prompt : (string) ($changes['description'] ?? ''),
                $selected_context
            );
        }

        if ($property !== 'description' || $selected_type !== 'field' || $selected_field_id <= 0 || $resolved_value === '') {
            return null;
        }

        $field = $this->findFieldSnapshot($structure, $selected_field_id);
        if (!is_array($field)) {
            return null;
        }

        return [
            'success_message' => __('Updated the selected field description.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'update_field',
                'target' => [
                    'scope' => 'field',
                    'item_id' => $selected_field_id,
                ],
                'changes' => [
                    'description' => $resolved_value,
                ],
            ]],
        ];
    }

    protected function resolveFieldPlaceholderPlan(array $plan, array $structure, array $selected_context, string $prompt = ''): ?array
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $changes = (array) ($operation['changes'] ?? []);
        $property = sanitize_key((string) array_key_first($changes));
        $plan_prompt = $this->normalizeText((string) ($plan['prompt'] ?? ''), 160);
        if ($prompt === '') {
            $prompt = $plan_prompt;
        }
        $prompt = $this->normalizeText($prompt, 160);

        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_field_id = absint($selected_context['item_id'] ?? 0);
        if ($property !== 'placeholder' || $selected_type !== 'field' || $selected_field_id <= 0) {
            return null;
        }

        $field = $this->findFieldSnapshot($structure, $selected_field_id);
        if (!is_array($field)) {
            return null;
        }

        $resolved_value = $this->normalizeText((string) ($changes['placeholder'] ?? ''), 120);
        if ($resolved_value === '') {
            $resolved_value = $this->resolveFieldPlaceholderValue($field, $prompt !== '' ? $prompt : (string) ($changes['placeholder'] ?? ''), $selected_context);
        }

        if ($resolved_value === '') {
            return null;
        }

        return [
            'success_message' => __('Updated the selected field placeholder.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'update_field',
                'target' => [
                    'scope' => 'field',
                    'item_id' => $selected_field_id,
                ],
                'changes' => [
                    'placeholder' => $resolved_value,
                ],
            ]],
        ];
    }

    protected function resolvePageMovePlan(array $plan, array $structure, array $selected_context): ?array
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $op = sanitize_key((string) ($operation['op'] ?? ''));
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_page_id = absint($selected_context['item_id'] ?? 0);

        if ($op !== 'move_page' || $selected_type !== 'formpage' || $selected_page_id <= 0) {
            return null;
        }

        $page = $this->findPageSnapshot($structure, $selected_page_id);
        if (!is_array($page)) {
            return null;
        }

        $pages = array_values((array) ($structure['pages'] ?? []));
        $page_ids = array_values(array_filter(array_map(static function ($page) {
            $page = (array) $page;
            return absint($page['item_id'] ?? 0);
        }, $pages)));
        $current_index = array_search($selected_page_id, $page_ids, true);
        if ($current_index === false) {
            return null;
        }

        $direction = sanitize_key((string) ($operation['direction'] ?? ''));
        $position = sanitize_key((string) ($operation['position'] ?? ''));
        $requested_position = $this->page_prompt_helper->extractRequestedPagePosition((string) ($selected_context['item_label'] ?? ''));
        if ($direction === '' && is_array($requested_position)) {
            $direction = (($requested_position['type'] ?? '') === 'edge' && (string) ($requested_position['value'] ?? '') === 'bottom') ? 'down' : 'up';
        }

        if ($direction === '' && $position !== '') {
            $direction = in_array($position, ['after', 'below', 'down'], true) ? 'down' : 'up';
        }

        if ($direction === '') {
            return null;
        }

        $anchor_id = 0;
        $anchor_position = 'before';

        if ($direction === 'up') {
            if ($current_index <= 0) {
                return [
                    'success_message' => __('This page is already at the top of the form.', 'griffinforms-form-builder'),
                    'operations' => [],
                ];
            }

            $anchor_id = absint($page_ids[$current_index - 1] ?? 0);
            $anchor_position = 'before';
        } else {
            if ($current_index >= count($page_ids) - 1) {
                return [
                    'success_message' => __('This page is already at the bottom of the form.', 'griffinforms-form-builder'),
                    'operations' => [],
                ];
            }

            $anchor_id = absint($page_ids[$current_index + 1] ?? 0);
            $anchor_position = 'after';
        }

        if ($anchor_id <= 0) {
            return null;
        }

        return [
            'success_message' => $direction === 'up'
                ? __('Moved the page up.', 'griffinforms-form-builder')
                : __('Moved the page down.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'move_page',
                'target' => [
                    'scope' => 'page',
                    'item_id' => $selected_page_id,
                ],
                'anchor' => [
                    'scope' => 'page',
                    'item_id' => $anchor_id,
                ],
                'position' => $anchor_position,
            ]],
        ];
    }

    protected function resolveRowDeletePlan(array $plan, array $structure, array $selected_context): ?array
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $op = sanitize_key((string) ($operation['op'] ?? ''));
        $target = (array) ($operation['target'] ?? []);
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_row_id = absint($selected_context['item_id'] ?? 0);

        if ($op !== 'remove_row') {
            return null;
        }

        if (empty($target)) {
            if (!in_array($selected_type, ['pagerow', 'row'], true) || $selected_row_id <= 0) {
                return null;
            }

            $row = $this->findRowSnapshot($structure, $selected_row_id);
            if (!is_array($row)) {
                return null;
            }

            $page = $this->findPageSnapshotForRow($structure, $selected_row_id);
            if (is_array($page) && count((array) ($page['rows'] ?? [])) <= 1) {
                return [
                    'success_message' => __('This is the only row on the page, so it cannot be deleted.', 'griffinforms-form-builder'),
                    'operations' => [],
                ];
            }

            return [
                'success_message' => __('Deleted the selected row.', 'griffinforms-form-builder'),
                'operations' => [[
                    'op' => 'remove_row',
                    'target' => [
                        'scope' => 'row',
                        'item_id' => $selected_row_id,
                    ],
                ]],
            ];
        }

        $scope = sanitize_key((string) ($target['scope'] ?? ''));
        if ($scope !== 'row') {
            return null;
        }

        $row_id = absint($target['item_id'] ?? 0);
        if ($row_id <= 0) {
            return null;
        }

        $page = $this->findPageSnapshotForRow($structure, $row_id);
        if (is_array($page) && count((array) ($page['rows'] ?? [])) <= 1) {
            return [
                'success_message' => __('This is the only row on the page, so it cannot be deleted.', 'griffinforms-form-builder'),
                'operations' => [],
            ];
        }

        return [
            'success_message' => __('Deleted the row.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'remove_row',
                'target' => [
                    'scope' => 'row',
                    'item_id' => $row_id,
                ],
            ]],
        ];
    }

    protected function resolveFieldLabelValue(array $field, string $prompt, array $selected_context): string
    {
        $existing = $this->normalizeText((string) ($field['label'] ?? ''), 120);
        $field_type = sanitize_key((string) ($field['field_type'] ?? ''));
        $field_name = $this->normalizeText((string) ($field['name'] ?? ''), 120);
        $context_label = $this->normalizeText((string) ($selected_context['item_label'] ?? ''), 120);
        $prompt = strtolower(trim($prompt));
        $wants_rewrite = (bool) preg_match('/\b(improve|update|rewrite|refine|polish|make|better|add|change|rename|turn)\b/', $prompt);


        if (!$wants_rewrite && $existing !== '' && !$this->looksLikeFieldIdentifier($existing)) {
            return $existing;
        }

        if ($wants_rewrite) {
            $rewrite_candidates = [];

            foreach ([$context_label, $field_name] as $candidate) {
                $humanized = $this->humanizeFieldLabelCandidate($candidate);
                if ($humanized !== '' && strcasecmp($humanized, $existing) !== 0) {
                    $rewrite_candidates[] = $humanized;
                }
            }

            if ($field_type === 'selectfield') {
                $option_context_label = $this->resolveSelectFieldContextLabel($field, $selected_context);
                if ($option_context_label !== '' && strcasecmp($option_context_label, $existing) !== 0) {
                    array_unshift($rewrite_candidates, $option_context_label);
                }

                $select_candidates = [
                    $context_label,
                    $field_name,
                    $this->normalizeText((string) ($field['description'] ?? ''), 120),
                    'Select',
                ];

                foreach ($select_candidates as $candidate) {
                    $humanized = $this->humanizeFieldLabelCandidate($candidate);
                    if ($humanized !== '' && strcasecmp($humanized, $existing) !== 0) {
                        array_unshift($rewrite_candidates, $humanized);
                    }
                }

                $rewrite_candidates = array_values(array_unique($rewrite_candidates));
            }

            if (in_array($field_type, ['inputemail', 'inputphone', 'inputaddress', 'textarea', 'inputnumber', 'inputdate', 'fileupload', 'signature', 'inputrating', 'inputrichtext'], true)) {
                $type_candidates = [
                    'inputemail' => __('Email Address', 'griffinforms-form-builder'),
                    'inputphone' => __('Phone Number', 'griffinforms-form-builder'),
                    'inputaddress' => __('Mailing Address', 'griffinforms-form-builder'),
                    'textarea' => __('Message', 'griffinforms-form-builder'),
                    'inputnumber' => __('Number', 'griffinforms-form-builder'),
                    'inputdate' => __('Date', 'griffinforms-form-builder'),
                    'fileupload' => __('Upload File', 'griffinforms-form-builder'),
                    'signature' => __('Signature', 'griffinforms-form-builder'),
                    'inputrating' => __('Rating', 'griffinforms-form-builder'),
                    'inputrichtext' => __('Rich Text', 'griffinforms-form-builder'),
                ];

                $candidate = (string) ($type_candidates[$field_type] ?? '');
                if ($candidate !== '' && strcasecmp($candidate, $existing) !== 0) {
                    array_unshift($rewrite_candidates, $candidate);
                }
            }

            foreach ($rewrite_candidates as $candidate) {
                if ($candidate !== '') {
                    return $candidate;
                }
            }
        }

        foreach ([$context_label, $field_name] as $candidate) {
            $humanized = $this->humanizeFieldLabelCandidate($candidate);
            if ($humanized !== '' && strcasecmp($humanized, $existing) !== 0) {
                return $humanized;
            }
        }

        return $existing !== '' ? $existing : __('Field', 'griffinforms-form-builder');
    }

    protected function resolveSelectFieldContextLabel(array $field, array $selected_context): string
    {
        $options = (array) ($field['options'] ?? []);
        if (empty($options)) {
            return '';
        }

        $labels = [];
        foreach ($options as $option) {
            $label = $this->normalizeText((string) ($option['label'] ?? ''), 60);
            if ($label !== '') {
                $labels[] = $label;
            }
        }

        $blob = strtolower(implode(' ', array_slice($labels, 0, 8)));
        if ($blob === '') {
            return '';
        }

        $candidates = [
            '/\b(state|province|county|region)\b/' => __('Select Your State', 'griffinforms-form-builder'),
            '/\b(country|nation)\b/' => __('Select Your Country', 'griffinforms-form-builder'),
            '/\b(city|town|municipality)\b/' => __('Select Your City', 'griffinforms-form-builder'),
            '/\b(category|type|kind|group)\b/' => __('Select a Category', 'griffinforms-form-builder'),
            '/\b(status|stage|progress)\b/' => __('Select a Status', 'griffinforms-form-builder'),
            '/\b(yes|no|maybe)\b/' => __('Choose an Answer', 'griffinforms-form-builder'),
            '/\b(business|company|organization)\b/' => __('Select a Business Type', 'griffinforms-form-builder'),
            '/\b(department|team|role)\b/' => __('Select a Department', 'griffinforms-form-builder'),
            '/\b(meal|food|diet|dietary)\b/' => __('Select a Meal Choice', 'griffinforms-form-builder'),
            '/\b(subscription|plan|package)\b/' => __('Select a Plan', 'griffinforms-form-builder'),
        ];

        foreach ($candidates as $pattern => $candidate) {
            if (preg_match($pattern, $blob)) {
                return $candidate;
            }
        }

        $context_label = $this->normalizeText((string) ($selected_context['item_label'] ?? ''), 120);
        if ($context_label !== '') {
            return $context_label;
        }

        return __('Select an Option', 'griffinforms-form-builder');
    }

    protected function humanizeFieldLabelCandidate(string $candidate): string
    {
        $candidate = $this->normalizeText($candidate, 120);
        if ($candidate === '') {
            return '';
        }

        $lower = strtolower($candidate);
        if (preg_match('/^[a-z0-9]+(?:_[a-z0-9]+)+$/', $lower) || preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)+$/', $lower)) {
            $candidate = str_replace(['_', '-'], ' ', $lower);
        }

        $candidate = preg_replace('/\s+/', ' ', trim($candidate));
        if ($candidate === '') {
            return '';
        }

        return mb_convert_case($candidate, MB_CASE_TITLE, 'UTF-8');
    }

    protected function looksLikeFieldIdentifier(string $value): bool
    {
        $value = strtolower(trim($value));
        if ($value === '') {
            return false;
        }

        return (bool) preg_match('/^[a-z0-9]+(?:[_-][a-z0-9]+)+$/', $value);
    }

    protected function resolveFieldDescriptionValue(array $field, string $prompt, array $selected_context): string
    {
        $existing = $this->normalizeText((string) ($field['description'] ?? ''), 180);
        $normalized_prompt = strtolower(trim($prompt));
        $wants_rewrite = (bool) preg_match('/\b(add|update|change|rewrite|refine|polish|improve|describe|description)\b/', $normalized_prompt);
        $sentence_count = $this->extractRequestedSentenceCount($normalized_prompt);
        $field_settings = $this->normalizeContextArray((array) ($field['field_settings'] ?? []));


        if (!$wants_rewrite && $existing !== '') {
            return $existing;
        }

        if (!empty($field_settings)) {
            $settings_blob = strtolower(wp_json_encode($field_settings, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
            if (preg_match('/\b(min|max|limit|length|count|required|allowed|blocked|custom_mimes|options|choices|country|tld|url|email|phone|date|number|pattern|regex)\b/', $settings_blob)) {
                $existing = $existing !== '' ? $existing : $this->normalizeText((string) ($field['label'] ?? ''), 120);
            }
        }

        $label = $this->resolveFieldLabelValue($field, $prompt, $selected_context);
        if ($label === '') {
            $label = __('Field', 'griffinforms-form-builder');
        }

        $field_type = sanitize_key((string) ($field['field_type'] ?? ''));
        if ($field_type === 'selectfield') {
            $select_candidates = [
                __('Choose the option that best matches your response.', 'griffinforms-form-builder'),
                __('Select the choice that applies to you.', 'griffinforms-form-builder'),
                __('Pick the option that best fits your answer.', 'griffinforms-form-builder'),
            ];

            foreach ($select_candidates as $candidate) {
                if ($candidate !== '' && strcasecmp($candidate, $existing) !== 0) {
                    return $candidate;
                }
            }
        }

        $first_sentence = match ($field_type) {
            'inputemail' => __('Enter your email address here.', 'griffinforms-form-builder'),
            'inputphone' => __('Enter a phone number where we can reach you.', 'griffinforms-form-builder'),
            'inputaddress' => __('Enter your mailing address.', 'griffinforms-form-builder'),
            'textarea' => __('Use this field to share a longer message or explanation.', 'griffinforms-form-builder'),
            'inputnumber' => __('Enter a number that applies to your response.', 'griffinforms-form-builder'),
            'inputdate' => __('Select the date that applies to your response.', 'griffinforms-form-builder'),
            'signature' => __('Sign your name in the space provided.', 'griffinforms-form-builder'),
            'inputrating' => __('Choose the rating that best matches your response.', 'griffinforms-form-builder'),
            'inputrichtext' => __('Use this field to enter formatted text or a longer note.', 'griffinforms-form-builder'),
            'fileupload' => $this->resolveFileUploadDescriptionSentence($field),
            default => sprintf(__('Use this field to provide your %s.', 'griffinforms-form-builder'), strtolower($label)),
        };

        $second_sentence = match ($field_type) {
            'inputemail' => __('We will use it to follow up about your submission.', 'griffinforms-form-builder'),
            'inputphone' => __('We will only use it if we need to contact you about your submission.', 'griffinforms-form-builder'),
            'inputaddress' => __('This helps us format your contact details correctly.', 'griffinforms-form-builder'),
            'textarea' => __('Include any details that help us understand your request.', 'griffinforms-form-builder'),
            'inputnumber' => __('Add any number that helps complete the form accurately.', 'griffinforms-form-builder'),
            'inputdate' => __('Choose the most appropriate date for this request.', 'griffinforms-form-builder'),
            'signature' => __('Use a clear signature that matches the name on the form.', 'griffinforms-form-builder'),
            'inputrating' => __('Choose the score or star rating that fits best.', 'griffinforms-form-builder'),
            'inputrichtext' => __('You can format this text if you need headings, bold text, or lists.', 'griffinforms-form-builder'),
            'fileupload' => __('Make sure the file is clear and matches the requested format.', 'griffinforms-form-builder'),
            default => __('This helps us route your request and respond accurately.', 'griffinforms-form-builder'),
        };

        if ($sentence_count <= 1) {
            return $first_sentence;
        }

        return trim($first_sentence . ' ' . $second_sentence);
    }

    protected function extractRequestedSentenceCount(string $prompt): int
    {
        if (preg_match('/\b(\d+)\s*(?:sentence|sentences)\b/', $prompt, $matches)) {
            return max(1, min(3, absint($matches[1])));
        }

        if (preg_match('/\b(one|single)\s+sentence\b/', $prompt)) {
            return 1;
        }

        if (preg_match('/\b(two|double|2)\s+sentence\b/', $prompt)) {
            return 2;
        }

        if (preg_match('/\b(three|triple|3)\s+sentence\b/', $prompt)) {
            return 3;
        }

        return 1;
    }

    protected function findFieldSnapshot(array $structure, int $field_id): ?array
    {
        foreach ((array) ($structure['pages'] ?? []) as $page) {
            foreach ((array) ($page['rows'] ?? []) as $row) {
                foreach ((array) ($row['columns'] ?? []) as $column) {
                    foreach ((array) ($column['fields'] ?? []) as $field) {
                        if (absint($field['item_id'] ?? 0) === $field_id) {
                            return (array) $field;
                        }
                    }
                }
            }
        }

        return null;
    }

    protected function resolveSubmitLabelChoicePlan(array $plan, array $selected_context, string $prompt): ?array
    {
        $draft = (array) ($plan['draft_patch'] ?? []);
        $operations = (array) ($draft['operations'] ?? []);
        $operation = (array) ($operations[0] ?? []);
        $changes = (array) ($operation['changes'] ?? []);
        $property = sanitize_key((string) array_key_first($changes));
        if ($property !== 'submit_label') {
            return null;
        }

        $choices = array_values((array) ($plan['required_inputs'] ?? []));
        if (empty($choices)) {
            return null;
        }

        $normalized_prompt = $this->normalizeText($prompt, 120);
        if ($normalized_prompt === '') {
            return null;
        }

        foreach ($choices as $choice) {
            $choice = (array) $choice;
            $label = $this->normalizeText((string) ($choice['label'] ?? ''), 120);
            $value = $this->normalizeText((string) ($choice['value'] ?? ''), 120);
            if ($label === '' || $value === '') {
                continue;
            }

            $exact_match = strcasecmp($normalized_prompt, $label) === 0 || strcasecmp($normalized_prompt, $value) === 0;
            $contains_quoted = preg_match('/\bto\s+"' . preg_quote($value, '/') . '"/i', $normalized_prompt)
                || preg_match('/\bto\s+"' . preg_quote($label, '/') . '"/i', $normalized_prompt);
            $contains_bare = preg_match('/\bto\s+' . preg_quote($value, '/') . '(?:\s|$)/i', $normalized_prompt)
                || preg_match('/\bto\s+' . preg_quote($label, '/') . '(?:\s|$)/i', $normalized_prompt);

            if (!$exact_match && !$contains_quoted && !$contains_bare) {
                continue;
            }

            return [
                'success_message' => __('Updated the submit button label.', 'griffinforms-form-builder'),
                'operations' => [[
                    'op' => 'update_form',
                    'target' => [
                        'scope' => 'form',
                        'item_id' => absint($selected_context['form_id'] ?? 0),
                    ],
                    'changes' => [
                        'submit_label' => $value,
                    ],
                ]],
            ];
        }

        return null;
    }

    protected function findRowSnapshot(array $structure, int $row_id): ?array
    {
        foreach ((array) ($structure['pages'] ?? []) as $page) {
            foreach ((array) ($page['rows'] ?? []) as $row) {
                if (absint($row['item_id'] ?? 0) === $row_id) {
                    return (array) $row;
                }
            }
        }

        return null;
    }

    protected function findPageSnapshot(array $structure, int $page_id): ?array
    {
        foreach ((array) ($structure['pages'] ?? []) as $page) {
            if (absint($page['item_id'] ?? 0) === $page_id) {
                return (array) $page;
            }
        }

        return null;
    }

    protected function findPageSnapshotForRow(array $structure, int $row_id): ?array
    {
        foreach ((array) ($structure['pages'] ?? []) as $page) {
            foreach ((array) ($page['rows'] ?? []) as $row) {
                if (absint($row['item_id'] ?? 0) === $row_id) {
                    return (array) $page;
                }
            }
        }

        return null;
    }

    protected function resolveRowHeadingValue(array $row, array $structure): string
    {
        return $this->resolveRowHeadingValueFromFields($row, $structure, false);
    }

    protected function resolvePageHeadingValue(array $page, bool $force_rewrite = false): string
    {
        // Page headings are visible to end users in the live form, so prefer polished, public-facing copy.
        $existing = $this->normalizeText((string) ($page['heading'] ?? ''), 120);
        if (!$force_rewrite && $existing !== '' && !in_array(strtolower($existing), ['page', 'step'], true)) {
            return $existing;
        }

        $name = $this->normalizeText((string) ($page['name'] ?? ''), 120);
        if ($name !== '' && !in_array(strtolower($name), ['page', 'step'], true)) {
            return $name;
        }

        return $force_rewrite ? __('Section Details', 'griffinforms-form-builder') : ($existing !== '' ? $existing : __('Section', 'griffinforms-form-builder'));
    }

    protected function resolvePageDescriptionValue(array $page, string $prompt = ''): string
    {
        // Page descriptions are visible to end users in the live form, so generate public-facing copy.
        $normalized_prompt = strtolower(trim($prompt));
        $line_count = $this->extractRequestedLineCount($normalized_prompt);
        $line_count = $line_count > 0 ? min($line_count, 4) : 1;
        $existing = $this->normalizeText((string) ($page['description'] ?? ''), 180);

        $line1 = __('Use this page to capture the next set of details.', 'griffinforms-form-builder');
        $line2 = __('Keep your answer focused on the information requested below.', 'griffinforms-form-builder');

        if (preg_match('/\b(contact|name|email|phone|address)\b/', $normalized_prompt)) {
            $line1 = __('Use this page to collect contact details.', 'griffinforms-form-builder');
            $line2 = __('Include the information needed to reach the right person.', 'griffinforms-form-builder');
        } elseif (preg_match('/\b(billing|invoice|payment|card|price|amount)\b/', $normalized_prompt)) {
            $line1 = __('Use this page to capture billing or payment details.', 'griffinforms-form-builder');
            $line2 = __('Include the information needed to complete this step.', 'griffinforms-form-builder');
        } elseif (preg_match('/\b(company|business|organization|job|title|role)\b/', $normalized_prompt)) {
            $line1 = __('Use this page to collect business or profile details.', 'griffinforms-form-builder');
            $line2 = __('Share the information that best describes your situation.', 'griffinforms-form-builder');
        } elseif (preg_match('/\b(message|comment|notes|details|description|about)\b/', $normalized_prompt)) {
            $line1 = __('Use this page to share the details requested here.', 'griffinforms-form-builder');
            $line2 = __('A short explanation is usually enough unless more context is needed.', 'griffinforms-form-builder');
        }

        $parts = [trim($line1)];
        if ($line_count >= 2) {
            $parts[] = trim($line2);
        }
        if ($line_count >= 3) {
            $parts[] = __('Add any extra context needed to complete this page.', 'griffinforms-form-builder');
        }
        if ($line_count >= 4) {
            $parts[] = __('Keep your response clear and specific.', 'griffinforms-form-builder');
        }

        $value = trim(implode(' ', array_filter($parts)));
        return $value !== '' ? $value : $existing;
    }

    protected function resolveFileUploadDescriptionSentence(array $field): string
    {
        $supported_types = $this->resolveSupportedFileTypesFromFieldSettings((array) ($field['field_settings'] ?? []));
        if (!empty($supported_types)) {
            $joined = implode(', ', $supported_types);
            return sprintf(__('Upload a file in one of the supported formats: %s.', 'griffinforms-form-builder'), $joined);
        }

        return __('Upload the file we requested here.', 'griffinforms-form-builder');
    }

    protected function resolveSupportedFileTypesFromFieldSettings(array $field_settings): array
    {
        $field_settings = $this->normalizeContextArray($field_settings);
        $custom_mimes = (array) ($field_settings['custom_mimes'] ?? []);
        $custom_mimes = array_values(array_filter(array_map('sanitize_text_field', array_map(static fn($item) => (string) $item, $custom_mimes))));

        return $custom_mimes;
    }

    protected function normalizeContextArray(array $value): array
    {
        $normalized = [];

        foreach ($value as $key => $item) {
            if ($item === null || $item === '' || $item === []) {
                continue;
            }

            if (is_array($item)) {
                $nested = $this->normalizeContextArray($item);
                if (!empty($nested)) {
                    $normalized[sanitize_key((string) $key)] = $nested;
                }
                continue;
            }

            $normalized[sanitize_key((string) $key)] = is_bool($item) || is_int($item) || is_float($item)
                ? $item
                : sanitize_text_field((string) $item);
        }

        return $normalized;
    }

    protected function resolveRowHeadingAlternative(array $row, array $structure, string $current_value): string
    {
        $alternatives = [
            __('Section Details', 'griffinforms-form-builder'),
            __('Your Details', 'griffinforms-form-builder'),
            __('Profile Details', 'griffinforms-form-builder'),
            __('More Information', 'griffinforms-form-builder'),
        ];

        foreach ($alternatives as $candidate) {
            if (strcasecmp($candidate, $current_value) !== 0) {
                return $candidate;
            }
        }

        return __('Section', 'griffinforms-form-builder');
    }

    protected function resolveRowHeadingValueFromFields(array $row, array $structure, bool $force_rewrite = false): string
    {
        // Row headings are visible to end users in the live form, so prefer polished, public-facing copy.
        $existing = $this->normalizeText((string) ($row['heading'] ?? ''), 120);
        if (!$force_rewrite && $existing !== '' && !in_array(strtolower($existing), ['row', 'section'], true)) {
            return $existing;
        }

        $field_labels = [];
        foreach ((array) ($row['columns'] ?? []) as $column) {
            foreach ((array) ($column['fields'] ?? []) as $field) {
                $label = $this->normalizeText((string) ($field['label'] ?? ''), 60);
                if ($label !== '') {
                    $field_labels[] = $label;
                }
            }
        }

        $field_blob = strtolower(implode(' ', array_slice($field_labels, 0, 4)));
        $candidates = [];

        if (preg_match('/\b(name|email|phone|contact|address)\b/', $field_blob)) {
            $candidates[] = __('Contact Details', 'griffinforms-form-builder');
            $candidates[] = __('Your Details', 'griffinforms-form-builder');
        } elseif (preg_match('/\b(billing|invoice|payment|card|price|amount)\b/', $field_blob)) {
            $candidates[] = __('Billing Details', 'griffinforms-form-builder');
            $candidates[] = __('Payment Details', 'griffinforms-form-builder');
        } elseif (preg_match('/\b(company|business|organization|job|title|role)\b/', $field_blob)) {
            $candidates[] = __('Business Details', 'griffinforms-form-builder');
            $candidates[] = __('Profile Details', 'griffinforms-form-builder');
        } elseif (preg_match('/\b(message|comment|notes|details|description|about)\b/', $field_blob)) {
            $candidates[] = __('Additional Details', 'griffinforms-form-builder');
            $candidates[] = __('More Information', 'griffinforms-form-builder');
        } elseif (!empty($field_labels)) {
            $candidates[] = __('Form Details', 'griffinforms-form-builder');
            $candidates[] = __('Section Details', 'griffinforms-form-builder');
        } else {
            $candidates[] = __('Section', 'griffinforms-form-builder');
            $candidates[] = __('Section Details', 'griffinforms-form-builder');
        }

        foreach ($candidates as $candidate) {
            if ($candidate !== '' && strcasecmp($candidate, $existing) !== 0) {
                return $candidate;
            }
        }

        return $force_rewrite ? __('Section Details', 'griffinforms-form-builder') : ($existing !== '' ? $existing : __('Section', 'griffinforms-form-builder'));
    }

    protected function resolveRowDescriptionValue(array $row, string $prompt = ''): string
    {
        // Row descriptions are visible to end users in the live form, so generate public-facing copy.
        $normalized_prompt = strtolower(trim($prompt));
        $line_count = $this->extractRequestedLineCount($normalized_prompt);
        $line_count = $line_count > 0 ? min($line_count, 4) : 1;
        $language = $this->extractRequestedLanguage($normalized_prompt);
        $field_labels = [];
        $field_types = [];
        foreach ((array) ($row['columns'] ?? []) as $column) {
            foreach ((array) ($column['fields'] ?? []) as $field) {
                $label = $this->normalizeText((string) ($field['label'] ?? ''), 60);
                if ($label !== '') {
                    $field_labels[] = $label;
                }
                $field_type = sanitize_key((string) ($field['field_type'] ?? ''));
                if ($field_type !== '') {
                    $field_types[] = $field_type;
                }
            }
        }

        $field_blob = strtolower(implode(' ', array_slice($field_labels, 0, 5)));
        $type_blob = implode(' ', array_slice($field_types, 0, 5));

        if ($language === 'fr') {
            return $this->buildFrenchRowDescriptionValue($field_blob, $type_blob, $line_count, $normalized_prompt);
        }

        if (preg_match('/\b(rewrite|refresh|improve|refine|reword|polish|update)\b/', $normalized_prompt)) {
            if (preg_match('/\b(name|email|phone|contact|address)\b/', $field_blob)) {
                return $this->buildRowDescriptionCopy(
                    __('Tell us a little about how we can reach you.', 'griffinforms-form-builder'),
                    __('We use this to follow up and keep your request moving.', 'griffinforms-form-builder'),
                    $line_count
                );
            }

            if (preg_match('/\b(billing|invoice|payment|card|price|amount)\b/', $field_blob . ' ' . $type_blob)) {
                return $this->buildRowDescriptionCopy(
                    __('Share the billing details needed to continue.', 'griffinforms-form-builder'),
                    __('This helps us prepare the next step without delays.', 'griffinforms-form-builder'),
                    $line_count
                );
            }

            if (preg_match('/\b(company|business|organization|job|title|role)\b/', $field_blob)) {
                return $this->buildRowDescriptionCopy(
                    __('Add the business or professional details that apply here.', 'griffinforms-form-builder'),
                    __('This gives us the context we need to proceed cleanly.', 'griffinforms-form-builder'),
                    $line_count
                );
            }

            if (preg_match('/\b(message|comment|notes|details|description|about)\b/', $field_blob)) {
                return $this->buildRowDescriptionCopy(
                    __('Provide any extra details that help us understand your request.', 'griffinforms-form-builder'),
                    __('A short explanation is usually enough.', 'griffinforms-form-builder'),
                    $line_count
                );
            }

            if (!empty($field_labels)) {
                return $this->buildRowDescriptionCopy(
                    __('Share the information requested below.', 'griffinforms-form-builder'),
                    __('Keep it short, but include anything that helps complete the section.', 'griffinforms-form-builder'),
                    $line_count
                );
            }

            return $this->buildRowDescriptionCopy(
                __('Provide the requested information.', 'griffinforms-form-builder'),
                __('A short answer is enough unless the form asks for more detail.', 'griffinforms-form-builder'),
                $line_count
            );
        }

        if (preg_match('/\b(name|email|phone|contact|address)\b/', $field_blob)) {
            return $this->buildRowDescriptionCopy(
                __('Capture the main contact details for this section.', 'griffinforms-form-builder'),
                __('We use this to identify and follow up with the right person.', 'griffinforms-form-builder'),
                $line_count
            );
        }

        if (preg_match('/\b(billing|invoice|payment|card|price|amount)\b/', $field_blob . ' ' . $type_blob)) {
            return $this->buildRowDescriptionCopy(
                __('Collect the billing information needed for this step.', 'griffinforms-form-builder'),
                __('This keeps the next stage of the form moving smoothly.', 'griffinforms-form-builder'),
                $line_count
            );
        }

        if (preg_match('/\b(company|business|organization|job|title|role)\b/', $field_blob)) {
            return $this->buildRowDescriptionCopy(
                __('Capture the business or profile details for this section.', 'griffinforms-form-builder'),
                __('These details help us tailor the rest of the form.', 'griffinforms-form-builder'),
                $line_count
            );
        }

        if (preg_match('/\b(message|comment|notes|details|description|about)\b/', $field_blob)) {
            return $this->buildRowDescriptionCopy(
                __('Capture additional details relevant to this section.', 'griffinforms-form-builder'),
                __('Include enough context for us to understand what you need.', 'griffinforms-form-builder'),
                $line_count
            );
        }

        if (!empty($field_labels)) {
            return $this->buildRowDescriptionCopy(
                __('Provide the information requested in this section.', 'griffinforms-form-builder'),
                __('A short answer is fine unless the form asks for more detail.', 'griffinforms-form-builder'),
                $line_count
            );
        }

        return $this->buildRowDescriptionCopy(
            __('Provide the requested information.', 'griffinforms-form-builder'),
            __('A brief answer is usually enough here.', 'griffinforms-form-builder'),
            $line_count
        );
    }

    protected function extractRequestedLineCount(string $prompt): int
    {
        if (preg_match('/\b(\d+)\s*(?:line|lines|row line|row lines|line description|line descriptions)\b/', $prompt, $matches)) {
            return max(1, absint($matches[1]));
        }

        if (preg_match('/\b(one|single)\s*line\b/', $prompt)) {
            return 1;
        }

        if (preg_match('/\b(two|double|2)\s*line\b/', $prompt)) {
            return 2;
        }

        if (preg_match('/\b(three|triple|3)\s*line\b/', $prompt)) {
            return 3;
        }

        if (preg_match('/\b(four|4)\s*line\b/', $prompt)) {
            return 4;
        }

        return 0;
    }

    protected function buildFrenchRowDescriptionValue(string $field_blob, string $type_blob, int $line_count, string $prompt = ''): string
    {
        $is_rewrite = (bool) preg_match('/\b(rewrite|refresh|improve|refine|reword|polish|update|change|translate|convert|make)\b/', $prompt);

        $build = function (string $line1, string $line2 = '') use ($line_count): string {
            $parts = [trim($line1)];
            if ($line_count >= 2 && $line2 !== '') {
                $parts[] = trim($line2);
            }
            if ($line_count >= 3) {
                $parts[] = 'Ajoutez des informations utiles pour terminer cette section.';
            }
            if ($line_count >= 4) {
                $parts[] = 'Répondez de façon claire et précise.';
            }

            return trim(implode(' ', array_filter($parts)));
        };

        if (preg_match('/\b(name|email|phone|contact|address)\b/', $field_blob)) {
            return $build(
                'Indiquez vos coordonnées pour que nous puissions vous contacter.',
                'Nous les utilisons pour assurer le suivi de votre demande.'
            );
        }

        if (preg_match('/\b(billing|invoice|payment|card|price|amount)\b/', $field_blob . ' ' . $type_blob)) {
            return $build(
                'Partagez les informations de facturation nécessaires pour continuer.',
                "Cela nous aide à préparer l'étape suivante sans délai."
            );
        }

        if (preg_match('/\b(company|business|organization|job|title|role)\b/', $field_blob)) {
            return $build(
                'Ajoutez les informations professionnelles ou d\'entreprise qui s\'appliquent ici.',
                'Ces informations nous aident à personnaliser le reste du formulaire.'
            );
        }

        if (preg_match('/\b(message|comment|notes|details|description|about)\b/', $field_blob)) {
            return $build(
                'Ajoutez des détails supplémentaires pour nous aider à comprendre votre demande.',
                'Une brève explication suffit généralement.'
            );
        }

        if (!empty($field_blob)) {
            return $build(
                'Indiquez les informations demandées ci-dessous.',
                'Gardez une réponse concise, mais ajoutez ce qui peut aider à compléter cette section.'
            );
        }

        return $build(
            $is_rewrite
                ? 'Fournissez la description demandée.'
                : 'Fournissez les informations demandées.',
            'Une réponse courte suffit généralement ici.'
        );
    }

    protected function buildRowDescriptionCopy(string $line1, string $line2, int $line_count): string
    {
        $parts = [trim($line1)];
        if ($line_count >= 2) {
            $parts[] = trim($line2);
        }
        if ($line_count >= 3) {
            $parts[] = __('Add any extra context needed to complete this section.', 'griffinforms-form-builder');
        }
        if ($line_count >= 4) {
            $parts[] = __('Keep your response clear and specific.', 'griffinforms-form-builder');
        }

        return trim(implode(' ', array_filter($parts)));
    }

    protected function resolveRowDescriptionAlternative(array $row, string $prompt, string $current_value): string
    {
        $alternatives = [
            __('Tell us a little about how we can reach you.', 'griffinforms-form-builder'),
            __('Share the information requested below.', 'griffinforms-form-builder'),
            __('Provide any extra details that help us understand your request.', 'griffinforms-form-builder'),
            __('Provide the requested information.', 'griffinforms-form-builder'),
        ];

        foreach ($alternatives as $candidate) {
            if (strcasecmp($candidate, $current_value) !== 0) {
                return $candidate;
            }
        }

        return __('Provide the requested information.', 'griffinforms-form-builder');
    }

    protected function maybeBuildDirectRowInsertPatch(int $form_id, string $prompt, array $selected_context, array $structure): ?array
    {
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_row_id = absint($selected_context['item_id'] ?? 0);
        $normalized_prompt = strtolower(trim($prompt));

        if (!$this->row_prompt_helper->isRowInsertRequest($normalized_prompt)) {
            return null;
        }

        // If the prompt contains semantic content (a purpose, topic, or named subject),
        // let the model handle it so it can populate heading and description.
        if ($this->rowInsertHasSemanticContent($normalized_prompt)) {
            return null;
        }

        $column_count = $this->row_prompt_helper->extractRequestedColumnCount($normalized_prompt);
        if ($column_count === null) {
            $column_count = 1;
        }

        if ($column_count < 1 || $column_count > 4) {
            return [
                'mode' => 'no_op',
                'message' => __('Rows can have at most 4 columns. Please request 4 or fewer columns.', 'griffinforms-form-builder'),
            ];
        }

        $selected_page_id = 0;
        if ($selected_type === 'formpage') {
            $selected_page_id = $selected_row_id;
        } elseif (!empty($selected_context['parent_id'])) {
            $selected_page_id = absint($selected_context['parent_id'] ?? 0);
        }

        if (!in_array($selected_type, ['form', 'formpage', 'pagerow', 'row'], true)) {
            return [
                'mode' => 'no_op',
                'message' => $this->buildRowInsertPlacementMessage($selected_context),
            ];
        }

        $selected_row_snapshot = null;
        if (in_array($selected_type, ['pagerow', 'row'], true) && $selected_row_id > 0) {
            $selected_row_snapshot = $this->findRowSnapshot($structure, $selected_row_id);
        }

        $direction = 'after';
        if (preg_match('/\b(above|before|prior)\b/', $normalized_prompt)) {
            $direction = 'before';
        } elseif (preg_match('/\b(below|after)\b/', $normalized_prompt)) {
            $direction = 'after';
        }

        // Only bail if we have no page to insert into at all.
        // A missing row snapshot just means we can't show an anchor label — that's fine.
        if (!in_array($selected_type, ['form', 'formpage'], true)
            && !in_array($selected_type, ['pagerow', 'row'], true)) {
            return null;
        }

        $row = is_array($selected_row_snapshot) ? $selected_row_snapshot : [];
        if ($selected_page_id <= 0) {
            foreach ((array) ($structure['pages'] ?? []) as $page) {
                foreach ((array) ($page['rows'] ?? []) as $candidate_row) {
                    if (absint($candidate_row['item_id'] ?? 0) === $selected_row_id) {
                        $selected_page_id = absint($page['item_id'] ?? 0);
                        break 2;
                    }
                }
            }
        }

        if ($selected_page_id <= 0) {
            $pages = (array) ($structure['pages'] ?? []);
            $selected_page_id = absint(($pages[0]['item_id'] ?? 0));
        }

        if ($selected_page_id <= 0) {
            return null;
        }

        $called_section = (bool) preg_match('/\bsection\b/i', $prompt);
        $item_word = $called_section
            ? __('section', 'griffinforms-form-builder')
            : __('row', 'griffinforms-form-builder');

        $anchor_label = '';
        if (is_array($row) && !empty($row)) {
            $anchor_label = $this->normalizeText((string) ($row['heading'] ?? ''), 120);
        }

        // When inserting into a page (not relative to a sibling row), the direction
        // concept doesn't apply — the row goes inside the page, not above/below it.
        $inserting_into_page = in_array($selected_type, ['form', 'formpage'], true);

        if ($inserting_into_page) {
            $success_message = $column_count > 1
                ? sprintf(
                    /* translators: 1: "row" or "section", 2: column count */
                    __('Added a new %1$s with %2$d columns to the current page.', 'griffinforms-form-builder'),
                    $item_word,
                    $column_count
                )
                : sprintf(
                    /* translators: 1: "row" or "section" */
                    __('Added a new %s to the current page.', 'griffinforms-form-builder'),
                    $item_word
                );
        } else {
            if ($anchor_label === '') {
                $anchor_label = __('selected row', 'griffinforms-form-builder');
            }
            $success_message = $column_count > 1
                ? sprintf(
                    /* translators: 1: "row" or "section", 2: column count */
                    __('Added a new %1$s with %2$d columns.', 'griffinforms-form-builder'),
                    $item_word,
                    $column_count
                )
                : ($direction === 'before'
                    ? sprintf(
                        /* translators: 1: "row" or "section", 2: anchor label */
                        __('Added a new %1$s above %2$s.', 'griffinforms-form-builder'),
                        $item_word,
                        $anchor_label
                    )
                    : sprintf(
                        /* translators: 1: "row" or "section", 2: anchor label */
                        __('Added a new %1$s below %2$s.', 'griffinforms-form-builder'),
                        $item_word,
                        $anchor_label
                    ));
        }

        $operation = [
            'op' => 'insert_row',
            'target' => [
                'scope' => 'page',
                'item_id' => $selected_page_id,
            ],
            'position' => $direction,
            'row' => [
                'heading' => '',
                'description' => '',
                'column_count' => $column_count,
            ],
        ];

        // When a sibling row is selected, pass it as the anchor so the applier
        // can insert before/after it rather than always appending to the end.
        // scope is required — resolveTarget returns null if scope is absent.
        if (in_array($selected_type, ['pagerow', 'row'], true) && $selected_row_id > 0) {
            $operation['anchor'] = ['scope' => 'pagerow', 'item_id' => $selected_row_id];
        }

        return [
            'mode' => 'patch',
            'success_message' => $success_message,
            'operations' => [$operation],
        ];
    }

    protected function maybeBuildDirectFieldOptionsPatch(int $form_id, string $prompt, array $selected_context, array $structure): ?array
    {
        $normalized_prompt = strtolower(trim($prompt));
        if (!preg_match('/\b(option label|option labels|options|choices|choice|radio option|radio options|checkbox option|checkbox options)\b/', $normalized_prompt)) {
            return null;
        }

        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_id = absint($selected_context['item_id'] ?? 0);
        if ($selected_type !== 'field' || $selected_id <= 0) {
            return null;
        }

        $field_snapshot = $this->findFieldSnapshot($structure, $selected_id);
        if (!is_array($field_snapshot)) {
            return null;
        }

        $field_type = sanitize_key((string) ($field_snapshot['field_type'] ?? ''));
        if (!in_array($field_type, ['selectfield', 'inputcheckbox', 'inputradio'], true)) {
            return null;
        }

        $options = $this->buildChoiceOptionsFromPrompt($normalized_prompt, $field_snapshot);
        if (empty($options)) {
            return [
                'mode' => 'no_op',
                'success_message' => __('Please provide the option values you want for this field.', 'griffinforms-form-builder'),
                'operations' => [],
            ];
        }

        return [
            'mode' => 'patch',
            'success_message' => __('Updated the selected field options.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'update_field',
                'target' => [
                    'scope' => 'field',
                    'item_id' => $selected_id,
                ],
                'changes' => [
                    'field_type' => $field_type,
                    'options' => $options,
                ],
            ]],
        ];
    }

    protected function maybeBuildDirectFormCopyPatch(string $normalized_prompt, array $selected_context, array $structure): ?array
    {
        if (!preg_match('/\b(polish|improve|rewrite|update|refine|edit|adjust|tweak|enhance)\b/', $normalized_prompt)) {
            return null;
        }

        if (!preg_match('/\b(form copy|form text|form wording|form content|copy)\b/', $normalized_prompt)) {
            return null;
        }

        $form_id = absint($selected_context['form_id'] ?? 0);
        if ($form_id <= 0) {
            return null;
        }

        $form = (array) ($structure['form'] ?? []);
        $operations = [];

        foreach ([
            'name' => $this->normalizeText((string) ($form['name'] ?? ''), 120),
            'heading' => $this->normalizeText((string) ($form['heading'] ?? ''), 120),
            'description' => $this->normalizeText((string) ($form['description'] ?? ''), 180),
            'submit_label' => $this->normalizeText((string) ($form['submit_label'] ?? ''), 120),
        ] as $property => $value) {
            if ($value === '') {
                continue;
            }

            $operations[] = [
                'op' => 'update_form',
                'target' => [
                    'scope' => 'form',
                    'item_id' => $form_id,
                ],
                'changes' => [
                    $property => $value,
                ],
            ];
        }

        foreach ((array) ($structure['pages'] ?? []) as $page) {
            $page = (array) $page;
            foreach ((array) ($page['rows'] ?? []) as $row) {
                $row = (array) $row;
                $row_id = absint($row['item_id'] ?? 0);
                if ($row_id <= 0) {
                    continue;
                }

                $heading = $this->resolveRowHeadingValueFromFields($row, $structure, true);
                if ($heading !== '') {
                    $operations[] = [
                        'op' => 'update_row',
                        'target' => [
                            'scope' => 'row',
                            'item_id' => $row_id,
                        ],
                        'changes' => [
                            'heading' => $heading,
                        ],
                    ];
                }

                $description = $this->resolveRowDescriptionValue($row, $normalized_prompt);
                if ($description !== '') {
                    $operations[] = [
                        'op' => 'update_row',
                        'target' => [
                            'scope' => 'row',
                            'item_id' => $row_id,
                        ],
                        'changes' => [
                            'description' => $description,
                        ],
                    ];
                }
            }
        }

        if (empty($operations)) {
            return null;
        }

        return [
            'mode' => 'patch',
            'success_message' => __('Updated the form copy and row copy.', 'griffinforms-form-builder'),
            'operations' => $operations,
        ];
    }

    protected function maybeBuildDirectFieldPlaceholderPatch(string $prompt, array $selected_context, array $structure): ?array
    {
        $normalized_prompt = strtolower(trim($prompt));
        if (!preg_match('/\b(placeholder|placeholders|placeholder text|hint text|helper text)\b/', $normalized_prompt)) {
            return null;
        }

        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_id = absint($selected_context['item_id'] ?? 0);
        if ($selected_type !== 'field' || $selected_id <= 0) {
            return null;
        }

        $field_snapshot = $this->findFieldSnapshot($structure, $selected_id);
        if (!is_array($field_snapshot)) {
            return null;
        }


        $resolved_placeholder = $this->resolveFieldPlaceholderValue($field_snapshot, $prompt, $selected_context);
        if ($resolved_placeholder === null) {
            return null;
        }

        return [
            'mode' => 'patch',
            'success_message' => __('Updated the selected field placeholder.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'update_field',
                'target' => [
                    'scope' => 'field',
                    'item_id' => $selected_id,
                ],
                'changes' => [
                    'placeholder' => $resolved_placeholder,
                ],
            ]],
        ];
    }

    protected function normalizeRowColumnCountForBatch(array $plan, string $normalized_prompt): array
    {
        $operations = (array) ($plan['operations'] ?? []);
        if (empty($operations)) {
            return $plan;
        }

        $has_field_insert = false;
        foreach ($operations as $op) {
            if (sanitize_key((string) ((array) $op)['op'] ?? '') === 'insert_field') {
                $has_field_insert = true;
                break;
            }
        }

        // Only normalise when fields are also being inserted — user-only column requests are unaffected
        if (!$has_field_insert) {
            return $plan;
        }

        // If the user explicitly requested a column count, respect it
        if ($this->extractRequestedColumnCount($normalized_prompt) !== null) {
            return $plan;
        }

        $normalised = [];
        foreach ($operations as $op) {
            $op = (array) $op;
            if (sanitize_key((string) ($op['op'] ?? '')) === 'insert_row' && isset($op['row'])) {
                $op['row']['column_count'] = 1;
            }
            $normalised[] = $op;
        }
        $plan['operations'] = $normalised;
        return $plan;
    }

    protected function isMultiFieldPrompt(string $normalized_prompt): bool
    {
        // Bullet/numbered list — user enumerated multiple fields
        if (preg_match('/(\n\s*[-•*]\s|\n\s*\d+[.)]\s)/', $normalized_prompt)) {
            return true;
        }
        // Explicit count: "two fields", "3 fields", "multiple fields", "several fields"
        if (preg_match('/\b(two|three|four|five|2|3|4|5|multiple|several)\b.*\b(fields|inputs|questions)\b/', $normalized_prompt)) {
            return true;
        }
        // Row + field combo — "add a new row with a text field", "new section containing email fields"
        if (preg_match('/\b(row|section)s?\b/', $normalized_prompt)
            && preg_match('/\bfields?\b|\binputs?\b|\bquestions?\b/', $normalized_prompt)
            && preg_match('/\b(add|insert|create|new)\b/', $normalized_prompt)) {
            return true;
        }
        return false;
    }

    protected function maybeBuildDirectFieldInsertPatch(int $form_id, string $prompt, array $selected_context): ?array
    {
        $normalized_prompt = strtolower(trim($prompt));
        if (!$this->isFieldGrowthPrompt($normalized_prompt)) {
            return null;
        }

        // If a field is selected, the prompt may be targeting that field (e.g. "add a description to this field")
        // rather than inserting a new one. Bail and let the field-edit path handle it.
        if (sanitize_key((string) ($selected_context['item_type'] ?? '')) === 'field' && !empty($selected_context['item_id'])) {
            return null;
        }

        // Multi-field requests — let the AI generate all operations
        if ($this->isMultiFieldPrompt($normalized_prompt)) {
            return null;
        }

        $field = $this->inferSingleFieldFromPrompt($normalized_prompt);
        if (!is_array($field) || empty($field['field_type'])) {
            return null;
        }

        $field_type = sanitize_key((string) ($field['field_type'] ?? ''));
        $label = $this->normalizeText((string) ($field['label'] ?? ''), 120);
        if ($field_type === '' || $label === '') {
            return null;
        }

        // These types require subject-aware generation — let the AI handle them
        if (in_array($field_type, ['inputlikert', 'payment'], true)) {
            return null;
        }

        // File upload with constraints (file type/size restrictions) — let the AI apply field_settings
        if ($field_type === 'fileupload' && preg_match('/\b(only|accept|image|photo|pdf|doc|video|type|format|limit|size|restrict)\b/', $normalized_prompt)) {
            return null;
        }

        $target_form_id = absint($selected_context['form_id'] ?? 0);
        if ($target_form_id <= 0) {
            $target_form_id = $form_id;
        }

        if ($target_form_id <= 0) {
            return null;
        }

        $name = $this->normalizeText((string) ($field['name'] ?? ''), 120);
        if ($name === '') {
            $name = sanitize_title($label);
        }
        if ($name === '') {
            $name = $field_type;
        }

        $field_payload = [
            'field_type' => $field_type,
            'name' => $name,
            'label' => $label,
            'description' => $this->normalizeText((string) ($field['description'] ?? ''), 180),
            'placeholder' => $this->normalizeText((string) ($field['placeholder'] ?? ''), 120),
            'required' => !empty($field['required']),
        ];

        if ($field_type === 'inputrating') {
            if ($field_payload['description'] === '') {
                $field_payload['description'] = $this->buildRatingFieldDescriptionForPrompt($normalized_prompt);
            }
            $field_payload['field_settings'] = $this->buildRatingFieldSettingsForPrompt($normalized_prompt, $label);
        }

        if (in_array($field_type, ['selectfield', 'inputcheckbox', 'inputradio'], true)) {
            // Only stay in the fast path when we have a hardcoded label set (meal, drink, yes/no, etc.)
            // that produces correct results without AI. For all other prompts — whether they have a subject
            // or none at all — let the AI generate contextual options so defaults are never generic ("Option 1").
            $subject = $this->extractFieldSubjectFromPrompt($normalized_prompt);
            if (!$this->hasHardcodedChoiceLabels($normalized_prompt)) {
                return null;
            }
            $field_payload['options'] = !empty($field['options']) && is_array($field['options'])
                ? $field['options']
                : $this->buildDefaultChoiceOptions($field_type, $normalized_prompt, $subject, $label);
        }

        return [
            'mode' => 'patch',
            'success_message' => sprintf(__('Added %s field.', 'griffinforms-form-builder'), $label),
            'operations' => [[
                'op' => 'insert_field',
                'target' => [
                    'scope' => 'form',
                    'item_id' => $target_form_id,
                ],
                'position' => 'after',
                'field' => $field_payload,
            ]],
        ];
    }

    protected function buildRatingFieldSettingsForPrompt(string $normalized_prompt, string $label = ''): array
    {
        $scale = 5;
        if (preg_match('/\b(\d+)\s*(?:star|stars|step|steps|point|points|rating|ratings)\b/', $normalized_prompt, $matches)) {
            $scale = max(2, min(10, absint($matches[1])));
        }

        $icon_group = $this->resolveRatingIconGroupForPrompt($normalized_prompt);

        $satisfaction_labels = [
            __('Very Poor', 'griffinforms-form-builder'),
            __('Poor', 'griffinforms-form-builder'),
            __('Okay', 'griffinforms-form-builder'),
            __('Good', 'griffinforms-form-builder'),
            __('Excellent', 'griffinforms-form-builder'),
        ];

        if (preg_match('/\b(satisfaction|experience|service|support|quality|feedback|review)\b/', $normalized_prompt)) {
            $step_labels = $satisfaction_labels;
        } else {
            $step_labels = [];
        }

        if ($scale !== count($step_labels) && !empty($step_labels)) {
            $step_labels = array_slice(array_pad($step_labels, $scale, ''), 0, $scale);
        }

        $available_icon_groups = ['star', 'heart', 'thumb', 'face', 'circle', 'flag', 'bolt', 'diamond', 'custom'];

        return [
            'scale' => $scale,
            'icon_group' => $icon_group,
            'icon_size' => 32,
            'icon_spacing' => 4,
            'color_filled' => '#f5a623',
            'color_empty' => '#d1d5db',
            'step_labels' => !empty($step_labels) ? $step_labels : array_fill(0, $scale, ''),
            'custom_image_id' => 0,
            'step_image_ids' => array_fill(0, $scale, 0),
            'allow_clear' => 1,
            'available_icon_groups' => $available_icon_groups,
            'available_icon_group_labels' => [
                'star' => __('Star', 'griffinforms-form-builder'),
                'heart' => __('Heart', 'griffinforms-form-builder'),
                'thumb' => __('Thumb', 'griffinforms-form-builder'),
                'face' => __('Face', 'griffinforms-form-builder'),
                'circle' => __('Circle', 'griffinforms-form-builder'),
                'flag' => __('Flag', 'griffinforms-form-builder'),
                'bolt' => __('Bolt', 'griffinforms-form-builder'),
                'diamond' => __('Diamond', 'griffinforms-form-builder'),
                'custom' => __('Custom', 'griffinforms-form-builder'),
            ],
            'available_icon_types' => [
                ['value' => 'star', 'label' => __('Star', 'griffinforms-form-builder')],
                ['value' => 'heart', 'label' => __('Heart', 'griffinforms-form-builder')],
                ['value' => 'thumb', 'label' => __('Thumb', 'griffinforms-form-builder')],
                ['value' => 'face', 'label' => __('Face', 'griffinforms-form-builder')],
                ['value' => 'circle', 'label' => __('Circle', 'griffinforms-form-builder')],
                ['value' => 'flag', 'label' => __('Flag', 'griffinforms-form-builder')],
                ['value' => 'bolt', 'label' => __('Bolt', 'griffinforms-form-builder')],
                ['value' => 'diamond', 'label' => __('Diamond', 'griffinforms-form-builder')],
                ['value' => 'custom', 'label' => __('Custom', 'griffinforms-form-builder')],
            ],
        ];
    }

    protected function resolveRatingIconGroupForPrompt(string $normalized_prompt): string
    {
        if (preg_match('/\b(heart|hearts|love|loved|like|liked)\b/', $normalized_prompt)) {
            return 'heart';
        }

        if (preg_match('/\b(thumb|thumbs|thumbs up|thumbs-down|thumbs down)\b/', $normalized_prompt)) {
            return 'thumb';
        }

        if (preg_match('/\b(face|smile|smiley|emoji|emotion)\b/', $normalized_prompt)) {
            return 'face';
        }

        if (preg_match('/\b(flag|flags)\b/', $normalized_prompt)) {
            return 'flag';
        }

        if (preg_match('/\b(bolt|lightning|energy)\b/', $normalized_prompt)) {
            return 'bolt';
        }

        if (preg_match('/\b(diamond|gem|jewel)\b/', $normalized_prompt)) {
            return 'diamond';
        }

        if (preg_match('/\b(circle|dot|dots)\b/', $normalized_prompt)) {
            return 'circle';
        }

        return 'star';
    }

    protected function buildRatingFieldDescriptionForPrompt(string $normalized_prompt): string
    {
        if (preg_match('/\b(satisfaction|experience|service|quality|feedback|review)\b/', $normalized_prompt)) {
            return __('Rate your satisfaction with the service.', 'griffinforms-form-builder');
        }

        return __('Choose the rating that best matches your response.', 'griffinforms-form-builder');
    }

    protected function buildChoiceOptionsFromPrompt(string $normalized_prompt, array $field_snapshot): array
    {
        $choices = [];

        if (preg_match('/meal|food|lunch|dinner|breakfast|vegetarian|vegan|gluten[- ]?free|non[- ]?vegetarian/', $normalized_prompt)) {
            $choices = [
                'Vegetarian',
                'Non-Vegetarian',
                'Vegan',
                'Gluten-Free',
            ];
        }

        if (preg_match('/delivery|shipping|pickup|takeout/', $normalized_prompt)) {
            $choices = [
                'Delivery',
                'Pickup',
                'Dine In',
            ];
        }

        if (preg_match('/yes|no|true|false|agree|disagree/', $normalized_prompt)) {
            $choices = [
                'Yes',
                'No',
            ];
        }

        if (empty($choices)) {
            return [];
        }

        $field_type = sanitize_key((string) ($field_snapshot['field_type'] ?? ''));
        $mapped = array_map(static fn(string $label) => [
            'label' => $label,
            'value' => sanitize_key(str_replace(' ', '_', strtolower($label))),
        ], $choices);

        if (in_array($field_type, ['inputcheckbox', 'inputradio'], true)) {
            return [[
                'option_column' => 1,
                'options' => $mapped,
            ]];
        }

        return $mapped;
    }

    protected function resolveFieldPlaceholderValue(array $field, string $prompt, array $selected_context): ?string
    {
        $field_type = sanitize_key((string) ($field['field_type'] ?? ''));
        $existing = $this->normalizeText((string) ($field['placeholder'] ?? ''), 120);
        $normalized_prompt = strtolower(trim($prompt));

        if (preg_match('/\bto\s+"([^"]{1,120})"\s*$/i', $prompt, $matches) || preg_match('/\bto\s+([A-Za-z0-9][A-Za-z0-9\s,._-]{1,120})$/i', $prompt, $matches)) {
            $candidate = $this->normalizeText((string) ($matches[1] ?? ''), 120);
            return $candidate !== '' ? $candidate : null;
        }

        if ($existing !== '' && preg_match('/\b(keep|preserve|leave)\b/', $normalized_prompt)) {
            return $existing;
        }

        return null;
    }

    protected function inferSingleFieldFromPrompt(string $prompt): ?array
    {
        $normalized = strtolower(trim($prompt));
        if (!$this->isFieldGrowthPrompt($normalized)) {
            return null;
        }

        $field_type = null;
        $label = '';
        $placeholder = '';
        $subject = $this->extractFieldSubjectFromPrompt($normalized);

        $map = [
            'inputemail' => ['keywords' => ['email'], 'label' => __('Email Address', 'griffinforms-form-builder'), 'placeholder' => __('you@example.com', 'griffinforms-form-builder')],
            'inputphone' => ['keywords' => ['phone', 'mobile', 'telephone'], 'label' => __('Phone Number', 'griffinforms-form-builder'), 'placeholder' => __('+1 (555) 987-6543', 'griffinforms-form-builder')],
            'inputaddress' => ['keywords' => ['billing address', 'shipping address', 'mailing address', 'street address', 'address'], 'label' => __('Mailing Address', 'griffinforms-form-builder'), 'placeholder' => ''],
            'textarea' => ['keywords' => ['message', 'notes', 'description', 'comment'], 'label' => __('Message', 'griffinforms-form-builder'), 'placeholder' => __('Type your message here...', 'griffinforms-form-builder')],
            'inputnumber' => ['keywords' => ['number', 'amount', 'quantity'], 'label' => __('Number', 'griffinforms-form-builder'), 'placeholder' => ''],
            'inputdate' => ['keywords' => ['date', 'day'], 'label' => __('Date', 'griffinforms-form-builder'), 'placeholder' => ''],
            'fileupload' => ['keywords' => ['file', 'upload', 'attachment', 'resume', 'document'], 'label' => __('Upload File', 'griffinforms-form-builder'), 'placeholder' => ''],
            'signature' => ['keywords' => ['signature', 'sign here', 'e-signature'], 'label' => __('Signature', 'griffinforms-form-builder'), 'placeholder' => ''],
            'inputrichtext' => ['keywords' => ['rich text', 'formatted text', 'wysiwyg', 'editor'], 'label' => __('Rich Text', 'griffinforms-form-builder'), 'placeholder' => ''],
            'inputlikert' => ['keywords' => ['likert', 'likert scale', 'likert table', 'likert chart', 'satisfaction matrix', 'survey matrix', 'survey grid', 'rating grid', 'matrix field', 'matrix table', 'media types', 'media type', 'content type', 'content types'], 'label' => __('Likert Scale', 'griffinforms-form-builder'), 'placeholder' => ''],
            'payment' => ['keywords' => ['payment field', 'payment item', 'payment items', 'product field', 'product selection', 'product listing', 'product selection field', 'pricing field', 'price field', 'ecommerce field', 'e-commerce field', 'selling field', 'shop field', 'store field', 'order field', 'buy field', 'purchase field', 'checkout field', 'product', 'products'], 'label' => __('Payment', 'griffinforms-form-builder'), 'placeholder' => ''],
            'inputrating' => ['keywords' => ['rating', 'star rating', 'score', 'satisfaction', 'feedback', 'review', 'rate'], 'label' => __('Rating', 'griffinforms-form-builder'), 'placeholder' => ''],
            'selectfield' => ['keywords' => ['select', 'dropdown', 'choice', 'choices'], 'label' => __('Select', 'griffinforms-form-builder'), 'placeholder' => ''],
            'inputradio' => ['keywords' => ['radio', 'single choice', 'single select'], 'label' => __('Radio Group', 'griffinforms-form-builder'), 'placeholder' => ''],
            'inputcheckbox' => ['keywords' => ['checkbox', 'check box', 'multi select', 'multiple choice'], 'label' => __('Checkbox Group', 'griffinforms-form-builder'), 'placeholder' => ''],
            'inputtext' => ['keywords' => ['name', 'text', 'company', 'city', 'state', 'label'], 'label' => __('Text Field', 'griffinforms-form-builder'), 'placeholder' => ''],
        ];

        $matched_keyword = '';
        foreach ($map as $candidate_type => $candidate) {
            foreach ($candidate['keywords'] as $keyword) {
                if (preg_match('/\b' . preg_quote($keyword, '/') . '\b/', $normalized)) {
                    $field_type = $candidate_type;
                    $label = $candidate['label'];
                    $placeholder = $candidate['placeholder'];
                    $matched_keyword = $keyword;
                    break 2;
                }
            }
        }

        if ($field_type === 'inputaddress' && $matched_keyword !== '' && $matched_keyword !== 'address') {
            $label = $this->humanizeSubjectLabel($matched_keyword);
        }

        if ($field_type === null) {
            if (preg_match('/\bradio\b/', $normalized)) {
                $field_type = 'inputradio';
                $label = $subject !== '' ? $this->humanizeSubjectLabel($subject) : __('Radio Group', 'griffinforms-form-builder');
            } elseif (preg_match('/\bcheckbox\b/', $normalized)) {
                $field_type = 'inputcheckbox';
                $label = $subject !== '' ? $this->humanizeSubjectLabel($subject) : __('Checkbox Group', 'griffinforms-form-builder');
            } elseif (preg_match('/\bselect|dropdown|choice|choices\b/', $normalized)) {
                $field_type = 'selectfield';
                $label = $subject !== '' ? $this->humanizeSubjectLabel($subject) : __('Select', 'griffinforms-form-builder');
            } else {
                return null;
            }
        }

        if ($label === '' && $subject !== '') {
            $label = $this->humanizeSubjectLabel($subject);
        }

        if ($label === '') {
            return null;
        }

        if ($field_type === 'inputtext') {
            if (str_contains($normalized, 'company')) {
                $label = __('Company', 'griffinforms-form-builder');
            } elseif (str_contains($normalized, 'city')) {
                $label = __('City', 'griffinforms-form-builder');
            } elseif (str_contains($normalized, 'state')) {
                $label = __('State', 'griffinforms-form-builder');
            } elseif (str_contains($normalized, 'name')) {
                $label = __('Name', 'griffinforms-form-builder');
            } elseif (str_contains($normalized, 'label')) {
                $label = __('Label', 'griffinforms-form-builder');
            }
        }

        if ($field_type === 'inputrating') {
            if (preg_match('/\b(satisfaction|experience|service|quality|feedback|review)\b/', $normalized)) {
                $label = __('Satisfaction Rating', 'griffinforms-form-builder');
            }
        }

        // For types whose hardcoded label is generic (Date, Number, Upload File, Message),
        // override with a humanized subject when the prompt supplies a meaningful purpose.
        if (in_array($field_type, ['inputdate', 'inputnumber', 'fileupload', 'textarea'], true) && $subject !== '') {
            $clean_subject = preg_replace('/\b(the|their|your|user\'s|users|user|my|our|a|an)\b/', ' ', $subject);
            $clean_subject = preg_replace('/\b(uploading|upload|enter|entering|input|inputting|selecting|select|choosing|choose|providing|provide)\b/', ' ', (string) $clean_subject);
            $clean_subject = preg_replace('/\s+/', ' ', trim((string) $clean_subject));
            if ($clean_subject !== '') {
                $label = $this->humanizeSubjectLabel($clean_subject);
            }
        }

        if ($field_type === 'inputlikert') {
            if (preg_match('/\b(media\s+types?|content\s+types?)\b/', $normalized)) {
                $label = __('Media Type Satisfaction', 'griffinforms-form-builder');
            } elseif (preg_match('/\bcleaning\b/', $normalized)) {
                $label = __('Cleaning Service Satisfaction', 'griffinforms-form-builder');
            } elseif (preg_match('/\bcar\s+rent(?:ing|al)?\b/', $normalized)) {
                $label = __('Car Rental Satisfaction', 'griffinforms-form-builder');
            } elseif (preg_match('/\bdigital\s+devices?\b|\bpersonal\s+devices?\b|\bhome\s+devices?\b/', $normalized)) {
                $label = __('Device Satisfaction', 'griffinforms-form-builder');
            } elseif (preg_match('/\b(customer\s+satisfaction|user\s+satisfaction|satisfaction|experience|service|quality|feedback|review)\b/', $normalized)) {
                $label = __('Customer Satisfaction', 'griffinforms-form-builder');
            }
        }

        $options = [];
        if (in_array($field_type, ['selectfield', 'inputradio', 'inputcheckbox'], true)) {
            $options = $this->buildDefaultChoiceOptions($field_type, $normalized, $subject, $label);
        }

        return [
            'field_type' => $field_type,
            'label' => $label,
            'description' => '',
            'placeholder' => $placeholder,
            'required' => false,
            'options' => $options,
        ];
    }

    protected function buildDefaultLikertRows(string $normalized_prompt): array
    {
        $make_rows = static function (array $labels): array {
            $rows = [];
            foreach ($labels as $i => $label) {
                $rows[] = ['id' => 'row_' . ($i + 1), 'label' => $label];
            }
            return $rows;
        };

        // Specific product/item lists — enumerate the items themselves as rows
        if (preg_match('/\bdigital\s+devices?\b|\bpersonal\s+devices?\b|\bhome\s+devices?\b/', $normalized_prompt)) {
            return $make_rows(['Television', 'Smartphone', 'Tablet', 'Laptop', 'Smart Speaker']);
        }
        if (preg_match('/\bcar\s+rent(?:ing|al)?\b/', $normalized_prompt)) {
            return $make_rows(['Booking Process', 'Vehicle Condition', 'Pickup Experience', 'Fuel Policy', 'Customer Support']);
        }
        if (preg_match('/\bmedia\s+types?|content\s+types?\b/', $normalized_prompt)) {
            return $make_rows(['Articles', 'Videos', 'Podcasts', 'Newsletters', 'Social Media Posts']);
        }

        // Cleaning services — enumerate tasks
        if (preg_match('/\bcleaning\b/', $normalized_prompt)) {
            return $make_rows(['Vacuuming', 'Mopping', 'Dusting', 'Window Cleaning', 'Bathroom Cleaning']);
        }

        // Plumbing / plumber — enumerate tasks
        if (preg_match('/\bplumb(?:ing|er|ers)?\b/', $normalized_prompt)) {
            return $make_rows(['Leak Repair', 'Drain Cleaning', 'Pipe Replacement', 'Water Heater Repair', 'Fixture Installation']);
        }

        // Any service/hiring context — use service-quality dimensions
        if (preg_match('/\b(service|hiring|contractor|agency|provider|technician|consultant|repair|maintenance|installation)\b/', $normalized_prompt)) {
            return $make_rows(['Quality of Work', 'Timeliness', 'Communication', 'Professionalism', 'Value for Money']);
        }

        // Customer-facing interaction context
        if (preg_match('/\b(customer|support|helpdesk|staff|team|employee)\b/', $normalized_prompt)) {
            return $make_rows(['Responsiveness', 'Communication', 'Helpfulness', 'Resolution Time', 'Professionalism']);
        }

        return $make_rows(['Row 1', 'Row 2', 'Row 3', 'Row 4']);
    }

    protected function buildDefaultLikertColumns(): array
    {
        $labels = [
            __('Very Dissatisfied', 'griffinforms-form-builder'),
            __('Dissatisfied', 'griffinforms-form-builder'),
            __('Neutral', 'griffinforms-form-builder'),
            __('Satisfied', 'griffinforms-form-builder'),
            __('Very Satisfied', 'griffinforms-form-builder'),
        ];
        $columns = [];
        foreach ($labels as $i => $label) {
            $columns[] = ['id' => 'col_' . ($i + 1), 'label' => $label];
        }
        return $columns;
    }

    protected function buildDefaultChoiceOptions(string $field_type, string $normalized_prompt, string $subject = '', string $label = ''): array
    {
        $subject_label = $label !== '' ? $label : ($subject !== '' ? $this->humanizeSubjectLabel($subject) : __('Option', 'griffinforms-form-builder'));
        $labels = $this->buildChoiceLabelsForSubject($subject_label, $normalized_prompt);

        if (in_array($field_type, ['inputcheckbox', 'inputradio'], true)) {
            return [[
                'option_column' => 1,
                'options' => array_map(static fn($label) => [
                    'label' => $label,
                    'value' => sanitize_key(str_replace(' ', '_', strtolower($label))),
                ], $labels),
            ]];
        }

        return array_map(static fn($label) => [
            'label' => $label,
            'value' => sanitize_key(str_replace(' ', '_', strtolower($label))),
        ], $labels);
    }

    protected function hasHardcodedChoiceLabels(string $normalized_prompt): bool
    {
        return (bool) preg_match('/\b(meal|drink|beverage|delivery|shipping|pickup|takeout|yes|no|true|false|agree|disagree)\b/', $normalized_prompt);
    }

    protected function buildChoiceLabelsForSubject(string $subject_label, string $normalized_prompt): array
    {
        $subject_label = trim($subject_label);
        if ($subject_label === '') {
            $subject_label = __('Option', 'griffinforms-form-builder');
        }

        if (preg_match('/\bmeal\b/', $normalized_prompt)) {
            return ['Vegetarian', 'Non-Vegetarian', 'Vegan', 'Gluten-Free'];
        }

        if (preg_match('/\bdrink|beverage\b/', $normalized_prompt)) {
            return ['Water', 'Coffee', 'Tea', 'Soda'];
        }

        if (preg_match('/\bdelivery|shipping|pickup|takeout\b/', $normalized_prompt)) {
            return ['Delivery', 'Pickup', 'Dine In'];
        }

        if (preg_match('/\byes|no|true|false|agree|disagree\b/', $normalized_prompt)) {
            return ['Yes', 'No'];
        }

        return [
            sprintf('%s 1', $subject_label),
            sprintf('%s 2', $subject_label),
            sprintf('%s 3', $subject_label),
        ];
    }

    protected function validateInsertFieldChoiceOptions(array $plan, string $prompt): array
    {
        $errors = [];
        $operations = (array) (($plan['operations'] ?? ($plan['draft_patch']['operations'] ?? [])) ?: []);
        if (empty($operations)) {
            return $errors;
        }

        $normalized_prompt = strtolower(trim($prompt));
        $mentions_choice_field = (bool) preg_match('/\b(radio|checkbox|select|dropdown|choice|choices)\b/', $normalized_prompt);

        foreach ($operations as $index => $operation) {
            $operation = (array) $operation;
            if (sanitize_key((string) ($operation['op'] ?? '')) !== 'insert_field') {
                continue;
            }

            $field = (array) ($operation['field'] ?? []);
            $field_type = sanitize_key((string) ($field['field_type'] ?? ''));
            if (!in_array($field_type, ['selectfield', 'inputcheckbox', 'inputradio'], true)) {
                continue;
            }

            $options = (array) ($field['options'] ?? []);
            if (empty($options)) {
                $errors[] = sprintf(
                    'Insert field operation %d uses choice field type "%s" but no options were provided. Prompt: %s',
                    $index + 1,
                    $field_type,
                    $mentions_choice_field ? 'choice field requested' : 'choice field inferred'
                );
                continue;
            }
        }

        return $errors;
    }

    protected function validateInsertFieldLikertSchema(array $plan, string $prompt): array
    {
        $errors = [];
        $operations = (array) (($plan['operations'] ?? ($plan['draft_patch']['operations'] ?? [])) ?: []);
        if (empty($operations)) {
            return $errors;
        }

        foreach ($operations as $index => $operation) {
            $operation = (array) $operation;
            if (sanitize_key((string) ($operation['op'] ?? '')) !== 'insert_field') {
                continue;
            }

            $field = (array) ($operation['field'] ?? []);
            $field_type = sanitize_key((string) ($field['field_type'] ?? ''));
            if ($field_type !== 'inputlikert') {
                continue;
            }

            $likert_rows = (array) ($field['likert_rows'] ?? []);
            $likert_columns = (array) ($field['likert_columns'] ?? []);

            if (empty($likert_rows)) {
                $errors[] = sprintf(
                    'Insert field operation %d inserts an inputlikert field but likert_rows is missing or empty. You must emit explicit row labels derived from the prompt subject. Prompt: "%s"',
                    $index + 1,
                    $prompt
                );
            }

            if (count($likert_columns) < 2) {
                $errors[] = sprintf(
                    'Insert field operation %d inserts an inputlikert field but likert_columns has fewer than 2 entries (%d found). You must emit at least 2 column labels. Prompt: "%s"',
                    $index + 1,
                    count($likert_columns),
                    $prompt
                );
            }
        }

        return $errors;
    }

    protected function extractFieldSubjectFromPrompt(string $normalized_prompt): string
    {
        if (preg_match('/\bwith\s+(.{3,120}?)\s+in\s+rows\b/', $normalized_prompt, $matches)) {
            $candidate = trim((string) ($matches[1] ?? ''));
            $candidate = preg_replace('/\b(measurement|survey|scale|matrix|field|fields|question|questions|different|various|home|device|devices)\b/', ' ', $candidate);
            $candidate = preg_replace('/\s+/', ' ', trim((string) $candidate));
            if ($candidate !== '') {
                return $candidate;
            }
        }

        foreach ([
            '/\bregarding\s+(.{3,120})$/',
            '/\babout\s+(.{3,120})$/',
            '/\bfor\s+(.{3,120})$/',
            '/\bon\s+(.{3,120})$/',
        ] as $pattern) {
            if (preg_match($pattern, $normalized_prompt, $matches)) {
                $candidate = trim((string) ($matches[1] ?? ''));
                $candidate = preg_replace('/\b(measurement|survey|scale|matrix|field|fields|question|questions)\b/', ' ', $candidate);
                $candidate = preg_replace('/\s+/', ' ', trim((string) $candidate));
                if ($candidate !== '') {
                    return $candidate;
                }
            }
        }

        $prompt = preg_replace('/\b(add|new|insert|include|create|make|set up|setup)\b/', '', $normalized_prompt);
        $prompt = preg_replace('/\b(field|fields|input|inputs|question|questions|radio|checkbox|select|dropdown|choice|choices)\b/', '', (string) $prompt);
        $prompt = preg_replace('/\b(for|to|a|an|the|with|using|selecting)\b/', ' ', (string) $prompt);
        $prompt = preg_replace('/\s+/', ' ', trim((string) $prompt));

        return $prompt !== '' ? $prompt : '';
    }

    protected function humanizeSubjectLabel(string $subject): string
    {
        $subject = trim($subject);
        if ($subject === '') {
            return '';
        }

        $subject = preg_replace('/[_\-]+/', ' ', $subject);
        $subject = preg_replace('/\s+/', ' ', $subject);
        $subject = trim((string) $subject);

        return ucwords($subject);
    }

    protected function isFieldGrowthPrompt(string $prompt): bool
    {
        $normalized = strtolower(trim($prompt));

        return (bool) preg_match(
            '/\b(add|append|insert|include|another|extra|additional|new|create|make|build|generate)\b.*\b(field|fields|input|inputs|question|questions|likert|likert\s+scale|matrix|survey\s+grid|rating\s+grid)\b|\b(field|fields|input|inputs|question|questions|likert|likert\s+scale|matrix|survey\s+grid|rating\s+grid)\b.*\b(add|append|insert|include|another|extra|additional|new|create|make|build|generate)\b|\b(add|append|insert|include|create|make|build|generate)\b.*\b(satisfaction|experience|service|quality|feedback|review)\b.*\b(measurement|survey|scale|matrix)\b/',
            $normalized
        );
    }

    protected function isRowMoveRequest(string $normalized_prompt): bool
    {
        return $this->row_prompt_helper->isRowMoveRequest($normalized_prompt);
    }

    protected function maybeBuildDirectPageMovePatch(int $form_id, string $prompt, array $selected_context, array $structure): ?array
    {
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_page_id = absint($selected_context['item_id'] ?? 0);
        $normalized_prompt = strtolower(trim($prompt));

        if ($selected_type !== 'formpage' || $selected_page_id <= 0) {
            return null;
        }

        if (!$this->page_prompt_helper->isPageMoveRequest($normalized_prompt)) {
            return null;
        }

        $pages = array_values((array) ($structure['pages'] ?? []));
        $page_ids = array_values(array_filter(array_map(static function ($page) {
            $page = (array) $page;
            return absint($page['item_id'] ?? 0);
        }, $pages)));
        $current_index = array_search($selected_page_id, $page_ids, true);
        if ($current_index === false) {
            return null;
        }

        $direction = null;
        $requested_position = $this->page_prompt_helper->extractRequestedPagePosition($normalized_prompt);

        if (is_array($requested_position)) {
            if (($requested_position['type'] ?? '') === 'edge') {
                $edge_value = (string) ($requested_position['value'] ?? '');
                if ($edge_value === 'top') {
                    if ($current_index <= 0) {
                        return [
                            'mode' => 'no_op',
                            'message' => __('This page is already at the top of the form.', 'griffinforms-form-builder'),
                        ];
                    }

                    $anchor_id = absint($page_ids[0] ?? 0);
                    if ($anchor_id <= 0) {
                        return null;
                    }

                    return [
                        'mode' => 'patch',
                        'success_message' => __('Moved the page to the top of the form.', 'griffinforms-form-builder'),
                        'operations' => [[
                            'op' => 'move_page',
                            'target' => [
                                'scope' => 'page',
                                'item_id' => $selected_page_id,
                            ],
                            'anchor' => [
                                'scope' => 'page',
                                'item_id' => $anchor_id,
                            ],
                            'position' => 'before',
                        ]],
                    ];
                }

                if ($current_index >= count($page_ids) - 1) {
                    return [
                        'mode' => 'no_op',
                        'message' => __('This page is already at the bottom of the form.', 'griffinforms-form-builder'),
                    ];
                }

                $anchor_id = absint($page_ids[count($page_ids) - 1] ?? 0);
                if ($anchor_id <= 0) {
                    return null;
                }

                return [
                    'mode' => 'patch',
                    'success_message' => __('Moved the page to the bottom of the form.', 'griffinforms-form-builder'),
                    'operations' => [[
                        'op' => 'move_page',
                        'target' => [
                            'scope' => 'page',
                            'item_id' => $selected_page_id,
                        ],
                        'anchor' => [
                            'scope' => 'page',
                            'item_id' => $anchor_id,
                        ],
                        'position' => 'after',
                    ]],
                ];
            } elseif (($requested_position['type'] ?? '') === 'ordinal') {
                $target_index = max(1, absint($requested_position['value'] ?? 0));
                if ($target_index <= 0) {
                    return null;
                }

                if ($target_index > count($page_ids)) {
                    return [
                        'mode' => 'no_op',
                        'message' => sprintf(
                            __('There are only %d pages in this form, so page position %d does not exist.', 'griffinforms-form-builder'),
                            count($page_ids),
                            $target_index
                        ),
                    ];
                }

                $anchor_index = $target_index - 1;
                if ($current_index === $anchor_index) {
                    return [
                        'mode' => 'no_op',
                        'message' => sprintf(
                            __('This page is already at position %d.', 'griffinforms-form-builder'),
                            $target_index
                        ),
                    ];
                }

                $anchor_id = absint($page_ids[$anchor_index] ?? 0);
                if ($anchor_id <= 0) {
                    return [
                        'mode' => 'no_op',
                        'message' => sprintf(
                            __('Page position %d does not exist on this form.', 'griffinforms-form-builder'),
                            $target_index
                        ),
                    ];
                }

                $position = $current_index < $anchor_index ? 'after' : 'before';

                return [
                    'mode' => 'patch',
                    'success_message' => sprintf(
                        __('Moved the page to position %d.', 'griffinforms-form-builder'),
                        $target_index
                    ),
                    'operations' => [[
                        'op' => 'move_page',
                        'target' => [
                            'scope' => 'page',
                            'item_id' => $selected_page_id,
                        ],
                        'anchor' => [
                            'scope' => 'page',
                            'item_id' => $anchor_id,
                        ],
                        'position' => $position,
                    ]],
                ];
            }
        }

        if ($this->containsAnyToken($this->tokenizePrompt($normalized_prompt), ['up', 'before', 'above', 'previous', 'prev'])) {
            $direction = 'up';
        } elseif ($this->containsAnyToken($this->tokenizePrompt($normalized_prompt), ['down', 'after', 'below', 'next'])) {
            $direction = 'down';
        }

        if ($direction === null) {
            return null;
        }

        $anchor_id = 0;
        $position = 'before';

        if ($direction === 'up') {
            if ($current_index <= 0) {
                return [
                    'mode' => 'no_op',
                    'message' => __('This page is already at the top of the form.', 'griffinforms-form-builder'),
                ];
            }

            $anchor_id = absint($page_ids[$current_index - 1] ?? 0);
            $position = 'before';
        } else {
            if ($current_index >= count($page_ids) - 1) {
                return [
                    'mode' => 'no_op',
                    'message' => __('This page is already at the bottom of the form.', 'griffinforms-form-builder'),
                ];
            }

            $anchor_id = absint($page_ids[$current_index + 1] ?? 0);
            $position = 'after';
        }

        if ($anchor_id <= 0) {
            return null;
        }

        return [
            'mode' => 'patch',
            'success_message' => $direction === 'up'
                ? __('Moved the page up.', 'griffinforms-form-builder')
                : __('Moved the page down.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'move_page',
                'target' => [
                    'scope' => 'page',
                    'item_id' => $selected_page_id,
                ],
                'anchor' => [
                    'scope' => 'page',
                    'item_id' => $anchor_id,
                ],
                'position' => $position,
            ]],
        ];
    }

    protected function isRowTargetingPrompt(string $normalized_prompt): bool
    {
        return $this->row_prompt_helper->isRowTargetingPrompt($normalized_prompt);
    }

    protected function maybeBuildDirectRowMovePatch(int $form_id, string $prompt, array $selected_context, array $structure): ?array
    {
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_row_id = absint($selected_context['item_id'] ?? 0);
        $normalized_prompt = strtolower(trim($prompt));

        if (!in_array($selected_type, ['pagerow', 'row'], true) || $selected_row_id <= 0) {
            return null;
        }

        if (!$this->row_prompt_helper->isRowMoveRequest($normalized_prompt)) {
            return null;
        }

        $ordinal_page_target = null;
        if (!preg_match('/\b(next|previous|prev|other|another|different)\s+page\b|\bto\s+(?:a\s+)?(?:new|another|different|other)\s+page\b/i', $normalized_prompt)) {
            if (preg_match('/\bto\s+(?:the\s+)?(\w+)\s+page\b|\bto\s+page\s+(\w+)\b/i', $normalized_prompt, $pm)) {
                $tok = strtolower(trim($pm[1] !== '' ? $pm[1] : $pm[2]));
                if ($tok !== '' && $tok !== 'a' && $tok !== 'the') {
                    $n = $this->parseOrdinalToken($tok);
                    if ($n !== null && $n > 0) {
                        $ordinal_page_target = $n;
                    }
                }
            }
        }

        $is_cross_page_prompt = $ordinal_page_target !== null
            || (bool) preg_match('/\b(next|previous|prev|other|another|different)\s+page\b|\bto\s+(?:a\s+)?(?:new|another|different|other)\s+page\b/i', $normalized_prompt);

        if ($is_cross_page_prompt) {
            $all_pages  = array_values((array) ($structure['pages'] ?? []));
            $page_count = count($all_pages);

            if ($page_count <= 1) {
                return [
                    'mode' => 'no_op',
                    'message' => __('There\'s only one page in this form, so there\'s no other page to move this row to. Add a new page first, then try again.', 'griffinforms-form-builder'),
                ];
            }

            $current_page = null;
            foreach ($all_pages as $candidate) {
                $candidate = (array) $candidate;
                foreach ((array) ($candidate['rows'] ?? []) as $row) {
                    if (absint(((array) $row)['item_id'] ?? 0) === $selected_row_id) {
                        $current_page = $candidate;
                        break 2;
                    }
                }
            }

            if (!is_array($current_page) && !empty($selected_context['parent_id'])) {
                $parent_id = absint($selected_context['parent_id']);
                foreach ($all_pages as $candidate) {
                    if (absint(((array) $candidate)['item_id'] ?? 0) === $parent_id) {
                        $current_page = (array) $candidate;
                        break;
                    }
                }
            }

            if (!is_array($current_page)) {
                return null;
            }

            $current_page_id = absint($current_page['item_id'] ?? 0);
            $all_page_ids    = array_values(array_filter(array_map(static function ($p) {
                return absint(((array) $p)['item_id'] ?? 0);
            }, $all_pages)));
            $current_index = array_search($current_page_id, $all_page_ids, true);
            if ($current_index === false) {
                return null;
            }

            if ($ordinal_page_target !== null) {
                $target_page_index = $ordinal_page_target - 1;
                if ($target_page_index < 0 || $target_page_index >= count($all_page_ids)) {
                    return [
                        'mode'    => 'no_op',
                        'message' => sprintf(
                            __('This form only has %d page(s), so page %d doesn\'t exist.', 'griffinforms-form-builder'),
                            count($all_page_ids),
                            $ordinal_page_target
                        ),
                    ];
                }
                if ($all_page_ids[$target_page_index] === $current_page_id) {
                    return [
                        'mode'    => 'no_op',
                        'message' => __('This row is already on that page.', 'griffinforms-form-builder'),
                    ];
                }
                $direction_label = sprintf(__('page %d', 'griffinforms-form-builder'), $ordinal_page_target);
            } else {
                $is_prev           = (bool) preg_match('/\b(prev|previous)\s+page\b/i', $normalized_prompt);
                $target_page_index = $is_prev ? $current_index - 1 : $current_index + 1;

                if ($target_page_index < 0) {
                    return [
                        'mode' => 'no_op',
                        'message' => __('This row is already on the first page.', 'griffinforms-form-builder'),
                    ];
                }

                if ($target_page_index >= count($all_page_ids)) {
                    return [
                        'mode' => 'no_op',
                        'message' => __('This row is already on the last page.', 'griffinforms-form-builder'),
                    ];
                }

                $direction_label = $is_prev
                    ? __('previous', 'griffinforms-form-builder')
                    : __('next', 'griffinforms-form-builder');
            }

            $target_page_id = $all_page_ids[$target_page_index];
            $target_page    = null;
            foreach ($all_pages as $candidate) {
                if (absint(((array) $candidate)['item_id'] ?? 0) === $target_page_id) {
                    $target_page = (array) $candidate;
                    break;
                }
            }

            if (!is_array($target_page)) {
                return null;
            }

            $target_row_ids = array_values(array_filter(array_map(static function ($r) {
                return absint(((array) $r)['item_id'] ?? 0);
            }, (array) ($target_page['rows'] ?? []))));

            if (!empty($target_row_ids)) {
                $anchor   = ['scope' => 'row', 'item_id' => $target_row_ids[count($target_row_ids) - 1]];
                $position = 'after';
            } else {
                $anchor   = ['scope' => 'page', 'item_id' => $target_page_id];
                $position = 'last';
            }

            return [
                'mode'            => 'patch',
                'success_message' => sprintf(__('Moved the row to the %s page.', 'griffinforms-form-builder'), $direction_label),
                'operations'      => [[
                    'op'       => 'move_row',
                    'target'   => ['scope' => 'row', 'item_id' => $selected_row_id],
                    'anchor'   => $anchor,
                    'position' => $position,
                ]],
            ];
        }

        $page = $this->findPageSnapshotForRow($structure, $selected_row_id);
        if (!is_array($page) && !empty($selected_context['parent_id'])) {
            $candidate_page_id = absint($selected_context['parent_id']);
            foreach ((array) ($structure['pages'] ?? []) as $candidate_page) {
                if (absint($candidate_page['item_id'] ?? 0) === $candidate_page_id) {
                    $page = (array) $candidate_page;
                    break;
                }
            }
        }
        if (!is_array($page)) {
            return null;
        }

        $rows = array_values((array) ($page['rows'] ?? []));
        $row_ids = array_values(array_filter(array_map(static function ($row) {
            $row = (array) $row;
            return absint($row['item_id'] ?? 0);
        }, $rows)));
        $current_index = array_search($selected_row_id, $row_ids, true);
        if ($current_index === false) {
            return null;
        }

        $direction = null;
        $requested_position = $this->row_prompt_helper->extractRequestedRowPosition($normalized_prompt);

        if (is_array($requested_position)) {
            if (($requested_position['type'] ?? '') === 'edge') {
                $edge_value = (string) ($requested_position['value'] ?? '');
                if ($edge_value === 'top') {
                    if ($current_index <= 0) {
                        return [
                            'mode' => 'no_op',
                            'message' => __('This row is already at the top of the page.', 'griffinforms-form-builder'),
                        ];
                    }
                    $anchor_id = absint($row_ids[0] ?? 0);
                    if ($anchor_id <= 0) {
                        return null;
                    }
                    return [
                        'mode' => 'patch',
                        'success_message' => __('Moved the row to the top of the page.', 'griffinforms-form-builder'),
                        'operations' => [[
                            'op' => 'move_row',
                            'target' => ['scope' => 'row', 'item_id' => $selected_row_id],
                            'anchor' => ['scope' => 'row', 'item_id' => $anchor_id],
                            'position' => 'before',
                        ]],
                    ];
                }

                if ($current_index >= count($row_ids) - 1) {
                    return [
                        'mode' => 'no_op',
                        'message' => __('This row is already at the bottom of the page.', 'griffinforms-form-builder'),
                    ];
                }
                $anchor_id = absint($row_ids[count($row_ids) - 1] ?? 0);
                if ($anchor_id <= 0) {
                    return null;
                }
                return [
                    'mode' => 'patch',
                    'success_message' => __('Moved the row to the bottom of the page.', 'griffinforms-form-builder'),
                    'operations' => [[
                        'op' => 'move_row',
                        'target' => ['scope' => 'row', 'item_id' => $selected_row_id],
                        'anchor' => ['scope' => 'row', 'item_id' => $anchor_id],
                        'position' => 'after',
                    ]],
                ];
            } elseif (($requested_position['type'] ?? '') === 'ordinal') {
                $target_index = max(1, absint($requested_position['value'] ?? 0));
                if ($target_index <= 0) {
                    return null;
                }

                if ($target_index > count($row_ids)) {
                    return [
                        'mode' => 'no_op',
                        'message' => sprintf(
                            __('There are only %d rows on this page, so row position %d does not exist.', 'griffinforms-form-builder'),
                            count($row_ids),
                            $target_index
                        ),
                    ];
                }

                $anchor_index = $target_index - 1;
                if ($current_index === $anchor_index) {
                    return [
                        'mode' => 'no_op',
                        'message' => sprintf(
                            __('This row is already at position %d.', 'griffinforms-form-builder'),
                            $target_index
                        ),
                    ];
                }

                $anchor_id = absint($row_ids[$anchor_index] ?? 0);
                if ($anchor_id <= 0) {
                    return [
                        'mode' => 'no_op',
                        'message' => sprintf(
                            __('Row position %d does not exist on this page.', 'griffinforms-form-builder'),
                            $target_index
                        ),
                    ];
                }

                $position = $current_index < $anchor_index ? 'after' : 'before';

                return [
                    'mode' => 'patch',
                    'success_message' => sprintf(
                        __('Moved the row to position %d.', 'griffinforms-form-builder'),
                        $target_index
                    ),
                    'operations' => [[
                        'op' => 'move_row',
                        'target' => [
                            'scope' => 'row',
                            'item_id' => $selected_row_id,
                        ],
                        'anchor' => [
                            'scope' => 'row',
                            'item_id' => $anchor_id,
                        ],
                        'position' => $position,
                    ]],
                ];
            }
        }

        if ($this->containsAnyToken($this->tokenizePrompt($normalized_prompt), ['up', 'before', 'above', 'previous', 'prev'])) {
            $direction = 'up';
        } elseif ($this->containsAnyToken($this->tokenizePrompt($normalized_prompt), ['down', 'after', 'below', 'next'])) {
            $direction = 'down';
        }

        if ($direction === null) {
            return null;
        }

        if (count($row_ids) <= 1) {
            return [
                'mode' => 'no_op',
                'message' => __('This is the only row on the page, so it cannot be moved.', 'griffinforms-form-builder'),
            ];
        }

        $anchor_id = 0;
        $position = 'before';

        if ($direction === 'up') {
            if ($current_index <= 0) {
                return [
                    'mode' => 'no_op',
                    'message' => __('This row is already at the top of the page.', 'griffinforms-form-builder'),
                ];
            }

            $anchor_id = absint($row_ids[$current_index - 1] ?? 0);
            $position = 'before';
        } else {
            if ($current_index >= count($row_ids) - 1) {
                return [
                    'mode' => 'no_op',
                    'message' => __('This row is already at the bottom of the page.', 'griffinforms-form-builder'),
                ];
            }

            $anchor_id = absint($row_ids[$current_index + 1] ?? 0);
            $position = 'after';
        }

        if ($anchor_id <= 0) {
            return null;
        }

        return [
            'mode' => 'patch',
            'success_message' => $direction === 'up'
                ? __('Moved the row up.', 'griffinforms-form-builder')
                : __('Moved the row down.', 'griffinforms-form-builder'),
            'operations' => [[
                'op' => 'move_row',
                'target' => [
                    'scope' => 'row',
                    'item_id' => $selected_row_id,
                ],
                'anchor' => [
                    'scope' => 'row',
                    'item_id' => $anchor_id,
                ],
                'position' => $position,
            ]],
        ];
    }

    protected function isRowInsertRequest(string $normalized_prompt): bool
    {
        $tokens = $this->tokenizePrompt($normalized_prompt);
        return $this->containsAnyToken($tokens, ['add', 'insert', 'create', 'new'])
            && $this->containsAnyToken($tokens, ['row', 'section', 'fila', 'ligne', 'zeile', 'riga']);
    }

    protected function maybeBuildDirectRowColumnSplitPatch(int $form_id, string $prompt, array $selected_context, array $structure): ?array
    {
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        if (!in_array($selected_type, ['pagerow', 'row'], true)) {
            return null;
        }

        $normalized = strtolower(trim($prompt));

        $is_split  = (bool) preg_match('/\b(split|divide|break|separate)\b.*\b(row|section|columns?)\b|\b(row|section)\b.*\b(split|divide|into)\b/i', $normalized);
        $is_add_col = (bool) preg_match('/\b(add|insert|new|another|extra|additional)\b.*\bcolumn\b|\bcolumn\b.*\b(add|insert)\b/i', $normalized);

        if (!$is_split && !$is_add_col) {
            return null;
        }

        // Read current column count first — needed for add-column target.
        $row_id_early  = absint($selected_context['item_id'] ?? 0);
        $row_snap_early = $this->findRowSnapshot($structure, $row_id_early);
        $current_count_early = 0;
        if (is_array($row_snap_early)) {
            foreach ((array) ($row_snap_early['columns'] ?? []) as $col) {
                if (absint($col['item_id'] ?? 0) > 0) {
                    $current_count_early++;
                }
            }
        }
        $current_count_early = max(1, $current_count_early);

        // Extract target column count from prompt.
        $target_count = null;
        $word_map = ['one' => 1, 'two' => 2, 'three' => 3, 'four' => 4];
        if (preg_match('/\binto\s+(\d+|one|two|three|four)\s+col/i', $normalized, $m)) {
            $target_count = is_numeric($m[1]) ? (int) $m[1] : ($word_map[strtolower($m[1])] ?? null);
        } elseif (preg_match('/\b(\d+|one|two|three|four)\s+col/i', $normalized, $m)) {
            $target_count = is_numeric($m[1]) ? (int) $m[1] : ($word_map[strtolower($m[1])] ?? null);
        } elseif (preg_match('/\bhalf\b/', $normalized)) {
            $target_count = 2;
        }

        if ($target_count === null) {
            if ($is_add_col) {
                // "add a column" with no explicit count — add one to current
                $target_count = $current_count_early + 1;
            } else {
                $target_count = 2; // default split into two
            }
        }

        if ($target_count < 1 || $target_count > 4) {
            return [
                'mode' => 'no_op',
                'message' => __('Rows can have between 1 and 4 columns. Please request a column count between 1 and 4.', 'griffinforms-form-builder'),
            ];
        }

        $row_id     = absint($selected_context['item_id'] ?? 0);
        $row_snap   = $this->findRowSnapshot($structure, $row_id);
        $current_columns = [];
        if (is_array($row_snap)) {
            foreach ((array) ($row_snap['columns'] ?? []) as $col) {
                $col_id = absint($col['item_id'] ?? 0);
                if ($col_id > 0) {
                    $current_columns[] = $col_id;
                }
            }
        }

        $current_count = max(1, count($current_columns));

        if ($target_count === $current_count) {
            return [
                'mode' => 'no_op',
                'message' => sprintf(
                    /* translators: %d: column count */
                    __('This row already has %d column(s). No change needed.', 'griffinforms-form-builder'),
                    $current_count
                ),
            ];
        }

        // Compute even widths for the target count (same logic as applier).
        $base      = (int) floor(12 / $target_count);
        $remainder = 12 % $target_count;
        $new_widths = [];
        for ($i = 0; $i < $target_count; $i++) {
            $new_widths[] = $base + ($i < $remainder ? 1 : 0);
        }

        $operations = [];

        // Resize existing columns that will survive.
        $keep = min($current_count, $target_count);
        for ($i = 0; $i < $keep; $i++) {
            $operations[] = [
                'op'      => 'update_column',
                'target'  => ['scope' => 'column', 'item_id' => $current_columns[$i]],
                'changes' => ['width' => $new_widths[$i]],
            ];
        }

        // Add new columns for the extra slots needed.
        for ($i = $keep; $i < $target_count; $i++) {
            $operations[] = [
                'op'       => 'insert_column',
                'target'   => ['scope' => 'row', 'item_id' => $row_id],
                'position' => 'after',
                'column'   => ['width' => $new_widths[$i]],
            ];
        }

        // Remove excess columns when reducing (fields in removed columns are preserved in DB;
        // they become orphaned but are not deleted — consistent with manual column removal).
        for ($i = $target_count; $i < $current_count; $i++) {
            $operations[] = [
                'op'     => 'remove_column',
                'target' => ['scope' => 'column', 'item_id' => $current_columns[$i]],
            ];
        }

        $called_section = (bool) preg_match('/\bsection\b/i', $prompt);
        $item_word = $called_section ? __('section', 'griffinforms-form-builder') : __('row', 'griffinforms-form-builder');

        return [
            'mode' => 'patch',
            'success_message' => sprintf(
                /* translators: 1: "row" or "section", 2: column count */
                __('Split %1$s into %2$d columns.', 'griffinforms-form-builder'),
                $item_word,
                $target_count
            ),
            'operations' => $operations,
        ];
    }

    protected function rowInsertHasSemanticContent(string $normalized_prompt): bool
    {
        // Purpose clause: "for collecting X", "for X", "about X", "to collect X"
        if (preg_match('/\b(for|about|to collect|to gather|to capture|to enter|to input|to submit)\b\s+\w/i', $normalized_prompt)) {
            return true;
        }
        // Named topic after the structural verb+noun: words that carry meaning beyond
        // "row"/"section"/"above"/"below"/"columns" and direction/count words.
        $structural_words = ['add', 'insert', 'create', 'new', 'a', 'an', 'the', 'row', 'section',
            'above', 'below', 'before', 'after', 'with', 'it', 'current', 'this',
            '1', '2', '3', '4', 'one', 'two', 'three', 'four', 'column', 'columns'];
        $tokens = preg_split('/\s+/', $normalized_prompt, -1, PREG_SPLIT_NO_EMPTY);
        foreach ($tokens as $token) {
            $token = trim($token, '.,!?');
            if ($token !== '' && !in_array($token, $structural_words, true)) {
                return true;
            }
        }
        return false;
    }

    protected function maybeBuildDirectRowPatch(int $form_id, string $prompt, array $selected_context, array $structure): ?array
    {
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_row_id = absint($selected_context['item_id'] ?? 0);
        $parent_type = sanitize_key((string) ($selected_context['parent_type'] ?? ''));
        $parent_id = absint($selected_context['parent_id'] ?? 0);
        $normalized_prompt = strtolower(trim($prompt));

        if ($selected_row_id <= 0 && in_array($parent_type, ['pagerow', 'row'], true) && $parent_id > 0) {
            $selected_row_id = $parent_id;
            $selected_type = $parent_type;
        }

        if (!in_array($selected_type, ['pagerow', 'row'], true) || $selected_row_id <= 0) {
            return null;
        }

        $row = $this->findRowSnapshot($structure, $selected_row_id);
        if (!is_array($row)) {
            return null;
        }

        if ($this->row_prompt_helper->isAllRowHeadingPrompt($normalized_prompt)) {
            $page = $this->findPageSnapshotForRow($structure, $selected_row_id);
            if (!is_array($page)) {
                return [
                    'mode' => 'no_op',
                    'message' => __('Rows can only be updated inside a page. Please select a page and try again.', 'griffinforms-form-builder'),
                ];
            }

            $operations = [];
            foreach ((array) ($page['rows'] ?? []) as $candidate_row) {
                $candidate_row = (array) $candidate_row;
                $candidate_row_id = absint($candidate_row['item_id'] ?? 0);
                if ($candidate_row_id <= 0) {
                    continue;
                }

                $heading = $this->row_copy_resolver->resolveHeading($candidate_row, $structure);
                if ($heading === '') {
                    continue;
                }

                $operations[] = [
                    'op' => 'update_row',
                    'target' => [
                        'scope' => 'row',
                        'item_id' => $candidate_row_id,
                    ],
                    'changes' => [
                        'heading' => $heading,
                    ],
                ];
            }

            if (empty($operations)) {
                return [
                    'mode' => 'no_op',
                    'message' => __('No row headings could be generated for this page.', 'griffinforms-form-builder'),
                ];
            }

            return [
                'mode' => 'patch',
                'success_message' => __('Updated all row headings on the page.', 'griffinforms-form-builder'),
                'operations' => $operations,
            ];
        }

        if ($this->page_prompt_helper->isAllPageHeadingPrompt($normalized_prompt)) {
            $page_context_id = absint($selected_context['item_id'] ?? 0);
            if ($page_context_id > 0 && $selected_type === 'formpage') {
                $operations = [];
                foreach ((array) ($structure['pages'] ?? []) as $candidate_page) {
                    $candidate_page = (array) $candidate_page;
                    $candidate_page_id = absint($candidate_page['item_id'] ?? 0);
                    if ($candidate_page_id <= 0) {
                        continue;
                    }

                    $heading = $this->resolvePageHeadingValue($candidate_page, true);
                    if ($heading === '') {
                        continue;
                    }

                    $operations[] = [
                        'op' => 'update_page',
                        'target' => [
                            'scope' => 'page',
                            'item_id' => $candidate_page_id,
                        ],
                        'changes' => [
                            'heading' => $heading,
                        ],
                    ];
                }

                if (!empty($operations)) {
                    $bulk_heading_patch = [
                        'mode' => 'patch',
                        'success_message' => __('Updated all page headings.', 'griffinforms-form-builder'),
                        'operations' => $operations,
                    ];

                    $apply_result = $this->applier->apply($form_id, $bulk_heading_patch, $prompt);
                    if (!empty($apply_result['success'])) {
                        return [
                            'success' => true,
                            'status' => 'applied',
                            'message' => (string) ($apply_result['message'] ?? $bulk_heading_patch['success_message'] ?? __('Patch applied successfully.', 'griffinforms-form-builder')),
                            'mutated_ids' => (array) ($apply_result['mutated_ids'] ?? []),
                            'prompt_key' => $this->getPromptKey(),
                            'patch_plan' => $bulk_heading_patch,
                            'patch_summary' => (array) ($apply_result['patch_summary'] ?? []),
                        ];
                    }

                    return $this->formatFailure(
                        (string) ($apply_result['message'] ?? __('Griffin Assist could not safely apply the generated patch plan.', 'griffinforms-form-builder')),
                        'patch_apply_failed'
                    );
                }
            }
        }

        // Page heading and description prompts with a selected formpage fall through to the AI model
        // so it can use full page context (rows, fields, examples) to generate quality copy.

        if ($this->page_prompt_helper->isAllPageDescriptionPrompt($normalized_prompt)) {
            $page_context_id = absint($selected_context['item_id'] ?? 0);
            if ($page_context_id > 0 && $selected_type === 'formpage') {
                $operations = [];
                foreach ((array) ($structure['pages'] ?? []) as $candidate_page) {
                    $candidate_page = (array) $candidate_page;
                    $candidate_page_id = absint($candidate_page['item_id'] ?? 0);
                    if ($candidate_page_id <= 0) {
                        continue;
                    }

                    $description = $this->resolvePageDescriptionValue($candidate_page, $normalized_prompt);
                    if ($description === '') {
                        continue;
                    }

                    $operations[] = [
                        'op' => 'update_page',
                        'target' => [
                            'scope' => 'page',
                            'item_id' => $candidate_page_id,
                        ],
                        'changes' => [
                            'description' => $description,
                        ],
                    ];
                }

                if (!empty($operations)) {
                    $bulk_description_patch = [
                        'mode' => 'patch',
                        'success_message' => __('Updated all page descriptions.', 'griffinforms-form-builder'),
                        'operations' => $operations,
                    ];

                    $apply_result = $this->applier->apply($form_id, $bulk_description_patch, $prompt);
                    if (!empty($apply_result['success'])) {
                        return [
                            'success' => true,
                            'status' => 'applied',
                            'message' => (string) ($apply_result['message'] ?? $bulk_description_patch['success_message'] ?? __('Patch applied successfully.', 'griffinforms-form-builder')),
                            'mutated_ids' => (array) ($apply_result['mutated_ids'] ?? []),
                            'prompt_key' => $this->getPromptKey(),
                            'patch_plan' => $bulk_description_patch,
                            'patch_summary' => (array) ($apply_result['patch_summary'] ?? []),
                        ];
                    }

                    return $this->formatFailure(
                        (string) ($apply_result['message'] ?? __('Griffin Assist could not safely apply the generated patch plan.', 'griffinforms-form-builder')),
                        'patch_apply_failed'
                    );
                }
            }
        }

        if ($this->row_prompt_helper->isAllRowDescriptionPrompt($normalized_prompt)) {
            $page = $this->findPageSnapshotForRow($structure, $selected_row_id);
            if (!is_array($page)) {
                return [
                    'mode' => 'no_op',
                    'message' => __('Rows can only be updated inside a page. Please select a page and try again.', 'griffinforms-form-builder'),
                ];
            }

            $operations = [];
            foreach ((array) ($page['rows'] ?? []) as $candidate_row) {
                $candidate_row = (array) $candidate_row;
                $candidate_row_id = absint($candidate_row['item_id'] ?? 0);
                if ($candidate_row_id <= 0) {
                    continue;
                }

                $description = $this->row_copy_resolver->resolveDescription($candidate_row, $normalized_prompt);
                if ($description === '') {
                    continue;
                }

                $operations[] = [
                    'op' => 'update_row',
                    'target' => [
                        'scope' => 'row',
                        'item_id' => $candidate_row_id,
                    ],
                    'changes' => [
                        'description' => $description,
                    ],
                ];
            }

            if (empty($operations)) {
                return [
                    'mode' => 'no_op',
                    'message' => __('No row descriptions could be generated for this page.', 'griffinforms-form-builder'),
                ];
            }

            return [
                'mode' => 'patch',
                'success_message' => __('Updated all row descriptions on the page.', 'griffinforms-form-builder'),
                'operations' => $operations,
            ];
        }

        if ($this->row_prompt_helper->isRowDeletePrompt($normalized_prompt) || $this->isRowDeleteRequest($normalized_prompt)) {
            $page = $this->findPageSnapshotForRow($structure, $selected_row_id);
            if (is_array($page) && count((array) ($page['rows'] ?? [])) <= 1) {
                return [
                    'mode' => 'no_op',
                    'message' => __('This is the only row on the page, so it cannot be deleted.', 'griffinforms-form-builder'),
                ];
            }

            return [
                'mode' => 'patch',
                'success_message' => __('Deleted the selected row.', 'griffinforms-form-builder'),
                'operations' => [[
                    'op' => 'remove_row',
                    'target' => [
                        'scope' => 'row',
                        'item_id' => $selected_row_id,
                    ],
                ]],
            ];
        }

        return null;
    }

    protected function isRowDescriptionRequest(string $normalized_prompt): bool
    {
        return (bool) preg_match('/\b(add|update|set|create|improve|rewrite|refine|polish)\b.*\b(row\s+)?description\b|\b(description)\b.*\b(row|section)\b/', $normalized_prompt);
    }

    protected function isRowHeadingRequest(string $normalized_prompt): bool
    {
        return (bool) preg_match('/\b(add|update|set|create|improve|rewrite|refine|polish)\b.*\b(row\s+)?(heading|title)\b|\b(section\s+title)\b/', $normalized_prompt);
    }

    protected function isRowDeleteRequest(string $normalized_prompt): bool
    {
        return (bool) preg_match('/\b(delete|remove|clear)\b.*\b(row|section)\b|\b(row|section)\b.*\b(delete|remove|clear)\b/', $normalized_prompt);
    }

    protected function validateRowInsertColumnLimit(array $plan): ?string
    {
        $operations = (array) (($plan['operations'] ?? ($plan['draft_patch']['operations'] ?? [])));
        foreach ($operations as $operation) {
            $operation = (array) $operation;
            if (sanitize_key((string) ($operation['op'] ?? '')) !== 'insert_row') {
                continue;
            }

            $row = (array) ($operation['row'] ?? []);
            $column_count = absint($row['column_count'] ?? 1);
            if ($column_count > 4) {
                return __('Rows can have at most 4 columns. Please request 4 or fewer columns.', 'griffinforms-form-builder');
            }
        }

        return null;
    }

    protected function validateRowInsertPlacement(array $plan, array $selected_context): ?string
    {
        $operations = (array) (($plan['operations'] ?? ($plan['draft_patch']['operations'] ?? [])));
        $has_row_insert = false;
        foreach ($operations as $operation) {
            $operation = (array) $operation;
            if (sanitize_key((string) ($operation['op'] ?? '')) === 'insert_row') {
                $has_row_insert = true;
                break;
            }
        }

        if (!$has_row_insert) {
            return null;
        }

        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        if (in_array($selected_type, ['form', 'formpage', 'pagerow', 'row'], true)) {
            return null;
        }

        return $this->buildRowInsertPlacementMessage($selected_context);
    }

    protected function buildRowInsertPlacementMessage(array $selected_context): string
    {
        $item_type  = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $item_label = sanitize_text_field((string) ($selected_context['item_label'] ?? ''));

        if ($item_type === 'field') {
            $field_ref = $item_label !== '' ? sprintf(__('the "%s" field', 'griffinforms-form-builder'), $item_label) : __('a field', 'griffinforms-form-builder');
            return sprintf(
                __('You currently have %s selected — rows can\'t be added inside a field. Please select a page or an adjacent row, then try again.', 'griffinforms-form-builder'),
                $field_ref
            );
        }

        if ($item_type === 'rowcolumn') {
            return __('You currently have a column selected — rows can\'t be added inside a column. Please select a page or an adjacent row, then try again.', 'griffinforms-form-builder');
        }

        return __('Rows can only be added inside a page. Please select a page or an adjacent row, then try again.', 'griffinforms-form-builder');
    }

    protected function looksLikeRowInsertRequest(string $normalized_prompt): bool
    {
        $tokens = $this->tokenizePrompt($normalized_prompt);
        return $this->containsAnyToken($tokens, ['add', 'insert', 'new', 'create'])
            && $this->containsAnyToken($tokens, ['row', 'section', 'linea', 'ligne', 'fila', 'zeile', 'riga']);
    }

    protected function extractRequestedColumnCount(string $prompt): ?int
    {
        $tokens = $this->tokenizePrompt($prompt);
        $units = ['column', 'columns', 'col', 'cols', 'columnas', 'columna', 'columnas', 'colonnes', 'colonne', 'spalten', 'colonna'];

        for ($i = 0, $count = count($tokens); $i < $count; $i++) {
            $value = $this->parseColumnCountToken($tokens[$i]);
            if ($value === null) {
                continue;
            }

            $next = $tokens[$i + 1] ?? '';
            $next2 = $tokens[$i + 2] ?? '';
            if (in_array($next, $units, true) || in_array($next2, $units, true)) {
                return $value;
            }

            if ($i > 0 && in_array($tokens[$i - 1], ['with', 'having', 'sobre', 'avec', 'mit', 'con'], true) && in_array($next, $units, true)) {
                return $value;
            }
        }

        return null;
    }

    protected function parseColumnCountToken(string $token): ?int
    {
        $token = strtolower(trim($token));
        if ($token === '') {
            return null;
        }

        $phrase_map = [
            'a couple' => 2,
            'couple' => 2,
            'a few' => 3,
            'few' => 3,
            'some' => 3,
            'several' => 4,
        ];
        if (isset($phrase_map[$token])) {
            return $phrase_map[$token];
        }

        if (ctype_digit($token)) {
            return absint($token);
        }

        if (is_numeric($token)) {
            return (int) round((float) $token, 0, PHP_ROUND_HALF_UP);
        }

        $map = [
            'one' => 1, 'single' => 1, 'uno' => 1, 'un' => 1, 'una' => 1, 'eins' => 1, 'ein' => 1,
            'two' => 2, 'double' => 2, 'dos' => 2, 'deux' => 2, 'zwei' => 2, 'due' => 2,
            'three' => 3, 'triple' => 3, 'tres' => 3, 'trois' => 3, 'drei' => 3, 'tre' => 3,
            'four' => 4, 'cuatro' => 4, 'quatre' => 4, 'vier' => 4, 'quattro' => 4,
            'five' => 5, 'cinco' => 5, 'cinq' => 5, 'funf' => 5, 'fuenf' => 5, 'cinque' => 5,
        ];

        return $map[$token] ?? null;
    }

    protected function parseOrdinalToken(string $token): ?int
    {
        $token = strtolower(trim($token));
        if ($token === '') {
            return null;
        }
        if (preg_match('/^(\d+)(?:st|nd|rd|th)?$/', $token, $m)) {
            return absint($m[1]);
        }
        $map = ['first' => 1, 'second' => 2, 'third' => 3, 'fourth' => 4, 'fifth' => 5,
                'sixth' => 6, 'seventh' => 7, 'eighth' => 8, 'ninth' => 9, 'tenth' => 10];
        return $map[$token] ?? null;
    }

    protected function tokenizePrompt(string $prompt): array
    {
        $prompt = strtolower(trim($prompt));
        $prompt = str_replace(['-', '_', '/', '\\', ':', ';', ',', '(', ')', '[', ']', '{', '}', '"', "'"], ' ', $prompt);
        $tokens = preg_split('/\s+/', $prompt) ?: [];
        return array_values(array_filter(array_map(static function ($token) {
            return trim($token, " \t\n\r\0\x0B.,!?");
        }, $tokens), static function ($token) {
            return $token !== '';
        }));
    }

    protected function containsAnyToken(array $tokens, array $needles): bool
    {
        $lookup = array_fill_keys($tokens, true);
        foreach ($needles as $needle) {
            if (isset($lookup[$needle])) {
                return true;
            }
        }

        return false;
    }

    protected function resolveNavigationLabelValue(string $property, array $structure, array $selected_context): ?string
    {
        $property = sanitize_key($property);

        return match ($property) {
            'next_label' => $this->resolveNextButtonLabel($structure, $selected_context),
            'previous_label' => $this->resolvePreviousButtonLabel($structure, $selected_context),
            'submit_label' => $this->resolveSubmitButtonLabel($structure, $selected_context),
            default => null,
        };
    }

    protected function resolveNextButtonLabel(array $structure, array $selected_context): string
    {
        $page_count = count((array) ($structure['pages'] ?? []));

        if ($page_count <= 1) {
            return __('Next', 'griffinforms-form-builder');
        }

        return __('Continue', 'griffinforms-form-builder');
    }

    protected function resolvePreviousButtonLabel(array $structure, array $selected_context): string
    {
        $page_count = count((array) ($structure['pages'] ?? []));

        if ($page_count <= 1) {
            return __('Previous', 'griffinforms-form-builder');
        }

        return __('Back', 'griffinforms-form-builder');
    }

    protected function resolveSubmitButtonLabel(array $structure, array $selected_context): string
    {
        $current = $this->normalizeText((string) ($structure['form']['submit_label'] ?? ''), 120);

        if ($current !== '' && !in_array(strtolower($current), ['submit', 'send'], true)) {
            return $current;
        }

        return __('Submit', 'griffinforms-form-builder');
    }

    public function getPromptKey(): string
    {
        return 'generate_patch_plan';
    }

    public function getUnsupportedMessage(): string
    {
        return __('I can only build Griffin Assist patch plans for supported form changes right now.', 'griffinforms-form-builder');
    }

    public function handle(int $form_id, string $prompt, string $provider, array $config): array
    {
        $selected_context = is_array($config['selected_context'] ?? null) ? (array) $config['selected_context'] : [];
        $request_context = is_array($config['request_context'] ?? null) ? (array) $config['request_context'] : [];
        $structure = (new StructureSnapshotBuilder())->buildFromForm($form_id);
        $normalized_prompt = strtolower(trim($prompt));
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_id = absint($selected_context['item_id'] ?? 0);

        $is_likert_field_selected = $selected_type === 'field'
            && sanitize_key((string) ($selected_context['field_type'] ?? '')) === 'inputlikert';
        if ($this->looksLikeRowInsertRequest($normalized_prompt)
            && !in_array($selected_type, ['form', 'formpage', 'pagerow', 'row'], true)
            && !$is_likert_field_selected
        ) {
            return [
                'success' => true,
                'status' => 'text',
                'message' => $this->buildRowInsertPlacementMessage($selected_context),
                'mutated_ids' => [],
                'prompt_key' => $this->getPromptKey(),
            ];
        }

        $direct_submit_label_patch = $this->maybeBuildDirectSubmitLabelPatch($prompt, $selected_context);
        if (is_array($direct_submit_label_patch)) {
            $direct_submit_label_patch = $this->attachPlanContext($direct_submit_label_patch, $selected_context, $request_context, $structure);
            $apply_result = $this->applier->apply($form_id, $direct_submit_label_patch, $prompt);
            if (!empty($apply_result['success'])) {
                return [
                    'success' => true,
                    'status' => 'applied',
                    'message' => (string) ($apply_result['message'] ?? $direct_submit_label_patch['success_message'] ?? __('Patch applied successfully.', 'griffinforms-form-builder')),
                    'mutated_ids' => (array) ($apply_result['mutated_ids'] ?? []),
                    'prompt_key' => $this->getPromptKey(),
                    'patch_plan' => $direct_submit_label_patch,
                    'patch_summary' => (array) ($apply_result['patch_summary'] ?? []),
                ];
            }

            return $this->formatFailure(
                (string) ($apply_result['message'] ?? __('Griffin Assist could not safely apply the generated patch plan.', 'griffinforms-form-builder')),
                'patch_apply_failed'
            );
        }

        $direct_placeholder_patch = $this->maybeBuildDirectFieldPlaceholderPatch($prompt, $selected_context, $structure);
        if (is_array($direct_placeholder_patch)) {
            if (($direct_placeholder_patch['mode'] ?? 'patch') === 'no_op') {
                return [
                    'success' => true,
                    'status' => 'text',
                    'message' => (string) ($direct_placeholder_patch['message'] ?? __('This field does not use a placeholder.', 'griffinforms-form-builder')),
                    'mutated_ids' => [],
                    'prompt_key' => $this->getPromptKey(),
                ];
            }

            $direct_placeholder_patch = $this->attachPlanContext($direct_placeholder_patch, $selected_context, $request_context, $structure);
            $apply_result = $this->applier->apply($form_id, $direct_placeholder_patch, $prompt);

            if (!empty($apply_result['success'])) {
                return [
                    'success' => true,
                    'status' => 'applied',
                    'message' => (string) ($apply_result['message'] ?? $direct_placeholder_patch['success_message'] ?? __('Patch applied successfully.', 'griffinforms-form-builder')),
                    'mutated_ids' => (array) ($apply_result['mutated_ids'] ?? []),
                    'prompt_key' => $this->getPromptKey(),
                    'patch_plan' => $direct_placeholder_patch,
                    'patch_summary' => (array) ($apply_result['patch_summary'] ?? []),
                ];
            }

            return $this->formatFailure(
                (string) ($apply_result['message'] ?? __('Griffin Assist could not safely apply the generated patch plan.', 'griffinforms-form-builder')),
                'patch_apply_failed'
            );
        }


        if ($this->looksLikeRowInsertRequest($normalized_prompt)) {
            $requested_columns = $this->extractRequestedColumnCount($normalized_prompt);
            if ($requested_columns > 4) {
                return [
                    'success' => true,
                    'status' => 'text',
                    'message' => __('Rows can have at most 4 columns. Please request 4 or fewer columns.', 'griffinforms-form-builder'),
                    'mutated_ids' => [],
                    'prompt_key' => $this->getPromptKey(),
                ];
            }
        }

        $selected_row_like = in_array($selected_type, ['pagerow', 'row'], true) && $selected_id > 0;
        if (!$selected_row_like && $this->isRowTargetingPrompt($normalized_prompt)) {
            return [
                'success' => true,
                'status' => 'text',
                'message' => __('Please select a row or mention the row name or heading to continue.', 'griffinforms-form-builder'),
                'mutated_ids' => [],
                'prompt_key' => $this->getPromptKey(),
            ];
        }

        $direct_move_patch = $this->maybeBuildDirectPageMovePatch($form_id, $prompt, $selected_context, $structure);
        if (!is_array($direct_move_patch)) {
            $direct_move_patch = $this->maybeBuildDirectRowMovePatch($form_id, $prompt, $selected_context, $structure);
        }
        if (is_array($direct_move_patch)) {
            if (($direct_move_patch['mode'] ?? 'patch') === 'no_op') {
                return [
                    'success' => true,
                    'status' => 'text',
                    'message' => (string) ($direct_move_patch['message'] ?? __('This row cannot be moved further in that direction.', 'griffinforms-form-builder')),
                    'mutated_ids' => [],
                    'prompt_key' => $this->getPromptKey(),
                ];
            }

            $direct_move_patch = $this->attachPlanContext($direct_move_patch, $selected_context, $request_context, $structure);
            $apply_result = $this->applier->apply($form_id, $direct_move_patch, $prompt);

            if (!empty($apply_result['success'])) {
                return [
                    'success' => true,
                    'status' => 'applied',
                    'message' => (string) ($apply_result['message'] ?? $direct_move_patch['success_message'] ?? __('Patch applied successfully.', 'griffinforms-form-builder')),
                    'mutated_ids' => (array) ($apply_result['mutated_ids'] ?? []),
                    'prompt_key' => $this->getPromptKey(),
                    'patch_plan' => $direct_move_patch,
                    'patch_summary' => (array) ($apply_result['patch_summary'] ?? []),
                ];
            }

            return $this->formatFailure(
                (string) ($apply_result['message'] ?? __('Griffin Assist could not safely apply the generated patch plan.', 'griffinforms-form-builder')),
                'patch_apply_failed'
            );
        }

        if ($this->row_prompt_helper->isAllRowDescriptionPrompt($normalized_prompt)) {
            $row_context_id = absint($selected_context['item_id'] ?? 0);
            if ($row_context_id <= 0) {
                $row_context_id = absint($selected_context['parent_id'] ?? 0);
            }

            if ($row_context_id > 0) {
                $page = $this->findPageSnapshotForRow($structure, $row_context_id);
                if (is_array($page)) {
                    $operations = [];
                    foreach ((array) ($page['rows'] ?? []) as $candidate_row) {
                        $candidate_row = (array) $candidate_row;
                        $candidate_row_id = absint($candidate_row['item_id'] ?? 0);
                        if ($candidate_row_id <= 0) {
                            continue;
                        }

                        $description = $this->row_copy_resolver->resolveDescription($candidate_row, $normalized_prompt);
                        if ($description === '') {
                            continue;
                        }

                        $operations[] = [
                            'op' => 'update_row',
                            'target' => [
                                'scope' => 'row',
                                'item_id' => $candidate_row_id,
                            ],
                            'changes' => [
                                'description' => $description,
                            ],
                        ];
                    }

                    if (!empty($operations)) {
                        $bulk_description_patch = [
                            'mode' => 'patch',
                            'success_message' => __('Updated all row descriptions on the page.', 'griffinforms-form-builder'),
                            'operations' => $operations,
                        ];

                        $bulk_description_patch = $this->attachPlanContext($bulk_description_patch, $selected_context, $request_context, $structure);
                        $apply_result = $this->applier->apply($form_id, $bulk_description_patch, $prompt);
                        if (!empty($apply_result['success'])) {
                            return [
                                'success' => true,
                                'status' => 'applied',
                                'message' => (string) ($apply_result['message'] ?? $bulk_description_patch['success_message'] ?? __('Patch applied successfully.', 'griffinforms-form-builder')),
                                'mutated_ids' => (array) ($apply_result['mutated_ids'] ?? []),
                                'prompt_key' => $this->getPromptKey(),
                                'patch_plan' => $bulk_description_patch,
                                'patch_summary' => (array) ($apply_result['patch_summary'] ?? []),
                            ];
                        }

                        return $this->formatFailure(
                            (string) ($apply_result['message'] ?? __('Griffin Assist could not safely apply the generated patch plan.', 'griffinforms-form-builder')),
                            'patch_apply_failed'
                        );
                    }
                }
            }
        }

        $direct_column_split_patch = $this->maybeBuildDirectRowColumnSplitPatch($form_id, $prompt, $selected_context, $structure);
        if (is_array($direct_column_split_patch)) {
            if (($direct_column_split_patch['mode'] ?? 'patch') === 'no_op') {
                return [
                    'success' => true,
                    'status' => 'text',
                    'message' => (string) ($direct_column_split_patch['message'] ?? ''),
                    'mutated_ids' => [],
                    'prompt_key' => $this->getPromptKey(),
                ];
            }
            $direct_column_split_patch = $this->attachPlanContext($direct_column_split_patch, $selected_context, $request_context, $structure);
            $apply_result = $this->applier->apply($form_id, $direct_column_split_patch, $prompt);
            if (!empty($apply_result['success'])) {
                return [
                    'success' => true,
                    'status' => 'applied',
                    'message' => (string) ($apply_result['message'] ?? $direct_column_split_patch['success_message'] ?? __('Patch applied successfully.', 'griffinforms-form-builder')),
                    'mutated_ids' => (array) ($apply_result['mutated_ids'] ?? []),
                    'prompt_key' => $this->getPromptKey(),
                    'patch_plan' => $direct_column_split_patch,
                    'patch_summary' => (array) ($apply_result['patch_summary'] ?? []),
                ];
            }
            return $this->formatFailure(
                (string) ($apply_result['message'] ?? __('Griffin Assist could not safely apply the generated patch plan.', 'griffinforms-form-builder')),
                'patch_apply_failed'
            );
        }

        $direct_insert_patch = $this->maybeBuildDirectRowInsertPatch($form_id, $prompt, $selected_context, $structure);
        if (is_array($direct_insert_patch)) {
            if (($direct_insert_patch['mode'] ?? 'patch') === 'no_op') {
                return [
                    'success' => true,
                    'status' => 'text',
                    'message' => (string) ($direct_insert_patch['message'] ?? __('Rows can only be added inside a page. Please select a page and try again.', 'griffinforms-form-builder')),
                    'mutated_ids' => [],
                    'prompt_key' => $this->getPromptKey(),
                ];
            }

            if (($direct_insert_patch['mode'] ?? 'patch') === 'patch') {
                $direct_insert_patch = $this->attachPlanContext($direct_insert_patch, $selected_context, $request_context, $structure);
                $apply_result = $this->applier->apply($form_id, $direct_insert_patch, $prompt);

                if (!empty($apply_result['success'])) {
                    return [
                        'success' => true,
                        'status' => 'applied',
                        'message' => (string) ($apply_result['message'] ?? $direct_insert_patch['success_message'] ?? __('Patch applied successfully.', 'griffinforms-form-builder')),
                        'mutated_ids' => (array) ($apply_result['mutated_ids'] ?? []),
                        'prompt_key' => $this->getPromptKey(),
                        'patch_plan' => $direct_insert_patch,
                        'patch_summary' => (array) ($apply_result['patch_summary'] ?? []),
                    ];
                }

                return $this->formatFailure(
                    (string) ($apply_result['message'] ?? __('Griffin Assist could not safely apply the generated patch plan.', 'griffinforms-form-builder')),
                    'patch_apply_failed'
                );
            }

            return [
                'success' => true,
                'status' => 'patch',
                'message' => (string) ($direct_insert_patch['success_message'] ?? __('Patch plan generated successfully.', 'griffinforms-form-builder')),
                'mutated_ids' => [],
                'prompt_key' => $this->getPromptKey(),
                'patch_plan' => $direct_insert_patch,
            ];
        }

        $direct_field_insert_patch = $this->maybeBuildDirectFieldInsertPatch($form_id, $prompt, $selected_context);
        if (is_array($direct_field_insert_patch)) {
            $direct_field_insert_patch = $this->attachPlanContext($direct_field_insert_patch, $selected_context, $request_context, $structure);
            $apply_result = $this->applier->apply($form_id, $direct_field_insert_patch, $prompt);

            if (!empty($apply_result['success'])) {
                return [
                    'success' => true,
                    'status' => 'applied',
                    'message' => (string) ($apply_result['message'] ?? $direct_field_insert_patch['success_message'] ?? __('Patch applied successfully.', 'griffinforms-form-builder')),
                    'mutated_ids' => (array) ($apply_result['mutated_ids'] ?? []),
                    'prompt_key' => $this->getPromptKey(),
                    'patch_plan' => $direct_field_insert_patch,
                    'patch_summary' => (array) ($apply_result['patch_summary'] ?? []),
                ];
            }

            return $this->formatFailure(
                (string) ($apply_result['message'] ?? __('Griffin Assist could not safely apply the generated patch plan.', 'griffinforms-form-builder')),
                'patch_apply_failed'
            );
        }

        $this->logBranchDecision(
            $this->getPromptKey(),
            'strict_patch_ai',
            $form_id,
            $prompt,
            [
                'selected_type' => (string) ($selected_context['item_type'] ?? ''),
                'selected_id' => absint($selected_context['item_id'] ?? 0),
                'request_surface' => (string) ($request_context['surface'] ?? ''),
            ]
        );
        $last_errors = [];
        $raw_content = '';

        for ($attempt = 0; $attempt <= self::MAX_REFLECTION_RETRIES; $attempt++) {
            $prompt_text = $attempt === 0
                ? $this->buildUserPrompt($prompt, $selected_context, $request_context, $structure)
                : $this->buildReflectionPrompt($prompt, $raw_content, $last_errors, $selected_context, $request_context, $structure);

            $response = $this->requestStrict(
                $provider,
                $prompt_text,
                ['system' => $this->buildSystemPrompt($normalized_prompt)],
                array_merge($config, ['timeout' => $attempt === 0 ? 20 : 12])
            );

            $raw_content = (string) ($response['content'] ?? '');
            [$decoded, $extract_error] = $this->extractor->extract($raw_content);

            $validation_errors = [];
            if ($extract_error !== null) {
                $validation_errors[] = $extract_error;
            } else {
                    [$plan, $validation_errors] = $this->parser->parse($decoded, $structure, [
                        'selected_context' => $selected_context,
                        'request_context' => $request_context,
                    ]);

                    if (empty($validation_errors) && is_array($plan)) {
                        $plan = $this->normalizeRowColumnCountForBatch($plan, $normalized_prompt);

                        if ($field_growth_plan = $this->convertSingleFieldGrowthPlanToPatch($plan, $structure, $selected_context, $prompt)) {
                            $plan = $field_growth_plan;
                        }

                        if (($plan['mode'] ?? '') === 'no_op') {
                            $move_tokens = $this->tokenizePrompt($normalized_prompt);
                            $is_move_prompt = $this->containsAnyToken($move_tokens, ['move', 'up', 'down', 'before', 'after', 'above', 'below', 'top', 'bottom']);
                            $selected_item_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
                            if ($is_move_prompt && in_array($selected_item_type, ['field', 'form'], true)) {
                                if ($selected_item_type === 'field') {
                                    return [
                                        'success' => true,
                                        'status' => 'text',
                                        'message' => __('Moving a field is not supported yet. Try moving the row instead.', 'griffinforms-form-builder'),
                                        'mutated_ids' => [],
                                        'prompt_key' => $this->getPromptKey(),
                                        'patch_plan' => $plan,
                                    ];
                                }

                                return [
                                    'success' => true,
                                    'status' => 'text',
                                    'message' => __('Select the field or row you want to move first, then try again.', 'griffinforms-form-builder'),
                                    'mutated_ids' => [],
                                    'prompt_key' => $this->getPromptKey(),
                                    'patch_plan' => $plan,
                                ];
                    }

                    if (!empty($validation_errors)) {
                    }
                }

                        $row_insert_placement_message = $this->validateRowInsertPlacement($plan, $selected_context);
                        if ($row_insert_placement_message !== null) {
                            return [
                                'success' => true,
                                'status' => 'text',
                                'message' => $row_insert_placement_message,
                                'mutated_ids' => [],
                                'prompt_key' => $this->getPromptKey(),
                                'patch_plan' => $plan,
                            ];
                        }

                        $row_column_limit_message = $this->validateRowInsertColumnLimit($plan);
                        if ($row_column_limit_message !== null) {
                            return [
                                'success' => true,
                                'status' => 'text',
                                'message' => $row_column_limit_message,
                                'mutated_ids' => [],
                                'prompt_key' => $this->getPromptKey(),
                                'patch_plan' => $plan,
                            ];
                        }

                        if (($plan['mode'] ?? '') === 'patch_with_user_input' && $this->canAutoResolveRowHeadingPlan($plan, $structure, $selected_context)) {
                            $plan = $this->convertRowHeadingPlanToPatch($plan, $structure, $selected_context);
                        }

                        if (($plan['mode'] ?? '') === 'patch_with_user_input' && $this->canAutoResolveRowDescriptionPlan($plan, $structure, $selected_context)) {
                            $plan = $this->convertRowDescriptionPlanToPatch($plan, $structure, $selected_context);
                        }

                        if (($plan['mode'] ?? '') === 'patch_with_user_input' && $this->canAutoResolveFormNamePlan($plan, $structure, $selected_context)) {
                            $plan = $this->convertFormNamePlanToPatch($plan, $structure, $selected_context);
                        }

                        if (($plan['mode'] ?? '') === 'patch_with_user_input' && $this->canAutoResolveFormDescriptionPlan($plan, $structure, $selected_context)) {
                            $plan = $this->convertFormDescriptionPlanToPatch($plan, $structure, $selected_context);
                        }

                        if (($plan['mode'] ?? '') === 'patch_with_user_input' && $this->canCommitSubmitLabelChoice($plan, $selected_context, $prompt)) {
                            $plan = $this->convertSubmitLabelChoicePlanToPatch($plan, $selected_context, $prompt);
                        }

                        if (($plan['mode'] ?? '') === 'patch_with_user_input' && $this->canAutoResolvePageHeadingPlan($plan, $structure, $selected_context)) {
                            $plan = $this->convertPageHeadingPlanToPatch($plan, $structure, $selected_context);
                        }

                        if (($plan['mode'] ?? '') === 'patch_with_user_input' && $this->canAutoResolvePageDescriptionPlan($plan, $structure, $selected_context)) {
                            $plan = $this->convertPageDescriptionPlanToPatch($plan, $structure, $selected_context);
                        }

                        if (($plan['mode'] ?? '') === 'patch_with_user_input' && $this->canAutoResolveFieldLabelPlan($plan, $structure, $selected_context, $prompt)) {
                            $plan = $this->convertFieldLabelPlanToPatch($plan, $structure, $selected_context, $prompt);
                        }

                        if (($plan['mode'] ?? '') === 'patch_with_user_input' && $this->canAutoResolveFieldDescriptionPlan($plan, $structure, $selected_context, $prompt)) {
                            $plan = $this->convertFieldDescriptionPlanToPatch($plan, $structure, $selected_context, $prompt);
                        }

                        if (($plan['mode'] ?? '') === 'patch_with_user_input' && $this->canAutoResolveFieldPlaceholderPlan($plan, $structure, $selected_context, $prompt)) {
                            $plan = $this->convertFieldPlaceholderPlanToPatch($plan, $structure, $selected_context, $prompt);
                        }

                        if (($plan['mode'] ?? '') === 'patch_with_user_input' && $this->canAutoResolveRowDeletePlan($plan, $structure, $selected_context)) {
                            $plan = $this->convertRowDeletePlanToPatch($plan, $structure, $selected_context);
                            if (empty($plan['operations'])) {
                                return [
                                    'success' => true,
                                    'status' => 'text',
                                    'message' => (string) ($plan['success_message'] ?? __('This is the only row on the page, so it cannot be deleted.', 'griffinforms-form-builder')),
                                    'mutated_ids' => [],
                                    'prompt_key' => $this->getPromptKey(),
                                    'patch_plan' => $plan,
                                ];
                            }
                        }

                        if (($plan['mode'] ?? '') === 'patch_with_user_input' && $this->canAutoResolveNavigationLabelPlan($plan, $structure, $selected_context)) {
                            $plan = $this->convertNavigationLabelPlanToPatch($plan, $structure, $selected_context);
                            if (empty($plan['operations'])) {
                                return [
                                    'success' => true,
                                    'status' => 'text',
                                    'message' => (string) ($plan['success_message'] ?? __('There is no navigation button on this form page.', 'griffinforms-form-builder')),
                                    'mutated_ids' => [],
                                    'prompt_key' => $this->getPromptKey(),
                                    'patch_plan' => $plan,
                                ];
                            }
                        }

                        // Also intercept direct patch mode for navigation labels so position guards apply.
                        if (($plan['mode'] ?? '') === 'patch') {
                            $direct_ops = (array) ($plan['operations'] ?? []);
                            if (count($direct_ops) === 1) {
                                $direct_op      = (array) ($direct_ops[0] ?? []);
                                $direct_changes = (array) ($direct_op['changes'] ?? []);
                                $direct_prop    = sanitize_key((string) array_key_first($direct_changes));
                                if (in_array($direct_prop, ['next_label', 'previous_label'], true)) {
                                    $wrapped = ['draft_patch' => $plan];
                                    if ($this->canAutoResolveNavigationLabelPlan($wrapped, $structure, $selected_context)) {
                                        $resolved = $this->convertNavigationLabelPlanToPatch($wrapped, $structure, $selected_context);
                                        if (empty($resolved['operations'])) {
                                            return [
                                                'success' => true,
                                                'status' => 'text',
                                                'message' => (string) ($resolved['success_message'] ?? __('There is no navigation button on this form page.', 'griffinforms-form-builder')),
                                                'mutated_ids' => [],
                                                'prompt_key' => $this->getPromptKey(),
                                                'patch_plan' => $resolved,
                                            ];
                                        }
                                        $plan = $resolved;
                                    }
                                }
                            }
                        }

                        $plan = $this->attachPlanContext($plan, $selected_context, $request_context, $structure);

                        $choice_insert_errors = $this->validateInsertFieldChoiceOptions($plan, $prompt);
                        if (!empty($choice_insert_errors)) {
                            $validation_errors = array_merge($validation_errors, $choice_insert_errors);
                            $this->logRetryFailure($this->getPromptKey(), $attempt + 1, $validation_errors, $raw_content);
                            $last_errors = $validation_errors;
                            continue;
                        }

                        $likert_insert_errors = $this->validateInsertFieldLikertSchema($plan, $prompt);
                        if (!empty($likert_insert_errors)) {
                            $validation_errors = array_merge($validation_errors, $likert_insert_errors);
                            $this->logRetryFailure($this->getPromptKey(), $attempt + 1, $validation_errors, $raw_content);
                            $last_errors = $validation_errors;
                            continue;
                        }

                        if (!empty($config['apply_patch_plan']) && ($plan['mode'] ?? 'patch') === 'patch') {
                            $apply_result = $this->applier->apply($form_id, $plan, $prompt);
                            if (!empty($apply_result['success'])) {
                            return [
                                'success' => true,
                                'status' => 'applied',
                                'message' => (string) ($apply_result['message'] ?? $plan['success_message'] ?? __('Patch applied successfully.', 'griffinforms-form-builder')),
                                'mutated_ids' => (array) ($apply_result['mutated_ids'] ?? []),
                                'prompt_key' => $this->getPromptKey(),
                                'patch_plan' => $plan,
                                'patch_summary' => (array) ($apply_result['patch_summary'] ?? []),
                            ];
                        }

                        return $this->formatFailure(
                            (string) ($apply_result['message'] ?? __('Griffin Assist could not safely apply the generated patch plan.', 'griffinforms-form-builder')),
                            'patch_apply_failed'
                        );
                    }

                    $response = [
                        'success' => true,
                        'status' => sanitize_key((string) ($plan['mode'] ?? 'patch')),
                        'message' => (string) ($plan['success_message'] ?? ($plan['mode'] === 'no_op'
                            ? ($plan['message'] !== '' ? $plan['message'] : __('Select the field, row, or page you want to change first.', 'griffinforms-form-builder'))
                            : __('Patch plan generated successfully.', 'griffinforms-form-builder'))),
                        'mutated_ids' => [],
                        'prompt_key' => $this->getPromptKey(),
                        'patch_plan' => $this->attachPlanContext($plan, $selected_context, $request_context, $structure),
                    ];

                    if ($response['status'] === 'patch_with_user_input') {
                        $advisory = $this->buildAdvisoryFromPlan($plan, $selected_context);
                        if (empty($advisory['choices'])) {
                            // Downgrade to text status if no button choices remain after filtering.
                            $response['status'] = 'text';
                            $response['message'] = $advisory['message'];
                        } else {
                            $response['advisory'] = $advisory;
                        }
                    }

                    return $response;
                }
            }

            $last_errors = $validation_errors;
            $this->logRetryFailure($this->getPromptKey(), $attempt + 1, $validation_errors, $raw_content);
        }


        return $this->formatFailure(
            __('I could not convert that into a safe Griffin Assist patch plan yet. Try a more specific request with a clear target and change.', 'griffinforms-form-builder'),
            'patch_plan_validation_failed'
        );
    }

    protected function buildSystemPrompt(string $normalized_prompt = ''): string
    {
        $extra = [];
        $detected = $this->detectInsertFieldTypeFromPrompt($normalized_prompt);
        if ($detected === 'payment') {
            $extra = $this->buildPaymentFieldExamples();
        } elseif ($detected === 'imageselection') {
            $extra = $this->buildImageSelectionFieldExamples();
        }

        return implode("\n", array_merge([
            'You are Griffin Assist in patch-planning mode for GriffinForms.',
            'Return JSON only. No markdown fences. No commentary outside JSON.',
            'Top-level mode must be one of: patch, patch_with_user_input, no_op.',
            'Patch plans must use only these operations:',
            'update_form, update_page, update_row, update_column, update_field, insert_page, insert_row, insert_column, insert_field, move_page, move_row, move_column, move_field, remove_page, remove_row, remove_column, remove_field, replace, no_op.',
            'Targets must use semantic scopes: form, page, row, column, field, or selected.',
            'Context Rule: If no specific element is targeted, and the prompt asks to update the "name", "title", "description", or "submit button", treat "form" as the target scope.',
            'Allowed field_type values: inputtext, inputemail, inputphone, inputaddress, textarea, selectfield, inputcheckbox, inputradio, inputnumber, inputdate, fileupload, signature, inputrating, inputrichtext, inputlikert, imageselection, payment.',
            'Ambiguity Rule: If a request like "Change heading" or "Edit description" is made and an element is currently "selected", prioritize the "selected" element as the target if it supports that property.',
            'For copy-only updates, use update_* operations with a changes object.',

            'For choice fields, preserve grouped option columns for radio and checkbox fields.',
            'If you are inserting a choice field, the options array must be present in the insert_field payload and must contain at least two concrete choices.',
            'For insert_field operations, the field object must be specific to the user request; do not return a generic label like "Radio Group" or "Checkbox Group" if the prompt names a subject such as seat type, drink type, meal choice, or plan type.',
            'If the request is about adding a checkbox, radio, or select field and the initial plan omits options, regenerate with concrete options. Only return patch_with_user_input if the topic for the choices is completely unknown.',
            'When the user requests a new choice field, include an options array with at least 2 concrete choices that match the subject of the prompt.',
            'For insert_field operations that use selectfield, inputcheckbox, or inputradio, the options array is required and must not be empty.',
            'Likert Rule: inputlikert is a matrix field, not a choice column field. Preserve likert_rows, likert_columns, likert_allow_na, required_mode, and likert_column_width when patching it.',
            'Likert Patch Rule: when the selected field is inputlikert, treat any request about rows, columns, scale labels, or N/A as Likert schema edits. Do not flatten the matrix into selectfield or checkbox options.',
            'Likert Safety Rule: preserve existing Likert row and column ids unless the prompt explicitly asks to rebuild or regenerate the matrix.',
            'Likert Generation Rule: if the prompt says "create likert field", "add likert", "add table", "add chart", "add matrix", "satisfaction matrix", "survey matrix", or "satisfaction measurement", you must emit likert_rows and likert_columns explicitly. Write explicit row labels in likert_rows; do not return generic Row 1/2/3 labels or a plain rating field.',
            'Likert Row Extraction Rule: identify the subject for rows from phrases like "for [subject]", "with [subject] as rows", "about [subject]", "on [subject]", or "based on [subject]". Derive 4-6 specific, realistic row labels from that subject. Examples: "home cleaning services" → Vacuuming, Mopping, Dusting, Window Cleaning, Bathroom Cleaning; "car renting services" → Booking Process, Vehicle Condition, Pickup Experience, Fuel Policy, Customer Support; "home devices" → Television, Smartphone, Tablet, Laptop, Smart Speaker; "plumbing services" → Leak Repair, Drain Cleaning, Pipe Replacement, Water Heater Repair, Fixture Installation; "personal digital devices" → Smartphone, Laptop, Tablet, Smartwatch, Desktop Computer; "customer service" → Responsiveness, Communication, Helpfulness, Resolution Time, Professionalism.',
            'Likert Existing Rows Contract: the Likert schema context provided in this prompt contains the currently stored rows and columns for the selected field under selected_field.likert_rows and selected_field.likert_columns. When appending, polishing, or updating rows or columns, you MUST use these existing values as the base. Never invent a new list from scratch when existing rows are provided — always start from the stored state.',
            'Likert Add Rows Rule: if the selected field is inputlikert and the prompt says "add more rows", "add N more rows", "add another row", "add [subject] rows", or "add two more [subject] row labels", append the requested number of new rows derived from the subject to the existing rows array. Preserve all existing row ids and labels — only append. Return an update_field op with field_settings in changes containing the FULL updated rows array (existing rows + new rows). Use the key "rows" (not "likert_rows"). The existing rows are in the Likert schema context under selected_field.likert_rows — copy them exactly, then append the new ones.',
            'Likert Polish Row Labels Rule: if the selected field is inputlikert and the prompt says "polish row labels", "improve row labels", "rewrite row labels", "refine row labels", or "regenerate row labels", update the label of each existing row to be clearer, more professional, and user-facing. Preserve the existing row ids. Do NOT add or remove rows. Return an update_field op with field_settings in changes using the key "rows" (not "likert_rows"). Example: {"op":"update_field","target":{"scope":"field","item_id":123},"changes":{"field_settings":{"rows":[{"id":"row_1","label":"New Label"},{"id":"row_2","label":"New Label 2"}]}}}',
            'Likert Regenerate Row Labels Rule: if the selected field is inputlikert and the prompt says "regenerate row labels based on [subject]", "rewrite row labels based on [subject]", "change row labels to [subject]", or "replace row labels with [subject]", replace all row labels with 4-6 new labels derived from the specified subject. Generate new stable ids. Return an update_field op with field_settings in changes using the key "rows" (not "likert_rows"). Example: {"op":"update_field","target":{"scope":"field","item_id":123},"changes":{"field_settings":{"rows":[{"id":"row_1","label":"Renaissance"},{"id":"row_2","label":"Baroque"},{"id":"row_3","label":"Classical"},{"id":"row_4","label":"Romantic"},{"id":"row_5","label":"Modern"}]}}}',
            'Likert Polish Column Labels Rule: if the selected field is inputlikert and the prompt says "polish column labels", "improve column labels", "rewrite column labels", "refine column labels", or "improve column labels based on [theme]", update the label of each existing column to be clearer and more user-facing. If a theme is given (e.g. "user emotions"), replace the column labels with theme-appropriate equivalents (e.g. Very Happy, Happy, Neutral, Unhappy, Very Unhappy). Preserve the number of columns and their existing ids. Return an update_field op with field_settings in changes using the key "columns" (not "likert_columns"). Example: {"op":"update_field","target":{"scope":"field","item_id":123},"changes":{"field_settings":{"columns":[{"id":"col_1","label":"New Label"},{"id":"col_2","label":"New Label 2"}]}}}',
            'Likert Column Rule: if the prompt mentions emotions, feelings, or sentiment level for columns, use appropriate emotion labels (e.g. Very Satisfied, Satisfied, Neutral, Dissatisfied, Very Dissatisfied). Default columns when not specified: Strongly Disagree, Disagree, Neutral, Agree, Strongly Agree.',
            'If the prompt says "meal type", "drink type", "seat type", "plan type", "contact method", or similar, choose an appropriate choice-field type and populate options with subject-specific values instead of using a generic field label.',
            'Do NOT use patch_with_user_input for non-choice fields like email, text, or phone fields. Generate the patch directly.',
            'Rows use a 12-column grid. Column width must be between 3 and 12.',
            'Never return rebuilt full form schema.',
            'If the user asks to add or insert an item without specifying where, use {"scope": "form"} as the target to append it safely. Do not use {"scope": "selected"} unless explicitly editing a selected item.',
            'If the user request is ambiguous or missing a required value for an EXISTING item (e.g. "Change label" without specifying the new label), do NOT use "patch" mode.',
            'Ambiguity Rule: For text-based property changes (name, heading, description, submit_label, previous_label, next_label) without a provided value, you MUST use "patch_with_user_input" and provide 3-4 professional alternatives as required_inputs. Exceptions: (1) if a field is selected and the change targets the field description, follow the Field Description Rule and generate the value directly. (2) if the form is selected and the change targets the form description, follow the Form Description Rule and generate the value directly. (3) if an inputnumber field is selected and the prompt mentions "range hint", follow the Number Range Hint Rule and generate the value directly. (4) if an inputdate field is selected and the prompt mentions "date window hint", follow the Date Window Hint Rule and generate the value directly. (5) if a textarea field is selected and the prompt mentions "character limit hint", follow the Character Limit Hint Rule and generate the value directly. (6) if a fileupload field is selected and the prompt mentions "describe upload requirements", follow the Upload Requirements Rule and generate the value directly. (7) if an inputlikert field is selected and the prompt mentions "polish row labels", "polish column labels", "improve row labels", "improve column labels", "rewrite row labels", "refine column labels", "regenerate row labels", or similar, follow the Likert Polish Row Labels Rule or Likert Polish Column Labels Rule and generate the patch directly. (8) if the prompt says "update submit button label", "rewrite submit label", "update submit label", or "change submit label" without providing a target value, follow the Submit Label Rule and generate the value directly.',
            'Strict Required Inputs Rule: The required_inputs array MUST ONLY contain final candidate strings for the property. NEVER include instructions, questions, or guidance text like "Choose a label:" or "Type your own:".',
            'Self-Update Guard: NEVER generate a patch where the new value is identical to the current value found in the structure snapshot. If the user asks for a change but does not provide a new value, follow the Ambiguity Rule.',
            'Page Heading Rule: If a form page is selected and the user says "add heading", "add page heading", "improve heading", "update heading", or "rewrite heading", generate a concrete polished page heading from the selected page name, the existing page heading, and the surrounding page labels. Treat the heading as public-facing user copy that will be visible in the live form, so it should be clear, professional, and safe for end users. Do NOT ask the user to choose a label or return patch_with_user_input for this case.',
            'Page Description Rule: If a form page is selected and the user says "add description", "add page description", "improve description", "update description", or "rewrite description", generate a concrete polished page description from the selected page name, the existing page description, and the surrounding page labels. Treat the description as public-facing user copy that will be visible in the live form, so it should be clear, professional, and safe for end users. Do NOT ask the user to choose a label or return patch_with_user_input for this case.',
            'Row Heading Rule: If a row is selected and the user says "add heading", "add row heading", "improve heading", "update heading", "rewrite heading", or "add section title", generate a concrete polished row heading from the fields in that row and the existing headings in the form. Treat the heading as public-facing user copy that will be visible in the live form, so it should be clear, professional, and safe for end users. Do NOT ask the user to choose a label or return patch_with_user_input for this case.',
            'Row Description Rule: If a row is selected and the user says "add description", "add row description", "improve description", "update description", "rewrite description", or any phrasing like "add [N] lines of description", "add a couple of lines of description", "write a description", "give it a description", or "add some description" — generate a concrete polished row description from the fields in that row and the existing row descriptions in the form. This is NOT a row insert — the user wants to set the description property of the currently selected row. Treat the description as public-facing user copy that will be visible in the live form, so it should be clear, professional, and safe for end users. Do NOT return no_op. Do NOT ask the user to choose a label or return patch_with_user_input for this case.',
            'Form Copy Rule: If the user says "polish form copy", "improve form copy", "rewrite form copy", or similar form-level copy wording, generate a patch plan that covers ALL of the following in a single patch — do NOT skip any category:',
            '(1) update_form: polish name, heading, description, submit_label, and (on multi-page forms) previous_label and next_label on the form.',
            '(2) update_page: for every page — polish heading, description, previous_label, and next_label.',
            '(3) update_row: for every row — polish heading and description. Fix garbled, nonsensical, or placeholder text.',
            '(4) update_field: for every field — polish label, description, and placeholder. Fix garbled, nonsensical, or clearly broken copy.',
            '(5) For selectfield/inputcheckbox/inputradio fields: also rewrite option labels in field.options using the same format as the Choice Field option rules. Preserve option values (slugs) — only change the human-readable labels.',
            '(6) For imageselection fields: rewrite each option title and description in field_settings.options. Preserve image_id, image_url, value, and is_none_option.',
            '(7) For payment fields: rewrite each product name and description in the options array. Preserve id, price, image, link, default_qty, min_qty, max_qty, allow_quantity, featured.',
            '(8) For inputrating fields: if step_labels are present in field_settings, rewrite them to be clear and user-facing.',
            'IMPORTANT: Do NOT skip any field, row, or page. Garbled, nonsensical, or placeholder text is exactly what must be fixed. Keep all output public-facing, professional, and consistent with the form purpose.',
            'Field Label Rule: If a field is selected and the user says "add label", "add field label", "improve label", "update label", "rewrite label", or "make label better", generate a concrete polished field label from the selected field type, field name, existing label, surrounding labels, all other field labels in the form, and form context. Treat the label as public-facing user copy that will be visible in the live form, so it should be clear, professional, and safe for end users. Keep the label length close to the average label length of the other field labels in the form unless the content requires a shorter or longer label. Do NOT ask the user to choose a label or return patch_with_user_input for this case.',
            'Dropdown Label Rule: If the selected field is a selectfield and the user says "improve label", "polish label", "rewrite label", or "update label", generate a clear dropdown label from the form heading, parent page heading, current field description, current dropdown options, and nearby field labels. Prefer user-facing labels that describe the choice category instead of generic labels like "Select". Keep the label concise and aligned with the average label length of the other labels in the form.',
            'Field Description Rule: If a field is selected and the user says "add description", "add field description", "improve description", "update description", "rewrite description", or "polish description", generate a concrete polished field description from the selected field type, existing description, field settings, surrounding labels, form context, and validation rules. Treat the description as public-facing user copy that will be visible in the live form, so it should be clear, professional, and safe for end users. Do NOT use template filler like "Use this field to provide your..." and do NOT ask the user to choose a label or return patch_with_user_input for this case. IMPORTANT: Place the description value in changes.description — NOT in changes.field_settings.description. Example: {"op":"update_field","target":{"scope":"field","item_id":123},"changes":{"description":"Your polished description here."}}',
            'Form Description Rule: If the form itself is selected (scope=form) and the user says "improve description", "improve form description", "update description", "rewrite description", or "polish description", generate a concrete short public-facing form description from the form name, form heading, visible field labels, row headings, and page headings. Keep it under 30 words, warm and direct. Do NOT use filler like "This form collects..." or "Fill out this form to...". Do NOT ask the user to choose a value or return patch_with_user_input for this case.',
            'Submit Label Rule: If the prompt says "update submit button label", "rewrite submit label", "update submit label", or "change submit label" without specifying a target value, generate a concrete submit button label derived from the form name, form heading, form purpose, and existing submit_label. Keep it under 5 words. Use action-oriented language (e.g. "Send Message", "Submit Request", "Get in Touch", "Register Now"). Do NOT return patch_with_user_input for this case. Generate the update_form patch directly.',
            'Upload Requirements Rule: If a fileupload field is selected and the prompt mentions "describe upload requirements" or "upload requirements", generate a concrete field description that tells the user exactly what to upload. Use the field constraints injected in the prompt (accepted_file_types, max_size_mb, max_files). If accepted_file_types are set, name them in plain language (e.g. "PDF, JPG, or PNG"). If max_size_mb is set, state the size limit. If max_files is set and greater than 1, say how many files are allowed. Combine all constraints naturally into one sentence. If no constraints are present, write a short contextual hint from the field label and form heading. Keep the result under 25 words. Do NOT return a generic sentence like "Upload a file." Generate the update_field patch directly in patch mode.',
            'Character Limit Hint Rule: If a textarea field is selected and the prompt mentions "character limit hint" or "char limit hint", generate a concrete field description that tells the user how much to write. Use the field constraints injected in the prompt (min_chars, max_chars). If both are set, state the range. If only max_chars, say the response must be under that limit. If only min_chars, say at least that many characters. If no constraints, write a short contextual hint from the field label and form heading that guides the expected length or depth of response. Keep the result under 20 words. Do NOT return a generic sentence like "Enter your message here." Generate the update_field patch directly in patch mode.',
            'Date Window Hint Rule: If an inputdate field is selected and the prompt mentions "date window hint" or "add date window", generate a concrete field description that tells the user what date to select. Use the field constraints injected in the prompt (date_type, min_date, max_date, relative_min, relative_max, day_type). For fixed_date with both min and max, state the range. For fixed_date with only one bound, state that bound. For relative_date, translate the relative values into plain language (e.g. "within the next 30 days", "up to 3 months from today"). If day_type is "weekdays", say weekdays only. If day_type is "weekends", say weekends only. If day_type is a specific day name like "sunday" or "monday", say that specific day only (e.g. "Sundays only", "Mondays only"). If no constraints are present in the prompt, fall back to a short contextual hint from the field label and form heading. Keep the result under 20 words. Do NOT return a generic sentence like "Select a date." Generate the update_field patch directly in patch mode.',
            'Number Range Hint Rule: If an inputnumber field is selected and the prompt mentions "range hint", "add range", or "range", generate a concrete field description that tells the user what number to enter. Check the field validations for min, max, number_type, allow_decimal, and multiples. If min and max are both set, state the range (e.g. "Enter a number between 1 and 100."). If only min, say at least that value. If only max, say no more than that value. If number_type is positive, say the number must be positive. If allow_decimal is false, say whole numbers only. If multiples is set, say it must be a multiple of that value. If no constraints apply, write a short contextual hint from the field label and form heading. Keep the result under 20 words. Do NOT return a generic sentence like "Enter a number." Generate the update_field patch directly in patch mode.',
            'Phone Format Hint Rule: If an inputphone field is selected and the prompt mentions "format hint", "format_type", "country code", or "international", generate a concrete field description that tells the user exactly what phone format is required. Check the field validations for format_type and allowed_country_codes. If format_type is international, say the number must include the country code (e.g. +44 or +1). If allowed_country_codes lists specific countries, name them. Keep the result under 20 words. Do NOT return a generic sentence like "Enter a phone number where we can reach you." Generate the update_field patch directly in patch mode.',
            'Field Placeholder Rule: If a field is selected and the user says "update placeholder", "rewrite placeholder", "improve placeholder", or "change placeholder", generate a concrete non-empty placeholder from the selected field label, field description, field type, and surrounding context. Do NOT return an empty placeholder value and do NOT ask the user to choose a placeholder for this case.',
            'New Field Rule: When the user asks to add a new field (e.g. "Add email field", "Add name field"), generate an insert_field patch in "patch" mode. Determine the correct field_type and a sensible label from the prompt. For choice fields, include concrete options.',
            'Multiple Fields Rule: When the user asks to add multiple fields — e.g. provides a bullet list, says "add two new fields", "add the following fields", or names more than one field — emit one insert_field operation per field in a single patch. Each operation uses {"scope": "form"} as the target. Do NOT collapse multiple fields into one operation.',
            'Row With Field Rule: When the user asks to add a new row that contains a field — e.g. "add a new row with a text field", "new section with an email field", "add row containing a phone input" — emit insert_row followed by insert_field in a single patch. Use column_count: 1 in the row unless the user explicitly requests multiple columns. IMPORTANT: The insert_field target MUST use the same scope and item_id as the insert_row target — NOT {"scope": "form"}. If insert_row targets {"scope": "page", "item_id": 779}, then insert_field must also use {"scope": "page", "item_id": 779}. This ensures the applier resolves the field into the last row of the correct page (the one just created), not into the last row of a different page. Derive the row heading from the field subject or topic mentioned in the prompt.',
            'Address Sub-label Rule: If the selected field is an inputaddress field and the user asks to improve, update, or rewrite sub-field labels, output an update_field patch. Place the field_settings object inside the "changes" key (not at operation root). The changes.field_settings.labels object must contain all six keys: line1, line2, city, state, postal, country. Use the form heading, parent page heading, and field label to infer address context (e.g. shipping, billing, mailing). Each value must be a short, public-facing label. Do not change any other field properties. Example: {"op":"update_field","target":{"scope":"field","item_id":123},"changes":{"field_settings":{"labels":{"line1":"Street Address","line2":"Apt, Suite, Unit","city":"City","state":"State","postal":"Postal Code","country":"Country"}}}}',
            'File Upload Constraints Rule: If the user explicitly asks to constrain the file upload field to specific file types or categories (e.g. "for images", "pdf only", "passport photos"), include a "field_settings" object in the field payload containing a "custom_mimes" array with the relevant file extensions without the leading dot (e.g. ["jpg", "jpeg", "png", "pdf"]).',
            'Validation Constraints Rule: To enforce character restrictions like "no spaces", "numbers only", or length limits, include a "validations" object in the field. For exact regex patterns, use {"type": "pattern", "pattern": "^[a-z]+$"}. For text constraints, use {"type": "custom-parameters", "allow_spaces": false, "allow_numbers": true, "min": 1, "max": 100}.',
            'Choice Field Insert Rule: If you choose selectfield, inputcheckbox, or inputradio for a new field, you MUST include an options array with at least 2 concrete option labels and values that match the user\'s subject. Do not output an empty options array.',
            'Image Selection Insert Rule: If the user asks to add an image selection field (phrases like "image selection field", "image picker", "image choice"), use field_type "imageselection" — do NOT use inputradio or selectfield. Include a field_settings object with an "options" array. Each option: {"image_id":0,"image_url":"","title":"Option Title","description":"","value":"option_slug","is_none_option":0}. Generate 3-5 options with relevant titles and value slugs from the prompt subject. Leave image_id as 0 and image_url as empty string.',
            'Payment Field Insert Rule: If the user asks to add a payment field — including phrases like "payment field", "product field", "product listing", "pricing field", "ecommerce field", "selling field", "shop field", "store field", "checkout field", "add items with prices", "add products to sell", "I want to sell", or anything implying purchasable items with prices — use field_type "payment" — do NOT use selectfield or inputradio. Include an "options" array directly in the field payload (not inside field_settings). Each option is a product item: {"id":"slug","name":"Product Name","price":0.00,"description":""}. Generate the number of items requested by the user with relevant names and prices derived from the prompt subject. Use realistic prices as floats. Generate a unique slug for each id.',
            'Choice Field Example:',
            '{',
            '  "mode": "patch",',
            '  "success_message": "Added a meal type checkbox field.",',
            '  "operations": [',
            '    {',
            '      "op": "insert_field",',
            '      "target": {"scope": "form", "item_id": 175},',
            '      "position": "after",',
            '      "field": {',
            '        "field_type": "inputcheckbox",',
            '        "name": "meal_type",',
            '        "label": "Meal Type",',
            '        "description": "Select your preferred meal type.",',
            '        "required": false,',
            '        "options": [',
            '          {"label": "Vegetarian", "value": "vegetarian"},',
            '          {"label": "Non-Vegetarian", "value": "non_vegetarian"},',
            '          {"label": "Vegan", "value": "vegan"},',
            '          {"label": "Gluten-Free", "value": "gluten_free"}',
            '        ]',
            '      }',
            '    }',
            '  ]',
            '}',
            'Row/Section Equivalence Rule: In GriffinForms, "section" is the UI label for a layout row — it is NOT a page. "Row" and "section" are always interchangeable and both map to insert_row. A form page is called a "step" in GriffinForms. Only use insert_page when the user explicitly says "add a new page", "add a step", "add a form page", or similar page/step-specific language. Never treat "section" — even "add a section called X" or "a named section" — as insert_page.',
            'Row Insert Rule: Treat "add row", "insert row", "create row", and similar verbs as row-insert requests. Always populate the row payload with a heading derived from any fields or topic mentioned in the prompt. column_count refers to the number of LAYOUT COLUMNS in the row — it is NOT the number of fields. Default column_count to 1 unless the user explicitly requests multiple columns (e.g. "2 columns", "split into 3 columns"). Never infer column_count from the number of fields requested. If the user specifies a column count, round fractional counts to the nearest whole number and accept only whole counts from 1 to 4.',
            'Row Insert Rule — heading required: Every insert_row operation MUST include a "row" object with at least a "heading" key. Derive the heading from the user prompt, fields mentioned, or the topic of the row. Never insert a row without a heading.',
            'Page Insert Rule — name and heading required: Every insert_page operation MUST include a "page" object with "name" and "heading" keys derived from the user prompt. If the user supplied a section/page name use it directly. Never insert a page without populating these fields.',
            'Row Insert Placement Rule: Rows can only be added inside a page — not inside a field, column, or other sub-page element. If the selected context is a field, respond with a no_op message that: (1) acknowledges the field is selected by name if available, (2) explains that a row cannot be added inside a field, and (3) asks the user to select a page or an adjacent row instead. Example message: "You currently have a field selected — rows can\'t be added inside a field. Please select a page or an adjacent row, then try again." If the selected context is a column, give a similar message referencing the column. If the selected context is a pagerow, you may insert relative to it using position "before" or "after" with an anchor. When the prompt says "above", "before", or "prior", set position to "before" with anchor item_id equal to the selected row\'s item_id. When the prompt says "below" or "after", set position to "after" with the same anchor. When no direction is specified and a row is selected, default to position "after" with the anchor. When a page is selected, insert into that page (omit the anchor). When nothing is selected and the form has one page, insert into that page.',
            'Row Move to Page Rule: If the user asks to move the selected row to a specific page — e.g. "move to page 2", "move to the 3rd page", "move to the next page", "move section to last page" — emit a move_row operation where: target = {"scope": "row", "item_id": <selected_row_item_id>}, anchor = {"scope": "page", "item_id": <target_page_item_id>}, position = "last". The anchor identifies the destination page, NOT the row. Do NOT set target.scope to "page" for row moves — that moves the page itself, not the row.',
            'Row Delete Rule: If the user asks to delete, remove, or clear a row, target the supported remove_row operation. Resolve the row by selected row, row name, or row heading, and do not fall back to a generic destructive failure for row deletion.',
            'Row Delete Auto-Resolve Rule: If a delete-row request arrives as patch_with_user_input but the target row is already clear from selection or exact row name/heading match, convert it to a direct remove_row patch instead of asking for choices.',
            'Row Delete Safety Rule: If the selected page only has one row, do not propose deleting it. Instead, return a clear message that the only row on the page cannot be deleted.',
            'Previous Button Logic:',
            ' - 1 Page: Inform the user there is no previous button on single-page forms.',
            ' - 2 Pages: Target scope "page" with "semantic_hint": "previous" (check structure for ID) even if not selected.',
            ' - > 2 Pages: If no page is selected or mentioned, use "no_op" to ask the user to specify which page they mean.',
            'Next Button Logic:',
            ' - 1 Page: Inform the user there is no next button on single-page forms.',
            ' - 2 Pages: Target scope "page" with "semantic_hint": "next" (check structure for ID) even if not selected.',
            ' - > 2 Pages: If no page is selected or mentioned, use "no_op" to ask the user to specify which page they mean.',
            'When targeting a navigation button (Next/Previous), you MUST include "semantic_hint": "next" or "semantic_hint": "previous" in the target object.',
            'For NEWLY CREATED items (e.g. "Add phone field"), do NOT use "no_op"; instead, generate a sensible default label, name, and configuration based on the user request.',
            'Example:',
            '{',
            '  "mode": "patch",',
            '  "success_message": "Added an email field after Name.",',
            '  "operations": [',
            '    {',
            '      "op": "insert_field",',
            '      "target": {"scope": "form"},',
            '      "position": "after",',
            '      "field": {',
            '        "field_type": "inputemail",',
            '        "name": "email",',
            '        "label": "Email",',
            '        "description": "",',
            '        "placeholder": "you@example.com",',
            '        "required": false',
            '      }',
            '    }',
            '  ]',
            '}',
            'For patch_with_user_input, include required_inputs and a draft_patch object whose mode is patch.',
            'Page With Fields Rule: When the user asks to "add a page with fields", "add a page for collecting X", "add a page that includes fields", or any prompt that implies a page should contain fields, you MUST emit multiple operations in a single patch in this exact sequence: (1) insert_page with scope "form", (2) insert_row with scope "page" and no item_id — the applier resolves to the page just created, (3) insert_field(s) with scope "form" and no item_id — each resolves down to the last page → last row → last column just created. Never emit only insert_page when the user clearly wants fields on that page. Choose field_types appropriate to the subject (e.g. contact info → inputtext for name, inputemail, inputphone, inputaddress).',
            'Empty Page Rule: When the user asks to add a field to a specific existing page that has NO rows (rows array is empty in the structure summary), you MUST emit two operations: (1) insert_row targeting that page by item_id with scope "page", (2) insert_field targeting that same page by item_id with scope "page". NEVER use scope "form" when inserting into a named page that has no rows — the applier cannot resolve a column on a page with no rows. Example: if the target page has item_id 802 and zero rows, emit insert_row with target {"scope":"page","item_id":802}, then insert_field with target {"scope":"page","item_id":802}.',
            'insert_page example (new form step / page):',
            '{',
            '  "mode": "patch",',
            '  "success_message": "Added a Medical History page.",',
            '  "operations": [',
            '    {',
            '      "op": "insert_page",',
            '      "target": {"scope": "form"},',
            '      "position": "after",',
            '      "page": {',
            '        "name": "Medical History",',
            '        "heading": "Medical History",',
            '        "description": ""',
            '      }',
            '    }',
            '  ]',
            '}',
            'insert_page with fields example (contact information page):',
            '{',
            '  "mode": "patch",',
            '  "success_message": "Added a Personal Contact Information page with fields.",',
            '  "operations": [',
            '    {',
            '      "op": "insert_page",',
            '      "target": {"scope": "form"},',
            '      "position": "after",',
            '      "page": {',
            '        "name": "Personal Contact Information",',
            '        "heading": "Personal Contact Information",',
            '        "description": "Please provide your personal contact details."',
            '      }',
            '    },',
            '    {',
            '      "op": "insert_row",',
            '      "target": {"scope": "page"},',
            '      "position": "after",',
            '      "row": {',
            '        "heading": "Contact Details",',
            '        "description": "",',
            '        "column_count": 1',
            '      }',
            '    },',
            '    {',
            '      "op": "insert_field",',
            '      "target": {"scope": "form"},',
            '      "position": "after",',
            '      "field": {',
            '        "field_type": "inputtext",',
            '        "name": "full_name",',
            '        "label": "Full Name",',
            '        "description": "",',
            '        "placeholder": "Enter your full name",',
            '        "required": true',
            '      }',
            '    },',
            '    {',
            '      "op": "insert_field",',
            '      "target": {"scope": "form"},',
            '      "position": "after",',
            '      "field": {',
            '        "field_type": "inputemail",',
            '        "name": "email",',
            '        "label": "Email Address",',
            '        "description": "",',
            '        "placeholder": "you@example.com",',
            '        "required": true',
            '      }',
            '    },',
            '    {',
            '      "op": "insert_field",',
            '      "target": {"scope": "form"},',
            '      "position": "after",',
            '      "field": {',
            '        "field_type": "inputphone",',
            '        "name": "phone",',
            '        "label": "Phone Number",',
            '        "description": "",',
            '        "placeholder": "Your phone number",',
            '        "required": false',
            '      }',
            '    }',
            '  ]',
            '}',
            'insert_row example (append to page — no sibling anchor):',
            '{',
            '  "mode": "patch",',
            '  "success_message": "Added an Emergency Contact row.",',
            '  "operations": [',
            '    {',
            '      "op": "insert_row",',
            '      "target": {"scope": "page", "item_id": 12},',
            '      "position": "after",',
            '      "row": {',
            '        "heading": "Emergency Contact",',
            '        "description": "",',
            '        "column_count": 1',
            '      }',
            '    }',
            '  ]',
            '}',
            'insert_row example (above a selected sibling row — anchor required):',
            'Selected context: {"item_type":"pagerow","item_id":2035,"parent_id":12}',
            'Prompt: "add a new row above current row"',
            '{',
            '  "mode": "patch",',
            '  "success_message": "Added a new row above the selected row.",',
            '  "operations": [',
            '    {',
            '      "op": "insert_row",',
            '      "target": {"scope": "page", "item_id": 12},',
            '      "anchor": {"item_id": 2035},',
            '      "position": "before",',
            '      "row": {',
            '        "heading": "",',
            '        "description": "",',
            '        "column_count": 1',
            '      }',
            '    }',
            '  ]',
            '}',
        ], $extra));
    }

    protected function detectInsertFieldTypeFromPrompt(string $normalized_prompt): string
    {
        if ($normalized_prompt === '') {
            return '';
        }
        if (!$this->isFieldGrowthPrompt($normalized_prompt)) {
            return '';
        }
        if (preg_match('/\b(payment field|payment item|payment items|product field|product selection|product listing|pricing field|price field|ecommerce field|e.commerce field|selling field|shop field|store field|order field|purchase field|checkout field)\b/', $normalized_prompt)
            || preg_match('/\b(add|create|insert)\b.*\b(product|products|item|items)\b.*\b(price|prices|cost|costs|amount)\b/', $normalized_prompt)
            || preg_match('/\b(sell|selling|sells)\b.*\b(field|form|product|item)\b|\b(field|form)\b.*\b(sell|selling)\b/', $normalized_prompt)
            || preg_match('/\b(add|create|insert)\b.*\b(product|products)\b/', $normalized_prompt)) {
            return 'payment';
        }
        if (preg_match('/\b(image selection field|image selection|image picker|image choice)\b/', $normalized_prompt)) {
            return 'imageselection';
        }
        return '';
    }

    protected function buildPaymentFieldExamples(): array
    {
        return [
            'Payment Field Schema: A payment field stores its items in the "options" array at the field payload root (not inside field_settings). Each item: {"id":"unique_slug","name":"Product Name","price":0.00,"description":""}. The id must be a unique snake_case slug. Price is a float. Description may be empty.',
            'Payment Field Example 1 (3 workshop items):',
            '{',
            '  "mode": "patch",',
            '  "success_message": "Added a workshop registration payment field.",',
            '  "operations": [',
            '    {',
            '      "op": "insert_field",',
            '      "target": {"scope": "form"},',
            '      "position": "after",',
            '      "field": {',
            '        "field_type": "payment",',
            '        "name": "workshop_registration",',
            '        "label": "Workshop Registration",',
            '        "description": "Select the workshop you wish to attend.",',
            '        "required": true,',
            '        "options": [',
            '          {"id": "morning_session", "name": "Morning Session", "price": 49.00, "description": "9am – 12pm"},',
            '          {"id": "afternoon_session", "name": "Afternoon Session", "price": 49.00, "description": "1pm – 4pm"},',
            '          {"id": "full_day", "name": "Full Day", "price": 89.00, "description": "9am – 4pm"}',
            '        ]',
            '      }',
            '    }',
            '  ]',
            '}',
            'Payment Field Example 2 (digital products):',
            '{',
            '  "mode": "patch",',
            '  "success_message": "Added a digital downloads payment field.",',
            '  "operations": [',
            '    {',
            '      "op": "insert_field",',
            '      "target": {"scope": "form"},',
            '      "position": "after",',
            '      "field": {',
            '        "field_type": "payment",',
            '        "name": "digital_download",',
            '        "label": "Choose Your Download",',
            '        "description": "Select one or more items to purchase.",',
            '        "required": false,',
            '        "options": [',
            '          {"id": "ebook_basic", "name": "Basic eBook", "price": 9.99, "description": "PDF format"},',
            '          {"id": "ebook_premium", "name": "Premium eBook", "price": 19.99, "description": "PDF + EPUB"},',
            '          {"id": "video_course", "name": "Video Course", "price": 79.00, "description": "12 hours of content"}',
            '        ]',
            '      }',
            '    }',
            '  ]',
            '}',
        ];
    }

    protected function buildImageSelectionFieldExamples(): array
    {
        return [
            'Image Selection Field Schema: An imageselection field stores its options inside field_settings.options (not at the field root). Each option: {"image_id":0,"image_url":"","title":"Option Title","description":"","value":"option_slug","is_none_option":0}. Leave image_id as 0 and image_url as empty string — the user will upload images later.',
            'Image Selection Field Example 1 (comfort foods):',
            '{',
            '  "mode": "patch",',
            '  "success_message": "Added a comfort food image selection field.",',
            '  "operations": [',
            '    {',
            '      "op": "insert_field",',
            '      "target": {"scope": "form"},',
            '      "position": "after",',
            '      "field": {',
            '        "field_type": "imageselection",',
            '        "name": "favorite_comfort_food",',
            '        "label": "Favorite Comfort Food",',
            '        "description": "Choose your favourite comfort food from the options below.",',
            '        "required": false,',
            '        "field_settings": {',
            '          "options": [',
            '            {"image_id": 0, "image_url": "", "title": "Pizza", "description": "", "value": "pizza", "is_none_option": 0},',
            '            {"image_id": 0, "image_url": "", "title": "Mac and Cheese", "description": "", "value": "mac_and_cheese", "is_none_option": 0},',
            '            {"image_id": 0, "image_url": "", "title": "Chocolate Cake", "description": "", "value": "chocolate_cake", "is_none_option": 0},',
            '            {"image_id": 0, "image_url": "", "title": "Chicken Soup", "description": "", "value": "chicken_soup", "is_none_option": 0}',
            '          ]',
            '        }',
            '      }',
            '    }',
            '  ]',
            '}',
            'Image Selection Field Example 2 (travel destinations):',
            '{',
            '  "mode": "patch",',
            '  "success_message": "Added a travel destination image selection field.",',
            '  "operations": [',
            '    {',
            '      "op": "insert_field",',
            '      "target": {"scope": "form"},',
            '      "position": "after",',
            '      "field": {',
            '        "field_type": "imageselection",',
            '        "name": "travel_destination",',
            '        "label": "Dream Travel Destination",',
            '        "description": "Pick the destination you would most like to visit.",',
            '        "required": false,',
            '        "field_settings": {',
            '          "options": [',
            '            {"image_id": 0, "image_url": "", "title": "Paris", "description": "City of Light", "value": "paris", "is_none_option": 0},',
            '            {"image_id": 0, "image_url": "", "title": "Tokyo", "description": "Modern meets tradition", "value": "tokyo", "is_none_option": 0},',
            '            {"image_id": 0, "image_url": "", "title": "New York", "description": "The Big Apple", "value": "new_york", "is_none_option": 0},',
            '            {"image_id": 0, "image_url": "", "title": "Sydney", "description": "Harbour City", "value": "sydney", "is_none_option": 0}',
            '          ]',
            '        }',
            '      }',
            '    }',
            '  ]',
            '}',
        ];
    }

    protected function buildUserPrompt(string $prompt, array $selected_context, array $request_context, array $structure): string
    {
        $normalized = strtolower(trim($prompt));
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_id   = absint($selected_context['item_id'] ?? 0);

        $extra_context = [];

        // Advisory continuation context — injected when this request came from an advisory action button.
        $advisory_context = sanitize_textarea_field((string) ($request_context['advisory_context'] ?? ''));
        $original_prompt  = sanitize_text_field((string) ($request_context['original_prompt'] ?? ''));
        if ($advisory_context !== '' || $original_prompt !== '') {
            $extra_context[] = 'Advisory context (this action was selected from a Griffin Assist suggestion):';
            if ($original_prompt !== '') {
                $extra_context[] = 'User\'s original question: ' . $original_prompt;
            }
            if ($advisory_context !== '') {
                $extra_context[] = 'Griffin Assist\'s suggestion: ' . $advisory_context;
            }
        }

        if (
            $selected_type === 'formpage' && $selected_id > 0
            && ($this->page_prompt_helper->isPageHeadingPrompt($normalized)
                || $this->page_prompt_helper->isPageDescriptionPrompt($normalized)
                || $this->page_prompt_helper->isPageRewritePrompt($normalized))
        ) {
            $page = $this->findPageSnapshot($structure, $selected_id);
            if (is_array($page)) {
                $extra_context[] = 'Selected page context:';
                $extra_context[] = wp_json_encode($this->buildPageHeadingContext($page), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            }
        }

        if (in_array($selected_type, ['pagerow', 'row'], true) && $selected_id > 0) {
            $row = $this->findRowSnapshot($structure, $selected_id);
            if (is_array($row)) {
                $row_fields = [];
                foreach ((array) ($row['columns'] ?? []) as $column) {
                    foreach ((array) ($column['fields'] ?? []) as $field) {
                        $field_entry = [
                            'item_id'    => absint($field['item_id'] ?? 0),
                            'field_type' => sanitize_key((string) ($field['field_type'] ?? '')),
                            'label'      => sanitize_text_field((string) ($field['label'] ?? '')),
                        ];
                        $desc = sanitize_textarea_field((string) ($field['description'] ?? ''));
                        if ($desc !== '') {
                            $field_entry['description'] = $desc;
                        }
                        $row_fields[] = $field_entry;
                    }
                }
                $extra_context[] = 'Selected row fields (use these to generate contextual heading and description):';
                $extra_context[] = wp_json_encode($row_fields, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            }
        }

        return implode("\n", array_merge([
            'User request:',
            $prompt,
            'Selected context:',
            wp_json_encode($selected_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'Routing context:',
            wp_json_encode([
                'surface' => (string) ($request_context['surface'] ?? ''),
                'origin' => (string) ($request_context['origin'] ?? ''),
                'response_mode' => (string) ($request_context['response_mode'] ?? ''),
                'semantic_intent' => (string) ($request_context['semantic_intent'] ?? ''),
            ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'Site context:',
            wp_json_encode((array) ($request_context['site_context'] ?? []), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'Current structure summary:',
            wp_json_encode($this->condenseStructure($structure), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
        ], $extra_context, [
            'Return only a Griffin Assist mutation plan JSON object.',
        ]));
    }

    protected function buildPageHeadingContext(array $page): array
    {
        $rows = [];
        $fields = [];

        foreach ((array) ($page['rows'] ?? []) as $row) {
            $row_entry = [];
            $heading = sanitize_text_field((string) ($row['heading'] ?? ''));
            $description = sanitize_textarea_field((string) ($row['description'] ?? ''));
            if ($heading !== '') {
                $row_entry['heading'] = $heading;
            }
            if ($description !== '') {
                $row_entry['description'] = $description;
            }

            foreach ((array) ($row['columns'] ?? []) as $column) {
                foreach ((array) ($column['fields'] ?? []) as $field) {
                    $label = sanitize_text_field((string) ($field['label'] ?? ''));
                    $desc  = sanitize_textarea_field((string) ($field['description'] ?? ''));
                    $type  = sanitize_key((string) ($field['field_type'] ?? ''));
                    if ($label === '') {
                        continue;
                    }
                    $field_entry = ['label' => $label, 'field_type' => $type];
                    if ($desc !== '') {
                        $field_entry['description'] = $desc;
                    }
                    $fields[] = $field_entry;
                }
            }

            if (!empty($row_entry)) {
                $rows[] = $row_entry;
            }
        }

        return [
            'current_heading'     => sanitize_text_field((string) ($page['heading'] ?? '')),
            'current_description' => sanitize_textarea_field((string) ($page['description'] ?? '')),
            'rows'                => $rows,
            'fields'              => $fields,
            'heading_examples'    => [
                'Your Contact Details',
                'Billing Information',
                'Tell Us About Yourself',
                'Event Preferences',
                'Medical Background',
                'Project Requirements',
                'Shipping Address',
                'Account Setup',
            ],
            'heading_rules' => 'Generate a concise, public-facing page heading (3–6 words). Derive it from the fields and rows on this page. Use title case. Do not use generic labels like "Page" or "Step". Return an update_page patch with the new heading in changes.',
        ];
    }

    protected function buildReflectionPrompt(string $prompt, string $raw_content, array $errors, array $selected_context, array $request_context, array $structure): string
    {
        $choice_hint = '';
        foreach ($errors as $error) {
            if (str_contains(strtolower((string) $error), 'no options were provided')) {
                $choice_hint = 'Choice field fix: if the field is selectfield, inputcheckbox, or inputradio, include a non-empty options array with at least two concrete choices. Do not omit options.';
                break;
            }
        }

        $likert_schema_context = $this->buildLikertSchemaContext($selected_context, $structure);

        return implode("\n", [
            'Your previous Griffin Assist patch plan response was invalid.',
            'Original request:',
            $prompt,
            'Validation errors:',
            wp_json_encode(array_values($errors), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            $choice_hint !== '' ? $choice_hint : '',
            'Likert schema reference:',
            wp_json_encode($likert_schema_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'Likert row-label requirement:',
            'If the request is for a likert, likert scale, matrix, survey grid, or satisfaction measurement field, you MUST return likert_rows as a JSON array of objects with stable id and label values. The labels must be explicit row labels derived from the prompt. Do not return generic Row 1, Row 2, Row 3 unless the prompt truly gives no subject at all.',
            'Likert row example requirement:',
            'If the prompt mentions different home devices, return rows like Television, Smartphone, Tablet, Laptop, and Smart Speaker. If the prompt mentions another concrete topic, derive row labels from that topic instead of using generic placeholders.',
            'Likert customer service example requirement:',
            'If the prompt says customer service feedback, return explicit row labels such as Responsiveness, Communication, Helpfulness, Resolution Time, and Professionalism instead of generic row numbers.',
            'Likert plumbing services example requirement:',
            'If the prompt says common plumbing services or similar, return explicit row labels such as Leak Repair, Drain Cleaning, Pipe Replacement, Water Heater Repair, and Fixture Installation instead of generic row numbers.',
            'Previous raw response:',
            $raw_content,
            'Selected context:',
            wp_json_encode($selected_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'Routing context:',
            wp_json_encode([
                'surface' => (string) ($request_context['surface'] ?? ''),
                'response_mode' => (string) ($request_context['response_mode'] ?? ''),
                'semantic_intent' => (string) ($request_context['semantic_intent'] ?? ''),
            ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'Current structure summary:',
            wp_json_encode($this->condenseStructure($structure), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'Return corrected JSON only.',
        ]);
    }

    protected function buildLikertSchemaContext(array $selected_context, array $structure): array
    {
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_field = [];

        if ($selected_type === 'field' && isset($selected_context['item_id'])) {
            $field_id = absint($selected_context['item_id']);
            foreach ((array) ($structure['pages'] ?? []) as $page) {
                foreach ((array) ($page['rows'] ?? []) as $row) {
                    foreach ((array) ($row['columns'] ?? []) as $column) {
                        foreach ((array) ($column['fields'] ?? []) as $field) {
                            if (absint($field['item_id'] ?? 0) !== $field_id) {
                                continue;
                            }

                            if (sanitize_key((string) ($field['field_type'] ?? '')) === 'inputlikert') {
                                $selected_field = $this->buildLikertFieldSchemaContext((array) $field);
                            }
                            break 3;
                        }
                    }
                }
            }
        }

        return [
            'selected_field' => $selected_field,
            'likert_examples' => [
                'field_type' => 'inputlikert',
                'label' => 'Customer Satisfaction',
                'likert_rows' => [
                    ['id' => 'row_1', 'label' => 'Support was helpful'],
                    ['id' => 'row_2', 'label' => 'Response time was fast'],
                    ['id' => 'row_3', 'label' => 'Issue was resolved'],
                ],
                'likert_columns' => [
                    ['id' => 'col_1', 'label' => 'Strongly disagree'],
                    ['id' => 'col_2', 'label' => 'Disagree'],
                    ['id' => 'col_3', 'label' => 'Neutral'],
                    ['id' => 'col_4', 'label' => 'Agree'],
                    ['id' => 'col_5', 'label' => 'Strongly agree'],
                ],
                'likert_allow_na' => true,
                'required_mode' => 'all',
                'likert_column_width' => 'auto',
            ],
            'stored_response_shape' => [
                'row_1' => 'col_4',
                'row_2' => 'col_5',
                'row_3' => 'na',
            ],
            'editing_rules' => [
                'preserve_existing_row_ids' => true,
                'preserve_existing_column_ids' => true,
                'do_not_flatten_into_choice_options' => true,
            ],
        ];
    }

    protected function buildLikertFieldSchemaContext(array $field): array
    {
        $fs = is_array($field['field_settings'] ?? null) ? (array) $field['field_settings'] : [];

        $raw_rows = !empty($field['likert_rows']) ? (array) $field['likert_rows'] : (array) ($fs['rows'] ?? []);
        $raw_cols = !empty($field['likert_columns']) ? (array) $field['likert_columns'] : (array) ($fs['columns'] ?? []);

        $sanitize = static function (array $items): array {
            return array_values(array_map(static function ($item) {
                return [
                    'id'    => sanitize_text_field((string) ($item['id'] ?? '')),
                    'label' => sanitize_text_field((string) ($item['label'] ?? '')),
                ];
            }, $items));
        };

        return [
            'item_id'            => absint($field['item_id'] ?? 0),
            'field_type'         => 'inputlikert',
            'label'              => sanitize_text_field((string) ($field['label'] ?? '')),
            'likert_rows'        => $sanitize($raw_rows),
            'likert_columns'     => $sanitize($raw_cols),
            'likert_allow_na'    => !empty($field['likert_allow_na'] ?? $fs['allow_na'] ?? false),
            'required_mode'      => sanitize_key((string) ($field['required_mode'] ?? $fs['required_mode'] ?? 'all')),
            'likert_column_width'=> sanitize_text_field((string) ($field['likert_column_width'] ?? $fs['column_width'] ?? '')),
        ];
    }

    protected function condenseStructure(array $structure): array
    {
        $form = [
            'name' => sanitize_text_field((string) ($structure['form']['name'] ?? '')),
            'heading' => sanitize_text_field((string) ($structure['form']['heading'] ?? '')),
            'description' => sanitize_textarea_field((string) ($structure['form']['description'] ?? '')),
            'submit_label' => sanitize_text_field((string) ($structure['form']['submit_label'] ?? 'Submit')),
        ];

        $pages = [];

        foreach (array_values((array) ($structure['pages'] ?? [])) as $page) {
            $rows = [];
            foreach (array_values((array) ($page['rows'] ?? [])) as $row_index => $row) {
                $columns = [];
                foreach (array_values((array) ($row['columns'] ?? [])) as $column) {
                    $columns[] = [
                        'item_id' => absint($column['item_id'] ?? 0),
                        'name' => sanitize_text_field((string) ($column['name'] ?? '')),
                        'column_label' => sanitize_text_field((string) ($column['column_label'] ?? '')),
                        'width' => absint($column['width'] ?? 12),
                        'field_count' => count((array) ($column['fields'] ?? [])),
                    ];
                }

                $rows[] = [
                    'item_id' => absint($row['item_id'] ?? 0),
                    'name' => sanitize_text_field((string) ($row['name'] ?? '')),
                    'heading' => sanitize_text_field((string) ($row['heading'] ?? '')),
                    'description' => sanitize_textarea_field((string) ($row['description'] ?? '')),
                    'row_label' => sprintf('Row %d', $row_index + 1),
                    'columns' => $columns,
                ];
            }

            $pages[] = [
                'item_id' => absint($page['item_id'] ?? 0),
                'name' => sanitize_text_field((string) ($page['name'] ?? '')),
                'heading' => sanitize_text_field((string) ($page['heading'] ?? '')),
                'description' => sanitize_textarea_field((string) ($page['description'] ?? '')),
                'previous_label' => sanitize_text_field((string) ($page['previous_label'] ?? 'Previous')),
                'next_label' => sanitize_text_field((string) ($page['next_label'] ?? 'Next')),
                'rows' => $rows,
            ];
        }

        $fields = [];
        foreach (array_values((array) ($structure['fields'] ?? [])) as $field) {
            $fields[] = [
                'item_id' => absint($field['item_id'] ?? 0),
                'field_type' => sanitize_key((string) ($field['field_type'] ?? '')),
                'label' => sanitize_text_field((string) ($field['label'] ?? '')),
                'page_label' => sanitize_text_field((string) ($field['page_label'] ?? '')),
                'row_label' => sanitize_text_field((string) ($field['row_label'] ?? '')),
                'column_label' => sanitize_text_field((string) ($field['column_label'] ?? '')),
            ];
        }

        return [
            'form' => (array) ($structure['form'] ?? []),
            'page_count' => count($pages),
            'field_count' => count($fields),
            'pages' => $pages,
            'fields' => array_slice($fields, 0, 16),
        ];
    }
}
