<?php

namespace GriffinForms\Includes\Ai\PromptRegistry\Handlers;

use GriffinForms\Admin\Sql\App;
use GriffinForms\Admin\Sql\Format;
use GriffinForms\Includes\Ai\PromptRegistry\ShadowDraftStructureApplier;

class GenerateFoundationPromptHandler extends BasePromptHandler
{
    protected const MAX_REFLECTION_RETRIES = 2;
    protected const SUPPORTED_FIELD_TYPES = [
        'inputtext',
        'inputemail',
        'inputphone',
        'inputaddress',
        'textarea',
        'selectfield',
        'inputcheckbox',
        'inputradio',
        'inputnumber',
        'inputdate',
        'fileupload',
        'signature',
        'inputrating',
        'inputrichtext',
        'inputlikert',
    ];

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

    public function getUnsupportedMessage(): string
    {
        return __('I can generate core form structure and supported Pro fields right now. Try asking for pages, sections, standard fields, signature, rating, rich text, or Likert fields.', 'griffinforms-form-builder');
    }

    public function handle(int $form_id, string $prompt, string $provider, array $config): array
    {
        $handler_started_at = microtime(true);
        $selected_context = is_array($config['selected_context'] ?? null) ? (array) $config['selected_context'] : [];
        $is_additive_prompt = $this->isAdditivePrompt($prompt);
        $snapshot_started_at = microtime(true);
        $existing_structure = ($is_additive_prompt || !empty($selected_context['item_type']) || !empty($selected_context['item_id']) || !empty($selected_context['parent_id']))
            ? $this->getExistingStructureSnapshot($form_id)
            : [];
        $snapshot_elapsed_ms = (int) round((microtime(true) - $snapshot_started_at) * 1000);
        $selected_context = $this->normalizeSelectedContext($selected_context, $existing_structure);
        $is_selected_text_edit = $this->isSelectedTextEditIntent($prompt, $selected_context);
        if ($this->isAmbiguousReorderIntent($prompt, $selected_context, $existing_structure)) {
            return $this->formatFailure(
                __('Reordering needs a clearer instruction. Try something like "move Email after Name", "put Message last", or "sort fields alphabetically" within the selected scope.', 'griffinforms-form-builder'),
                'ambiguous_reorder_intent',
                409
            );
        }
        $existing_field_signatures = $this->collectExistingFieldSignatures($existing_structure);
        $existing_field_count = $this->countFieldsInExistingStructure($existing_structure);
        $this->logAdditiveState(
            'handler_start',
            $form_id,
            $prompt,
            [
                'is_additive' => $is_additive_prompt ? 1 : 0,
                'existing_field_count' => $existing_field_count,
                'existing_pages' => count((array) ($existing_structure['pages'] ?? [])),
                'selected_type' => (string) ($selected_context['item_type'] ?? ''),
                'selected_id' => absint($selected_context['item_id'] ?? 0),
                'selected_text_edit' => $is_selected_text_edit ? 1 : 0,
            ]
        );
        $system_prompt = $is_selected_text_edit
            ? $this->buildSelectedTextEditSystemPrompt($selected_context)
            : $this->buildSystemPrompt();
        $user_prompt = $is_selected_text_edit
            ? $this->buildSelectedTextEditUserPrompt($prompt, $selected_context, $existing_structure)
            : $this->buildUserPrompt($prompt, $existing_structure, $is_additive_prompt, $selected_context);

        $this->logAdditiveState(
            'handler_context_ready',
            $form_id,
            $prompt,
            [
                'snapshot_ms' => $snapshot_elapsed_ms,
                'handler_ms' => (int) round((microtime(true) - $handler_started_at) * 1000),
                'selected_type' => (string) ($selected_context['item_type'] ?? ''),
                'selected_id' => absint($selected_context['item_id'] ?? 0),
            ]
        );

        $this->logBranchDecision(
            $this->getPromptKey(),
            $is_selected_text_edit ? 'selected_text_edit' : 'strict_ai_only',
            $form_id,
            $prompt,
            [
                'selected_type' => (string) ($selected_context['item_type'] ?? ''),
                'selected_id' => absint($selected_context['item_id'] ?? 0),
                'field_growth' => (!$is_selected_text_edit && $this->isFieldGrowthPrompt($prompt)) ? 1 : 0,
            ]
        );

        $last_errors = [];
        $raw_content = '';
        $field_growth_prompt = !$is_selected_text_edit && $this->isFieldGrowthPrompt($prompt);

        for ($attempt = 0; $attempt <= self::MAX_REFLECTION_RETRIES; $attempt++) {
            $attempt_started_at = microtime(true);
            $attempt_timeout = $attempt === 0
                ? ($field_growth_prompt ? 35 : 20)
                : 15;
            $attempt_prompt = $attempt === 0
                ? $user_prompt
                : (
                    $is_selected_text_edit
                        ? $this->buildSelectedTextEditReflectionPrompt($prompt, $raw_content, $last_errors, $selected_context, $existing_structure)
                        : $this->buildReflectionPrompt($prompt, $raw_content, $last_errors, $existing_structure, $is_additive_prompt, $selected_context)
                );

            $response = $this->requestStrict(
                $provider,
                $attempt_prompt,
                ['system' => $system_prompt],
                array_merge($config, ['timeout' => $attempt_timeout])
            );
            $this->logAdditiveState(
                'request_complete',
                $form_id,
                $prompt,
                [
                    'attempt' => $attempt + 1,
                    'elapsed_ms' => (int) round((microtime(true) - $attempt_started_at) * 1000),
                    'timeout_s' => $attempt_timeout,
                    'field_growth' => $field_growth_prompt ? 1 : 0,
                ]
            );

            $raw_content = (string) ($response['content'] ?? '');
            [$decoded, $extract_error] = $this->extractor->extract($raw_content);
            $validation_errors = [];
            if ($extract_error !== null) {
                $validation_errors[] = $extract_error;
            } elseif ($is_selected_text_edit) {
                $validation_errors = $this->validateSelectedTextEditPayload($decoded, $prompt, $selected_context);
            } else {
                $validation_errors = $this->validateSchema($decoded);
            }

            if (empty($validation_errors) && is_array($decoded)) {
                if ($is_selected_text_edit) {
                    $target_schema = $this->applySelectedTextEditPayload($prompt, $selected_context, $existing_structure, $decoded);
                    if ($target_schema === null) {
                        return $this->formatFailure(
                            __('Griffin Assist could not apply that change to the selected element without risking the rest of the draft.', 'griffinforms-form-builder'),
                            'selected_edit_apply_failed'
                        );
                    }

                    $schema_validation_errors = $this->validateSchema($target_schema);
                    if (!empty($schema_validation_errors)) {
                        $last_errors = $schema_validation_errors;
                        $this->logRetryFailure($this->getPromptKey(), $attempt + 1, $schema_validation_errors, $raw_content);
                        continue;
                    }

                    $applier = new ShadowDraftStructureApplier();
                    $apply_result = $applier->apply($form_id, $target_schema, $prompt);

                    if (!empty($apply_result['success'])) {
                        return [
                            'success' => true,
                            'status' => 'applied',
                            'message' => $this->extractSelectedTextEditSuccessMessage($decoded, $prompt, $selected_context),
                            'mutated_ids' => $apply_result['mutated_ids'] ?? [],
                            'lineage' => $apply_result['lineage'] ?? [],
                            'is_new' => true,
                            'prompt_key' => $this->getPromptKey(),
                        ];
                    }

                    return $this->formatFailure(
                        (string) ($apply_result['message'] ?? __('I generated a targeted edit, but Griffin Assist could not safely apply it to the draft.', 'griffinforms-form-builder')),
                        'apply_failed'
                    );
                }

                $decoded = $this->normalizeInternalNames($existing_structure, $decoded);
                $decoded = $this->normalizeNewPagePresentation($prompt, $existing_structure, $decoded);
                $decoded = $this->applySelectedElementFallbacks($prompt, $selected_context, $existing_structure, $decoded);
                [$decoded, $merged_additive] = $this->prepareSchemaForApply(
                    $prompt,
                    $form_id,
                    $is_additive_prompt,
                    $existing_structure,
                    $existing_field_signatures,
                    $decoded
                );

                $generated_field_count = $this->countFieldsInSchema($decoded);
                $generated_field_signatures = $this->collectSchemaFieldSignatures($decoded);

                if (
                    $is_additive_prompt
                    && $this->isFieldGrowthPrompt($prompt)
                    && (
                        $generated_field_count <= $existing_field_count
                        || count($generated_field_signatures) <= count($existing_field_signatures)
                    )
                ) {
                    $this->logAdditiveState(
                        'field_growth_ai_under_generated',
                        $form_id,
                        $prompt,
                        [
                            'existing_field_count' => $existing_field_count,
                            'generated_field_count' => $generated_field_count,
                            'existing_signatures' => count($existing_field_signatures),
                            'generated_signatures' => count($generated_field_signatures),
                        ],
                        'warning'
                    );
                }

                if ($this->wouldUnsafeOverwriteAdditiveRequest(
                    $prompt,
                    $is_additive_prompt,
                    $existing_field_signatures,
                    $generated_field_signatures,
                    $existing_field_count,
                    $generated_field_count
                )) {
                    $this->logAdditiveState(
                        'destructive_additive_blocked',
                        $form_id,
                        $prompt,
                        [
                            'existing_field_count' => $existing_field_count,
                            'generated_field_count' => $generated_field_count,
                            'existing_signatures' => count($existing_field_signatures),
                            'generated_signatures' => count($generated_field_signatures),
                        ],
                        'warning'
                    );
                    return $this->formatFailure(
                        __('I stopped this change because the generated draft would remove existing fields while your request looked additive. Try asking Griffin Assist to preserve the current form and append the new field or section only.', 'griffinforms-form-builder'),
                        'destructive_additive_blocked',
                        409
                    );
                }

                $applier = new ShadowDraftStructureApplier();
                $apply_result = $applier->apply($form_id, $decoded, $prompt);

                if (!empty($apply_result['success'])) {
                    $success_message = $this->extractSuccessMessage($decoded, $prompt, $merged_additive);
                    return [
                        'success' => true,
                        'status' => 'applied',
                        'message' => $success_message,
                        'mutated_ids' => $apply_result['mutated_ids'] ?? [],
                        'lineage' => $apply_result['lineage'] ?? [],
                        'is_new' => true,
                        'prompt_key' => $this->getPromptKey(),
                    ];
                }

                return $this->formatFailure(
                    (string) ($apply_result['message'] ?? __('I generated a structure, but Griffin Assist could not safely apply it to the draft.', 'griffinforms-form-builder')),
                    'apply_failed'
                );
            }

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

        return $this->formatFailure(
            __('I could not convert that request into a safe GriffinForms structure. Try a simpler request like "Create a contact form with name, email, message, and file upload".', 'griffinforms-form-builder'),
            'validation_failed'
        );
    }

    protected function buildSystemPrompt(): string
    {
        return implode("\n", [
            'You are Griffin Assist, generating GriffinForms builder structure.',
            'Return JSON only. Do not include markdown, commentary, or code fences.',
            'Allowed root schema:',
            '{',
            '  "form": {"name": "string", "heading": "string", "description": "string"},',
            '  "success_message": "string",',
            '  "pages": [',
            '    {',
            '      "name": "string",',
            '      "heading": "string",',
            '      "description": "string",',
            '      "rows": [',
            '        {',
            '          "name": "string",',
            '          "columns": [',
            '            {',
            '              "name": "string",',
            '              "width": 1-12,',
            '              "fields": [',
            '                {',
            '                  "name": "string",',
            '                  "field_type": "inputtext|inputemail|inputphone|inputaddress|textarea|selectfield|inputcheckbox|inputradio|inputnumber|inputdate|fileupload|signature|inputrating|inputrichtext|inputlikert",',
            '                  "label": "string",',
            '                  "description": "string",',
            '                  "placeholder": "string",',
            '                  "required": true|false,',
            '                  "options": [{"label": "string", "value": "string"}] OR [{"column": 1, "options": [{"label": "string", "value": "string"}]}]',
            '                }',
            '              ]',
            '            }',
            '          ]',
            '        }',
            '      ]',
            '    }',
            '  ]',
            '}',
            'Rules:',
            '- Include at least 1 page, 1 row, 1 column, and 1 field.',
            '- Layout Rule: place each field in its own row with a single width-12 column unless the user explicitly asks for a multi-column or side-by-side layout. Never stack multiple fields inside one column.',
            '- Use only allowed field types.',
            '- Use "options" only for selectfield, inputcheckbox, and inputradio.',
            '- For selectfield, "options" should be a flat list of {"label","value"} pairs.',
            '- Builder "columns" are layout columns inside rows. They are not the same thing as checkbox/radio option columns.',
            '- For inputcheckbox and inputradio, "options" should preserve visual option columns: each top-level item is one rendered option column containing its own list of {"label","value"} pairs.',
            '- Use "option_column" only inside checkbox/radio option groups. Do not confuse option columns with builder layout columns.',
            '- When editing checkbox or radio option layout, keep all existing option labels/values unless the user explicitly asks to remove or rewrite them.',
            '- For inputlikert, you must generate likert_rows and likert_columns arrays explicitly from the user prompt. Write explicit row labels in likert_rows; do not rely on generic Row 1/2/3 labels. Use stable id/label pairs, plus likert_allow_na, required_mode, and likert_column_width when present.',
            '- For inputlikert, the stored submission value is a JSON map of row id to column id or "na". Do not flatten it into checkbox/radio options.',
            '- When editing Likert fields, preserve existing row and column ids unless the user explicitly requests a structural rewrite.',
            '- Likert Row Extraction Rule: identify the subject matter for rows from the user prompt. Look for 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; "customer service" → Responsiveness, Communication, Helpfulness, Resolution Time, Professionalism; "personal digital devices" → Smartphone, Laptop, Tablet, Smartwatch, Desktop Computer. If the prompt gives no subject at all, use 3-5 generic but plausible rows appropriate for a survey.',
            '- Likert Column Rule: if the prompt mentions emotions, feelings, or satisfaction level, use appropriate emotion/sentiment labels (e.g. Very Satisfied, Satisfied, Neutral, Dissatisfied, Very Dissatisfied). Default columns when not specified: Strongly Disagree, Disagree, Neutral, Agree, Strongly Agree.',
            '- Do not invent GriffinForms settings outside this schema.',
            '- When editing an existing form, preserve the existing structure unless the user explicitly asks to replace it.',
            '- For additive requests, prefer placing new fields into the best existing page and row.',
            '- Do not create a new page unless the user explicitly asks for one or the requested structure cannot reasonably fit into the current pages.',
            '- For newly added pages, follow the existing page heading/description convention unless the user explicitly asks for a different presentation.',
            '- Preserve GriffinForms internal names for form pages, rows, columns, and fields whenever you are regenerating existing elements.',
            '- Placeholder Rule: when a field placeholder is requested, the returned placeholder must be a non-empty string. Do not return an empty placeholder value.',
            '- Include a short "success_message" describing what Griffin Assist changed for the user.',
            '- The success_message must be plain text, under 140 characters, and must not claim anything that is not reflected in the returned schema.',
        ]);
    }

    protected function buildUserPrompt(string $prompt, array $existing_structure = [], bool $is_additive_prompt = false, array $selected_context = []): string
    {
        $parts = [
            'Generate a safe GriffinForms structure for this request:',
            $prompt,
        ];

        if (!empty($selected_context['item_type'])) {
            $parts[] = '';
            $parts[] = 'Selected builder element context:';
            $parts[] = wp_json_encode($selected_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            $parts[] = 'Selected element structure context:';
            $parts[] = wp_json_encode($this->buildSelectedElementPromptContext($selected_context, $existing_structure), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            $parts[] = 'If the request is ambiguous, interpret it relative to this selected element.';

            $selected_type = $this->resolveSelectedSemanticType($selected_context);
            $selected_snapshot = $this->getSelectedElementSnapshot($selected_context, $existing_structure);
            if ($selected_type === 'field' && in_array(sanitize_key((string) ($selected_snapshot['field_type'] ?? '')), ['inputcheckbox', 'inputradio'], true)) {
                $parts[] = 'Important: when the user talks about columns for this selected field, treat that as option columns inside the choice field unless they explicitly mention builder layout columns.';
            }
            if ($selected_type === 'field' && sanitize_key((string) ($selected_snapshot['field_type'] ?? '')) === 'inputlikert') {
                $parts[] = 'Important: this selected field is a Likert matrix. Treat rows and columns as the Likert schema, not as builder layout columns, and preserve existing row and column ids unless the prompt explicitly asks to rewrite the matrix.';
            }
        }

        if ($is_additive_prompt && !empty($existing_structure['fields'])) {
            $page_convention = $this->getPagePresentationConvention($existing_structure);
            $parts[] = '';
            $parts[] = 'This request is additive. Preserve the existing draft and return the full updated schema.';
            $parts[] = 'Do not remove or rename existing fields unless the user explicitly asked to replace them.';
            $parts[] = 'Preserve GriffinForms internal names for existing pages, rows, columns, and fields unless the user explicitly asks to rename them.';
            $parts[] = 'If the user did not explicitly define placement, choose the best existing page and row based on the current structure.';
            $parts[] = 'Do not create a new page just to hold the new field unless the user asked for a new page/step or there is no reasonable existing placement.';
            $parts[] = sprintf(
                'Existing page presentation convention: heading=%s, description=%s.',
                !empty($page_convention['use_heading']) ? 'yes' : 'no',
                !empty($page_convention['use_description']) ? 'yes' : 'no'
            );
            $parts[] = 'Current draft snapshot:';
            $parts[] = wp_json_encode($existing_structure, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        }

        return implode("\n", $parts);
    }

    protected function isAdditivePrompt(string $prompt): bool
    {
        $normalized = strtolower(trim($prompt));
        if ($normalized === '') {
            return false;
        }

        $replace_markers = [
            'from scratch',
            'replace the form',
            'rebuild the form',
            'regenerate the form',
            'start over',
            'reset the form',
            'create a new form',
        ];

        foreach ($replace_markers as $marker) {
            if (str_contains($normalized, $marker)) {
                return false;
            }
        }

        return (bool) preg_match('/\b(add|append|insert|include|also add|another|new field|new section|contact section|extra field|additional)\b/', $normalized);
    }

    protected function isAmbiguousReorderIntent(string $prompt, array $selected_context, array $existing_structure): bool
    {
        $normalized = strtolower(trim($prompt));
        if (!preg_match('/\b(reorder|re-arrange|rearrange|sort|swap|move)\b/', $normalized)) {
            return false;
        }

        $selected_type = $this->resolveSelectedSemanticType($selected_context);
        if (!in_array($selected_type, ['formpage', 'pagerow', 'rowcolumn'], true)) {
            return false;
        }

        if ($this->hasExplicitReorderDirective($normalized)) {
            return false;
        }

        $field_labels = $this->collectSelectedScopeFieldLabels($selected_context, $existing_structure);
        $matched_labels = 0;
        foreach ($field_labels as $label) {
            if ($label !== '' && str_contains($normalized, strtolower($label))) {
                $matched_labels++;
            }
        }

        return $matched_labels < 2;
    }

    protected function hasExplicitReorderDirective(string $normalized_prompt): bool
    {
        return (bool) preg_match(
            '/\b(before|after|first|last|top|bottom|start|end|beginning|alphabetical|alphabetically|a-z|z-a|ascending|descending)\b/',
            $normalized_prompt
        );
    }

    protected function collectSelectedScopeFieldLabels(array $selected_context, array $existing_structure): array
    {
        $selected_type = $this->resolveSelectedSemanticType($selected_context);
        $snapshot = $this->getSelectedElementSnapshot($selected_context, $existing_structure);
        $labels = [];

        if ($selected_type === 'formpage') {
            $labels = wp_list_pluck($this->summarizePageChildrenForPrompt((array) $snapshot), 'label');
        } elseif ($selected_type === 'pagerow') {
            foreach ($this->summarizeRowChildrenForPrompt((array) $snapshot) as $column) {
                foreach ((array) ($column['fields'] ?? []) as $field) {
                    $labels[] = (string) ($field['label'] ?? '');
                }
            }
        } elseif ($selected_type === 'rowcolumn') {
            $labels = wp_list_pluck($this->summarizeColumnChildrenForPrompt((array) $snapshot), 'label');
        }

        $labels = array_map(
            static fn($label) => sanitize_text_field((string) $label),
            array_values(array_unique(array_filter($labels)))
        );

        return $labels;
    }

    protected function buildReflectionPrompt(string $prompt, string $raw_content, array $errors, array $existing_structure = [], bool $is_additive_prompt = false, array $selected_context = []): string
    {
        $parts = [
            'The previous response failed validation.',
            'Original request:',
            $prompt,
            '',
            'Validation errors:',
            '- ' . implode("\n- ", array_map('sanitize_text_field', $errors)),
            '',
            'Previous response:',
            $raw_content,
            '',
            'Return corrected JSON only and obey the schema exactly.',
        ];

        $has_likert_error = false;
        foreach ($errors as $error) {
            if (stripos((string) $error, 'likert') !== false) {
                $has_likert_error = true;
                break;
            }
        }

        if ($has_likert_error) {
            $parts[] = '';
            $parts[] = 'Likert schema reminder:';
            $parts[] = 'inputlikert requires likert_rows (array of {id, label} objects) and likert_columns (array of {id, label} objects, minimum 2).';
            $parts[] = 'Row labels must be specific to the prompt topic — not generic "Row 1", "Row 2" placeholders.';
            $parts[] = 'Identify the subject matter from the original request (phrases like "for [subject]", "with [subject] as rows", "about [subject]") and derive 4-6 specific realistic row labels from that subject.';
            $parts[] = 'Example for "home cleaning services": Vacuuming, Mopping, Dusting, Window Cleaning, Bathroom Cleaning.';
            $parts[] = 'Example for "car renting services": Booking Process, Vehicle Condition, Pickup Experience, Fuel Policy, Customer Support.';
            $parts[] = 'Example for "personal digital devices": Smartphone, Laptop, Tablet, Smartwatch, Desktop Computer.';
            $parts[] = 'Likert example structure: {"field_type":"inputlikert","label":"Customer Satisfaction","likert_rows":[{"id":"row_1","label":"Responsiveness"},{"id":"row_2","label":"Helpfulness"}],"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":false,"required_mode":"all","likert_column_width":"auto"}';
        }

        if (!empty($selected_context['item_type'])) {
            $parts[] = 'Selected builder element context:';
            $parts[] = wp_json_encode($selected_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            $parts[] = 'Selected element structure context:';
            $parts[] = wp_json_encode($this->buildSelectedElementPromptContext($selected_context, $existing_structure), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            $parts[] = 'Interpret the request relative to this selected element.';
        }

        if ($is_additive_prompt && !empty($existing_structure['fields'])) {
            $parts[] = 'This is still an additive request. Preserve the existing draft and return the full updated schema.';
            $parts[] = 'If placement was not specified, choose the best existing page and row instead of creating a new page.';
            $parts[] = 'Current draft snapshot:';
            $parts[] = wp_json_encode($existing_structure, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        }

        return implode("\n", $parts);
    }

    protected function validateSchema(?array $decoded): array
    {
        $errors = [];
        if (!is_array($decoded)) {
            $errors[] = 'Decoded payload is not an array.';
            return $errors;
        }

        $form = $decoded['form'] ?? null;
        $pages = $decoded['pages'] ?? null;

        if (!is_array($form)) {
            $errors[] = 'Missing form object.';
        }

        if (!is_array($pages) || empty($pages)) {
            $errors[] = 'At least one page is required.';
            return $errors;
        }

        foreach ($pages as $page_index => $page) {
            if (!is_array($page)) {
                $errors[] = sprintf('Page %d is invalid.', $page_index + 1);
                continue;
            }

            $rows = $page['rows'] ?? null;
            if (!is_array($rows) || empty($rows)) {
                $errors[] = sprintf('Page %d must contain at least one row.', $page_index + 1);
                continue;
            }

            foreach ($rows as $row_index => $row) {
                $columns = $row['columns'] ?? null;
                if (!is_array($columns) || empty($columns)) {
                    $errors[] = sprintf('Page %d row %d must contain at least one column.', $page_index + 1, $row_index + 1);
                    continue;
                }

                foreach ($columns as $column_index => $column) {
                    $width = absint($column['width'] ?? 0);
                    $fields = $column['fields'] ?? null;

                    if ($width < 1 || $width > 12) {
                        $errors[] = sprintf('Page %d row %d column %d has invalid width.', $page_index + 1, $row_index + 1, $column_index + 1);
                    }

                    if (!is_array($fields) || empty($fields)) {
                        $errors[] = sprintf('Page %d row %d column %d must contain at least one field.', $page_index + 1, $row_index + 1, $column_index + 1);
                        continue;
                    }

                    foreach ($fields as $field_index => $field) {
                        $field_type = sanitize_key((string) ($field['field_type'] ?? ''));
                        if (!in_array($field_type, self::SUPPORTED_FIELD_TYPES, true)) {
                            $errors[] = sprintf(
                                'Page %d row %d column %d field %d uses unsupported field type "%s".',
                                $page_index + 1,
                                $row_index + 1,
                                $column_index + 1,
                                $field_index + 1,
                                $field_type
                            );
                        }

                        $label = $this->normalizeText((string) ($field['label'] ?? ''), 120);
                        if ($label === '') {
                            $errors[] = sprintf('Page %d row %d column %d field %d is missing a label.', $page_index + 1, $row_index + 1, $column_index + 1, $field_index + 1);
                        }

                        $options = $field['options'] ?? [];
                        $needs_options = in_array($field_type, ['selectfield', 'inputcheckbox', 'inputradio'], true);
                        if ($needs_options) {
                            if (!is_array($options) || $this->countChoiceOptions($field_type, $options) < 2) {
                                $errors[] = sprintf('Page %d row %d column %d field %d requires at least two options.', $page_index + 1, $row_index + 1, $column_index + 1, $field_index + 1);
                            }
                        }

                        if ($field_type === 'inputlikert') {
                            $likert_rows = array_values((array) ($field['likert_rows'] ?? []));
                            $likert_columns = array_values((array) ($field['likert_columns'] ?? []));
                            if (empty($likert_rows)) {
                                $errors[] = sprintf('Page %d row %d column %d field %d requires explicit likert_rows.', $page_index + 1, $row_index + 1, $column_index + 1, $field_index + 1);
                            }
                            if (count($likert_columns) < 2) {
                                $errors[] = sprintf('Page %d row %d column %d field %d requires at least two likert_columns.', $page_index + 1, $row_index + 1, $column_index + 1, $field_index + 1);
                            }

                            foreach ($likert_rows as $likert_row_index => $likert_row) {
                                $likert_label = $this->normalizeText((string) (is_array($likert_row) ? ($likert_row['label'] ?? '') : ''), 120);
                                if ($likert_label === '' || preg_match('/^row\s*\d+$/i', $likert_label)) {
                                    $errors[] = sprintf(
                                        'Page %d row %d column %d field %d likert row %d must use an explicit prompt-derived label.',
                                        $page_index + 1,
                                        $row_index + 1,
                                        $column_index + 1,
                                        $field_index + 1,
                                        $likert_row_index + 1
                                    );
                                }
                            }
                        }
                    }
                }
            }
        }

        return $errors;
    }

    protected function countFieldsInSchema(array $decoded): int
    {
        $count = 0;

        foreach ((array) ($decoded['pages'] ?? []) as $page) {
            foreach ((array) ($page['rows'] ?? []) as $row) {
                foreach ((array) ($row['columns'] ?? []) as $column) {
                    $count += count((array) ($column['fields'] ?? []));
                }
            }
        }

        return $count;
    }

    protected function getExistingFieldCount(int $form_id): int
    {
        $app_sql = App::getInstance();
        return count((array) $app_sql->fieldsInForm($form_id));
    }

    protected function prepareSchemaForApply(
        string $prompt,
        int $form_id,
        bool $is_additive_prompt,
        array $existing_structure,
        array $existing_field_signatures,
        array $decoded
    ): array {
        if (!$is_additive_prompt || empty($existing_structure['pages']) || empty($existing_field_signatures)) {
            return [$decoded, false];
        }

        $existing_field_count = $this->countFieldsInExistingStructure($existing_structure);
        $generated_field_count = $this->countFieldsInSchema($decoded);
        $generated_field_signatures = $this->collectSchemaFieldSignatures($decoded);
        $missing_existing = array_diff($existing_field_signatures, $generated_field_signatures);
        if (
            empty($missing_existing)
            && !(
                $this->isFieldGrowthPrompt($prompt)
                && (
                    $generated_field_count <= $existing_field_count
                    || count($generated_field_signatures) <= count($existing_field_signatures)
                )
            )
        ) {
            $this->logAdditiveState(
                'preserved_by_model',
                $form_id,
                $prompt,
                [
                    'generated_field_count' => $generated_field_count,
                    'missing_existing' => 0,
                ]
            );
            return [$decoded, false];
        }

        $this->logAdditiveState(
                'merge_recovery_attempt',
                $form_id,
                $prompt,
                [
                    'missing_existing' => count($missing_existing),
                    'generated_field_count' => $generated_field_count,
                ],
                'warning'
            );

        $merged_schema = $this->mergeAdditiveSchema($prompt, $existing_structure, $decoded, $existing_field_signatures);
        if (!is_array($merged_schema)) {
            $this->logAdditiveState(
                'merge_recovery_failed_no_fallback',
                $form_id,
                $prompt,
                [],
                'warning'
            );
            return [$decoded, false];
        }

        $merged_validation_errors = $this->validateSchema($merged_schema);
        if (!empty($merged_validation_errors)) {
            $this->logAdditiveState(
                'merged_schema_validation_failed',
                $form_id,
                $prompt,
                [
                    'validation_errors' => count($merged_validation_errors),
                ],
                'warning'
            );
            return [$decoded, false];
        }

        $this->logAdditiveState(
            'merged_schema_ready',
            $form_id,
            $prompt,
            [
                'merged_field_count' => $this->countFieldsInSchema($merged_schema),
            ]
        );

        return [$merged_schema, true];
    }

    protected function countFieldsInExistingStructure(array $existing_structure): int
    {
        $count = 0;

        foreach ((array) ($existing_structure['pages'] ?? []) as $page) {
            foreach ((array) ($page['rows'] ?? []) as $row) {
                foreach ((array) ($row['columns'] ?? []) as $column) {
                    $count += count((array) ($column['fields'] ?? []));
                }
            }
        }

        return $count;
    }

    protected function wouldUnsafeOverwriteAdditiveRequest(
        string $prompt,
        bool $is_additive_prompt,
        array $existing_field_signatures,
        array $generated_field_signatures,
        int $existing_field_count,
        int $generated_field_count
    ): bool
    {
        if (!$is_additive_prompt || $existing_field_count <= 0) {
            return false;
        }

        $missing_existing = array_diff($existing_field_signatures, $generated_field_signatures);
        if (!empty($missing_existing)) {
            return true;
        }

        if ($this->isFieldGrowthPrompt($prompt)) {
            if ($generated_field_count < $existing_field_count) {
                return true;
            }

            if (count($generated_field_signatures) <= count($existing_field_signatures)) {
                return true;
            }
        }

        if ($generated_field_count <= 0) {
            return true;
        }

        return false;
    }

    protected function mergeAdditiveSchema(string $prompt, array $existing_structure, array $generated_schema, array $existing_field_signatures): ?array
    {
        $novel_fields = $this->extractNovelFields($generated_schema, $existing_field_signatures);
        if (empty($novel_fields)) {
            return null;
        }

        $merged = $existing_structure;
        $target_page_index = $this->resolveTargetPageIndex($prompt, $existing_structure, $generated_schema);

        if (!isset($merged['pages'][$target_page_index])) {
            $target_page_index = 0;
        }

        if (empty($merged['pages'][$target_page_index]['rows']) || !is_array($merged['pages'][$target_page_index]['rows'])) {
            $merged['pages'][$target_page_index]['rows'] = [
                [
                    'row_label' => 'Row 1',
                    'columns' => [
                        [
                            'column_label' => 'Column 1',
                            'width' => 12,
                            'fields' => [],
                        ],
                    ],
                ],
            ];
        }

        $target_row_index = count($merged['pages'][$target_page_index]['rows']) - 1;
        if ($target_row_index < 0) {
            $target_row_index = 0;
        }

        if (empty($merged['pages'][$target_page_index]['rows'][$target_row_index]['columns']) || !is_array($merged['pages'][$target_page_index]['rows'][$target_row_index]['columns'])) {
            $merged['pages'][$target_page_index]['rows'][$target_row_index]['columns'] = [
                [
                    'column_label' => 'Column 1',
                    'width' => 12,
                    'fields' => [],
                ],
            ];
        }

        $target_column_index = count($merged['pages'][$target_page_index]['rows'][$target_row_index]['columns']) - 1;
        if ($target_column_index < 0) {
            $target_column_index = 0;
        }

        if (!isset($merged['pages'][$target_page_index]['rows'][$target_row_index]['columns'][$target_column_index]['fields']) || !is_array($merged['pages'][$target_page_index]['rows'][$target_row_index]['columns'][$target_column_index]['fields'])) {
            $merged['pages'][$target_page_index]['rows'][$target_row_index]['columns'][$target_column_index]['fields'] = [];
        }

        foreach ($novel_fields as $field) {
            $merged['pages'][$target_page_index]['rows'][$target_row_index]['columns'][$target_column_index]['fields'][] = $field;
        }

        return $merged;
    }

    protected function extractNovelFields(array $generated_schema, array $existing_field_signatures): array
    {
        $novel_fields = [];

        foreach ((array) ($generated_schema['pages'] ?? []) as $page) {
            foreach ((array) ($page['rows'] ?? []) as $row) {
                foreach ((array) ($row['columns'] ?? []) as $column) {
                    foreach ((array) ($column['fields'] ?? []) as $field) {
                        $signature = $this->fieldSignature((string) ($field['label'] ?? ''), (string) ($field['field_type'] ?? ''));
                        if ($signature === '' || in_array($signature, $existing_field_signatures, true)) {
                            continue;
                        }

                        $novel_fields[$signature] = $field;
                    }
                }
            }
        }

        return array_values($novel_fields);
    }

    protected function resolveTargetPageIndex(string $prompt, array $existing_structure, array $generated_schema): int
    {
        $normalized_prompt = strtolower($prompt);
        $existing_pages = array_values((array) ($existing_structure['pages'] ?? []));
        if (count($existing_pages) <= 1) {
            return 0;
        }

        foreach ($existing_pages as $index => $page) {
            $page_name = strtolower(trim((string) ($page['name'] ?? '')));
            $page_label = strtolower(trim((string) ($page['page_label'] ?? '')));
            if (($page_name !== '' && str_contains($normalized_prompt, $page_name)) || ($page_label !== '' && str_contains($normalized_prompt, $page_label))) {
                return $index;
            }
        }

        $generated_page_names = [];
        foreach ((array) ($generated_schema['pages'] ?? []) as $page) {
            $name = strtolower(trim((string) ($page['name'] ?? '')));
            if ($name !== '') {
                $generated_page_names[] = $name;
            }
        }

        foreach ($existing_pages as $index => $page) {
            $page_name = strtolower(trim((string) ($page['name'] ?? '')));
            if ($page_name !== '' && in_array($page_name, $generated_page_names, true)) {
                return $index;
            }
        }

        return 0;
    }

    protected function buildDirectAdditiveFallbackSchema(string $prompt, array $existing_structure): ?array
    {
        $field = $this->inferSingleFieldFromPrompt($prompt);
        if ($field === null || empty($existing_structure['pages'])) {
            return null;
        }

        $merged = $existing_structure;
        $target_page_index = $this->resolveTargetPageIndex($prompt, $existing_structure, []);
        if (!isset($merged['pages'][$target_page_index])) {
            $target_page_index = 0;
        }

        if (empty($merged['pages'][$target_page_index]['rows']) || !is_array($merged['pages'][$target_page_index]['rows'])) {
            $merged['pages'][$target_page_index]['rows'] = [
                [
                    'row_label' => 'Row 1',
                    'columns' => [
                        [
                            'column_label' => 'Column 1',
                            'width' => 12,
                            'fields' => [],
                        ],
                    ],
                ],
            ];
        }

        $target_row_index = count($merged['pages'][$target_page_index]['rows']) - 1;
        $target_column_index = count((array) ($merged['pages'][$target_page_index]['rows'][$target_row_index]['columns'] ?? [])) - 1;

        if ($target_column_index < 0) {
            $merged['pages'][$target_page_index]['rows'][$target_row_index]['columns'] = [
                [
                    'column_label' => 'Column 1',
                    'width' => 12,
                    'fields' => [],
                ],
            ];
            $target_column_index = 0;
        }

        $merged['pages'][$target_page_index]['rows'][$target_row_index]['columns'][$target_column_index]['fields'][] = $field;

        return $merged;
    }

    protected function buildLikertRowsFromPrompt(string $prompt, string $label): array
    {
        $normalized = strtolower(trim($prompt));
        $subject = $this->extractLikertSubjectFromPrompt($normalized);
        if ($subject === '') {
            $subject = trim($label);
        }

        if (preg_match('/\bhome\s+devices?\b/', $normalized)) {
            $rows = ['Television', 'Smartphone', 'Tablet', 'Laptop', 'Smart Speaker'];
        } elseif (preg_match('/\bmedia\s+types?\b/', $normalized)) {
            $rows = ['Video', 'Audio', 'Images', 'Articles'];
        } elseif (preg_match('/\bcontent\s+types?\b/', $normalized)) {
            $rows = ['Blog Posts', 'Videos', 'Podcasts', 'Infographics'];
        } elseif (preg_match('/\bcustomer\s+satisfaction\b/', $normalized)) {
            $rows = ['Support', 'Response Time', 'Resolution', 'Communication'];
        } elseif (preg_match('/\bplumbing\s+services?\b/', $normalized)) {
            $rows = ['Leak Repair', 'Drain Cleaning', 'Pipe Replacement', 'Water Heater Repair', 'Fixture Installation'];
        } elseif (preg_match('/\bsatisfaction|experience|service|quality|feedback|review\b/', $normalized)) {
            $rows = ['Ease of Use', 'Helpfulness', 'Speed', 'Overall Experience'];
        } elseif ($subject !== '') {
            $rows = [
                $subject . ' - Ease',
                $subject . ' - Quality',
                $subject . ' - Speed',
                $subject . ' - Overall',
            ];
        } else {
            $rows = [];
        }

        $payload = [];
        foreach (array_values(array_unique(array_filter(array_map('trim', $rows)))) as $index => $row_label) {
            $payload[] = [
                'id' => 'row_' . ($index + 1),
                'label' => $row_label,
            ];
        }

        return $payload;
    }

    protected function buildLikertColumnsFromPrompt(string $prompt): array
    {
        return [
            ['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'],
        ];
    }

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

        $field_type = null;
        $label = '';
        $placeholder = '';

        $map = [
            'inputemail' => ['keywords' => ['email'], 'label' => __('Email', 'griffinforms-form-builder'), 'placeholder' => __('you@example.com', 'griffinforms-form-builder')],
            'inputphone' => ['keywords' => ['phone', 'mobile', 'telephone'], 'label' => __('Phone', 'griffinforms-form-builder'), 'placeholder' => __('+1 (555) 987-6543', 'griffinforms-form-builder')],
            'inputaddress' => ['keywords' => ['address', 'street address', 'mailing address', 'shipping address', 'billing address'], 'label' => __('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' => ''],
            'inputrating' => ['keywords' => ['rating', 'star rating', 'score', 'satisfaction', 'feedback', 'review', 'rate'], 'label' => __('Rating', 'griffinforms-form-builder'), 'placeholder' => ''],
            'inputtext' => ['keywords' => ['name', 'text', 'company', 'city', 'state'], 'label' => __('Text Field', 'griffinforms-form-builder'), 'placeholder' => ''],
        ];

        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'];
                    break 2;
                }
            }
        }

        if ($field_type === null) {
            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');
            }
        }

        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');
            }
        }

        return [
            'field_type' => $field_type,
            'label' => $label,
            'description' => '',
            'placeholder' => $placeholder,
            'required' => false,
            'options' => in_array($field_type, ['selectfield', 'inputcheckbox', 'inputradio'], true) ? [] : [],
        ];
    }

    protected function logAdditiveState(string $stage, int $form_id, string $prompt, array $context = [], string $level = 'info'): void
    {
        $context_pairs = [];
        foreach ($context as $key => $value) {
            $context_pairs[] = sanitize_key((string) $key) . '=' . sanitize_text_field((string) $value);
        }

        $message = sprintf(
            'Additive pipeline [%s] prompt="%s"%s',
            sanitize_text_field($stage),
            sanitize_text_field(function_exists('mb_substr') ? mb_substr($prompt, 0, 120) : substr($prompt, 0, 120)),
            !empty($context_pairs) ? ' ' . implode(' ', $context_pairs) : ''
        );

        $this->log->add(
            $level,
            'ai_prompt',
            $form_id,
            (int) get_current_user_id(),
            $message,
            'ai_additive'
        );
    }

    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 extractLikertSubjectFromPrompt(string $normalized_prompt): string
    {
        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;
                }
            }
        }

        return '';
    }

    protected function getExistingStructureSnapshot(int $form_id): array
    {
        $format_sql = Format::getInstance();
        $form = $format_sql->itemValues($form_id, 'form', ['name', 'heading', 'description', 'submit_label', 'formpages']);
        if (!is_object($form)) {
            return [];
        }

        $snapshot = [
            'form' => [
                'name' => sanitize_text_field((string) ($form->name ?? '')),
                'heading' => sanitize_text_field((string) ($form->heading ?? '')),
                'description' => sanitize_textarea_field((string) ($form->description ?? '')),
                'submit_label' => sanitize_text_field((string) ($form->submit_label ?? '')),
            ],
            'pages' => [],
            'fields' => [],
        ];

        $page_ids = $this->decodeValue($form->formpages ?? []);
        foreach ((array) $page_ids as $page_id) {
            $page = $format_sql->itemValues(absint($page_id), 'formpage', ['name', 'heading', 'description', 'previous_label', 'next_label', 'pagerows']);
            if (!is_object($page)) {
                continue;
            }

            $page_snapshot = [
                'item_id' => absint($page_id),
                '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 ?? '')),
                'next_label' => sanitize_text_field((string) ($page->next_label ?? '')),
                'page_label' => sprintf('Page %d', count($snapshot['pages']) + 1),
                'rows' => [],
            ];

            $row_ids = $this->decodeValue($page->pagerows ?? []);
            foreach (array_values((array) $row_ids) as $row_index => $row_id) {
                $row = $format_sql->itemValues(absint($row_id), 'pagerow', ['name', 'rowcolumns']);
                if (!is_object($row)) {
                    continue;
                }

                $row_snapshot = [
                    'item_id' => absint($row_id),
                    'name' => sanitize_text_field((string) ($row->name ?? '')),
                    'row_label' => sprintf('Row %d', $row_index + 1),
                    'columns' => [],
                ];
                $column_ids = $this->decodeValue($row->rowcolumns ?? []);
                foreach (array_values((array) $column_ids) as $column_index => $column_id) {
                    $column = $format_sql->itemValues(absint($column_id), 'rowcolumn', ['name', 'width', 'fields']);
                    if (!is_object($column)) {
                        continue;
                    }

                    $column_snapshot = [
                        'item_id' => absint($column_id),
                        'name' => sanitize_text_field((string) ($column->name ?? '')),
                        'column_label' => sprintf('Column %d', $column_index + 1),
                        'width' => absint($column->width ?? 12),
                        'fields' => [],
                    ];

                    $field_ids = $this->decodeValue($column->fields ?? []);
                    foreach ((array) $field_ids as $field_id) {
                        $field = $format_sql->itemValues(absint($field_id), 'field', ['field_type', 'name', 'heading', 'description', 'placeholder', 'is_required', 'field_options']);
                        if (!is_object($field)) {
                            continue;
                        }

                        $field_type = sanitize_key((string) ($field->field_type ?? ''));
                        $decoded_options = $this->decodeValue($field->field_options ?? []);

                        $field_snapshot = [
                            'item_id' => absint($field_id),
                            'name' => sanitize_text_field((string) ($field->name ?? '')),
                            'field_type' => $field_type,
                            'label' => sanitize_text_field((string) ($field->heading ?? $field->name ?? '')),
                            'description' => sanitize_textarea_field((string) ($field->description ?? '')),
                            'placeholder' => sanitize_text_field((string) ($field->placeholder ?? '')),
                            'required' => !empty($field->is_required),
                            'options' => $this->normalizeExistingOptions(
                                $field_type,
                                $decoded_options
                            ),
                        ];

                        if ($field_type === 'inputlikert' && is_array($decoded_options)) {
                            $field_snapshot['likert_rows'] = array_values((array) ($decoded_options['likert_rows'] ?? []));
                            $field_snapshot['likert_columns'] = array_values((array) ($decoded_options['likert_columns'] ?? []));
                            $field_snapshot['likert_allow_na'] = !empty($decoded_options['likert_allow_na']);
                            $field_snapshot['required_mode'] = sanitize_key((string) ($decoded_options['required_mode'] ?? 'all'));
                            $field_snapshot['likert_column_width'] = sanitize_text_field((string) ($decoded_options['likert_column_width'] ?? ''));
                        }

                        $column_snapshot['fields'][] = $field_snapshot;
                        $snapshot['fields'][] = [
                            'field_type' => $field_snapshot['field_type'],
                            'label' => $field_snapshot['label'],
                        ];
                    }

                    $row_snapshot['columns'][] = $column_snapshot;
                }

                $page_snapshot['rows'][] = $row_snapshot;
            }

            $snapshot['pages'][] = $page_snapshot;
        }

        return $snapshot;
    }

    protected function normalizeExistingOptions(string $field_type, $options): array
    {
        if (!is_array($options) || empty($options)) {
            return [];
        }

        if (in_array($field_type, ['inputcheckbox', 'inputradio'], true)) {
            $normalized = [];
            foreach ($options as $group_index => $group) {
                if (!is_array($group)) {
                    continue;
                }

                $group_payload = [
                    'option_column' => is_numeric($group_index) ? absint($group_index) + 1 : count($normalized) + 1,
                    'options' => [],
                ];

                foreach ($group as $label => $value) {
                    if (is_array($value)) {
                        $nested_label = sanitize_text_field((string) ($value['label'] ?? ''));
                        $nested_value = sanitize_text_field((string) ($value['value'] ?? ''));
                    } else {
                        $nested_label = sanitize_text_field((string) $label);
                        $nested_value = sanitize_text_field((string) $value);
                    }

                    if ($nested_label === '' || $nested_value === '') {
                        continue;
                    }

                    $group_payload['options'][] = [
                        'label' => $nested_label,
                        'value' => $nested_value,
                    ];
                }

                if (!empty($group_payload['options'])) {
                    $normalized[] = $group_payload;
                }
            }

            return $normalized;
        }

        $normalized = [];
        foreach ($options as $group) {
            if (!is_array($group)) {
                continue;
            }

            foreach ($group as $label => $value) {
                if (is_array($value)) {
                    foreach ($value as $nested_label => $nested_value) {
                        $normalized[] = [
                            'label' => sanitize_text_field((string) $nested_label),
                            'value' => sanitize_text_field((string) $nested_value),
                        ];
                    }

                    continue;
                }

                $normalized[] = [
                    'label' => sanitize_text_field((string) $label),
                    'value' => sanitize_text_field((string) $value),
                ];
            }
        }

        return $normalized;
    }

    protected function countChoiceOptions(string $field_type, array $options): int
    {
        if (!in_array($field_type, ['selectfield', 'inputcheckbox', 'inputradio'], true)) {
            return 0;
        }

        if (in_array($field_type, ['inputcheckbox', 'inputradio'], true)) {
            $count = 0;
            foreach ($options as $group) {
                if (is_array($group) && isset($group['options']) && is_array($group['options'])) {
                    $count += count(array_filter($group['options'], static function ($option) {
                        return is_array($option)
                            && sanitize_text_field((string) ($option['label'] ?? '')) !== ''
                            && sanitize_text_field((string) ($option['value'] ?? '')) !== '';
                    }));
                    continue;
                }

                if (is_array($group)) {
                    foreach ($group as $label => $value) {
                        $resolved_label = is_array($value)
                            ? sanitize_text_field((string) ($value['label'] ?? ''))
                            : sanitize_text_field((string) $label);
                        $resolved_value = is_array($value)
                            ? sanitize_text_field((string) ($value['value'] ?? ''))
                            : sanitize_text_field((string) $value);

                        if ($resolved_label !== '' && $resolved_value !== '') {
                            $count++;
                        }
                    }
                }
            }

            return $count;
        }

        return count(array_filter($options, static function ($option) {
            return is_array($option)
                && sanitize_text_field((string) ($option['label'] ?? '')) !== ''
                && sanitize_text_field((string) ($option['value'] ?? '')) !== '';
        }));
    }

    protected function collectExistingFieldSignatures(array $existing_structure): array
    {
        $signatures = [];
        foreach ((array) ($existing_structure['fields'] ?? []) as $field) {
            $signature = $this->fieldSignature((string) ($field['label'] ?? ''), (string) ($field['field_type'] ?? ''));
            if ($signature !== '') {
                $signatures[] = $signature;
            }
        }

        return array_values(array_unique($signatures));
    }

    protected function collectSchemaFieldSignatures(array $decoded): array
    {
        $signatures = [];
        foreach ((array) ($decoded['pages'] ?? []) as $page) {
            foreach ((array) ($page['rows'] ?? []) as $row) {
                foreach ((array) ($row['columns'] ?? []) as $column) {
                    foreach ((array) ($column['fields'] ?? []) as $field) {
                        $signature = $this->fieldSignature((string) ($field['label'] ?? ''), (string) ($field['field_type'] ?? ''));
                        if ($signature !== '') {
                            $signatures[] = $signature;
                        }
                    }
                }
            }
        }

        return array_values(array_unique($signatures));
    }

    protected function fieldSignature(string $label, string $field_type): string
    {
        $normalized_label = strtolower(trim(preg_replace('/\s+/', ' ', wp_strip_all_tags($label))));
        $normalized_type = sanitize_key($field_type);

        if ($normalized_label !== '') {
            return $normalized_label;
        }

        return $normalized_type;
    }

    protected function getPagePresentationConvention(array $existing_structure): array
    {
        $pages = (array) ($existing_structure['pages'] ?? []);
        if (empty($pages)) {
            return [
                'use_heading' => false,
                'use_description' => false,
            ];
        }

        $heading_count = 0;
        $description_count = 0;
        foreach ($pages as $page) {
            if (trim((string) ($page['heading'] ?? '')) !== '') {
                $heading_count++;
            }

            if (trim((string) ($page['description'] ?? '')) !== '') {
                $description_count++;
            }
        }

        $total = count($pages);

        return [
            'use_heading' => $heading_count > 0 && $heading_count >= (int) ceil($total / 2),
            'use_description' => $description_count > 0 && $description_count >= (int) ceil($total / 2),
        ];
    }

    protected function normalizeInternalNames(array $existing_structure, array $decoded): array
    {
        foreach (array_values((array) ($decoded['pages'] ?? [])) as $page_index => $page) {
            $existing_page = (array) (($existing_structure['pages'][$page_index] ?? []));
            $decoded['pages'][$page_index]['name'] = $this->resolveInternalName(
                (string) ($page['name'] ?? ''),
                (string) ($existing_page['name'] ?? ''),
                (string) ($page['heading'] ?? ''),
                sprintf(__('Page %d', 'griffinforms-form-builder'), $page_index + 1)
            );

            foreach (array_values((array) ($page['rows'] ?? [])) as $row_index => $row) {
                $existing_row = (array) (($existing_page['rows'][$row_index] ?? []));
                $decoded['pages'][$page_index]['rows'][$row_index]['name'] = $this->resolveInternalName(
                    (string) ($row['name'] ?? ''),
                    (string) ($existing_row['name'] ?? ''),
                    '',
                    sprintf(__('Row %d', 'griffinforms-form-builder'), $row_index + 1)
                );

                foreach (array_values((array) ($row['columns'] ?? [])) as $column_index => $column) {
                    $existing_column = (array) (($existing_row['columns'][$column_index] ?? []));
                    $decoded['pages'][$page_index]['rows'][$row_index]['columns'][$column_index]['name'] = $this->resolveInternalName(
                        (string) ($column['name'] ?? ''),
                        (string) ($existing_column['name'] ?? ''),
                        '',
                        sprintf(__('Column %d', 'griffinforms-form-builder'), $column_index + 1)
                    );

                    foreach (array_values((array) ($column['fields'] ?? [])) as $field_index => $field) {
                        $existing_field = $this->findExistingFieldForSchemaField($existing_structure, $page_index, $row_index, $column_index, $field_index, (array) $field);
                        $decoded['pages'][$page_index]['rows'][$row_index]['columns'][$column_index]['fields'][$field_index]['name'] = $this->resolveInternalName(
                            (string) ($field['name'] ?? ''),
                            (string) ($existing_field['name'] ?? ''),
                            (string) ($field['label'] ?? ''),
                            (string) ($field['label'] ?? __('Field', 'griffinforms-form-builder'))
                        );
                    }
                }
            }
        }

        return $decoded;
    }

    protected function resolveInternalName(string $generated_name, string $existing_name, string $fallback_source = '', string $default = ''): string
    {
        $generated_name = sanitize_text_field($generated_name);
        if ($generated_name !== '') {
            return $generated_name;
        }

        $existing_name = sanitize_text_field($existing_name);
        if ($existing_name !== '') {
            return $existing_name;
        }

        $fallback_source = sanitize_text_field($fallback_source);
        if ($fallback_source !== '') {
            return $fallback_source;
        }

        return sanitize_text_field($default);
    }

    protected function findExistingFieldForSchemaField(array $existing_structure, int $page_index, int $row_index, int $column_index, int $field_index, array $field): array
    {
        $existing_by_position = (array) (($existing_structure['pages'][$page_index]['rows'][$row_index]['columns'][$column_index]['fields'][$field_index] ?? []));
        $candidate_signature = $this->fieldSignature((string) ($field['label'] ?? ''), (string) ($field['field_type'] ?? ''));

        if (!empty($existing_by_position)) {
            $existing_signature = $this->fieldSignature((string) ($existing_by_position['label'] ?? ''), (string) ($existing_by_position['field_type'] ?? ''));
            if ($candidate_signature === '' || $candidate_signature === $existing_signature) {
                return $existing_by_position;
            }
        }

        foreach ((array) ($existing_structure['pages'] ?? []) as $page) {
            foreach ((array) ($page['rows'] ?? []) as $row) {
                foreach ((array) ($row['columns'] ?? []) as $column) {
                    foreach ((array) ($column['fields'] ?? []) as $existing_field) {
                        $existing_signature = $this->fieldSignature((string) ($existing_field['label'] ?? ''), (string) ($existing_field['field_type'] ?? ''));
                        if ($candidate_signature !== '' && $candidate_signature === $existing_signature) {
                            return (array) $existing_field;
                        }
                    }
                }
            }
        }

        return [];
    }

    protected function applySelectedElementFallbacks(string $prompt, array $selected_context, array $existing_structure, array $decoded): array
    {
        $selected_type = (string) ($selected_context['item_type'] ?? '');
        if ($selected_type !== 'formpage') {
            return $decoded;
        }

        $target_page_index = $this->resolveSelectedPageIndex($selected_context, $existing_structure);
        if ($target_page_index < 0 || !isset($decoded['pages'][$target_page_index])) {
            return $decoded;
        }

        $page = (array) $decoded['pages'][$target_page_index];
        $name = sanitize_text_field((string) ($page['name'] ?? ''));
        $heading = sanitize_text_field((string) ($page['heading'] ?? ''));
        $description = sanitize_textarea_field((string) ($page['description'] ?? ''));

        if ($this->promptExplicitlyRequestsPageHeading($prompt) || $this->promptRequestsGenericHeading($prompt)) {
            if ($heading === '') {
                $heading = $name !== '' ? $name : sprintf(__('Page %d', 'griffinforms-form-builder'), $target_page_index + 1);
                $decoded['pages'][$target_page_index]['heading'] = $heading;
            }
        }

        if ($this->promptExplicitlyRequestsPageDescription($prompt) || $this->promptRequestsGenericDescription($prompt)) {
            if ($description === '') {
                $decoded['pages'][$target_page_index]['description'] = $this->generatePageDescription($page, $heading !== '' ? $heading : $name);
            }
        }

        return $decoded;
    }

    protected function resolveSelectedPageIndex(array $selected_context, array $existing_structure): int
    {
        $selected_id = absint($selected_context['item_id'] ?? 0);
        if ($selected_id > 0) {
            foreach (array_values((array) ($existing_structure['pages'] ?? [])) as $page_index => $page) {
                if (absint($page['item_id'] ?? 0) === $selected_id) {
                    return $page_index;
                }
            }
        }

        if (count((array) ($existing_structure['pages'] ?? [])) === 1) {
            return 0;
        }

        return -1;
    }

    protected function normalizeNewPagePresentation(string $prompt, array $existing_structure, array $decoded): array
    {
        $page_convention = $this->getPagePresentationConvention($existing_structure);
        $existing_page_signatures = $this->collectExistingPageSignatures($existing_structure);
        $explicit_heading_request = $this->promptExplicitlyRequestsPageHeading($prompt);
        $explicit_description_request = $this->promptExplicitlyRequestsPageDescription($prompt);

        foreach (array_values((array) ($decoded['pages'] ?? [])) as $page_index => $page) {
            $page_signature = $this->pageSignatureForArray((array) $page, $page_index);
            $is_new_page = !in_array($page_signature, $existing_page_signatures, true);
            if (!$is_new_page) {
                continue;
            }

            $name = sanitize_text_field((string) ($page['name'] ?? ''));
            $heading = sanitize_text_field((string) ($page['heading'] ?? ''));
            $description = sanitize_textarea_field((string) ($page['description'] ?? ''));

            $should_have_heading = !empty($page_convention['use_heading']) || $explicit_heading_request;
            $should_have_description = !empty($page_convention['use_description']) || $explicit_description_request;

            if ($should_have_heading) {
                if ($heading === '') {
                    $heading = $name !== '' ? $name : sprintf(__('Page %d', 'griffinforms-form-builder'), $page_index + 1);
                }
            } elseif (!$explicit_heading_request) {
                $heading = '';
            }

            if ($should_have_description) {
                if ($description === '') {
                    $description = $this->generatePageDescription((array) $page, $heading !== '' ? $heading : $name);
                }
            } elseif (!$explicit_description_request) {
                $description = '';
            }

            $decoded['pages'][$page_index]['heading'] = $heading;
            $decoded['pages'][$page_index]['description'] = $description;
        }

        return $decoded;
    }

    protected function collectExistingPageSignatures(array $existing_structure): array
    {
        $signatures = [];
        foreach (array_values((array) ($existing_structure['pages'] ?? [])) as $page_index => $page) {
            $signature = $this->pageSignatureForArray((array) $page, $page_index);
            if ($signature !== '') {
                $signatures[] = $signature;
            }
        }

        return array_values(array_unique($signatures));
    }

    protected function pageSignatureForArray(array $page, int $page_index): string
    {
        $name = strtolower(trim(preg_replace('/\s+/', ' ', wp_strip_all_tags((string) ($page['name'] ?? '')))));
        if ($name !== '') {
            return 'page:' . $name;
        }

        $heading = strtolower(trim(preg_replace('/\s+/', ' ', wp_strip_all_tags((string) ($page['heading'] ?? '')))));
        if ($heading !== '') {
            return 'page_heading:' . $heading;
        }

        return 'page_index:' . $page_index;
    }

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

        return (bool) preg_match('/\b(page|step|section)\b.*\b(heading|title|headline)\b|\b(heading|title|headline)\b.*\b(page|step|section)\b/', $normalized);
    }

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

        return (bool) preg_match('/\b(page|step|section)\b.*\b(description|intro|introduction|instructions|subtext|help text|guidance)\b|\b(description|intro|introduction|instructions|subtext|help text|guidance)\b.*\b(page|step|section)\b/', $normalized);
    }

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

        return (bool) preg_match('/^(add|set|create|generate)\s+(a\s+)?heading\b/', $normalized);
    }

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

        return (bool) preg_match('/^(add|set|create|generate)\s+(a\s+)?description\b/', $normalized);
    }

    protected function generatePageDescription(array $page, string $page_title = ''): string
    {
        $labels = [];
        foreach ((array) ($page['rows'] ?? []) as $row) {
            foreach ((array) ($row['columns'] ?? []) as $column) {
                foreach ((array) ($column['fields'] ?? []) as $field) {
                    $label = sanitize_text_field((string) ($field['label'] ?? ''));
                    if ($label !== '') {
                        $labels[] = strtolower($label);
                    }
                }
            }
        }

        $labels = array_values(array_unique($labels));
        if (count($labels) === 1) {
            return sprintf(__('Provide your %s in this section.', 'griffinforms-form-builder'), $labels[0]);
        }

        if (count($labels) >= 2 && count($labels) <= 3) {
            $last = array_pop($labels);
            return sprintf(__('Provide your %s and %s in this section.', 'griffinforms-form-builder'), implode(', ', $labels), $last);
        }

        if ($page_title !== '') {
            return sprintf(__('Complete the %s section.', 'griffinforms-form-builder'), strtolower($page_title));
        }

        return __('Complete this section.', 'griffinforms-form-builder');
    }

    protected function extractSuccessMessage(array $decoded, string $prompt, bool $merged_additive = false): string
    {
        $candidate = sanitize_text_field((string) ($decoded['success_message'] ?? ''));
        if ($candidate !== '') {
            if (function_exists('mb_substr')) {
                $candidate = mb_substr($candidate, 0, 140);
            } else {
                $candidate = substr($candidate, 0, 140);
            }

            return $candidate;
        }

        if ($merged_additive) {
            return __('Griffin Assist appended the new field to your draft and preserved the existing structure.', 'griffinforms-form-builder');
        }

        if ($this->promptExplicitlyRequestsPageHeading($prompt) || $this->promptRequestsGenericHeading($prompt)) {
            return __('Griffin Assist added the requested heading to your draft.', 'griffinforms-form-builder');
        }

        if ($this->promptExplicitlyRequestsPageDescription($prompt) || $this->promptRequestsGenericDescription($prompt)) {
            return __('Griffin Assist added the requested description to your draft.', 'griffinforms-form-builder');
        }

        return __('Structure generated and applied to your draft.', 'griffinforms-form-builder');
    }

    protected function isSelectedTextEditIntent(string $prompt, array $selected_context): bool
    {
        $selected_type = $this->resolveSelectedSemanticType($selected_context);
        if ($selected_type === '') {
            return false;
        }

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

        if (preg_match('/\b(description|label|placeholder|heading|title|headline|help text|instructions|subtext)\b/', $normalized)) {
            return true;
        }

        if (
            ($selected_type === 'form' && preg_match('/\bsubmit\b.*\b(label|button)\b|\b(label|button)\b.*\bsubmit\b/', $normalized))
            || ($selected_type === 'formpage' && preg_match('/\b(prev|previous|back|next|continue)\b.*\b(label|button)\b|\b(label|button)\b.*\b(prev|previous|back|next|continue)\b/', $normalized))
        ) {
            return true;
        }

        return (bool) preg_match('/\b(change|update|edit|rewrite|improve|make|set|add|generate|turn)\b.*\b(more|better|warmer|friendlier|clearer|clear|question|questions)\b/', $normalized);
    }

    protected function buildSelectedTextEditSystemPrompt(array $selected_context): string
    {
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $allowed_properties = $selected_type === 'form'
            ? 'name|heading|description|submit_label'
            : ($selected_type === 'formpage'
                ? 'heading|description|previous_label|next_label'
                : 'label|description|placeholder');

        return implode("\n", [
            'You are Griffin Assist, editing copy for one selected GriffinForms builder element.',
            'Return JSON only. Do not include markdown, commentary, or code fences.',
            'Allowed root schema:',
            '{',
            '  "updates": {"property": "string"},',
            '  "target_property": "' . $allowed_properties . '",',
            '  "value": "string",',
            '  "success_message": "string"',
            '}',
            'Rules:',
            '- Only change copy for the selected element.',
            '- Do not regenerate or describe the full form structure.',
            '- Use "updates" when the user asks to change more than one property on the selected element.',
            '- If only one property is being changed, you may use either "updates" or the single-property "target_property" + "value" shape.',
            '- Every update value must contain only the requested copy text.',
            '- The success_message must be plain text, under 140 characters, and must not claim anything outside the selected copy change.',
            '- For placeholder updates on fields, treat the prompt as a request to rewrite the placeholder text, not to ask for choices.',
            '- Use the selected field label, field description, field type, and surrounding context to generate one concrete, non-empty placeholder string.',
            '- Never return an empty placeholder value.',
            '- If the selected field already has a placeholder, rewrite it to something better rather than returning it unchanged.',
            '- Placeholder examples:',
            '  - inputtext with label "Customer Name" and description "Enter your name" -> "Enter your full name"',
            '  - inputemail with label "Email Address" and description "We will contact you here" -> "you@example.com"',
            '  - textarea with label "Message" and description "Share your feedback" -> "Type your message here..."',
            '- When asked to update a placeholder, never return required_inputs or choices. Return a direct placeholder string in the value field.',
        ]);
    }

    protected function buildSelectedTextEditUserPrompt(string $prompt, array $selected_context, array $existing_structure): string
    {
        $selected_snapshot = $this->buildSelectedElementPromptContext($selected_context, $existing_structure);
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $parts = [
            'Rewrite copy for the selected GriffinForms element only.',
            'User request:',
            $prompt,
            '',
            'Selected builder element context:',
            wp_json_encode($selected_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'Selected element snapshot:',
            wp_json_encode($selected_snapshot, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
        ];

        if (($selected_snapshot['type'] ?? '') === 'field') {
            $field = (array) ($selected_snapshot['field'] ?? []);
            $sibling_fields = (array) ($selected_snapshot['context']['sibling_fields'] ?? []);
            $sibling_labels = array_values(array_filter(array_map(static function ($item) {
                return sanitize_text_field((string) ($item['label'] ?? ''));
            }, $sibling_fields)));
            $label_lengths = array_values(array_filter(array_map(static function ($label) {
                return mb_strlen(trim((string) $label));
            }, $sibling_labels)));
            $average_label_length = !empty($label_lengths) ? (int) round(array_sum($label_lengths) / count($label_lengths)) : 0;
            $parts[] = 'Selected field context for placeholder rewriting:';
            $parts[] = wp_json_encode([
                'field_type' => (string) ($field['field_type'] ?? ''),
                'label' => (string) ($field['label'] ?? ''),
                'description' => (string) ($field['description'] ?? ''),
                'placeholder' => (string) ($field['placeholder'] ?? ''),
                'required' => !empty($field['required']),
                'validations' => (array) ($field['validations'] ?? []),
                'validation_summary' => $this->buildFieldValidationsPromptContext($field),
                'sibling_labels' => $sibling_labels,
                'average_sibling_label_length' => $average_label_length,
            ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            $parts[] = 'Current placeholder text must be rewritten, not reused as empty.';
            $parts[] = 'If the prompt is about the placeholder, return target_property "placeholder" with one concrete replacement string and do not return an empty value.';
            $parts[] = 'Use the label and description to infer a better placeholder. Example: "Customer Name" + "Enter your name" -> "Enter your full name".';
            if ($selected_type === 'field' && !empty($field['field_type'])) {
                $parts[] = 'For this field type, preserve the field meaning but rewrite the placeholder so it is useful and non-empty.';
            }
            if (!empty($sibling_labels)) {
                $parts[] = 'Other field labels in this form:';
                $parts[] = wp_json_encode($sibling_labels, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
                $parts[] = 'Keep the label roughly aligned with the average label length in this form: ' . $average_label_length . ' characters.';
            }
        }

        if (!empty($existing_structure['form'])) {
            $parts[] = 'Form context:';
            $parts[] = wp_json_encode($existing_structure['form'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        }

        return implode("\n", $parts);
    }

    protected function resolveSafePageNavigationLabel(string $property, array $existing_structure): string
    {
        $page_count = count((array) ($existing_structure['pages'] ?? []));
        if ($property === 'next_label') {
            return $page_count <= 1
                ? __('Next', 'griffinforms-form-builder')
                : __('Continue', 'griffinforms-form-builder');
        }

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

        return '';
    }

    protected function buildSelectedTextEditReflectionPrompt(string $prompt, string $raw_content, array $errors, array $selected_context, array $existing_structure): string
    {
        $parts = [
            'The previous selected-element copy edit response failed validation.',
            'Original request:',
            $prompt,
            '',
            'Validation errors:',
            '- ' . implode("\n- ", array_map('sanitize_text_field', $errors)),
            '',
            'Previous response:',
            $raw_content,
            '',
            'Return corrected JSON only for the selected element edit.',
            'Selected builder element context:',
            wp_json_encode($selected_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'Selected element snapshot:',
            wp_json_encode($this->buildSelectedElementPromptContext($selected_context, $existing_structure), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
        ];

        return implode("\n", $parts);
    }

    protected function validateSelectedTextEditPayload(?array $decoded, string $prompt, array $selected_context): array
    {
        $errors = [];
        if (!is_array($decoded)) {
            $errors[] = 'Decoded selected text edit payload is not an array.';
            return $errors;
        }

        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $allowed = $selected_type === 'form'
            ? ['name', 'heading', 'description', 'submit_label']
            : ($selected_type === 'formpage'
                ? ['heading', 'description', 'previous_label', 'next_label']
                : ['label', 'description', 'placeholder']);

        $updates = $this->extractSelectedTextUpdates($decoded, $prompt, $selected_type);
        if (empty($updates)) {
            $errors[] = 'No valid copy updates were returned for the selected element edit.';
            return $errors;
        }

        foreach ($updates as $property => $value) {
            if (!in_array($property, $allowed, true)) {
                $errors[] = sprintf('Property "%s" is not allowed for selected type "%s".', $property, $selected_type);
            }

            if ($value === '') {
                $errors[] = sprintf('Selected element edit value for "%s" is empty.', $property);
            }

            if ($property === 'placeholder' && $value === '') {
                $errors[] = 'Placeholder updates must include a non-empty placeholder value.';
                $errors[] = 'Placeholder updates must not fall back to required_inputs or an empty choice list.';
            }
        }

        return $errors;
    }

    protected function applySelectedTextEditPayload(string $prompt, array $selected_context, array $existing_structure, array $decoded): ?array
    {
        if (empty($existing_structure['pages'])) {
            return null;
        }

        $selected_type = $this->resolveSelectedSemanticType($selected_context);
        $updates = $this->extractSelectedTextUpdates($decoded, $prompt, $selected_type);
        if (empty($updates)) {
            return null;
        }

        $updated = $existing_structure;

        if ($selected_type === 'form') {
            foreach ($updates as $property => $value) {
                $updated['form'][$property] = $value;
            }
            return $updated;
        }

        if ($selected_type === 'formpage') {
            $page_index = $this->resolveSelectedPageIndex($selected_context, $existing_structure);
            if ($page_index < 0 || !isset($updated['pages'][$page_index])) {
                return null;
            }

            foreach ($updates as $property => $value) {
                if (in_array($property, ['previous_label', 'next_label'], true)) {
                    $value = $this->resolveSafePageNavigationLabel($property, $existing_structure);
                }
                $updated['pages'][$page_index][$property] = $value;
            }
            return $updated;
        }

        if ($selected_type === 'field') {
            $field_path = $this->resolveSelectedFieldPath($selected_context, $existing_structure);
            if ($field_path === null) {
                return null;
            }

            foreach ($updates as $property => $value) {
                $updated['pages'][$field_path['page_index']]['rows'][$field_path['row_index']]['columns'][$field_path['column_index']]['fields'][$field_path['field_index']][$property] = $value;
            }
            return $updated;
        }

        return null;
    }

    protected function extractSelectedTextUpdates(array $decoded, string $prompt, string $selected_type): array
    {
        $updates = [];

        if (isset($decoded['updates']) && is_array($decoded['updates'])) {
            foreach ($decoded['updates'] as $property => $value) {
                $property = $this->normalizeSelectedTextProperty(
                    sanitize_key((string) $property),
                    $prompt,
                    $selected_type
                );
                $updates[$property] = $property === 'description'
                    ? sanitize_textarea_field((string) $value)
                    : sanitize_text_field((string) $value);
            }
        }

        if (empty($updates)) {
            $property = $this->normalizeSelectedTextProperty(
                sanitize_key((string) ($decoded['target_property'] ?? $this->resolveSelectedTextPropertyFromPrompt($prompt, $selected_type))),
                $prompt,
                $selected_type
            );
            $value = $property === 'description'
                ? sanitize_textarea_field((string) ($decoded['value'] ?? ''))
                : sanitize_text_field((string) ($decoded['value'] ?? ''));

            if ($property !== '' && $value !== '') {
                $updates[$property] = $value;
            }
        }

        return array_filter($updates, static fn($value) => $value !== '');
    }

    protected function normalizeSelectedTextProperty(string $property, string $prompt, string $selected_type): string
    {
        $property = sanitize_key($property);
        if ($property === '') {
            return $this->resolveSelectedTextPropertyFromPrompt($prompt, $selected_type);
        }

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

        if ($selected_type === 'form') {
            if (in_array($property, ['label', 'button', 'button_label', 'submit', 'submitbutton'], true)
                && preg_match('/\bsubmit\b.*\b(label|button)\b|\b(label|button)\b.*\bsubmit\b/', $normalized)) {
                return 'submit_label';
            }
            if ($property === 'title') {
                return 'name';
            }
            if ($property === 'headline') {
                return 'heading';
            }
        }

        if ($selected_type === 'formpage') {
            if (in_array($property, ['label', 'button', 'button_label'], true)) {
                if (preg_match('/\b(prev|previous|back)\b.*\b(label|button)\b|\b(label|button)\b.*\b(prev|previous|back)\b/', $normalized)) {
                    return 'previous_label';
                }
                if (preg_match('/\b(next|continue)\b.*\b(label|button)\b|\b(label|button)\b.*\b(next|continue)\b/', $normalized)) {
                    return 'next_label';
                }
            }
            if ($property === 'title' || $property === 'headline') {
                return 'heading';
            }
        }

        if ($selected_type === 'field') {
            if (in_array($property, ['title', 'heading'], true)) {
                return 'label';
            }
            if (in_array($property, ['help_text', 'instructions', 'subtext'], true)) {
                return 'description';
            }
        }

        return $property;
    }

    protected function resolveSelectedTextPropertyFromPrompt(string $prompt, string $selected_type): string
    {
        $normalized = strtolower(trim($prompt));

        if (preg_match('/\bplaceholder\b/', $normalized)) {
            return 'placeholder';
        }

        if (preg_match('/\b(description|help text|instructions|subtext)\b/', $normalized)) {
            return 'description';
        }

        if ($selected_type === 'form' && preg_match('/\bsubmit\b.*\b(label|button)\b|\b(label|button)\b.*\bsubmit\b/', $normalized)) {
            return 'submit_label';
        }

        if ($selected_type === 'form' && preg_match('/\b(name|title)\b/', $normalized)) {
            return 'name';
        }

        if ($selected_type === 'form' && preg_match('/\b(heading|headline)\b/', $normalized)) {
            return 'heading';
        }

        if ($selected_type === 'formpage' && preg_match('/\b(prev|previous|back)\b.*\b(label|button)\b|\b(label|button)\b.*\b(prev|previous|back)\b/', $normalized)) {
            return 'previous_label';
        }

        if ($selected_type === 'formpage' && preg_match('/\b(next|continue)\b.*\b(label|button)\b|\b(label|button)\b.*\b(next|continue)\b/', $normalized)) {
            return 'next_label';
        }

        if ($selected_type === 'formpage' && preg_match('/\b(heading|title|headline)\b/', $normalized)) {
            return 'heading';
        }

        if (in_array($selected_type, ['pagerow', 'row'], true) && preg_match('/\b(heading|title|headline)\b/', $normalized)) {
            return 'heading';
        }

        if (in_array($selected_type, ['pagerow', 'row'], true) && preg_match('/\b(description|help text|instructions|subtext)\b/', $normalized)) {
            return 'description';
        }

        if ($selected_type === 'field' && preg_match('/\b(label|heading|title)\b/', $normalized)) {
            return 'label';
        }

        return '';
    }

    protected function resolvePageNextButtonLabel(array $structure): string
    {
        $page_count = count((array) ($structure['pages'] ?? []));
        return $page_count <= 1
            ? __('Next', 'griffinforms-form-builder')
            : __('Continue', 'griffinforms-form-builder');
    }

    protected function resolvePagePreviousButtonLabel(array $structure): string
    {
        $page_count = count((array) ($structure['pages'] ?? []));
        return $page_count <= 1
            ? __('Previous', 'griffinforms-form-builder')
            : __('Back', 'griffinforms-form-builder');
    }

    protected function normalizeSelectedContext(array $selected_context, array $existing_structure): array
    {
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        if (in_array($selected_type, ['form', 'field', 'formpage', 'pagerow', 'rowcolumn'], true)) {
            return $selected_context;
        }

        $selected_id = absint($selected_context['item_id'] ?? 0);
        if ($selected_id > 0) {
            foreach ((array) ($existing_structure['pages'] ?? []) as $page) {
                if (absint($page['item_id'] ?? 0) === $selected_id) {
                    $selected_context['item_type'] = 'formpage';
                    return $selected_context;
                }

                foreach ((array) ($page['rows'] ?? []) as $row) {
                    if (absint($row['item_id'] ?? 0) === $selected_id) {
                        $selected_context['item_type'] = 'pagerow';
                        return $selected_context;
                    }

                    foreach ((array) ($row['columns'] ?? []) as $column) {
                        if (absint($column['item_id'] ?? 0) === $selected_id) {
                            $selected_context['item_type'] = 'rowcolumn';
                            return $selected_context;
                        }

                        foreach ((array) ($column['fields'] ?? []) as $field) {
                            if (absint($field['item_id'] ?? 0) === $selected_id) {
                                $selected_context['item_type'] = 'field';
                                return $selected_context;
                            }
                        }
                    }
                }
            }
        }

        $parent_type = sanitize_key((string) ($selected_context['parent_type'] ?? ''));
        if (in_array($parent_type, ['form', 'field', 'formpage', 'pagerow', 'rowcolumn'], true)) {
            $selected_context['item_type'] = $parent_type;
        }

        return $selected_context;
    }

    protected function resolveSelectedSemanticType(array $selected_context): string
    {
        $candidates = [
            sanitize_key((string) ($selected_context['item_type'] ?? '')),
            sanitize_key((string) ($selected_context['parent_type'] ?? '')),
        ];

        foreach ($candidates as $candidate) {
            if (in_array($candidate, ['form', 'field', 'formpage', 'pagerow', 'rowcolumn'], true)) {
                return $candidate;
            }
        }

        return '';
    }

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

        if ($selected_type === 'form' && !empty($existing_structure['form'])) {
            return (array) $existing_structure['form'];
        }

        if ($selected_type === 'formpage') {
            $page_index = $this->resolveSelectedPageIndex($selected_context, $existing_structure);
            if ($page_index >= 0 && isset($existing_structure['pages'][$page_index])) {
                return (array) $existing_structure['pages'][$page_index];
            }
        }

        if ($selected_type === 'pagerow') {
            $row_path = $this->resolveSelectedRowPath($selected_context, $existing_structure);
            if ($row_path !== null) {
                return (array) $existing_structure['pages'][$row_path['page_index']]['rows'][$row_path['row_index']];
            }
        }

        if ($selected_type === 'rowcolumn') {
            $column_path = $this->resolveSelectedColumnPath($selected_context, $existing_structure);
            if ($column_path !== null) {
                return (array) $existing_structure['pages'][$column_path['page_index']]['rows'][$column_path['row_index']]['columns'][$column_path['column_index']];
            }
        }

        if ($selected_type === 'field') {
            $field_path = $this->resolveSelectedFieldPath($selected_context, $existing_structure);
            if ($field_path !== null) {
                return (array) $existing_structure['pages'][$field_path['page_index']]['rows'][$field_path['row_index']]['columns'][$field_path['column_index']]['fields'][$field_path['field_index']];
            }
        }

        return [];
    }

    protected function buildSelectedElementPromptContext(array $selected_context, array $existing_structure): array
    {
        $selected_type = $this->resolveSelectedSemanticType($selected_context);
        $snapshot = $this->getSelectedElementSnapshot($selected_context, $existing_structure);

        if ($selected_type === 'form' && !empty($snapshot)) {
            return [
                'type' => 'form',
                'form' => [
                    'item_id' => absint($selected_context['form_id'] ?? $selected_context['item_id'] ?? 0),
                    'name' => sanitize_text_field((string) ($snapshot['name'] ?? '')),
                    'heading' => sanitize_text_field((string) ($snapshot['heading'] ?? '')),
                    'description' => sanitize_textarea_field((string) ($snapshot['description'] ?? '')),
                    'submit_label' => sanitize_text_field((string) ($snapshot['submit_label'] ?? '')),
                ],
                'children' => [
                    'page_count' => count((array) ($existing_structure['pages'] ?? [])),
                    'pages' => array_map(
                        static function ($page) {
                            return [
                                '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'] ?? '')),
                            ];
                        },
                        array_values((array) ($existing_structure['pages'] ?? []))
                    ),
                ],
            ];
        }

        if ($selected_type === 'formpage' && !empty($snapshot)) {
            return [
                'type' => 'formpage',
                'page' => [
                    'item_id' => absint($snapshot['item_id'] ?? 0),
                    'name' => sanitize_text_field((string) ($snapshot['name'] ?? '')),
                    'heading' => sanitize_text_field((string) ($snapshot['heading'] ?? '')),
                    'description' => sanitize_textarea_field((string) ($snapshot['description'] ?? '')),
                    'previous_label' => sanitize_text_field((string) ($snapshot['previous_label'] ?? '')),
                    'next_label' => sanitize_text_field((string) ($snapshot['next_label'] ?? '')),
                ],
                'children' => [
                    'row_count' => count((array) ($snapshot['rows'] ?? [])),
                    'fields' => $this->summarizePageChildrenForPrompt($snapshot),
                ],
            ];
        }

        if ($selected_type === 'pagerow' && !empty($snapshot)) {
            $row_path = $this->resolveSelectedRowPath($selected_context, $existing_structure);
            $page_context = [];
            if ($row_path !== null && isset($existing_structure['pages'][$row_path['page_index']])) {
                $page = (array) $existing_structure['pages'][$row_path['page_index']];
                $page_context = [
                    'page_item_id' => absint($page['item_id'] ?? 0),
                    'page_name' => sanitize_text_field((string) ($page['name'] ?? '')),
                    'page_heading' => sanitize_text_field((string) ($page['heading'] ?? '')),
                    'page_description' => sanitize_textarea_field((string) ($page['description'] ?? '')),
                ];
            }

            return [
                'type' => 'pagerow',
                'row' => [
                    'item_id' => absint($snapshot['item_id'] ?? 0),
                    'name' => sanitize_text_field((string) ($snapshot['name'] ?? '')),
                    'row_label' => sanitize_text_field((string) ($snapshot['row_label'] ?? '')),
                ],
                'context' => $page_context,
                'children' => [
                    'layout_column_count' => count((array) ($snapshot['columns'] ?? [])),
                    'layout_columns' => $this->summarizeRowChildrenForPrompt($snapshot),
                ],
            ];
        }

        if ($selected_type === 'rowcolumn' && !empty($snapshot)) {
            $column_path = $this->resolveSelectedColumnPath($selected_context, $existing_structure);
            $row_context = [];
            if ($column_path !== null && isset($existing_structure['pages'][$column_path['page_index']]['rows'][$column_path['row_index']])) {
                $page = (array) $existing_structure['pages'][$column_path['page_index']];
                $row = (array) $existing_structure['pages'][$column_path['page_index']]['rows'][$column_path['row_index']];
                $row_context = [
                    'page_item_id' => absint($page['item_id'] ?? 0),
                    'page_name' => sanitize_text_field((string) ($page['name'] ?? '')),
                    'page_heading' => sanitize_text_field((string) ($page['heading'] ?? '')),
                    'row_item_id' => absint($row['item_id'] ?? 0),
                    'row_name' => sanitize_text_field((string) ($row['name'] ?? '')),
                    'row_label' => sanitize_text_field((string) ($row['row_label'] ?? '')),
                ];
            }

            return [
                'type' => 'rowcolumn',
                'layout_column' => [
                    'item_id' => absint($snapshot['item_id'] ?? 0),
                    'name' => sanitize_text_field((string) ($snapshot['name'] ?? '')),
                    'layout_column_label' => sanitize_text_field((string) ($snapshot['column_label'] ?? '')),
                    'width' => absint($snapshot['width'] ?? 12),
                ],
                'context' => $row_context,
                'children' => [
                    'field_count' => count((array) ($snapshot['fields'] ?? [])),
                    'fields' => $this->summarizeColumnChildrenForPrompt($snapshot),
                ],
            ];
        }

        if ($selected_type === 'field' && !empty($snapshot)) {
            $field_path = $this->resolveSelectedFieldPath($selected_context, $existing_structure);
            $page_context = [];
            if ($field_path !== null && isset($existing_structure['pages'][$field_path['page_index']])) {
                $page = (array) $existing_structure['pages'][$field_path['page_index']];
                $page_context = [
                    'page_name' => sanitize_text_field((string) ($page['name'] ?? '')),
                    'page_heading' => sanitize_text_field((string) ($page['heading'] ?? '')),
                    'page_description' => sanitize_textarea_field((string) ($page['description'] ?? '')),
                    'sibling_fields' => $this->summarizeSiblingFieldsForPrompt($existing_structure, $field_path),
                ];
            }

            return [
                'type' => 'field',
                'field' => [
                    'item_id' => absint($snapshot['item_id'] ?? 0),
                    'name' => sanitize_text_field((string) ($snapshot['name'] ?? '')),
                    'label' => sanitize_text_field((string) ($snapshot['label'] ?? '')),
                    'description' => sanitize_textarea_field((string) ($snapshot['description'] ?? '')),
                    'placeholder' => sanitize_text_field((string) ($snapshot['placeholder'] ?? '')),
                    'field_type' => sanitize_key((string) ($snapshot['field_type'] ?? '')),
                    'options' => $this->buildFieldOptionsPromptContext($snapshot),
                    'validations' => $this->buildFieldValidationsPromptContext($snapshot),
                ],
                'context' => $page_context,
            ];
        }

        return $snapshot;
    }

    protected function summarizePageChildrenForPrompt(array $page_snapshot): array
    {
        $summary = [];

        foreach ((array) ($page_snapshot['rows'] ?? []) as $row_index => $row) {
            foreach ((array) ($row['columns'] ?? []) as $column_index => $column) {
                foreach ((array) ($column['fields'] ?? []) as $field_index => $field) {
                    $summary[] = [
                        'row' => $row_index + 1,
                        'layout_column' => $column_index + 1,
                        'position' => $field_index + 1,
                        'label' => sanitize_text_field((string) ($field['label'] ?? '')),
                        'field_type' => sanitize_key((string) ($field['field_type'] ?? '')),
                        'description' => sanitize_textarea_field((string) ($field['description'] ?? '')),
                    ];
                }
            }
        }

        return $summary;
    }

    protected function summarizeRowChildrenForPrompt(array $row_snapshot): array
    {
        $summary = [];

        foreach ((array) ($row_snapshot['columns'] ?? []) as $column_index => $column) {
            $summary[] = [
                'position' => $column_index + 1,
                'item_id' => absint($column['item_id'] ?? 0),
                'layout_column_label' => sanitize_text_field((string) ($column['column_label'] ?? '')),
                'width' => absint($column['width'] ?? 12),
                'fields' => $this->summarizeColumnChildrenForPrompt((array) $column),
            ];
        }

        return $summary;
    }

    protected function summarizeColumnChildrenForPrompt(array $column_snapshot): array
    {
        $summary = [];

        foreach ((array) ($column_snapshot['fields'] ?? []) as $field_index => $field) {
            $summary[] = [
                'position' => $field_index + 1,
                'item_id' => absint($field['item_id'] ?? 0),
                'label' => sanitize_text_field((string) ($field['label'] ?? '')),
                'field_type' => sanitize_key((string) ($field['field_type'] ?? '')),
                'description' => sanitize_textarea_field((string) ($field['description'] ?? '')),
            ];
        }

        return $summary;
    }

    protected function summarizeSiblingFieldsForPrompt(array $existing_structure, array $field_path): array
    {
        $summary = [];
        $fields = (array) ($existing_structure['pages'][$field_path['page_index']]['rows'][$field_path['row_index']]['columns'][$field_path['column_index']]['fields'] ?? []);

        foreach ($fields as $index => $field) {
            $summary[] = [
                'position' => $index + 1,
                'item_id' => absint($field['item_id'] ?? 0),
                'label' => sanitize_text_field((string) ($field['label'] ?? '')),
                'field_type' => sanitize_key((string) ($field['field_type'] ?? '')),
            ];
        }

        return $summary;
    }

    protected function buildFieldOptionsPromptContext(array $field_snapshot): array
    {
        $field_type = sanitize_key((string) ($field_snapshot['field_type'] ?? ''));
        $options = (array) ($field_snapshot['options'] ?? []);

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

        if (in_array($field_type, ['inputcheckbox', 'inputradio'], true)) {
            $option_columns = [];

            foreach ($options as $group_index => $group) {
                $option_columns[] = [
                    'option_column' => absint($group['option_column'] ?? $group['column'] ?? $group_index + 1),
                    'options' => array_values(array_map(
                        static function ($option) {
                            return [
                                'label' => sanitize_text_field((string) ($option['label'] ?? '')),
                                'value' => sanitize_text_field((string) ($option['value'] ?? '')),
                            ];
                        },
                        array_values((array) ($group['options'] ?? []))
                    )),
                ];
            }

            return [
                'type' => 'grouped_option_columns',
                'option_column_count' => count($option_columns),
                'option_columns' => $option_columns,
            ];
        }

        if ($field_type === 'selectfield') {
            $grouped_options = [];
            foreach ($options as $group_name => $group_options) {
                if (!is_array($group_options) || empty($group_options)) {
                    continue;
                }

                $group_map = [];
                foreach ($group_options as $option_label => $option_value) {
                    if (is_array($option_value)) {
                        $label = sanitize_text_field((string) ($option_value['label'] ?? ''));
                        $value = sanitize_text_field((string) ($option_value['value'] ?? ''));
                    } else {
                        $label = sanitize_text_field((string) $option_label);
                        $value = sanitize_text_field((string) $option_value);
                    }

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

                    $group_map[$label] = $value;
                }

                if (!empty($group_map)) {
                    $grouped_options[] = [
                        'group_name' => sanitize_text_field((string) $group_name),
                        'options' => $group_map,
                    ];
                }
            }

            return [
                'type' => 'grouped_selectfield_options',
                'groups' => $grouped_options,
            ];
        }

        if ($field_type === 'inputlikert') {
            $rows = [];
            foreach ((array) ($field_snapshot['likert_rows'] ?? []) as $row) {
                if (!is_array($row)) {
                    continue;
                }

                $rows[] = [
                    'id' => sanitize_text_field((string) ($row['id'] ?? '')),
                    'label' => sanitize_text_field((string) ($row['label'] ?? '')),
                ];
            }

            $columns = [];
            foreach ((array) ($field_snapshot['likert_columns'] ?? []) as $column) {
                if (!is_array($column)) {
                    continue;
                }

                $columns[] = [
                    'id' => sanitize_text_field((string) ($column['id'] ?? '')),
                    'label' => sanitize_text_field((string) ($column['label'] ?? '')),
                ];
            }

            return [
                'type' => 'likert_matrix',
                'likert_rows' => $rows,
                'likert_columns' => $columns,
                'likert_allow_na' => !empty($field_snapshot['likert_allow_na']),
                'required_mode' => sanitize_key((string) ($field_snapshot['required_mode'] ?? 'all')),
                'likert_column_width' => sanitize_text_field((string) ($field_snapshot['likert_column_width'] ?? '')),
            ];
        }

        return [
            'type' => 'flat_options',
            'options' => array_values(array_map(
                static function ($option) {
                    return [
                        'label' => sanitize_text_field((string) ($option['label'] ?? '')),
                        'value' => sanitize_text_field((string) ($option['value'] ?? '')),
                    ];
                },
                $options
            )),
        ];
    }

    protected function buildFieldValidationsPromptContext(array $field_snapshot): array
    {
        $field_type = sanitize_key((string) ($field_snapshot['field_type'] ?? ''));
        $validations = (array) ($field_snapshot['validations'] ?? []);
        if (empty($validations)) {
            return [];
        }

        $summary = [];
        foreach ($validations as $key => $value) {
            $semantic_key = sanitize_key((string) $key);
            if ($field_type === 'inputurl') {
                if ($semantic_key === 'allowed_tlds') {
                    $summary[] = [
                        'key' => 'allowed_tlds',
                        'meaning' => 'Allowed top-level domains',
                        'values' => array_values((array) $value),
                    ];
                    continue;
                }

                if ($semantic_key === 'blocked_tlds') {
                    $summary[] = [
                        'key' => 'blocked_tlds',
                        'meaning' => 'Blocked top-level domains',
                        'values' => array_values((array) $value),
                    ];
                    continue;
                }
            }

            if ($field_type === 'inputphone' && $semantic_key === 'allowed_country_codes') {
                $summary[] = [
                    'key' => 'allowed_country_codes',
                    'meaning' => 'Allowed phone country codes',
                    'values' => array_values((array) $value),
                ];
                continue;
            }

            $summary[] = [
                'key' => $semantic_key,
                'meaning' => $semantic_key,
                'value' => $value,
            ];
        }

        return $summary;
    }

    protected function resolveSelectedRowPath(array $selected_context, array $existing_structure): ?array
    {
        $selected_id = absint($selected_context['item_id'] ?? 0);
        if ($selected_id <= 0) {
            return null;
        }

        foreach (array_values((array) ($existing_structure['pages'] ?? [])) as $page_index => $page) {
            foreach (array_values((array) ($page['rows'] ?? [])) as $row_index => $row) {
                if (absint($row['item_id'] ?? 0) === $selected_id) {
                    return [
                        'page_index' => $page_index,
                        'row_index' => $row_index,
                    ];
                }
            }
        }

        return null;
    }

    protected function resolveSelectedColumnPath(array $selected_context, array $existing_structure): ?array
    {
        $selected_id = absint($selected_context['item_id'] ?? 0);
        if ($selected_id <= 0) {
            return null;
        }

        foreach (array_values((array) ($existing_structure['pages'] ?? [])) as $page_index => $page) {
            foreach (array_values((array) ($page['rows'] ?? [])) as $row_index => $row) {
                foreach (array_values((array) ($row['columns'] ?? [])) as $column_index => $column) {
                    if (absint($column['item_id'] ?? 0) === $selected_id) {
                        return [
                            'page_index' => $page_index,
                            'row_index' => $row_index,
                            'column_index' => $column_index,
                        ];
                    }
                }
            }
        }

        return null;
    }

    protected function resolveSelectedFieldPath(array $selected_context, array $existing_structure): ?array
    {
        $selected_id = absint($selected_context['item_id'] ?? 0);
        if ($selected_id <= 0) {
            return null;
        }

        foreach (array_values((array) ($existing_structure['pages'] ?? [])) as $page_index => $page) {
            foreach (array_values((array) ($page['rows'] ?? [])) as $row_index => $row) {
                foreach (array_values((array) ($row['columns'] ?? [])) as $column_index => $column) {
                    foreach (array_values((array) ($column['fields'] ?? [])) as $field_index => $field) {
                        if (absint($field['item_id'] ?? 0) === $selected_id) {
                            return [
                                'page_index' => $page_index,
                                'row_index' => $row_index,
                                'column_index' => $column_index,
                                'field_index' => $field_index,
                            ];
                        }
                    }
                }
            }
        }

        return null;
    }

    protected function extractSelectedTextEditSuccessMessage(array $decoded, string $prompt, array $selected_context): string
    {
        $candidate = sanitize_text_field((string) ($decoded['success_message'] ?? ''));
        if ($candidate !== '') {
            return function_exists('mb_substr') ? mb_substr($candidate, 0, 140) : substr($candidate, 0, 140);
        }

        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $updates = $this->extractSelectedTextUpdates($decoded, $prompt, $selected_type);
        $properties = array_keys($updates);
        $multiple = count($properties) > 1;
        $property = $properties[0] ?? $this->resolveSelectedTextPropertyFromPrompt($prompt, $selected_type);

        if ($selected_type === 'form') {
            if ($multiple) {
                return __('Updated the form copy.', 'griffinforms-form-builder');
            }
            if ($property === 'submit_label') {
                return __('Updated the submit button label.', 'griffinforms-form-builder');
            }
            if ($property === 'description') {
                return __('Updated the form description.', 'griffinforms-form-builder');
            }
            if ($property === 'heading') {
                return __('Updated the form heading.', 'griffinforms-form-builder');
            }
            if ($property === 'name') {
                return __('Updated the form name.', 'griffinforms-form-builder');
            }
        }

        if ($selected_type === 'field') {
            if ($multiple) {
                return __('Updated the selected field copy.', 'griffinforms-form-builder');
            }
            if ($property === 'description') {
                return __('Updated the selected field description.', 'griffinforms-form-builder');
            }
            if ($property === 'placeholder') {
                return __('Updated the selected field placeholder.', 'griffinforms-form-builder');
            }

            return __('Updated the selected field copy.', 'griffinforms-form-builder');
        }

        if ($selected_type === 'formpage') {
            if ($multiple) {
                return __('Updated the selected page copy.', 'griffinforms-form-builder');
            }
            if ($property === 'previous_label') {
                return __('Updated the selected page previous button label.', 'griffinforms-form-builder');
            }
            if ($property === 'next_label') {
                return __('Updated the selected page next button label.', 'griffinforms-form-builder');
            }
            if ($property === 'description') {
                return __('Updated the selected page description.', 'griffinforms-form-builder');
            }
            if ($property === 'heading') {
                return __('Updated the selected page heading.', 'griffinforms-form-builder');
            }
        }

        return __('Updated the selected element.', 'griffinforms-form-builder');
    }

    protected function decodeValue($value)
    {
        if (is_array($value)) {
            return $value;
        }

        if (!is_string($value) || $value === '') {
            return $value;
        }

        $maybe_unserialized = maybe_unserialize($value);
        if ($maybe_unserialized !== $value || $value === 'b:0;') {
            return $maybe_unserialized;
        }

        $decoded_json = json_decode($value, true);
        if (json_last_error() === JSON_ERROR_NONE) {
            return $decoded_json;
        }

        return $value;
    }
}
