<?php

namespace GriffinForms\Includes\Ai\PromptRegistry\Handlers;

use GriffinForms\Admin\Sql\Format;

class GenerateFieldOptionsPromptHandler extends BasePromptHandler
{
    protected const MAX_REFLECTION_RETRIES = 1;

    protected const LIKERT_MAX_ROWS    = 20;
    protected const LIKERT_MAX_COLUMNS = 10;
    protected const LIKERT_MIN_ROWS    = 1;
    protected const LIKERT_MIN_COLUMNS = 2;

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

    public function getUnsupportedMessage(): string
    {
        return __('I can only manage options for select, radio, checkbox, or likert fields.', 'griffinforms-form-builder');
    }

    public function handle(int $form_id, string $prompt, string $provider, array $config): array
    {
        $selected_context = is_array($config['selected_context'] ?? null) ? (array) $config['selected_context'] : [];
        $request_context  = is_array($config['request_context'] ?? null) ? (array) $config['request_context'] : [];

        $field_id   = absint($selected_context['item_id'] ?? 0);
        $field_type = sanitize_key((string) ($selected_context['field_type'] ?? ''));

        if ($field_id <= 0 || !in_array($field_type, ['selectfield', 'inputradio', 'inputcheckbox', 'inputlikert'], true)) {
            return $this->formatFailure(
                __('Please select a select, radio, checkbox, or likert field before asking for option changes.', 'griffinforms-form-builder'),
                'selection_required',
                409,
                ['request_context' => $request_context]
            );
        }

        $format_sql   = Format::getInstance();
        $is_likert    = $field_type === 'inputlikert';

        if ($is_likert) {
            return $this->handleLikert($form_id, $field_id, $prompt, $provider, $config, $format_sql, $selected_context, $request_context);
        }

        return $this->handleFieldOptions($form_id, $field_id, $field_type, $prompt, $provider, $config, $format_sql, $selected_context, $request_context);
    }

    // -------------------------------------------------------------------------
    // select / radio / checkbox branch
    // -------------------------------------------------------------------------

    protected function handleFieldOptions(int $form_id, int $field_id, string $field_type, string $prompt, string $provider, array $config, Format $format_sql, array $selected_context, array $request_context): array
    {
        $field = $format_sql->itemValues($field_id, 'field', ['heading', 'field_type', 'field_options']);
        $current_options = $this->decodeValue($field->field_options ?? null);
        if (!is_array($current_options)) {
            $current_options = [];
        }

        $flat_count = 0;
        foreach ($current_options as $group_options) {
            if (is_array($group_options)) {
                $flat_count += count($group_options);
            }
        }
        if ($flat_count <= 1 && $this->looksLikeRemoveIntent($prompt)) {
            return $this->formatFailure(
                __('This field only has one option, so it cannot be removed.', 'griffinforms-form-builder'),
                'cannot_remove_last_option',
                409,
                ['request_context' => $request_context]
            );
        }

        $last_errors = [];
        $raw_content = '';
        for ($attempt = 0; $attempt <= self::MAX_REFLECTION_RETRIES; $attempt++) {
            $prompt_text = $attempt === 0
                ? $this->buildFieldOptionsUserPrompt($prompt, $field, $field_type, $current_options, $selected_context, $request_context)
                : $this->buildReflectionPrompt($prompt, $raw_content, $last_errors, 'field_options');

            $response    = $this->requestStrict(
                $provider,
                $prompt_text,
                ['system' => $this->buildFieldOptionsSystemPrompt($field_type)],
                array_merge($config, ['timeout' => $attempt === 0 ? 22 : 14])
            );

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

            $validation_errors = [];
            if ($extract_error !== null) {
                $validation_errors[] = $extract_error;
            } elseif (!is_array($decoded)) {
                $validation_errors[] = __('Field options response must decode to an object.', 'griffinforms-form-builder');
            } else {
                $validation_errors = $this->validateFieldOptionsPayload($decoded);
            }

            if (empty($validation_errors) && is_array($decoded)) {
                $new_options = $this->normalizeFieldOptions((array) ($decoded['field_options'] ?? []));
                if (empty($new_options)) {
                    $validation_errors[] = __('Field options response must include at least one valid option.', 'griffinforms-form-builder');
                } else {
                    $format_sql->updateItemValue('field', $field_id, 'field_options', maybe_serialize($new_options));
                    return [
                        'success'       => true,
                        'status'        => 'applied',
                        'message'       => $this->normalizeText((string) ($decoded['success_message'] !== '' ? ($decoded['success_message'] ?? '') : '') ?: __('Updated the field options.', 'griffinforms-form-builder'), 140),
                        'mutated_ids'   => [$field_id],
                        'prompt_key'    => $this->getPromptKey(),
                        'patch_plan'    => [],
                        'patch_summary' => [],
                    ];
                }
            }

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

        return $this->formatFailure(
            __('I could not update the field options safely. Try being more specific about what to change.', 'griffinforms-form-builder'),
            'field_options_validation_failed',
            422,
            ['request_context' => $request_context]
        );
    }

    protected function buildFieldOptionsSystemPrompt(string $field_type): string
    {
        return implode("\n", [
            'You are Griffin Assist managing options for a ' . $field_type . ' field in GriffinForms.',
            'Return JSON only. No markdown, no code fences.',
            'Exact shape: {"field_options":[{"label":"Option One","value":"option_one","group":""},{"label":"Option Two","value":"option_two","group":""}],"success_message":"Updated the options."}',
            'field_options is a flat JSON array. Each element is an object with exactly three string keys:',
            '  "label"  — the human-readable display text shown to the user (e.g. "Prefer not to say").',
            '  "value"  — the stored slug: lowercase letters, digits, underscores only (e.g. "prefer_not_to_say").',
            '  "group"  — the option group name, or empty string "" for ungrouped (most common).',
            '',
            'CRITICAL RULES — read carefully:',
            '1. You MUST return every existing option in the output, preserving label, value, and group exactly.',
            '2. When ADDING an option: include all existing options first, then append the new one(s).',
            '3. When RENAMING a label: keep "value" identical — only change "label".',
            '4. When REMOVING: include every other option; omit only the one(s) explicitly removed.',
            '5. When REORDERING: include every option; only change their position in the array.',
            '6. Never change existing "value" slugs. New values must be lowercase snake_case.',
            '7. Return only field_options (array) and success_message. No other keys.',
        ]);
    }

    protected function buildFieldOptionsUserPrompt(string $prompt, $field, string $field_type, array $current_options, array $selected_context, array $request_context): string
    {
        $flat_options = $this->flattenOptionsForDisplay($current_options);

        return implode("\n", [
            'User request: ' . $prompt,
            '',
            'Field: ' . sanitize_text_field((string) ($field->heading ?? '')) . ' (' . $field_type . ')',
            '',
            'EXISTING OPTIONS — you must include ALL of these in your response, in the same flat array format:',
            wp_json_encode($flat_options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            '',
            'Return the full updated field_options array (flat, with label/value/group keys) including all existing options plus any changes.',
        ]);
    }

    protected function flattenOptionsForDisplay(array $current_options): array
    {
        $flat = [];
        foreach ($current_options as $group_name => $group_options) {
            if (!is_array($group_options)) {
                continue;
            }
            $group_key = is_string($group_name) ? $group_name : '';
            foreach ($group_options as $label => $value) {
                $flat[] = [
                    'label' => (string) $label,
                    'value' => (string) $value,
                    'group' => $group_key,
                ];
            }
        }
        return $flat;
    }

    protected function validateFieldOptionsPayload(array $decoded): array
    {
        $options = $decoded['field_options'] ?? null;
        if (!is_array($options) || empty($options)) {
            return [__('field_options must be a non-empty array.', 'griffinforms-form-builder')];
        }
        $errors = [];
        foreach ($options as $i => $opt) {
            if (!is_array($opt)) {
                $errors[] = sprintf(__('Option entry %d is not an object.', 'griffinforms-form-builder'), $i + 1);
                continue;
            }
            if (!isset($opt['label']) || (string) $opt['label'] === '') {
                $errors[] = sprintf(__('Option entry %d is missing a label.', 'griffinforms-form-builder'), $i + 1);
            }
            if (!isset($opt['value']) || sanitize_key((string) $opt['value']) === '') {
                $errors[] = sprintf(__('Option entry %d is missing a valid value slug.', 'griffinforms-form-builder'), $i + 1);
            }
        }
        return $errors;
    }

    protected function normalizeFieldOptions(array $options): array
    {
        // Rebuild as grouped associative array: array(group => array(label => value))
        $groups = [];
        foreach ($options as $opt) {
            if (!is_array($opt)) {
                continue;
            }
            $label = $this->normalizeText((string) ($opt['label'] ?? ''), 120);
            $value = sanitize_key((string) ($opt['value'] ?? ''));
            $group = isset($opt['group']) ? $this->normalizeText((string) $opt['group'], 120) : '';
            if ($label === '' || $value === '') {
                continue;
            }
            $groups[$group][$label] = $value;
        }
        return $groups;
    }

    // -------------------------------------------------------------------------
    // inputlikert branch
    // -------------------------------------------------------------------------

    protected function handleLikert(int $form_id, int $field_id, string $prompt, string $provider, array $config, Format $format_sql, array $selected_context, array $request_context): array
    {
        $field = $format_sql->itemValues($field_id, 'field', ['heading', 'field_type', 'field_settings']);
        $current_settings = $this->decodeValue($field->field_settings ?? null);
        if (!is_array($current_settings)) {
            $current_settings = ['rows' => [], 'columns' => [], 'allow_na' => false, 'required_mode' => 'all', 'column_width' => 'auto'];
        }

        $last_errors = [];
        $raw_content = '';
        for ($attempt = 0; $attempt <= self::MAX_REFLECTION_RETRIES; $attempt++) {
            $prompt_text = $attempt === 0
                ? $this->buildLikertUserPrompt($prompt, $field, $current_settings, $selected_context, $request_context)
                : $this->buildReflectionPrompt($prompt, $raw_content, $last_errors, 'field_settings');

            $response    = $this->requestStrict(
                $provider,
                $prompt_text,
                ['system' => $this->buildLikertSystemPrompt()],
                array_merge($config, ['timeout' => $attempt === 0 ? 22 : 14])
            );

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

            $validation_errors = [];
            if ($extract_error !== null) {
                $validation_errors[] = $extract_error;
            } elseif (!is_array($decoded)) {
                $validation_errors[] = __('Likert settings response must decode to an object.', 'griffinforms-form-builder');
            } else {
                $validation_errors = $this->validateLikertPayload($decoded, $current_settings);
            }

            if (empty($validation_errors) && is_array($decoded)) {
                $new_settings = $this->mergeLikertSettings($current_settings, (array) ($decoded['field_settings'] ?? []));
                $format_sql->updateItemValue('field', $field_id, 'field_settings', wp_json_encode($new_settings));
                return [
                    'success'       => true,
                    'status'        => 'applied',
                    'message'       => $this->normalizeText((string) ($decoded['success_message'] !== '' ? ($decoded['success_message'] ?? '') : '') ?: __('Updated the likert options.', 'griffinforms-form-builder'), 140),
                    'mutated_ids'   => [$field_id],
                    'prompt_key'    => $this->getPromptKey(),
                    'patch_plan'    => [],
                    'patch_summary' => [],
                ];
            }

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

        return $this->formatFailure(
            __('I could not update the likert options safely. Try being more specific about which rows or columns to change.', 'griffinforms-form-builder'),
            'likert_options_validation_failed',
            422,
            ['request_context' => $request_context]
        );
    }

    protected function buildLikertSystemPrompt(): string
    {
        return implode("\n", [
            'You are Griffin Assist managing rows and columns for an inputlikert field in GriffinForms.',
            'Return JSON only.',
            'Exact shape: {"field_settings":{"rows":[{"id":"row_xxx","label":"string"}],"columns":[{"id":"col_xxx","label":"string"}],"allow_na":bool,"required_mode":"string","column_width":"string"},"success_message":"string"}',
            'Rules:',
            '- Return the full rows and columns arrays — partial arrays will lose existing entries.',
            '- Preserve all existing row and column IDs exactly when renaming or reordering.',
            '- When adding a new row, generate a unique ID: "row_" + 6 lowercase alphanumeric characters.',
            '- When adding a new column, generate a unique ID: "col_" + 6 lowercase alphanumeric characters.',
            '- Minimum 1 row, minimum 2 columns, maximum 20 rows, maximum 10 columns.',
            '- Preserve allow_na, required_mode, and column_width unchanged unless the prompt explicitly targets them.',
            '- success_message must be a specific, non-empty confirmation of what changed — e.g. "Polished 4 row labels." or "Added 2 new rows." Never return an empty string.',
            '- Do not return markdown, code fences, or required_inputs.',
        ]);
    }

    protected function buildLikertUserPrompt(string $prompt, $field, array $current_settings, array $selected_context, array $request_context): string
    {
        return implode("\n", [
            'User request:',
            $prompt,
            'Selected likert field:',
            wp_json_encode([
                'field_id'        => absint($selected_context['item_id'] ?? 0),
                'label'           => sanitize_text_field((string) ($field->heading ?? '')),
                'current_settings'=> $current_settings,
            ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'Apply the requested change. Return the full updated field_settings object with rows and columns.',
        ]);
    }

    protected function validateLikertPayload(array $decoded, array $current_settings): array
    {
        $errors   = [];
        $settings = $decoded['field_settings'] ?? null;

        if (!is_array($settings)) {
            $errors[] = __('field_settings must be an object.', 'griffinforms-form-builder');
            return $errors;
        }

        $rows    = $settings['rows'] ?? null;
        $columns = $settings['columns'] ?? null;

        if (!is_array($rows) || empty($rows)) {
            $errors[] = __('field_settings.rows must be a non-empty array.', 'griffinforms-form-builder');
        } elseif (count($rows) < self::LIKERT_MIN_ROWS) {
            $errors[] = sprintf(__('Likert fields require at least %d row.', 'griffinforms-form-builder'), self::LIKERT_MIN_ROWS);
        } elseif (count($rows) > self::LIKERT_MAX_ROWS) {
            $errors[] = sprintf(__('Likert fields allow a maximum of %d rows.', 'griffinforms-form-builder'), self::LIKERT_MAX_ROWS);
        }

        if (!is_array($columns) || empty($columns)) {
            $errors[] = __('field_settings.columns must be a non-empty array.', 'griffinforms-form-builder');
        } elseif (count($columns) < self::LIKERT_MIN_COLUMNS) {
            $errors[] = sprintf(__('Likert fields require at least %d columns.', 'griffinforms-form-builder'), self::LIKERT_MIN_COLUMNS);
        } elseif (count($columns) > self::LIKERT_MAX_COLUMNS) {
            $errors[] = sprintf(__('Likert fields allow a maximum of %d columns.', 'griffinforms-form-builder'), self::LIKERT_MAX_COLUMNS);
        }

        return $errors;
    }

    protected function mergeLikertSettings(array $current, array $new): array
    {
        return [
            'rows'          => is_array($new['rows'] ?? null) ? $new['rows'] : ($current['rows'] ?? []),
            'columns'       => is_array($new['columns'] ?? null) ? $new['columns'] : ($current['columns'] ?? []),
            'allow_na'      => isset($new['allow_na']) ? (bool) $new['allow_na'] : ($current['allow_na'] ?? false),
            'required_mode' => isset($new['required_mode']) ? sanitize_key((string) $new['required_mode']) : ($current['required_mode'] ?? 'all'),
            'column_width'  => isset($new['column_width']) ? sanitize_text_field((string) $new['column_width']) : ($current['column_width'] ?? 'auto'),
        ];
    }

    // -------------------------------------------------------------------------
    // Shared helpers
    // -------------------------------------------------------------------------

    protected function looksLikeRemoveIntent(string $prompt): bool
    {
        $lower = mb_strtolower($prompt);
        foreach (['remov', 'delet', 'drop', 'get rid'] as $kw) {
            if (str_contains($lower, $kw)) {
                return true;
            }
        }
        return false;
    }

    protected function buildReflectionPrompt(string $prompt, string $raw_content, array $errors, string $response_key): string
    {
        return implode("\n", [
            'Your previous response was invalid.',
            'Original request:',
            $prompt,
            'Validation errors:',
            wp_json_encode(array_values($errors), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'Previous raw response:',
            $raw_content,
            'Return corrected JSON only with a valid ' . $response_key . ' key.',
        ]);
    }

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

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

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

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

        return $value;
    }
}
