<?php

namespace Bricksforge\ProForms\Actions;

if (!defined('ABSPATH')) exit; // Exit if accessed directly

include_once(BRICKSFORGE_PATH . '/includes/api/FormsHelper.php');
require_once(BRICKSFORGE_PATH . '/includes/elements/pro-forms/LoggingActionWrapper.php');


use Bricksforge\ProForms\LoggingActionWrapper;
use Bricksforge\ProForms\ProFormsLogger;

class Init
{
    private $forms_helper;
    private $post_id;
    private $form_id;
    private $form_id_fallback;
    private $initial_files = [];
    private $form_data;
    private $form_structure;

    public function __construct()
    {
        $this->forms_helper = new \Bricksforge\Api\FormsHelper();
    }

    /**
     * Before Form Submit
     *
     * @param WP_REST_Request $request Full details about the request.
     *
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function form_submit(\WP_REST_Request $request)
    {
        $form_data = $request->get_body_params();

        // We need to disable all errors except for E_ERROR, E_PARSE, E_CORE_ERROR and E_COMPILE_ERROR
        $original_error_reporting = error_reporting();
        error_reporting(E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR);

        $form_id = $request->get_param('formId');
        $this->form_id = $form_id;
        $form_id_fallback = $request->get_param('formIdFallback');
        $this->form_id_fallback = $form_id_fallback;

        $file_uploads = isset($form_data['temporaryFileUploads']) ? json_decode($form_data['temporaryFileUploads'], true) : [];

        if (!empty($file_uploads)) {
            $file_uploads = $this->handle_temporary_file_uploads($file_uploads);
        }

        $merged_files = array_merge($file_uploads, $request->get_file_params());

        $initial_files = isset($form_data['initialFiles']) ? json_decode($form_data['initialFiles'], true) : [];
        if (!empty($initial_files)) {
            $this->initial_files = $initial_files;
        }

        $post_id = absint($form_data['postId']);
        $this->post_id = $post_id;
        $dynamic_post_id = $request->get_param('dynamicPostId');

        $field_ids = $request->get_param('fieldIds') ? json_decode($request->get_param('fieldIds')) : null;
        $post_context = $request->get_param('postContext') ? json_decode($request->get_param('postContext')) : null;
        $captcha_result = $request->get_param('captchaResult');
        $turnstile_result = $request->get_param('turnstileResult');

        $form_settings = \Bricks\Helpers::get_element_settings($post_id, $form_id);

        if (empty($form_settings)) {
            $form_settings = \Bricks\Helpers::get_element_settings($post_id, $form_id_fallback);
        }

        // If $form_settings is empty, we try to replace the $post_id with the current template id
        if (empty($form_settings)) {
            $template_ids = $request->get_param('templateIds') ? json_decode($request->get_param('templateIds')) : null;

            if (!empty($template_ids)) {
                // We try it with all template ids. If we find a match, we use it.
                foreach ($template_ids as $template_id) {
                    $form_settings = \Bricks\Helpers::get_element_settings($template_id, $form_id);
                    if (!empty($form_settings)) {
                        break;
                    }
                    // Also try with form_id_fallback (for Components where the original element ID differs)
                    $form_settings = \Bricks\Helpers::get_element_settings($template_id, $form_id_fallback);
                    if (!empty($form_settings)) {
                        break;
                    }
                }
            }
        }

        // Fallback for Bricks Components: Try to get settings from component element (@since 3.1.8.1)
        if (empty($form_settings)) {
            if (method_exists('\Bricks\Helpers', 'get_component_element_by_id')) {
                // Extract the actual element ID from the combined form_id (elementId-componentInstanceId)
                $original_form_id = $form_id;
                if (strpos($form_id, '-') !== false) {
                    $parts = explode('-', $form_id);
                    $original_form_id = $parts[0]; // First part is the element ID
                }

                $component_element = \Bricks\Helpers::get_component_element_by_id($original_form_id);

                if (isset($component_element['settings'])) {
                    $form_settings = $component_element['settings'];
                }

                // Try with form_id_fallback if still not found
                if (empty($form_settings) && $form_id_fallback) {
                    $fallback_original = $form_id_fallback;
                    if (strpos($form_id_fallback, '-') !== false) {
                        $parts = explode('-', $form_id_fallback);
                        $fallback_original = $parts[0];
                    }

                    $component_element = \Bricks\Helpers::get_component_element_by_id($fallback_original);

                    if (isset($component_element['settings'])) {
                        $form_settings = $component_element['settings'];
                    }
                }
            }
        }

        if (!isset($form_settings) || empty($form_settings)) {
            wp_send_json_error(array(
                'message' => __('No form settings found.', 'bricksforge'),
            ));

            return false;
        }

        $hcaptcha_enabled = $this->has_hcaptcha_enabled($form_settings, $post_id, $form_id);
        $turnstile_enabled = $this->has_turnstile_enabled($form_settings, $post_id, $form_id);
        $recaptcha_enabled = $this->has_recaptcha_enabled($form_settings);

        if (!isset($form_settings['actions']) || empty($form_settings['actions'])) {
            wp_send_json_error(array(
                'message' => __('No action has been set for this form.', 'bricksforge'),
            ));
        }

        $form_actions = $form_settings['actions'];

        // Handle files with form settings for validation
        $form_files = $this->handle_files($merged_files, $form_settings);

        // Check if file handling failed due to validation errors
        if (is_wp_error($form_files)) {
            wp_send_json_error(array(
                'message' => $form_files->get_error_message(),
                'rejected_files' => $form_files->get_error_data()['rejected_files'] ?? []
            ));
            return;
        }

        $return_values = array();

        // Honeypot check. If $form_data contains a key form-field-guardian42 and the value is 1, we stop here.
        if (isset($form_data['form-field-guardian42']) && $form_data['form-field-guardian42'] == 1) {
            wp_send_json_error(array(
                'message' => __('You are not allowed to submit this form.', 'bricksforge'),
            ));

            die();
        }

        // First of all, we need to check if the captcha is valid. If not, we need to stop here.
        if ($hcaptcha_enabled == true) {
            if (!$this->forms_helper->handle_hcaptcha($form_settings, $form_data, $captcha_result ? $captcha_result : null)) {
                wp_send_json_error(array(
                    'message' => __('Please verify that you are human.', 'bricksforge'),
                ));
                return false;
            }
        }

        if ($turnstile_enabled == true) {
            if (!$this->forms_helper->handle_turnstile($form_settings, $form_data, $turnstile_result ? $turnstile_result : null)) {
                wp_send_json_error(array(
                    'message' => __(isset($form_settings['turnstileErrorMessage']) ? $form_settings['turnstileErrorMessage'] : 'Your submission is being verified. Please wait a moment before submitting again.', 'bricksforge'),
                ));

                return false;
            }
        }

        // Google ReCaptcha v3 validation
        if ($recaptcha_enabled == true) {
            if (!$this->forms_helper->handle_recaptcha($form_settings, $form_data, $captcha_result ? $captcha_result : null)) {
                wp_send_json_error(array(
                    'message' => __('Google reCaptcha verification failed. Please try again.', 'bricksforge'),
                ));

                return false;
            }
        }

        // Add File Data to Form Data
        if (isset($form_files) && !empty($form_files)) {
            foreach ($form_files as $field => $files) {
                foreach ($files as $file) {

                    if ($field == 'brfr' && isset($file['fieldId']) && isset($file['itemIndex']) && isset($file['subFieldId'])) {
                        $form_data["brfr"][$file['fieldId']][$file['itemIndex']][$file['subFieldId']] = $file;
                    } else {
                        $form_data[$field] = $file;
                    }
                }
            }
        }

        // Run initial sanitation
        $form_data = $this->forms_helper->initial_sanitization($form_settings, $form_data, $field_ids, $post_id);
        $this->form_data = $form_data;

        // Validate Fields
        $hidden_fields = $request->get_param('hiddenFields') ? json_decode($request->get_param('hiddenFields')) : null;
        $fields_to_validate = $request->get_param('fieldsToValidate') ? json_decode($request->get_param('fieldsToValidate')) : null;
        $validation_result = $this->forms_helper->validate($field_ids, $form_data, $post_id, $hidden_fields, $fields_to_validate);

        $validation_result = apply_filters('bricksforge/pro_forms/validate', $validation_result, $form_data, $post_id, $form_id);

        if ($validation_result !== true) {
            LoggingActionWrapper::log_validation_error($validation_result, $form_id, $post_id);
            wp_send_json_error(array(
                'validation' => $validation_result
            ));
        }

        // Log form submission start
        LoggingActionWrapper::log_form_submission_start($form_id, $post_id, $form_data);

        // Trigger bricksforge/pro_forms/before_submit action
        do_action('bricksforge/pro_forms/before_submit', $form_data);

        $this->form_structure = $this->forms_helper->build_form_structure($post_id, $form_id);

        if ($this->has_action_conditions($form_settings)) {
            $form_actions = $this->handle_action_conditions($form_settings);
        }

        // If there are no actions left, we stop here
        if (empty($form_actions)) {
            wp_send_json_error(array(
                'message' => __('No actions to perform. Please contact the site administrator.', 'bricksforge'),
            ));

            return false;
        }

        // Form Base
        $base = new \Bricksforge\ProForms\Actions\Base($form_settings, $form_data, $form_files, $post_id, $form_id, $dynamic_post_id, $this->form_structure, $post_context);
        $executed_actions = array();

        // IMPORTANT: If Stripe action is present, execute it FIRST and wait for webhook before executing other actions
        if (in_array('stripe', $form_actions)) {
            $executed_actions[] = 'stripe';
            $base->update_proceeding_action('stripe');

            $action = new \Bricksforge\ProForms\Actions\Stripe();
            $result = LoggingActionWrapper::execute_with_logging('stripe', function () use ($action, $base) {
                return $action->run($base);
            }, $base);

            // If Stripe action was successful, save all OTHER actions for webhook execution
            if (isset($result['stripe_redirect']) && $result['stripe_redirect'] === true) {
                $session_identifier = $result['session_identifier'];

                // Remove 'stripe' from actions to execute later
                $actions_to_execute_after_payment = array_diff($form_actions, ['stripe']);

                // Store actions and all necessary data for webhook
                set_transient('brf_stripe_pending_actions_' . $session_identifier, array(
                    'form_actions' => array_values($actions_to_execute_after_payment),
                    'form_settings' => $form_settings,
                    'form_data' => $form_data,
                    'form_files' => $form_files,
                    'post_id' => $post_id,
                    'form_id' => $form_id,
                    'dynamic_post_id' => $dynamic_post_id,
                    'form_structure' => $this->form_structure,
                    'post_context' => $post_context,
                    'status' => 'pending',
                    'created_at' => time(),
                    'checkout_session_id' => $result['session_id'] // Stripe Checkout Session ID for status verification
                ), 30 * MINUTE_IN_SECONDS);

                // Return immediately with Stripe redirect
                $return_values['results'] = $base->results;
                error_reporting($original_error_reporting);
                return $return_values;
            }
        }

        // IMPORTANT: If Mollie action is present, execute it FIRST and wait for webhook before executing other actions
        if (in_array('mollie', $form_actions)) {
            $executed_actions[] = 'mollie';
            $base->update_proceeding_action('mollie');

            $action = new \Bricksforge\ProForms\Actions\Mollie();
            $result = LoggingActionWrapper::execute_with_logging('mollie', function () use ($action, $base) {
                return $action->run($base);
            }, $base);

            // If Mollie action was successful, save all OTHER actions for webhook execution
            if (isset($result['mollie_redirect']) && $result['mollie_redirect'] === true) {
                $session_identifier = $result['session_identifier'];

                // Remove 'mollie' from actions to execute later
                $actions_to_execute_after_payment = array_diff($form_actions, ['mollie']);

                // Store actions and all necessary data for webhook
                set_transient('brf_mollie_pending_actions_' . $session_identifier, array(
                    'form_actions' => array_values($actions_to_execute_after_payment),
                    'form_settings' => $form_settings,
                    'form_data' => $form_data,
                    'form_files' => $form_files,
                    'post_id' => $post_id,
                    'form_id' => $form_id,
                    'dynamic_post_id' => $dynamic_post_id,
                    'form_structure' => $this->form_structure,
                    'post_context' => $post_context,
                    'status' => 'pending',
                    'created_at' => time(),
                    'payment_id' => $result['payment_id'] // Mollie Payment ID for status verification
                ), 30 * MINUTE_IN_SECONDS);

                // Return immediately with Mollie redirect
                $return_values['results'] = $base->results;
                error_reporting($original_error_reporting);
                return $return_values;
            }
        }

        // Handle Form Actions
        if (in_array('create_pdf', $form_actions)) {
            $base->update_proceeding_action('create_pdf');
            $executed_actions[] = 'create_pdf';

            $result = LoggingActionWrapper::execute_with_logging('create_pdf', function () use ($base) {
                $action = new \Bricksforge\ProForms\Actions\Create_Pdf();
                return $action->run($base);
            }, $base);
        }

        if (in_array('post_create', $form_actions)) {
            $base->update_proceeding_action('post_create');
            $executed_actions[] = 'post_create';

            $result = LoggingActionWrapper::execute_with_logging('post_create', function () use ($base) {
                $action = new \Bricksforge\ProForms\Actions\Create_Post();
                return $action->run($base);
            }, $base);
        }

        if (in_array('post_update', $form_actions)) {
            $executed_actions[] = 'post_update';
            $base->update_proceeding_action('post_update');

            $action = new \Bricksforge\ProForms\Actions\Update_Post();
            $result = LoggingActionWrapper::execute_with_logging('post_update', function () use ($action, $base) {
                return $action->run($base);
            }, $base);
        }

        if (in_array('post_delete', $form_actions)) {
            $executed_actions[] = 'post_delete';
            $base->update_proceeding_action('post_delete');

            $action = new \Bricksforge\ProForms\Actions\Delete_Post();
            $result = LoggingActionWrapper::execute_with_logging('post_delete', function () use ($action, $base) {
                return $action->run($base);
            }, $base);

            if ($result === false) {
                wp_send_json_error(array(
                    'message' => __('Deletion failed.', 'bricksforge'),
                ));
            }
        }

        if (in_array('add_option', $form_actions)) {
            $executed_actions[] = 'add_option';
            $base->update_proceeding_action('add_option');

            $action = new \Bricksforge\ProForms\Actions\Add_Option();
            $result = LoggingActionWrapper::execute_with_logging('add_option', function () use ($action, $base) {
                return $action->run($base);
            }, $base);
        }

        if (in_array('update_option', $form_actions)) {
            $executed_actions[] = 'update_option';
            $base->update_proceeding_action('update_option');

            $action = new \Bricksforge\ProForms\Actions\Update_Option();
            $result = LoggingActionWrapper::execute_with_logging('update_option', function () use ($action, $base) {
                return $action->run($base);
            }, $base);

            if ($result) {
                $return_values['update_option'] = $result;
            }
        }

        if (in_array('delete_option', $form_actions)) {
            $executed_actions[] = 'delete_option';
            $base->update_proceeding_action('delete_option');

            $action = new \Bricksforge\ProForms\Actions\Delete_Option();
            $result = LoggingActionWrapper::execute_with_logging('delete_option', function () use ($action, $base) {
                return $action->run($base);
            }, $base);
        }

        if (in_array('update_post_meta', $form_actions)) {
            $executed_actions[] = 'update_post_meta';
            $base->update_proceeding_action('update_post_meta');

            $action = new \Bricksforge\ProForms\Actions\Update_Post_Meta();
            $result = LoggingActionWrapper::execute_with_logging('update_post_meta', function () use ($action, $base) {
                return $action->run($base);
            }, $base);

            if ($result) {
                $return_values['update_post_meta'] = $result;
            }
        }

        if (in_array('set_storage_item', $form_actions)) {
            $executed_actions[] = 'set_storage_item';
            $base->update_proceeding_action('set_storage_item');

            $action = new \Bricksforge\ProForms\Actions\Set_Storage_Item();
            $result = LoggingActionWrapper::execute_with_logging('set_storage_item', function () use ($action, $base) {
                return $action->run($base);
            }, $base);

            if ($result) {
                $return_values['set_storage_item'] = $result;
            }
        }

        if (in_array('create_submission', $form_actions)) {
            $executed_actions[] = 'create_submission';
            $base->update_proceeding_action('create_submission');

            $action = new \Bricksforge\ProForms\Actions\Create_Submission();
            $result = LoggingActionWrapper::execute_with_logging('create_submission', function () use ($action, $base) {
                return $action->run($base);
            }, $base);

            // If result is an array and result[0] is true, we have a duplicate submission
            if (is_array($result) && isset($result["status"]) && $result["status"] === "duplicate") {
                wp_send_json_error(array(
                    'message' => $result["message"],
                ));
            }
        }

        if (in_array('wc_add_to_cart', $form_actions)) {
            $executed_actions[] = 'wc_add_to_cart';
            $base->update_proceeding_action('wc_add_to_cart');

            $action = new \Bricksforge\ProForms\Actions\Wc_Add_To_Cart();
            $result = LoggingActionWrapper::execute_with_logging('wc_add_to_cart', function () use ($action, $base) {
                return $action->run($base);
            }, $base);

            if ($result) {
                $return_values['wc_add_to_cart'] = $result;
            }
        }

        if (in_array('webhook', $form_actions)) {
            $executed_actions[] = 'webhook';
            $base->update_proceeding_action('webhook');

            $action = new \Bricksforge\ProForms\Actions\Webhook();
            $result = LoggingActionWrapper::execute_with_logging('webhook', function () use ($action, $base) {
                return $action->run($base);
            }, $base);

            if ($result) {
                $return_values['webhook'] = $result;
            }
        }

        if (in_array('login', $form_actions)) {
            $executed_actions[] = 'login';
            $base->update_proceeding_action('login');

            $action = new \Bricksforge\ProForms\Actions\Login();
            LoggingActionWrapper::execute_with_logging('login', function () use ($action, $base) {
                return $action->run($base);
            }, $base);
        }

        if (in_array('registration', $form_actions)) {
            $executed_actions[] = 'registration';
            $base->update_proceeding_action('registration');

            $action = new \Bricksforge\ProForms\Actions\Registration();
            LoggingActionWrapper::execute_with_logging('registration', function () use ($action, $base) {
                return $action->run($base);
            }, $base);
        }

        if (in_array('update_user', $form_actions)) {
            $executed_actions[] = 'update_user';
            $base->update_proceeding_action('update_user');

            $action = new \Bricksforge\ProForms\Actions\Update_User();
            LoggingActionWrapper::execute_with_logging('update_user', function () use ($action, $base) {
                return $action->run($base);
            }, $base);
        }

        if (in_array('update_user_meta', $form_actions)) {
            $executed_actions[] = 'update_user_meta';
            $base->update_proceeding_action('update_user_meta');

            $action = new \Bricksforge\ProForms\Actions\Update_User_Meta();
            $result = LoggingActionWrapper::execute_with_logging('update_user_meta', function () use ($action, $base) {
                return $action->run($base);
            }, $base);

            if ($result) {
                $return_values['update_user_meta'] = $result;
            }
        }

        if (in_array('reset_user_password', $form_actions)) {
            $executed_actions[] = 'reset_user_password';
            $base->update_proceeding_action('reset_user_password');

            $action = new \Bricksforge\ProForms\Actions\Reset_User_Password();
            LoggingActionWrapper::execute_with_logging('reset_user_password', function () use ($action, $base) {
                return $action->run($base);
            }, $base);
        }

        if (in_array('mailchimp', $form_actions)) {
            $executed_actions[] = 'mailchimp';
            $base->update_proceeding_action('mailchimp');

            $action = new \Bricksforge\ProForms\Actions\Mailchimp();
            LoggingActionWrapper::execute_with_logging('mailchimp', function () use ($action, $base) {
                return $action->run($base);
            }, $base);
        }

        if (in_array('mailerpress', $form_actions)) {
            $executed_actions[] = 'mailerpress';
            $base->update_proceeding_action('mailerpress');

            $action = new \Bricksforge\ProForms\Actions\Mailerpress();
            LoggingActionWrapper::execute_with_logging('mailerpress', function () use ($action, $base) {
                return $action->run($base);
            }, $base);
        }

        if (in_array('sendgrid', $form_actions)) {
            $executed_actions[] = 'sendgrid';
            $base->update_proceeding_action('sendgrid');

            $action = new \Bricksforge\ProForms\Actions\Sendgrid();
            LoggingActionWrapper::execute_with_logging('sendgrid', function () use ($action, $base) {
                return $action->run($base);
            }, $base);
        }

        if (in_array('email', $form_actions)) {
            $base->update_proceeding_action('email');
            $executed_actions[] = 'email';

            LoggingActionWrapper::execute_with_logging('email', function () use ($base) {
                $action = new \Bricksforge\ProForms\Actions\Email();
                return $action->run($base);
            }, $base);
        }

        if (in_array('custom', $form_actions)) {
            $executed_actions[] = 'custom';
            $base->update_proceeding_action('custom');

            $action = new \Bricksforge\ProForms\Actions\Custom();
            LoggingActionWrapper::execute_with_logging('custom', function () use ($action, $base) {
                return $action->run($base);
            }, $base);
        }

        // Stripe action is now handled at the beginning of form_submit()
        // to ensure payment is processed before other actions

        if (in_array('show_download_info', $form_actions)) {
            $base->update_proceeding_action('show_download_info');

            $download_info_element = array_filter($this->form_structure, function ($element) {
                return $element['name'] === 'file-download';
            });

            // Re-index the array
            $download_info_element = array_values($download_info_element);

            $download_url = isset($download_info_element) && isset($download_info_element[0]) && isset($download_info_element[0]['settings']) && isset($download_info_element[0]['settings']['download_url']) ? $download_info_element[0]['settings']['download_url'] : '';
            $download_trigger = isset($download_info_element) && isset($download_info_element[0]) && isset($download_info_element[0]['settings']) && isset($download_info_element[0]['settings']['trigger']) ? $download_info_element[0]['settings']['trigger'] : 'automatic';

            $base->set_result(
                array(
                    'action'  => 'show_download_info',
                    'status' => 'success',
                    'trigger' => $download_trigger,
                    'downloadUrl' => $download_url
                )
            );
        }

        if (in_array('redirect', $form_actions)) {
            $base->update_proceeding_action('redirect');

            $action = new \Bricksforge\ProForms\Actions\Redirect();
            $action->run($base);
        }

        if (in_array('reload', $form_actions)) {
            $base->update_proceeding_action('reload');

            $action = new \Bricksforge\ProForms\Actions\Reload();
            $action->run($base);
        }

        if (in_array('confetti', $form_actions)) {
            $base->update_proceeding_action('confetti');

            $base->set_result(
                array(
                    'action'  => 'confetti',
                    'status' => 'success',
                )
            );
        }

        $return_values['results'] = $base->results;

        // Log form submission completion
        LoggingActionWrapper::log_form_submission_complete($form_id, $post_id, $executed_actions);

        // Trigger bricksforge/pro_forms/before_submit action
        do_action('bricksforge/pro_forms/after_submit', $form_data, $return_values);

        // Remove submitted files from temp tracking (they are now permanent)
        $this->clean_up_temp_files($form_files);

        // Note: Temp directory cleanup is handled by hourly cron job
        // to prevent conflicts with long-running form sessions

        // Restore the original error reporting
        error_reporting($original_error_reporting);

        return $return_values;
    }

    public function has_action_conditions($form_settings)
    {
        return isset($form_settings['actionHasConditions']) ? $form_settings['actionHasConditions'] : false;
    }

    public function handle_action_conditions($form_settings)
    {
        $form_actions = $form_settings['actions'];

        $conditions = isset($form_settings['actionConditions']) ? $form_settings['actionConditions'] : [];

        if (empty($conditions)) {
            return $form_actions;
        }

        $result = $this->forms_helper->handle_conditions($conditions, null, $this->form_data, true, "action");

        // Iterate through form actions to remove those that fail their conditions
        foreach ($form_actions as $index => $action) {

            // If the action has a condition and the condition is false, remove it
            if (isset($result[$action]) && !$result[$action]) {
                $key = array_search($action, $form_actions, true);
                if ($key !== false) {
                    unset($form_actions[$key]);
                }
            }
        }

        // Re-index the array to maintain sequential keys
        $form_actions = array_values($form_actions);

        return $form_actions;
    }

    public function handle_action_conditions_result($form_data, $form_settings)
    {
        return $form_data;
    }

    public function handle_temporary_file_uploads($file_uploads)
    {
        // $file_uploads is an array of file names, which are located in /uploads/bricksforge/tmp/
        // We want to locate it and convert it in a format like retrieved from $_FILES, which we can use with the handle_files function

        if (!is_array($file_uploads) || empty($file_uploads)) {
            return;
        }

        $files = [];

        foreach ($file_uploads as $file) {
            $file_data = $file["file"];
            $file_field = $file["field"];

            $file_path = $file_data['file'];
            $file_url = $file_data['url'];

            if (file_exists($file_path)) {
                $file_info = pathinfo($file_path);
                $file_size = filesize($file_path);

                $file_array = [
                    'name'     => [
                        $file_info['basename']
                    ],
                    'type'     => [
                        mime_content_type($file_path)
                    ],
                    'tmp_name' =>
                    [
                        $file_path
                    ],
                    'full_path' => [
                        $file_path
                    ],
                    'error'    => [
                        UPLOAD_ERR_OK
                    ],
                    'size'     => [
                        $file_size
                    ],
                    'url'      => [
                        $file_url
                    ]
                ];


                if (isset($files[$file_field])) {
                    $files[$file_field]['name'][] = $file_info['basename'];
                    $files[$file_field]['type'][] = mime_content_type($file_path);
                    $files[$file_field]['tmp_name'][] = $file_path;
                    $files[$file_field]['full_path'][] = $file_path;
                    $files[$file_field]['error'][] = UPLOAD_ERR_OK;
                    $files[$file_field]['size'][] = $file_size;
                    $files[$file_field]['url'][] = $file_url;
                } else {
                    $files[$file_field] = $file_array;
                }
            }
        }

        return $files;
    }

    /**
     * Clean up temporary files
     * @param mixed $form_files
     * @return void
     */
    public function clean_up_temp_files($form_files)
    {
        if (!is_array($form_files)) {
            return;
        }

        // Use lock to prevent race condition with cron cleanup and uploads
        $lock_key = 'bricksforge_temp_cleanup_lock';
        $lock_acquired = false;
        $max_attempts = 10;
        $attempt = 0;

        // Try to acquire lock with retry mechanism
        while (!$lock_acquired && $attempt < $max_attempts) {
            if (!get_transient($lock_key)) {
                set_transient($lock_key, 1, 10); // 10 seconds lock
                $lock_acquired = true;
            } else {
                usleep(100000); // Wait 0.1 seconds before retry
                $attempt++;
            }
        }

        if ($lock_acquired) {
            $temp_files = get_option('brf_tmp_files', array());

            if (!empty($temp_files)) {
                // Flatten the $form_files array and extract file paths
                $form_file_paths = array();

                foreach ($form_files as $field => $files) {
                    foreach ($files as $file) {
                        $form_file_paths[] = $file['file'];
                    }
                }

                // Use array_flip for faster lookup and removal
                $temp_files_flipped = array_flip($temp_files);

                foreach ($form_file_paths as $file_path) {
                    if (isset($temp_files_flipped[$file_path])) {
                        unset($temp_files_flipped[$file_path]);
                    }
                }

                // Flip back to get the cleaned up temp files array
                $temp_files = array_flip($temp_files_flipped);

                update_option('brf_tmp_files', $temp_files);
            }

            // Release lock
            delete_transient($lock_key);
        }
        // If lock not acquired, skip cleanup - will be handled by cron
    }

    /**
     * Handle files
     *
     * @param array $files
     * @return array|WP_Error
     */
    public function handle_files($files, $form_settings = null)
    {
        require_once(ABSPATH . 'wp-admin/includes/file.php');

        $initial_files = [];
        if (!empty($this->initial_files)) {
            $initial_files = $this->handle_initial_files();
        }

        if (empty($files) && empty($initial_files)) {
            return [];
        }

        $uploaded_files = [];
        $processed_files = []; // Array to keep track of processed files
        $rejected_files = []; // Track rejected files for error reporting

        foreach ($files as $input_name => $file_group) {
            $is_repeater = false;

            // Check if comes from repeater field
            if ($input_name == "brfr") {
                $is_repeater = true;
            }

            if ($is_repeater) {
                // Iterate through each level of the repeater field
                foreach ($file_group['name'] as $group_key => $item_group) {
                    foreach ($item_group as $item_key => $sub_item_group) {
                        foreach ($sub_item_group as $sub_item_key => $file_names) {
                            $file_array = $this->prepare_repeater_file_array($group_key, $item_key, $sub_item_key, $file_group);
                            // Use the adjusted structure to process files
                            $this->process_file_group($file_array, $input_name, $uploaded_files, $processed_files, $rejected_files);
                        }
                    }
                }
            } else {
                $this->process_file_group($file_group, $input_name, $uploaded_files, $processed_files, $rejected_files);
            }
        }

        if (!empty($initial_files)) {
            // We add the initial files to the uploaded files
            foreach ($initial_files as $file) {
                $field = $file['field'];

                if (!$field) {
                    continue;
                }

                if (isset($uploaded_files[$field]) && is_array($uploaded_files[$field])) {
                    array_push($uploaded_files[$field], $file);
                } else {
                    $uploaded_files[$field][] = $file;
                }
            }
        }

        $uploaded_files = $this->merge_and_order_files($uploaded_files);

        // If any files were rejected due to validation failures, return an error
        if (!empty($rejected_files)) {
            return new \WP_Error(
                'file_validation_failed',
                __('Some uploaded files were rejected due to security validation.', 'bricksforge'),
                array('rejected_files' => $rejected_files)
            );
        }

        return $uploaded_files;
    }

    /**
     * Reorders the final $uploaded_files array to respect FilePond ordering.
     * Requires a hidden input named "fileOrder" in the form_data, e.g.:
     *    <input type="hidden" name="fileOrder" value='{"form-field-xyz": ["file1url","file2url"]}' />
     */
    private function merge_and_order_files($uploaded_files)
    {
        // Sanitize and validate input from $_POST
        $file_order_array = isset($_POST['fileOrder']) ? sanitize_text_field($_POST['fileOrder']) : '';
        $file_order_array = stripslashes($file_order_array);
        $file_order = !empty($file_order_array)
            ? json_decode($file_order_array, true)
            : [];

        // If no order was posted, do nothing
        if (empty($file_order) || !is_array($file_order)) {
            return $uploaded_files;
        }

        // For each field in $uploaded_files, reorder based on $file_order[field]
        foreach ($uploaded_files as $field => &$files) {
            // Check if we have an order array for this field
            $order_key = $field;
            // Also check with '[]' appended (common in file input names)
            $order_key_alt = $field . '[]';

            $field_order = null;
            if (isset($file_order[$order_key])) {
                $field_order = $file_order[$order_key];
            } elseif (isset($file_order[$order_key_alt])) {
                $field_order = $file_order[$order_key_alt];
            }

            if ($field_order && is_array($field_order)) {
                $ordered_files = [];
                $unordered_files = $files;

                foreach ($field_order as $source_url) {
                    foreach ($unordered_files as $idx => $file) {
                        if (isset($file['url']) && $file['url'] === $source_url) {
                            $ordered_files[] = $file;
                            unset($unordered_files[$idx]);
                            break;
                        }
                    }
                }

                // Append any remaining files that weren't in the order array
                $files = array_merge($ordered_files, array_values($unordered_files));
            }
        }

        return $uploaded_files;
    }

    /**
     * Handle Initial Files
     * @param mixed $initial_files
     * @return void
     */
    private function handle_initial_files()
    {
        $initial_files = $this->initial_files;
        $files = [];

        $form_id = $this->form_id;

        // We find the initial files of the current form submission
        $form_initial_files_array = array_filter($initial_files, function ($file) use ($form_id) {
            return $file['formId'] == $form_id;
        });

        foreach ($form_initial_files_array as $item) {
            $file_array = isset($item['files']) ? $item['files'] : [];

            if (empty($file_array)) {
                continue;
            }

            foreach ($file_array as $single_file) {
                $url = $single_file['source'];

                $attachment_id = attachment_url_to_postid($url);

                if (!$attachment_id) {
                    wp_send_json_error('Invalid file URL');
                    return [];
                }

                // Get the file path and mime type
                $attachment_path = get_attached_file($attachment_id);
                $attachment_type = mime_content_type($attachment_path);
                $attachment_size = filesize($attachment_path);

                // Get file name
                $attachment_name = basename($attachment_path);

                array_push($files, [
                    'file' => $attachment_path,
                    'type' => $attachment_type,
                    'name' => $attachment_name,
                    'url' => $url,
                    'field' => "form-field-{$item['fieldId']}",
                    'attachmentId' => $attachment_id,
                    'fieldId' => $item['fieldId'],
                    'isInitialFile' => true
                ]);
            }
        }

        return $files;
    }

    private function process_file_group($file_group, $input_name, &$uploaded_files, &$processed_files, &$rejected_files)
    {
        if (empty($file_group['name'])) {
            return;
        }

        // SECURITY: Build form structure from form settings (not from user input)
        // Try with primary form_id first, then fallback to form_id_fallback if available
        $form_structure = $this->forms_helper->build_form_structure($this->post_id, $this->form_id);

        // If form structure is empty and we have a fallback form_id, try that
        if (empty($form_structure) && !empty($this->form_id_fallback)) {
            $form_structure = $this->forms_helper->build_form_structure($this->post_id, $this->form_id_fallback);
        }

        foreach ($file_group['name'] as $key => $value) {
            if (empty($file_group['name'][$key]) || $file_group['error'][$key] !== UPLOAD_ERR_OK) {
                continue;
            }

            $file_name = $file_group['name'][$key];
            $file_size = $file_group['size'][$key];
            $file_identifier = $file_name . '_' . $file_size; // Create a unique identifier

            $field_id = isset($file_group['fieldId']) ? $file_group['fieldId'] : '';
            $sub_field_id = isset($file_group['subFieldId']) ? $file_group['subFieldId'] : '';
            $item_index = isset($file_group['itemIndex']) ? $file_group['itemIndex'] : false;

            // Check if this file identifier has already been processed
            if (in_array($file_identifier, $processed_files)) {
                continue; // Skip this file as it's a duplicate
            }

            $file = [
                'name'     => $file_name,
                'type'     => $file_group['type'][$key],
                'tmp_name' => $file_group['tmp_name'][$key],
                'error'    => $file_group['error'][$key],
                'size'     => $file_size,
            ];

            if ($field_id) {
                $file['fieldId'] = $field_id;
            }

            if ($sub_field_id) {
                $file['subFieldId'] = $sub_field_id;
            }

            if ($item_index !== false) {
                $file['itemIndex'] = $item_index;
            }

            $uploaded = null;

            if (!is_uploaded_file($file['tmp_name'])) {
                // Already uploaded file - re-validate MIME type for security
                $finfo = finfo_open(FILEINFO_MIME_TYPE);
                $real_mime_type = finfo_file($finfo, $file['tmp_name']);
                finfo_close($finfo);

                // Additional validation: Check if extension matches MIME type
                $file_info = wp_check_filetype_and_ext($file['tmp_name'], $file['name']);
                $wp_allowed_mime_type = $file_info['type'];

                // Manual extension to MIME type validation
                $expected_mime_from_ext = $this->get_mime_type_from_extension($file['name']);

                // Get allowed file types from form structure for this specific field
                $allowed_file_types = $this->get_allowed_file_types_from_settings($form_structure, $input_name);
                $allowed_file_types = apply_filters('bricksforge/pro_forms/allowed_file_types', $allowed_file_types);

                // Security check: Validate MIME types
                // 1. Allowed types must be an array
                // 2. Real MIME type must be allowed
                // 3. If WordPress detected a type, it must also be allowed
                // 4. Real MIME type must match expected MIME type from extension (if known)
                if (
                    !is_array($allowed_file_types) ||
                    !in_array($real_mime_type, $allowed_file_types) ||
                    ($wp_allowed_mime_type !== false && !in_array($wp_allowed_mime_type, $allowed_file_types)) ||
                    ($expected_mime_from_ext && $real_mime_type !== $expected_mime_from_ext)
                ) {
                    // Track rejected file
                    $rejected_files[] = $file['name'] . ' (' . $real_mime_type . ')';
                    continue;
                }

                // Already uploaded file
                $uploaded = [
                    'file' => $file['tmp_name'],
                    'type' => $real_mime_type, // Use validated MIME type
                    'name' => $file['name'],
                    'url' => $file_group['url'][$key],
                    'field' => $input_name,
                ];

                if ($field_id) {
                    $uploaded['fieldId'] = $field_id;
                }

                if ($sub_field_id) {
                    $uploaded['subFieldId'] = $sub_field_id;
                }

                if ($item_index !== false) {
                    $uploaded['itemIndex'] = $item_index;
                }

                $uploaded_files[$input_name][] = $uploaded;
                $processed_files[] = $file_identifier; // Add file identifier to processed files
                continue;
            } else {
                // NEWLY UPLOADED FILE - Validate MIME type before processing
                $finfo = finfo_open(FILEINFO_MIME_TYPE);
                $real_mime_type = finfo_file($finfo, $file['tmp_name']);
                finfo_close($finfo);

                // Additional validation: Check if extension matches MIME type
                $file_info = wp_check_filetype_and_ext($file['tmp_name'], $file['name']);
                $wp_allowed_mime_type = $file_info['type'];

                // Manual extension to MIME type validation
                $expected_mime_from_ext = $this->get_mime_type_from_extension($file['name']);

                // Get allowed file types from form structure for this specific field
                $allowed_file_types = $this->get_allowed_file_types_from_settings($form_structure, $input_name);
                $allowed_file_types = apply_filters('bricksforge/pro_forms/allowed_file_types', $allowed_file_types);

                // Security check: Validate MIME types
                if (
                    !is_array($allowed_file_types) ||
                    !in_array($real_mime_type, $allowed_file_types) ||
                    ($wp_allowed_mime_type !== false && !in_array($wp_allowed_mime_type, $allowed_file_types)) ||
                    ($expected_mime_from_ext && $real_mime_type !== $expected_mime_from_ext)
                ) {
                    // Track rejected file
                    $rejected_files[] = $file['name'] . ' (' . $real_mime_type . ')';
                    continue;
                }


                // We use a filter to change the directory to
                add_filter('upload_dir', array($this, 'temporary_change_upload_dir'));

                $uploaded = wp_handle_upload($file, array('test_form' => false));

                remove_filter('upload_dir', array($this, 'temporary_change_upload_dir'));

                if ($uploaded && !isset($uploaded['error'])) {
                    // Use validated MIME type for consistency
                    $uploaded['type'] = $real_mime_type;
                    $uploaded['name'] = $file['name'];
                    $uploaded['field'] = $input_name;

                    if ($field_id) {
                        $uploaded['fieldId'] = $field_id;
                    }

                    if ($sub_field_id) {
                        $uploaded['subFieldId'] = $sub_field_id;
                    }

                    if ($item_index !== false) {
                        $uploaded['itemIndex'] = $item_index;
                    }

                    $uploaded_files[$input_name][] = $uploaded;
                    $processed_files[] = $file_identifier; // Add file identifier to processed files
                }
            }
        }
    }

    public function temporary_change_upload_dir($dir)
    {
        return array(
            'path' => $dir['basedir'] . BRICKSFORGE_TEMP_DIR_PATH_FROM_WP_UPLOAD_DIR,
            'url' => $dir['baseurl'] . BRICKSFORGE_TEMP_DIR_PATH_FROM_WP_UPLOAD_DIR,
            'subdir' => BRICKSFORGE_TEMP_DIR_PATH_FROM_WP_UPLOAD_DIR,
        ) + $dir;
    }

    private function prepare_repeater_file_array($group_key, $item_key, $sub_item_key, $file_group)
    {


        // Extracting and preparing file information from the complex repeater structure
        $file_array = [
            'name' => [],
            'type' => [],
            'tmp_name' => [],
            'error' => [],
            'size' => [],
            'fieldId' => '',
            'itemIndex' => '',
            'subFieldId' => ''
        ];
        foreach (['name', 'type', 'tmp_name', 'error', 'size'] as $attr) {
            foreach ($file_group[$attr][$group_key][$item_key][$sub_item_key] as $index => $value) {
                $file_array[$attr][] = $value;
            }
        }

        // We add the ID to the file array ($group_key)
        $file_array['fieldId'] = $group_key;
        $file_array['subFieldId'] = $sub_item_key;
        $file_array['itemIndex'] = $item_key;

        $result = $file_array;

        return $result;
    }

    /**
     * Check if hCaptcha is enabled (either through form settings or hCaptcha element)
     *
     * @param array $form_settings
     * @param int $post_id
     * @param string $form_id
     * @return bool
     */
    private function has_hcaptcha_enabled($form_settings, $post_id, $form_id)
    {
        // Check legacy form settings first (backwards compatibility)
        if (isset($form_settings['enableHCaptcha']) && $form_settings['enableHCaptcha']) {
            return true;
        }

        // Check if form has hCaptcha element (for nestable forms)
        $form_structure = $this->forms_helper->build_form_structure($post_id, $form_id);
        return $this->search_for_hcaptcha_element($form_structure);
    }

    /**
     * Check if Turnstile is enabled (either through form settings or Turnstile element)
     *
     * @param array $form_settings
     * @param int $post_id
     * @param string $form_id
     * @return bool
     */
    private function has_turnstile_enabled($form_settings, $post_id, $form_id)
    {
        // Check legacy form settings first (backwards compatibility)
        if (isset($form_settings['enableTurnstile']) && $form_settings['enableTurnstile']) {
            return true;
        }

        // Check if form has turnstile element (for nestable forms)
        $form_structure = $this->forms_helper->build_form_structure($post_id, $form_id);
        return $this->search_for_turnstile_element($form_structure);
    }

    /**
     * Check if Google ReCaptcha is enabled
     *
     * @param array $form_settings
     * @return bool
     */
    private function has_recaptcha_enabled($form_settings)
    {
        // Check if ReCaptcha is enabled in form settings
        if (isset($form_settings['enableRecaptcha']) && $form_settings['enableRecaptcha']) {
            return true;
        }

        return false;
    }

    /**
     * Recursively search for enabled hCaptcha element in form structure
     *
     * @param array $structure
     * @return bool
     */
    private function search_for_hcaptcha_element($structure)
    {
        if (!is_array($structure)) {
            return false;
        }

        foreach ($structure as $element) {
            if (isset($element['name']) && $element['name'] === 'brf-pro-forms-field-hcaptcha') {
                // Check if the element is enabled (default to true if setting doesn't exist)
                return !isset($element['settings']['enableHCaptcha']) || $element['settings']['enableHCaptcha'] === true;
            }

            if (isset($element['children']) && is_array($element['children'])) {
                if ($this->search_for_hcaptcha_element($element['children'])) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Recursively search for enabled Turnstile element in form structure
     *
     * @param array $structure
     * @return bool
     */
    private function search_for_turnstile_element($structure)
    {
        if (!is_array($structure)) {
            return false;
        }

        foreach ($structure as $element) {
            if (isset($element['name']) && $element['name'] === 'turnstile') {
                // Check if the element is enabled (default to true if setting doesn't exist)
                return !isset($element['settings']['enableTurnstile']) || $element['settings']['enableTurnstile'] === true;
            }

            if (isset($element['children']) && is_array($element['children'])) {
                if ($this->search_for_turnstile_element($element['children'])) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Get allowed file types from form structure for a specific field
     *
     * @param array $form_structure The form structure containing field information
     * @param string $input_name The input name (e.g., 'form-field-988a15')
     * @return array Array of allowed MIME types
     */
    private function get_allowed_file_types_from_settings($form_structure, $input_name)
    {
        // Default allowed file types (same as in form_file_upload)
        $default_types = array('image/jpeg', 'image/png', 'image/webp', 'image/gif', 'application/pdf', 'video/mp4', 'audio/mpeg');

        // If no form structure, return defaults
        if (empty($form_structure)) {
            return $default_types;
        }

        // Extract field ID from input name (e.g., 'form-field-988a15' -> '988a15')
        $field_id = str_replace('form-field-', '', $input_name);

        // Recursively search through form structure (including nested children) to find the field
        $field_settings = $this->find_field_settings_recursive($form_structure, $field_id);

        if ($field_settings && isset($field_settings['fileUploadAllowedTypes'])) {
            $types_string = $field_settings['fileUploadAllowedTypes'];

            // If types string is empty, return defaults
            if (empty($types_string)) {
                return $default_types;
            }

            $types = strtolower($types_string);
            $types = array_map(function ($type) {
                $type = trim($type);
                // Remove dots from extensions
                if (substr($type, 0, 1) === '.') {
                    $type = substr($type, 1);
                }

                // Convert extensions to MIME types (similar to form_file_upload logic)
                if (in_array($type, ['mp4', 'mov', 'avi', 'wmv'])) {
                    return 'video/' . $type;
                }
                if (in_array($type, ['mp3', 'wav', 'ogg'])) {
                    return 'audio/' . $type;
                }
                if (in_array($type, ['pdf', 'zip'])) {
                    return 'application/' . $type;
                }
                if (in_array($type, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
                    return 'image/' . $type;
                }

                return $type;
            }, explode(',', $types));

            // Expand official MIME types to include their real detected equivalents
            // finfo_file() often detects exotic files as octet-stream or text/plain
            $types = $this->expand_mime_types_for_detection($types);

            return array_unique($types);
        }

        // If we didn't find the specific field or it has no allowed types, return defaults
        return $default_types;
    }

    /**
     * Recursively search through form structure to find field settings by field_id
     * This handles nested children in steps, blocks, conditional-wrappers, etc.
     *
     * @param array $elements Array of form elements
     * @param string $field_id The field ID to search for
     * @return array|null The field settings if found, null otherwise
     */
    private function find_field_settings_recursive($elements, $field_id)
    {
        foreach ($elements as $element) {
            // Check if this element matches the field_id
            if (isset($element['field_id']) && $element['field_id'] === $field_id) {
                return isset($element['settings']) ? $element['settings'] : null;
            }

            // Recursively search through children
            if (isset($element['children']) && !empty($element['children'])) {
                $found = $this->find_field_settings_recursive($element['children'], $field_id);
                if ($found !== null) {
                    return $found;
                }
            }
        }

        return null;
    }

    /**
     * Expand official MIME types to include their real detected equivalents
     * finfo_file() detects many exotic file types as application/octet-stream or text/plain
     *
     * @param array $types Original MIME types from settings
     * @return array Expanded MIME types including detected equivalents
     */
    private function expand_mime_types_for_detection($types)
    {
        // Mapping of official MIME types to their real detected equivalents
        // finfo_file() often returns generic types like octet-stream or text/plain
        $mime_expansions = [
            // ===================
            // CAD/3D FILES
            // ===================
            'model/stl' => ['application/octet-stream', 'text/plain', 'application/sla', 'application/vnd.ms-pki.stl'],
            'model/obj' => ['text/plain', 'application/octet-stream'],
            'model/step' => ['text/plain', 'application/octet-stream'],
            'application/step' => ['text/plain', 'application/octet-stream'],
            'model/iges' => ['text/plain', 'application/octet-stream'],
            'application/iges' => ['text/plain', 'application/octet-stream'],
            'image/vnd.dwg' => ['application/octet-stream', 'application/acad', 'application/x-dwg'],
            'image/vnd.dxf' => ['text/plain', 'application/octet-stream', 'application/dxf'],
            'model/vnd.dwf' => ['application/octet-stream', 'application/x-dwf'],
            'model/gltf+json' => ['application/json', 'text/plain'],
            'model/gltf-binary' => ['application/octet-stream'],
            'model/3mf' => ['application/octet-stream', 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml'],
            'application/acad' => ['application/octet-stream'],
            'application/x-dwg' => ['application/octet-stream'],
            'application/dxf' => ['text/plain', 'application/octet-stream'],
            'application/x-dxf' => ['text/plain', 'application/octet-stream'],

            // ===================
            // MICROSOFT OFFICE
            // ===================
            // Modern Office formats (OOXML) are ZIP-based
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ['application/zip', 'application/octet-stream'], // .docx
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['application/zip', 'application/octet-stream'], // .xlsx
            'application/vnd.openxmlformats-officedocument.presentationml.presentation' => ['application/zip', 'application/octet-stream'], // .pptx
            // Legacy Office formats
            'application/msword' => ['application/octet-stream'], // .doc
            'application/vnd.ms-excel' => ['application/octet-stream'], // .xls
            'application/vnd.ms-powerpoint' => ['application/octet-stream'], // .ppt

            // ===================
            // ARCHIVES
            // ===================
            'application/x-rar-compressed' => ['application/octet-stream', 'application/vnd.rar'],
            'application/vnd.rar' => ['application/octet-stream', 'application/x-rar-compressed'],
            'application/x-7z-compressed' => ['application/octet-stream'],
            'application/x-tar' => ['application/octet-stream'],
            'application/gzip' => ['application/octet-stream', 'application/x-gzip'],
            'application/x-bzip2' => ['application/octet-stream'],

            // ===================
            // EBOOKS
            // ===================
            'application/epub+zip' => ['application/zip', 'application/octet-stream'],
            'application/x-mobipocket-ebook' => ['application/octet-stream'], // .mobi

            // ===================
            // VECTOR GRAPHICS
            // ===================
            'image/svg+xml' => ['text/plain', 'text/xml', 'application/xml', 'text/html'],
            'application/postscript' => ['application/octet-stream', 'text/plain'], // .eps, .ai (some)
            'application/illustrator' => ['application/pdf', 'application/octet-stream'], // .ai

            // ===================
            // DATA/TEXT FORMATS
            // ===================
            'text/csv' => ['text/plain', 'application/csv'],
            'application/json' => ['text/plain'],
            'application/xml' => ['text/plain', 'text/xml'],
            'text/xml' => ['text/plain', 'application/xml'],
            'application/javascript' => ['text/plain', 'text/javascript'],
            'text/javascript' => ['text/plain', 'application/javascript'],
            'text/markdown' => ['text/plain'],
            'application/x-yaml' => ['text/plain'],
            'text/yaml' => ['text/plain'],

            // ===================
            // FONTS
            // ===================
            'font/ttf' => ['application/octet-stream', 'application/x-font-ttf'],
            'font/otf' => ['application/octet-stream', 'application/x-font-otf'],
            'font/woff' => ['application/octet-stream', 'application/font-woff'],
            'font/woff2' => ['application/octet-stream', 'application/font-woff2'],
            'application/x-font-ttf' => ['application/octet-stream', 'font/ttf'],
            'application/x-font-otf' => ['application/octet-stream', 'font/otf'],

            // ===================
            // EXECUTABLES/INSTALLERS (if intentionally allowed)
            // ===================
            'application/x-msdownload' => ['application/octet-stream'], // .exe, .dll
            'application/x-msi' => ['application/octet-stream'], // .msi
            'application/vnd.apple.installer+xml' => ['application/octet-stream', 'application/xml'], // .pkg
            'application/x-apple-diskimage' => ['application/octet-stream'], // .dmg
        ];

        $expanded = $types;

        foreach ($types as $type) {
            $type_lower = strtolower(trim($type));
            if (isset($mime_expansions[$type_lower])) {
                $expanded = array_merge($expanded, $mime_expansions[$type_lower]);
            }
        }

        return array_unique($expanded);
    }

    /**
     * Get expected MIME type from file extension
     *
     * @param string $filename
     * @return string|null
     */
    private function get_mime_type_from_extension($filename)
    {
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

        $mime_types = array(
            'jpg' => 'image/jpeg',
            'jpeg' => 'image/jpeg',
            'png' => 'image/png',
            'gif' => 'image/gif',
            'webp' => 'image/webp',
            'pdf' => 'application/pdf',
            'mp4' => 'video/mp4',
            'mov' => 'video/quicktime',
            'avi' => 'video/x-msvideo',
            'wmv' => 'video/x-ms-wmv',
            'mp3' => 'audio/mpeg',
            'wav' => 'audio/wav',
            'ogg' => 'audio/ogg',
            'zip' => 'application/zip',
        );

        return isset($mime_types[$extension]) ? $mime_types[$extension] : null;
    }
}
