<?php

namespace Bricksforge;

use Bricksforge\Api\Utils as ApiUtils;

if (!defined('ABSPATH')) {
    exit;
}

class ExternalApiLoops
{
    /**
     * API Items
     *
     * @var array
     */
    public $items = [];

    /**
     * Active Items
     *
     * @var array
     */
    public $active_items = [];

    /**
     * Static cache for API responses across different lifecycles/instances.
     *
     * @var array
     */
    private static $cached_responses = [];

    /**
     * Utils instance.
     *
     * @var ApiUtils
     */
    private $utils;

    /**
     * Simple Array Placeholder
     *
     * @var string
     */
    private $simple_array_placeholder = 'item';

    /**
     * Not found marker
     *
     * @var string
     */
    private $not_found_marker = '::brf_not_found::';

    /**
     * Real loop data
     *
     * @var array
     */
    private $real_loop_data = [];

    /**
     * Get a property from an item (handles both array and object formats)
     *
     * @param mixed $item The item (array or object)
     * @param string $property The property name
     * @param mixed $default Default value if not found
     * @return mixed The property value or default
     */
    private function get_item_property($item, $property, $default = null)
    {
        if (is_array($item)) {
            return isset($item[$property]) ? $item[$property] : $default;
        } elseif (is_object($item)) {
            return isset($item->$property) ? $item->$property : $default;
        }
        return $default;
    }

    public function __construct($skip_init = false)
    {
        $this->utils = new ApiUtils();

        if (!$skip_init) {
            $this->init();
        }
    }

    /**
     * Initialize the class if the tool is activated.
     */
    public function init()
    {

        if ($this->activated() === false) {
            return;
        }

        if (!$this->allowed()) {
            return;
        }

        $this->get_items();
        $this->collect_active_items();

        // Load all API responses in a centralized way.
        // This ensures that the responses are available in all later filters.
        // Deactivated as this would create requests in every page.
        // $this->load_all_api_responses();

        $this->setup_filters();

        $this->remove_unneeded_elements();
    }

    private function allowed()
    {
        if (is_admin() && !bricks_is_builder() && !bricks_is_builder_call()) {
            return false;
        }

        if (defined('DOING_AJAX') && DOING_AJAX && !bricks_is_builder() && !bricks_is_builder_call()) {
            return false;
        }

        if (isset($_REQUEST['q']) && $_REQUEST['q'] === 'favicon.ico') {
            return false;
        }

        if (isset($_POST['action']) && in_array($_POST['action'], ['heartbeat', 'bricks_save_post'], true)) {
            return false;
        }

        if (isset($_REQUEST['_key'])) {
            return false;
        }

        if (
            strpos($_SERVER['REQUEST_URI'], 'wpcron') !== false ||
            strpos($_SERVER['REQUEST_URI'], 'wp-admin') !== false ||
            strpos($_SERVER['REQUEST_URI'], '/wp-json/') !== false
        ) {
            return false;
        }

        return true;
    }

    /**
     * Get items from the WordPress option.
     */
    public function get_items()
    {
        $items = get_option('brf_external_api_loops');

        // Deep convert arrays to objects for consistency with rest of codebase
        // Using json_decode(json_encode()) ensures all nested arrays become objects
        if (is_array($items)) {
            $this->items = json_decode(json_encode($items));
        } else {
            $this->items = $items;
        }
    }

    /**
     * Collect active items from the retrieved items.
     */
    public function collect_active_items()
    {
        if (empty($this->items)) {
            return;
        }

        foreach ($this->items as $item) {
            // Handle both array and object formats
            $status = is_array($item) ? (isset($item['status']) ? $item['status'] : null) : (isset($item->status) ? $item->status : null);

            // Check if item status is set to 'active'
            $active = ($status === 'active');
            if (!$active) {
                continue;
            }

            $needed = $this->is_needed($item);

            if (!$needed) {
                continue;
            }

            // Convert array to object for consistency with rest of codebase
            if (is_array($item)) {
                $item = (object) $item;
            }

            $this->active_items[] = $item;
        }
    }

    /**
     * Check if the tool is activated.
     *
     * @return bool
     */
    public function activated()
    {
        $activated_tools = get_option('brf_activated_tools');
        return $activated_tools && in_array(18, $activated_tools);
    }

    /**
     * Check if the item is needed.
     *
     * @param object|array $item
     * @return bool
     */
    private function is_needed($item)
    {
        $conditions = $this->get_item_property($item, 'conditions', []);

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

        // Handle both array and object formats for conditions
        if (is_array($conditions)) {
            $conditions = (object) $conditions;
        }

        // Get current post ID
        $current_id = get_queried_object_id();

        if ($current_id == 0) {
            return false;
        }

        // Get Bricks active template
        $bricks_active_template = isset(\Bricks\Database::$active_templates['content']) ? \Bricks\Database::$active_templates['content'] : false;

        // Check loading conditions
        $load_on = isset($conditions->loadOn) ? $conditions->loadOn : 'everywhere';

        // Load everywhere except specific pages
        if ($load_on === 'everywhere') {
            $except_pages = isset($conditions->loadOnExceptPages) ? $conditions->loadOnExceptPages : [];

            if (!empty($except_pages) && in_array($current_id, $except_pages)) {
                return false;
            }

            return true;
        }

        // Load on specific pages
        if ($load_on === 'specificPages') {
            $choice_pages = isset($conditions->loadOnChoicePages) ? $conditions->loadOnChoicePages : [];

            if (!empty($choice_pages) && in_array($current_id, $choice_pages)) {
                return true;
            }

            return false;
        }

        // Load on specific Bricks templates
        if ($load_on === 'specificBricksTemplates') {
            $choice_templates = isset($conditions->loadOnChoiceBricksTemplates) ? $conditions->loadOnChoiceBricksTemplates : [];

            if ($bricks_active_template && !empty($choice_templates) && in_array($bricks_active_template, $choice_templates)) {
                return true;
            }

            return false;
        }

        // Load on specific post IDs
        if ($load_on === 'specificPostIds') {
            $custom_post_ids = isset($conditions->loadOnCustomPostIds) ? $conditions->loadOnCustomPostIds : '';

            if (empty($custom_post_ids)) {
                return false;
            }

            // Convert comma-separated string to array
            $post_ids = array_map('trim', explode(',', $custom_post_ids));

            if (in_array($current_id, $post_ids)) {
                return true;
            }

            return false;
        }

        return true;
    }

    /**
     * Setup WordPress filters.
     */
    public function setup_filters()
    {
        add_filter('bricks/setup/control_options', [$this, 'add_control_options']);
        add_filter('bricks/query/run', [$this, 'run_query'], 10, 2);
        add_filter('bricks/query/loop_object', [$this, 'modify_loop_object'], 10, 3);
        add_filter('bricks/query/result', [$this, 'modify_query_result'], 10, 2);
        add_filter('bricks/dynamic_tags_list', [$this, 'add_dynamic_tags_list']);
        add_filter('bricks/dynamic_data/render_content', [$this, 'render_tag'], 30, 3);
        add_filter('bricks/element/render_attributes', [$this, 'bricks_render_attributes'], 10, 3);
        add_filter('bricks/frontend/render_data', [$this, 'bricks_render_html'], 10, 3);
    }

    /**
     * Add control options for each active API item.
     *
     * @param array $control_options
     * @return array
     */
    public function add_control_options($control_options)
    {
        if (empty($this->active_items)) {
            return $control_options;
        }

        // Add control option for each active API item.
        foreach ($this->active_items as $item) {
            $label = $item->label ?? 'Untitled API';

            // Basic JSON
            $control_options['queryTypes']["brfExternalApiLoop-" . esc_attr($item->id)] = esc_html__($label, 'bricksforge');

            $selected_nested_arrays = isset($item->selectedNestedArrays) && is_array($item->selectedNestedArrays) ? $item->selectedNestedArrays : [];

            if (!empty($selected_nested_arrays)) {
                // Nested Arrays
                $json_response = $item->apiResponse ?? false;

                if (!$json_response) {
                    continue;
                }

                $json_response_nested_arrays  = $this->get_nested_arrays_from_json($json_response);

                foreach ($json_response_nested_arrays as $key => $value) {
                    // Only add control option if this nested array is selected
                    if (in_array($key, $selected_nested_arrays)) {
                        $control_options['queryTypes']["brfExternalApiLoop-" . esc_attr($item->id) . "::" . esc_attr($key)] = esc_html__($label . " - " . $this->to_human_readable($key), 'bricksforge');
                    }
                }
            }
        }

        return $control_options;
    }

    public function get_nested_arrays_from_json($json_response)
    {
        // If the response is a JSON string, decode it to an associative array.
        if (is_string($json_response)) {
            $json_response = json_decode($json_response, true);
        }

        $nested_arrays = [];
        $this->find_nested_arrays($json_response, $nested_arrays);

        return $nested_arrays;
    }

    /**
     * Recursively find all nested arrays in the data structure.
     * Numeric keys are excluded from the result.
     *
     * @param mixed $data The data to search through.
     * @param array &$result The array to store results in.
     */
    private function find_nested_arrays($data, &$result)
    {
        if (!is_array($data) && !is_object($data)) {
            return;
        }

        if (is_object($data)) {
            $data = get_object_vars($data);
        }

        foreach ($data as $key => $value) {
            if (is_array($value)) {
                if (!is_numeric($key)) {
                    // Merge arrays from different items instead of overwriting.
                    if (isset($result[$key]) && is_array($result[$key])) {
                        $result[$key] = array_merge_recursive($result[$key], $value);
                    } else {
                        $result[$key] = $value;
                    }
                }
                $this->find_nested_arrays($value, $result);
            } elseif (is_object($value)) {
                $this->find_nested_arrays(get_object_vars($value), $result);
            }
        }
    }

    /**
     * Run the query for the given query object.
     *
     * @param array  $results
     * @param object $query_object
     * @return array
     */
    public function run_query($results, $query_object)
    {
        // Get query type from query object.
        $object_type = $query_object->object_type ?? '';

        // If the query type does not start with our prefix, return results.
        if (strpos($object_type, 'brfExternalApiLoop-') !== 0) {
            return $results;
        }

        // If its something like brfExternalApiLoop-29255f7d-922b-1e4a-c76b-f23ada9aa54d::reviews, this is a nested array
        $is_nested_array = strpos($object_type, '::') !== false;

        // Extract the API item ID from the query type.
        $item_id = sanitize_text_field(str_replace('brfExternalApiLoop-', '', $object_type));

        // Use centralized cached response.
        if ($is_nested_array) {
            // We need to get the nested array key. We can split the object type by :: and take the last part.
            $nested_array_key = explode('::', $object_type);
            $nested_array_key = $nested_array_key[count($nested_array_key) - 1];

            // This takes the final count of the nested array.
            // WE NEED TO RETURN THE CORRECT COUNT HERE
            $response = $this->run_api_request($item_id, $nested_array_key);
            $response = apply_filters('bricksforge/api_query_builder/response', $response);
        } else {
            $response = $this->run_api_request($item_id);

            $response = apply_filters('bricksforge/api_query_builder/response', $response);
        }

        if (empty($response)) {
            return $results;
        }

        // If the decoded response is an array, return it as results.
        if (is_array($response)) {
            return $response;
        }

        return $results;
    }

    public function modify_loop_object($loop_object, $loop_key, $query_obj)
    {
        return $loop_object;
    }

    public function modify_query_result($result, $query_obj)
    {
        return $result;
    }

    /**
     * Get cached API response for a given item ID.
     *
     * @param mixed $item_id
     * @return mixed
     */
    public function get_cached_response($item_id, $force_request = false)
    {

        if (isset(self::$cached_responses[$item_id]) && !$force_request) {
            return self::$cached_responses[$item_id];
        }

        // If not cached yet, run the API request.
        return $this->run_api_request($item_id, null, $force_request);
    }

    static function clear_cache($item_id)
    {
        $item_id = sanitize_text_field($item_id);
        unset(self::$cached_responses[$item_id]);
        delete_transient('brf_api_' . $item_id);

        // Also clear stored dynamic tokens to force fresh token retrieval
        $current_items = get_option('brf_external_api_loops', []);
        foreach ($current_items as &$item) {
            // Handle both array and object formats
            $current_id = is_array($item) ? (isset($item['id']) ? $item['id'] : null) : (isset($item->id) ? $item->id : null);
            $auth = is_array($item) ? (isset($item['auth']) ? $item['auth'] : null) : (isset($item->auth) ? $item->auth : null);

            if ($current_id === $item_id && $auth !== null) {
                if (is_array($item)) {
                    unset($item['auth']['bearerToken']);
                    unset($item['auth']['bearerRefreshToken']);
                } else {
                    unset($item->auth->bearerToken);
                    unset($item->auth->bearerRefreshToken);
                }
                break;
            }
        }
        // CRITICAL: Write directly to DB to avoid cache issues
        // This prevents overwriting data that was just saved
        global $wpdb;
        wp_cache_delete('brf_external_api_loops', 'options');
        wp_cache_delete('alloptions', 'options');

        $serialized_value = maybe_serialize($current_items);
        $wpdb->query($wpdb->prepare(
            "UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s",
            $serialized_value,
            'brf_external_api_loops'
        ));

        wp_cache_delete('brf_external_api_loops', 'options');
        wp_cache_delete('alloptions', 'options');
    }

    /**
     * Run the API request for the given item ID.
     *
     * This method uses static caching and WordPress transients to reduce duplicate requests.
     *
     * @param mixed $item_id
     * @param mixed $nested_array_key
     * @param bool $force_request
     * @param array $loading_details
     * @param array $pagination_params Pagination parameters (page, offset, limit, etc.)
     * @return mixed
     */
    public function run_api_request($item_id = null, $nested_array_key = null, $force_request = false, $loading_details = [], $pagination_params = [])
    {
        $item_id = sanitize_text_field($item_id);
        $cache_item_id = $item_id;

        $is_nested_array = $nested_array_key !== null;
        $token = null;

        // CRITICAL: If force_request is true, clear all caches first
        if ($force_request) {
            // Clear static cache
            unset(self::$cached_responses[$cache_item_id]);

            // Clear transient cache
            delete_transient('brf_api_' . $cache_item_id);

            // Also clear nested array caches if nested_array_key is provided
            if ($nested_array_key) {
                delete_transient('brf_api_' . $cache_item_id . '_' . $nested_array_key);
                unset(self::$cached_responses[$cache_item_id . '_' . $nested_array_key]);
            }
        }

        if (isset(self::$cached_responses[$cache_item_id]) && !$force_request) {
            return self::$cached_responses[$cache_item_id];
        }

        $item = $this->get_item($item_id);

        if (empty($item)) {
            return;
        }

        // Check if this is a custom JSON source
        if (isset($item->sourceType) && $item->sourceType === 'custom_json') {
            return $this->handle_custom_json($item, $cache_item_id, $force_request, $is_nested_array, $nested_array_key);
        }

        // For API sources, URL is required
        if (empty($item->url)) {
            return;
        }

        $bearer_token = $item->auth->bearerToken ?? "";
        $bearer_refresh_token = $item->auth->bearerRefreshToken ?? "";
        $static_bearer_token = $item->auth->token ?? "";
        $dynamic_token_data = $item->auth->dynamicTokenData ?? [];

        $username = $item->auth->username ?? "";
        $password = $item->auth->password ?? "";

        $custom_auth_headers = $item->auth->customHeaders ?? [];

        $custom_headers = $item->customHeaders ?? [];
        $query_params = $item->queryParams ?? [];

        // If there are encrypted tokens, decrypt them.
        if ($this->utils->is_encrypted($bearer_token)) {
            $bearer_token = $this->utils->decrypt($bearer_token);
        }

        if ($this->utils->is_encrypted($bearer_refresh_token)) {
            $bearer_refresh_token = $this->utils->decrypt($bearer_refresh_token);
        }

        if ($this->utils->is_encrypted($static_bearer_token)) {
            $static_bearer_token = $this->utils->decrypt($static_bearer_token);
        }

        if (is_array($dynamic_token_data)) {
            foreach ($dynamic_token_data as &$d) {
                $d_key = is_array($d) ? (isset($d['key']) ? $d['key'] : '') : (isset($d->key) ? $d->key : '');
                $d_value = is_array($d) ? (isset($d['value']) ? $d['value'] : '') : (isset($d->value) ? $d->value : '');

                if (!empty($d_key) && $this->utils->is_encrypted($d_key)) {
                    $decrypted_key = $this->utils->decrypt($d_key);
                    if (is_array($d)) {
                        $d['key'] = $decrypted_key;
                    } else {
                        $d->key = $decrypted_key;
                    }
                }
                if (!empty($d_value) && $this->utils->is_encrypted($d_value)) {
                    $decrypted_value = $this->utils->decrypt($d_value);
                    if (is_array($d)) {
                        $d['value'] = $decrypted_value;
                    } else {
                        $d->value = $decrypted_value;
                    }
                }
            }
            unset($d); // Unset reference after loop
        }

        if ($this->utils->is_encrypted($username)) {
            $username = $this->utils->decrypt($username);
        }

        if ($this->utils->is_encrypted($password)) {
            $password = $this->utils->decrypt($password);
        }


        if (is_array($custom_auth_headers)) {
            foreach ($custom_auth_headers as &$h) {
                $h_key = is_array($h) ? (isset($h['key']) ? $h['key'] : '') : (isset($h->key) ? $h->key : '');
                $h_value = is_array($h) ? (isset($h['value']) ? $h['value'] : '') : (isset($h->value) ? $h->value : '');

                if (!empty($h_key) && $this->utils->is_encrypted($h_key)) {
                    $decrypted_key = $this->utils->decrypt($h_key);
                    if (is_array($h)) {
                        $h['key'] = $decrypted_key;
                    } else {
                        $h->key = $decrypted_key;
                    }
                }
                if (!empty($h_value) && $this->utils->is_encrypted($h_value)) {
                    $decrypted_value = $this->utils->decrypt($h_value);
                    if (is_array($h)) {
                        $h['value'] = $decrypted_value;
                    } else {
                        $h->value = $decrypted_value;
                    }
                }
            }
            unset($h); // Unset reference after loop
        }

        if (is_array($custom_headers)) {
            foreach ($custom_headers as &$h) {
                $h_key = is_array($h) ? (isset($h['key']) ? $h['key'] : '') : (isset($h->key) ? $h->key : '');
                $h_value = is_array($h) ? (isset($h['value']) ? $h['value'] : '') : (isset($h->value) ? $h->value : '');

                if (!empty($h_key) && $this->utils->is_encrypted($h_key)) {
                    $decrypted_key = $this->utils->decrypt($h_key);
                    if (is_array($h)) {
                        $h['key'] = $decrypted_key;
                    } else {
                        $h->key = $decrypted_key;
                    }
                }
                if (!empty($h_value) && $this->utils->is_encrypted($h_value)) {
                    $decrypted_value = $this->utils->decrypt($h_value);
                    if (is_array($h)) {
                        $h['value'] = $decrypted_value;
                    } else {
                        $h->value = $decrypted_value;
                    }
                }
            }
            unset($h); // Unset reference after loop
        }

        if (is_array($query_params)) {
            foreach ($query_params as &$q) {
                $q_key = is_array($q) ? (isset($q['key']) ? $q['key'] : '') : (isset($q->key) ? $q->key : '');
                $q_value = is_array($q) ? (isset($q['value']) ? $q['value'] : '') : (isset($q->value) ? $q->value : '');

                if (!empty($q_key) && $this->utils->is_encrypted($q_key)) {
                    $decrypted_key = $this->utils->decrypt($q_key);
                    if (is_array($q)) {
                        $q['key'] = $decrypted_key;
                    } else {
                        $q->key = $decrypted_key;
                    }
                }
                if (!empty($q_value) && $this->utils->is_encrypted($q_value)) {
                    $decrypted_value = $this->utils->decrypt($q_value);
                    if (is_array($q)) {
                        $q['value'] = $decrypted_value;
                    } else {
                        $q->value = $decrypted_value;
                    }
                }
            }
            unset($q); // Unset reference after loop
        }

        // Determine effective cache lifetime.
        $cache_lifetime = absint($item->cacheLifetime ?? 0);
        if ($cache_lifetime == 0) {
            if (bricks_is_builder() || bricks_is_builder_call()) {
                $effective_cache_time = 5; // fallback cache duration (in seconds)
            } else {
                $effective_cache_time = 1;
            }
        } else {
            $effective_cache_time = $cache_lifetime * HOUR_IN_SECONDS;
        }

        if ($force_request) {
            $effective_cache_time = 1;
        }

        if ($effective_cache_time > 0 && !$force_request) {
            // Include nested array key in cache key if present
            $cache_key = $cache_item_id;
            if ($is_nested_array && $nested_array_key) {
                $cache_key .= '_' . $nested_array_key;
            }

            $transient_key = 'brf_api_' . $cache_key;
            $cached_response = get_transient($transient_key);

            if ($cached_response !== false) {
                self::$cached_responses[$cache_key] = $cached_response;
                return $cached_response;
            }
        }

        // Build request headers.
        // Remove this header. We want the user to be responsible for headers
        $headers = [
            //'Content-Type' => 'application/json',
        ];

        $body = [];

        // Process authentication based on method
        if (isset($item->auth) && isset($item->auth->method)) {
            switch ($item->auth->method) {
                case 'bearer':
                    if (isset($item->auth->tokenType)) {
                        if ($item->auth->tokenType === 'static' && !empty($static_bearer_token)) {
                            $headers['Authorization'] = 'Bearer ' . sanitize_text_field($static_bearer_token);
                        } elseif ($item->auth->tokenType === 'dynamic') {
                            if (empty($bearer_token)) {
                                $token = $this->retrieve_dynamic_token($item);
                                if (!$token) {
                                    return [];
                                }

                                if (!isset($item->auth->sendTokenAs)) {
                                    $item->auth->sendTokenAs = 'headers';
                                }

                                $token_key = $item->auth->accessTokenKeyFromResult ?? 'accessToken';

                                if ($item->auth->sendTokenAs === 'headers') {
                                    $headers['Authorization'] = 'Bearer ' . sanitize_text_field($token);
                                }

                                $encrypted_bearer_token = $this->utils->is_encrypted($token) ? $token : $this->utils->encrypt($token);
                                $item->auth->bearerToken = $encrypted_bearer_token;

                                // Persist the new token in the option.
                                $current_items = get_option('brf_external_api_loops', []);

                                $bearer_refresh_token = $item->auth->bearerRefreshToken ?? "";

                                $this->update_tokens($item_id, $encrypted_bearer_token, $bearer_refresh_token);
                            } else {
                                $headers['Authorization'] = 'Bearer ' . sanitize_text_field($bearer_token);
                            }
                        }
                    }
                    break;

                case 'basic':
                    if (!empty($username)) {
                        $base64_credentials = base64_encode($username . ':' . $password);
                        $headers['Authorization'] = 'Basic ' . $base64_credentials;
                    }
                    break;

                case 'custom_headers':
                    if (isset($item->auth->customHeaders) && is_array($item->auth->customHeaders)) {
                        foreach ($item->auth->customHeaders as $header) {
                            $header_key = is_array($header) ? (isset($header['key']) ? $header['key'] : '') : (isset($header->key) ? $header->key : '');
                            $header_value = is_array($header) ? (isset($header['value']) ? $header['value'] : '') : (isset($header->value) ? $header->value : '');
                            if (!empty($header_key) && !empty($header_value)) {
                                $headers[$header_key] = sanitize_text_field($header_value);
                            }
                        }
                    }
                    break;
            }
        }

        // Add custom headers to the request
        if (isset($item->customHeaders) && is_array($item->customHeaders)) {
            foreach ($item->customHeaders as $header) {
                $header_key = is_array($header) ? (isset($header['key']) ? $header['key'] : '') : (isset($header->key) ? $header->key : '');
                $header_value = is_array($header) ? (isset($header['value']) ? $header['value'] : '') : (isset($header->value) ? $header->value : '');
                if (!empty($header_key) && !empty($header_value)) {
                    $headers[$header_key] = sanitize_text_field($header_value);
                }
            }
        }

        $cookies = [];

        if (isset($item->auth) && isset($item->auth->useWpNonce) && $item->auth->useWpNonce) {
            // We need to send the cookies to the API. Otherwise, the nonce will not be valid.
            foreach ($_COOKIE as $key => $value) {
                $cookies[] = new \WP_Http_Cookie([
                    'name'  => $key,
                    'value' => $value,
                ]);
            }

            $headers['X-WP-Nonce'] = wp_create_nonce('wp_rest');
        }

        $api_url = $this->handle_dynamic_routes($item);
        $api_url = $this->handle_dynamic_data_tags($api_url);

        if (isset($item->auth) && isset($item->auth->tokenType) && $item->auth->tokenType === 'dynamic' && isset($item->auth->sendTokenAs)) {
            $token_key = $item->auth->accessTokenKeyFromResult ?? 'accessToken';
            if ($token) {
                if ($item->auth->sendTokenAs === 'queryParam') {
                    $api_url = add_query_arg($token_key, $token, $api_url);
                } else if ($item->auth->sendTokenAs === 'body') {
                    $body[$token_key] = $token;
                }
            }
        }

        // If the url still contains dynamic routes, return an empty array.
        if ($this->contains_dynamic_routes($api_url)) {
            return [];
        }

        // Add query params to the URL
        if (isset($query_params) && is_array($query_params)) {
            $filtered_query_params = [];
            foreach ($query_params as $param) {
                // Handle both array and object formats
                $param_key = is_array($param) ? (isset($param['key']) ? $param['key'] : '') : (isset($param->key) ? $param->key : '');
                $param_value = is_array($param) ? (isset($param['value']) ? $param['value'] : '') : (isset($param->value) ? $param->value : '');

                // We parse dynamic data tags in the query params
                $param_key = bricks_render_dynamic_data($param_key);
                $param_value = bricks_render_dynamic_data($param_value);

                // Ensure both key and value are sanitized and added to the filtered array
                if (!empty($param_key)) {
                    $filtered_query_params[sanitize_text_field($param_key)] = sanitize_text_field($param_value);
                }
            }
            // Append the filtered query parameters to the URL
            $api_url = add_query_arg($filtered_query_params, $api_url);
        }

        // TODO: Infinite and pagination loading types
        // We need to find a way to handle the loading types in the API request.
        // If we have loading details, we add them to the URL
        if (isset($loading_details) && is_array($loading_details) && !empty($loading_details)) {
            foreach ($loading_details as $key => $value) {
                $api_url = add_query_arg($key, $value, $api_url);
            }

            // If item loading type is infinite or pagination, we need to add the loading details to the URL
            if (isset($item->loadingType) && (($item->loadingType == 'infinite' || $item->loadingType == 'pagination')) && isset($item->limitKey)) {
                $items_to_load = $item->itemsToLoad ?? 10;
                $api_url = add_query_arg($item->limitKey, $items_to_load, $api_url);
            }
        }

        // Add pagination parameters to URL if provided
        if (isset($pagination_params) && is_array($pagination_params) && !empty($pagination_params)) {
            foreach ($pagination_params as $key => $value) {
                if ($value !== null && $value !== '') {
                    $api_url = add_query_arg(sanitize_text_field($key), sanitize_text_field($value), $api_url);
                }
            }
        }

        // We add some filters, for example for modifying the headers
        $headers = apply_filters('bricksforge/api_query_builder/request_headers', $headers);
        $api_url = apply_filters('bricksforge/api_query_builder/request_url', $api_url);

        // Prepare request arguments
        $request_method = isset($item->requestMethod) ? strtoupper($item->requestMethod) : 'GET';
        $request_args = [
            'method'  => $request_method,
            'headers' => $headers,
            'cookies' => $cookies,
        ];

        // Handle POST request body
        if ($request_method === 'POST' && !empty($item->requestBody)) {
            $body_data = $this->build_request_body($item->requestBody);
            $body_type = isset($item->requestBodyType) ? $item->requestBodyType : 'json';

            if ($body_type === 'json') {
                $request_args['body'] = wp_json_encode($body_data);
                $request_args['headers']['Content-Type'] = 'application/json';
            } else {
                // Form data - wp_remote_request handles encoding automatically
                $request_args['body'] = $body_data;
                $request_args['headers']['Content-Type'] = 'application/x-www-form-urlencoded';
            }
        }

        // Apply filter for request body
        $request_args = apply_filters('bricksforge/api_query_builder/request_args', $request_args, $item);

        $response = wp_remote_request(esc_url_raw($api_url), $request_args);

        // If a 401 is received and we're using a dynamic token, try to refresh token.
        if (
            !is_wp_error($response) &&
            wp_remote_retrieve_response_code($response) == 401 &&
            isset($item->auth) &&
            $item->auth->method === 'bearer' &&
            $item->auth->tokenType === 'dynamic'
        ) {
            if (!empty($item->auth->refreshTokenEndpoint)) {
                $newToken = $this->refresh_dynamic_token($item);
            } else {
                unset($item->auth->bearerToken);
                $newToken = $this->retrieve_dynamic_token($item);
            }

            if ($newToken) {

                $headers['Authorization'] = 'Bearer ' . sanitize_text_field($newToken);

                $encrypted_bearer_token = $this->utils->is_encrypted($newToken) ? $newToken : $this->utils->encrypt($newToken);
                $item->auth->bearerToken = $encrypted_bearer_token;

                // Persist the updated token.
                $current_items = get_option('brf_external_api_loops', []);

                $bearer_refresh_token = $item->auth->bearerRefreshToken ?? "";

                $this->update_tokens($item_id, $encrypted_bearer_token, $bearer_refresh_token);

                // Update headers in request_args and retry request
                $request_args['headers'] = $headers;
                $response = wp_remote_request(esc_url_raw($item->url), $request_args);
            } else {
                error_log("Bricksforge: Dynamic token refresh failed for item id: {$item_id}");
            }
        }

        // If we dont have status code 200, we return the error
        if (wp_remote_retrieve_response_code($response) !== 200 && $force_request) {
            $code = wp_remote_retrieve_response_code($response);
            $body = wp_remote_retrieve_body($response);
            switch ($code) {
                case 401:
                    $error_message = "401: The API request failed because of authentication error.";
                    break;
                case 404:
                    $error_message = "404: The API request failed because the resource was not found.";
                    break;
                case 429:
                    $error_message = "429: The API request failed because of too many requests.";
                    break;
                case 500:
                    $error_message = "500: The API request failed because of an internal server error.";
                    break;
                default:
                    $error_message = $body;
            }

            error_log("Bricksforge: API request failed for item id: {$item_id}: " . $body);
            return new \WP_Error($code, $error_message);
        }

        if (is_wp_error($response)) {
            error_log("Bricksforge: API request error for item id: {$item_id}: " . $response->get_error_message());

            if ($force_request) {
                return $response->get_error_message();
            }

            return [];
        }

        $body = wp_remote_retrieve_body($response);

        $headers_response = wp_remote_retrieve_headers($response);
        $content_type = isset($headers_response['content-type']) ? $headers_response['content-type'] : '';

        $decoded_response = $this->decode_api_response($body, $content_type, $force_request);

        if (isset($item->rootPath) && trim($item->rootPath) !== '') {
            $decoded_response = $this->get_start_position_from_root_path($decoded_response, sanitize_text_field($item->rootPath));
        }

        if (is_array($decoded_response) && !empty($decoded_response) && !array_key_exists(0, $decoded_response) && !$force_request) {
            $decoded_response = array($decoded_response);
        }

        if ($is_nested_array && $nested_array_key) {
            // We need to return the nested array key.
            // TODO: Ich denke hier ist der Fehler!
            $decoded_response = $this->find_nested_array_key($decoded_response, $nested_array_key);
        }

        if ($this->is_google_sheet($api_url)) {
            $decoded_response = $this->maybe_array_of_arrays($decoded_response);
        }

        if ($effective_cache_time > 0 && !$force_request) {
            // Include nested array key in cache key if present
            $cache_key = $cache_item_id;
            if ($is_nested_array && $nested_array_key) {
                $cache_key .= '_' . $nested_array_key;
            }

            $transient_key = 'brf_api_' . $cache_key;
            $cached_response = get_transient($transient_key);

            if ($cached_response !== false) {
                self::$cached_responses[$cache_key] = $cached_response;

                // apply_filters('bricksforge/api_query_builder/response', $cached_response, $item);
                $cached_response = apply_filters('bricksforge/api_query_builder/response', $cached_response);

                return $cached_response;
            }
        }

        if ($effective_cache_time > 0 && !$force_request) {
            self::$cached_responses[$cache_key] = $decoded_response;
            set_transient($transient_key, $decoded_response, $effective_cache_time);
        }

        // apply_filters('bricksforge/api_query_builder/response', $decoded_response, $item);
        $decoded_response = apply_filters('bricksforge/api_query_builder/response', $decoded_response);

        return $decoded_response;
    }

    private function is_google_sheet($api_url)
    {
        return strpos($api_url, 'sheets.googleapis') !== false;
    }

    private function maybe_array_of_arrays($response)
    {

        // If the response is not an array or is empty, just return it.
        if (!is_array($response) || empty($response)) {
            return $response;
        }

        // If the first element isn't an array, assume the response is already in the desired format.
        if (!isset($response[0]) || !is_array($response[0])) {
            return $response;
        }

        // Use the first row as headers. Normalize keys (e.g., to lowercase).
        $headers = array_map('strtolower', $response[0]);

        $result = [];
        // Process each row after the header row.
        for ($i = 1; $i < count($response); $i++) {
            // Make sure the current row is an array.
            if (!is_array($response[$i])) {
                continue;
            }
            $row = $response[$i];
            $assoc = [];
            // Combine headers with row values. If a value is missing, default to null.
            foreach ($headers as $index => $header) {
                $assoc[$header] = isset($row[$index]) ? $row[$index] : null;
            }
            // Cast to an object (stdClass). If you prefer an associative array, simply push $assoc.
            $result[] = (object)$assoc;
        }

        return $result;
    }

    private function find_nested_array_key($response, $key)
    {

        // Handle non-array/object inputs
        if (!is_array($response) && !is_object($response)) {
            return [];
        }

        // Direct match at current level
        if (isset($response[$key])) {
            return $response[$key];
        }

        $loop_index = \Bricks\Query::get_query_object();

        // Recursively search through all array/object values
        // TODO: Hier müssen wir ansetzen! Aktuell nehmen wir das größte Ergebnis. 
        // Das sorgt aber dafür, dass es bei kleineren Arrays irgendwie Dopplungen gibt. Das müssen wir verhindern.
        // Vielleicht für jeden Index die Anzahl speichern und später abrufen?

        $highest_count = 0;
        $best_result = [];
        $results = [];

        foreach ($response as $value) {
            if (is_array($value) || is_object($value)) {
                $result = $this->find_nested_array_key($value, $key);

                $results[] = $result;

                if (!empty($result)) {
                    $count = count($result);
                    if ($count > $highest_count) {
                        $highest_count = $count;
                        $best_result = $result;
                    }
                }
            }
        }

        return $best_result;
    }

    /**
     * Decodes an API response based on its content type.
     *
     * @param string $body The response body.
     * @param string $content_type The content type header.
     * @return mixed The decoded response, or an empty array on failure.
     */
    private function decode_api_response($body, $content_type = '', $force_request = false)
    {
        // If the response is CSV, parse it accordingly.
        if (!empty($content_type) && strpos($content_type, 'text/csv') !== false) {
            return $this->parseCSVFromString($body);
        }

        // If the response is XML, parse it accordingly.
        if (!empty($content_type) && strpos($content_type, 'text/xml') !== false) {
            return $this->parseXMLFromString($body);
        }

        // Otherwise, assume JSON.
        $decoded = json_decode($body, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            error_log("JSON decoding error: " . json_last_error_msg());

            if ($force_request) {
                return "The API response is not valid JSON.";
            }

            return [];
        }
        return $decoded;
    }

    private function update_tokens($item_id, $encrypted_bearer_token, $bearer_refresh_token)
    {
        $current_items = get_option('brf_external_api_loops', []);

        foreach ($current_items as &$item) {
            // Handle both array and object formats
            $current_id = is_array($item) ? (isset($item['id']) ? $item['id'] : null) : (isset($item->id) ? $item->id : null);
            $auth = is_array($item) ? (isset($item['auth']) ? $item['auth'] : null) : (isset($item->auth) ? $item->auth : null);

            if ($current_id === $item_id && $auth !== null) {
                if (is_array($item)) {
                    if (!isset($item['auth'])) {
                        $item['auth'] = [];
                    }
                    $item['auth']['bearerToken'] = $encrypted_bearer_token;
                    $item['auth']['bearerRefreshToken'] = $bearer_refresh_token;
                } else {
                    if (!isset($item->auth)) {
                        $item->auth = new \stdClass();
                    }
                    $item->auth->bearerToken = $encrypted_bearer_token;
                    $item->auth->bearerRefreshToken = $bearer_refresh_token;
                }
                break;
            }
        }

        // CRITICAL: Write directly to DB to avoid cache issues
        // This prevents overwriting data that was just saved
        global $wpdb;
        wp_cache_delete('brf_external_api_loops', 'options');
        wp_cache_delete('alloptions', 'options');

        $serialized_value = maybe_serialize($current_items);
        $wpdb->query($wpdb->prepare(
            "UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s",
            $serialized_value,
            'brf_external_api_loops'
        ));

        wp_cache_delete('brf_external_api_loops', 'options');
        wp_cache_delete('alloptions', 'options');
    }

    /**
     * Build request body from key-value pairs array
     *
     * @param array $request_body Array of key-value pair objects
     * @return array Associative array of body parameters
     */
    private function build_request_body($request_body)
    {
        $body_data = [];

        if (!is_array($request_body)) {
            return $body_data;
        }

        foreach ($request_body as $param) {
            // Handle both object and array formats
            if (is_object($param)) {
                $key = isset($param->key) ? sanitize_text_field($param->key) : '';
                $value = isset($param->value) ? $param->value : '';
            } else {
                $key = isset($param['key']) ? sanitize_text_field($param['key']) : '';
                $value = isset($param['value']) ? $param['value'] : '';
            }

            if (!empty($key)) {
                $body_data[$key] = $value;
            }
        }

        return $body_data;
    }

    /**
     * Handle custom JSON data source
     *
     * @param object $item
     * @param string $cache_item_id
     * @param bool $force_request
     * @param bool $is_nested_array
     * @param string|null $nested_array_key
     * @return mixed
     */
    private function handle_custom_json($item, $cache_item_id, $force_request, $is_nested_array, $nested_array_key)
    {
        // CRITICAL: If force_request is true, clear all caches first
        if ($force_request) {
            // Clear static cache
            unset(self::$cached_responses[$cache_item_id]);

            // Clear transient cache
            delete_transient('brf_api_' . $cache_item_id);

            // Also clear nested array caches if nested_array_key is provided
            if ($nested_array_key) {
                delete_transient('brf_api_' . $cache_item_id . '_' . $nested_array_key);
                unset(self::$cached_responses[$cache_item_id . '_' . $nested_array_key]);
            }
        }

        // Check cache first
        if (isset(self::$cached_responses[$cache_item_id]) && !$force_request) {
            return self::$cached_responses[$cache_item_id];
        }

        // Determine effective cache lifetime
        $cache_lifetime = absint($item->cacheLifetime ?? 0);
        if ($cache_lifetime == 0) {
            if (bricks_is_builder() || bricks_is_builder_call()) {
                $effective_cache_time = 5; // fallback cache duration (in seconds)
            } else {
                $effective_cache_time = 1;
            }
        } else {
            $effective_cache_time = $cache_lifetime * HOUR_IN_SECONDS;
        }

        if ($force_request) {
            $effective_cache_time = 1;
        }

        if ($effective_cache_time > 0 && !$force_request) {
            $cache_key = $cache_item_id;
            if ($is_nested_array && $nested_array_key) {
                $cache_key .= '_' . $nested_array_key;
            }
            $transient_key = 'brf_api_' . $cache_key;
            $cached_response = get_transient($transient_key);
            if ($cached_response !== false) {
                self::$cached_responses[$cache_key] = $cached_response;
                return $cached_response;
            }
        }

        // Get and parse custom JSON
        $custom_json = $item->customJson ?? '';

        if (empty($custom_json)) {
            if ($force_request) {
                return new \WP_Error('empty_json', 'No custom JSON provided');
            }
            return [];
        }

        $decoded_response = json_decode($custom_json, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            $error_message = 'Invalid JSON: ' . json_last_error_msg();
            if ($force_request) {
                return new \WP_Error('invalid_json', $error_message);
            }
            error_log("Bricksforge: " . $error_message);
            return [];
        }

        // Apply root path if set
        if (isset($item->rootPath) && trim($item->rootPath) !== '') {
            $decoded_response = $this->get_start_position_from_root_path($decoded_response, sanitize_text_field($item->rootPath));
        }

        // Ensure response is array
        if (is_array($decoded_response) && !empty($decoded_response) && !array_key_exists(0, $decoded_response) && !$force_request) {
            $decoded_response = array($decoded_response);
        }

        // Handle nested arrays
        if ($is_nested_array && $nested_array_key) {
            $decoded_response = $this->find_nested_array_key($decoded_response, $nested_array_key);
        }

        // Cache the response
        if ($effective_cache_time > 0 && !$force_request) {
            $cache_key = $cache_item_id;
            if ($is_nested_array && $nested_array_key) {
                $cache_key .= '_' . $nested_array_key;
            }
            self::$cached_responses[$cache_key] = $decoded_response;
            set_transient('brf_api_' . $cache_key, $decoded_response, $effective_cache_time);
        }

        $decoded_response = apply_filters('bricksforge/api_query_builder/response', $decoded_response);

        return $decoded_response;
    }

    /**
     * Retrieve a dynamic bearer token using the token endpoint.
     *
     * @param object $item
     * @return string|false The token string on success, or false on failure.
     */
    private function retrieve_dynamic_token($item)
    {
        if (empty($item->auth) || empty($item->auth->tokenEndpoint) || !filter_var($item->auth->tokenEndpoint, FILTER_VALIDATE_URL)) {
            return false;
        }

        $dynamicType = $item->auth->dynamicTokenType ?? 'headers';
        $args = [];

        if ($dynamicType === 'headers') {
            $headers = [];
            if (!empty($item->auth->dynamicTokenData) && is_array($item->auth->dynamicTokenData)) {
                foreach ($item->auth->dynamicTokenData as $datum) {
                    $datum_key = is_array($datum) ? (isset($datum['key']) ? $datum['key'] : '') : (isset($datum->key) ? $datum->key : '');
                    $datum_value = is_array($datum) ? (isset($datum['value']) ? $datum['value'] : '') : (isset($datum->value) ? $datum->value : '');
                    if (!empty($datum_key) && !empty($datum_value)) {
                        $headers[sanitize_text_field($datum_key)] = sanitize_text_field($datum_value);
                    }
                }
            }
            $args['headers'] = $headers;
        } elseif ($dynamicType === 'body') {
            $body = [];
            if (!empty($item->auth->dynamicTokenData) && is_array($item->auth->dynamicTokenData)) {
                foreach ($item->auth->dynamicTokenData as $datum) {
                    $datum_key = is_array($datum) ? (isset($datum['key']) ? $datum['key'] : '') : (isset($datum->key) ? $datum->key : '');
                    $datum_value = is_array($datum) ? (isset($datum['value']) ? $datum['value'] : '') : (isset($datum->value) ? $datum->value : '');
                    if (!empty($datum_key) && !empty($datum_value)) {
                        $body[sanitize_text_field($datum_key)] = sanitize_text_field($datum_value);
                    }
                }
            }
            $args['body'] = $body;
        } else {
            return false;
        }

        // In any case, we want to add the item headers to the args.
        // Add headers if they exist and are not empty
        if (isset($item->customHeaders) && !empty($item->customHeaders)) {
            // Convert header objects to associative array
            $custom_headers = [];
            foreach ($item->customHeaders as $header) {
                $header_key = is_array($header) ? (isset($header['key']) ? $header['key'] : '') : (isset($header->key) ? $header->key : '');
                $header_value = is_array($header) ? (isset($header['value']) ? $header['value'] : '') : (isset($header->value) ? $header->value : '');
                if (!empty($header_key) && !empty($header_value)) {
                    $custom_headers[sanitize_text_field($header_key)] = sanitize_text_field($header_value);
                }
            }

            // Merge with existing headers if any
            $args['headers'] = isset($args['headers']) ? array_merge($args['headers'], $custom_headers) : $custom_headers;
        }

        // Apply filters for request headers and body
        $args['headers'] = apply_filters('bricksforge/api_query_builder/retrieve_token_request_headers', $args['headers'] ?? [], $item);
        $args['body'] = apply_filters('bricksforge/api_query_builder/retrieve_token_request_body', $args['body'] ?? [], $item);

        $tokenResponse = wp_remote_post(esc_url_raw($item->auth->tokenEndpoint), $args);

        if (is_wp_error($tokenResponse)) {
            return false;
        }

        $responseBody = wp_remote_retrieve_body($tokenResponse);
        $decoded = json_decode($responseBody, true);

        if (json_last_error() !== JSON_ERROR_NONE || empty($decoded)) {
            return false;
        }
        $tokenKey = $item->auth->accessTokenKeyFromResult ?? 'accessToken';

        if (empty($decoded[$tokenKey])) {
            return false;
        }
        $token = $decoded[$tokenKey];
        $refreshKey = $item->auth->refreshTokenKeyFromResult ?? 'refreshToken';

        if (!empty($decoded[$refreshKey])) {

            // Encrypt the token if it is not already encrypted.
            $encrypted_bearer_refresh_token = $this->utils->is_encrypted($decoded[$refreshKey]) ? $decoded[$refreshKey] : $this->utils->encrypt($decoded[$refreshKey]);

            $item->auth->bearerRefreshToken = $encrypted_bearer_refresh_token;
        }

        return $token;
    }

    /**
     * Parse a CSV string into an array.
     *
     * @param string $csvString
     * @return array
     */
    private function parseCSVFromString($csvString)
    {
        // If the CSV string is empty or contains only whitespace, return an empty JSON array.
        if (trim($csvString) === '') {
            return [];
        }

        // Open a temporary memory stream to emulate a file.
        $handle = fopen('php://temp', 'r+');
        if ($handle === false) {
            // Return an empty JSON array if the stream cannot be opened.
            return [];
        }

        // Write the CSV string into the memory stream and rewind it to the beginning.
        fwrite($handle, $csvString);
        rewind($handle);

        // Read the first line as headers.
        $headers = fgetcsv($handle);
        if ($headers === false) {
            fclose($handle);
            return [];
        }

        $data = [];
        // Loop through the rest of the CSV rows.
        while (($row = fgetcsv($handle)) !== false) {
            // Skip rows that are empty (e.g., a single empty string).
            if (count($row) === 1 && trim($row[0]) === '') {
                continue;
            }
            // Ensure the row has the same number of elements as headers.
            if (count($row) === count($headers)) {
                $data[] = array_combine($headers, $row);
            }
            // Optionally, you can handle rows with mismatched column counts here.
        }

        fclose($handle);

        // Return the JSON-encoded data.
        return $data;
    }

    /**
     * Parse an XML string into a PHP array.
     *
     * This function parses the XML using DOMDocument and then converts it
     * recursively into a PHP array. It stores element attributes under the
     * key "attributes" and returns text directly when an element's children
     * consist solely of text.
     *
     * @param string $xmlString
     * @return array
     */
    private function parseXMLFromString($xmlString)
    {
        libxml_use_internal_errors(true);
        $dom = new \DOMDocument();
        if (!$dom->loadXML($xmlString)) {
            $errors = libxml_get_errors();
            libxml_clear_errors();
            error_log("Error parsing XML: " . print_r($errors, true));
            return [];
        }
        $root = $dom->documentElement;
        return $this->domNodeToArray($root);
    }

    /**
     * Recursively converts a DOMNode into a PHP array.
     *
     * - Element attributes are stored under the "attributes" key.
     * - If all child nodes are text (or CDATA), their combined text is returned.
     * - Otherwise, child elements are processed recursively. If multiple children
     *   share the same node name, they are stored in an indexed array.
     *
     * @param \DOMNode $node
     * @return mixed The resulting array, string, or null.
     */
    private function domNodeToArray(\DOMNode $node)
    {
        // Handle text or CDATA nodes: return the trimmed text.
        if ($node->nodeType === XML_TEXT_NODE || $node->nodeType === XML_CDATA_SECTION_NODE) {
            $text = trim($node->nodeValue);
            return $text === "" ? null : $text;
        }

        $result = [];

        // Process element node attributes.
        if ($node->nodeType === XML_ELEMENT_NODE && $node->hasAttributes()) {
            $attrs = [];
            foreach ($node->attributes as $attr) {
                $attrs[$attr->nodeName] = $attr->nodeValue;
            }
            if (!empty($attrs)) {
                $result['attributes'] = $attrs;
            }
        }

        // Process child nodes.
        if ($node->hasChildNodes()) {
            $childNodes = $node->childNodes;
            $allText = true;
            $textContent = "";
            // First, check if all children are text or CDATA.
            foreach ($childNodes as $child) {
                if ($child->nodeType === XML_TEXT_NODE || $child->nodeType === XML_CDATA_SECTION_NODE) {
                    $trimmed = trim($child->nodeValue);
                    if ($trimmed !== "") {
                        $textContent .= $trimmed . " ";
                    }
                } else {
                    $allText = false;
                }
            }
            if ($allText && trim($textContent) !== "") {
                return trim($textContent);
            }
            // Otherwise, process each child node.
            foreach ($childNodes as $child) {
                // Skip empty text nodes.
                if (($child->nodeType === XML_TEXT_NODE || $child->nodeType === XML_CDATA_SECTION_NODE) && trim($child->nodeValue) === "") {
                    continue;
                }
                // If mixed content, we ignore raw text nodes.
                if ($child->nodeType === XML_TEXT_NODE || $child->nodeType === XML_CDATA_SECTION_NODE) {
                    continue;
                }
                $childArray = $this->domNodeToArray($child);
                $childName = $child->nodeName;
                if (isset($result[$childName])) {
                    if (!is_array($result[$childName]) || (is_array($result[$childName]) && !isset($result[$childName][0]))) {
                        $result[$childName] = [$result[$childName]];
                    }
                    $result[$childName][] = $childArray;
                } else {
                    $result[$childName] = $childArray;
                }
            }
        }
        return $result;
    }
    /**
     * Refresh a dynamic bearer token using the refresh endpoint.
     *
     * @param object $item
     * @return string|false The new token string on success, or false on failure.
     */
    private function refresh_dynamic_token($item)
    {
        $bearer_refresh_token = $item->auth->bearerRefreshToken ?? "";

        // Encrypt the token if it is not already encrypted.
        if ($this->utils->is_encrypted($bearer_refresh_token)) {
            $bearer_refresh_token = $this->utils->decrypt($bearer_refresh_token);
        }

        if (
            empty($item->auth) || empty($item->auth->refreshTokenEndpoint) || empty($bearer_refresh_token)
            || !filter_var($item->auth->refreshTokenEndpoint, FILTER_VALIDATE_URL)
        ) {
            return false;
        }

        $args = [
            'body' => [
                ($item->auth->refreshTokenKeyFromResult ?? 'refreshToken') => sanitize_text_field($bearer_refresh_token),
                //'expiresInMins' => 1 // Todo: Development only
            ]
        ];

        // Apply filters for request headers and body
        $args['headers'] = apply_filters('bricksforge/api_query_builder/refresh_token_request_headers', $args['headers'] ?? [], $item);
        $args['body'] = apply_filters('bricksforge/api_query_builder/refresh_token_request_body', $args['body'] ?? [], $item);

        $refreshResponse = wp_remote_post(esc_url_raw($item->auth->refreshTokenEndpoint), $args);
        if (is_wp_error($refreshResponse)) {
            return false;
        }
        $responseBody = wp_remote_retrieve_body($refreshResponse);
        $decoded = json_decode($responseBody, true);
        if (json_last_error() !== JSON_ERROR_NONE || empty($decoded)) {
            return false;
        }
        $tokenKey = $item->auth->accessTokenKeyFromResult ?? 'accessToken';
        if (empty($decoded[$tokenKey])) {
            return false;
        }
        $newToken = $decoded[$tokenKey];
        $refreshKey = $item->auth->refreshTokenKeyFromResult ?? 'refreshToken';
        if (!empty($decoded[$refreshKey])) {

            // Encrypt the token if it is not already encrypted.
            $encrypted_bearer_refresh_token = $this->utils->is_encrypted($decoded[$refreshKey]) ? $decoded[$refreshKey] : $this->utils->encrypt($decoded[$refreshKey]);

            $item->auth->bearerRefreshToken = $encrypted_bearer_refresh_token;
        }
        return $newToken;
    }

    /**
     * Handle dynamic routes.
     *
     * @param object $item
     * @return string
     */
    public function handle_dynamic_routes($item)
    {
        // Check if the item has a valid URL.
        if (!isset($item->url) || !is_string($item->url) || empty($item->url)) {
            return '';
        }

        $url = $item->url;

        // If the item url contains "sheets.googleapis.com", we stop here and return the url.
        if ($this->is_google_sheet($url)) {
            return $url;
        }

        // If there is no dynamic route, return the URL.
        $contains_dynamic_routes = $this->contains_dynamic_routes($url);
        if (!$contains_dynamic_routes) {
            return $url;
        }

        $is_bricksforge_settings_page = isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], '/wp-json/bricksforge/v1/fetch_api') !== false;

        // Replace placeholders (e.g., :id) with corresponding GET parameter values.
        // If a parameter is missing, log a warning and replace it with an empty string.
        $url = preg_replace_callback('/:(\w+)/', function ($matches) use ($item, $is_bricksforge_settings_page) {
            $paramName = sanitize_text_field($matches[1]);

            if (isset($_GET[$paramName])) {
                // Use urlencode to safely inject the parameter into the URL.
                return urlencode(sanitize_text_field($_GET[$paramName]));
            } else {
                if (bricks_is_builder() || bricks_is_builder_call() || $is_bricksforge_settings_page) {
                    $fallbacks = isset($item->dynamicRoutesFallbacks) ? $item->dynamicRoutesFallbacks : [];
                    foreach ($fallbacks as $fallback) {
                        $route_without_prefix = str_replace(':', '', sanitize_text_field($fallback->route));
                        if ($route_without_prefix === $paramName) {
                            if (empty($fallback->value)) {
                                return sanitize_text_field($fallback->route);
                            }

                            return sanitize_text_field($fallback->value);
                        }
                    }
                }
            }
        }, $url);

        // Validate the final URL. This doesn't block execution, but logs a warning if the URL is malformed.
        if (filter_var($url, FILTER_VALIDATE_URL) === false) {
            error_log("Bricksforge: The processed URL ('{$url}') is not a valid URL.");
        }

        return $url;
    }

    /**
     * Handle dynamic data tags.
     *
     * @param object $item
     * @return string
     */
    public function handle_dynamic_data_tags($api_url)
    {
        // Validate that $item->url exists and is a string.
        if (empty($api_url) || !is_string($api_url)) {
            return '';
        }

        // Cache replacement values for repeated tags.
        $cache = [];

        // Use preg_replace_callback to replace dynamic data tags of the form {tag_name}.
        // This regex matches any characters except other curly braces.
        $url = preg_replace_callback('/\{([^\{\}]+)\}/', function ($matches) use (&$cache) {
            $fullTag = $matches[0]; // The matched tag (with curly braces)

            // Return previously computed value if available.
            if (array_key_exists($fullTag, $cache)) {
                return $cache[$fullTag];
            }

            // Ensure the helper function exists.
            if (!function_exists('bricks_render_dynamic_data')) {
                $cache[$fullTag] = $fullTag;
                return $fullTag;
            }

            // Fetch the dynamic value.
            $value = bricks_render_dynamic_data($fullTag);

            // Convert null/false responses to an empty string.
            if ($value === null || $value === false) {
                $value = '';
            }

            $cache[$fullTag] = $value;

            return $value;
        }, $api_url);

        // Optionally update the item's URL directly.
        $api_url = $url;

        return $api_url;
    }

    /**
     * Check if the URL contains dynamic routes.
     *
     * @param string $url
     * @return bool
     */
    private function contains_dynamic_routes($url)
    {
        if ($this->is_google_sheet($url)) {
            return false;
        }

        $regex = '/:(\w+)/';
        return preg_match($regex, $url) > 0;
    }

    /**
     * Get the start position from the root path.
     *
     * @param array $response
     * @param string $root_path
     * @return array
     */
    public function get_start_position_from_root_path($response, $root_path)
    {

        // If the root path includes dynamic data tags, we need to replace them with the actual values.
        $regex_dynamic_data_tags = '/\{([^\{\}]+)\}/';
        if (preg_match($regex_dynamic_data_tags, $root_path)) {
            // For every tag, we use bricks_render_dynamic_data to get the value.
            $root_path = preg_replace_callback($regex_dynamic_data_tags, function ($matches) {
                return bricks_render_dynamic_data($matches[0]);
            }, $root_path);
        }

        $root_path_parts = preg_split('/[\/\.]+/', sanitize_text_field($root_path));

        $current_level = $response;

        foreach ($root_path_parts as $part) {
            if (isset($current_level[$part])) {
                $current_level = $current_level[$part];
            } else {
                return [];
            }
        }

        return $current_level;
    }

    /**
     * Get a single item by item ID.
     *
     * @param mixed $item_id
     * @return mixed|null
     */
    public function get_item($item_id)
    {
        // If the item ID contains ::, we split to get the real item id
        if (strpos($item_id, '::') !== false) {
            $item_id = explode('::', $item_id)[0];
        }

        if (empty($this->items)) {
            return null;
        }

        foreach ($this->items as $item) {
            // Handle both array and object formats
            $current_id = $this->get_item_property($item, 'id', null);

            if ($current_id === sanitize_text_field($item_id)) {
                // Convert array to object for consistency with rest of codebase
                if (is_array($item)) {
                    return (object) $item;
                }
                return $item;
            }
        }

        return null;
    }

    /**
     * Add dynamic tags to the list using the API responses.
     *
     * @param array $tags
     * @return array
     */
    public function add_dynamic_tags_list($tags)
    {
        if (empty($this->active_items)) {
            return $tags;
        }

        // Loop through each active API item.
        foreach ($this->active_items as $item) {
            // Check if dynamic data tags are enabled.
            if (!isset($item->createDynamicDataTags) || !$item->createDynamicDataTags) {
                continue;
            }

            // Use centralized cached response.
            $response = $this->get_cached_response($item->id);

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

            // Extract keys from the API response.
            $keys = $this->get_keys_from_response($response);

            // Remove keys that end with an underscore followed by digits (e.g. "tags_0", "reviews_0_rating")
            $keys = array_filter($keys, function ($key) {
                $segments = explode('_', $key);
                foreach ($segments as $segment) {
                    if (ctype_digit($segment)) {
                        return false;
                    }
                }
                return true;
            });

            // Create a prefix for dynamic tags (lowercase, underscores instead of spaces).
            $prefix = $this->to_slug($item->label);

            $selected_nested_arrays = isset($item->selectedNestedArrays) && is_array($item->selectedNestedArrays) ? $item->selectedNestedArrays : [];
            $simple_array_keys = [];

            // First identify what simple arrays will be in nested groups
            if (!empty($selected_nested_arrays)) {
                $nested_arrays = $this->get_nested_arrays_from_json($response);

                // Collect keys that contain simple arrays
                foreach ($nested_arrays as $key => $value) {
                    if (is_array($value) && !empty($value)) {
                        $first_item = is_array(reset($value)) ? reset($value) : $value;

                        if (is_array($first_item)) {
                            foreach ($first_item as $nested_key => $nested_value) {
                                if (is_numeric($nested_key)) {
                                    $simple_array_keys[] = $key;
                                    break;
                                }
                            }
                        }
                    }
                }
            }

            foreach ($keys as $key) {
                // Skip this key if it's in the simple_array_keys list
                if (in_array($key, $simple_array_keys)) {
                    continue;
                }

                $tags[] = [
                    'name'  => '{brf_api_' . sanitize_text_field($prefix) . '_' . sanitize_text_field($key) . '}',
                    'label' => sanitize_text_field($key),
                    'group' => sanitize_text_field($item->label),
                ];
            }

            if (!empty($selected_nested_arrays)) {
                // We want to consider nested keys as well.
                $nested_arrays = $this->get_nested_arrays_from_json($response);

                $keys_from_nested_arrays = $this->get_keys_from_nested_arrays($nested_arrays);
                $keys_from_nested_arrays = $this->filter_redundant_keys($keys_from_nested_arrays);

                foreach ($keys_from_nested_arrays as $key) {
                    // Only process this nested array if it's in the selected list
                    if (!in_array($key, $selected_nested_arrays)) {
                        continue;
                    }
                    // For each key of the nested arrays, we add a tag.
                    // Example:
                    // Group: Products - Reviews
                    // Tags: name: {brf_api_api_products_reviews__rating}, label: rating, group: Products - Reviews

                    if (isset($nested_arrays[$key])) {
                        // Use the key intact if available.
                        $data_from_key = $nested_arrays[$key];
                    } else {
                        // Fallback: split the composite key by underscores.
                        $key_parts = explode('_', $key);
                        $data_from_key = $nested_arrays;
                        foreach ($key_parts as $part) {
                            $data_from_key = $data_from_key[$part] ?? [];
                        }
                    }

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

                    // Get first item to extract structure
                    if (is_array($data_from_key)) {
                        if (array_keys($data_from_key) === range(0, count($data_from_key) - 1)) {
                            // Handle simple arrays like "tags"
                            $first_item = $data_from_key;
                        } else {
                            // Handle associative arrays or objects
                            $first_item = reset($data_from_key);
                        }
                    } else {
                        $first_item = $data_from_key;
                    }

                    // Ensure that $first_item is processed correctly for nested arrays
                    if (is_array($first_item) && !empty($first_item) && is_array(reset($first_item))) {
                        // If the first item is an array and contains arrays, process it as a nested array
                        $first_item = reset($first_item);
                    }

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

                    // Create tags based on the structure of the first item
                    $existing_tag_names = [];
                    foreach ($first_item as $nested_data_key => $nested_data_value) {
                        // If the key is numeric (a simple array), use "item" as the placeholder.
                        if (is_numeric($nested_data_key)) {
                            $placeholder = $this->simple_array_placeholder;
                        } else {
                            $placeholder = $this->to_slug($nested_data_key);
                        }

                        // Build the tag name
                        $name = '{brf_api_' . sanitize_text_field($prefix) . '_' . sanitize_text_field($this->to_slug($key)) . '__' . sanitize_text_field($placeholder) . '}';

                        // If this tag name is already added, skip it.
                        if (in_array($name, $existing_tag_names)) {
                            continue;
                        }
                        $existing_tag_names[] = $name;

                        $label = sanitize_text_field($placeholder);
                        $group = sanitize_text_field($item->label) . ' - ' . sanitize_text_field($key);

                        $tags[] = $this->create_dynamic_data_tag($name, $label, $group);
                    }
                }
            }
        }

        return $tags;
    }

    public function create_dynamic_data_tag($name, $label, $group)
    {
        return [
            'name'  => $name,
            'label' => $label,
            'group' => $group,
        ];
    }

    public function get_keys_from_nested_arrays($nested_arrays, $prefix = '')
    {
        $keys = [];

        foreach ($nested_arrays as $key => $value) {
            // Skip numeric keys – these are array indexes.
            if (is_numeric($key)) {
                continue;
            }

            // Build the full key path.
            $full_key = $prefix ? $prefix . '_' . $key : $key;

            if (is_array($value)) {
                // Only add this key if the array is a list array (i.e. it has numeric indexes).
                if ($this->is_list_array($value)) {
                    $keys[] = $full_key;
                }
                // Recursively search deeper.
                $nested_keys = $this->get_keys_from_nested_arrays($value, $full_key);
                $keys = array_merge($keys, $nested_keys);
            }
        }

        return array_unique($keys);
    }

    private function filter_redundant_keys(array $keys)
    {
        $filtered = [];
        foreach ($keys as $key) {
            // Check if the key is standalone (contains no underscore)
            if (strpos($key, '_') === false) {
                $isRedundant = false;
                // Look for any other key that ends with '_' followed by this key
                foreach ($keys as $other) {
                    if ($other !== $key && substr($other, -strlen('_' . $key)) === '_' . $key) {
                        $isRedundant = true;
                        break;
                    }
                }
                if (!$isRedundant) {
                    $filtered[] = $key;
                }
            } else {
                // Always keep composite keys.
                $filtered[] = $key;
            }
        }
        return array_unique($filtered);
    }

    private function is_list_array($array)
    {
        // Check that the array keys match 0 .. count-1.
        return is_array($array) && array_keys($array) === range(0, count($array) - 1);
    }

    /**
     * Recursively extract keys from the API response.
     *
     * @param mixed $response
     * @return array
     */
    public function get_keys_from_response($response)
    {
        $keys = [];

        // Helper function to recursively collect leaf keys.
        $get_nested_keys = function ($obj, $prefix = '') use (&$get_nested_keys) {
            $collected_keys = [];

            foreach ((array)$obj as $key => $value) {
                // Normalize the key (e.g. "CPU model" -> "cpu_model")
                $clean_key = $this->to_slug($key);

                // If the value is an array or object, recursively process it.
                if (is_object($value) || is_array($value)) {
                    $nested_keys = $get_nested_keys($value, $prefix . $clean_key . '_');
                    $collected_keys = array_merge($collected_keys, $nested_keys);
                } else {
                    $collected_keys[] = $prefix . $clean_key;
                }
            }

            return $collected_keys;
        };

        if (is_array($response)) {
            foreach ($response as $item) {
                $keys = array_merge($keys, $get_nested_keys($item));
            }
        } else {
            $keys = $get_nested_keys($response);
        }

        return array_unique($keys);
    }

    /**
     * Render dynamic tag content by replacing the tag with the actual API value.
     *
     * @param string $content
     * @param mixed  $post
     * @param string $context
     * @return string
     */
    public function render_tag($content, $post, $context = 'text', $static_json = [])
    {
        $bricks_loop_id = \Bricks\Query::is_any_looping();
        // Sicherstellen, dass für die aktuelle Loop-ID die real_loop_data initialisiert ist
        if (!isset($this->real_loop_data[$bricks_loop_id])) {
            $this->real_loop_data[$bricks_loop_id] = ["values" => []];
        }

        if (strpos($content, '{brf_api_') === false && strpos($content, '{brf_api_results_count}') === false) {
            return $content;
        }

        // Ersetze dynamische Tags wie {brf_api_...}
        $content = preg_replace_callback(
            '/\{brf_api_([a-zA-Z0-9_]+)\}/',
            function ($matches) use ($post, $context) {
                $tag = sanitize_text_field($matches[1]);
                $response_value = '';
                foreach ($this->active_items as $item) {
                    $slugged_label = $this->to_slug($item->label) . '_';
                    if (strpos($tag, $slugged_label) !== false) {
                        $key = str_replace($slugged_label, '', $tag);

                        // Falls es sich um einen verschachtelten Wert handelt:
                        if (strpos($key, '__') !== false) {
                            $response_value = $this->get_value_from_nested_array($key, $item->id);
                        } else {
                            $response_value = $this->get_value_from_response($key, $item->id);
                        }
                    }
                }
                // Falls der Rückgabewert ein Array ist, in einen String umwandeln
                if (is_array($response_value)) {
                    return implode(", ", $response_value);
                }
                return $response_value;
            },
            $content
        );

        // Ersetze dynamische Tags für das Ergebniszähler-Format {brf_api_results_count:...}
        $content = preg_replace_callback(
            '/\{brf_api_results_count:([a-zA-Z0-9_]+)\}/',
            function ($matches) use ($bricks_loop_id) {
                // Wir suchen in den real_loop_data nach dem Wert der Loop-ID
                if (isset($this->real_loop_data[$bricks_loop_id]) && isset($this->real_loop_data[$bricks_loop_id]["values"])) {
                    return count($this->real_loop_data[$bricks_loop_id]["values"]);
                }
                return 0;
            },
            $content
        );

        return $content;
    }

    public function bricks_render_attributes($attributes, $key, $element)
    {

        // Check if element settings and query exist before accessing properties
        if (!isset($element->settings) || !is_array($element->settings)) {
            return $attributes;
        }

        // Check if query exists and is an array
        if (!isset($element->settings['query']) || !is_array($element->settings['query'])) {
            return $attributes;
        }

        // Check if objectType exists and matches external API loop pattern
        $is_api_loop = isset($element->settings['query']['objectType']) &&
            strpos($element->settings['query']['objectType'], 'brfExternalApiLoop-') !== false;

        if (!$is_api_loop) {
            return $attributes;
        }

        $item_id = str_replace('brfExternalApiLoop-', '', $element->settings['query']['objectType']);

        if (!$item_id) {
            return $attributes;
        }

        $item = $this->get_item($item_id);

        if (!$item) {
            return $attributes;
        }

        if (!isset($item->loadingType)) {
            return $attributes;
        }

        $data_script_id = isset($element->element) && isset($element->element["id"]) ? $element->element["id"] : null;

        $is_loading_all = $item->loadingType === 'all';
        $is_infinite_scroll = $item->loadingType === 'infinite';
        $is_pagination = $item->loadingType === 'pagination';

        // If the loadingType is "all", we just return the attributes as is.
        if ($is_loading_all) {
            // We just add the query id to the attributes.
            $attributes[$key]['data-brf-query-id'] = $data_script_id;

            return $attributes;
        }


        if ($data_script_id) {
            $attributes[$key]['data-brf-query-id'] = $data_script_id;
        }

        // We give the $element the item id as a data attribute.
        $attributes[$key]['data-brf-item-id'] = $item_id;

        if ($is_infinite_scroll || $is_pagination) {
            $attributes[$key]['data-brf-loading-type'] = $item->loadingType;
        }


        return $attributes;
    }

    // TODO: Das unten funktioniert! Das Problem: {query_results_count} ergibt immer das höchste.
    public function bricks_render_html($content, $post, $area)
    {
        // Early return: Skip DOM parsing if no API Query Builder elements exist
        // This improves performance and avoids unintended side effects on other content
        if (strpos($content, 'data-brf-query-id') === false) {
            return $content;
        }

        require_once(BRICKSFORGE_PATH . '/includes/vendor/simple_html_dom.php');

        if (!class_exists('simple_html_dom')) {
            return $content;
        }

        global $bricks_loop_query;

        // Protect <pre> and <code> blocks from DOM manipulation
        // simple_html_dom strips newlines by default, which breaks code formatting
        $protectedBlocks = [];
        $content = preg_replace_callback('/<pre[^>]*>.*?<\/pre>/s', function($match) use (&$protectedBlocks) {
            $placeholder = '<!--BRF_PROTECTED_BLOCK_' . count($protectedBlocks) . '-->';
            $protectedBlocks[] = $match[0];
            return $placeholder;
        }, $content);

        $html = str_get_html($content);
        if (!$html) {
            // Restore protected blocks before returning
            foreach ($protectedBlocks as $i => $block) {
                $content = str_replace('<!--BRF_PROTECTED_BLOCK_' . $i . '-->', $block, $content);
            }
            return $content;
        }

        // Find all elements with data-brf-query-id
        $blocks = $html->find('[data-brf-query-id]');
        $blocks_to_remove = array();

        foreach ($blocks as $block) {
            // Only process elements that directly contain the marker (not their children)
            $directContent = $block->plaintext;

            // Get all child elements with data-brf-query-id
            $children = $block->find('[data-brf-query-id]');

            // Remove children's text from directContent
            foreach ($children as $child) {
                $directContent = str_replace($child->plaintext, '', $directContent);
            }

            // Only mark for removal if:
            // 1. The element's direct content contains the marker
            // 2. The element has no children with data-brf-query-id OR all children are empty
            if (strpos($directContent, $this->not_found_marker) !== false) {
                $hasValidChildren = false;
                foreach ($children as $child) {
                    if (strpos($child->plaintext, $this->not_found_marker) === false && trim($child->plaintext) !== '') {
                        $hasValidChildren = true;
                        break;
                    }
                }

                if (!$hasValidChildren) {
                    $blocks_to_remove[] = $block;
                }
            }
        }

        // Remove the identified blocks
        foreach ($blocks_to_remove as $block) {
            $block->outertext = '';
        }

        $content = $html->save();

        // Restore protected <pre> blocks with their original content (preserving newlines)
        foreach ($protectedBlocks as $i => $block) {
            $content = str_replace('<!--BRF_PROTECTED_BLOCK_' . $i . '-->', $block, $content);
        }

        // Remove any remaining not_found_marker instances from the content
        $content = str_replace($this->not_found_marker, '', $content);

        return $content;
    }

    public function get_parent_loop_index()
    {
        $level = 1;

        $query_id = $this->get_bricks_looping_parent_query_id_by_level($level);

        if (!$query_id) {
            return 0;
        }

        $loop_index = \Bricks\Query::get_loop_index($query_id);

        return $loop_index;
    }

    public function get_parent_loop()
    {
        $level = 1;

        $query_id = $this->get_bricks_looping_parent_query_id_by_level($level);

        if (!$query_id) {
            return [];
        }

        return \Bricks\Query::get_loop_object($query_id);
    }

    // Thanks, Itchy! :)
    // Reference: https://itchycode.com/bricks-builder-useful-functions-and-tips/
    public function get_bricks_looping_parent_query_id_by_level($level = 1)
    {
        global $bricks_loop_query;

        if (empty($bricks_loop_query) || $level < 1) {
            return false;
        }

        $current_query_id = \Bricks\Query::is_any_looping();

        if (!$current_query_id) {
            return false;
        }

        if (!isset($bricks_loop_query[$current_query_id])) {
            return false;
        }

        $query_ids = array_reverse(array_keys($bricks_loop_query));

        if (!isset($query_ids[$level])) {
            return false;
        }

        if ($bricks_loop_query[$query_ids[$level]]->is_looping) {
            return $query_ids[$level];
        }

        return false;
    }

    /**
     * Hilfsmethode zur Aufnahme eines Werts in das real_loop_data für einen bestimmten Loop.
     *
     * @param string $query_object_id
     * @param mixed  $value
     * @param mixed  $item_id
     * @param object $raw_query_object
     * @param bool   $is_nested
     */
    private function add_to_real_loop_data($query_object_id, $value, $item_id, $raw_query_object, $is_nested = false)
    {
        if (!isset($this->real_loop_data[$query_object_id])) {
            $this->real_loop_data[$query_object_id] = [
                "values" => [],
                "loop_id" => $query_object_id,
                "loop_index" => \Bricks\Query::get_loop_index(),
                "parent_loop_index" => $this->get_parent_loop_index() ?? 0,
                "item_id" => $item_id,
                "loop_element_id" => isset($raw_query_object->element_id) ? $raw_query_object->element_id : null,
                "is_nested" => $is_nested,
            ];
        }
        $this->real_loop_data[$query_object_id]["values"][] = $value;
    }

    public function get_value_from_nested_array($key, $item_id)
    {
        global $bricks_loop_query;

        $query_object_id = \Bricks\Query::is_any_looping();
        $query_object = \Bricks\Query::get_loop_object();
        $raw_query_object = $bricks_loop_query[$query_object_id];

        // Split the key on one or more consecutive double underscores.
        $parts = preg_split('/__+/', $key);
        if (empty($parts)) {
            return '';
        }

        // The first part is used to get the cached value.
        $mainKey = array_shift($parts);

        $parent_loop_index = $this->get_parent_loop_index() ?? 0;
        $current_loop_index = \Bricks\Query::get_loop_index();

        $parentData = $this->get_cached_response($item_id);
        $currentParentData = isset($parentData[$parent_loop_index]) ? $parentData[$parent_loop_index] : null;

        // Attempt to retrieve the value by normalized key.
        $value = $this->search_value_by_normalized_key($currentParentData, $mainKey);

        // Falls noch kein Wert gefunden wurde, versuche eine Zerlegung des Schlüssels.
        if ($value === null) {
            $subParts = explode('_', $mainKey);
            $value = $currentParentData;
            foreach ($subParts as $subKey) {
                $value = $this->search_value_by_normalized_key($value, $subKey);
                if ($value === null) {
                    return '';
                }
            }
        }

        // Wenn der Wert ein Array ist, dann nimm den Eintrag zum aktuellen Loop-Index.
        if (is_array($value) && isset($value[$current_loop_index])) {
            $value = $value[$current_loop_index];

            // Mit der neuen Hilfsmethode hinzufügen
            $this->add_to_real_loop_data($query_object_id, $value, $item_id, $raw_query_object, true);
        } else {
            return $this->not_found_marker;
        }

        // Für alle weiteren Teile traversiere den verschachtelten Array.
        foreach ($parts as $segment) {
            if ($segment === $this->simple_array_placeholder) {
                continue;
            }
            if (is_array($value)) {
                $found = $this->search_value_by_normalized_key($value, $segment);
                if ($found === null) {
                    return '';
                }
                $value = $found;
            } else {
                return '';
            }
        }

        return $value;
    }

    /**
     * Render a variable from the static JSON.
     *
     * @param string $variable
     * @param string $item_label
     * @param array  $static_json
     * @return string
     */
    public function render_variable_from_static_json($variable, $item_label, $static_json)
    {
        // $variable: {brf_api_my_loop_my-variable}
        // $item_label: My Loop

        if (empty($static_json)) {
            return '';
        }

        // Extract the tag from the variable format {brf_api_XXX}
        if (preg_match('/\{brf_api_([a-zA-Z0-9_-]+)\}/', $variable, $matches)) {
            $tag = sanitize_text_field($matches[1]);

            // Convert item_label to slug for comparison
            $slugged_label = $this->to_slug($item_label) . '_';

            if (strpos($tag, $slugged_label) !== false) {
                // Extract the key by removing the slugged_label prefix
                $key = str_replace($slugged_label, '', $tag);

                // Handle nested array if key contains __
                if (strpos($key, '__') !== false) {
                    // Split the key by __ to get the parts
                    $parts = preg_split('/__+/', $key);
                    if (!empty($parts)) {
                        $mainKey = array_shift($parts);
                        $nestedKey = !empty($parts) ? $parts[0] : null;

                        if (isset($static_json[$mainKey]) && $nestedKey && isset($static_json[$mainKey][$nestedKey])) {
                            return $static_json[$mainKey][$nestedKey];
                        }
                    }
                } else {
                    // Direct key access
                    if (isset($static_json[$key])) {
                        return $static_json[$key];
                    }
                }
            }
        }

        return '';
    }

    public function get_value_from_response($key, $item_id)
    {
        global $bricks_loop_query;

        $looped_object = \Bricks\Query::get_loop_object();

        // Wir ermitteln die aktuelle Loop-ID
        $query_object_id = \Bricks\Query::is_any_looping();

        $raw_query_object = $bricks_loop_query[$query_object_id];

        // Wir ermitteln den Loop-Index
        $loop_index = \Bricks\Query::get_loop_index();

        if (empty($looped_object)) {
            return '';
        }

        $value = $this->search_value_by_normalized_key($looped_object, sanitize_text_field($key));
        if ($value !== null) {

            $this->add_to_real_loop_data($query_object_id, $value, $item_id, $raw_query_object, false);

            return $value;
        }

        if (strpos($key, '_') !== false) {
            $parts = array_map('sanitize_text_field', explode('_', $key));
            return $this->search_nested_value($looped_object, $parts);
        }

        return '';
    }

    /**
     * Searches for a key in an array or object, whose normalized version exactly matches $key.
     *
     * @param mixed  $data
     * @param string $key The already normalized key
     * @return mixed|null
     */
    private function search_value_by_normalized_key($data, $key)
    {
        if (is_array($data)) {
            foreach ($data as $k => $v) {
                if ($this->to_slug($k) === $key) {
                    return $v;
                }
                // Recursively search in nested arrays
                if (is_array($v) || is_object($v)) {
                    $result = $this->search_value_by_normalized_key($v, $key);
                    if ($result !== null) {
                        return $result;
                    }
                }
            }
        } elseif (is_object($data)) {
            foreach (get_object_vars($data) as $k => $v) {
                if ($this->to_slug($k) === $key) {
                    return $v;
                }
                // Recursively search in nested objects
                if (is_array($v) || is_object($v)) {
                    $result = $this->search_value_by_normalized_key($v, $key);
                    if ($result !== null) {
                        return $result;
                    }
                }
            }
        }

        return null;
    }

    /**
     * Recursive search for a nested value by the parts of the key.
     *
     * @param mixed $data
     * @param array $parts Array of key parts
     * @return mixed
     */
    private function search_nested_value($data, $parts)
    {
        if (empty($parts)) {
            return $data;
        }

        // Versuche einen direkten Lookup, indem alle Teile zu einem Schlüssel zusammengefügt werden.
        // Beispiel: ["feels", "like"] => "feels_like"
        $mergedKey = implode('_', array_map('sanitize_text_field', $parts));
        $direct = $this->search_value_by_normalized_key($data, $mergedKey);
        if ($direct !== null) {
            return $direct;
        }

        // Falls kein direkter Treffer vorliegt, fahre rekursiv fort.
        $currentPart = sanitize_text_field(array_shift($parts));

        if (is_array($data)) {
            foreach ($data as $k => $v) {
                if ($this->to_slug($k) === $currentPart) {
                    $result = $this->search_nested_value($v, $parts);
                    if ($result !== '') {
                        return $result;
                    }
                }
            }
        } elseif (is_object($data)) {
            foreach (get_object_vars($data) as $k => $v) {
                if ($this->to_slug($k) === $currentPart) {
                    $result = $this->search_nested_value($v, $parts);
                    if ($result !== '') {
                        return $result;
                    }
                }
            }
        }

        return '';
    }

    public function to_slug($string)
    {
        $string = strtolower($string);
        $string = preg_replace('/[-\s]+/', ' ', $string);
        $string = trim($string);
        $string = str_replace(' ', '_', $string);

        return $string;
    }

    public function to_human_readable($string)
    {
        $string = str_replace('_', ' ', $string);
        $string = ucwords($string);
        return $string;
    }

    private function remove_unneeded_elements()
    {
        // Would be only a workaround
    }

    /**
     * Centralized method to load API responses for all active items.
     *
     * This ensures that the API requests are made only once per page load.
     * // Important: Deactivated as this would create requests in every page.
     */
    public function load_all_api_responses()
    {
        if (empty($this->active_items)) {
            return;
        }

        foreach ($this->active_items as $item) {
            $this->run_api_request($item->id);
        }
    }
}
