<?php

namespace GriffinForms\Frontend\Ajax;

use \GriffinForms\Frontend\Ajax\ManageDbSubmission;
use \GriffinForms\Frontend\Ajax\FileHandler;
use GriffinForms\Includes\Helpers\Compliance;
use GriffinForms\Includes\ImportExport\FormExport;

class Submission extends \GriffinForms\Frontend\Format
{
    use ManageDbSubmission, FileHandler;

    protected $config;
    protected $sql;
    protected $log;
    protected $antispam;
    protected $data;
    protected $submission;
    protected $response = array();
    /**
     * Cache payment detection per form to avoid repeated tree parsing.
     *
     * @var array<int,bool>
     */
    protected $form_payment_map = [];
    /**
     * Cache payment field IDs per form.
     *
     * @var array<int,array<int>>
     */
    protected $form_payment_field_ids_map = [];

    protected function setSql()
    {
        $this->sql = \GriffinForms\Frontend\Sql\Submission::getInstance();
    }

    protected function setAntiSpam()
    {
        $this->antispam = \GriffinForms\Frontend\Html\Security\AntiSpam::getInstance();
    }

    /**
     * Enforce rate limiting for page submissions.
     * Performs a fast check and exits with a friendly message if limited.
     * Otherwise, marks this submit attempt.
     */
    protected function enforceRateLimit()
    {
        // Bot limiter is optional; bail safely if class missing
        if (!class_exists('GriffinForms\\Frontend\\Security\\BotLimiter')) {
            if (isset($this->log)) {
                $this->log->add(
                    'warning',
                    'submission',
                    0,
                    get_current_user_id(),
                    'BotLimiter class missing, skipping rate limit enforcement.',
                    'security'
                );
            }
            return;
        }

        $form_id = isset($this->data['form']) ? (int) $this->data['form'] : 0;
        if ($form_id <= 0) {
            if (isset($this->log)) {
                $this->log->add(
                    'warning',
                    'submission',
                    0,
                    get_current_user_id(),
                    'Invalid form_id detected during rate limit enforcement.',
                    'security'
                );
            }
            return;
        }

        $limiter = \GriffinForms\Frontend\Security\BotLimiter::getInstance();
        $limiter->init($form_id);

        if ($limiter->isRateLimited()) {
            if (isset($this->log)) {
                $this->log->add(
                    'error',
                    'submission',
                    0,
                    get_current_user_id(),
                    "Rate limit exceeded for form_id {$form_id}",
                    'security'
                );
            }
            $this->response['success'] = false;
            $this->response['message'] = [ __('You have made too many attempts in a short period. Please wait a few moments before trying again. This helps us prevent spam and abuse. Thank you for your patience!', 'griffinforms-form-builder') ];
            echo wp_json_encode($this->response);
            wp_die();
        }

        // Record this submission attempt
        $limiter->markEvent();
    }
    
    public function saveFormpage()
    {
        // Check if nonce is set
        if (isset($_POST['nonce'])) {
            // Verify the nonce
            $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
            $nonce_status = wp_verify_nonce($nonce, 'save_formpage');
    
            if ($nonce_status !== false) {
                // Ensure $_POST data is properly set before using it
                if (isset($_POST['form'])) {

                    $this->sanitizeAndAssignPostData();

                    if (method_exists($this->antispam, 'setFormContext') && isset($this->data['form'])) {
                        $this->antispam->setFormContext((int) $this->data['form']);
                    }

                    // Enforce rate limiting early
                    $this->enforceRateLimit();

                    // Call the server-side validation method

                    if (!$this->sql->itemExists('form', $this->data['form'])) {
                        $this->response['success'] = false;
                        $this->response['message'][] = __('This form no longer exists.', 'griffinforms-form-builder');
                        $this->logAjaxEvent(
                            'error',
                            'saveFormpage aborted: form not found (ID ' . (int) $this->data['form'] . ').',
                            'data'
                        );
                        echo wp_json_encode($this->response);
                        wp_die();
                    }

                    $is_edit = $this->isEditSession();
                    if ($is_edit) {
                        $this->loadSubmission();
                        $this->response['edit'] = true;
                    } else {
                        $this->response['edit'] = false;
                    }

                    $submission_context = [];
                    if ($is_edit && !empty($this->data['sub_id'])) {
                        $submission_context = \GriffinForms\Includes\Pipelines\SubmissionContextBuilder::getInstance()->build((int) $this->data['sub_id']);
                    }

                    $guard_manager = new \GriffinForms\Frontend\Security\FormGuardManager();
                    $guard_result = $guard_manager->evaluate((int) $this->data['form'], [
                        'context' => 'submission',
                        'is_edit' => $is_edit,
                        'submission_id' => isset($this->data['sub_id']) ? (int) $this->data['sub_id'] : 0,
                        'submission_token' => isset($this->data['sub_token']) ? (string) $this->data['sub_token'] : '',
                        'submission_context' => $submission_context,
                        'position' => $this->data['position'] ?? ''
                    ]);

                    if (!empty($guard_result['blocked'])) {
                        $message = isset($guard_result['message']) ? $guard_result['message'] : __('This form is not accepting submissions.', 'griffinforms-form-builder');
                        $guard_key = isset($guard_result['key']) ? $guard_result['key'] : 'unknown';
                        $this->response['success'] = false;
                        $this->response['message'][] = $message;
                        $this->logAjaxEvent(
                            'warning',
                            'saveFormpage blocked by guard (' . $guard_key . '): ' . $message,
                            'validation'
                        );
                        echo wp_json_encode($this->response);
                        wp_die();
                    }

                    // **Step 1: Perform antispam assessment**
                    $this->antispam->assessSpam($this->data);

                    if ($this->antispam->hasErrors()) {
                        $this->response['success'] = false;
                        $this->response['message'] = $this->antispam->getErrorNotices();
                        $this->logAjaxEvent(
                            'warning',
                            'saveFormpage blocked by antispam: ' . wp_json_encode($this->response['message']),
                            'security'
                        );
                        echo wp_json_encode($this->response);
                        wp_die();
                    }

                    
                    // **Step 2: Perform field validation only after spam test passes**
                    $validation_result = $this->validateSubmissionFields();

                    if ($validation_result !== true) {
                        // If validation failed, send the error messages and exit
                        $this->response['success'] = false;
                        $this->response['message'] = $this->formatValidationErrors($validation_result);
                        $this->logAjaxEvent(
                            'warning',
                            'saveFormpage validation failed: ' . wp_json_encode($this->response['message']),
                            'validation'
                        );
                        echo wp_json_encode($this->response);
                        wp_die();
                    }

                    $form_validation_result = $this->validateFormLevelSubmission();

                    if ($form_validation_result !== true) {
                        $this->response['success'] = false;
                        $this->response['message'] = $this->formatValidationErrors($form_validation_result);
                        $this->logAjaxEvent(
                            'warning',
                            'saveFormpage form-level validation failed: ' . wp_json_encode($this->response['message']),
                            'validation'
                        );
                        echo wp_json_encode($this->response);
                        wp_die();
                    }
    
                    if ($is_edit) {
                        $this->saveEditSubmission();
                    } elseif ($this->isContinuation()) {
                        $this->saveContinuationPage();
                    } else {
                        $this->saveNewSubmission();
                    }
    
	                    // Retention cleanup moved to scheduler to avoid slowing submissions.
	                    $this->response['success'] = true;
	                    $form_name = $this->resolveFormName((int) ($this->data['form'] ?? 0));
	                    $submission_id = isset($this->data['sub_id']) ? (int) $this->data['sub_id'] : 0;
	                    $position = sanitize_text_field($this->data['position'] ?? '');
	                    $is_complete = in_array($position, array('single', 'last'), true);
	                    if ($is_edit) {
	                        if ($is_complete) {
	                            /* translators: 1: form name, 2: submission id */
	                            $log_message = sprintf(
	                                __('Submission edited for "%1$s" (Submission #%2$d).', 'griffinforms-form-builder'),
	                                $form_name,
	                                $submission_id
	                            );
	                        } else {
	                            /* translators: 1: form name, 2: submission id */
	                            $log_message = sprintf(
	                                __('Submission edit progress saved for "%1$s" (Submission #%2$d).', 'griffinforms-form-builder'),
	                                $form_name,
	                                $submission_id
	                            );
	                        }
	                        $log_category = 'Submission Edit';
	                    } else {
	                        if ($is_complete) {
	                            /* translators: 1: form name, 2: submission id */
	                            $log_message = sprintf(
	                                __('Submission received for "%1$s" (Submission #%2$d).', 'griffinforms-form-builder'),
	                                $form_name,
	                                $submission_id
	                            );
	                        } else {
	                            /* translators: 1: form name, 2: submission id */
	                            $log_message = sprintf(
	                                __('Submission progress saved for "%1$s" (Submission #%2$d).', 'griffinforms-form-builder'),
	                                $form_name,
	                                $submission_id
	                            );
	                        }
	                        $log_category = 'Frontend Submission';
	                    }
	                    $this->logAjaxEvent(
	                        'info',
	                        $log_message,
	                        $log_category,
	                        $submission_id
	                    );
    
                } else {
                    // Handle missing 'form' data
                    $this->response['success'] = false;
                    $this->response['message'] = __('Form data is missing.', 'griffinforms-form-builder');
                    $this->logAjaxEvent(
                        'error',
                        'saveFormpage aborted: form data missing. Payload keys: ' . implode(', ', array_keys($_POST)),
                        'ajax'
                    );
                }
    
            } else {
                // Handle nonce verification failure
                $this->response['success'] = false;
                $this->response['message'] = __('Nonce verification failed.', 'griffinforms-form-builder');
                $this->logAjaxEvent(
                    'error',
                    'saveFormpage aborted: nonce verification failed.',
                    'security'
                );
            }
    
        } else {
            // Handle missing nonce
            $this->response['success'] = false;
            $this->response['message'] = __('Nonce not provided.', 'griffinforms-form-builder');
            $this->logAjaxEvent(
                'error',
                'saveFormpage aborted: nonce missing from request.',
                'security'
            );
        }
    
        // Return the response as a JSON response
        echo wp_json_encode($this->response);
        wp_die();
    }

    private function availabilityWindowMessage(int $form_id): ?string
    {
        if ($form_id <= 0) {
            return null;
        }

        $form = new \GriffinForms\Items\Form();
        if (!$form->loadItem($form_id)) {
            return null;
        }

        $window = $form->getProp('availability_window');
        if (!is_array($window) || empty($window['toggle'])) {
            return null;
        }

        $start_utc = isset($window['start_utc']) ? $window['start_utc'] : '';
        $end_utc = isset($window['end_utc']) ? $window['end_utc'] : '';
        $pre_message = isset($window['pre_message']) ? $window['pre_message'] : '';
        $post_message = isset($window['post_message']) ? $window['post_message'] : '';

        $default_pre = __('This form is not accepting submissions yet. Please check back later.', 'griffinforms-form-builder');
        $default_post = __('This form is no longer accepting submissions.', 'griffinforms-form-builder');

        $now = current_time('timestamp', true);

        if (!empty($start_utc)) {
            $start_ts = (new \DateTimeImmutable($start_utc, new \DateTimeZone('UTC')))->getTimestamp();
            if ($now < $start_ts) {
                return $pre_message !== '' ? $pre_message : $default_pre;
            }
        }

        if (!empty($end_utc)) {
            $end_ts = (new \DateTimeImmutable($end_utc, new \DateTimeZone('UTC')))->getTimestamp();
            if ($now > $end_ts) {
                return $post_message !== '' ? $post_message : $default_post;
            }
        }

        return null;
    }

    private function sanitizeAndAssignPostData()
    {
        $this->data = array(
            'nonce' => sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')),
            'form' => absint(wp_unslash($_POST['form'] ?? 0)),
            'sub_id' => isset($_POST['sub_id']) ? absint(wp_unslash($_POST['sub_id'])) : 0,
            'sub_token' => isset($_POST['sub_token']) ? sanitize_text_field(wp_unslash($_POST['sub_token'])) : '',
            'edit_session' => !empty($_POST['edit_session']) ? 1 : 0,
            'page_data' => wp_unslash($_POST['page_data'] ?? ''),
            'position' => isset($_POST['position']) ? sanitize_text_field(wp_unslash($_POST['position'])) : '',
            'page' => isset($_POST['page']) ? absint(wp_unslash($_POST['page'])) : 0,
            'antispam_data' => isset($_POST['antispam_data']) ? json_decode(wp_unslash($_POST['antispam_data']), true) : [],
            'submission_meta_event' => isset($_POST['submission_meta_event']) ? wp_unslash($_POST['submission_meta_event']) : '',
            'form_url' => isset($_POST['form_url']) ? esc_url_raw(wp_unslash($_POST['form_url'])) : ''
        );

        if (!is_array($this->data['antispam_data']) || json_last_error() !== JSON_ERROR_NONE) {
            $this->data['antispam_data'] = [];
        } else {
            $this->data['antispam_data'] = array_map('sanitize_text_field', $this->data['antispam_data']);
        }
        $raw_meta_event = $this->data['submission_meta_event'];
        if (is_string($raw_meta_event) && $raw_meta_event !== '') {
            $decoded_meta_event = json_decode($raw_meta_event, true);
            if (json_last_error() === JSON_ERROR_NONE && is_array($decoded_meta_event)) {
                $this->data['submission_meta_event'] = $this->sanitizeSubmissionMetaEvent($decoded_meta_event);
            } else {
                $this->data['submission_meta_event'] = [];
            }
        } elseif (is_array($raw_meta_event)) {
            $this->data['submission_meta_event'] = $this->sanitizeSubmissionMetaEvent($raw_meta_event);
        } else {
            $this->data['submission_meta_event'] = [];
        }


        // Fix for Checkbox Fields: Decode JSON string into an array for multiple iterations.
        // Scoped to inputcheckbox only — other field types that send JSON (e.g. inputlikert)
        // must not have their string values decoded into arrays here.
        if (!empty($this->data['page_data'])) {
            $decoded_page_data = json_decode($this->data['page_data'], true);

            if (is_array($decoded_page_data)) {
                foreach ($decoded_page_data as $field_id => $field_values) {
                    if (!$this->sql->itemExists('field', $field_id)) {
                        continue;
                    }
                    $field = new \GriffinForms\Items\Field();
                    $field->loadItem($field_id);
                    if ($field->getProp('field_type') !== 'inputcheckbox') {
                        continue;
                    }
                    if (isset($field_values) && is_array($field_values)) {
                        foreach ($field_values as $index => $value) {
                            if (is_string($value)) {
                                $decoded_values = json_decode($value, true);
                                if (is_array($decoded_values)) {
                                    $decoded_page_data[$field_id][$index] = $decoded_values;
                                }
                            }
                        }
                    }
                }

                $this->data['page_data'] = $decoded_page_data;
            }
        }
    }

    private function sanitizeSubmissionMetaEvent(array $event): array
    {
        $clean = array();
        $clean['type'] = isset($event['type']) ? sanitize_text_field($event['type']) : '';
        $clean['page_id'] = isset($event['page_id']) ? absint($event['page_id']) : 0;
        $clean['timestamp'] = $this->sanitizeIsoTimestamp($event['timestamp'] ?? '');
        $clean['form_opened_at'] = $this->sanitizeIsoTimestamp($event['form_opened_at'] ?? '');
        $clean['page_opened_at'] = $this->sanitizeIsoTimestamp($event['page_opened_at'] ?? '');
        return $clean;
    }

    private function sanitizeIsoTimestamp(string $value): string
    {
        $value = trim($value);
        if ($value === '') {
            return '';
        }
        return sanitize_text_field($value);
    }

    private function buildSubmissionEvent(
        int $page_id,
        array $client_event,
        bool $store_ip,
        bool $store_user_agent,
        string $client_ip,
        string $client_ua
    ): array {
        if (empty($client_event)) {
            return [];
        }
        $event = array();
        $event_type = $client_event['type'] ?? '';
        if ($event_type === '') {
            $event_type = 'page_submit';
        }
        $event['type'] = $event_type;
        $event['page_id'] = !empty($client_event['page_id']) ? (int) $client_event['page_id'] : $page_id;
        $event['timestamp'] = $client_event['timestamp'] ?? current_time('c', true);
        if (!empty($client_event['form_opened_at'])) {
            $event['form_opened_at'] = $client_event['form_opened_at'];
        }
        if (!empty($client_event['page_opened_at'])) {
            $event['page_opened_at'] = $client_event['page_opened_at'];
        }
        if ($store_ip && $client_ip !== '') {
            $event['ip'] = $client_ip;
        }
        if ($store_user_agent && $client_ua !== '') {
            $event['user_agent'] = $client_ua;
        }
        return array_filter($event, static function ($value) {
            return $value !== '' && $value !== null;
        });
    }

    private function mergeSubmissionMeta(array $existing_meta, array $event, array $context = []): array
    {
        $meta = is_array($existing_meta) ? $existing_meta : array();
        if (!isset($meta['events']) || !is_array($meta['events'])) {
            $meta['events'] = array();
        }
        if (!empty($event)) {
            $meta['events'][] = $event;
        }
        if (!empty($context)) {
            if (!isset($meta['compliance']) || !is_array($meta['compliance'])) {
                $meta['compliance'] = array();
            }
            foreach ($context as $key => $value) {
                if (!array_key_exists($key, $meta['compliance']) || $meta['compliance'][$key] === '' || $meta['compliance'][$key] === null) {
                    $meta['compliance'][$key] = $value;
                }
            }
        }
        return $meta;
    }

    private function buildSubmissionMetaContext(array $layout_context): array
    {
        $flags = isset($layout_context['compliance_flags']) && is_array($layout_context['compliance_flags'])
            ? $layout_context['compliance_flags']
            : array();
        $mode = !empty($layout_context['compliance_mode']) ? 1 : 0;
        $profile = isset($layout_context['compliance_profile']) ? (string) $layout_context['compliance_profile'] : 'standard';
        if ($mode === 0) {
            $profile = '';
            $flags = array();
        }

        return array(
            'mode' => $mode,
            'profile' => $profile,
            'flags' => $flags,
            'captured_at' => current_time('c', true),
        );
    }

    private function getSubmissionMeta(int $submission_id): array
    {
        if ($submission_id <= 0) {
            return array();
        }
        global $wpdb;
        $table = $this->config->getTable('submission');
        $raw = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT submission_meta FROM %i WHERE id = %d",
                $table,
                $submission_id
            )
        );
        if ($raw === null || $raw === '') {
            return array();
        }
        $decoded = $this->maybeDecode($raw);
        return is_array($decoded) ? $decoded : array();
    }

    public function recordSubmissionEvent()
    {
        if (!isset($_POST['nonce'])) {
            $this->response['success'] = false;
            $this->response['message'] = __('Nonce not provided.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }

        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        if (!wp_verify_nonce($nonce, 'save_formpage')) {
            $this->response['success'] = false;
            $this->response['message'] = __('Nonce verification failed.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }

        $form_id = absint(wp_unslash($_POST['form'] ?? 0));
        $sub_id = absint(wp_unslash($_POST['sub_id'] ?? 0));
        $sub_token = sanitize_text_field(wp_unslash($_POST['sub_token'] ?? ''));
        $raw_event = wp_unslash($_POST['submission_meta_event'] ?? '');

        if ($sub_id <= 0 || $sub_token === '') {
            $this->response['success'] = false;
            $this->response['message'] = __('Submission not found.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }

        $decoded_event = [];
        if (is_string($raw_event) && $raw_event !== '') {
            $decoded_event = json_decode($raw_event, true);
            if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded_event)) {
                $decoded_event = [];
            }
        } elseif (is_array($raw_event)) {
            $decoded_event = $raw_event;
        }

        $clean_event = $this->sanitizeSubmissionMetaEvent($decoded_event);

        global $wpdb;
        $table = $this->config->getTable('submission');
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT id, form_id FROM %i WHERE id = %d AND public_token = %s",
                $table,
                $sub_id,
                $sub_token
            ),
            ARRAY_A
        );

        if (!is_array($row)) {
            $this->response['success'] = false;
            $this->response['message'] = __('Submission not found.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }

        $resolved_form_id = isset($row['form_id']) ? (int) $row['form_id'] : 0;
        if ($form_id > 0 && $resolved_form_id > 0 && $resolved_form_id !== $form_id) {
            $this->response['success'] = false;
            $this->response['message'] = __('Submission form mismatch.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }

        $layout_context = $this->getFormComplianceContext($resolved_form_id);
        $flags = isset($layout_context['compliance_flags']) && is_array($layout_context['compliance_flags'])
            ? $layout_context['compliance_flags']
            : array();
        $store_ip = !empty($layout_context['compliance_mode']) && !empty($flags['store_ip']);
        $store_user_agent = !empty($layout_context['compliance_mode']) && !empty($flags['store_user_agent']);
        $client_ip = $store_ip ? $this->getClientIp() : '';
        $client_ua = $store_user_agent ? $this->getClientUserAgent() : '';
        $this->applyGdprMasking($layout_context['compliance_profile'] ?? 'standard', $store_ip, $store_user_agent, $client_ip, $client_ua);

        $event_payload = $this->buildSubmissionEvent(
            (int) ($clean_event['page_id'] ?? 0),
            $clean_event,
            $store_ip,
            $store_user_agent,
            $client_ip,
            $client_ua
        );

        $existing_meta = $this->getSubmissionMeta($sub_id);
        $merged = $this->mergeSubmissionMeta($existing_meta, $event_payload, $this->buildSubmissionMetaContext($layout_context));
        $update_data = array(
            'submission_meta' => wp_json_encode($merged)
        );
        $update_formats = array('%s');

        if ($store_ip && $client_ip !== '') {
            $update_data['submission_ip'] = $client_ip;
            $update_formats[] = '%s';
        }
        if ($store_user_agent && $client_ua !== '') {
            $update_data['submission_user_agent'] = $client_ua;
            $update_formats[] = '%s';
        }

        $updated = $wpdb->update(
            $table,
            $update_data,
            array('id' => $sub_id),
            $update_formats,
            array('%d')
        );

        if (!empty($this->log)) {
            $event_type = isset($event_payload['type']) ? (string) $event_payload['type'] : 'unknown';
            $event_page = isset($event_payload['page_id']) ? (int) $event_payload['page_id'] : 0;
            $this->log->add(
                'info',
                'submission',
                $sub_id,
                get_current_user_id(),
                sprintf('Submission meta event recorded (%s, page %d).', $event_type, $event_page),
                'audit'
            );
        }

        $this->response['success'] = $updated !== false;
        echo wp_json_encode($this->response);
        wp_die();
    }

    private function validateSubmissionFields()
    {
        $page_data = is_array($this->data['page_data'])
            ? $this->data['page_data']
            : json_decode(wp_unslash($this->data['page_data']), true);
    
        // Debug Log: Check for invalid page_data format
        if (!is_array($page_data)) {
            $this->log->add('error', 'submission', 0, get_current_user_id(), '⚠️ GriffinForms - Invalid page_data received: ' . print_r($this->data['page_data'], true), 'data');
            $page_data = [];
        }
    
        $errors = [];
        $sanitized_values = [];
    
        foreach ($page_data as $field_id => $field_iterations) {
            if (!$this->sql->itemExists('field', $field_id)) {
                // Log when a field no longer exists
                $this->log->add('error', 'submission', 0, get_current_user_id(), 'Field ID ' . absint($field_id) . ' no longer exists in the form.', 'data');
                /* translators: %d is the field ID */
                $errors[$field_id] = sprintf(__('Field ID %d no longer exists. Please refresh the form and try again, or contact the site administrator.', 'griffinforms-form-builder'), $field_id);
                continue;
            }
    
            $field = new \GriffinForms\Items\Field();
            $field->loadItem($field_id);
            $field_type = $field->getProp('field_type');

            $validator_class = $this->config->getFieldValidatorClass((string) $field_type);
    
            if (!class_exists($validator_class)) {
                /* translators: %s is the field type name such as "inputtext", "selectfield", etc. */
                $errors[$field_id] = sprintf(__('No validator exists for field type "%s".', 'griffinforms-form-builder'), $field_type);
                $this->log->add('warning', 'field', $field_id, get_current_user_id(), sprintf('Unavailable validator for field type "%s".', $field_type), 'package_provider');
                continue;
            }
    
    
            $field_validator = new $validator_class();
            $field_validator->setField($field);
            $validation_result = $field_validator->validateAllIterations($field_iterations);

            if ($validation_result['errors'] !== true) {
                $errors[$field_id] = $validation_result['errors'];
            } else {
                $sanitized_values[$field_id] = $validation_result['sanitized_values'];
            }
        }
    
        if (!empty($errors)) {
            return $errors;
        }

        $this->data['sanitized_page_data'] = $sanitized_values;
        return true;
    }

    private function validateFormLevelSubmission()
    {
        $form_id = isset($this->data['form']) ? absint($this->data['form']) : 0;
        if ($form_id <= 0) {
            return true;
        }

        $manager = new \GriffinForms\Frontend\Security\FormValidations\Manager();
        $errors = $manager->validate($form_id, [
            'page_data' => $this->data['page_data'] ?? [],
            'sanitized_page_data' => $this->data['sanitized_page_data'] ?? [],
        ]);

        return empty($errors) ? true : $errors;
    }

    // Format validation errors for display
    //
    // This method flattens the validation error structure into a simple array of error messages.
    // The $errors array may contain:
    // - entries where the value is a string (e.g., field deleted, or no validator exists)
    // - entries where the value is an array of iterations, each of which may be a string or array of strings
    // This function handles both formats and extracts all error messages into a single array.
    private function formatValidationErrors($errors)
    {
        $formatted_errors = [];

        // Iterate over each field's error block
        foreach ($errors as $field_id => $iterations) {
            // Handle simple string error (e.g., field deleted or no validator)
            if (!is_array($iterations)) {
                $formatted_errors[] = $iterations;
                continue;
            }

            // Iterate over iteration-based error sets
            foreach ($iterations as $iteration_index => $error_messages) {
                // Handle multiple error messages within a single iteration
                if (is_array($error_messages)) {
                    foreach ($error_messages as $error_message) {
                        $formatted_errors[] = $error_message;
                    }
                } else {
                    // Single message for the iteration
                    $formatted_errors[] = $error_messages;
                }
            }
        }

        return $formatted_errors;
    }
    
    private function isEditSession(): bool
    {
        return absint($this->data['sub_id']) > 0
            && strlen($this->data['sub_token']) === 40
            && !empty($this->data['edit_session']);
    }

    private function isContinuation(): bool
    {
        return absint($this->data['sub_id']) > 0
            && strlen($this->data['sub_token']) === 40
            && empty($this->data['edit_session']);
    }
    
    private function loadSubmission()
    {
        global $wpdb;
        $table = $this->config->getTable('submission');
        $sub_id = absint($this->data['sub_id']);
        $sub_token = sanitize_text_field($this->data['sub_token']);
    
        $cache_key = 'submission_' . $sub_id . '_' . $sub_token;
        $cache_group = 'griffinforms';
    
        // Try to get the submission from the cache
        $submission = wp_cache_get($cache_key, $cache_group);
    
        // If it's not cached, fetch it from the database
        if (false === $submission) {
            $submission = $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT submission FROM %i WHERE id = %d AND public_token = %s",
                    $table,
                    $sub_id,
                    $sub_token
                )
            );
    
            // If a submission is found, cache it
            if ($submission !== null) {
                wp_cache_set($cache_key, $submission, $cache_group, 86400);
            }
        }
    
        // If a valid submission is found, unserialize it
        if ($submission !== null) {
            $this->submission = $this->maybeDecode($submission);
        }
    }
    
    private function saveNewSubmission()
    {
        global $wpdb;
        $table = $this->config->getTable('submission');
        $token = $this->generatePublicToken();
        $form_id = (int) ($this->data['form'] ?? 0);
        $submission_payload = $this->pageData();
        $layout_context = $this->getFormComplianceContext($form_id);
        $flags = isset($layout_context['compliance_flags']) && is_array($layout_context['compliance_flags'])
            ? $layout_context['compliance_flags']
            : array();
        $store_ip = !empty($layout_context['compliance_mode']) && !empty($flags['store_ip']);
        $store_user_agent = !empty($layout_context['compliance_mode']) && !empty($flags['store_user_agent']);
        $client_ip = $store_ip ? $this->getClientIp() : '';
        $client_ua = $store_user_agent ? $this->getClientUserAgent() : '';
        $this->applyGdprMasking($layout_context['compliance_profile'] ?? 'standard', $store_ip, $store_user_agent, $client_ip, $client_ua);
        $submission_event = $this->buildSubmissionEvent(
            (int) ($this->data['page'] ?? 0),
            $this->data['submission_meta_event'] ?? array(),
            $store_ip,
            $store_user_agent,
            $client_ip,
            $client_ua
        );
        $submission_meta_payload = $this->mergeSubmissionMeta(array(), $submission_event, $this->buildSubmissionMetaContext($layout_context));
        $submission_meta_json = !empty($submission_meta_payload) ? wp_json_encode($submission_meta_payload) : '';
        $has_payment_field = $this->formRequiresPayment($form_id);
        $submission_status = $this->submissionStatus();
        $requires_payment = $has_payment_field;
        if ($submission_status === 1 && $has_payment_field) {
            $requires_payment = $this->submissionRequiresPayment($form_id, $submission_payload);
        }
        $payment_status = $this->resolvePaymentStatus($requires_payment, $submission_status);
        $submission_hash = '';
        $submission_version = 0;
        if (!empty($layout_context['compliance_mode'])) {
            $submission_hash = Compliance::hashSubmission($submission_payload);
            $submission_version = 1;
        }
        $insert_data = array(
            'form_id' => $form_id,
            'status' => $submission_status,
            'is_payment_form' => $requires_payment ? 1 : 0,
            'payment_status' => $payment_status,
            'public_token' => $token,
            'submission' => maybe_serialize($submission_payload),
            'form_layout_version' => !empty($layout_context['compliance_mode']) ? (int) ($layout_context['layout_version'] ?? 0) : 0,
            'form_layout_hash' => !empty($layout_context['compliance_mode']) ? (string) ($layout_context['layout_hash'] ?? '') : '',
            'layout_snapshot_id' => null,
            'submission_version' => $submission_version,
            'submission_hash' => $submission_hash,
            'submission_ip' => $client_ip,
            'submission_user_agent' => $client_ua,
            'submission_meta' => $submission_meta_json,
            'source_url' => substr(esc_url_raw((string) ($this->data['form_url'] ?? '')), 0, 2083),
            'author' => get_current_user_id(),
            'created' => current_time('mysql', true)
        );

        $insert_formats = array(
            '%d', '%d', '%d', '%s', '%s', '%s', '%d', '%s', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%d', '%s'
        );

        $insert = $wpdb->insert($table, $insert_data, $insert_formats);
        
        $this->response['success'] = $insert;
        $this->response['sub_id'] = $wpdb->insert_id;
        $this->response['sub_token'] = $token;

        if ($insert && !empty($layout_context['compliance_mode']) && !empty($layout_context['layout_json'])) {
            $snapshot_id = $this->saveLayoutSnapshot(
                $form_id,
                (int) $this->response['sub_id'],
                (int) ($layout_context['layout_version'] ?? 0),
                (string) ($layout_context['layout_hash'] ?? ''),
                (string) $layout_context['layout_json']
            );
            if ($snapshot_id > 0) {
                $wpdb->update(
                    $table,
                    array('layout_snapshot_id' => $snapshot_id),
                    array('id' => (int) $this->response['sub_id']),
                    array('%d'),
                    array('%d')
                );
            }
        }

        if ($insert && !empty($this->response['sub_id']) && !empty($this->response['complete'])) {
            $history = \GriffinForms\Includes\Pipelines\SubmissionHistoryManager::getInstance();
            $history->recordRevision(
                (int) $this->response['sub_id'],
                $form_id,
                is_array($submission_payload) ? $submission_payload : array(),
                'create',
                array(
                    'actor_user_id' => get_current_user_id(),
                    'actor_label' => (string) wp_get_current_user()->display_name,
                    'previous_snapshot' => array(),
                    'update_pointer' => true,
                )
            );
        }

        if ($insert && $submission_status) {
            $this->incrementLifetimeSubmissionCount($form_id);
            //setting data for post actions
            $this->data['sub_id'] = $this->response['sub_id'];
            $this->data['sub_token'] = $this->response['sub_token'];
            $this->data['submission'] = $this->sql->getItemById($this->response['sub_id'], 'submission');
            if ($payment_status === 'paid') {
                $this->postSubmissionActions();
            } else {
                $this->logPendingPaymentNotice((int) $this->response['sub_id'], $requires_payment);
                $this->ensureRetryToken((int) $this->response['sub_id']);
            }
        }
    }
    
    private function saveEditSubmission(): void
    {
        $form_id = (int) ($this->data['form'] ?? 0);
        $submission_id = absint($this->data['sub_id']);
        $previous_snapshot = is_array($this->submission) ? $this->submission : array();

        $submission_payload = $this->pageData();
        $update = $this->persistSubmissionUpdate($form_id, $submission_id, $submission_payload);

        if ($update && $submission_id > 0 && !empty($this->response['complete'])) {
            global $wpdb;
            $table = $this->config->getTable('submission');
            $wpdb->query(
                $wpdb->prepare(
                    "UPDATE {$table} SET submission_edit_count = submission_edit_count + 1 WHERE id = %d",
                    $submission_id
                )
            );

            $history = \GriffinForms\Includes\Pipelines\SubmissionHistoryManager::getInstance();
            $history->recordRevision(
                $submission_id,
                $form_id,
                is_array($submission_payload) ? $submission_payload : array(),
                'edit',
                array(
                    'actor_user_id' => get_current_user_id(),
                    'actor_label' => (string) wp_get_current_user()->display_name,
                    'previous_snapshot' => $previous_snapshot,
                    'update_pointer' => true,
                )
            );
        }

        if ($update && !empty($this->response['complete'])) {
            $previous_status = $this->getSubmissionStatus($form_id, $this->data['sub_token']);
            if ((int) $previous_status !== 1) {
                $this->incrementLifetimeSubmissionCount($form_id);
            }
            $this->data['submission'] = $this->sql->getItemById($this->data['sub_id'], 'submission');
            $has_payment_field = $this->formRequiresPayment($form_id);
            $requires_payment = $has_payment_field && $this->submissionRequiresPayment($form_id, $submission_payload);
            if ($this->resolvePaymentStatus($requires_payment, 1) === 'paid') {
                $this->postSubmissionActions();
            } else {
                $this->logPendingPaymentNotice($submission_id, $requires_payment);
                $this->ensureRetryToken($submission_id);
            }
        }
    }

    private function saveContinuationPage(): void
    {
        $form_id = (int) ($this->data['form'] ?? 0);
        $submission_id = absint($this->data['sub_id']);

        $submission_payload = $this->pageData();
        $update = $this->persistSubmissionUpdate($form_id, $submission_id, $submission_payload);

        if ($update && $submission_id > 0 && !empty($this->response['complete'])) {
            $history = \GriffinForms\Includes\Pipelines\SubmissionHistoryManager::getInstance();
            $history->recordRevision(
                $submission_id,
                $form_id,
                is_array($submission_payload) ? $submission_payload : array(),
                'create',
                array(
                    'actor_user_id' => get_current_user_id(),
                    'actor_label' => (string) wp_get_current_user()->display_name,
                    'previous_snapshot' => array(),
                    'update_pointer' => true,
                )
            );
        }

        if ($update && !empty($this->response['complete'])) {
            $previous_status = $this->getSubmissionStatus($form_id, $this->data['sub_token']);
            if ((int) $previous_status !== 1) {
                $this->incrementLifetimeSubmissionCount($form_id);
            }
            $this->data['submission'] = $this->sql->getItemById($this->data['sub_id'], 'submission');
            $has_payment_field = $this->formRequiresPayment($form_id);
            $requires_payment = $has_payment_field && $this->submissionRequiresPayment($form_id, $submission_payload);
            if ($this->resolvePaymentStatus($requires_payment, 1) === 'paid') {
                $this->postSubmissionActions();
            } else {
                $this->logPendingPaymentNotice($submission_id, $requires_payment);
                $this->ensureRetryToken($submission_id);
            }
        }
    }

    private function persistSubmissionUpdate(int $form_id, int $submission_id, array $submission_payload): bool
    {
        global $wpdb;
        $table = $this->config->getTable('submission');

        $this->response['sub_id'] = $submission_id;
        $this->response['sub_token'] = (string) ($this->data['sub_token'] ?? '');

        $layout_context = $this->getFormComplianceContext($form_id);
        $flags = isset($layout_context['compliance_flags']) && is_array($layout_context['compliance_flags'])
            ? $layout_context['compliance_flags']
            : array();
        $store_ip = !empty($layout_context['compliance_mode']) && !empty($flags['store_ip']);
        $store_user_agent = !empty($layout_context['compliance_mode']) && !empty($flags['store_user_agent']);
        $client_ip = $store_ip ? $this->getClientIp() : '';
        $client_ua = $store_user_agent ? $this->getClientUserAgent() : '';
        $this->applyGdprMasking($layout_context['compliance_profile'] ?? 'standard', $store_ip, $store_user_agent, $client_ip, $client_ua);

        $existing_meta = $this->getSubmissionMeta($submission_id);
        $submission_event = $this->buildSubmissionEvent(
            (int) ($this->data['page'] ?? 0),
            $this->data['submission_meta_event'] ?? array(),
            $store_ip,
            $store_user_agent,
            $client_ip,
            $client_ua
        );
        $submission_meta_payload = $this->mergeSubmissionMeta($existing_meta, $submission_event, $this->buildSubmissionMetaContext($layout_context));
        $submission_meta_json = !empty($submission_meta_payload) ? wp_json_encode($submission_meta_payload) : '';

        $current_version = $this->getSubmissionVersion($submission_id);
        $submission_hash = '';
        $new_version = $current_version;
        if (!empty($layout_context['compliance_mode'])) {
            $submission_hash = Compliance::hashSubmission($submission_payload);
            $new_version = $current_version + 1;
        }

        $submission_status = $this->submissionStatus();
        $existing_payment_status = $this->getCurrentPaymentStatus($submission_id);
        $has_payment_field = $this->formRequiresPayment($form_id);
        $requires_payment = $has_payment_field;
        if ($submission_status === 1 && $has_payment_field) {
            $requires_payment = $this->submissionRequiresPayment($form_id, $submission_payload);
        }
        $payment_status = $this->resolvePaymentStatus($requires_payment, $submission_status);
        if ($existing_payment_status === 'paid') {
            $payment_status = 'paid';
        }
        $this->response['payment_status'] = $payment_status;

        $update_data = array(
            'status'              => $submission_status,
            'is_payment_form'     => $requires_payment ? 1 : 0,
            'payment_status'      => $payment_status,
            'submission'          => maybe_serialize($submission_payload),
            'form_layout_version' => !empty($layout_context['compliance_mode']) ? (int) ($layout_context['layout_version'] ?? 0) : 0,
            'form_layout_hash'    => !empty($layout_context['compliance_mode']) ? (string) ($layout_context['layout_hash'] ?? '') : '',
            'submission_version'  => $new_version,
            'submission_hash'     => $submission_hash,
            'last_editor'         => get_current_user_id(),
            'last_edited'         => current_time('mysql', false),
        );
        if ($store_ip) {
            $update_data['submission_ip'] = $client_ip;
        }
        if ($store_user_agent) {
            $update_data['submission_user_agent'] = $client_ua;
        }
        if ($submission_meta_json !== '') {
            $update_data['submission_meta'] = $submission_meta_json;
        }

        $update = (bool) $wpdb->update(
            $table,
            $update_data,
            array('form_id' => $this->data['form'], 'public_token' => $this->data['sub_token'])
        );

        $this->response['success'] = $update;

        if ($update) {
            wp_cache_delete('submission_' . $submission_id . '_' . $this->data['sub_token'], 'griffinforms');
        }

        if ($update && !empty($layout_context['compliance_mode']) && !empty($layout_context['layout_json'])) {
            $snapshot_id = $this->getSubmissionSnapshotId($submission_id);
            if ($snapshot_id <= 0) {
                $snapshot_id = $this->saveLayoutSnapshot(
                    $form_id,
                    $submission_id,
                    (int) ($layout_context['layout_version'] ?? 0),
                    (string) ($layout_context['layout_hash'] ?? ''),
                    (string) $layout_context['layout_json']
                );
                if ($snapshot_id > 0) {
                    $wpdb->update(
                        $table,
                        array('layout_snapshot_id' => $snapshot_id),
                        array('id' => $submission_id),
                        array('%d'),
                        array('%d')
                    );
                }
            }
            if (!empty($this->log)) {
                $this->log->add('info', 'submission', $submission_id, get_current_user_id(),
                    sprintf('Submission version updated to %d.', $new_version), 'audit');
            }
        }

        return $update;
    }

    private function generatePublicToken(): string
    {
        try {
            return bin2hex(random_bytes(20));
        } catch (\Throwable $throwable) {
            return sha1(
                (string) wp_generate_uuid4() . '|' .
                (string) wp_rand() . '|' .
                (string) microtime(true) . '|' .
                (string) wp_salt('auth')
            );
        }
    }

    private function getSubmissionStatus(int $form_id, string $token): ?int
    {
        if ($form_id <= 0 || $token === '') {
            return null;
        }

        global $wpdb;
        $table = $this->config->getTable('submission');

        $status = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT status FROM %i WHERE form_id = %d AND public_token = %s",
                $table,
                $form_id,
                $token
            )
        );

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

        return (int) $status;
    }

    private function getCurrentPaymentStatus(int $submission_id): string
    {
        if ($submission_id <= 0) {
            return '';
        }

        global $wpdb;
        $table = $this->config->getTable('submission');
        $status = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT payment_status FROM %i WHERE id = %d",
                $table,
                $submission_id
            )
        );

        return is_string($status) ? sanitize_key($status) : '';
    }

    private function getSubmissionVersion(int $submission_id): int
    {
        if ($submission_id <= 0) {
            return 0;
        }

        global $wpdb;
        $table = $this->config->getTable('submission');
        $version = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT submission_version FROM %i WHERE id = %d",
                $table,
                $submission_id
            )
        );

        return $version === null ? 0 : (int) $version;
    }

    private function getSubmissionSnapshotId(int $submission_id): int
    {
        if ($submission_id <= 0) {
            return 0;
        }

        global $wpdb;
        $table = $this->config->getTable('submission');
        $snapshot_id = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT layout_snapshot_id FROM %i WHERE id = %d",
                $table,
                $submission_id
            )
        );

        return $snapshot_id === null ? 0 : (int) $snapshot_id;
    }

    private function getClientIp(): string
    {
        $settings = \GriffinForms\Settings::getInstance();
        $trust_headers = (int) $settings->getOption('compliance_trust_proxy_headers', 0);

        if ($trust_headers === 1) {
            $header_ip = $this->getClientIpFromHeaders();
            if ($header_ip !== '') {
                return $header_ip;
            }
        }

        $ip = isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '';
        $ip = trim($ip);
        if ($ip === '') {
            return '';
        }
        return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '';
    }

    private function getClientIpFromHeaders(): string
    {
        $candidates = array(
            'HTTP_CF_CONNECTING_IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_REAL_IP',
            'HTTP_CLIENT_IP',
        );

        foreach ($candidates as $key) {
            if (empty($_SERVER[$key])) {
                continue;
            }
            $raw = (string) $_SERVER[$key];
            $raw = trim($raw);
            if ($raw === '') {
                continue;
            }

            if ($key === 'HTTP_X_FORWARDED_FOR') {
                $parts = array_map('trim', explode(',', $raw));
                foreach ($parts as $part) {
                    if ($part !== '' && filter_var($part, FILTER_VALIDATE_IP)) {
                        return $part;
                    }
                }
                continue;
            }

            if (filter_var($raw, FILTER_VALIDATE_IP)) {
                return $raw;
            }
        }

        return '';
    }

    private function getClientUserAgent(): string
    {
        $ua = isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
        $ua = trim($ua);
        if ($ua === '') {
            return '';
        }
        return sanitize_text_field($ua);
    }

    private function applyGdprMasking(string $profile, bool &$store_ip, bool &$store_user_agent, string &$client_ip, string &$client_ua): void
    {
        if ($profile !== 'gdpr') {
            return;
        }

        if ($store_ip && $client_ip !== '') {
            $client_ip = $this->maskIp($client_ip);
        }

        if ($store_user_agent && $client_ua !== '') {
            $client_ua = $this->summarizeUserAgent($client_ua);
            if ($client_ua === '') {
                $store_user_agent = false;
            }
        }
    }

    private function maskIp(string $ip): string
    {
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            $parts = explode('.', $ip);
            if (count($parts) === 4) {
                $parts[3] = '***';
                return implode('.', $parts);
            }
            return $ip;
        }

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            $packed = @inet_pton($ip);
            if ($packed === false) {
                return $ip;
            }
            $bytes = unpack('C*', $packed);
            if (!is_array($bytes)) {
                return $ip;
            }
            $total = count($bytes);
            for ($i = $total - 8 + 1; $i <= $total; $i++) {
                if (isset($bytes[$i])) {
                    $bytes[$i] = 0;
                }
            }
            $masked = pack('C*', ...array_values($bytes));
            $result = inet_ntop($masked);
            if ($result === false) {
                return $ip;
            }
            if (strpos($result, '::') !== false) {
                return preg_replace('/::.+$/', '::****', $result);
            }
            return $result . '::****';
        }

        return $ip;
    }

    private function summarizeUserAgent(string $ua): string
    {
        $ua = trim($ua);
        if ($ua === '') {
            return '';
        }

        $browser = '';
        $version = '';

        if (preg_match('/Edg\\/([\\d\\.]+)/i', $ua, $match)) {
            $browser = 'Edge';
            $version = $match[1];
        } elseif (preg_match('/Chrome\\/([\\d\\.]+)/i', $ua, $match) && stripos($ua, 'Edg/') === false) {
            $browser = 'Chrome';
            $version = $match[1];
        } elseif (preg_match('/Firefox\\/([\\d\\.]+)/i', $ua, $match)) {
            $browser = 'Firefox';
            $version = $match[1];
        } elseif (preg_match('/Version\\/([\\d\\.]+).*Safari/i', $ua, $match)) {
            $browser = 'Safari';
            $version = $match[1];
        }

        $os = '';
        if (preg_match('/Mac OS X ([\\d_\\.]+)/i', $ua, $match)) {
            $os = 'macOS ' . str_replace('_', '.', $match[1]);
        } elseif (preg_match('/Windows NT ([\\d\\.]+)/i', $ua, $match)) {
            $os = 'Windows ' . $match[1];
        } elseif (preg_match('/Android ([\\d\\.]+)/i', $ua, $match)) {
            $os = 'Android ' . $match[1];
        } elseif (preg_match('/iPhone OS ([\\d_]+)/i', $ua, $match)) {
            $os = 'iOS ' . str_replace('_', '.', $match[1]);
        } elseif (preg_match('/iPad; CPU OS ([\\d_]+)/i', $ua, $match)) {
            $os = 'iPadOS ' . str_replace('_', '.', $match[1]);
        }

        if ($browser === '' && $os === '') {
            return '';
        }

        $parts = [];
        if ($browser !== '') {
            $parts[] = trim($browser . ' ' . $version);
        }
        if ($os !== '') {
            $parts[] = $os;
        }

        return implode(' · ', $parts);
    }

    private function getFormComplianceContext(int $form_id): array
    {
        $context = array(
            'compliance_mode' => 0,
            'compliance_profile' => 'standard',
            'compliance_flags' => array(),
            'layout_version' => 0,
            'layout_hash' => '',
            'layout_json' => '',
            'layout_payload' => array(),
        );

        if ($form_id <= 0) {
            return $context;
        }

        global $wpdb;
        $table = $this->config->getTable('form');
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT compliance_mode, compliance_profile, compliance_flags, layout_version, layout_hash FROM %i WHERE id = %d",
                $table,
                $form_id
            ),
            ARRAY_A
        );

        if (!is_array($row)) {
            return $context;
        }

        $context['compliance_mode'] = !empty($row['compliance_mode']) ? 1 : 0;
        $context['compliance_profile'] = isset($row['compliance_profile']) ? (string) $row['compliance_profile'] : 'standard';
        $flags = isset($row['compliance_flags']) ? $this->maybeDecode($row['compliance_flags']) : array();
        $flags = is_array($flags) ? $flags : array();
        $resolved_flags = \GriffinForms\Includes\Helpers\Compliance::resolveFlags($flags);
        $context['compliance_flags'] = $resolved_flags;

        if (isset($resolved_flags['mode']) && $resolved_flags['mode'] === 'inherit') {
            $settings = \GriffinForms\Settings::getInstance();
            $defaults_enabled = (int) $settings->getOption('compliance_defaults_enabled', 0);
            $context['compliance_mode'] = $defaults_enabled === 1 ? 1 : 0;
            $context['compliance_profile'] = (string) $settings->getOption('compliance_default_profile', 'standard');
        }

        if (empty($context['compliance_mode'])) {
            return $context;
        }

        $layout_data = $this->buildLayoutSnapshotData($form_id);
        $current_hash = isset($row['layout_hash']) ? (string) $row['layout_hash'] : '';
        $current_version = isset($row['layout_version']) ? (int) $row['layout_version'] : 0;
        $new_hash = $layout_data['layout_hash'];
        $new_version = $current_version;

        if ($new_hash !== '' && $new_hash !== $current_hash) {
            $new_version = $current_version + 1;
            $wpdb->update(
                $table,
                array(
                    'layout_version' => $new_version,
                    'layout_hash' => $new_hash
                ),
                array('id' => $form_id),
                array('%d', '%s'),
                array('%d')
            );

            if (!empty($this->log)) {
                $this->log->add(
                    'info',
                    'form',
                    $form_id,
                    get_current_user_id(),
                    sprintf('Form layout version updated to %d during submission processing.', $new_version),
                    'audit'
                );
            }
        } elseif ($new_hash === '') {
            $new_hash = $current_hash;
        }

        $context['layout_version'] = $new_version;
        $context['layout_hash'] = $new_hash;
        $context['layout_json'] = $layout_data['layout_json'];
        $context['layout_payload'] = $layout_data['layout_payload'];

        return $context;
    }

    private function buildLayoutSnapshotData(int $form_id): array
    {
        $result = array(
            'layout_payload' => array(),
            'layout_json' => '',
            'layout_hash' => ''
        );

        if ($form_id <= 0) {
            return $result;
        }

        try {
            $exporter = new FormExport($form_id);
            $payload = $exporter->getFormData();
            if (!is_array($payload)) {
                return $result;
            }

            $payload = $this->normalizeLayoutPayload($payload);
            $result['layout_payload'] = $payload;
            $result['layout_json'] = Compliance::canonicalJson($payload);
            $result['layout_hash'] = Compliance::hashLayout($payload);
        } catch (\Throwable $t) {
            if (!empty($this->log)) {
                $this->log->add(
                    'warning',
                    'form',
                    $form_id,
                    get_current_user_id(),
                    'Unable to compute layout snapshot data: ' . $t->getMessage(),
                    'audit'
                );
            }
        }

        return $result;
    }

    private function normalizeLayoutPayload(array $payload): array
    {
        $strip_keys = array(
            'author',
            'created',
            'last_editor',
            'last_edited',
            'created_at',
            'updated_at',
            'lifetime_submission_count',
            'layout_version',
            'layout_hash',
            'compliance_mode',
            'compliance_profile',
            'compliance_flags'
        );

        return Compliance::stripKeysRecursive($payload, $strip_keys);
    }

    private function saveLayoutSnapshot(int $form_id, int $submission_id, int $layout_version, string $layout_hash, string $layout_json): int
    {
        if ($form_id <= 0 || $layout_json === '') {
            return 0;
        }

        global $wpdb;
        $table = $this->config->getTable('layout_snapshot');
        $insert = $wpdb->insert(
            $table,
            array(
                'form_id' => $form_id,
                'submission_id' => $submission_id > 0 ? $submission_id : null,
                'layout_version' => $layout_version,
                'layout_hash' => $layout_hash,
                'layout_json' => $layout_json,
                'created_at' => current_time('mysql', true),
                'created_by' => get_current_user_id()
            ),
            array('%d', '%d', '%d', '%s', '%s', '%s', '%d')
        );

        if (!$insert && !empty($this->log)) {
            $this->log->add(
                'warning',
                'submission',
                $submission_id > 0 ? $submission_id : $form_id,
                get_current_user_id(),
                'Layout snapshot insert failed: ' . (string) $wpdb->last_error,
                'audit'
            );
        }

        if ($insert && !empty($this->log)) {
            $this->log->add(
                'info',
                'submission',
                $submission_id > 0 ? $submission_id : $form_id,
                get_current_user_id(),
                sprintf('Layout snapshot saved (form_id=%d, version=%d).', $form_id, $layout_version),
                'audit'
            );
        }

        return $insert ? (int) $wpdb->insert_id : 0;
    }

    private function incrementLifetimeSubmissionCount(int $form_id): void
    {
        if ($form_id <= 0) {
            return;
        }

        if ($this->shouldExcludeAdminFromLifetimeCount()) {
            return;
        }

        global $wpdb;
        $form_table = $this->config->getTable('form');

        $wpdb->query(
            $wpdb->prepare(
                "UPDATE %i SET lifetime_submission_count = lifetime_submission_count + 1 WHERE id = %d",
                $form_table,
                $form_id
            )
        );
    }

    private function shouldExcludeAdminFromLifetimeCount(): bool
    {
        $settings = \GriffinForms\Settings::getInstance();
        $exclude = (int) $settings->getOption('exclude_admin_submissions_from_lifetime_count', 0);
        if ($exclude !== 1) {
            return false;
        }

        return is_user_logged_in() && current_user_can('manage_options');
    }
    
    private function submissionStatus()
    {
        if ($this->data['position'] === 'single' || $this->data['position'] === 'last') {
            $this->response['complete'] = 1;
            return 1;
        }

        $this->response['complete'] = 0;
        return 0;
    }
    
    private function pageData(): array
    {
        $key = 'page_' . $this->data['page'];
        $page_data = $this->data['sanitized_page_data'] ?? json_decode(wp_unslash($this->data['page_data']), true);
        if (!is_array($page_data)) {
            $page_data = [];
        }

        // Both edit sessions and multi-page continuations must merge into the existing submission.
        if ($this->isEditSession() || $this->isContinuation()) {
            if (!is_array($this->submission)) {
                $this->loadSubmission();
            }
            if ($this->isEditSession()) {
                $page_data = $this->preserveLockedPaidPaymentFields($page_data, $key);
            }
            $this->submission[$key] = $page_data;
            return is_array($this->submission) ? $this->submission : array($key => $page_data);
        }

        return array($key => $page_data);
    }

    /**
     * Resolve a form name for logging; falls back to "Form #ID".
     */
    protected function resolveFormName(int $form_id): string
    {
        if ($form_id <= 0 || !is_object($this->sql) || !method_exists($this->sql, 'getItemValue')) {
            return sprintf(__('Form #%d', 'griffinforms-form-builder'), $form_id);
        }

        $name = $this->sql->getItemValue('form', 'id', $form_id, 'name');
        if (!empty($name)) {
            return $name;
        }

        return sprintf(__('Form #%d', 'griffinforms-form-builder'), $form_id);
    }

    private function formRequiresPayment(int $form_id): bool
    {
        if ($form_id <= 0) {
            return false;
        }

        if (array_key_exists($form_id, $this->form_payment_map)) {
            return (bool) $this->form_payment_map[$form_id];
        }

        $has_payment = false;
        $form = new \GriffinForms\Items\Form();
        if ($form->loadItem($form_id)) {
            $has_payment = $form->hasPaymentField();
        }

        $this->form_payment_map[$form_id] = $has_payment;
        return $has_payment;
    }

    private function resolvePaymentStatus(bool $requires_payment, int $submission_status): string
    {
        if ($submission_status === 1 && !$requires_payment) {
            return 'paid';
        }
        return 'pending';
    }

    private function preserveLockedPaidPaymentFields(array $page_data, string $page_key): array
    {
        $submission_id = absint($this->data['sub_id'] ?? 0);
        if ($submission_id <= 0 || $this->getCurrentPaymentStatus($submission_id) !== 'paid') {
            return $page_data;
        }

        $form_id = (int) ($this->data['form'] ?? 0);
        $payment_field_ids = $this->getPaymentFieldIds($form_id);
        if (empty($payment_field_ids)) {
            return $page_data;
        }

        $existing_page_data = [];
        if (is_array($this->submission) && isset($this->submission[$page_key]) && is_array($this->submission[$page_key])) {
            $existing_page_data = $this->submission[$page_key];
        }

        foreach ($payment_field_ids as $field_id) {
            if (array_key_exists($field_id, $existing_page_data)) {
                $page_data[$field_id] = $existing_page_data[$field_id];
            }
        }

        return $page_data;
    }

    /**
     * Determine whether this specific submission requires payment.
     *
     * Uses the form's payment fields and checks if at least one product item was selected.
     * This prevents optional payment fields from blocking non-payment submissions.
     */
    private function submissionRequiresPayment(int $form_id, array $submission_payload): bool
    {
        if ($form_id <= 0) {
            return false;
        }

        $payment_field_ids = $this->getPaymentFieldIds($form_id);
        if (empty($payment_field_ids)) {
            return false;
        }

        foreach ($submission_payload as $page_key => $page_data) {
            if (!is_array($page_data)) {
                continue;
            }

            foreach ($page_data as $field_id => $value) {
                $field_id = (int) $field_id;
                if (!in_array($field_id, $payment_field_ids, true)) {
                    continue;
                }

                if (!is_array($value)) {
                    continue;
                }

                $items = $value['items'] ?? null;
                if (empty($items) || !is_array($items)) {
                    continue;
                }

                foreach ($items as $item) {
                    if (!is_array($item)) {
                        continue;
                    }
                    $quantity = isset($item['quantity']) ? (int) $item['quantity'] : 0;
                    if ($quantity > 0) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    /**
     * Returns payment field IDs for a form (cached).
     */
    private function getPaymentFieldIds(int $form_id): array
    {
        if ($form_id <= 0) {
            return array();
        }

        if (array_key_exists($form_id, $this->form_payment_field_ids_map)) {
            return $this->form_payment_field_ids_map[$form_id];
        }

        $payment_field_ids = array();
        $form = new \GriffinForms\Items\Form();
        if ($form->loadItem($form_id)) {
            $fields = $form->getFormFields(array('id', 'field_type'));
            foreach ($fields as $field) {
                $type = isset($field['field_type']) ? strtolower((string) $field['field_type']) : '';
                if ($type !== 'payment') {
                    continue;
                }
                $id = isset($field['id']) ? (int) $field['id'] : 0;
                if ($id > 0) {
                    $payment_field_ids[] = $id;
                }
            }
        }

        $payment_field_ids = array_values(array_unique($payment_field_ids));
        $this->form_payment_field_ids_map[$form_id] = $payment_field_ids;
        return $payment_field_ids;
    }

    private function logPendingPaymentNotice(int $submission_id, bool $requires_payment): void
    {
        if (!isset($this->log)) {
            return;
        }

        $category = $requires_payment ? 'Payment Integration (pending)' : 'submission';
        $message = $requires_payment
            ? __('Submission requires payment. Notification workflow deferred until payment completes.', 'griffinforms-form-builder')
            : __('Submission completed without payment. No products were selected.', 'griffinforms-form-builder');

        $this->log->add(
            'info',
            'submission',
            $submission_id,
            get_current_user_id(),
            $message,
            $category
        );
    }

    private function postSubmissionActions()
    {
        $post_submission = new \GriffinForms\Frontend\Actions\PostSubmission($this->data);
        $post_submission->runActions();
        return $post_submission->runResponse();
    }

    /**
     * Generate a retry token for pending payment submissions.
     */
    private function ensureRetryToken(int $submission_id): void
    {
        try {
            $meta = \GriffinForms\Includes\Payments\SubmissionPaymentMeta::getInstance();
            $retry = $meta->ensureRetryToken($submission_id);
            $resume_url = isset($this->data['form_url']) ? (string) $this->data['form_url'] : '';
            if ($resume_url === '') {
                $ref = wp_get_referer();
                $resume_url = is_string($ref) ? $ref : '';
            }
            if ($resume_url !== '') {
                $resume_url = esc_url_raw(remove_query_arg('gf_payment_resume', $resume_url));
                if ($resume_url !== '') {
                    $meta->mergeMeta($submission_id, array(
                        'retry' => array(
                            'resume_url' => $resume_url,
                        )
                    ));
                }
            }
        } catch (\Exception $e) {
            // Silent fail; optional log could be added if needed.
        }
    }

    /**
     * Helper to send structured debug logs.
     */
    private function logAjaxEvent(string $level, string $message, string $item_type = 'ajax', int $item_id = 0): void
    {
        $form_id = isset($this->data['form']) ? (int) $this->data['form'] : 0;
        $resolved_item_id = $item_id > 0 ? $item_id : $form_id;

        if (isset($this->log)) {
            $this->log->add(
                $level,
                'submission',
                $resolved_item_id,
                get_current_user_id(),
                $message,
                $item_type
            );
        }

        // Intentionally avoid error_log() noise; rely on GriffinForms native logging.
    }
    
    public function __construct()
    {
        add_action('wp_ajax_nopriv_saveFormpage', array($this, 'saveFormpage'));
        add_action('wp_ajax_saveFormpage', array($this, 'saveFormpage'));
        add_action('wp_ajax_nopriv_recordSubmissionEvent', array($this, 'recordSubmissionEvent'));
        add_action('wp_ajax_recordSubmissionEvent', array($this, 'recordSubmissionEvent'));
        add_action('wp_ajax_nopriv_fileUploadTemp', array($this, 'fileUploadTemp'));
        add_action('wp_ajax_fileUploadTemp', array($this, 'fileUploadTemp'));
        add_action('wp_ajax_fileUploadDelete', [$this, 'fileUploadDelete']);
        add_action('wp_ajax_nopriv_fileUploadDelete', [$this, 'fileUploadDelete']);
        $this->setAll();
    }
}
