<?php

namespace GriffinForms\Includes\Ai;

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

class PatchPlanApplier
{
    protected App $app_sql;
    protected Format $format_sql;
    protected \GriffinForms\Config $config;
    protected \GriffinForms\Log $log;
    protected StructureSnapshotBuilder $snapshot_builder;

    public function __construct()
    {
        $this->app_sql = App::getInstance();
        $this->format_sql = Format::getInstance();
        $this->config = \GriffinForms\Config::getInstance();
        $this->log = \GriffinForms\Log::getInstance();
        $this->snapshot_builder = new StructureSnapshotBuilder();
    }

    public function apply(int $form_id, array $patch_plan, string $source_prompt = ''): array
    {
        global $wpdb;

        $form_id = absint($form_id);
        if ($form_id <= 0 || !$this->format_sql->itemExists('form', $form_id)) {
            return [
                'success' => false,
                'message' => __('The target draft form could not be found.', 'griffinforms-form-builder'),
            ];
        }

        $mode = sanitize_key((string) ($patch_plan['mode'] ?? ''));
        if ($mode === 'no_op') {
            return [
                'success' => true,
                'message' => (string) ($patch_plan['message'] ?? $patch_plan['success_message'] ?? __('Griffin Assist reviewed the form but no changes were required.', 'griffinforms-form-builder')),
                'kind' => 'no_op',
            ];
        }

        if ($mode === 'patch_with_user_input') {
            return [
                'success' => true,
                'message' => (string) ($patch_plan['success_message'] ?? __('Griffin Assist has some suggestions. Which one do you prefer?', 'griffinforms-form-builder')),
                'kind' => 'choices_required',
            ];
        }

        if (!in_array($mode, ['patch', 'patch_with_user_input'], true)) {
            return [
                'success' => false,
                'message' => sprintf(__('Griffin Assist received an unsupported patch mode: %s', 'griffinforms-form-builder'), $mode),
            ];
        }

        $operations = array_values((array) ($patch_plan['operations'] ?? (($patch_plan['draft_patch']['operations'] ?? []))));
        if (empty($operations)) {
            return [
                'success' => false,
                'message' => __('Griffin Assist did not receive any executable patch operations.', 'griffinforms-form-builder'),
            ];
        }

        $structure = (array) ($patch_plan['structure'] ?? []);
        $width_error = $this->validateRowColumnWidths($operations, $structure);
        if ($width_error !== null) {
            return [
                'success' => false,
                'message' => $width_error,
            ];
        }

        $mutated_ids = [];
        $warnings = [];
        $summary = [
            'updated' => 0,
            'inserted' => 0,
            'moved' => 0,
            'blocked' => 0,
        ];

        try {
            $wpdb->query('START TRANSACTION');

            $operation_context = [
                'selected_context' => (array) ($patch_plan['selected_context'] ?? []),
                'request_context' => [
                    'prompt' => $source_prompt,
                ],
                'structure' => (array) ($patch_plan['structure'] ?? []),
            ];

            // Tracks IDs created earlier in the same batch so subsequent operations can
            // resolve into newly-inserted rows/columns that may not be positionally last.
            $batch_state = [
                'last_inserted_page_id'        => 0,
                'last_inserted_row_id'         => 0,
                'last_inserted_column_id'      => 0,
                'last_inserted_row_column_ids' => [], // ordered column IDs from last insert_row; consumed one-per-field
            ];

            foreach ($operations as $operation) {
                $result = $this->applyOperation($form_id, (array) $operation, $source_prompt, $operation_context, $batch_state);
                if (empty($result['success'])) {
                    throw new \RuntimeException((string) ($result['message'] ?? __('Patch operation failed.', 'griffinforms-form-builder')));
                }

                $op = sanitize_key((string) ((array) $operation)['op'] ?? '');
                $ids = (array) ($result['mutated_ids'] ?? []);
                if ($op === 'insert_page' && !empty($ids[0])) {
                    $batch_state['last_inserted_page_id']        = absint($ids[0]);
                    $batch_state['last_inserted_row_id']         = 0;
                    $batch_state['last_inserted_column_id']      = 0;
                    $batch_state['last_inserted_row_column_ids'] = [];
                } elseif ($op === 'insert_row' && !empty($ids[0])) {
                    // mutated_ids layout: [row_id, page_id, col_id_1, col_id_2, ...]
                    $batch_state['last_inserted_row_id']         = absint($ids[0]);
                    $batch_state['last_inserted_column_id']      = 0;
                    $batch_state['last_inserted_row_column_ids'] = count($ids) > 2
                        ? array_values(array_map('absint', array_slice($ids, 2)))
                        : [];
                } elseif ($op === 'insert_column' && !empty($ids[0])) {
                    $batch_state['last_inserted_column_id'] = absint($ids[0]);
                }

                $mutated_ids = array_merge($mutated_ids, $ids);
                if (!empty($result['warning'])) {
                    $warnings[] = (string) $result['warning'];
                }

                $kind = sanitize_key((string) ($result['kind'] ?? ''));
                if (isset($summary[$kind])) {
                    $summary[$kind]++;
                }
            }

            $this->updateFormPatchMetadata($form_id, $patch_plan, $source_prompt, $summary);

            $wpdb->query('COMMIT');

            $this->log->add(
                'info',
                'ai_patch',
                $form_id,
                (int) get_current_user_id(),
                sprintf(
                    'Patch apply summary: updated=%d inserted=%d moved=%d blocked=%d warnings=%d',
                    absint($summary['updated']),
                    absint($summary['inserted']),
                    absint($summary['moved']),
                    absint($summary['blocked']),
                    count($warnings)
                ),
                'ai_patch'
            );

            $final_message = (string) ($patch_plan['success_message'] ?? __('Patch applied successfully.', 'griffinforms-form-builder'));
            if (!empty($warnings)) {
                $final_message = $this->stitchTruthfulMessage($final_message, $warnings);
            }

            return [
                'success' => true,
                'message' => $final_message,
                'mutated_ids' => array_values(array_unique(array_map('absint', $mutated_ids))),
                'patch_summary' => $summary,
                'warnings' => $warnings,
            ];
        } catch (\Throwable $throwable) {
            $wpdb->query('ROLLBACK');

            $this->log->add(
                'error',
                'ai_patch',
                $form_id,
                (int) get_current_user_id(),
                'Patch apply failed: ' . sanitize_text_field($throwable->getMessage()),
                'ai_patch'
            );

            return [
                'success' => false,
                'message' => sanitize_text_field($throwable->getMessage()) ?: __('Griffin Assist could not safely apply that patch plan.', 'griffinforms-form-builder'),
            ];
        }
    }

    protected function applyOperation(int $form_id, array $operation, string $source_prompt, array $context = [], array &$batch_state = []): array
    {
        $op = sanitize_key((string) ($operation['op'] ?? ''));

        if (str_starts_with($op, 'update_')) {
            return $this->applyUpdateOperation($form_id, $operation, $context);
        }

        if (str_starts_with($op, 'insert_')) {
            return $this->applyInsertOperation($form_id, $operation, $context, $batch_state);
        }

        if (str_starts_with($op, 'move_')) {
            return $this->applyMoveOperation($form_id, $operation, $context);
        }

        if ($op === 'remove_row') {
            return $this->applyRemoveRowOperation($form_id, $operation, $context);
        }

        if ($op === 'remove_field') {
            return $this->applyRemoveFieldOperation($form_id, $operation, $context);
        }

        if ($op === 'remove_column') {
            return $this->applyRemoveColumnOperation($form_id, $operation, $context);
        }

        if ($op === 'remove_page') {
            return $this->applyRemovePageOperation($form_id, $operation, $context);
        }

        if (str_starts_with($op, 'remove_') || $op === 'replace') {
            return [
                'success' => false,
                'message' => __('Griffin Assist blocked this destructive patch because dependency-safe removal and replace handling is not enabled yet.', 'griffinforms-form-builder'),
                'kind' => 'blocked',
            ];
        }

        if ($op === 'no_op') {
            return [
                'success' => true,
                'mutated_ids' => [],
                'kind' => 'updated',
            ];
        }

        return [
            'success' => false,
            'message' => __('Griffin Assist encountered an unsupported patch operation.', 'griffinforms-form-builder'),
            'kind' => 'blocked',
        ];
    }

    protected function applyUpdateOperation(int $form_id, array $operation, array $context = []): array
    {
        $target = $this->resolveTarget($form_id, (array) ($operation['target'] ?? []), $context);
        if (!is_array($target)) {
            return [
                'success' => false,
                'message' => __('Griffin Assist could not resolve the patch target for update.', 'griffinforms-form-builder'),
                'kind' => 'blocked',
            ];
        }

        $scope = sanitize_key((string) ($target['scope'] ?? ''));
        $changes = (array) ($operation['changes'] ?? []);

        if ($scope === 'form') {
            $data = $this->mapFormChanges($changes);
            if (empty($data)) {
                return [
                    'success' => false,
                    'message' => __('No valid form changes were provided for this patch.', 'griffinforms-form-builder'),
                    'kind' => 'blocked',
                ];
            }

            $this->updateRecord('form', $form_id, $data);
            return ['success' => true, 'mutated_ids' => [$form_id], 'kind' => 'updated'];
        }

        $item_id = absint($target['item_id'] ?? 0);
        if ($item_id <= 0) {
            return [
                'success' => false,
                'message' => __('The requested patch target no longer exists.', 'griffinforms-form-builder'),
                'kind' => 'blocked',
            ];
        }

        if ($scope === 'page') {
            $data = $this->mapPageChanges($changes, (array) ($operation['page'] ?? []));
            if (empty($data)) {
                return ['success' => false, 'message' => __('No valid page changes were provided for this patch.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }
            $this->updateRecord('formpage', $item_id, $data);
            return ['success' => true, 'mutated_ids' => [$item_id], 'kind' => 'updated'];
        }

        if ($scope === 'row') {
            $data = $this->mapRowChanges($changes, (array) ($operation['row'] ?? []));
            if (empty($data)) {
                return ['success' => false, 'message' => __('No valid row changes were provided for this patch.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }
            $this->updateRecord('pagerow', $item_id, $data);
            $message = __('Updated the row.', 'griffinforms-form-builder');
            if (array_key_exists('heading', $data)) {
                $message = __('Updated the row heading.', 'griffinforms-form-builder');
            } elseif (array_key_exists('description', $data)) {
                $message = __('Updated the row description.', 'griffinforms-form-builder');
            }

            return ['success' => true, 'mutated_ids' => [$item_id], 'kind' => 'updated', 'message' => $message];
        }

        if ($scope === 'column') {
            $data = $this->mapColumnChanges($changes, (array) ($operation['column'] ?? []));
            if (empty($data)) {
                return ['success' => false, 'message' => __('No valid column changes were provided for this patch.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }
            $this->updateRecord('rowcolumn', $item_id, $data);
            return ['success' => true, 'mutated_ids' => [$item_id], 'kind' => 'updated'];
        }

        if ($scope === 'field') {
            if (isset($changes['field_settings']) && is_array($changes['field_settings'])) {
                $existing_settings = $this->format_sql->itemValues($item_id, 'field', ['field_settings']);
                $existing_fs = [];
                if (is_object($existing_settings) && isset($existing_settings->field_settings)) {
                    $raw = maybe_unserialize($existing_settings->field_settings);
                    if (is_array($raw)) {
                        $existing_fs = $raw;
                    }
                }
                $changes['field_settings'] = array_merge($existing_fs, $changes['field_settings']);
            }
            $data = $this->mapFieldChanges($changes, (array) ($operation['field'] ?? []));
            if (empty($data)) {
                return ['success' => false, 'message' => __('No valid field changes were provided for this patch.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }
            $this->updateRecord('field', $item_id, $data);
            return ['success' => true, 'mutated_ids' => [$item_id], 'kind' => 'updated'];
        }

        return [
            'success' => false,
            'message' => __('Griffin Assist could not map this update patch to a supported scope.', 'griffinforms-form-builder'),
            'kind' => 'blocked',
        ];
    }

    protected function applyInsertOperation(int $form_id, array $operation, array $context = [], array &$batch_state = []): array
    {
        $op = sanitize_key((string) ($operation['op'] ?? ''));
        $target = $this->resolveTarget($form_id, (array) ($operation['target'] ?? []), $context) ?: ['scope' => 'form'];
        $anchor = $this->resolveTarget($form_id, (array) ($operation['anchor'] ?? []), $context, true) ?: [];
        $position = sanitize_key((string) ($operation['position'] ?? 'after'));
        $warning = ($target['warning'] ?? '') ?: ($anchor['warning'] ?? '');

        if ($op === 'insert_page') {
            $page_id = $this->app_sql->createFormPage($form_id);
            if (!is_int($page_id) || $page_id <= 0) {
                return ['success' => false, 'message' => __('Failed to create page for patch apply.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }

            $this->updateRecord('formpage', $page_id, $this->mapPageChanges([], (array) ($operation['page'] ?? [])));
            $formpages = $this->decodeValue($this->format_sql->getItemValue($form_id, 'formpages', 'form'));
            $formpages = is_array($formpages) ? array_values(array_map('absint', $formpages)) : [];
            $reference_id = absint(($anchor['item_id'] ?? 0) ?: ($target['item_id'] ?? 0));
            $formpages = $this->insertIntoList($formpages, $page_id, $reference_id, $position);
            $this->format_sql->updateItemValue('form', $form_id, 'formpages', maybe_serialize($formpages));

            return ['success' => true, 'mutated_ids' => [$page_id, $form_id], 'kind' => 'inserted'];
        }

        if ($op === 'insert_row') {
            $page_id = $this->resolveParentPageIdForInsert($form_id, $target);
            if ($page_id <= 0) {
                return ['success' => false, 'message' => __('Rows can only be added inside a page. Please select a page and try again.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }

            $row_id = $this->app_sql->createPageRow($form_id);
            if (!is_int($row_id) || $row_id <= 0) {
                return ['success' => false, 'message' => __('Failed to create row for patch apply.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }

            $row_payload = (array) ($operation['row'] ?? []);
            $this->updateRecord('pagerow', $row_id, $this->mapRowChanges([], $row_payload));
            $requested_column_count = absint($row_payload['column_count'] ?? 1);
            $requested_column_count = max(1, min(4, $requested_column_count));
            $column_ids = $this->decodeValue($this->format_sql->getItemValue($row_id, 'rowcolumns', 'pagerow'));
            $column_ids = is_array($column_ids) ? array_values(array_filter(array_map('absint', $column_ids))) : [];

            while (count($column_ids) < $requested_column_count) {
                $column_id = $this->app_sql->createRowColumn(12, $form_id);
                if (!is_int($column_id) || $column_id <= 0) {
                    return ['success' => false, 'message' => __('Failed to create column for the new row.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
                }

                $column_ids[] = $column_id;
            }

            $widths = $this->buildEvenColumnWidths(count($column_ids));
            foreach ($column_ids as $index => $column_id) {
                $this->app_sql->updateRowColumnWidth((int) $column_id, (int) ($widths[$index] ?? 12));
            }
            $this->app_sql->updatePageRowColumns($row_id, $column_ids);

            $rows = $this->decodeValue($this->format_sql->getItemValue($page_id, 'pagerows', 'formpage'));
            $rows = is_array($rows) ? array_values(array_map('absint', $rows)) : [];
            $reference_id = absint(($anchor['item_id'] ?? 0) ?: (in_array($target['scope'] ?? '', ['row', 'pagerow'], true) ? ($target['item_id'] ?? 0) : 0));
            $rows = $this->insertIntoList($rows, $row_id, $reference_id, $position);
            $this->format_sql->updateItemValue('formpage', $page_id, 'pagerows', maybe_serialize($rows));

            return ['success' => true, 'mutated_ids' => array_merge([$row_id, $page_id], $column_ids), 'kind' => 'inserted'];
        }

        if ($op === 'insert_column') {
            $row_id = $this->resolveParentRowIdForInsert($form_id, $target, $batch_state);
            if ($row_id <= 0) {
                return ['success' => false, 'message' => __('Griffin Assist could not resolve the target row for the new column.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }

            $width = absint(($operation['column']['width'] ?? 12));
            $column_id = $this->app_sql->createRowColumn($width > 0 ? $width : 12, $form_id);
            if (!is_int($column_id) || $column_id <= 0) {
                return ['success' => false, 'message' => __('Failed to create column for patch apply.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }

            $this->updateRecord('rowcolumn', $column_id, $this->mapColumnChanges([], (array) ($operation['column'] ?? [])));
            $columns = $this->decodeValue($this->format_sql->getItemValue($row_id, 'rowcolumns', 'pagerow'));
            $columns = is_array($columns) ? array_values(array_map('absint', $columns)) : [];
            $reference_id = absint(($anchor['item_id'] ?? 0) ?: ($target['scope'] === 'column' ? ($target['item_id'] ?? 0) : 0));
            $columns = $this->insertIntoList($columns, $column_id, $reference_id, $position);
            $this->app_sql->updatePageRowColumns($row_id, $columns);

            return ['success' => true, 'mutated_ids' => [$column_id, $row_id], 'kind' => 'inserted'];
        }

        if ($op === 'insert_field') {
            $column_id = $this->resolveParentColumnIdForInsert($form_id, $target, $batch_state);
            if ($column_id <= 0) {
                return ['success' => false, 'message' => __('Griffin Assist could not resolve the target column for the new field.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }

            $field_id = $this->app_sql->createField($this->mapFieldInsertData((array) ($operation['field'] ?? [])));
            if (!is_int($field_id) || $field_id <= 0) {
                return ['success' => false, 'message' => __('Failed to create field for patch apply.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }

            $fields = $this->decodeValue($this->format_sql->getItemValue($column_id, 'fields', 'rowcolumn'));
            $fields = is_array($fields) ? array_values(array_map('absint', $fields)) : [];
            $reference_id = absint(($anchor['item_id'] ?? 0) ?: ($target['scope'] === 'field' ? ($target['item_id'] ?? 0) : 0));
            $fields = $this->insertIntoList($fields, $field_id, $reference_id, $position);
            $this->app_sql->updateRowColumnFields($column_id, $fields);

            return [
                'success' => true,
                'mutated_ids' => [$field_id, $column_id],
                'kind' => 'inserted',
                'warning' => $warning ? sprintf(__('%s So I added the field at the end of the form instead.', 'griffinforms-form-builder'), $warning) : null,
            ];
        }

        return ['success' => false, 'message' => __('Unsupported insert patch operation.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
    }

    protected function applyMoveOperation(int $form_id, array $operation, array $context = []): array
    {
        $target = $this->resolveTarget($form_id, (array) ($operation['target'] ?? []), $context);
        if (!is_array($target)) {
            return ['success' => false, 'message' => __('Griffin Assist could not resolve the item to move.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $anchor = $this->resolveTarget($form_id, (array) ($operation['anchor'] ?? []), $context, true);
        $position = sanitize_key((string) ($operation['position'] ?? 'after'));
        $scope = sanitize_key((string) ($target['scope'] ?? ''));
        $item_id = absint($target['item_id'] ?? 0);

        if ($item_id <= 0) {
            return ['success' => false, 'message' => __('Griffin Assist could not resolve the item to move.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        if ($scope === 'page') {
            $formpages = $this->decodeValue($this->format_sql->getItemValue($form_id, 'formpages', 'form'));
            $formpages = is_array($formpages) ? array_values(array_map('absint', $formpages)) : [];
            $formpages = $this->moveWithinList($formpages, $item_id, absint($anchor['item_id'] ?? 0), $position);
            $this->format_sql->updateItemValue('form', $form_id, 'formpages', maybe_serialize($formpages));
            return ['success' => true, 'mutated_ids' => [$item_id, $form_id], 'kind' => 'moved'];
        }

        if ($scope === 'row') {
            $source_page_id = $this->findParentPageIdForRow($form_id, $item_id);
            if ($source_page_id <= 0) {
                return ['success' => false, 'message' => __('Griffin Assist could not resolve the row parent.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }

            $anchor_scope    = sanitize_key((string) ($anchor['scope'] ?? ''));
            $anchor_item_id  = absint($anchor['item_id'] ?? 0);
            $target_page_id  = 0;

            if ($anchor_scope === 'page' && $anchor_item_id > 0) {
                $target_page_id = $anchor_item_id;
            } elseif ($anchor_scope === 'row' && $anchor_item_id > 0) {
                $anchor_page_id = $this->findParentPageIdForRow($form_id, $anchor_item_id);
                if ($anchor_page_id > 0 && $anchor_page_id !== $source_page_id) {
                    $target_page_id = $anchor_page_id;
                }
            }

            if ($target_page_id > 0) {
                $source_rows = $this->decodeValue($this->format_sql->getItemValue($source_page_id, 'pagerows', 'formpage'));
                $source_rows = is_array($source_rows) ? array_values(array_filter(array_map('absint', $source_rows), static function ($id) use ($item_id) { return $id !== $item_id; })) : [];
                $this->format_sql->updateItemValue('formpage', $source_page_id, 'pagerows', maybe_serialize($source_rows));

                $target_rows = $this->decodeValue($this->format_sql->getItemValue($target_page_id, 'pagerows', 'formpage'));
                $target_rows = is_array($target_rows) ? array_values(array_map('absint', $target_rows)) : [];
                $ref_id      = $anchor_scope === 'row' ? $anchor_item_id : 0;
                $target_rows = $this->insertIntoList($target_rows, $item_id, $ref_id, $position);
                $this->format_sql->updateItemValue('formpage', $target_page_id, 'pagerows', maybe_serialize($target_rows));

                return ['success' => true, 'mutated_ids' => [$item_id, $source_page_id, $target_page_id], 'kind' => 'moved'];
            }

            $rows = $this->decodeValue($this->format_sql->getItemValue($source_page_id, 'pagerows', 'formpage'));
            $rows = is_array($rows) ? array_values(array_map('absint', $rows)) : [];
            $rows = $this->moveWithinList($rows, $item_id, $anchor_item_id, $position);
            $this->format_sql->updateItemValue('formpage', $source_page_id, 'pagerows', maybe_serialize($rows));
            return ['success' => true, 'mutated_ids' => [$item_id, $source_page_id], 'kind' => 'moved'];
        }

        if ($scope === 'column') {
            $row_id = $this->findParentRowIdForColumn($form_id, $item_id);
            if ($row_id <= 0) {
                return ['success' => false, 'message' => __('Griffin Assist could not resolve the column parent.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }
            $columns = $this->decodeValue($this->format_sql->getItemValue($row_id, 'rowcolumns', 'pagerow'));
            $columns = is_array($columns) ? array_values(array_map('absint', $columns)) : [];
            $columns = $this->moveWithinList($columns, $item_id, absint($anchor['item_id'] ?? 0), $position);
            $this->app_sql->updatePageRowColumns($row_id, $columns);
            return ['success' => true, 'mutated_ids' => [$item_id, $row_id], 'kind' => 'moved'];
        }

        if ($scope === 'field') {
            $source_column_id = $this->findParentColumnIdForField($form_id, $item_id);
            if ($source_column_id <= 0) {
                return ['success' => false, 'message' => __('Griffin Assist could not resolve the field parent.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }

            $anchor_scope    = sanitize_key((string) ($anchor['scope'] ?? ''));
            $anchor_item_id  = absint($anchor['item_id'] ?? 0);

            // Cross-column move: anchor.scope === 'column' with a different target column.
            if ($anchor_scope === 'column' && $anchor_item_id > 0 && $anchor_item_id !== $source_column_id) {
                $source_fields = $this->decodeValue($this->format_sql->getItemValue($source_column_id, 'fields', 'rowcolumn'));
                $source_fields = is_array($source_fields) ? array_values(array_filter(array_map('absint', $source_fields), static function ($id) use ($item_id) { return $id !== $item_id; })) : [];
                $this->app_sql->updateRowColumnFields($source_column_id, $source_fields);

                $target_fields = $this->decodeValue($this->format_sql->getItemValue($anchor_item_id, 'fields', 'rowcolumn'));
                $target_fields = is_array($target_fields) ? array_values(array_map('absint', $target_fields)) : [];
                $target_fields[] = $item_id;
                $this->app_sql->updateRowColumnFields($anchor_item_id, $target_fields);

                return ['success' => true, 'mutated_ids' => [$item_id, $source_column_id, $anchor_item_id], 'kind' => 'moved'];
            }

            // Within-column reorder.
            $fields = $this->decodeValue($this->format_sql->getItemValue($source_column_id, 'fields', 'rowcolumn'));
            $fields = is_array($fields) ? array_values(array_map('absint', $fields)) : [];
            $fields = $this->moveWithinList($fields, $item_id, $anchor_item_id, $position);
            $this->app_sql->updateRowColumnFields($source_column_id, $fields);
            return ['success' => true, 'mutated_ids' => [$item_id, $source_column_id], 'kind' => 'moved'];
        }

        return ['success' => false, 'message' => __('Unsupported move patch operation.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
    }

    protected function applyRemoveRowOperation(int $form_id, array $operation, array $context = []): array
    {
        $target = $this->resolveTarget($form_id, (array) ($operation['target'] ?? []), $context);
        if (!is_array($target)) {
            return ['success' => false, 'message' => __('Griffin Assist could not resolve the row to delete.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $item_id = absint($target['item_id'] ?? 0);
        if (sanitize_key((string) ($target['scope'] ?? '')) !== 'row' || $item_id <= 0) {
            return ['success' => false, 'message' => __('Griffin Assist could not resolve the row to delete.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $page_id = $this->findParentPageIdForRow($form_id, $item_id);
        if ($page_id <= 0) {
            return ['success' => false, 'message' => __('Griffin Assist could not resolve the row parent.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $rows = $this->decodeValue($this->format_sql->getItemValue($page_id, 'pagerows', 'formpage'));
        $rows = is_array($rows) ? array_values(array_filter(array_map('absint', $rows))) : [];
        if (count($rows) <= 1) {
            return [
                'success' => false,
                'message' => __('This is the only row on the page, so it cannot be deleted.', 'griffinforms-form-builder'),
                'kind' => 'blocked',
            ];
        }

        $delete = $this->format_sql->deletePageRow($item_id, $page_id);
        if (empty($delete)) {
            return ['success' => false, 'message' => __('Griffin Assist could not delete the selected row.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        return ['success' => true, 'mutated_ids' => [$item_id, $page_id], 'kind' => 'updated'];
    }

    protected function applyRemoveFieldOperation(int $form_id, array $operation, array $context = []): array
    {
        $target = $this->resolveTarget($form_id, (array) ($operation['target'] ?? []), $context);
        if (!is_array($target)) {
            return ['success' => false, 'message' => __('Griffin Assist could not resolve the field to delete.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $item_id = absint($target['item_id'] ?? 0);
        if (sanitize_key((string) ($target['scope'] ?? '')) !== 'field' || $item_id <= 0) {
            return ['success' => false, 'message' => __('Griffin Assist could not resolve the field to delete.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $column_id = $this->findParentColumnIdForField($form_id, $item_id);
        if ($column_id <= 0) {
            return ['success' => false, 'message' => __('Griffin Assist could not resolve the field parent.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        if (!$this->format_sql->itemExists('field', $item_id)) {
            return ['success' => false, 'message' => __('Griffin Assist could not find the field to delete.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $field = $this->format_sql->itemValues($item_id, 'field');
        if ($field->is_reusable) {
            // Detach only — do not delete reusable fields from the DB.
            $this->format_sql->removeField($item_id, $column_id);
        } else {
            $delete = $this->format_sql->deleteField($item_id, $column_id);
            if (empty($delete)) {
                return ['success' => false, 'message' => __('Griffin Assist could not delete the selected field.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
            }
        }

        return ['success' => true, 'mutated_ids' => [$item_id, $column_id], 'kind' => 'updated'];
    }

    protected function applyRemoveColumnOperation(int $form_id, array $operation, array $context = []): array
    {
        $target = $this->resolveTarget($form_id, (array) ($operation['target'] ?? []), $context);
        if (!is_array($target)) {
            return ['success' => false, 'message' => __('Griffin Assist could not resolve the column to delete.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $item_id = absint($target['item_id'] ?? 0);
        if (sanitize_key((string) ($target['scope'] ?? '')) !== 'column' || $item_id <= 0) {
            return ['success' => false, 'message' => __('Griffin Assist could not resolve the column to delete.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $row_id = $this->findParentRowIdForColumn($form_id, $item_id);
        if ($row_id <= 0) {
            return ['success' => false, 'message' => __('Griffin Assist could not resolve the column parent.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $columns = $this->decodeValue($this->format_sql->getItemValue($row_id, 'rowcolumns', 'pagerow'));
        $columns = is_array($columns) ? array_values(array_filter(array_map('absint', $columns))) : [];
        if (count($columns) <= 1) {
            return ['success' => false, 'message' => __('This is the only column in the row, so it cannot be deleted.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $delete = $this->format_sql->deleteRowColumn($item_id, $row_id);
        if (empty($delete)) {
            return ['success' => false, 'message' => __('Griffin Assist could not delete the selected column.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        return ['success' => true, 'mutated_ids' => [$item_id, $row_id], 'kind' => 'updated'];
    }

    protected function applyRemovePageOperation(int $form_id, array $operation, array $context = []): array
    {
        $target = $this->resolveTarget($form_id, (array) ($operation['target'] ?? []), $context);
        if (!is_array($target)) {
            return ['success' => false, 'message' => __('Griffin Assist could not resolve the page to delete.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $item_id = absint($target['item_id'] ?? 0);
        if (sanitize_key((string) ($target['scope'] ?? '')) !== 'page' || $item_id <= 0) {
            return ['success' => false, 'message' => __('Griffin Assist could not resolve the page to delete.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $pages = $this->decodeValue($this->format_sql->getItemValue($form_id, 'formpages', 'form'));
        $pages = is_array($pages) ? array_values(array_filter(array_map('absint', $pages))) : [];
        if (count($pages) <= 1) {
            return ['success' => false, 'message' => __('This is the only page on the form, so it cannot be deleted.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        $delete = $this->format_sql->deleteFormPage($item_id, $form_id);
        if (empty($delete)) {
            return ['success' => false, 'message' => __('Griffin Assist could not delete the selected page.', 'griffinforms-form-builder'), 'kind' => 'blocked'];
        }

        return ['success' => true, 'mutated_ids' => [$item_id, $form_id], 'kind' => 'updated'];
    }

    protected function resolveTarget(int $form_id, array $target, array $context = [], bool $allow_empty = false): ?array
    {
        $structure = is_array($context['structure'] ?? null) ? (array) $context['structure'] : [];
        $request_context = is_array($context['request_context'] ?? null) ? (array) $context['request_context'] : [];
        $request_prompt = sanitize_text_field((string) ($request_context['prompt'] ?? ''));

        if (empty($target)) {
            return $allow_empty ? null : null;
        }

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

        $item_id = absint($target['item_id'] ?? 0);
        if ($item_id > 0) {
            return [
                'scope' => $scope,
                'item_id' => $item_id,
                'item_label' => sanitize_text_field((string) ($target['item_label'] ?? '')),
                'prompt' => $request_prompt,
            ];
        }

        if ($scope === 'form' || $scope === 'selected') {
            $selected = is_array($context['selected_context'] ?? null) ? (array) $context['selected_context'] : [];
            if (!empty($selected['item_type']) && ($scope === 'selected' || $this->mapSelectedTypeToScope($selected['item_type']) === $scope)) {
                return [
                    'scope' => $this->mapSelectedTypeToScope($selected['item_type']),
                    'item_id' => absint($selected['item_id'] ?? 0),
                    'item_label' => sanitize_text_field((string) ($selected['item_label'] ?? '')),
                    'prompt' => $request_prompt,
                    'source' => 'implicit_resolution',
                ];
            }
            if ($scope === 'form') {
                return ['scope' => 'form', 'item_id' => $form_id, 'prompt' => $request_prompt];
            }
        }

        if ($scope === 'row') {
            $selected = is_array($context['selected_context'] ?? null) ? (array) $context['selected_context'] : [];
            $selected_type = sanitize_key((string) ($selected['item_type'] ?? ''));
            $parent_type = sanitize_key((string) ($selected['parent_type'] ?? ''));
            $selected_item_id = absint($selected['item_id'] ?? 0);
            $parent_item_id = absint($selected['parent_id'] ?? 0);

            if ($selected_item_id > 0 && in_array($selected_type, ['pagerow', 'row'], true)) {
                return [
                    'scope' => 'row',
                    'item_id' => $selected_item_id,
                    'item_label' => sanitize_text_field((string) ($selected['item_label'] ?? '')),
                    'prompt' => $request_prompt,
                    'source' => 'implicit_selection',
                ];
            }

            if ($parent_item_id > 0 && in_array($parent_type, ['pagerow', 'row'], true)) {
                return [
                    'scope' => 'row',
                    'item_id' => $parent_item_id,
                    'item_label' => sanitize_text_field((string) ($selected['item_label'] ?? '')),
                    'prompt' => $request_prompt,
                    'source' => 'implicit_selection',
                ];
            }
        }

        $match = (array) ($target['match'] ?? []);

        // Step 2: Semantic match resolution (label, name, etc.)
        if (!empty($match)) {
            $resolved_id = $this->resolveMatchToId($form_id, $scope, $match);
            if ($resolved_id > 0) {
                return [
                    'scope' => $scope,
                    'item_id' => $resolved_id,
                    'item_label' => sanitize_text_field((string) ($match['label'] ?? $target['item_label'] ?? '')),
                    'prompt' => $request_prompt,
                ];
            }

            // If match was requested but not found, we return a virtual object with a warning intent
            return [
                'scope' => $scope,
                'item_id' => 0,
                'unresolved_match' => $match,
                'warning' => sprintf(__('I couldn\'t find "%s".', 'griffinforms-form-builder'), sanitize_text_field((string) ($match['label'] ?? ''))),
            ];
        }

        // Step 4: Semantic button fallbacks for multi-page forms
        $pages = is_array($structure['pages'] ?? null) ? $structure['pages'] : [];
        $page_count = count($pages);
        $hint = strtolower((string) ($target['semantic_hint'] ?? ''));

        if ($scope === 'page' && $page_count === 2) {
            if (str_contains($hint, 'next')) {
                return [
                    'scope' => 'page',
                    'item_id' => absint($pages[0]['item_id'] ?? 0),
                    'prompt' => $request_prompt,
                    'source' => 'semantic_fallback_next',
                ];
            }
            if (str_contains($hint, 'previous')) {
                return [
                    'scope' => 'page',
                    'item_id' => absint($pages[1]['item_id'] ?? 0),
                    'prompt' => $request_prompt,
                    'source' => 'semantic_fallback_prev',
                ];
            }
        }

        // Step 5: Implicit selection resolution
        $selected = is_array($context['selected_context'] ?? null) ? (array) $context['selected_context'] : [];
        if (!empty($selected['item_type']) && $this->mapSelectedTypeToScope($selected['item_type']) === $scope) {
            return [
                'scope' => $scope,
                'item_id' => absint($selected['item_id'] ?? 0),
                'item_label' => sanitize_text_field((string) ($selected['item_label'] ?? '')),
                'prompt' => $request_prompt,
                'source' => 'implicit_selection',
            ];
        }
        return $allow_empty ? null : null;
    }

    protected function mapSelectedTypeToScope(string $selected_type): string
    {
        $selected_type = sanitize_key($selected_type);

        return match ($selected_type) {
            'form' => 'form',
            'formpage', 'page' => 'page',
            'pagerow', 'row' => 'row',
            'rowcolumn', 'column' => 'column',
            'field' => 'field',
            default => '',
        };
    }

    protected function updateRecord(string $type, int $item_id, array $data): void
    {
        global $wpdb;

        if ($item_id <= 0 || empty($data)) {
            return;
        }

        $table = $this->config->getTable($type);
        $data['last_editor'] = get_current_user_id();
        $data['last_edited'] = current_time('mysql', true);

        $wpdb->update(
            $table,
            $data,
            ['id' => $item_id]
        );
    }

    protected function mapFormChanges(array $changes): array
    {
        $data = [];
        foreach (['name', 'heading', 'description', 'submit_label'] as $property) {
            if (!array_key_exists($property, $changes)) {
                continue;
            }

            $data[$property] = $property === 'description'
                ? sanitize_textarea_field((string) $changes[$property])
                : sanitize_text_field((string) $changes[$property]);
        }

        return $data;
    }

    protected function mapPageChanges(array $changes, array $page_payload = []): array
    {
        $combined = array_merge($page_payload, $changes);
        $data = [];
        foreach (['name', 'heading', 'description', 'previous_label', 'next_label'] as $property) {
            if (!array_key_exists($property, $combined)) {
                continue;
            }

            $data[$property] = $property === 'description'
                ? sanitize_textarea_field((string) $combined[$property])
                : sanitize_text_field((string) $combined[$property]);
        }

        return $data;
    }

    protected function mapRowChanges(array $changes, array $row_payload = []): array
    {
        $combined = array_merge($row_payload, $changes);
        $data = [];
        foreach (['name', 'heading', 'description'] as $property) {
            if (!array_key_exists($property, $combined)) {
                continue;
            }

            $data[$property] = $property === 'description'
                ? sanitize_textarea_field((string) $combined[$property])
                : sanitize_text_field((string) $combined[$property]);
        }

        return $data;
    }

    protected function mapColumnChanges(array $changes, array $column_payload = []): array
    {
        $combined = array_merge($column_payload, $changes);
        $data = [];
        foreach (['name', 'heading', 'description'] as $property) {
            if (!array_key_exists($property, $combined)) {
                continue;
            }

            $data[$property] = $property === 'description'
                ? sanitize_textarea_field((string) $combined[$property])
                : sanitize_text_field((string) $combined[$property]);
        }

        if (array_key_exists('width', $combined)) {
            $width = absint($combined['width']);
            if ($width >= 3 && $width <= 12) {
                $data['width'] = $width;
            }
        }

        return $data;
    }

    protected function mapFieldChanges(array $changes, array $field_payload = []): array
    {
        $combined = array_merge($field_payload, $changes);
        $data = [];
        $property_map = [
            'name' => 'name',
            'label' => 'heading',
            'description' => 'description',
            'placeholder' => 'placeholder',
        ];

        foreach ($property_map as $source => $target) {
            if (!array_key_exists($source, $combined)) {
                continue;
            }

            $value = $source === 'description'
                ? sanitize_textarea_field((string) $combined[$source])
                : sanitize_text_field((string) $combined[$source]);

            // Skip empty placeholder only when it was not explicitly set in $changes
            // (i.e. it came from field_payload fallback). An explicit empty string means "remove".
            if ($source === 'placeholder' && $value === '' && !array_key_exists('placeholder', $changes)) {
                continue;
            }

            $data[$target] = $value;
        }

        if (array_key_exists('required', $combined)) {
            $data['is_required'] = !empty($combined['required']) ? 1 : 0;
        }

        if (!empty($combined['options']) && is_array($combined['options'])) {
            $field_type = sanitize_key((string) ($combined['field_type'] ?? $this->inferFieldTypeFromPayload($combined)));
            $data['field_options'] = maybe_serialize($this->mapFieldOptions($field_type, (array) $combined['options']));
        }

        foreach (['field_settings', 'validations'] as $key) {
            if (isset($combined[$key]) && is_array($combined[$key])) {
                $data[$key] = maybe_serialize($combined[$key]);
            }
        }

        if (!empty($combined['field_options']) && is_array($combined['field_options'])) {
            $data['field_options'] = maybe_serialize($combined['field_options']);
        }

        return $data;
    }

    protected function mapFieldInsertData(array $field_payload): array
    {
        $field_type = sanitize_key((string) ($field_payload['field_type'] ?? 'inputtext'));

        $options = !empty($field_payload['options']) && is_array($field_payload['options'])
            ? maybe_serialize($this->mapFieldOptions($field_type, (array) $field_payload['options']))
            : maybe_serialize([]);

        $data = [
            'field_type' => $field_type,
            'name' => sanitize_text_field((string) ($field_payload['name'] ?? $field_payload['label'] ?? 'Field')),
            'heading' => sanitize_text_field((string) ($field_payload['label'] ?? 'Field')),
            'description' => sanitize_textarea_field((string) ($field_payload['description'] ?? '')),
            'placeholder' => sanitize_text_field((string) ($field_payload['placeholder'] ?? '')),
            'is_required' => !empty($field_payload['required']) ? 1 : 0,
            'field_options' => $options,
            'is_reusable' => 0,
        ];

        if ($field_type === 'inputlikert') {
            $data['field_settings'] = maybe_serialize($this->mapLikertFieldOptions($field_payload));
        }

        foreach (['field_settings', 'validations'] as $key) {
            if (isset($field_payload[$key]) && is_array($field_payload[$key])) {
                $data[$key] = maybe_serialize($field_payload[$key]);
            }
        }

        return $data;
    }

    protected function mapLikertFieldOptions(array $field_payload): array
    {
        $raw_rows = is_array($field_payload['likert_rows'] ?? null) ? $field_payload['likert_rows'] : [];
        $rows = [];
        foreach ($raw_rows as $row) {
            if (!is_array($row) || !isset($row['id'], $row['label'])) {
                continue;
            }
            $rows[] = [
                'id'    => sanitize_key((string) $row['id']),
                'label' => sanitize_text_field((string) $row['label']),
            ];
        }

        $raw_cols = is_array($field_payload['likert_columns'] ?? null) ? $field_payload['likert_columns'] : [];
        $columns = [];
        foreach ($raw_cols as $col) {
            if (!is_array($col) || !isset($col['id'], $col['label'])) {
                continue;
            }
            $columns[] = [
                'id'    => sanitize_key((string) $col['id']),
                'label' => sanitize_text_field((string) $col['label']),
            ];
        }

        if (count($columns) < 2) {
            $columns = [
                ['id' => 'col_1', 'label' => 'Strongly Disagree'],
                ['id' => 'col_2', 'label' => 'Disagree'],
                ['id' => 'col_3', 'label' => 'Neutral'],
                ['id' => 'col_4', 'label' => 'Agree'],
                ['id' => 'col_5', 'label' => 'Strongly Agree'],
            ];
        }

        $allowed_req_modes  = ['all', 'none'];
        $allowed_col_widths = ['auto', 'equal'];

        $result = [
            'rows'          => array_slice($rows, 0, 20),
            'columns'       => array_slice($columns, 0, 10),
            'allow_na'      => !empty($field_payload['likert_allow_na']),
            'required_mode' => in_array($field_payload['likert_required_mode'] ?? '', $allowed_req_modes, true) ? $field_payload['likert_required_mode'] : 'all',
            'column_width'  => in_array($field_payload['likert_column_width']  ?? '', $allowed_col_widths, true) ? $field_payload['likert_column_width'] : 'auto',
        ];

        return $result;
    }

    /**
     * Pre-flight check: simulate column width operations against the structure snapshot.
     * Returns an error string if any row's column widths would sum to more than 12, null otherwise.
     */
    protected function validateRowColumnWidths(array $operations, array $structure): ?string
    {
        // Build per-row column-width maps from the structure snapshot.
        // $row_widths[ row_id ][ col_id ] = width
        $row_widths = [];
        $col_to_row = []; // col_id → row_id reverse map

        foreach ((array) ($structure['pages'] ?? []) as $page) {
            foreach ((array) ($page['rows'] ?? []) as $row) {
                $row_id = absint($row['item_id'] ?? 0);
                if ($row_id <= 0) {
                    continue;
                }
                $row_widths[$row_id] = [];
                foreach ((array) ($row['columns'] ?? []) as $col) {
                    $col_id = absint($col['item_id'] ?? 0);
                    if ($col_id <= 0) {
                        continue;
                    }
                    $width = absint($col['width'] ?? 12);
                    $row_widths[$row_id][$col_id] = $width;
                    $col_to_row[$col_id] = $row_id;
                }
            }
        }

        // Simulate each column operation on our in-memory maps.
        foreach ($operations as $operation) {
            $op = sanitize_key((string) ($operation['op'] ?? ''));
            $target = (array) ($operation['target'] ?? []);
            $target_id = absint($target['item_id'] ?? 0);

            if ($op === 'update_column') {
                $new_width = absint(($operation['changes']['width'] ?? 0));
                if ($new_width <= 0) {
                    continue; // no width change in this update
                }
                $row_id = $col_to_row[$target_id] ?? 0;
                if ($row_id > 0 && isset($row_widths[$row_id][$target_id])) {
                    $row_widths[$row_id][$target_id] = $new_width;
                }
            } elseif ($op === 'insert_column') {
                $new_width = absint(($operation['column']['width'] ?? 12));
                // target for insert_column is the row
                $row_id = $target_id;
                if (isset($row_widths[$row_id])) {
                    // Use a temporary negative key so it doesn't collide with real column IDs.
                    $temp_key = -count($row_widths[$row_id]) - 1;
                    $row_widths[$row_id][$temp_key] = $new_width;
                }
            } elseif ($op === 'remove_column') {
                $row_id = $col_to_row[$target_id] ?? 0;
                if ($row_id > 0) {
                    unset($row_widths[$row_id][$target_id]);
                }
            }
        }

        // Validate that no row's columns sum to more than 12.
        foreach ($row_widths as $row_id => $cols) {
            if (empty($cols)) {
                continue;
            }
            $total = array_sum($cols);
            if ($total > 12) {
                return __('Griffin Assist produced a column layout that would exceed the 12-column grid. Please try rephrasing your request.', 'griffinforms-form-builder');
            }
        }

        return null;
    }

    protected function buildEvenColumnWidths(int $count): array
    {
        $count = max(1, $count);
        $base = intdiv(12, $count);
        $remainder = 12 - ($base * $count);
        $widths = [];

        for ($i = 0; $i < $count; $i++) {
            $widths[] = max(1, $base + ($i < $remainder ? 1 : 0));
        }

        return $widths;
    }

    protected function mapFieldOptions(string $field_type, array $options): array
    {
        if (in_array($field_type, ['inputcheckbox', 'inputradio'], true)) {
            $mapped_groups = [];
            foreach ($options as $group) {
                $group_map = [];
                foreach ((array) ($group['options'] ?? []) as $option) {
                    $label = sanitize_textarea_field((string) ($option['label'] ?? ''));
                    $value = sanitize_key((string) ($option['value'] ?? ''));
                    if ($label === '' || $value === '') {
                        continue;
                    }
                    $group_map[$label] = $value;
                }
                if (!empty($group_map)) {
                    $mapped_groups[] = $group_map;
                }
            }
            return $mapped_groups;
        }

        if ($field_type === 'selectfield') {
            // Flat array of {label, value} objects from the model — wrap under the empty group key.
            $is_flat = array_is_list($options) && isset($options[0]) && is_array($options[0]) && array_key_exists('label', $options[0]);
            if ($is_flat) {
                $group_map = [];
                foreach ($options as $option) {
                    $label = sanitize_textarea_field((string) ($option['label'] ?? ''));
                    $value = sanitize_key((string) ($option['value'] ?? ''));
                    if ($label === '' || $value === '') {
                        continue;
                    }
                    $group_map[$label] = $value;
                }
                return !empty($group_map) ? ['' => $group_map] : [];
            }

            $mapped = [];
            foreach ($options as $group_name => $group) {
                $group_name = is_string($group_name) ? sanitize_textarea_field($group_name) : '';
                $group_map = [];

                if (is_array($group) && isset($group['options']) && is_array($group['options'])) {
                    foreach ($group['options'] as $option) {
                        $label = sanitize_textarea_field((string) ($option['label'] ?? ''));
                        $value = sanitize_key((string) ($option['value'] ?? ''));
                        if ($label === '' || $value === '') {
                            continue;
                        }
                        $group_map[$label] = $value;
                    }
                } elseif (is_array($group)) {
                    foreach ($group as $option_label => $option_value) {
                        if (is_array($option_value)) {
                            $label = sanitize_textarea_field((string) ($option_value['label'] ?? ''));
                            $value = sanitize_key((string) ($option_value['value'] ?? ''));
                        } else {
                            $label = sanitize_textarea_field((string) $option_label);
                            $value = sanitize_key((string) $option_value);
                        }

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

                if (!empty($group_map)) {
                    $mapped[$group_name !== '' ? $group_name : ''] = $group_map;
                }
            }

            if (!empty($mapped)) {
                return $mapped;
            }

            return [];
        }

        if ($field_type === 'payment') {
            $products = [];
            foreach ($options as $item) {
                if (!is_array($item)) {
                    continue;
                }
                $name = sanitize_text_field((string) ($item['name'] ?? ''));
                if ($name === '') {
                    continue;
                }
                $raw_id = sanitize_key((string) ($item['id'] ?? ''));
                if ($raw_id === '') {
                    $raw_id = 'prod_' . substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, 6);
                } elseif (!str_starts_with($raw_id, 'prod_')) {
                    $raw_id = 'prod_' . substr($raw_id, 0, 6);
                }
                $products[] = [
                    'id'             => $raw_id,
                    'name'           => $name,
                    'price'          => round((float) ($item['price'] ?? 0), 2),
                    'description'    => sanitize_text_field((string) ($item['description'] ?? '')),
                    'image'          => '',
                    'link'           => '',
                    'default_qty'    => 1,
                    'min_qty'        => 1,
                    'max_qty'        => 1,
                    'allow_quantity' => 0,
                    'featured'       => 0,
                ];
            }
            return $products;
        }

        $mapped = [];
        foreach ($options as $option) {
            $label = sanitize_textarea_field((string) ($option['label'] ?? ''));
            $value = sanitize_key((string) ($option['value'] ?? ''));
            if ($label === '' || $value === '') {
                continue;
            }
            $mapped[$label] = $value;
        }

        return [$mapped];
    }

    protected function inferFieldTypeFromPayload(array $combined): string
    {
        return sanitize_key((string) ($combined['field_type'] ?? ''));
    }

    protected function insertIntoList(array $list, int $item_id, int $reference_id, string $position): array
    {
        $list = array_values(array_filter(array_map('absint', $list)));
        $list = array_values(array_filter($list, static function ($id) use ($item_id) {
            return $id !== $item_id;
        }));

        if ($position === 'first') {
            array_unshift($list, $item_id);
            return $list;
        }

        if ($position === 'last' || $reference_id <= 0) {
            $list[] = $item_id;
            return $list;
        }

        $index = array_search($reference_id, $list, true);
        if ($index === false) {
            $list[] = $item_id;
            return $list;
        }

        $insert_index = $position === 'before' ? $index : $index + 1;
        array_splice($list, $insert_index, 0, [$item_id]);
        return array_values($list);
    }

    protected function moveWithinList(array $list, int $item_id, int $reference_id, string $position): array
    {
        $list = array_values(array_filter(array_map('absint', $list)));
        $list = array_values(array_filter($list, static function ($id) use ($item_id) {
            return $id !== $item_id;
        }));

        if ($position === 'first') {
            array_unshift($list, $item_id);
            return $list;
        }

        if ($position === 'last' || $reference_id <= 0) {
            $list[] = $item_id;
            return $list;
        }

        $index = array_search($reference_id, $list, true);
        if ($index === false) {
            $list[] = $item_id;
            return $list;
        }

        $insert_index = $position === 'before' ? $index : $index + 1;
        array_splice($list, $insert_index, 0, [$item_id]);
        return array_values($list);
    }

    protected function resolveParentPageIdForInsert(int $form_id, array $target): int
    {
        $scope = sanitize_key((string) ($target['scope'] ?? ''));
        $item_id = absint($target['item_id'] ?? 0);

        if ($scope === 'page') {
            if ($item_id > 0 && $this->format_sql->itemExists('formpage', $item_id)) {
                return $item_id;
            }

            return $this->getLastPageId($form_id);
        }

        if ($scope === 'row' && $item_id > 0) {
            return $this->findParentPageIdForRow($form_id, $item_id);
        }

        // Fallback: Default to last page for broad form-level requests
        if ($scope === 'form') {
            return $this->getLastPageId($form_id);
        }

        return 0;
    }

    protected function resolveParentRowIdForInsert(int $form_id, array $target, array $batch_state = []): int
    {
        $scope = sanitize_key((string) ($target['scope'] ?? ''));
        $item_id = absint($target['item_id'] ?? 0);

        if ($scope === 'row') {
            return $item_id > 0 ? $item_id : $this->getLastRowId($form_id, $this->getLastPageId($form_id));
        }

        if ($scope === 'column' && $item_id > 0) {
            return $this->findParentRowIdForColumn($form_id, $item_id);
        }

        // If a row was inserted earlier in this batch, prefer it over the positionally-last row.
        $last_inserted_row = absint($batch_state['last_inserted_row_id'] ?? 0);
        if ($last_inserted_row > 0) {
            return $last_inserted_row;
        }

        // Fallback: Resolve downward from form or page to the last existing row
        $page_id = 0;
        if ($scope === 'form') {
            $page_id = $this->getLastPageId($form_id);
        } elseif ($scope === 'page' && $item_id > 0) {
            $page_id = $item_id;
        }

        if ($page_id > 0) {
            return $this->getLastRowId($form_id, $page_id);
        }

        return 0;
    }

    protected function resolveParentColumnIdForInsert(int $form_id, array $target, array &$batch_state = []): int
    {
        $scope = sanitize_key((string) ($target['scope'] ?? ''));
        $item_id = absint($target['item_id'] ?? 0);

        if ($scope === 'column') {
            return $item_id > 0 ? $item_id : $this->getLastColumnId($form_id, $this->getLastRowId($form_id, $this->getLastPageId($form_id)));
        }

        if ($scope === 'field' && $item_id > 0) {
            return $this->findParentColumnIdForField($form_id, $item_id);
        }

        // If a row was inserted earlier in this batch, consume its column IDs in order
        // (field 1 → column 1, field 2 → column 2, …). Falls back to last column when
        // the queue is exhausted (more fields than columns).
        $last_inserted_row = absint($batch_state['last_inserted_row_id'] ?? 0);
        if ($last_inserted_row > 0) {
            $col_queue = &$batch_state['last_inserted_row_column_ids'];
            if (!empty($col_queue)) {
                return (int) array_shift($col_queue);
            }
            return $this->getLastColumnId($form_id, $last_inserted_row);
        }

        // Fallback: Resolve downward to the last existing column
        $row_id = 0;
        if ($scope === 'form' || $scope === 'page') {
            $row_id = $this->resolveParentRowIdForInsert($form_id, $target, $batch_state);
        } elseif ($scope === 'row' && $item_id > 0) {
            $row_id = $item_id;
        }

        if ($row_id > 0) {
            return $this->getLastColumnId($form_id, $row_id);
        }

        return 0;
    }

    protected function findParentPageIdForRow(int $form_id, int $row_id): int
    {
        $structure = $this->snapshot_builder->buildFromForm($form_id);
        foreach ((array) ($structure['pages'] ?? []) as $page) {
            foreach ((array) ($page['rows'] ?? []) as $row) {
                if (absint($row['item_id'] ?? 0) === $row_id) {
                    return absint($page['item_id'] ?? 0);
                }
            }
        }
        return 0;
    }

    protected function findParentRowIdForColumn(int $form_id, int $column_id): int
    {
        $structure = $this->snapshot_builder->buildFromForm($form_id);
        foreach ((array) ($structure['pages'] ?? []) as $page) {
            foreach ((array) ($page['rows'] ?? []) as $row) {
                foreach ((array) ($row['columns'] ?? []) as $column) {
                    if (absint($column['item_id'] ?? 0) === $column_id) {
                        return absint($row['item_id'] ?? 0);
                    }
                }
            }
        }
        return 0;
    }

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

    protected function updateFormPatchMetadata(int $form_id, array $patch_plan, string $source_prompt, array $summary): void
    {
        $integration_settings = $this->getFormIntegrationSettings($form_id);
        $integration_settings['ai_draft'] = array_merge(
            (array) ($integration_settings['ai_draft'] ?? []),
            [
                'last_prompt' => sanitize_textarea_field($source_prompt),
                'last_patch_plan' => $patch_plan,
                'last_patch_applied_at' => current_time('mysql', true),
                'last_patch_summary' => $summary,
            ]
        );

        $this->updateRecord('form', $form_id, [
            'integration_settings' => maybe_serialize($integration_settings),
        ]);
    }

    protected function getFormIntegrationSettings(int $form_id): array
    {
        $settings = $this->decodeValue($this->format_sql->getItemValue($form_id, 'integration_settings', 'form'));
        return is_array($settings) ? $settings : [];
    }

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

    protected function getLastPageId(int $form_id): int
    {
        $formpages = $this->decodeValue($this->format_sql->getItemValue($form_id, 'formpages', 'form'));
        if (!is_array($formpages) || empty($formpages)) {
            return 0;
        }
        return absint(end($formpages));
    }

    protected function getLastRowId(int $form_id, int $page_id): int
    {
        $rows = $this->decodeValue($this->format_sql->getItemValue($page_id, 'pagerows', 'formpage'));
        if (!is_array($rows) || empty($rows)) {
            return 0;
        }
        return absint(end($rows));
    }

    protected function getLastColumnId(int $form_id, int $row_id): int
    {
        $columns = $this->decodeValue($this->format_sql->getItemValue($row_id, 'rowcolumns', 'pagerow'));
        if (!is_array($columns) || empty($columns)) {
            return 0;
        }
        return absint(end($columns));
    }

    protected function resolveMatchToId(int $form_id, string $scope, array $match): int
    {
        $structure = $this->snapshot_builder->buildFromForm($form_id);
        $target_label = strtolower(trim((string) ($match['label'] ?? '')));
        $target_name = strtolower(trim((string) ($match['name'] ?? '')));

        if ($scope === 'field') {
            foreach ((array) ($structure['fields'] ?? []) as $field) {
                $field_label = strtolower(trim((string) ($field['label'] ?? '')));
                $field_name = strtolower(trim((string) ($field['name'] ?? '')));
                if (($target_label !== '' && $field_label === $target_label) || ($target_name !== '' && $field_name === $target_name)) {
                    return absint($field['item_id'] ?? 0);
                }
            }
        }

        if ($scope === 'page') {
            foreach ((array) ($structure['pages'] ?? []) as $page) {
                $page_name = strtolower(trim((string) ($page['name'] ?? '')));
                $page_heading = strtolower(trim((string) ($page['heading'] ?? '')));
                if (($target_label !== '' && $page_name === $target_label) || ($target_label !== '' && $page_heading === $target_label)) {
                    return absint($page['item_id'] ?? 0);
                }
            }
        }

        if ($scope === 'row') {
            foreach ((array) ($structure['pages'] ?? []) as $page) {
                foreach ((array) ($page['rows'] ?? []) as $row) {
                    $row_name = strtolower(trim((string) ($row['name'] ?? '')));
                    $row_heading = strtolower(trim((string) ($row['heading'] ?? '')));
                    if (($target_label !== '' && ($row_name === $target_label || $row_heading === $target_label))
                        || ($target_name !== '' && $row_name === $target_name)) {
                        return absint($row['item_id'] ?? 0);
                    }
                }
            }
        }

        return 0;
    }

    protected function stitchTruthfulMessage(string $original_message, array $warnings): string
    {
        // If we have a warning like "I couldn't find X. So I added it at the end.",
        // we should prefer that over the AI's optimistic claim.
        return end($warnings);
    }
}
