<?php

namespace GriffinForms\Frontend\Actions;

class PostSubmission extends \GriffinForms\Frontend\Actions\Format
{
    public $alert_emails = [];
    private $mail_log_category;
    
    protected function isEmailAlert()
    {
        if ($this->form === null) {
            return false;
        }
    
        $alert_emails = $this->form->getProp('alert_emails');
        if ($alert_emails === null || !is_array($alert_emails) || empty($alert_emails)) {
            return false;
        }
        
        $this->alert_emails = $alert_emails;
        return true;
    }

    /**
     * Normalize alert email data into a map of email => message_id.
     *
     * @param array $alert_emails
     * @return array<string,int>
     */
    protected function normalizeAlertEmailMap(array $alert_emails): array
    {
        if (empty($alert_emails)) {
            return [];
        }

        $keys = array_keys($alert_emails);
        $is_assoc = $keys !== range(0, count($alert_emails) - 1);
        $normalized = [];
        $form_id = $this->getFormId();

        if ($is_assoc) {
            foreach ($alert_emails as $email => $message_id) {
                $clean_email = sanitize_email($email);
                if (empty($clean_email)) {
                    if ($form_id > 0) {
                        $this->log->add(
                            'warning',
                            'form',
                            $form_id,
                            get_current_user_id(),
                            'Admin alert email ignored: invalid recipient address in mapping.',
                            $this->getMailLogCategory()
                        );
                    }
                    continue;
                }
                $normalized[$clean_email] = absint($message_id);
            }
            return $normalized;
        }

        if ($form_id > 0) {
            $this->log->add(
                'info',
                'form',
                $form_id,
                get_current_user_id(),
                'Legacy alert_emails format detected; normalized to message mapping with default template.',
                $this->getMailLogCategory()
            );
        }

        foreach ($alert_emails as $email) {
            $clean_email = sanitize_email($email);
            if (!empty($clean_email)) {
                $normalized[$clean_email] = 0;
            } elseif ($form_id > 0) {
                $this->log->add(
                    'warning',
                    'form',
                    $form_id,
                    get_current_user_id(),
                    'Admin alert email ignored: invalid recipient address in legacy list.',
                    $this->getMailLogCategory()
                );
            }
        }

        return $normalized;
    }

    /**
     * Processes and sends admin notification emails based on configured alert emails.
     */
    public function processAdminEmailAlert()
    {
        $this->data['form'] = $this->form;
        $submission_id = isset($this->data['submission']['id']) ? (int) $this->data['submission']['id'] : 0;
        $merge_engine = new \GriffinForms\Includes\MergeTokens\Engine();
        $merge_context = \GriffinForms\Includes\Pipelines\SubmissionContextBuilder::getInstance()->build($submission_id);
        $merge_context['form_object'] = $this->form;
        $form_id = $this->getFormId();
        if ($submission_id > 0 && (empty($merge_context) || empty($merge_context['submission']))) {
            $this->log->add(
                'warning',
                'submission',
                $submission_id,
                get_current_user_id(),
                'Merge context missing for admin alert; tokens may not resolve.',
                $this->getMailLogCategory()
            );
        }
        $mailer = new \GriffinForms\Includes\Mailer\MailHandler();
        $mailer->setLogContext('submission', $submission_id);
        $mailer->setSubmissionId($submission_id);

        $alert_map = $this->normalizeAlertEmailMap($this->alert_emails);
        $default_template = new \GriffinForms\Includes\MailTemplates\AdminSubmissionAlert($this->data);
        $default_subject = $merge_engine->render($default_template->getSubject(), $merge_context);
        $default_content = $merge_engine->render($default_template->getContent(), $merge_context);

        foreach ($alert_map as $email => $message_id) {
            $subject = '';
            $content = '';
            $has_custom_message = false;
            if ($message_id > 0) {
                $message = $this->sql->getItemById($message_id, 'message');
                if (is_array($message)) {
                    $subject = $merge_engine->render($message['name'] ?? '', $merge_context);
                    $content = $merge_engine->render($message['content'] ?? '', $merge_context);
                    $has_custom_message = true;
                    if ($submission_id > 0 && trim($content) === '') {
                        $this->log->add(
                            'warning',
                            'submission',
                            $submission_id,
                            get_current_user_id(),
                            sprintf('Admin alert message %d has empty content; using default template.', $message_id),
                            $this->getMailLogCategory()
                        );
                    }
                } elseif ($submission_id > 0) {
                    $this->log->add(
                        'warning',
                        'submission',
                        $submission_id,
                        get_current_user_id(),
                        sprintf('Admin alert message %d no longer exists; using default template.', $message_id),
                        $this->getMailLogCategory()
                    );
                }
            }

            if ($subject === '' && $content === '') {
                $subject = $default_subject;
                $content = $default_content;
            } elseif (trim($content) === '') {
                $content = $default_content;
                if ($subject === '') {
                    $subject = $default_subject;
                }
            } elseif ($has_custom_message && $subject === '') {
                $subject = $default_subject;
            }

            $mailer->setEmailSubject($subject);
            $mailer->setEmailContent($content);
            $mailer->setEmailRecipient($email);
            $mailer->sendMail();
        }
    }


    /**
     * Checks if the form has user autoresponder mappings configured.
     *
     * @return bool
     */
    protected function hasUserAutoresponderMappings()
    {
        $mappings = $this->form->getProp('autoresponder');

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

        if (!is_array($mappings)) {
            $this->log->add(
                'info',
                'submission',
                $this->data['submission']['id'],
                get_current_user_id(),
                'No autoresponder email sent: submitter email field is not configured.',
                $this->getMailLogCategory()
            );
            return false;
        }

        return !empty($mappings);
    }

    /**
     * Retrieves all submitted values for a given field ID.
     *
     * @param int $field_id
     * @return array
     */
    protected function getSubmittedFieldValues($field_id)
    {
        $values = [];
        foreach ($this->getFieldCollections() as $fieldsById) {
            if (!isset($fieldsById[$field_id])) {
                continue;
            }
            $values = array_merge($values, $this->flattenFieldValues($fieldsById[$field_id]));
        }

        $values = array_map('trim', $values);
        $values = array_filter($values, static function ($value) {
            return $value !== '';
        });

        return array_values($values);
    }

    /**
     * Recursively flatten nested field values into a string list.
     *
     * @param mixed $value
     * @return array
     */
    private function flattenFieldValues($value): array
    {
        if (is_array($value)) {
            $flattened = [];
            foreach ($value as $entry) {
                $flattened = array_merge($flattened, $this->flattenFieldValues($entry));
            }
            return $flattened;
        }

        if (is_scalar($value)) {
            return [(string) $value];
        }

        return [];
    }

    /**
     * Processes and sends user autoresponder messages based on the provided mappings.
     *
     * @param array $mappings List of email field to message ID mappings.
     */
    public function processUserAutoresponderMappings($mappings)
    {
        $submission_id = isset($this->data['submission']['id']) ? (int) $this->data['submission']['id'] : 0;
        $merge_engine = new \GriffinForms\Includes\MergeTokens\Engine();
        $merge_context = \GriffinForms\Includes\Pipelines\SubmissionContextBuilder::getInstance()->build($submission_id);
        $merge_context['form_object'] = $this->form;
        $form_id = $this->getFormId();
        if ($submission_id > 0 && (empty($merge_context) || empty($merge_context['submission']))) {
            $this->log->add(
                'warning',
                'submission',
                $submission_id,
                get_current_user_id(),
                'Merge context missing for autoresponder; tokens may not resolve.',
                $this->getMailLogCategory()
            );
        }

        foreach ($mappings as $mapping) {
            $email_field_id = $mapping['email_field_id'];
            $message_id = $mapping['message_id'];

            $email_values = $this->getSubmittedFieldValues($email_field_id);
            if (empty($email_values)) {
                $this->log->add(
                    'warning',
                    'submission',
                    $this->data['submission']['id'],
                    get_current_user_id(),
                    sprintf('Autoresponder mapping found no email value for field ID %d.', $email_field_id),
                    $this->getMailLogCategory()
                );
                continue;
            }

            foreach ($email_values as $email_value) {
                if (empty($email_value)) {
                    $this->log->add(
                        'error',
                        'submission',
                        $this->data['submission']['id'],
                        get_current_user_id(),
                        sprintf(
                            'Autoresponder email not sent: mapped field ID %d is empty or missing a value.',
                            $email_field_id
                        ),
                        $this->getMailLogCategory()
                    );
                    continue;
                }
                if (!is_email($email_value)) {
                    if ($submission_id > 0) {
                        $this->log->add(
                            'warning',
                            'submission',
                            $submission_id,
                            get_current_user_id(),
                            sprintf(
                                'Autoresponder email ignored: mapped field ID %d contains an invalid email.',
                                $email_field_id
                            ),
                            $this->getMailLogCategory()
                        );
                    }
                    $this->log->add(
                        'error',
                        'submission', 
                        $this->data['submission']['id'],
                        get_current_user_id(),
                        sprintf(
                            'Autoresponder email not sent: mapped field ID %d contains an invalid email.',
                            $email_field_id
                        ),
                        $this->getMailLogCategory()
                    );
                    continue;
                }

                $message = $this->sql->getItemById($message_id, 'message');

                if (!$message) {
                    if ($submission_id > 0) {
                        $this->log->add(
                            'warning',
                            'submission',
                            $submission_id,
                            get_current_user_id(),
                            sprintf('Autoresponder message %d no longer exists; email not sent.', $message_id),
                            $this->getMailLogCategory()
                        );
                    }
                    $this->log->add(
                        'error',
                        'submission',
                        $this->data['submission']['id'],
                        get_current_user_id(),
                        'Message ID ' . $message_id . ' does not exist or is invalid.',
                        $this->getMailLogCategory()
                    );
                    continue;
                }

                $mailer = new \GriffinForms\Includes\Mailer\MailHandler();
                $mailer->setLogContext('submission', $submission_id);
                $mailer->setSubmissionId($submission_id);
                $subject = $merge_engine->render($message['name'] ?? '', $merge_context);
                $content = $merge_engine->render($message['content'] ?? '', $merge_context);
                if ($submission_id > 0 && trim($content) === '') {
                    $this->log->add(
                        'warning',
                        'submission',
                        $submission_id,
                        get_current_user_id(),
                        sprintf('Autoresponder message %d has empty content; email not sent.', $message_id),
                        $this->getMailLogCategory()
                    );
                    continue;
                }
                $mailer->setEmailSubject($subject);
                $mailer->setEmailContent($content);
                $mailer->setEmailRecipient($email_value);

                $mailer->sendMail();
            }
        }
    }

    /**
     * Extract tokens from a string or nested array. Strings may be comma-separated.
     *
     * @param mixed $value
     * @return array<string>
     */
    private function extractTokensFromValue($value): array
    {
        $out = [];
        if (is_string($value) && $value !== '') {
            foreach (explode(',', $value) as $part) {
                $part = trim($part);
                if ($part !== '') { $out[] = $part; }
            }
        } elseif (is_array($value)) {
            foreach ($value as $v) {
                $out = array_merge($out, $this->extractTokensFromValue($v));
            }
        }
        return $out;
    }

    /**
     * Collect file tokens from submitted data. Accepts either comma-separated strings
     * or arrays of tokens. Values are trimmed and de-duplicated.
     *
     * @return array<string>
     */
    private function collectFileTokens(): array
    {        
        $tokens = [];

        // Prefer sanitized_page_data; fallback to page_data; also scan full serialized submission on final page
        $sources = $this->getFieldCollections();

        foreach ($sources as $fieldsById) {
            // $fieldsById: [ field_id => [ instance_idx => value ] ]
            foreach ($fieldsById as $field_id => $instances) {
                if (is_array($instances)) {
                    foreach ($instances as $instance_idx => $value) {
                        $tokens = array_merge($tokens, $this->extractTokensFromValue($value));
                    }
                } else {
                    // Single value edge-case
                    $tokens = array_merge($tokens, $this->extractTokensFromValue($instances));
                }
            }
        }

        // Normalize: trim, de-duplicate, and keep only sha1-like tokens (40 hex chars)
        $tokens = array_map('trim', $tokens);
        $tokens = array_filter($tokens, function($t){ return $t !== '' && preg_match('/^[a-f0-9]{40}$/i', $t); });

        return array_values(array_unique($tokens));
    }

    /**
     * Aggregate sanitized, current-page, and stored submission field data.
     */
    private function getFieldCollections(): array
    {
        $sources = [];
        if (isset($this->data['sanitized_page_data']) && is_array($this->data['sanitized_page_data'])) {
            $sources[] = $this->data['sanitized_page_data'];
        }
        if (isset($this->data['page_data']) && is_array($this->data['page_data'])) {
            $sources[] = $this->data['page_data'];
        }
        if (isset($this->data['submission']['submission'])) {
            $all_pages = maybe_unserialize($this->data['submission']['submission']);
            if (is_array($all_pages)) {
                foreach ($all_pages as $page_array) {
                    if (is_array($page_array)) {
                        $sources[] = $page_array;
                    }
                }
            }
        }

        return $sources;
    }

    /**
     * Check if a column exists on a table (cached per request).
     */
    private function hasTableColumn(string $table, string $column): bool
    {
        static $cache = [];
        $key = $table.'::'.$column;
        if (isset($cache[$key])) return $cache[$key];
        global $wpdb;
        $col = $wpdb->get_var($wpdb->prepare('SHOW COLUMNS FROM %i LIKE %s', $table, $column));
        $cache[$key] = !empty($col);
        return $cache[$key];
    }

    /**
     * Promote temporary uploaded files (by token) to attached state for this submission.
     *
     * This method scans the submitted data for file tokens (sha1-like strings),
     * and updates any corresponding rows in the file table from status='temp'
     * to status='attached', associating them with the current submission.
     * Safe to call multiple times; only affects rows with status='temp'.
     *
     * Note: We do not check whether the originating field is of type 'file upload'.
     * This avoids extra database queries. Safety is preserved because:
     *   - Only sha1-like tokens are considered valid.
     *   - The update only applies to rows in the 'file' table with status='temp'.
     *   - Non-matching values are ignored and logged as 0 promoted files.
     * Therefore a field-type lookup is unnecessary overhead.
     *
     * @return int Number of rows promoted
     */
    private function promoteFiles(): int
    {
        $submission_id = isset($this->data['sub_id']) ? (int) $this->data['sub_id']
            : (isset($this->data['submission']['id']) ? (int) $this->data['submission']['id'] : 0);

        // Prefer explicit 'form' key, then submission->form_id, then form object
        if (isset($this->data['form'])) {
            $form_id = (int) $this->data['form'];
        } elseif (isset($this->data['submission']['form_id'])) {
            $form_id = (int) $this->data['submission']['form_id'];
        } elseif ($this->form && method_exists($this->form, 'getProp')) {
            $form_id = (int) $this->form->getProp('id');
        } else {
            $form_id = 0;
        }

        if ($submission_id <= 0) { return 0; }

        $tokens = $this->collectFileTokens();

        if (empty($tokens)) { return 0; }

        global $wpdb;
        $table = $this->config->getTable('file');
        $now_utc = gmdate('Y-m-d H:i:s');

        // Build a safe IN (...) clause
        $placeholders = implode(',', array_fill(0, count($tokens), '%s'));
        $params = $tokens;

        // Limit by form_id when available
        $sql = "UPDATE %i 
                SET submission_id = %d, status = 'attached', expires_at = NULL, updated_at = %s
                WHERE status = 'temp' AND token IN ($placeholders)";
        $args = array_merge([ $table, $submission_id, $now_utc ], $params);

        if ($form_id > 0) {
            $sql .= " AND form_id = %d";
            $args[] = $form_id;
        }

        $prepared = $wpdb->prepare($sql, $args);

        $affected = $wpdb->query($prepared);

        // Fallback: if nothing updated and we constrained by form_id, retry without form_id constraint
        if ((int) $affected === 0 && $form_id > 0) {
            $sql2 = "UPDATE %i 
                     SET submission_id = %d, status = 'attached', expires_at = NULL, updated_at = %s
                     WHERE status = 'temp' AND token IN ($placeholders)";
            $args2 = array_merge([ $table, $submission_id, $now_utc ], $params);
            $prepared2 = $wpdb->prepare($sql2, $args2);
            $affected = $wpdb->query($prepared2);
        }

        // If any of the promoted rows are meant for Media Library, create attachments now
        if (!empty($tokens)) {
            // Load media/attachment libs if needed
            if (!function_exists('wp_generate_attachment_metadata')) {
                require_once ABSPATH . 'wp-admin/includes/file.php';
                require_once ABSPATH . 'wp-admin/includes/image.php';
                require_once ABSPATH . 'wp-admin/includes/media.php';
            }

            // Fetch promoted rows for these tokens
            $placeholders2 = implode(',', array_fill(0, count($tokens), '%s'));
            $sqlFetch = "SELECT id, path, url, mime, storage_mode, token" . ($this->hasTableColumn($table, 'attachment_id') ? ", attachment_id" : "") . " FROM %i 
                          WHERE token IN ($placeholders2) AND submission_id = %d AND status = 'attached'";
            $argsFetch = array_merge([ $table ], $tokens, [ $submission_id ]);
            $rows = $wpdb->get_results($wpdb->prepare($sqlFetch, $argsFetch), ARRAY_A);

            if ($rows) {
                $canStoreAttachmentId = $this->hasTableColumn($table, 'attachment_id');
                foreach ($rows as $row) {
                    // Normalize storage_mode for media library
                    $mode = isset($row['storage_mode']) ? strtolower(trim((string) $row['storage_mode'])) : '';
                    $new_url = isset($row['url']) ? $row['url'] : '';
                    $mime = isset($row['mime']) ? $row['mime'] : '';
                    $attach_id = isset($row['attachment_id']) ? $row['attachment_id'] : 0;
                    if (in_array($mode, array('media_library','media'), true)) {
                        $file_path = $row['path'] ?? '';
                        $mime      = $row['mime'] ?? '';
                        if (!$file_path || !file_exists($file_path)) { 
                            // Fire per-file promotion hook even if file missing
                            $promoted_context = array(
                                'id'             => isset($row['id']) ? (int) $row['id'] : 0,
                                'token'          => isset($row['token']) ? (string) $row['token'] : '',
                                'submission_id'  => (int) $submission_id,
                                'form_id'        => (int) $form_id,
                                'path'           => isset($row['path']) ? (string) $row['path'] : '',
                                'url'            => isset($new_url) && $new_url ? (string) $new_url : (isset($row['url']) ? (string) $row['url'] : ''),
                                'mime'           => isset($mime) && $mime ? (string) $mime : (isset($row['mime']) ? (string) $row['mime'] : ''),
                                'storage_mode'   => $mode,
                                'attachment_id'  => isset($attach_id) && $attach_id ? (int) $attach_id : (isset($row['attachment_id']) ? (int) $row['attachment_id'] : 0),
                                'promoted_at_utc'=> $now_utc,
                            );
                            /**
                             * Fires after a single file is promoted to 'attached' for a submission.
                             *
                             * @param array $promoted_context Context with file row data and submission IDs.
                             */
                            do_action('griffinforms_files_after_promote', $promoted_context);
                            continue; 
                        }

                        // Derive MIME if missing
                        if ($mime === '' || !is_string($mime)) {
                            $ft = wp_check_filetype($file_path);
                            if (!empty($ft['type'])) {
                                $mime = $ft['type'];
                            } elseif (function_exists('mime_content_type')) {
                                $mime = (string) @mime_content_type($file_path);
                            } else {
                                $mime = 'application/octet-stream';
                            }
                        }

                        // Prepare attachment post
                        $file_name = wp_basename($file_path);
                        $attachment = array(
                            'post_mime_type' => $mime,
                            'post_title'     => sanitize_text_field(pathinfo($file_name, PATHINFO_FILENAME)),
                            'post_content'   => '',
                            'post_status'    => 'inherit',
                        );

                        // Insert attachment and generate metadata
                        $attach_id = wp_insert_attachment($attachment, $file_path);
                        if (!is_wp_error($attach_id) && $attach_id) {
                            $metadata = wp_generate_attachment_metadata($attach_id, $file_path);
                            if (!is_wp_error($metadata) && !empty($metadata)) {
                                wp_update_attachment_metadata($attach_id, $metadata);
                            }
                            $new_url = wp_get_attachment_url($attach_id);

                            // Build update data
                            $update = array('url' => $new_url, 'updated_at' => $now_utc);
                            $fmt    = array('%s','%s');
                            if ($canStoreAttachmentId) { $update['attachment_id'] = (int) $attach_id; $fmt[] = '%d'; }

                            $wpdb->update($table, $update, array('id' => (int)$row['id']), $fmt, array('%d'));
                        }
                    }
                    // Fire per-file promotion hook with contextual data
                    $promoted_context = array(
                        'id'             => isset($row['id']) ? (int) $row['id'] : 0,
                        'token'          => isset($row['token']) ? (string) $row['token'] : '',
                        'submission_id'  => (int) $submission_id,
                        'form_id'        => (int) $form_id,
                        'path'           => isset($row['path']) ? (string) $row['path'] : '',
                        'url'            => isset($new_url) && $new_url ? (string) $new_url : (isset($row['url']) ? (string) $row['url'] : ''),
                        'mime'           => isset($mime) && $mime ? (string) $mime : (isset($row['mime']) ? (string) $row['mime'] : ''),
                        'storage_mode'   => $mode,
                        'attachment_id'  => isset($attach_id) && $attach_id ? (int) $attach_id : (isset($row['attachment_id']) ? (int) $row['attachment_id'] : 0),
                        'promoted_at_utc'=> $now_utc,
                    );
                    /**
                     * Fires after a single file is promoted to 'attached' for a submission.
                     *
                     * @param array $promoted_context Context with file row data and submission IDs.
                     */
                    do_action('griffinforms_files_after_promote', $promoted_context);
                }
                /**
                 * Fires after processing a batch of promoted files for a submission.
                 *
                 * @param int   $submission_id Submission ID.
                 * @param int   $count         Number of rows processed in this batch.
                 * @param array $tokens        Original tokens seen in this promotion call (may include non-updated).
                 */
                do_action('griffinforms_files_after_promote_batch', (int) $submission_id, (int) count($rows), $tokens);
            }
        }

        // Log a compact summary
        $this->log->add(
            'info',
            'submission',
            $submission_id,
            get_current_user_id(),
            sprintf('Promoted %d file(s) for submission. Tokens: %s', (int) $affected, substr(implode(',', $tokens), 0, 500)),
            'upload'
        );

        return (int) $affected;
    }

    /**
     * Schedules admin and user autoresponder email tasks if configured.
     */
    public function runActions()
    {
        $submission_id = isset($this->data['sub_id']) ? (int) $this->data['sub_id']
            : (isset($this->data['submission']['id']) ? (int) $this->data['submission']['id'] : 0);

        // 1) Promote any temp-uploaded files referenced by this submission
        $this->promoteFiles();

        $email_alert = $this->isEmailAlert();
        $alert_emails = $email_alert ? $this->normalizeAlertEmailMap($this->alert_emails) : [];

        $has_autoresponder = $this->hasUserAutoresponderMappings();
        $autoresponder_mappings = $has_autoresponder ? $this->form->getProp('autoresponder') : [];

        // 2) Dispatch pipeline handlers with reconstructed context
        $this->dispatchPipelineHandlers($submission_id, [
            'mail_meta' => [
                'alert_emails' => $alert_emails,
                'autoresponder_mappings' => $autoresponder_mappings,
                'field_value_map' => $this->buildFieldValueMap(),
            ],
            'account_meta' => [
                'field_value_map' => $this->buildFieldValueMap(),
                'field_sequence_map' => $this->buildFieldSequenceMap(),
            ],
        ]);
    }

    private function dispatchPipelineHandlers(int $submission_id, array $extra_context = []): void
    {
        if ($submission_id <= 0) {
            return;
        }

        $builder = \GriffinForms\Includes\Pipelines\SubmissionContextBuilder::getInstance();
        $context = $builder->build($submission_id);

        $context['request'] = [
            'form_id'  => isset($this->data['form']) ? (int) $this->data['form'] : 0,
            'page'     => isset($this->data['page']) ? (int) $this->data['page'] : 0,
            'position' => $this->data['position'] ?? '',
            'raw'      => $this->data,
        ];

        // Expose the fully-loaded Form object so handlers can call getProp() (e.g. 'notifications').
        if ($this->form !== null) {
            $context['form_object'] = $this->form;
        }

        $context = apply_filters('griffinforms_post_submission_pipeline_context', $context, $this->data);

        if (!empty($extra_context)) {
            $context = array_merge($context, $extra_context);
        }

        $pipeline = \GriffinForms\Includes\Pipelines\PostSubmissionPipeline::getInstance();
        do_action('griffinforms_post_submission_before_pipeline', $context);
        $pipeline->dispatch($context);
        do_action('griffinforms_post_submission_after_pipeline', $context);
    }

    private function buildFieldValueMap(): array
    {
        $map = [];
        foreach ($this->getFieldCollections() as $fieldsById) {
            foreach ($fieldsById as $field_id => $instances) {
                $values = $this->flattenFieldValues($instances);
                if (!isset($map[$field_id])) {
                    $map[$field_id] = [];
                }
                $map[$field_id] = array_merge($map[$field_id], $values);
            }
        }

        foreach ($map as $field_id => $values) {
            $values = array_map('trim', $values);
            $values = array_filter($values, static function ($value) {
                return $value !== '';
            });
            $map[$field_id] = array_values(array_unique($values));
        }

        return $map;
    }

    private function buildFieldSequenceMap(): array
    {
        $map = [];
        $seen_sequences = [];

        foreach ($this->getFieldCollections() as $fieldsById) {
            foreach ($fieldsById as $field_id => $instances) {
                $values = $this->flattenFieldValuesPreserveSequence($instances);
                if (empty($values)) {
                    continue;
                }

                $sequence_key = wp_json_encode(array_values($values));
                if (!isset($seen_sequences[$field_id])) {
                    $seen_sequences[$field_id] = [];
                }

                if ($sequence_key !== false && isset($seen_sequences[$field_id][$sequence_key])) {
                    continue;
                }

                if (!isset($map[$field_id])) {
                    $map[$field_id] = [];
                }

                $map[$field_id] = array_merge($map[$field_id], $values);

                if ($sequence_key !== false) {
                    $seen_sequences[$field_id][$sequence_key] = true;
                }
            }
        }

        return $map;
    }

    private function flattenFieldValuesPreserveSequence($value): array
    {
        if (is_array($value)) {
            if (array_key_exists('value', $value) && is_scalar($value['value'])) {
                return [trim((string) $value['value'])];
            }

            $flattened = [];
            foreach ($value as $entry) {
                $flattened = array_merge($flattened, $this->flattenFieldValuesPreserveSequence($entry));
            }
            return $flattened;
        }

        if (is_scalar($value)) {
            return [trim((string) $value)];
        }

        return [];
    }

    /**
     * Resolve the logging category for mail-related events.
     */
    private function getMailLogCategory(): string
    {
        if ($this->mail_log_category) {
            return $this->mail_log_category;
        }

        $registry = \GriffinForms\Includes\Integrations\Categories\Mailer::getInstance();
        $provider = $registry ? $registry->getActiveProviderId() : 'server_mail';
        if (empty($provider)) {
            $provider = 'server_mail';
        }

        $this->mail_log_category = sprintf('Mail Integration (%s)', $provider);
        return $this->mail_log_category;
    }

    private function getFormId(): int
    {
        if ($this->form && method_exists($this->form, 'getProp')) {
            $form_id = (int) ($this->form->getProp('id') ?? 0);
            if ($form_id > 0) {
                return $form_id;
            }
        }

        return isset($this->data['form']) ? (int) $this->data['form'] : 0;
    }
}
