<?php

namespace GriffinForms\Includes\Ai;

use GriffinForms\Includes\Ai\Exceptions\AiBrokerError;
use GriffinForms\Includes\Ai\Exceptions\ButlerTimeoutException;
use GriffinForms\Includes\Ai\PromptRegistry\JsonExtractor;
use GriffinForms\Includes\Ai\PromptRegistry\PromptRegistry;
use GriffinForms\Includes\Extensions\ExtensionRegistry;
use GriffinForms\Includes\Packages\PackageAdmission;

class BuilderAssistRouter
{
    protected AssistRequestRouter $request_router;
    protected AiBroker $broker;
    protected PromptRegistry $registry;
    protected JsonExtractor $extractor;
    protected \GriffinForms\Log $log;
    protected AssistFailureFramework $failure_framework;
    protected ExtensionRegistry $extension_registry;
    protected PackageAdmission $package_admission;

    protected IntentClassifier $classifier;
    protected OperationPlanner $operation_planner;

    public function __construct()
    {
        $this->request_router = new AssistRequestRouter();
        $this->broker = AiBroker::getInstance();
        $this->registry = new PromptRegistry();
        $this->extractor = new JsonExtractor();
        $this->log = \GriffinForms\Log::getInstance();
        $this->failure_framework = new AssistFailureFramework();
        $this->extension_registry = ExtensionRegistry::getInstance();
        $this->package_admission = PackageAdmission::getInstance();
        $this->classifier = new IntentClassifier(
            $this->broker,
            $this->failure_framework,
            $this->log,
            $this->extension_registry,
            $this->package_admission
        );
        $this->operation_planner = new OperationPlanner($this->registry);
    }

    public function execute(int $form_id, string $prompt, array $selected_context = [], array $request_context = []): array
    {
        $this->log->add(
            'debug',
            'ai_butler_request',
            $form_id,
            (int) get_current_user_id(),
            sprintf(
                'Butler execute start prompt=%s selected_context=%s request_context=%s',
                sanitize_text_field($prompt),
                wp_json_encode($selected_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                wp_json_encode($request_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
            ),
            'ai_butler_request'
        );

        $request = $this->request_router->buildRequest($form_id, $prompt, $selected_context, $request_context);
        $this->log->add(
            'debug',
            'ai_butler_request',
            $form_id,
            (int) get_current_user_id(),
            sprintf(
                'Butler built request surface=%s origin=%s response_mode=%s response_family=%s semantic_intent=%s prompt=%s request_context=%s selected_context=%s',
                (string) ($request['surface'] ?? ''),
                (string) ($request['origin'] ?? ''),
                (string) ($request['response_mode'] ?? ''),
                (string) ($request['response_family'] ?? ''),
                (string) ($request['semantic_intent'] ?? ''),
                sanitize_text_field((string) ($request['prompt'] ?? '')),
                wp_json_encode((array) ($request['request_context'] ?? []), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                wp_json_encode((array) ($request['selected_context'] ?? []), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
            ),
            'ai_butler_request'
        );

        $preflight = $this->request_router->preflight($request);
        if (is_array($preflight)) {
            $this->log->add(
                'debug',
                'ai_butler_request',
                $form_id,
                (int) get_current_user_id(),
                sprintf(
                    'Butler preflight blocked status=%s code=%s message=%s',
                    (string) ($preflight['status'] ?? ''),
                    (string) ($preflight['code'] ?? ''),
                    sanitize_text_field((string) ($preflight['message'] ?? ''))
                ),
                'ai_butler_request'
            );
            return $preflight;
        }

        $package_field_block = $this->maybeBlockUnavailablePackageFieldRequest($request, $prompt, $selected_context);
        if (is_array($package_field_block)) {
            return $package_field_block;
        }

        $prompt = (string) ($request['prompt'] ?? '');
        $selected_context = (array) ($request['selected_context'] ?? []);
        $request_context = (array) ($request['request_context'] ?? []);

        // =====================================================================
        // Suggestion-id prompt enrichment
        // Each block augments $prompt with context-specific rules/examples.
        // Routing decisions are made by IntentClassifier below — not here.
        // =====================================================================

        if (preg_match('/\bpolish\s+form\s+copy\b/i', $prompt)) {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_context = is_array($request_context['suggestion_context'] ?? null) ? (array) $request_context['suggestion_context'] : [];
            $suggestion_id = $this->normalizeSuggestionId((string) ($suggestion_context['suggestion_id'] ?? ''));
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if ($suggestion_id === 'form.improve_description' && !empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nForm description rules:\n- " . implode("\n- ", $rule_lines);
                }
            }
            $this->log->add(
                'debug',
                'ai_butler_request',
                $form_id,
                (int) get_current_user_id(),
                sprintf(
                    'Butler form-copy shortcut suggestion_id=%s suggestion_meta=%s prompt=%s',
                    $suggestion_id,
                    wp_json_encode($suggestion_meta, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                    sanitize_text_field($prompt)
                ),
                'ai_butler_request'
            );
            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => true,
                    'request_context' => $request_context,
                ]
            );

            return $this->dispatchAndWrap('generate_patch_plan', $form_id, $prompt, $provider, $config, $request);
        }

        $suggestion_context = is_array($request_context['suggestion_context'] ?? null) ? (array) $request_context['suggestion_context'] : [];
        $suggestion_id = $this->normalizeSuggestionId((string) ($suggestion_context['suggestion_id'] ?? ''));
        $this->log->add(
            'debug',
            'ai_butler_request',
            $form_id,
            (int) get_current_user_id(),
            sprintf(
                'Butler suggestion routing suggestion_id=%s selected_context=%s prompt=%s',
                $suggestion_id,
                wp_json_encode($selected_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                sanitize_text_field($prompt)
            ),
            'ai_butler_request'
        );
        if (in_array($suggestion_id, ['row.improve_heading', 'row.polish_description'], true)) {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if (!empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nRow copy rules:\n- " . implode("\n- ", $rule_lines);
                }
            }

            if (!empty($suggestion_meta['ideal_row_heading_examples']) && is_array($suggestion_meta['ideal_row_heading_examples'])) {
                $example_lines = array_filter(array_map(static fn($example) => trim((string) $example), $suggestion_meta['ideal_row_heading_examples']));
                if (!empty($example_lines)) {
                    $prompt .= "\n\nIdeal row heading examples:\n- " . implode("\n- ", $example_lines);
                }
            }
            $this->log->add(
                'debug',
                'ai_butler_request',
                $form_id,
                (int) get_current_user_id(),
                sprintf(
                    'Butler row-copy shortcut suggestion_id=%s suggestion_meta=%s prompt=%s',
                    $suggestion_id,
                    wp_json_encode($suggestion_meta, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                    sanitize_text_field($prompt)
                ),
                'ai_butler_request'
            );

            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => true,
                    'request_context' => $request_context,
                ]
            );

            return $this->dispatchAndWrap('generate_patch_plan', $form_id, $prompt, $provider, $config, $request);
        }

        if (in_array($suggestion_id, ['field.polish_description', 'selectfield.polish_description', 'inputcheckbox.polish_description', 'inputradio.polish_description', 'inputtime.polish_description', 'inputpassword.polish_description', 'termsconditions.polish_description', 'payment.polish_description'], true)) {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if (!empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nField description rules:\n- " . implode("\n- ", $rule_lines);
                }
            }

            if (!empty($suggestion_meta['ideal_field_description_examples']) && is_array($suggestion_meta['ideal_field_description_examples'])) {
                $example_lines = array_filter(array_map(static fn($example) => trim((string) $example), $suggestion_meta['ideal_field_description_examples']));
                if (!empty($example_lines)) {
                    $prompt .= "\n\nIdeal field description examples:\n- " . implode("\n- ", $example_lines);
                }
            }

            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => true,
                    'request_context' => $request_context,
                ]
            );

            return $this->dispatchAndWrap('generate_patch_plan', $form_id, $prompt, $provider, $config, $request);
        }

        if ($suggestion_id === 'inputaddress.improve_sublabels') {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if (!empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nAddress sub-label rules:\n- " . implode("\n- ", $rule_lines);
                }
            }

            if (!empty($suggestion_meta['ideal_sublabel_examples']) && is_array($suggestion_meta['ideal_sublabel_examples'])) {
                $example_parts = [];
                foreach ($suggestion_meta['ideal_sublabel_examples'] as $example) {
                    if (is_array($example)) {
                        $example_parts[] = wp_json_encode($example, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
                    }
                }
                if (!empty($example_parts)) {
                    $prompt .= "\n\nIdeal sub-label examples:\n" . implode("\n", $example_parts);
                }
            }

            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => true,
                    'request_context' => $request_context,
                ]
            );

            return $this->dispatchAndWrap('generate_patch_plan', $form_id, $prompt, $provider, $config, $request);
        }

        if ($suggestion_id === 'inputnumber.add_range_hint') {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if (!empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nNumber range hint rules:\n- " . implode("\n- ", $rule_lines);
                }
            }

            if (!empty($suggestion_meta['ideal_range_hint_examples']) && is_array($suggestion_meta['ideal_range_hint_examples'])) {
                $example_lines = array_filter(array_map(static fn($example) => trim((string) $example), $suggestion_meta['ideal_range_hint_examples']));
                if (!empty($example_lines)) {
                    $prompt .= "\n\nIdeal range hint examples:\n- " . implode("\n- ", $example_lines);
                }
            }

            $selected_field_id = absint($selected_context['item_id'] ?? 0);
            if ($selected_field_id > 0) {
                $format_sql = \GriffinForms\Admin\Sql\Format::getInstance();
                $field_row = $format_sql->itemValues($selected_field_id, 'field', ['validations', 'field_settings']);
                $constraints = [];
                if (is_object($field_row)) {
                    $validations = maybe_unserialize($field_row->validations ?? '');
                    if (is_array($validations)) {
                        if (isset($validations['min_chars']) && $validations['min_chars'] !== '') {
                            $constraints[] = 'min: ' . $validations['min_chars'];
                        }
                        if (isset($validations['max_chars']) && $validations['max_chars'] !== '') {
                            $constraints[] = 'max: ' . $validations['max_chars'];
                        }
                        if (isset($validations['number_type']) && $validations['number_type'] !== '') {
                            $constraints[] = 'number_type: ' . $validations['number_type'];
                        }
                        if (isset($validations['allow_decimal'])) {
                            $constraints[] = 'allow_decimal: ' . ($validations['allow_decimal'] ? 'true' : 'false');
                        }
                        if (!empty($validations['multiples']) && is_array($validations['multiples'])) {
                            $constraints[] = 'multiples_of: ' . implode(', ', $validations['multiples']);
                        }
                    }
                }
                if (!empty($constraints)) {
                    $prompt .= "\n\nField constraints (use these to write the hint):\n- " . implode("\n- ", $constraints);
                }
            }

            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => true,
                    'request_context' => $request_context,
                ]
            );

            return $this->dispatchAndWrap('generate_patch_plan', $form_id, $prompt, $provider, $config, $request);
        }

        if ($suggestion_id === 'selectfield.refine_options') {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if (!empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nOption refinement rules:\n- " . implode("\n- ", $rule_lines);
                }
            }

            if (!empty($suggestion_meta['ideal_option_label_examples']) && is_array($suggestion_meta['ideal_option_label_examples'])) {
                $example_lines = array_filter(array_map(static fn($example) => trim((string) $example), $suggestion_meta['ideal_option_label_examples']));
                if (!empty($example_lines)) {
                    $prompt .= "\n\nIdeal option label examples:\n- " . implode("\n- ", $example_lines);
                }
            }

            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'request_context' => $request_context,
                    'suggestion_id' => 'selectfield.refine_options',
                ]
            );

            return $this->dispatchAndWrap('generate_selectfield_options', $form_id, $prompt, $provider, $config, $request);
        }

        if ($suggestion_id === 'selectfield.add_option_groups') {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if (!empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nOption grouping rules:\n- " . implode("\n- ", $rule_lines);
                }
            }

            if (!empty($suggestion_meta['ideal_group_name_examples']) && is_array($suggestion_meta['ideal_group_name_examples'])) {
                $example_lines = array_filter(array_map(static fn($example) => trim((string) $example), $suggestion_meta['ideal_group_name_examples']));
                if (!empty($example_lines)) {
                    $prompt .= "\n\nIdeal optgroup name examples:\n- " . implode("\n- ", $example_lines);
                }
            }

            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'request_context' => $request_context,
                    'suggestion_id' => 'selectfield.add_option_groups',
                ]
            );

            return $this->dispatchAndWrap('generate_selectfield_options', $form_id, $prompt, $provider, $config, $request);
        }

        if ($suggestion_id === 'termsconditions.improve_checkbox_label') {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if (!empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nCheckbox label rules:\n- " . implode("\n- ", $rule_lines);
                }
            }

            if (!empty($suggestion_meta['ideal_checkbox_label_examples']) && is_array($suggestion_meta['ideal_checkbox_label_examples'])) {
                $example_lines = array_filter(array_map(static fn($example) => trim((string) $example), $suggestion_meta['ideal_checkbox_label_examples']));
                if (!empty($example_lines)) {
                    $prompt .= "\n\nIdeal checkbox label examples:\n- " . implode("\n- ", $example_lines);
                }
            }

            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => true,
                    'request_context' => $request_context,
                ]
            );

            return $this->dispatchAndWrap('generate_patch_plan', $form_id, $prompt, $provider, $config, $request);
        }

        if ($suggestion_id === 'termsconditions.improve_anchor_text') {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if (!empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nAnchor text rules:\n- " . implode("\n- ", $rule_lines);
                }
            }

            if (!empty($suggestion_meta['ideal_anchor_text_examples']) && is_array($suggestion_meta['ideal_anchor_text_examples'])) {
                $example_lines = array_filter(array_map(static fn($example) => trim((string) $example), $suggestion_meta['ideal_anchor_text_examples']));
                if (!empty($example_lines)) {
                    $prompt .= "\n\nIdeal anchor text examples:\n- " . implode("\n- ", $example_lines);
                }
            }

            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => true,
                    'request_context' => $request_context,
                ]
            );

            return $this->dispatchAndWrap('generate_patch_plan', $form_id, $prompt, $provider, $config, $request);
        }

        if ($suggestion_id === 'payment.improve_product_descriptions') {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if (!empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nProduct description rules:\n- " . implode("\n- ", $rule_lines);
                }
            }

            if (!empty($suggestion_meta['ideal_product_description_examples']) && is_array($suggestion_meta['ideal_product_description_examples'])) {
                $example_lines = array_filter(array_map(static fn($example) => trim((string) $example), $suggestion_meta['ideal_product_description_examples']));
                if (!empty($example_lines)) {
                    $prompt .= "\n\nIdeal product description examples:\n- " . implode("\n- ", $example_lines);
                }
            }

            $selected_field_id = absint($selected_context['item_id'] ?? 0);
            $current_products = [];
            if ($selected_field_id > 0) {
                $format_sql = \GriffinForms\Admin\Sql\Format::getInstance();
                $field_row = $format_sql->itemValues($selected_field_id, 'field', ['field_options']);
                if (is_object($field_row)) {
                    $field_options = maybe_unserialize($field_row->field_options ?? '');
                    if (is_array($field_options) && !empty($field_options)) {
                        $current_products = $field_options;
                        $product_lines = [];
                        foreach ($field_options as $product) {
                            if (!is_array($product)) {
                                continue;
                            }
                            $product_name = sanitize_text_field((string) ($product['name'] ?? ''));
                            $product_desc = sanitize_text_field((string) ($product['description'] ?? ''));
                            $product_price = sanitize_text_field((string) ($product['price'] ?? ''));
                            if ($product_name !== '') {
                                $summary = $product_name;
                                if ($product_price !== '') {
                                    $summary .= ' (' . $product_price . ')';
                                }
                                if ($product_desc !== '') {
                                    $summary .= ': ' . $product_desc;
                                }
                                $product_lines[] = $summary;
                            }
                        }
                        if (!empty($product_lines)) {
                            $prompt .= "\n\nCurrent products (preserve all product fields exactly — only rewrite the description of each):\n- " . implode("\n- ", $product_lines);
                        }
                    }
                }
            }

            $prompt .= "\n\nIMPORTANT: Return the full updated product array as field_options in the changes object. Each product must be a complete object with all original fields preserved (id, name, price, type, quantity, etc.) — only the description value should change. Example structure:\n{\"op\":\"update_field\",\"target\":{\"scope\":\"field\",\"item_id\":" . $selected_field_id . "},\"changes\":{\"field_options\":[{\"id\":\"...\",\"name\":\"...\",\"price\":\"...\",\"description\":\"new description here\"}]}}";

            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => true,
                    'request_context' => $request_context,
                ]
            );

            return $this->dispatchAndWrap('generate_patch_plan', $form_id, $prompt, $provider, $config, $request);
        }

        if ($suggestion_id === 'fileupload.describe_upload_requirements') {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if (!empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nUpload requirements hint rules:\n- " . implode("\n- ", $rule_lines);
                }
            }

            if (!empty($suggestion_meta['ideal_upload_requirement_examples']) && is_array($suggestion_meta['ideal_upload_requirement_examples'])) {
                $example_lines = array_filter(array_map(static fn($example) => trim((string) $example), $suggestion_meta['ideal_upload_requirement_examples']));
                if (!empty($example_lines)) {
                    $prompt .= "\n\nIdeal upload requirement examples:\n- " . implode("\n- ", $example_lines);
                }
            }

            $selected_field_id = absint($selected_context['item_id'] ?? 0);
            if ($selected_field_id > 0) {
                $format_sql = \GriffinForms\Admin\Sql\Format::getInstance();
                $field_row = $format_sql->itemValues($selected_field_id, 'field', ['field_settings']);
                $constraints = [];
                if (is_object($field_row)) {
                    $field_settings = maybe_unserialize($field_row->field_settings ?? '');
                    if (is_array($field_settings)) {
                        $custom_mimes = array_values(array_filter(array_map('sanitize_text_field', (array) ($field_settings['custom_mimes'] ?? []))));
                        if (!empty($custom_mimes)) {
                            $constraints[] = 'accepted_file_types: ' . implode(', ', $custom_mimes);
                        }
                        $max_size_mb = $field_settings['max_size_mb'] ?? '';
                        if ($max_size_mb !== '' && is_numeric($max_size_mb) && (float) $max_size_mb > 0) {
                            $constraints[] = 'max_size_mb: ' . $max_size_mb;
                        }
                        $max_files = $field_settings['max_files'] ?? '';
                        if ($max_files !== '' && is_numeric($max_files) && (int) $max_files > 0) {
                            $constraints[] = 'max_files: ' . (int) $max_files;
                        }
                    }
                }
                if (!empty($constraints)) {
                    $prompt .= "\n\nField upload constraints (use these to write the description):\n- " . implode("\n- ", $constraints);
                }
            }

            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => true,
                    'request_context' => $request_context,
                ]
            );

            return $this->dispatchAndWrap('generate_patch_plan', $form_id, $prompt, $provider, $config, $request);
        }

        if ($suggestion_id === 'textarea.add_char_limit_hint') {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if (!empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nCharacter limit hint rules:\n- " . implode("\n- ", $rule_lines);
                }
            }

            if (!empty($suggestion_meta['ideal_char_limit_hint_examples']) && is_array($suggestion_meta['ideal_char_limit_hint_examples'])) {
                $example_lines = array_filter(array_map(static fn($example) => trim((string) $example), $suggestion_meta['ideal_char_limit_hint_examples']));
                if (!empty($example_lines)) {
                    $prompt .= "\n\nIdeal character limit hint examples:\n- " . implode("\n- ", $example_lines);
                }
            }

            $selected_field_id = absint($selected_context['item_id'] ?? 0);
            if ($selected_field_id > 0) {
                $format_sql = \GriffinForms\Admin\Sql\Format::getInstance();
                $field_row = $format_sql->itemValues($selected_field_id, 'field', ['validations']);
                $constraints = [];
                if (is_object($field_row)) {
                    $validations = maybe_unserialize($field_row->validations ?? '');
                    if (is_array($validations)) {
                        if (isset($validations['min_chars']) && $validations['min_chars'] !== '' && $validations['min_chars'] > 0) {
                            $constraints[] = 'min_chars: ' . absint($validations['min_chars']);
                        }
                        if (isset($validations['max_chars']) && $validations['max_chars'] !== '' && $validations['max_chars'] > 0) {
                            $constraints[] = 'max_chars: ' . absint($validations['max_chars']);
                        }
                    }
                }
                if (!empty($constraints)) {
                    $prompt .= "\n\nField character constraints (use these to write the hint):\n- " . implode("\n- ", $constraints);
                }
            }

            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => true,
                    'request_context' => $request_context,
                ]
            );

            return $this->dispatchAndWrap('generate_patch_plan', $form_id, $prompt, $provider, $config, $request);
        }

        if ($suggestion_id === 'inputdate.add_date_window_hint') {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if (!empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nDate window hint rules:\n- " . implode("\n- ", $rule_lines);
                }
            }

            if (!empty($suggestion_meta['ideal_date_window_hint_examples']) && is_array($suggestion_meta['ideal_date_window_hint_examples'])) {
                $example_lines = array_filter(array_map(static fn($example) => trim((string) $example), $suggestion_meta['ideal_date_window_hint_examples']));
                if (!empty($example_lines)) {
                    $prompt .= "\n\nIdeal date window hint examples:\n- " . implode("\n- ", $example_lines);
                }
            }

            $selected_field_id = absint($selected_context['item_id'] ?? 0);
            if ($selected_field_id > 0) {
                $format_sql = \GriffinForms\Admin\Sql\Format::getInstance();
                $field_row = $format_sql->itemValues($selected_field_id, 'field', ['validations']);
                $constraints = [];
                if (is_object($field_row)) {
                    $validations = maybe_unserialize($field_row->validations ?? '');
                    if (is_array($validations)) {
                        $date_type = sanitize_text_field((string) ($validations['date_type'] ?? ''));
                        if ($date_type !== '') {
                            $constraints[] = 'date_type: ' . $date_type;
                        }
                        if ($date_type === 'fixed_date') {
                            if (!empty($validations['min'])) {
                                $constraints[] = 'min_date: ' . sanitize_text_field((string) $validations['min']);
                            }
                            if (!empty($validations['max'])) {
                                $constraints[] = 'max_date: ' . sanitize_text_field((string) $validations['max']);
                            }
                        }
                        if ($date_type === 'relative_date') {
                            foreach (['relative_min', 'relative_max'] as $rel_key) {
                                if (!empty($validations[$rel_key]) && is_array($validations[$rel_key])) {
                                    $rel = $validations[$rel_key];
                                    $parts = [];
                                    foreach (['days', 'weeks', 'months', 'years'] as $unit) {
                                        $val = absint($rel[$unit] ?? 0);
                                        if ($val > 0) {
                                            $parts[] = $val . ' ' . $unit;
                                        }
                                    }
                                    $logic = sanitize_text_field((string) ($rel['date_logic'] ?? ''));
                                    $variable = sanitize_text_field((string) ($rel['date_variable'] ?? ''));
                                    $summary = $rel_key . ': ';
                                    if (!empty($parts)) {
                                        $summary .= implode(', ', $parts) . ' ';
                                    }
                                    if ($logic !== '') {
                                        $summary .= $logic . ' ';
                                    }
                                    if ($variable !== '') {
                                        $summary .= $variable;
                                    }
                                    $constraints[] = trim($summary);
                                }
                            }
                        }
                        $day_type = sanitize_text_field((string) ($validations['day_type'] ?? ''));
                        if ($day_type !== '') {
                            $constraints[] = 'day_type: ' . $day_type;
                        }
                    }
                }
                if (!empty($constraints)) {
                    $prompt .= "\n\nField date constraints (use these to write the hint):\n- " . implode("\n- ", $constraints);
                }
            }

            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => true,
                    'request_context' => $request_context,
                ]
            );

            return $this->dispatchAndWrap('generate_patch_plan', $form_id, $prompt, $provider, $config, $request);
        }

        if ($suggestion_id === 'page.polish_copy') {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $suggestion_meta = $this->decodeSuggestionMeta($suggestion_context['suggestion_meta'] ?? []);

            if (!empty($suggestion_meta['prompt_rules']) && is_array($suggestion_meta['prompt_rules'])) {
                $rule_lines = array_filter(array_map(static fn($rule) => trim((string) $rule), $suggestion_meta['prompt_rules']));
                if (!empty($rule_lines)) {
                    $prompt .= "\n\nPage copy rules:\n- " . implode("\n- ", $rule_lines);
                }
            }

            foreach ([
                'ideal_page_heading_examples' => 'Ideal page heading examples',
                'ideal_page_description_examples' => 'Ideal page description examples',
                'ideal_row_heading_examples' => 'Ideal row heading examples',
                'ideal_row_description_examples' => 'Ideal row description examples',
                'ideal_field_label_examples' => 'Ideal field label examples',
                'ideal_field_description_examples' => 'Ideal field description examples',
            ] as $meta_key => $label) {
                if (empty($suggestion_meta[$meta_key]) || !is_array($suggestion_meta[$meta_key])) {
                    continue;
                }

                $lines = array_filter(array_map(static fn($item) => trim((string) $item), $suggestion_meta[$meta_key]));
                if (!empty($lines)) {
                    $prompt .= "\n\n{$label}:\n- " . implode("\n- ", $lines);
                }
            }

            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => true,
                    'request_context' => $request_context,
                ]
            );

            return $this->dispatchAndWrap('generate_bulk_text_mutation', $form_id, $prompt, $provider, $config, $request);
        }

        if ($this->isSelectedFormThemeIntent($prompt, $selected_context, $request_context)) {
            $route = $this->request_router->routeConfig($request);
            $provider = (string) ($route['provider'] ?? '');
            $config = (array) ($route['config'] ?? []);
            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => false,
                    'request_context' => $request_context,
                ]
            );

            return $this->dispatchAndWrap('generate_form_theme', $form_id, $prompt, $provider, $config, $request);
        }

        if ($this->isSelectedFieldRemoveRequest($prompt, $selected_context)) {
            return $this->failure(
                'unsupported_prompt_key',
                422,
                [
                    'surface' => (string) ($request['surface'] ?? ''),
                    'chat_mode' => (string) ($request['response_mode'] ?? '') === 'text_only',
                ]
            );
        }

        if ($this->isSelectedFieldMoveRequest($prompt, $selected_context)) {
            return $this->failure(
                'unsupported_prompt_key',
                422,
                [
                    'surface' => (string) ($request['surface'] ?? ''),
                    'chat_mode' => (string) ($request['response_mode'] ?? '') === 'text_only',
                ],
                __('Moving a field is not supported yet. Try moving the row up instead.', 'griffinforms-form-builder')
            );
        }

        $option_column_count = $this->extractRequestedOptionColumnCount($prompt);
        if ($option_column_count === -1 && $this->isSelectedChoiceFieldColumnSplitIntent($prompt, $selected_context)) {
            return $this->failure(
                'bulk_text_validation_failed',
                422,
                [
                    'surface' => (string) ($request['surface'] ?? ''),
                    'chat_mode' => (string) ($request['response_mode'] ?? '') === 'text_only',
                ],
                __('Option columns must be a whole number between 1 and 4.', 'griffinforms-form-builder')
            );
        }

        if ($this->promptRequiresSelection($prompt) && empty($selected_context['item_type'])) {
            return $this->failure(
                'selection_required',
                409,
                [
                    'surface' => (string) ($request['surface'] ?? ''),
                    'chat_mode' => (string) ($request['response_mode'] ?? '') === 'text_only',
                ]
            );
        }

        $route = $this->request_router->routeConfig($request);
        $provider = (string) ($route['provider'] ?? '');
        $config = (array) ($route['config'] ?? []);

        $thinking_log = [];

        try {
            $config = array_merge(
                $config,
                [
                    'selected_context' => $selected_context,
                    'apply_patch_plan' => ($request['response_family'] ?? 'mutation') === 'mutation',
                    'request_context' => (array) ($request['request_context'] ?? []),
                ]
            );

            // Payment field prompt enrichment (non-suggestion_id path).
            // Skip when this is a product management prompt — generate_payment_products builds its own context.
            $is_payment_product_mutation = $this->isSelectedPaymentFieldIntent($prompt, $selected_context)
                && (bool) preg_match('/\b(add|remove|delete|rename|update|change|edit|set|create|price|featured|product|plan|tier|option)\b/i', strtolower(trim($prompt)));
            if ($this->isSelectedPaymentFieldIntent($prompt, $selected_context) && !$is_payment_product_mutation) {
                $payment_field_id = absint($selected_context['item_id'] ?? 0);
                $is_regenerate_intent = (bool) preg_match('/\b(regenerate|replace|rewrite|redo|redo|rebuild|recreate|create new|generate new|new products?|change products?|swap products?|tailor|adapt|update.*for|make.*for|suit(?:able)?\s+for)\b/i', $prompt)
                    || (bool) preg_match('/\bfor\s+(a|an|the|my)?\s*\w+\s+(business|company|shop|store|agency|firm|clinic|studio|service|industry)\b/i', $prompt)
                    || (bool) preg_match('/\b(business|company|shop|store|agency|firm|clinic|studio|service)\b.*\b(products?|items?|plans?|packages?|offerings?)\b/i', $prompt);
                if ($payment_field_id > 0) {
                    $format_sql = \GriffinForms\Admin\Sql\Format::getInstance();
                    $field_row = $format_sql->itemValues($payment_field_id, 'field', ['field_options']);
                    if (is_object($field_row)) {
                        $field_options = maybe_unserialize($field_row->field_options ?? '');
                        if (is_array($field_options) && !empty($field_options)) {
                            $product_count = count($field_options);
                            $product_lines = [];
                            foreach ($field_options as $product) {
                                if (!is_array($product)) {
                                    continue;
                                }
                                $product_name = sanitize_text_field((string) ($product['name'] ?? ''));
                                $product_desc = sanitize_text_field((string) ($product['description'] ?? ''));
                                $product_price = sanitize_text_field((string) ($product['price'] ?? ''));
                                if ($product_name !== '') {
                                    $summary = $product_name;
                                    if ($product_price !== '') {
                                        $summary .= ' (' . $product_price . ')';
                                    }
                                    if ($product_desc !== '') {
                                        $summary .= ': ' . $product_desc;
                                    }
                                    $product_lines[] = $summary;
                                }
                            }
                            if ($is_regenerate_intent) {
                                if (!empty($product_lines)) {
                                    $prompt .= "\n\nExisting products (for reference — you may replace names and descriptions to match the new context, but keep the same number of products: " . $product_count . "):\n- " . implode("\n- ", $product_lines);
                                }
                                $prompt .= "\n\nIMPORTANT: Generate " . $product_count . " new products suited to the requested context. You MUST change the product names and descriptions to reflect the new business type. Preserve the existing id and price values exactly. Return the full product array as field_options in the changes object. Example structure:\n{\"op\":\"update_field\",\"target\":{\"scope\":\"field\",\"item_id\":" . $payment_field_id . "},\"changes\":{\"field_options\":[{\"id\":\"...\",\"name\":\"new name here\",\"price\":\"...\",\"description\":\"new description here\"}]}}";
                            } else {
                                if (!empty($product_lines)) {
                                    $prompt .= "\n\nCurrent products (preserve all product fields exactly — only change what the prompt requests):\n- " . implode("\n- ", $product_lines);
                                }
                                $prompt .= "\n\nIMPORTANT: Return the full updated product array as field_options in the changes object. Each product must be a complete object with all original fields preserved (id, name, price, type, quantity, etc.) — only change what was requested. Example structure:\n{\"op\":\"update_field\",\"target\":{\"scope\":\"field\",\"item_id\":" . $payment_field_id . "},\"changes\":{\"field_options\":[{\"id\":\"...\",\"name\":\"...\",\"price\":\"...\",\"description\":\"...\"}]}}";
                            }
                        }
                    }
                }
            }

            // =====================================================================
            // Intent classification — IntentClassifier resolves handler key.
            // Pass provider + config in request_context so runModelLayer() can call
            // the broker if pattern matching fails.
            // =====================================================================
            $classifier_request_context = array_merge(
                $request_context,
                [
                    'provider'         => $provider,
                    'classifier_config' => $config,
                    'response_family'  => (string) ($request['response_family'] ?? 'mutation'),
                    'semantic_intent'  => (string) ($request['semantic_intent'] ?? ''),
                    'form_id'          => $form_id,
                ]
            );

            $classification = $this->classifier->classify($prompt, $selected_context, $classifier_request_context);
            $thinking_log[] = ['message' => __('Identifying what you want to change', 'griffinforms-form-builder'), 'phase' => 'classify'];

            // scope_mismatch: tell the user their prompt doesn't match the selection
            if ($classification->type === 'scope_mismatch') {
                return $this->failure(
                    'scope_mismatch',
                    422,
                    [
                        'surface' => (string) ($request['surface'] ?? ''),
                        'chat_mode' => (string) ($request['response_mode'] ?? '') === 'text_only',
                    ],
                    $classification->scope_mismatch_message
                );
            }

            // potential_paths: all model matches were low confidence (3.1.9.0 only)
            if ($classification->type === 'potential_paths') {
                $user_id = get_current_user_id();
                $token_paths = [];
                foreach ($classification->potential_paths as $path) {
                    $token = wp_generate_password(8, false);
                    $transient_key = "gf_assist_paths_{$user_id}_{$form_id}_{$token}";
                    set_transient($transient_key, ['handler_key' => $path['handler_key']], 300);
                    $token_paths[] = ['token' => $token, 'user_label' => $path['user_label']];
                }
                return [
                    'success'         => true,
                    'status'          => 'potential_paths',
                    'message'         => __("I wasn't sure what you meant. Did you want to:", 'griffinforms-form-builder'),
                    'potential_paths' => $token_paths,
                    'thinking_log'    => [],
                    'batch_status'    => [],
                ];
            }

            $prompt_key = $classification->handler_key;

            $this->log->add(
                'debug',
                'ai_butler_request',
                $form_id,
                (int) get_current_user_id(),
                sprintf(
                    'Butler routed prompt_key=%s surface=%s origin=%s response_mode=%s response_family=%s semantic_intent=%s prompt=%s selected_context=%s request_context=%s',
                    $prompt_key,
                    (string) ($request['surface'] ?? ''),
                    (string) ($request['origin'] ?? ''),
                    (string) ($request['response_mode'] ?? ''),
                    (string) ($request['response_family'] ?? ''),
                    (string) ($request['semantic_intent'] ?? ''),
                    sanitize_text_field((string) ($request['prompt'] ?? $prompt)),
                    wp_json_encode((array) $selected_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                    wp_json_encode((array) $request_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
                ),
                'ai_butler_request'
            );

            if (!$this->registry->has($prompt_key)) {
                return $this->failure(
                    'unsupported_prompt_key',
                    422,
                    [
                        'surface' => (string) ($request['surface'] ?? ''),
                        'chat_mode' => (string) ($request['response_mode'] ?? '') === 'text_only',
                    ]
                );
            }

            $handler = $this->registry->get($prompt_key);
            if ($handler === null) {
                return $this->failure('missing_handler', 500, [
                    'surface' => (string) ($request['surface'] ?? ''),
                    'chat_mode' => (string) ($request['response_mode'] ?? '') === 'text_only',
                ]);
            }

            $is_mutation = ($request['response_family'] ?? 'mutation') === 'mutation';
            if ($is_mutation) {
                $thinking_log[] = ['message' => __('Planning the edit', 'griffinforms-form-builder'), 'phase' => 'plan'];
                $thinking_log[] = ['message' => __('Applying changes', 'griffinforms-form-builder'), 'phase' => 'execute'];
            }

            // Execute via OperationPlan so batch_status is always present.
            $plan_item = new OperationItem($prompt_key, $config, $prompt_key);
            $plan_item->status = 'working';

            try {
                $handler_result = $handler->handle($form_id, $prompt, $provider, $config);
                $plan_item->result = $handler_result;
                $plan_item->status = !empty($handler_result['success']) ? 'done' : 'failed';
            } catch (\Throwable $item_err) {
                $plan_item->status = 'failed';
                $plan_item->result = ['success' => false, 'message' => sanitize_text_field($item_err->getMessage())];
                throw $item_err;
            }

            $batch_status = [[
                'handler_key' => $plan_item->handler_key,
                'label'       => $plan_item->label,
                'status'      => $plan_item->status,
                'result'      => $plan_item->result ?? [],
            ]];

            return array_merge(
                $plan_item->result ?? [],
                [
                    'thinking_log' => $thinking_log,
                    'batch_status' => $batch_status,
                ]
            );
        } catch (ButlerTimeoutException $e) {
            $this->log->add(
                'warning',
                'ai_timeout',
                $form_id,
                (int) get_current_user_id(),
                'Griffin Assist timeout: ' . sanitize_text_field($e->getMessage()),
                'ai_timeout'
            );

            return $this->failure(
                'timeout',
                408,
                [
                    'surface' => (string) ($request['surface'] ?? ''),
                    'chat_mode' => (string) ($request['response_mode'] ?? '') === 'text_only',
                ]
            );
        } catch (AiBrokerError $e) {
            $safe_message = $this->redactProviderErrorMessage($e->getMessage());
            $provider_message = $this->resolveProviderFailureMessage(
                (string) ($provider ?? ''),
                (int) $e->getCode(),
                $safe_message
            );

            $this->log->add(
                'error',
                'ai_broker',
                $form_id,
                (int) get_current_user_id(),
                'Griffin Assist broker error: ' . $safe_message,
                'ai_broker'
            );

            return $this->failure(
                'provider_error',
                (int) $e->getCode(),
                [
                    'surface' => (string) ($request['surface'] ?? ''),
                    'chat_mode' => (string) ($request['response_mode'] ?? '') === 'text_only',
                ],
                $provider_message
            );
        } catch (\Throwable $throwable) {
            $this->log->add(
                'error',
                'ai_pipeline',
                $form_id,
                (int) get_current_user_id(),
                'Griffin Assist pipeline failure: ' . sanitize_text_field($throwable->getMessage()),
                'ai_pipeline'
            );

            return $this->failure(
                'pipeline_error',
                500,
                [
                    'surface' => (string) ($request['surface'] ?? ''),
                    'chat_mode' => (string) ($request['response_mode'] ?? '') === 'text_only',
                ]
            );
        }
    }

    protected function classifyPromptKey(string $provider, array $config, string $prompt, array $selected_context = []): string
    {
        $normalized_prompt = strtolower(trim($prompt));
        if (
            (preg_match('/\b(page headings?|page heading|page descriptions?|page description|page titles?|page title|headings?|heading|descriptions?|description|titles?|title|field labels?|placeholders?|copy|form|text)\b/i', $normalized_prompt)
            && preg_match('/\b(change|translate|rewrite|align|normalize|update|make|convert|turn|polish|improve|edit|refine)\b/i', $normalized_prompt))
            || preg_match('/\b(translate|convert|turn|make)\b.*\b(to|into|in)\b.*\b(french|spanish|german|deutsch|italian|portuguese|dutch|hindi|punjabi|panjabi|french|francais|fr|es|de|it|pt|nl|hi|pa)\b/i', $normalized_prompt)
        ) {
            return 'generate_bulk_text_mutation';
        }

        if ($this->isSelectedChoiceFieldOptionTextIntent($normalized_prompt, $selected_context)) {
            return 'generate_bulk_text_mutation';
        }

        $selection_text = '';
        if (!empty($selected_context['item_type'])) {
            $selection_text = "\nSelected element context: " . wp_json_encode($selected_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        }

        $request_context = (array) ($config['request_context'] ?? []);
        $routing_text = "\nRouting context: " . wp_json_encode([
            'surface' => (string) ($request_context['surface'] ?? ''),
            'origin' => (string) ($request_context['origin'] ?? ''),
            'response_mode' => (string) ($request_context['response_mode'] ?? ''),
            'response_family' => (string) ($request_context['response_family'] ?? ''),
            'semantic_intent' => (string) ($request_context['semantic_intent'] ?? ''),
        ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        $site_text = '';
        if (!empty($request_context['site_context']) && is_array($request_context['site_context'])) {
            $site_text = "\nSite context: " . wp_json_encode($request_context['site_context'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        }

        $response = $this->broker->requestStrict(
            $provider,
            "Classify this GriffinForms request. Return JSON only.\nRequest: " . $prompt . $selection_text . $routing_text . $site_text,
            ['system' => $this->getClassifierSystemPrompt()],
            array_merge($config, ['timeout' => 5])
        );

        [$decoded, $extract_error] = $this->extractor->extract((string) ($response['content'] ?? ''));
        if ($extract_error !== null || !is_array($decoded)) {
            return 'unsupported';
        }

        return sanitize_key((string) ($decoded['prompt_key'] ?? 'unsupported'));
    }

    protected function isSelectedFormCopyIntent(string $prompt, array $selected_context = [], array $request_context = []): bool
    {
        $normalized = strtolower(trim($prompt));
        $suggestion_context = is_array($request_context['suggestion_context'] ?? null) ? (array) $request_context['suggestion_context'] : [];
        $suggestion_id = $this->normalizeSuggestionId((string) ($suggestion_context['suggestion_id'] ?? ''));
        if ($suggestion_id === 'form.improve_description') {
            return true;
        }
        if (!preg_match('/\b(polish|improve|rewrite|update|refine|edit|adjust|tweak|enhance)\b/', $normalized)) {
            return false;
        }

        if (!preg_match('/\b(form copy|form text|form wording|form content|form description|form heading|submit button|submit label|submit text|copy)\b/', $normalized)) {
            return false;
        }

        return true;
    }

    protected function isSelectedFormThemeIntent(string $prompt, array $selected_context = [], array $request_context = []): bool
    {
        $suggestion_context = is_array($request_context['suggestion_context'] ?? null) ? (array) $request_context['suggestion_context'] : [];
        $suggestion_id = $this->normalizeSuggestionId((string) ($suggestion_context['suggestion_id'] ?? ''));
        if ($suggestion_id === 'form.apply_random_theme') {
            return true;
        }

        $normalized = strtolower(trim($prompt));
        if (!preg_match('/\b(apply|change|switch|pick|choose|select|random)\b.*\b(theme|style|look|design|appearance)\b|\b(theme|style|look|design|appearance)\b.*\b(apply|change|switch|pick|choose|select|random)\b/', $normalized)) {
            return false;
        }

        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        return in_array($selected_type, ['form', 'selected', ''], true);
    }

    protected function getClassifierSystemPrompt(): string
    {
        return implode("\n", [
            'You classify GriffinForms builder requests.',
            'Return JSON only in this exact shape: {"prompt_key":"generate_patch_plan"}, {"prompt_key":"generate_foundation"}, or {"prompt_key":"unsupported"}',
            'Only classify mutation-capable builder requests. Advisory or text-only requests are routed earlier and should not reach this classifier.',
            'Choose "generate_patch_plan" for surgical incremental changes (add field, insert page, move row, remove element, rename label/page/section). This is the preferred mode for most specific user requests.',
            'Choose "generate_foundation" only for broad structural generation (e.g., "Create a contact form", "I need a multi-page survey") where the user expects a broad new structure or a complete rebuild.',
            'If a request is an incremental add or edit, always pick "generate_patch_plan".',
            'Choose "unsupported" for styling, integrations, payments setup, conditional logic, or non-structural requests.',
        ]);
    }

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

        return (bool) preg_match(
            '/^(add|set|update|change|edit|rename|remove|delete|rewrite|make|improve)\s+(heading|title|description|label|placeholder|help text|instructions|subtext)\b|\b(this|selected|current)\b|\bfor this\b|\bon this\b|\b(heading|title|description|label|placeholder|help text|instructions|subtext)\b.*\b(more|better|warmer|friendlier|clearer|clear)\b/',
            $normalized
        );
    }

    protected function isSelectedFieldPlaceholderIntent(string $prompt, array $selected_context): bool
    {
        $normalized = strtolower(trim($prompt));
        if (!preg_match('/\bplaceholder\b/', $normalized)) {
            return false;
        }

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

    protected function isSelectedSelectFieldOptionsIntent(string $prompt, array $selected_context): bool
    {
        $normalized = strtolower(trim($prompt));
        if (!preg_match('/\b(option|options|dropdown|select|choice|choices|refine options|improve options|polish options|rewrite options|option groups?|grouped options?)\b/', $normalized)) {
            return false;
        }

        if (sanitize_key((string) ($selected_context['item_type'] ?? '')) !== 'field') {
            return false;
        }

        $field_type = sanitize_key((string) ($selected_context['field_type'] ?? ''));
        if ($field_type === 'selectfield') {
            return !empty($selected_context['item_id']);
        }

        $field_id = absint($selected_context['item_id'] ?? 0);
        if ($field_id > 0 && $field_type === '') {
            $format_sql = \GriffinForms\Admin\Sql\Format::getInstance();
            $field_row = $format_sql->itemValues($field_id, 'field', ['field_type']);
            if (is_object($field_row) && sanitize_key((string) ($field_row->field_type ?? '')) === 'selectfield') {
                return true;
            }
        }

        return false;
    }

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

        $normalized = strtolower(trim($prompt));
        $row_like_context = in_array(sanitize_key((string) ($selected_context['item_type'] ?? '')), ['pagerow', 'row', 'rowcolumn'], true)
            || in_array(sanitize_key((string) ($selected_context['parent_type'] ?? '')), ['pagerow', 'row'], true)
            || (!empty($selected_context['parent_id']) && !empty($selected_context['item_id']));
        $has_any_selection = !empty($selected_context['item_id']) || !empty($selected_context['parent_id']) || !empty($selected_context['form_id']);

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

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

        if ($selected_type === 'field' && $this->isSelectedFieldOptionLayoutIntent($normalized)) {
            return true;
        }

        if ($selected_type === 'field' && preg_match('/\b(remove|delete|clear)\b.*\b(field|this|selected|current)\b|\b(field|this|selected|current)\b.*\b(remove|delete|clear)\b/', $normalized)) {
            return true;
        }

        if ($selected_type === 'field' && preg_match('/\b(move|shift|reorder|relocate)\b/', $normalized)) {
            return true;
        }

        if (($row_like_context || $has_any_selection) && preg_match('/\b(all|every|each|bulk)\b.*\b(row|rows|section|sections)\b.*\b(heading|headings|title|titles)\b/', $normalized)) {
            return true;
        }

        if ($row_like_context && preg_match('/\b(delete|remove|clear)\b/', $normalized)) {
            return true;
        }

        if ($row_like_context && preg_match('/\b(move|shift|reorder)\b(\s+row)?\s+\b(up|down|above|below|before|after|earlier|later|previous|next|top|bottom)\b/', $normalized)) {
            return true;
        }

        if ($row_like_context && preg_match('/\b(rewrite|change|update|edit|improve|make|set|add|generate|turn)\b.*\b(heading|title|description|help text|instructions|subtext)\b/', $normalized)) {
            return true;
        }

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

    protected function isLikertRowColumnIntent(string $normalized_prompt, array $selected_context): bool
    {
        // Explicit Likert/matrix keywords always route to patch plan regardless of verb
        if (preg_match('/\b(likert|row labels?|column labels?|matrix rows?|matrix columns?)\b/', $normalized_prompt)) {
            return true;
        }

        $field_type = sanitize_key((string) ($selected_context['field_type'] ?? ''));
        if ($field_type !== 'inputlikert') {
            return false;
        }

        // Any row/column manipulation with a Likert field selected
        if (preg_match('/\b(rows?|columns?|labels?)\b/', $normalized_prompt)) {
            return true;
        }

        return false;
    }

    protected function isSelectedChoiceFieldOptionTextIntent(string $normalized_prompt, array $selected_context): bool
    {
        if (!preg_match('/\b(change|translate|rewrite|align|normalize|update|make|convert|turn|polish|improve|edit|refine)\b/', $normalized_prompt)) {
            return false;
        }

        if (!preg_match('/\b(option label|option labels|options|choices|choice|radio option|radio options|checkbox option|checkbox options)\b/', $normalized_prompt)) {
            return false;
        }

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

        $field_type = sanitize_key((string) ($selected_context['field_type'] ?? ''));
        return in_array($field_type, ['selectfield', 'inputcheckbox', 'inputradio'], true);
    }

    protected function isSelectedFieldOptionLayoutIntent(string $normalized_prompt): bool
    {
        if (!preg_match('/\b(option|options|choice|choices|radio|checkbox|column|columns)\b/', $normalized_prompt)) {
            return false;
        }

        return (bool) preg_match(
            '/\b(divide|split|arrange|distribute|spread|organize|group|regroup|place|put|move|reorder|re-arrange|rearrange|balance)\b/',
            $normalized_prompt
        );
    }

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

        $normalized = strtolower(trim($prompt));
        return (bool) preg_match('/\b(remove|delete|clear)\b.*\b(field|this|selected|current)\b|\b(field|this|selected|current)\b.*\b(remove|delete|clear)\b/', $normalized);
    }

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

        $normalized = strtolower(trim($prompt));
        return (bool) preg_match(
            '/\b(move|shift|reorder|relocate|put|place)\b.*\b(field|this|selected|current)\b|\b(field|this|selected|current)\b.*\b(move|shift|reorder|relocate|put|place)\b|\b(field|this|selected|current)\b.*\b(up|down|top|bottom|before|after|above|below)\b|\b(to\s+the\s+)?(top|bottom|beginning|end)\b/',
            $normalized
        );
    }

    protected function isFieldGrowthPrompt(string $normalized_prompt): bool
    {
        return (bool) preg_match('/\b(add|append|insert|include|another|extra|additional|new)\b.*\b(field|fields|input|inputs|question|questions)\b|\b(field|fields|input|inputs|question|questions)\b.*\b(add|append|insert|include|another|extra|additional|new)\b/', $normalized_prompt);
    }

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

        $field_type = sanitize_key((string) ($selected_context['field_type'] ?? ''));
        if ($field_type !== 'payment') {
            $field_id = absint($selected_context['item_id'] ?? 0);
            if ($field_id <= 0) {
                return false;
            }
            $format_sql = \GriffinForms\Admin\Sql\Format::getInstance();
            $field_row = $format_sql->itemValues($field_id, 'field', ['field_type']);
            $db_field_type = is_object($field_row) ? sanitize_key((string) ($field_row->field_type ?? '')) : '';
            if ($db_field_type !== 'payment') {
                return false;
            }
        }

        return true;
    }

    protected function isSelectedChoiceFieldColumnSplitIntent(string $prompt, array $selected_context): bool
    {
        $normalized = strtolower(trim($prompt));
        if (!preg_match('/\b(divide|split|spread|arrange|distribute|organize|reorganize|re-arrange|rearrange|group|group\s+into|grouping|section|sections)\b.*\b(options?|choices?)\b.*\b(columns?|col(?:umn)?s?|sections?)\b|\b(options?|choices?)\b.*\b(columns?|col(?:umn)?s?|sections?)\b.*\b(divide|split|spread|arrange|distribute|organize|reorganize|re-arrange|rearrange|group|group\s+into|grouping|section|sections)\b/', $normalized)) {
            return false;
        }

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

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

        $field_id = absint($selected_context['item_id'] ?? 0);
        if ($field_id > 0) {
            $format_sql = \GriffinForms\Admin\Sql\Format::getInstance();
            $field_row = $format_sql->itemValues($field_id, 'field', ['field_type']);
            if (is_object($field_row)) {
                $db_field_type = sanitize_key((string) ($field_row->field_type ?? ''));
                return in_array($db_field_type, ['inputcheckbox', 'inputradio'], true);
            }
        }

        return false;
    }

    protected function isSelectedFieldLabelIntent(string $prompt, array $selected_context): bool
    {
        if (sanitize_key((string) ($selected_context['item_type'] ?? '')) !== 'field' || empty($selected_context['item_id'])) {
            return false;
        }

        $normalized = strtolower(trim($prompt));
        if (!preg_match('/\b(field\s+)?label\b/', $normalized)) {
            return false;
        }

        return (bool) preg_match('/\b(rewrite|improve|update|change|edit|refine|polish|turn)\b/', $normalized);
    }

    protected function extractRequestedOptionColumnCount(string $prompt): ?int
    {
        $normalized = strtolower(trim($prompt));
        if (!preg_match('/\b(?:divide|split|spread|arrange|distribute|organize|group|group\s+into|grouping|section|sections)\b.*\b(?:field\s+)?(?:options?|choices?)\b.*\b(?:into\s+)?(?:([1-9]\d*)|one|two|three|four)\b.*\b(?:columns?|sections?)\b|\b(?:[1-9]\d*|one|two|three|four)\b.*\b(?:columns?|sections?)\b/', $normalized, $matches)) {
            return null;
        }

        $raw = strtolower(trim((string) ($matches[1] ?? '')));
        if ($raw === '') {
            if (preg_match('/\b(one|two|three|four)\b/', $normalized, $word_matches)) {
                $raw = (string) ($word_matches[1] ?? '');
            } elseif (preg_match('/\b([1-9]\d*)\b/', $normalized, $number_matches)) {
                $raw = (string) ($number_matches[1] ?? '');
            }
        }

        $map = [
            'one' => 1,
            'two' => 2,
            'three' => 3,
            'four' => 4,
        ];

        if (isset($map[$raw])) {
            return $map[$raw];
        }

        $value = absint($raw);
        if ($value < 1 || $value > 4) {
            return -1;
        }

        return $value;
    }

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

        if (!empty($selected_context['item_id']) || !empty($selected_context['parent_id']) || !empty($selected_context['form_id'])) {
            return 'selected';
        }

        return '';
    }

    protected function maybeBlockUnavailablePackageFieldRequest(array $request, string $prompt, array $selected_context): ?array
    {
        $prompt = strtolower(trim($prompt));
        $field_type = $this->resolveRequestedFieldType($prompt, $selected_context);
        if ($field_type === '') {
            return null;
        }

        $package_id = sanitize_key((string) ($this->extension_registry->getPackageIdForFieldType($field_type) ?: ''));
        if ($package_id === '' || $package_id === 'core') {
            return null;
        }

        if (!$this->shouldEnforceFieldPackageCheck($prompt, $selected_context, $field_type)) {
            return null;
        }

        $field_extension = $this->extension_registry->getForFieldType($field_type);
        $package_name = trim((string) ($field_extension['name'] ?? ''));
        if ($package_name === '') {
            $package_name = $this->extension_registry->getName($package_id);
        }

        $package_state = $this->package_admission->getPackageState($package_id);
        $require_entitlement = !empty($package_state['supports_entitlement']) || !empty($field_extension['requires_entitlement']);
        if ($this->package_admission->canAdmitPackage($package_id, $require_entitlement)) {
            return null;
        }

        $blocking_reason = (string) ($this->package_admission->getBlockingReasonKey($package_id, $require_entitlement) ?? '');
        $message = $this->buildPackageFieldUnavailableMessage($field_type, $package_name, $blocking_reason, $package_state);

        return $this->failure(
            'package_unavailable',
            422,
            [
                'surface' => (string) ($request['surface'] ?? ''),
                'chat_mode' => (string) ($request['response_mode'] ?? '') === 'text_only',
                'request_context' => (array) ($request['request_context'] ?? []),
            ],
            $message
        );
    }

    protected function shouldEnforceFieldPackageCheck(string $prompt, array $selected_context, string $field_type): bool
    {
        if (($selected_context['item_type'] ?? '') === 'field' && sanitize_key((string) ($selected_context['field_type'] ?? '')) === $field_type) {
            return true;
        }

        return $this->isFieldMutationPrompt($prompt) || $this->isFieldAddPrompt($prompt, $field_type);
    }

    protected function isFieldMutationPrompt(string $prompt): bool
    {
        return (bool) preg_match('/\b(add|insert|create|configure|update|change|edit|improve|polish|rewrite|build|set up|setup|add a|add an)\b/', $prompt);
    }

    protected function isFieldAddPrompt(string $prompt, string $field_type): bool
    {
        $aliases = $this->getFieldTypeAliases($field_type);
        if (empty($aliases)) {
            return false;
        }

        foreach ($aliases as $alias) {
            $alias = trim(strtolower($alias));
            if ($alias === '') {
                continue;
            }

            if (preg_match('/\b' . preg_quote($alias, '/') . '\b/', $prompt)) {
                return true;
            }
        }

        return false;
    }

    protected function resolveRequestedFieldType(string $prompt, array $selected_context): string
    {
        $selected_type = sanitize_key((string) ($selected_context['item_type'] ?? ''));
        $selected_field_type = sanitize_key((string) ($selected_context['field_type'] ?? ''));
        if ($selected_type === 'field' && $selected_field_type !== '') {
            return $selected_field_type;
        }

        $normalized = strtolower(trim($prompt));
        if ($normalized === '') {
            return '';
        }

        foreach ($this->getKnownFieldTypeAliases() as $field_type => $aliases) {
            foreach ($aliases as $alias) {
                $alias = trim(strtolower($alias));
                if ($alias === '') {
                    continue;
                }

                if (preg_match('/\b' . preg_quote($alias, '/') . '\b/', $normalized)) {
                    return sanitize_key($field_type);
                }
            }
        }

        return '';
    }

    protected function getKnownFieldTypeAliases(): array
    {
        return [
            'inputtext' => ['text field', 'text', 'name', 'single line text'],
            'inputemail' => ['email field', 'email address', 'email'],
            'inputphone' => ['phone field', 'phone number', 'telephone', 'phone'],
            'inputaddress' => ['address field', 'mailing address', 'billing address', 'shipping address', 'address'],
            'textarea' => ['textarea', 'text area', 'message', 'comment', 'notes', 'long text'],
            'selectfield' => ['dropdown', 'select field', 'select'],
            'inputcheckbox' => ['checkbox', 'checkbox field', 'check box'],
            'inputradio' => ['radio', 'radio field', 'single choice', 'choice'],
            'inputnumber' => ['number field', 'number', 'amount', 'quantity'],
            'inputdate' => ['date field', 'date', 'calendar date'],
            'fileupload' => ['file upload', 'upload field', 'attachment', 'file'],
            'signature' => ['signature field', 'signature', 'sign here', 'e-signature'],
            'inputrating' => ['rating field', 'rating', 'star rating', 'score'],
            'inputrichtext' => ['rich text field', 'rich text', 'formatted text', 'wysiwyg', 'editor'],
            'inputlikert' => ['likert field', 'likert scale', 'matrix question', 'likert'],
            'imageselection' => ['image selection field', 'image choice', 'image picker', 'visual selector'],
        ];
    }

    protected function getFieldTypeAliases(string $field_type): array
    {
        $field_type = sanitize_key($field_type);
        return (array) ($this->getKnownFieldTypeAliases()[$field_type] ?? []);
    }

    protected function buildPackageFieldUnavailableMessage(string $field_type, string $package_name, string $blocking_reason, array $package_state): string
    {
        $field_label = $this->humanizeFieldTypeForMessage($field_type);
        $package_name = $package_name !== '' ? $package_name : __('the required package', 'griffinforms-form-builder');

        $state_phrase = match ($blocking_reason) {
            'package_not_installed' => __('not installed', 'griffinforms-form-builder'),
            'package_inactive' => __('installed but inactive', 'griffinforms-form-builder'),
            'package_unapproved' => __('not approved for this installation', 'griffinforms-form-builder'),
            'package_unavailable', 'package_unknown' => __('currently unavailable', 'griffinforms-form-builder'),
            'verified_inactive_or_expired', 'key_not_entered', 'unverified_connection_issue', 'grace_period' => __('installed, but its entitlement is not active', 'griffinforms-form-builder'),
            'incompatible_core_too_old', 'incompatible_package_too_old', 'missing_required_core' => __('not compatible with this site', 'griffinforms-form-builder'),
            default => __('not available', 'griffinforms-form-builder'),
        };

        $message = sprintf(
            /* translators: 1: field label, 2: package name, 3: package state phrase */
            __('%1$s requires %2$s, which is %3$s. Install or activate %2$s to continue.', 'griffinforms-form-builder'),
            $field_label,
            $package_name,
            $state_phrase
        );

        $recommended_action = (string) ($package_state['recommended_user_action'] ?? '');
        if ($recommended_action !== '') {
            $message .= ' ' . sprintf(__('Recommended action: %s.', 'griffinforms-form-builder'), $this->humanizePackageAction($recommended_action));
        }

        return $message;
    }

    protected function humanizeFieldTypeForMessage(string $field_type): string
    {
        return match (sanitize_key($field_type)) {
            'signature' => __('Signature field', 'griffinforms-form-builder'),
            'inputrating' => __('Rating field', 'griffinforms-form-builder'),
            'inputrichtext' => __('Rich text field', 'griffinforms-form-builder'),
            'inputlikert' => __('Likert field', 'griffinforms-form-builder'),
            'imageselection' => __('Image selection field', 'griffinforms-form-builder'),
            default => __('Field', 'griffinforms-form-builder'),
        };
    }

    protected function humanizePackageAction(string $action): string
    {
        return match (sanitize_key($action)) {
            'install_pro' => __('install GriffinForms Pro', 'griffinforms-form-builder'),
            'activate_pro' => __('activate GriffinForms Pro', 'griffinforms-form-builder'),
            'enter_license_key' => __('enter the Pro license key', 'griffinforms-form-builder'),
            'reactivate_license' => __('reactivate the Pro license', 'griffinforms-form-builder'),
            'retry_license_verification' => __('retry license verification', 'griffinforms-form-builder'),
            default => $action !== '' ? $action : __('review the package status', 'griffinforms-form-builder'),
        };
    }

    protected function failure(string $status, int $code, array $context = [], ?string $message_override = null): array
    {
        return $this->failure_framework->build($status, $code, $context, $message_override);
    }

    protected function redactProviderErrorMessage(string $message): string
    {
        $message = preg_replace('/Incorrect API key provided:\s*([^\s".,]+)/i', 'Incorrect API key provided: [redacted]', $message);
        $message = preg_replace('/Bearer\s+[A-Za-z0-9_\-\.=]+/i', 'Bearer [redacted]', $message);
        return sanitize_text_field((string) $message);
    }

    protected function resolveProviderFailureMessage(string $provider, int $code, string $safe_message): ?string
    {
        $provider = sanitize_key($provider);
        $message_lc = strtolower($safe_message);

        if (
            $provider === 'gemini'
            && $code === 429
            && (
                str_contains($message_lc, 'resource_exhausted')
                || str_contains($message_lc, 'quota exceeded')
                || str_contains($message_lc, 'rate limit')
            )
        ) {
            return __('Google Gemini quota exceeded for this API project. Check billing/quota and retry.', 'griffinforms-form-builder');
        }

        return null;
    }

    protected function normalizeSuggestionId(string $suggestion_id): string
    {
        $suggestion_id = strtolower(trim($suggestion_id));
        if ($suggestion_id === '') {
            return '';
        }

        return preg_replace('/[^a-z0-9._-]/', '', $suggestion_id) ?: '';
    }

    protected function decodeSuggestionMeta($suggestion_meta_raw): array
    {
        if (is_string($suggestion_meta_raw) && $suggestion_meta_raw !== '') {
            $decoded_meta = json_decode($suggestion_meta_raw, true);
            if (is_array($decoded_meta)) {
                return $decoded_meta;
            }
        } elseif (is_array($suggestion_meta_raw)) {
            return $suggestion_meta_raw;
        }

        return [];
    }

    /**
     * Execute a pre-resolved handler key directly, bypassing classification.
     * Used by the path-token resolution flow (Issue 3).
     */
    public function executeResolved(int $form_id, string $handler_key, array $selected_context = [], array $request_context = []): array
    {
        $handler_key = sanitize_key($handler_key);
        if ($handler_key === '' || !$this->registry->has($handler_key)) {
            return $this->failure('unsupported_prompt_key', 422, [
                'surface'   => 'form_builder',
                'chat_mode' => false,
            ]);
        }

        $request = $this->request_router->buildRequest($form_id, $handler_key, $selected_context, $request_context);
        $route   = $this->request_router->routeConfig($request);
        $provider = (string) ($route['provider'] ?? '');
        $config   = array_merge(
            (array) ($route['config'] ?? []),
            [
                'selected_context' => $selected_context,
                'apply_patch_plan' => true,
                'request_context'  => $request_context,
            ]
        );

        return $this->dispatchAndWrap($handler_key, $form_id, $handler_key, $provider, $config, $request);
    }

    /**
     * Fetches handler by key, executes it, and wraps the result with 3.1.8.0 scaffolding
     * keys (thinking_log, batch_status). Used by all early-return suggestion_id blocks.
     */
    protected function dispatchAndWrap(
        string $handler_key,
        int $form_id,
        string $prompt,
        string $provider,
        array $config,
        array $request
    ): array {
        if (!$this->registry->has($handler_key)) {
            return $this->failure('missing_handler', 500, [
                'surface'   => (string) ($request['surface'] ?? ''),
                'chat_mode' => (string) ($request['response_mode'] ?? '') === 'text_only',
            ]);
        }
        $handler = $this->registry->get($handler_key);
        if ($handler === null) {
            return $this->failure('missing_handler', 500, [
                'surface'   => (string) ($request['surface'] ?? ''),
                'chat_mode' => (string) ($request['response_mode'] ?? '') === 'text_only',
            ]);
        }
        $dispatch_thinking_log = [
            ['message' => __('Identifying what you want to change', 'griffinforms-form-builder'), 'phase' => 'classify'],
            ['message' => __('Planning the edit', 'griffinforms-form-builder'), 'phase' => 'plan'],
            ['message' => __('Applying changes', 'griffinforms-form-builder'), 'phase' => 'execute'],
        ];
        $result = $handler->handle($form_id, $prompt, $provider, $config);
        $status = !empty($result['success']) ? 'done' : 'failed';
        return array_merge(
            $result,
            [
                'thinking_log' => $dispatch_thinking_log,
                'batch_status' => [[
                    'handler_key' => $handler_key,
                    'label'       => $handler_key,
                    'status'      => $status,
                    'result'      => $result,
                ]],
            ]
        );
    }
}
